zot/internal
patriceckhart bb50aa3044 feat(tui): context diffs + framed tool blocks + paced streaming
Overhauls how tool calls render in the chat so the transcript
reads as a sequence of self-contained action blocks with inline
diffs instead of nested result boxes full of file contents. Plus
a few polish items around markdown rendering, code fences, and
streaming output.

Tool-call framing

  Every tool call (read, write, edit, bash) is now rendered as
  a block bracketed by full-width muted horizontal rules. Inside
  the block: the "tool name path" header, then the body (file
  content, diff, or shell output). No more nested "result"
  sub-header or rules-within-rules around the body. Works for
  both the live streaming overlay (during a turn) and the
  finalised transcript (after the turn ends).

  Duplicate suppression: while a turn is in flight, the
  transcript often already contains an assistant ToolCallBlock
  OR a tool-role ToolResultBlock for the same call the live
  overlay is still tracking. Without a skip check both copies
  render at the same time, producing visual doubling and a
  flicker. Build() now collects every finalised tool id from
  the transcript (matching on either ToolCallBlock.ID or
  ToolResultBlock.CallID) and the live overlay skips any live
  entry whose id is already in that set.

  Streaming state also clears the live toolCalls map on
  EvAssistantStart so a completed round's live entries can't
  carry over into the next turn's overlay.

Context diffs for the edit tool

  The edit tool used to emit a full-file unified diff with a
  "--- path / +++ path" header and every unchanged line prefixed
  with a space. For a small edit in a thousand-line file that's
  a transcript wall. The generator now keeps only
  diffContextLines (=3) unchanged lines on each side of every
  +/- row and collapses longer runs of unchanged content into
  a single "..." marker row. The legacy header is dropped: the
  surrounding tool-call header already shows the path, and the
  "applied N edit(s) to X" prose prefix is dropped for the same
  reason (the diff speaks for itself; the edit count lives in
  Details for json/rpc consumers).

  View-side: a new looksLikeUnifiedDiff detects the stripped
  format (rows start with +/-/space, with at least one +/-) and
  routes through a new renderUnifiedDiff helper that draws each
  row with a combined sign+number gutter ("+123", "-123",
  " 123") in the add / remove / muted colours. The "..." marker
  renders as a horizontal-ellipsis in muted type. Unchanged
  context code stays muted so the eye lands on the changes.

  renderDiffRow was rewritten to share the single gutter format
  between all three row types and to fall back to a muted code
  colour for the unchanged rows so context reads as background.

System-prompt nudge

  Added a short line to the default identity telling the model
  to prefer the edit tool for in-place mutations and the write
  tool for creating or fully replacing files, and to avoid
  using bash + redirect tricks (cat >> foo, echo >> foo, sed
  -i, tee) to mutate files. Those bash approaches render as
  opaque shell output whereas edit renders as a readable diff.

Markdown cleanup

  Code fences in assistant prose no longer get horizontal rules
  around them. Syntax highlighting + the accent colour of
  un-lang'd fences already signal "this is code"; a rule around
  a one-line rm -rf is pure noise and on ultra-wide terminals
  produces an edge-to-edge stroke that dwarfs the snippet it
  wraps.

  Partial-fence handling: if the model's output is truncated
  mid-fence (rare, but happens on aborted streams), the
  buffered content now flushes at end of input instead of
  disappearing.

Streaming-overlay guards

  - Empty streaming blocks (streamOn=true, Streaming="") no
    longer render their "zot" bar. Used to appear as a stray
    empty message bubble above the tool overlay on turns whose
    first content was a tool_use, not text.

  - The live-streaming toolCalls overlay is kept in sync with
    the transcript's finalised entries (described above) so the
    hand-off from "streaming preview" to "finalised in
    transcript" happens without a doubled frame.

renderToolCall split

  The function now has two shapes:

    - streaming (Streaming=true, no Result): render only the
      header and the live body. The live body is already framed
      by wrapLiveBody's own top+bottom rules; adding more would
      produce four-lines-per-block and a visible extra rule at
      the bottom while the user watches the tool run.

    - finished (Result present): opening rule, header, body,
      closing rule. Matches the transcript-side framing in
      renderMessage exactly.

toolBlockRule helper

  Single source for the muted horizontal separator used for
  tool blocks. Spans the full content width; clamps at a
  minimum of 8 cells so dialogs can still call Build on
  absurdly narrow widths without panicking.

refreshToolPaths unchanged

  Kept as-is; the earlier attempt to thread tool-names and raw
  args through it was reverted because the eventual renderer
  didn't need them.

Tested manually with mixed read/write/edit/bash sequences on
both api-key and oauth-subscription anthropic paths. Typewriter
streaming (from the earlier pacer patch) still works; tool
blocks render cleanly once and don't flicker during the stream.
2026-04-20 15:50:39 +02:00
..
agent feat(tui): context diffs + framed tool blocks + paced streaming 2026-04-20 15:50:39 +02:00
assets assets: refresh zot logo to cleaner pixel-art Z 2026-04-20 12:01:43 +02:00
auth feat(auth,tui): dark login pages + /logout picker 2026-04-19 20:14:22 +02:00
core feat(session): /session fork + /session tree 2026-04-20 11:10:56 +02:00
extproto feat(ext): phase 4 - full-event interception, arg rewrites, /reload-ext 2026-04-19 17:02:04 +02:00
provider perf(anthropic): fix cost double-count, tighten caching, correct catalog 2026-04-19 18:57:18 +02:00
skills perf(prompt): cut system prompt to the bone (410 -> 54 tokens) 2026-04-19 17:39:38 +02:00
tui feat(tui): context diffs + framed tool blocks + paced streaming 2026-04-20 15:50:39 +02:00