feat(chat): embedded chat — view and reply to instance conversations from lab's UI #7

Closed
opened 2026-07-06 23:17:05 +02:00 by dominik.polakovics · 0 comments

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

  1. Complement, not replace: the claude.ai deep link stays as the escape hatch; the chat is an additional surface. Applies to all instances (manual + AFK), view + reply.
  2. Vocabulary (add to CONTEXT.md): Chat — the rendered conversation of an instance in lab's UI. Transcript — the provider-native session file the chat reads through, never rendered raw.

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 the AgentProvider seam to a small universal schema: text | tool | dialog | lifecycle.
4. Reply: tmux send-keys into 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_use in 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 as deep_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.changed envelope {type, repoID, runID}; the chat view refetches GET /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/:id route; 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 /runs history rows (read-only for ended runs). Keep v0 design language: max-width 760px, 44px touch targets, 16px inputs, dark via prefers-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: extend AgentProvider with the chat surface (locate/read transcript into the universal schema; send reply; answer dialog; interrupt). Update providertest fake accordingly.
  • internal/compat: four new pinned couplings, each documented in compat.md and 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: SendKeys exists but is login-only today; instance-directed sends are new call sites.
  • internal/events + web/src/sse.ts: add run.messages.changed to the canonical event list.
  • Migrations: next free number adding runs.transcript_path (sqlite + postgres mirrors).
  • Web: new route in web/src/index.tsx, chat view component(s), createResource + events.subscribe refetch pattern as in Dashboard.tsx.

Deliverables

  • ADR at docs/adr/<next-free-number>-embedded-chat.md recording 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 on main at implementation time; do not hardcode 0015. The ADR must be part of this issue's commits/PR, not a follow-up.
  • CONTEXT.md: add Chat and Transcript to the Language section (+ Relationships if warranted).
  • internal/compat/compat.md: the four new pinned couplings with verification notes.
  • Backend: tailer, provider seam methods, API endpoints, SSE event, migration.
  • Web UI: /runs/:id chat view, dialog buttons, interrupt, badges, composer states.
  • Tests per the definition of done (guarded by the existing hermetic test env; recipe snapshot tests in compat style).

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).

## 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** 1. **Complement, not replace**: the claude.ai deep link stays as the escape hatch; the chat is an additional surface. Applies to **all instances** (manual + AFK), view + reply. 2. **Vocabulary** (add to CONTEXT.md): **Chat** — the rendered conversation of an instance in lab's UI. **Transcript** — the provider-native session file the chat reads through, never rendered raw. **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 the `AgentProvider` seam to a small universal schema: `text | tool | dialog | lifecycle`. 4. **Reply**: `tmux send-keys` into 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_use` in 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 as `deep_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.changed` envelope `{type, repoID, runID}`; the chat view refetches `GET /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/:id` route; 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 `/runs` history rows (read-only for ended runs). Keep v0 design language: max-width 760px, 44px touch targets, 16px inputs, dark via `prefers-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`: extend `AgentProvider` with the chat surface (locate/read transcript into the universal schema; send reply; answer dialog; interrupt). Update `providertest` fake accordingly. - `internal/compat`: four **new pinned couplings**, each documented in `compat.md` and 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`: `SendKeys` exists but is login-only today; instance-directed sends are new call sites. - `internal/events` + `web/src/sse.ts`: add `run.messages.changed` to the canonical event list. - Migrations: next free number adding `runs.transcript_path` (sqlite + postgres mirrors). - Web: new route in `web/src/index.tsx`, chat view component(s), `createResource` + `events.subscribe` refetch pattern as in `Dashboard.tsx`. ## Deliverables - [ ] **ADR** at `docs/adr/<next-free-number>-embedded-chat.md` recording 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 on `main` at implementation time; do not hardcode 0015. **The ADR must be part of this issue's commits/PR**, not a follow-up. - [ ] CONTEXT.md: add **Chat** and **Transcript** to the Language section (+ Relationships if warranted). - [ ] `internal/compat/compat.md`: the four new pinned couplings with verification notes. - [ ] Backend: tailer, provider seam methods, API endpoints, SSE event, migration. - [ ] Web UI: `/runs/:id` chat view, dialog buttons, interrupt, badges, composer states. - [ ] Tests per the definition of done (guarded by the existing hermetic test env; recipe snapshot tests in compat style). ## 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).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab#7
No description provided.