feat(app): Claude's question card takes screenshots with its answer

ST
2d ago
+779-55

Summary

Claude's question card takes screenshots. Paste one into a question's Something else…, or drop files on the card, and they go with the answer for Claude to look at. Files already in the reply box, or dropped there while the question waits, move to the card. They leave the reply box once the answer goes, so they aren't sent again.

 <ConversationPane>
+  <ChatFiles target={box, session}>      # one useAttachments, shared; progress redraws only its readers
     <QuestionsContext>
       <QuestionCard> → <Form>
+        take() while drawn                # the reply box hands it its files
+        owner: file → question on screen   # kept per question tool
+        chips · paste into own words · drop on card · Submit waits for the files it sends
     <ChatFoot> → <Reply>
-      useAttachments(attach)
+      useChatFiles()                      # chips hidden while a form takes them

An answer stays one line of text. No box API change.

this one is off [attached: /…/My Projects/shop/.berth/attachments/…-checkout-step-2.png, /…/pasted-163707.png]

Paths are parted by , and the next path's /, so a path with a space stays one. Not quotes: on Claude's SDK the box reads the answer back from Claude's "question"="answer" result text, which doesn't escape quotes inside an answer.

attachedTo / splitAttached sit next to withAttachments in lib/attachments.ts. The answered card shows the files by name (📎 checkout-step-2.png, pasted-…png), not by path, without the box's time and same-second count (20261025-152924-1-shot.png → shot.png).

Box, terminal driver: Claude Code breaks a path that is longer than a line inside it (…/acme-shop/.b ⏎ erth/attachments/…). The screen check used to join wrapped lines with a space, so in an 80x24 pane the answer stuck. Now answer.go treats a line break as either a space or nothing, and only there; a space anywhere else still counts.

Verify skill: features/attachments.md is new (the composer and the card). features/chat-questions.md gets the card case. New scenarios chat-question-images (mock) and chat-question-images-live (real box, either driver). dev-box.sh gets two fixes:

  • The caller's BERTH_* no longer reaches the box's agents. Started from a structured worker, BERTH_DRIVER=structured turned their hooks off.
  • It waits for Claude's trust question or its prompt (up to 90 s), answers the question once, and stops with Claude's screen when neither shows. A folder already trusted no longer gets Up+Enter, which re-sent the last prompt.
  • env quotes a box dir with a space (VERIFY_BOX_DIR='/tmp/verify berthd').

Evidence

