[twarp 07b] Claude Code panel: resurrect view + event model#67
Open
timomak wants to merge 6 commits into
Open
Conversation
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>
This was referenced Jun 2, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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::ClaudeCodePanelso the panel is reachable from a plaincargo 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
claudeCLI 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/TranscriptItemmodel — the contract.driver:spawn_sessionrunsclaude -p --input-format stream-json --output-format stream-json --verbose [--resume <id>] [--permission-mode <mode>] [--allowedTools …]withkill_on_drop(true); emitsTranscriptEvents via a defensive line-by-line parser (futures::stream::unfoldoverBufReader::lines). Unknown event types / content blocks / non-JSON lines are skipped without crashing (PRODUCT §53). SIGINT-basedinterruptfor Stop (PRODUCT §11, Unix).sessions: reads~/.claude/projects/<encoded-cwd>/*.jsonlfor 7h resume — best-effort title from the first user message, mtime for sort, no twarp-side DB (PRODUCT §46–§51).Panel
app/src/claude_code_panel/:EditorViewwithautogrow + soft_wrap); Enter sends, Shift+Enter newline (PRODUCT §43–§45).async_channelof user turns intoclaude's stdin; reader stream feedsTranscriptEvents back to the panel viactx.spawn_stream_local(PRODUCT §8–§15).Read/Write/Edit/MultiEdit/NotebookEdit/Bash/BashOutput/KillShell/Grep/Glob/WebFetch/WebSearch/Task/TodoWrite/ExitPlanMode), generic card for anymcp__*/ unmapped tool, expand/collapse for long results (PRODUCT §23–§29).Edit/MultiEdit/Writeviasimilar::TextDiff::unified_diff()(PRODUCT §30–§33).bypassPermissionsso the smoke test doesn't deadlock waiting on the undocumented interactive permission protocol (TECH §Risks).ResumeSession(id)and spawn with--resume(PRODUCT §46–§50).claudeisn't onPATH— re-checked each render (PRODUCT §6).Registration / keybinding (unchanged from prior commits):
ToolPanelView::ClaudeCode+LeftPanelDisplayedTab::ClaudeCode(bothFromdirections), toolbelt button (Agent Mode icon), focus/render arms.custom_tag_to_keystroke+.with_custom_action(neverwith_key_binding— feature-06 lesson).Known limitations (documented, not blockers for the smoke test)
--permission-mode+--allowedToolschosen at spawn — and rendersPermissionevents as informational cards. The mode selector covers the common cases.UniformListisn't wired (PRODUCT §21–§22). The transcript is aFlex::column; long sessions may overflow until a scrollable wrapper lands.std::env::current_dir()(the process cwd) rather than the focused pane's cwd. Plumbing per-pane cwd fromLeftPanelView::set_active_pane_groupis a small follow-up.Validation
cargo checkandcargo clippyclean on the workspace.rustfmtclean.claude_codeunit tests: 19 / 19 pass.warp-ossbuilds 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../script/presubmitnot runnable on this Mac (clang-format / wgslfmt / nextest gaps); the checks above are the runnable subset.Smoke test
./script/run(orcargo run --bin warp-oss) — the Claude Code tab is now in the left-panel toolbelt unconditionally.claudeon PATH, confirm the unavailable state instead.claudeprocess spawns, a user bubble shows, and assistant text streams in. Status pill flips to "Streaming…" and a Stop link appears.Readtool card with the file path shows; ask Claude to run a shell command → aBashcard with the command + description; expand/collapse output works on long results.claude --resume <id>; the history replays fromclaude's own store.🤖 Generated with Claude Code