feat(chat): embedded chat — view and reply to instance conversations from lab's UI #7
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab#7
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Add the embedded remote interface promised by ADR-0005's roadmap: a phone-first Chat view inside lab's UI that shows an instance's conversation and lets the operator reply directly — for manual instances and AFK runs alike. Claude Code is the only provider implementation; the message schema and provider seam must be provider-neutral so Codex/Gemini later mean one new implementation, zero refactor.
Design was fully grilled with the operator on 2026-07-06; the decisions below are settled — do not re-litigate them, record them in an ADR (see Deliverables).
Decisions (settled)
Product shape
Mechanism
3. Read: tail the provider-native transcript (Claude Code: live JSONL under
~/.claude/projects/<cwd-slug>/, located by worktree-cwd match analogous to deep-link capture), mapped behind theAgentProviderseam to a small universal schema:text | tool | dialog | lifecycle.4. Reply:
tmux send-keysinto the settled TUI. The argv-only stance for the initial prompt stands (cold-start TUI race); mid-session replies are exempt. Free text queues if the agent is mid-turn (show a subtle "queued" hint); composer disabled for ended instances.5. Interactive dialogs (AskUserQuestion, plan approval): detect as unanswered
tool_usein the transcript → render native tappable option buttons from the structured input (never scrape the TUI widget). Answering = pinned keystroke recipe (cursor-normalize, Down×N, Enter; Space toggles multi-select; "Other" = select, type, Enter). While a dialog is pending, lock the free-text composer so stray text cannot hit a focused picker. Unknown dialog types degrade to a "showing an interactive dialog — open in claude.ai" deep-link hint. Answers given in claude.ai flow back via the transcript; no divergence handling needed.6. Interrupt: explicit button (send Escape) with a confirm tap. No slash-command UI — slash commands are plain text through the composer.
Architecture
7. Persistence: read-through only. One new nullable column on the run row:
transcript_path(captured async by cwd-match, survives restarts, same pattern asdeep_link_url). No message table. Ended runs stay readable while the provider retains the file; if the transcript is gone, show a graceful "transcript no longer available" state.8. Transport: existing SSE bus, existing refetch-on-event pattern. A per-live-instance tailer publishes a debounced
run.messages.changedenvelope{type, repoID, runID}; the chat view refetchesGET /api/v1/runs/{id}/messages?after=<cursor>(append-only cursor; load the latest window first, older messages on scroll-up). SSE payloads stay envelope-small (ADR-0005).9. Fidelity: chat-first. Prominent: user messages, assistant text, pending dialogs. Tool activity = one-line chips ("Edit main.go", "Ran go test ✓") expanding on tap to truncated input/output. Thinking hidden behind a per-chat toggle. Errors always surfaced.
10. UI: new
/runs/:idroute; the body IS the chat. Full-screen on phone, fixed bottom composer, compact header (repo~label, conversational state, deep link, Stop). Entry points: Dashboard instance rows and/runshistory rows (read-only for ended runs). Keep v0 design language: max-width 760px, 44px touch targets, 16px inputs, dark viaprefers-color-scheme.11. Signaling: the tailer derives conversational state — working / needs input / question pending — served on the instance list and shown as live badges on Dashboard rows and the chat header. Web Push stays a separate roadmap item that later plugs into these states.
12. Neutrality of intervention: replying to or interrupting an AFK run has no effect on its budget clock, claim, or the three-strikes counter.
Implementation notes (seams, not prescriptions)
internal/provider/provider.go: extendAgentProviderwith the chat surface (locate/read transcript into the universal schema; send reply; answer dialog; interrupt). Updateprovidertestfake accordingly.internal/compat: four new pinned couplings, each documented incompat.mdand live-verified against the pinned Claude Code version like the existing four: transcript location + JSONL event mapping, reply send-keys recipe, dialog keystroke recipes, Escape interrupt.internal/tmuxx:SendKeysexists but is login-only today; instance-directed sends are new call sites.internal/events+web/src/sse.ts: addrun.messages.changedto the canonical event list.runs.transcript_path(sqlite + postgres mirrors).web/src/index.tsx, chat view component(s),createResource+events.subscriberefetch pattern as inDashboard.tsx.Deliverables
docs/adr/<next-free-number>-embedded-chat.mdrecording the decisions above in the house style (statement title, Status, Considered options, Consequences). NOTE: issue #1 (GitHub tracker) also commits an ADR claimed as 0015 — take the next number free onmainat implementation time; do not hardcode 0015. The ADR must be part of this issue's commits/PR, not a follow-up.internal/compat/compat.md: the four new pinned couplings with verification notes./runs/:idchat view, dialog buttons, interrupt, badges, composer states.Out of scope (explicitly deferred)
Web Push notifications · archive-on-teardown of transcripts · raw-terminal escape-hatch tab (xterm.js) · message DB table · curated slash-command sheet · Codex/Gemini transcript parsers (schema neutrality only).