After the rebase onto main and the review fixes (head 2ef40c56): chat-question-images-live on a dev box built from this branch, with VERIFY_BOX_DIR='/tmp/vb 2541', so every attached path has a space in it. The dropped file is named checkout step 2.png (the box makes it checkout-step-2.png). Both drivers pass, with no console errors. The scenario now also checks the answer again after Claude's record replaces the one sent.

  • Terminal driver, pane at 80x24. Transcript answer: this one is off [attached: /tmp/vb 2541/acme-shop/.berth/attachments/20261027-134208-checkout-step-2.png, /tmp/vb 2541/acme-shop/.berth/attachments/20261027-134209-pasted-134209.png], then Read of both.
  • Structured driver. Transcript answer: this one is off [attached: /tmp/vb 2541/acme-web/.berth/attachments/20261027-134246-checkout-step-2.png, /tmp/vb 2541/acme-web/.berth/attachments/20261027-134248-pasted-134248.png], then Read of both.
  • Before, a first try quoted each path ("…"). The terminal driver passed, but on the structured driver the answered card showed this one is off [attached: once Claude's record came back, cut at the first quote. That's why the format uses commas.
Card, 80x24 terminalAnsweredClaude read them (terminal)Claude read them (structured)

dev-box.sh's trust wait, driven with a stubbed tmux: trust question → answered once → prompt; already trusted → straight on; neither → exit 1 with the screen; capture-pane failing → exit 1.

From the first round (before the rebase):

Mock, production build (drive.mjs chat-question-images, no console errors):

Reply box files on the cardSubmit waits for the uploadAnswered

Real Claude Code on a dev box, chat-question-images-live. Claude asks a question; the prompt says nothing about images. One image is dropped on the reply box and one pasted into the card. Both have words on them that only reading them reveals.

  • Terminal driver, pane at 80x24. Box log: answer ask: answered 1 questions. Transcript: Read: …-giraffe.png, …-pasted-163655.png. Claude: "One is an orange card reading "ORANGE GIRAFFE 7316" and the other a teal card reading "TEAL ANCHOR 0948"…"

  • Structured driver (Claude's SDK). answer sdk: answered 1 questions, two Reads. Claude: "…one is a plain orange card reading "ORANGE GIRAFFE 7316" and the other is a teal card reading "TEAL ANCHOR 0948"."

  • Answered in Claude's own terminal instead: the card closes and the image is back in the reply box.

Before: at 80x24 the same answer failed with Claude's form didn't take your words: it shows "this is the one that is off [attached: /tmp/verify-berthd-bo…". Real screen: testdata/answer/own-words-paths-80.txt.

Tests:

  • Before the box fix, TestDriveAttachedPathsOnAShortPane and TestReadFormRealScreens (the new fixture) failed; both pass now.
  • TestWordsAsWrapped: "use /tmp/a b" doesn't hold "use /tmp/ab", "now here" doesn't hold "nowhere", and a path broken at a line end does hold.
  • e2e chat.spec.ts:
    • "files in the reply box and dropped on the form go with its answer": Submit held while a 2.5 MB file uploads; the answer's paths are checked in what the mock box received.
    • "a screenshot pasted into the form's own words…": a failed submit keeps it; Retry sends it.
  • answer-attachments.test.ts covers the one-line format, round-trips paths with a space, a comma and brackets, and shownName with the same-second count.

Checks:

  • go vet: passes.
  • go test -race ./...: passes except tests that fail on origin/main on this machine too. TestTheCatalogIsCapped counts the machine's own commands. TestReviewRoutesOverTheWire, TestReviewSetupSays…, TestGitHubLooks…, TestDetach and TestAnSSHRouteThatFails… flake under load (after the rebase, TestAnSSHRouteThatFails… failed in the full run at load ~50 and then passed 5/5 on both this branch and origin/main): the same combination failed 1 in 3 on origin/main, and each passes on its own.
  • App: typecheck:plugins, check:*, pnpm test (583), pnpm build and tsc -p e2e pass.
  • e2e: chat, composer, drop and ask-card specs, 29 tests, pass. The new tests passed 5× repeated. After the rebase: the two attachment tests in chat.spec.ts pass, and drive.mjs chat-question-images (mock) passes.
  • cargo check can't run on this Linux VM (no GTK dev libraries); no Rust changed.

Pre-PR review: Codex review and adversarial-review. Fixed after review:

  • Whitespace-blind matching became wrap-aware.
  • A remounted form keeps its files in the answer.
  • Submit waits only on the files it sends.

Merge Danger

Door: two-way

Reverting restores the old card, reply box and screen check. Uploads still land in <worktree>/.berth/attachments as before. The box API and stored data are unchanged.

Blast Radius: chat

  • The reply box now reads its attachments from ChatFiles. A bug there shows up as chips missing or attachments not sent from the chat's reply box. Home's composer and the prompt dialog are untouched, apart from useAttachments now returning stable handlers.
  • On the terminal driver, the screen check for typed own words treats a line break as a space or nothing. Spaces elsewhere are still compared.
  • Rebased onto main at 71efa89a, after #186 and #180 merged. dev-box.sh keeps main's VERIFY_DRIVER=claude session and restart; the trust wait runs only for the terminal session. conversation-pane.tsx keeps main's structured-form changes beside the ChatFiles target. Still overlaps with #181 (lib/attachments.ts, dev-box.sh header).

🤖 Generated with Claude Code