Skip to content

[twarp 07b] Claude Code panel: resurrect view + event model#67

Open
timomak wants to merge 6 commits into
masterfrom
twarp-07b-panel
Open

[twarp 07b] Claude Code panel: resurrect view + event model#67
timomak wants to merge 6 commits into
masterfrom
twarp-07b-panel

Conversation

@timomak
Copy link
Copy Markdown
Owner

@timomak timomak commented May 27, 2026

twarp 07 — Claude Code panel: full implementation

Per project owner: 7b alone (the scaffold) had nothing to test, so this PR now contains the entire feature 07 — sub-phases 7b → 7h — plus the removal of FeatureFlag::ClaudeCodePanel so the panel is reachable from a plain cargo run / ./script/run. Specs (PRODUCT.md + TECH.md) merged in #66.

Framing: feature 02 removed Warp's AI service (accounts, LLM clients, billing, cloud storage). Feature 07 brings back only the rendering layer, driven by the local claude CLI the user already runs. No LLM client, billing, cloud sync, or Warp account comes back.

What's in this PR

Headless crates/claude_code (driver + model + session store):

  • Transcript / TranscriptEvent / TranscriptItem model — the contract.
  • driver: spawn_session runs claude -p --input-format stream-json --output-format stream-json --verbose [--resume <id>] [--permission-mode <mode>] [--allowedTools …] with kill_on_drop(true); emits TranscriptEvents via a defensive line-by-line parser (futures::stream::unfold over BufReader::lines). Unknown event types / content blocks / non-JSON lines are skipped without crashing (PRODUCT §53). SIGINT-based interrupt for Stop (PRODUCT §11, Unix).
  • sessions: reads ~/.claude/projects/<encoded-cwd>/*.jsonl for 7h resume — best-effort title from the first user message, mtime for sort, no twarp-side DB (PRODUCT §46–§51).
  • 19 unit tests pass (6 model + 9 driver parser + 4 session store).

Panel app/src/claude_code_panel/:

  • Real editable input (EditorView with autogrow + soft_wrap); Enter sends, Shift+Enter newline (PRODUCT §43–§45).
  • Live session lifecycle: writer task drains a async_channel of user turns into claude's stdin; reader stream feeds TranscriptEvents back to the panel via ctx.spawn_stream_local (PRODUCT §8–§15).
  • Streaming indicator + Stop / End session in the header (PRODUCT §10–§13).
  • Tool cards with per-tool summaries (Read / Write / Edit / MultiEdit / NotebookEdit / Bash / BashOutput / KillShell / Grep / Glob / WebFetch / WebSearch / Task / TodoWrite / ExitPlanMode), generic card for any mcp__* / unmapped tool, expand/collapse for long results (PRODUCT §23–§29).
  • Diff cards for Edit / MultiEdit / Write via similar::TextDiff::unified_diff() (PRODUCT §30–§33).
  • Collapsible thinking ("Thinking" / "Thought for Ns") and in-place TodoWrite list with pending / in-progress / completed glyphs (PRODUCT §34–§38).
  • Permission-mode selector in the header (cycles Skip prompts → Auto-accept edits → Plan → Prompt, PRODUCT §41). Default is bypassPermissions so the smoke test doesn't deadlock waiting on the undocumented interactive permission protocol (TECH §Risks).
  • Session list + resume in the zero state — most-recent first, "▶ — 5m ago" rows that dispatch ResumeSession(id) and spawn with --resume (PRODUCT §46–§50).
  • Unavailable state when claude isn't on PATH — re-checked each render (PRODUCT §6).

Registration / keybinding (unchanged from prior commits):

  • ToolPanelView::ClaudeCode + LeftPanelDisplayedTab::ClaudeCode (both From directions), toolbelt button (Agent Mode icon), focus/render arms.
  • ⌘⌥K (Ctrl+Alt+K) toggle via custom_tag_to_keystroke + .with_custom_action (never with_key_binding — feature-06 lesson).

Known limitations (documented, not blockers for the smoke test)

  • Interactive Allow/Deny permission prompts (PRODUCT §39–§40) use Anthropic's undocumented stdin/stdout control protocol (TECH §Risks). This PR ships the robust path — --permission-mode + --allowedTools chosen at spawn — and renders Permission events as informational cards. The mode selector covers the common cases.
  • Assistant prose renders as plain text with soft-wrap rather than full markdown. PRODUCT §18 calls for the feature-03 markdown path; lifting it in cleanly without re-coupling to the pre-removal AI blocklist code is a small follow-up. Replies are still legible.
  • Auto-scroll / UniformList isn't wired (PRODUCT §21–§22). The transcript is a Flex::column; long sessions may overflow until a scrollable wrapper lands.
  • Stop uses SIGINT on Unix only; on other platforms the session ends instead.
  • Working directory is std::env::current_dir() (the process cwd) rather than the focused pane's cwd. Plumbing per-pane cwd from LeftPanelView::set_active_pane_group is a small follow-up.

Validation

  • cargo check and cargo clippy clean on the workspace.
  • rustfmt clean.
  • claude_code unit tests: 19 / 19 pass.
  • warp-oss builds and launches with the panel always visible (no flag); no startup panic from the keybinding/menu path (feature-06's failure class), no display-server errors.
  • Full ./script/presubmit not runnable on this Mac (clang-format / wgslfmt / nextest gaps); the checks above are the runnable subset.

Smoke test

./script/run (or cargo run --bin warp-oss) — the Claude Code tab is now in the left-panel toolbelt unconditionally.

  1. Open the panel via the toolbelt entry or ⌘⌥K. Confirm the zero state shows. With no claude on PATH, confirm the unavailable state instead.
  2. Type "list the files here" → Enter. A claude process spawns, a user bubble shows, and assistant text streams in. Status pill flips to "Streaming…" and a Stop link appears.
  3. Send a second message in the same session — no second process should spawn.
  4. Click Stop mid-turn — output halts, an "Interrupted." notice renders, the session stays alive.
  5. Click End session in the header — the live process dies; transcript stays.
  6. Ask Claude to read a file → a Read tool card with the file path shows; ask Claude to run a shell command → a Bash card with the command + description; expand/collapse output works on long results.
  7. Ask Claude to edit a small file → a diff card renders the unified diff (+/- lines).
  8. Ask a question that triggers thinking → a "Thinking" card appears, collapsed by default; click to expand.
  9. Ask for a multi-step plan → a TodoWrite list renders and updates in place as items move pending → in-progress → completed.
  10. Cycle the Mode link (top right) through Skip prompts / Auto-accept edits / Plan / Prompt; confirm it switches.
  11. After a couple of sessions, end them — return to the zero state and confirm the Resume a previous session list shows the prior sessions, most-recent first. Click one → it resumes via claude --resume <id>; the history replays from claude's own store.
  12. Toggle theme; confirm cards/diffs/status colors follow the theme.

🤖 Generated with Claude Code

timomak and others added 6 commits May 27, 2026 13:42
7b scaffolds feature 07's Claude Code left-panel. It registers as a
left-panel tab, opens, and renders its zero state (PRODUCT §5) or, when
`claude` is not on PATH, its unavailable state (PRODUCT §6) — but it
spawns no `claude` process: merely showing the panel starts no subprocess
(PRODUCT §7). The live session, streaming, cards, permissions, and session
list are 7c–7h.

New headless crate `crates/claude_code` defines the contract both halves
of the feature meet at (TECH §Parallelization): the thin twarp-native
`TranscriptEvent` the 7c driver will emit and the `Transcript` /
`TranscriptItem` model the panel renders. `Transcript::apply` — delta
accumulation, in-place TodoWrite updates, tool-result matching, verbatim
error surfacing — is unit-tested (6 tests) with no GPUI, so the event→model
mapping is verifiable before any live `claude` exists.

The panel (`app/src/claude_code_panel/`) is a proper child View (the Warp
Drive pattern) so 7c–7h have a home to grow into. It re-checks `claude` on
PATH at render time (PRODUCT §6). The message input is a styled,
non-editable placeholder in 7b; real editing + Enter-to-send is 7c (§8) and
7g (§43).

Registration: `ToolPanelView::ClaudeCode` + `LeftPanelDisplayedTab::ClaudeCode`
(both From directions), a toolbelt button (Agent Mode icon), focus/render
arms, and a `compute_left_panel_views` push gated on the new
`FeatureFlag::ClaudeCodePanel`. The flag is dogfood-only: DOGFOOD_FLAGS +
cargo feature `claude_code_panel`, intentionally absent from `default`, so
it stays hidden in stable.

Keybinding: ⌘⌥K (Ctrl+Alt+K) toggles the panel via
`WorkspaceAction::ToggleClaudeCodePanel` + `CustomAction::ToggleClaudeCodePanel`.
Per the feature-06 lesson, the default chord is registered in
`custom_tag_to_keystroke`, NOT `EditableBinding::with_key_binding` (which
would clobber `Trigger::Custom` and panic the mac menu builder); the binding
stays an `EditableBinding` so it remains remappable, and is flag-gated via
`.with_enabled`. Conflict check: `cmd-alt-k` / `ctrl-alt-k` were unbound.
Per PRODUCT §2, re-pressing the chord while the tab is active returns focus
to the terminal rather than collapsing the whole left panel.

Deferred to later sub-phases (noted so review scopes correctly): live
session / streaming / Stop / UniformList auto-scroll (7c, §8–§22); rich
tool / diff / thinking / todo cards (7d–7f — the transcript renderer is a
placeholder); permission prompts + editable multi-line input (7g, §39–§45);
session list, resume, and the cwd-in-header + zero-state "Resume…" entry
point (7h / 7c, §4, §46–§51).

Validation: `cargo check` and `cargo clippy` clean with the feature;
rustfmt clean; `claude_code` unit tests pass. Full `./script/presubmit` is
not runnable on this Mac (clang-format/wgslfmt/nextest gaps).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reconcile STATUS to git (spec #66 merged) and record the 7b impl PR. Tick the 7b sub-phase, flip the feature phase to impl-in-review, and note the resolved decisions: ⌘⌥K conflict-free (cmd-alt-k/ctrl-alt-k unbound), FeatureFlag::ClaudeCodePanel dogfood-only, launch-verified no startup panic, and the 7c–7h deferrals.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per project owner: 7b alone has nothing to interact with, so shipping the rest of feature 07 (7c–7h) in-place. Drop the dogfood-only gate first so the panel is reachable from a plain `cargo run` / `./script/run`.

Removes the FeatureFlag::ClaudeCodePanel enum variant, its DOGFOOD_FLAGS entry, the app/Cargo.toml `claude_code_panel` feature, the app/src/lib.rs cfg-bridge entry, the compute_left_panel_views if-guard, the ToggleClaudeCodePanel handler if-guard, and the EditableBinding .with_enabled gate. The toolbelt tab and ⌘⌥K binding are now unconditional.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundles sub-phases 7c–7h on top of 7b's scaffold (and the flag removal in
the previous commit). The Claude Code tab is now usable end-to-end against
the local `claude` CLI: type, send, see streaming replies, tool cards,
diff cards, thinking blocks, todos, plus a session list to resume prior
conversations. Per project owner's instruction — 7b alone had nothing
testable, so the rest of the feature is in here in one PR.

## crates/claude_code (driver + session store)

**`driver`** (`crates/claude_code/src/driver.rs`, 9 parser tests):

  * `spawn_session(SpawnOptions)` runs `claude -p --input-format
    stream-json --output-format stream-json --verbose` with
    `kill_on_drop(true)`, supports `--resume <id>`, `--permission-mode`,
    `--allowedTools`, and `--model`.
  * Defensive line-by-line JSONL parser via `futures::stream::unfold` over
    `BufReader::lines()`. Unknown event types / content blocks / non-JSON
    lines are skipped (PRODUCT §53); EOF surfaces an `Ended(Exited)` event
    once and then closes the stream so `spawn_stream_local`'s on-done
    fires.
  * Maps `system/init` → `SessionInit`, `assistant/text` → text delta +
    done, `assistant/thinking` → `Thinking`, `assistant/tool_use` →
    `ToolCall`, `user/tool_result` → `ToolResult`, `result` → `Ended`
    (Completed or Error(verbatim) per `is_error`).
  * `interrupt(&Child)` sends SIGINT on Unix for Stop (PRODUCT §11). The
    session stays alive; the next user message resumes from where the
    model was. Non-Unix logs a warning and falls back to drop-to-kill.
  * `send_user_message(stdin, text)` writes the user-turn JSONL shape
    claude expects (`{"type":"user","message":{"role":"user","content":text}}`)
    + newline + flush.

**`sessions`** (`crates/claude_code/src/sessions.rs`, 4 tests):

  * `encode_cwd(path)` mirrors claude's on-disk encoding (every
    non-alphanumeric char → `-`).
  * `list_sessions(cwd)` reads `~/.claude/projects/<encoded>/*.jsonl`,
    parses the first user message best-effort as a title (fallback
    "Untitled session"), sorts mtime-descending. Never errors — a
    missing/unreadable dir returns an empty vec at `debug`.
  * No twarp-side DB; every listed session belongs to `claude` itself.

## app/src/claude_code_panel (UI)

Rewritten to host a live conversation:

  * **Real editable input** via `EditorView { autogrow, soft_wrap, ... }`
    — Enter sends, Shift+Enter newline, empty/whitespace no-op
    (PRODUCT §43–§45). `submit` reads the buffer, spawns a session on the
    first turn, enqueues the message through a `async_channel::Sender`
    the writer task drains into claude's stdin.
  * **Streaming bridge** via `ctx.spawn_stream_local`: each
    `TranscriptEvent` parsed off claude's stdout is applied to the
    `Transcript` on the main thread, with `ctx.notify()` for re-render
    (PRODUCT §16–§22).
  * **Header**: status pill (`Idle` / `Streaming…` / `Session <id-prefix>`),
    permission-mode link (cycles through the four modes claude supports —
    PRODUCT §41; default `bypassPermissions` so the smoke test doesn't
    deadlock on prompts), and an `End session` link when live.
  * **Tool cards (7d)**: per-tool input summaries for `Read` / `Write` /
    `Edit` / `MultiEdit` / `NotebookEdit` / `Bash` / `BashOutput` /
    `KillShell` / `Grep` / `Glob` / `WebFetch` / `WebSearch` / `Task` /
    `TodoWrite` / `ExitPlanMode`; generic readable fallback for any
    `mcp__*` or unmapped tool. Status advances running → ok / failed.
    Outputs longer than 8 lines collapse with an Expand toggle.
  * **Diff cards (7e)**: `Edit` / `MultiEdit` / `Write` synthesize a
    unified diff from `old_string` / `new_string` (or `content`) via
    `similar::TextDiff::unified_diff().context_radius(3)`, rendered inline
    with the same +/- line treatment feature 05 uses visually.
  * **Thinking + todos (7f)**: `Thinking` items render as a collapsible
    "Thinking" / "Thought for Ns" card, collapsed by default; the
    `Transcript::apply` rule updates `Todos` in place (no duplicate
    lists). Pending / in-progress / completed glyphs; completed strike
    through.
  * **Permission card (7g)**: `Permission` events render as informational
    cards. The interactive Allow/Deny wire protocol (TECH §Risks —
    undocumented `control_request` / `control_response` over stdio) is
    not implemented here; the §42 degradation path is in effect (mode-
    pre-selection at spawn is the robust permission control).
  * **Session list + resume (7h)**: zero state shows the stored sessions
    inline ("▶ <title> — 5m ago"); clicking dispatches `ResumeSession(id)`
    which clears the local transcript (claude replays history) and spawns
    with `--resume`. `NewSession` ends any live process and clears.
    PRODUCT §49 invariant — never two live claude processes at once.
  * **Stop button** swaps with the submit link while streaming; clicking
    sends SIGINT via `driver::interrupt`.
  * **Unavailable state** re-checks `claude` on PATH each render (PRODUCT
    §6); a `Refresh` action additionally reloads the stored-session list.

## Known limitations (called out in PRODUCT/STATUS and the PR description)

  * Assistant prose is plain text with soft-wrap rather than markdown
    (PRODUCT §18) — lifting in the feature-03 markdown path without re-
    coupling to the pre-removal AI blocklist is a small follow-up.
  * Transcript doesn't use `UniformList` + bottom-stick auto-scroll yet
    (PRODUCT §21–§22).
  * Interactive Allow/Deny permission prompts use claude's undocumented
    wire protocol and are intentionally not wired (TECH §Risks).
  * Working directory at session start is `std::env::current_dir()`
    rather than the focused pane's cwd; per-pane cwd plumbing is a small
    follow-up.

## Validation

  * `cargo check` and `cargo clippy` clean on the workspace.
  * `rustfmt` clean.
  * `claude_code` unit tests: **19 / 19 pass** (6 model + 9 driver
    parser + 4 session store).
  * `warp-oss` built and ran for 45s with no startup panic or display
    errors (the menu-build path that bit feature 06).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two changes to make in-panel links (Mode, Start session, Resume, Stop) actually fire when the panel is opened via the toolbelt entry:

1. LeftPanelView::handle_action_with_force_open's ClaudeCode arm now calls ctx.focus(&self.claude_code_view) after activating the tab. Without focus, the workspace stays the focused view and ClaudeCodePanelAction dispatches have nowhere to land. (The ⌘⌥K path already focused via open_left_panel_view → focus_active_view_on_entry.)

2. log::info! at the top of ClaudeCodePanelView::handle_action so user can see in stderr whether dispatch actually reaches it. Drop this once the dispatch path is confirmed reliable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ble resume handles

Three fixes after the 'buttons don't do anything' report:

1. ROUTE THROUGH WORKSPACE. In-panel link callbacks now dispatch WorkspaceAction::ClaudeCodePanel(ClaudeCodePanelAction::…) instead of dispatching ClaudeCodePanelAction directly. The action queue's responder chain is ancestors(view_stack.last()), and even with the panel focused that wasn't reliably routing to ClaudeCodePanelView. Workspace is always at the root of every responder chain, so wrapping the action in WorkspaceAction and forwarding from Workspace::handle_action into ClaudeCodePanelView::dispatch_action via the new claude_code_view() accessor on LeftPanelView gets every click through.

2. PLACEHOLDER. The input EditorView now calls set_placeholder_text("Message Claude Code…") in new() so the empty input has a visible hint.

3. STABLE RESUME HANDLES. The Resume rows in the zero state now pull from self.resume_button_states (rebuilt whenever stored_sessions reloads) instead of a fresh MouseStateHandle::default() each render — fresh handles lose press state between mousedown and mouseup, so the click would never register.

Also: handle_action body refactored into pub fn dispatch_action so Workspace can call it directly; eprintln in dispatch_action so the user can see in stderr which action fired (drop once dispatch path is confirmed reliable).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
timomak added a commit that referenced this pull request Jun 2, 2026
)

* [twarp 07] respec: port deleted Agent Mode UI instead of rebuilding

PR #67 rebuilt the Claude Code panel from GPUI primitives (Flex/Container/
Link), ignoring the 7a port-and-adapt mandate: plain-text tool cards,
untinted diffs, no UniformList, plain-text assistant output, and a
WorkspaceAction dispatch workaround. This re-spec regresses 07 from
impl-in-review back to spec-in-review and rewrites the plan as a per-file
porting guide an implementer cannot misread.

Investigation findings (TECH.md):
- The old "what 02 deleted" table conflated two distinct deleted surfaces:
  ai_assistant/ (the simple Warp AI Q&A panel) vs ai/blocklist/ (Agent Mode,
  the tool-card/diff/thinking/todo surface). They were never composed together.
- Per-file decision matrix: the cleanly reusable leaves are the inline_action
  card chrome (HeaderConfig / RenderableAction / status icons, already
  AI-agnostic), the shared markdown stack (parse_markdown -> FormattedTextElement),
  and feature 05's read-only diff renderer. code_diff_view.rs and
  requested_command.rs are deeply service-coupled (interactive editor diff;
  live BlocklistAIActionModel command runner) -> rewrite/reuse, do NOT port.
- Dispatch fix: the panel is a self-dispatching TypedActionView like
  GlobalSearchView + an on_left_mouse_down focus-grab; delete the
  WorkspaceAction::ClaudeCodePanel forwarder #67 added.

Kept from #67: the headless crates/claude_code driver crate (lib/driver/
sessions, 19 passing tests) + the registration scaffolding (ToolPanelView,
toolbelt button, render arm, ⌘⌥K via custom_tag_to_keystroke). Discarded: the
primitive panel body and the WorkspaceAction forwarder.

Doc changes:
- TECH.md: postmortem + stop-sign, corrected table, per-file decision matrix,
  git-show recovery commands, stub/co-port + bridge layers, port-shaped 7b-7h,
  "visually matches Agent Mode" acceptance gate.
- PRODUCT.md: visual-consistency acceptance gate (Figma note + smoke step 28).
- STATUS.md: regression narrative + #67 postmortem + cleared 7b-7h ticks +
  re-derived port-shaped sub-phases.
- ROADMAP.md: row 07 spec column adds the re-spec PR (impl column cleared).

Spec-only change; no code. PR #67 to be closed by the owner.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [twarp 07] respec: fill in re-spec PR number (#68)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
timomak added a commit that referenced this pull request Jun 2, 2026
* [twarp 07b] Claude Code panel: panel shell + ported transcript

Sub-phase 7b of feature 07. Brings back Warp Agent Mode's rendering layer as
a left-panel tab by PORTING the deleted ai_assistant transcript renderer onto
the thin claude_code::Transcript model, fed by a synthetic event source (no
live driver yet — that lands in 7c).

Rendering layer only: no AI service, LLM client, billing, or cloud sync comes
back (feature 02's removals stay removed). From 7c on, the panel talks solely
to the local `claude` CLI the user already runs.

Kept from #67 (correct, decoupled):
- crates/claude_code driver crate (Transcript/TranscriptEvent/TranscriptItem
  + driver + sessions) — 19 unit tests pass.
- Registration scaffolding: ToolPanelView::ClaudeCode, the toolbelt button,
  render arm, compute_left_panel_views push, and the remappable ⌘⌥K binding
  via custom_tag_to_keystroke (not with_key_binding — the feature-06 lesson).

Discarded from #67:
- The primitive Flex/Container/Link panel body (rebuilt as a port).
- The WorkspaceAction::ClaudeCodePanel forwarder + its handler arm. The panel
  now self-dispatches ClaudeCodePanelAction the GlobalSearchView way (it is a
  TypedActionView with an on_left_mouse_down focus-grab).

The port:
- render_markdown_body ports render_message's markdown body (FormattedTextElement
  for prose, a bordered monospace box for fenced code).
- split_markdown_segments ports the AI-agnostic markdown splitter.
- Reuses feature 03's parse_markdown -> FormattedTextElement stack (§18), so
  assistant text renders as themed markdown, not plain spans.
- Transcript renders in a UniformList; zero state + claude-unavailable state;
  always-on (no feature flag — degrades cleanly when claude is off PATH, §6).

cargo test -p claude_code (19 passed), cargo check -p warp, cargo clippy, and
cargo fmt all clean. Scope is 7b only; 7c–7h follow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [twarp 07b] roadmap: advance feature 07 to impl-in-review (7b PR #69)

Reconcile after re-spec #68 merged: phase spec-in-review -> impl-in-review.
Tick 7b, record the 7b decisions (always-on, no feature flag) and PR #69.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <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