Skip to content

feat(chat): sent images, line diffs, compaction marker, h4-h6 - #73

Merged
devswha merged 5 commits into
mainfrom
feat/chat-media
Sep 26, 2026
Merged

devswha merged 5 commits into
mainfrom
feat/chat-media

Conversation

@devswha

@devswha devswha commented Sep 26, 2026

Copy link
Copy Markdown
Owner

Stacked on #72 (← #71 ← #70). Merge in order #70 → #71 → #72 → this.

More from comparing our chat with chatmux's (behavior only).

Images the user sent

  • Pasted into Claude's prompt: the transcript keeps these as base64 in the user entry.
    • The parser emits { kind: "image", media_type, ref: "<entry uuid>:<block index>" } with no data. Only png/jpeg/gif/webp are included; svg is excluded.
    • GET /api/pane/conversation/image?pane_id&ref resolves the pane's transcript and decodes that one block. It responds with cache-control: private, max-age=86400, immutable and nosniff.
    • So the 2 s conversation poll never carries megabytes, and the browser fetches each image once.
    • The route sits under /api/pane/, so it relays to remote PCs like the rest.
  • Attached through this app: these show up in the text as @…/.herdr-web-ui/paste-….png mentions and are shown through the existing /api/fs/file.
  • Display: thumbnails sit above the user bubble, two per row on a phone. A tap opens them (viewer for mentions, new tab for pasted ones). An image-only message has no empty bubble or copy button.

Edit as a line diff

src/lib/diff.ts is an LCS line diff. Unchanged lines show once, and removed/added lines appear in place with - /+ . Past 250k line pairs it falls back to removed-then-added. An empty side counts as no lines. MultiEdit's edits[] each get their own diff.

Compaction marker

Claude's isCompactSummary entry was dropped. It is now a divider, "Conversation compacted · time", which opens to the summary. One existing test asserted the drop and was updated deliberately.

Markdown

####–###### render as h4–h6. Below h3 the size stays the same; weight and then a dim colour tell the levels apart.

Tests

  • bun run typecheck; bun test 421/421; test:ui 9/9.
  • New tests:
    • Image parts carry refs, not data, and skip svg.
    • Compaction turn.
    • transcriptImage decodes the named block and returns null for a text block, svg, another entry, a traversal-looking ref, or a missing file.
    • lineDiff: placement, insertion/removal, and the size fallback.
  • A live end-to-end check against the test herdr was not possible: pane.report_agent does not set agent_session there. The endpoint was checked to 404 a bad ref; the lookup itself is covered by the transcriptImage unit test.
  • Screenshotted at 390 px (Korean) with mocked data: two thumbnails side by side, compaction divider opened, Edit diff, #### heading; no overflow.

Not done

Codex input_image and omp image parts are not shown yet; this covers Claude and our own attachments.

🤖 Generated with Claude Code

devswha and others added 5 commits September 27, 2026 00:23
One-tap messages (continue, yes, no, commit and push, retry to start with),
as collie's Quick dock has. A reply goes through the composer's own send
path, so it is queued while the agent works and taken as the answer when a
question card is open; a draft in the box is left alone.

The list lives in each device's settings: quick replies are a phone thing
(a PC has a keyboard), so nothing is stored on the server. Settings ->
Quick replies edits it (up to 12, 200 characters each; blank rows are kept
while being typed and never shown), and a button beside the paperclip
hides or shows the row, remembered per device.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The row takes a line above the message box on every pane; it now waits for
the ⚡ button, and the choice is remembered per device as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Transcripts already record what each request filled:
- Codex: token_count events with last_token_usage and model_context_window
- Claude Code: assistant usage, input + cache creation + cache read,
  subagent (sidechain) entries skipped
- omp/omo/gjc: message usage, input + cacheRead + cacheWrite

ConversationMetadata gains an optional `context { used, window }`. The
window is only what the transcript leaves no doubt about: Codex states it,
and a Claude request past 200k can only have run in the 1M window (kept
through a compaction). Otherwise the line shows the tokens used, never a
guessed percentage.

The composer shows `74% left` or `68k used`, red at 20% or less, with the
counts on hover. PaneTerminal's metadata dedupe compared only model and
effort, so it now compares the context too, or the number would never move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, a loading state

From comparing the chat with chatmux's:

- A failed tool call looked like a success inside "Worked for…". Tool parts
  gain `error`: Claude's tool_result is_error and omp's toolResult isError,
  and for Codex, which records no flag, the output's own verdict
  (exit code, "Script failed", "apply_patch verification failed"; a
  completed script is judged as a whole). The row reads "failed" in red,
  its output is headed Error, and the block header counts failures.
- A file path in a tool row opens the FileViewer, like paths in prose.
- Codex's apply_patch, bare or as the string an exec script passes to
  tools.apply_patch, renders as a per-file diff and its row is summed up
  by the files it touches (shared/patch.ts, used by server and client).
- The jump-to-latest button showed only for new messages; it now shows
  whenever the reader is scrolled up (a round arrow), and stays the "New
  messages" pill when there are some.
- The chat said "No conversation yet" until the first answer came back;
  it now says it is loading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
More from comparing with chatmux's chat:

- Images the user sent: Claude transcripts keep pasted images as base64
  in the user entry. The parser now emits an image part naming it
  (`<entry uuid>:<block index>`, png/jpeg/gif/webp only) instead of the
  data, and GET /api/pane/conversation/image decodes it on request
  (immutable cache), so a 2 s poll never carries megabytes. Images this
  app attaches appear as `@…png` mentions and show through /api/fs/file.
  Thumbnails sit above the user bubble; a tap opens them.
- Edit/MultiEdit render as one line diff (src/lib/diff.ts, an LCS that
  falls back to removed-then-added past 250k line pairs).
- A compaction's summary, which was dropped, is a divider that opens to
  it.
- Markdown headings #### to ###### render as h4-h6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant