Commit Graph
485 Commits
Author SHA1 Message Date
jpmschweitzerandClaude Opus 4.7 c30a707b41 hide prompted tool-use payloads from the conversation log
A permission-gated tool or AskUserQuestion already surfaces as a prompt
in the composer zone, so its raw tool-use card was redundant noise. The
session now tracks which tool_use_ids surfaced as a prompt; the
conversation view hides those tool-use cards. AskUserQuestion also hides
its result (the chosen answer is logged separately); permission-tool
results are kept — that's the useful outcome. The pane rebuilds the
view on each prompt change so the payload vanishes the moment its prompt
appears.

T-176, T-177, D-78.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 09:50:39 +02:00
jpmschweitzerandClaude Opus 4.7 de264e7141 flesh out the Claude prompt UX: options, stepper, collapse
Builds on the in-composer prompt surface (D-78):

- Permission prompts (T-175): Allow / Allow-and-don't-ask-again / Deny.
  "Don't ask again" appears only when the request carries a
  permission_suggestion and echoes it back as updatedPermissions. An
  optional note rides Deny as the message, or Allow as a follow-up user
  message (the protocol has no allow-with-message).
- AskUserQuestion picker (T-176): a single question renders bare; 2-4
  questions step one at a time (nav shows "N · Header", ✓ when answered)
  then a review/confirm screen. Each question offers an "Other" free-text
  choice and a per-choice note; multi-select joins labels. A "chat
  instead" escape denies the prompt so the user can type freely. On
  submit the answer is echoed into the log, since the card is ephemeral.
- Collapsed tool cards (T-177): multi-line tool_use / tool_result start
  collapsed behind a one-line summary; one-line output renders inline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 09:30:50 +02:00
jpmschweitzerandClaude Opus 4.7 fbf5a720a8 add Claude prompt UX wireframes (frame0)
Wireframes for the in-composer prompt surface (D-78): single
AskUserQuestion (bare), multi-question stepper, the review/confirm step,
and the permission Allow / Allow-and-remember / Deny prompt. Authored as
JSON, rendered + exported via the frame0-wireframe skill.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 09:30:40 +02:00
jpmschweitzerandClaude Opus 4.7 39f9ed018c drive the Claude pane over stream-json with native prompts
Replaces the Claude pane's tmux-TUI + transcript-tail backend with
Claude Code's stream-json control protocol (D-77/D-78). A
StreamJsonSession owns the `claude` process: its event stream feeds the
existing ConversationController, and permission / AskUserQuestion
prompts arrive as can_use_tool control_requests. Those surface as a
ToolPrompt in the composer zone — the pane swaps the text input for an
Allow/Deny card or an option picker while a prompt is open, so
interaction stays out of the conversation stream and the prompt buttons
don't fight the message-card hover chrome. The decision is written back
as a control_response (allow echoes updatedInput; AskUserQuestion
answers go in updatedInput.answers). Unsupported control subtypes are
answered with an error so a turn never hangs.

Session continuity is --resume (existing transcript) vs --session-id
(new); /clear and /resume respawn the process. The transcript reader
still backs the sidebar/status/team surfaces. T-165, T-166.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 08:40:18 +02:00
jpmschweitzerandClaude Opus 4.7 8310730506 document the stream-json control protocol + D-78
Empirical spike against claude 2.1.150 (driving the real CLI over
stdin/stdout + reading the shipped binary's zod schemas) pinned the
wire shapes for the stream-json control protocol: the can_use_tool
permission request, the control_response envelope, the
--permission-prompt-tool stdio enabler (without it "ask" tools silently
auto-deny), the allow-requires-updatedInput quirk, and AskUserQuestion
answered via updatedInput.answers. Captured in a version-pinned spike
note with a resilience section (detection canaries + a ranked fallback
menu) so a future Anthropic change to this undocumented contract doesn't
leave us at a blank slate.

D-78 records the decision: permissions ride the stdio control channel,
not MCP (MCP is reserved for capability/tool provision); refines D-77.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 08:40:05 +02:00
jpmschweitzerandClaude Opus 4.7 e672f1a77b render every conversation turn through one card primitive
test / unit + widget + golden + a11y (push) Failing after 24s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 25s
The Claude pane's conversation view hand-rolled a separate card layout
per message kind (user/assistant/thinking/tool-use/tool-result), so any
shared chrome had to be added five times. ConversationCard is one
template with three variants (stripe/bordered/bare) that wires the
chrome once: a copy button revealed on hover (yielding the turn's raw
text), an always-visible collapse/expand caret for collapsible turns,
and an extensible MessageAction list. It's decoupled from
ConversationItem — the view maps each item to (variant, accent, label,
body, copyText, actions) — so the typed event cards coming with the
stream-json work reuse the same chrome with a different body.

T-173.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 19:10:02 +02:00
jpmschweitzerandClaude Opus 4.7 1e77d72361 add StreamJsonSession transport (T-165 foundation)
test / unit + widget + golden + a11y (push) Failing after 25s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 23s
The testable core of the stream-json pivot (D-77): StreamJsonSession drives
a claude process in stream-json mode — parsing its line-delimited events into
the existing ConversationItem / SessionStatus types (reusing
parseTranscriptChunk, since stream-json assistant/user events share the
transcript's message.content shapes), pulling permission-mode off the init
event, and sending user input as stream-json over stdin (with a local echo
so the user's own message renders immediately). The process is abstracted
behind StreamJsonProcess so it unit-tests without spawning; the real
ClaudeStreamJsonProcess wraps Process.start.

Not yet wired into the pane — the claude_pane integration (replace the
tmux/PTY spawn + TranscriptReader feed, route input through send) and live
verification are the next step.

T-165.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 18:41:34 +02:00
jpmschweitzerandClaude Opus 4.7 4489765db9 add D-77: drive Claude via stream-json; clide-owned unified session model
test / unit + widget + golden + a11y (push) Failing after 29s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 27s
Accepted (phased). Pivot the Claude pane from the interactive tmux TUI to
the stream-json control protocol: structured events instead of transcript
tailing, permissions + AskUserQuestion handled natively via canUseTool,
persistence via --resume. Claude's tmux agent-team mode is headless-
incompatible, so teams become clide-orchestrated — N managed sessions
coordinated by a clide-hosted MCP broker, with team-awareness injected via
--append-system-prompt/--agents.

Captures the unified-session-model upside: teammate / secondary tab /
forked branch / inline subagent collapse into one primitive (a managed
session rendered as a pane), with the sidebar as the cockpit. Amends D-41
(persistence) and evolves D-75 (rendering source). Phase 1 single-agent
first; phase 2 the unified model.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 18:25:33 +02:00
jpmschweitzerandClaude Opus 4.7 0d45416bb8 resume existing Claude sessions with --resume, not --session-id
test / unit + widget + golden + a11y (push) Failing after 32s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 32s
Confirmed root cause of the dead-pane bug: `claude --session-id <id>`
rejects an id that already exists ("Session ID … is already in use") and
exits. The primary pane uses a deterministic id to resume across restarts,
and /resume re-binds to an existing id — both relaunched with --session-id,
so whenever the tmux session wasn't already alive (clean boot, or after
/clear+/resume) Claude exited instantly and the pane had no live backend:
typed input vanished while the transcript still rendered. The pane now
launches an existing session (transcript on disk) with `--resume <id>` and
only a brand-new one with `--session-id <id>`. Fresh secondaries and /clear
(fresh ids) were always fine. Verified empirically against a live session.

T-161.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 11:45:03 +02:00
jpmschweitzerandClaude Opus 4.7 b8168d8ffe fix ClidePane notifying focus listeners during build
test / unit + widget + golden + a11y (push) Failing after 34s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 28s
ClidePane.didChangeDependencies/didUpdateWidget run in the build phase and
called FocusTracker.setStatusWidget -> notifyListeners() synchronously,
rebuilding the focus-listening status-bar item mid-build — Flutter threw
"markNeedsBuild called during build" on every frame once a Claude pane was
focused. The convey now defers to a post-frame callback when mid-build
(re-checking focus then) and applies immediately otherwise. The T-150
widget tests missed this because no focus listener was in their tree;
added a regression test with PaneContextStatusItem present.

T-159.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 10:54:26 +02:00
jpmschweitzerandClaude Opus 4.7 6e4f357c09 show live per-member status in the team sidebar
test / unit + widget + golden + a11y (push) Failing after 31s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 29s
The teammate status was emitted by each TranscriptPublisher's statusStream
but never reached the bus. The observer now forwards it onto a shared
member-status channel ({agentId, model, permissionMode, contextTokens});
the meta sidebar subscribes and folds each member's live permission-mode
and context-token count into its roster row. No re-tailing — reuses the
existing stream (D-75).

T-157.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 22:57:01 +02:00
jpmschweitzerandClaude Opus 4.7 3038cd5eed add Claude meta sidebar — activity + team roster
test / unit + widget + golden + a11y (push) Failing after 28s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 25s
An always-pickable left-panel tab. Shows Claude activity read from
~/.claude/stats-cache.json (latest day's messages/sessions/tool-calls +
lifetime totals, polled) and, when a tmux agent team is running, a roster
of its members (colour · name · agent type · model) from the observer's
join/left events — nothing re-tailed here.

Scoped down from the original ticket: the account/team token budget isn't
programmatically exposed under subscription auth (TUI-only; upstream
#44328) and live per-member status needs the teammate status stream wired
onto the bus — filed as T-158 and T-157 respectively.

T-141.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 22:49:37 +02:00
jpmschweitzerandClaude Opus 4.7 8907abeca7 add Claude session storage view with user-driven cleanup
The claude.session-storage command opens a modal listing the workspace's
session transcripts with their on-disk sizes (the <id>.jsonl plus the
<id>/ subagents dir) and a total. Each row deletes with a deliberate
two-click confirm; deletion is guarded against unsafe ids and clide never
removes transcripts on its own. SessionSummary gains a sizeBytes field and
session_index gains formatBytes + deleteSession.

T-148.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 17:02:20 +02:00
jpmschweitzerandClaude Opus 4.7 849024b7ee show the configured skills count in the Claude status line
The status slot showed only live session fields (model/mode/context).
It now also shows the configured skills count from ClaudeConfig — the
environment side alongside the live session — and the pane rebuilds when
the config changes so the count appears once skills load and tracks
.claude edits.

T-154.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:50:09 +02:00
jpmschweitzerandClaude Opus 4.7 2fd911d01d auto-load extension i18n catalogs; add the missing tab-title ones
Five builtins (tickets, decisions, git, pql, problems) declared a
localized tab title but shipped no catalog and weren't in the hand-kept
preload list, so each logged "namespace not registered" on boot.
ExtensionManager now loads the i18n namespace of every localized
TabContribution when its extension activates — no manual list edit for a
new tab — and the five missing en_US catalogs are added.

T-155.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:49:56 +02:00
jpmschweitzerandClaude Opus 4.7 e5fa6302d3 handle /resume in clide with a native session picker
test / unit + widget + golden + a11y (push) Failing after 24s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 26s
Like /clear (T-156), Claude Code's /resume forks to a session the
transcript reader can't follow. clide now owns it: /resume opens a modal
picker of the workspace's recorded sessions — each labelled by its first
… last user prompt and last-active time — and re-binds the pane to the
chosen session-id (killing the current tmux session and respawning on the
picked id). Session enumeration reads bookend prompts from a bounded
window at each end of the transcript, so even multi-MB sessions summarise
cheaply.

T-156.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 14:05:03 +02:00
jpmschweitzerandClaude Opus 4.7 c4cc68ad2e handle /clear in clide: reset to a fresh session instead of forwarding
Claude Code's /clear forks the conversation to a new session-id, which
clide's transcript reader (pinned to the spawn --session-id) can't
follow — so after /clear the pane froze on the old transcript and looked
dead. clide now owns /clear: it's intercepted in the composer's send
path (never forwarded to tmux), tears the pane's session down, and
respawns a fresh empty one. A new session-id is forced even for the
primary so it starts empty rather than resuming the old transcript;
_spawn's self-heal kills the stale tmux session. The old transcript is
left on disk.

Known follow-up (T-156): /resume and /compact have the same forking
problem but need different handling (a session picker, not a wipe).

T-156.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 13:36:01 +02:00
jpmschweitzerandClaude Opus 4.7 f6a5d72ccd add slash-command typeahead to the Claude composer
Typing a slash — anywhere in the message, not just at the start — opens
an anchored typeahead listing matching commands and skills from
ClaudeConfig's slash list. Arrow keys move the selection, Enter/Tab
completes (inserting "/command "), Escape dismisses; with the popup
closed, Enter still submits and Tab still traverses. The recognition,
filtering, and completion are pure functions (slash_commands.dart) so
they're cheaply unit-tested; the overlay is a no-Material
CompositedTransformFollower keyed off the field's focus node.

T-152.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 12:43:49 +02:00
jpmschweitzerandClaude Opus 4.7 5fad8dc430 deliver slash commands typed so the Claude TUI runs them
The composer routed everything through tmux paste-buffer -p (bracketed
paste), and Claude's TUI deliberately doesn't parse a leading slash on
pasted input — so /command and /skill arrived as literal text instead of
running. Now a recognised command (single-line, leading slash, token in
ClaudeConfig's slash list) is delivered via send-keys -l (typed) so the
TUI fires it; everything else keeps the bracketed-paste path, which also
leaves a stray leading slash (e.g. a /tmp path) as literal text rather
than mis-parsing it. The slash list is warmed lazily when a Claude pane
opens so custom commands are recognised.

T-153.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 12:35:53 +02:00
jpmschweitzerandClaude Opus 4.7 0a781de0cd add ClaudeConfig service — layered Claude env + version-keyed slash probe
Builtin-owned, app-wide source of truth for Claude Code's environment
(D-76): skills, custom commands, settings, and permission rules read
from ~/.claude and the repo's .claude, layered local-over-global, watched
for changes. Built-in slash commands come from the stream-json `init`
event, captured by a one-turn probe cached in clide's own dir keyed on
the resolved claude version — so it runs at most once per claude version
per machine. load() stays cheap (version + cache-read + disk + watch);
the paid probe is a lazy ensureProbe() consumers call on first need, so
app-init and tests never pay for a model turn. Wired into the Claude
extension lifecycle and exposed as a builtin singleton.

T-151.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 12:20:09 +02:00
jpmschweitzerandClaude Opus 4.7 e27b2b7fb1 add D-76: ClaudeConfig as clide's Claude config surface
Extends D-75's accepted CC-internals coupling from the transcript/team
schema to the config layout: a builtin-owned ClaudeConfig service is the
app-wide source of truth for skills, commands, settings, and permissions
(global + local, layered), watched and refreshable. Built-in slash
commands come from a stream-json probe cached per claude version id.
Kernel stays Claude-agnostic — Claude is a non-disableable extension but
still an extension. Implemented by T-151..T-154.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 11:54:04 +02:00
jpmschweitzerandClaude Opus 4.7 1cb41c071b trim status-bar changelog bullet under the 60-word cap
test / unit + widget + golden + a11y (push) Failing after 27s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 27s
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 11:30:45 +02:00
jpmschweitzerandClaude Opus 4.7 478cff6050 add ClidePane primitive + focus-driven status-bar slot
Replace the MessageBus-based pane-context slot with a focus-driven one.
Panes keep their status widget locally; the FocusTracker holds the
focused pane's widget (activeStatusWidget) and ClidePane conveys it to
the shared slot only while its contribution is focused, re-conveying on
change and clearing on blur. The status-bar item just renders
focus.activeStatusWidget, height-clamped and marquee-scrolled when it
overflows. Removes the publish/subscribe race the bus version had.

T-150.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 11:23:30 +02:00
jpmschweitzerandClaude Opus 4.7 9590f555c9 share a generous test I/O timeout constant
test / unit + widget + golden + a11y (push) Failing after 26s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 23s
The flaky-gate fixes kept hand-tuning magic seconds in each real-I/O
test. Pull them into one Flutter-free constant — ioTimeout (20s) in
test/helpers/timeouts.dart, importable by both the dart-test (pty) and
flutter-test suites — and route the real-external-wait timeouts through
it: PTY output (session + registry), and fs-watcher events (timeout +
poll ceiling). Tune in one place instead of scattering durations.

The ipc socket round-trip timeouts (2s) are left as-is — they haven't
flaked and local sockets respond in ms; they can adopt the constant
later if needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 10:41:17 +02:00
jpmschweitzerandClaude Opus 4.7 b0d5aa36f4 move the per-session status to the bottom status bar (T-145)
Per feedback, the model · permission-mode · context line reads better in
the status bar than as a strip above the conversation. Adds a generic,
publisher-agnostic status-bar context slot: a pane publishes a short
string to the `statusbar.context` MessageBus channel and the bar shows
the latest. The active Claude sub-tab publishes (inactive panes stay
quiet, so no race); switching tabs swaps the slot to the focused pane.

Replaces the in-pane ClaudeStatusStrip with a formatStatusLine helper +
PaneContextStatusItem (the status-bar widget) and a StatusItemContribution.
ClaudeSessionHost passes `active` so only the visible sub-tab publishes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 10:37:16 +02:00
jpmschweitzerandClaude Opus 4.7 06cf9f8298 add per-session status strip: model / permission-mode / context (T-145)
test / unit + widget + golden + a11y (push) Failing after 28s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 23s
The transcript reader now also extracts a SessionStatus — current model
(assistant message.model), permission mode (the permission-mode records,
previously skipped), and context-window tokens (message.usage input +
cache-read + cache-creation) — and emits it on a statusStream, merging
deltas so it only fires on change. All CC-internals parsing stays in the
drift-contained reader (D-75).

The Claude pane renders this as a thin strip above the conversation
(model · permission-mode · context). Context is shown as a token count,
not a percentage: the transcript carries usage but not the model's window
limit, and the model id doesn't encode the 1M vs 200k tier.

Lead pane done; teammate-tile mirror and the sidebar (T-141) consume the
same status next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 10:14:05 +02:00
jpmschweitzerandClaude Opus 4.7 4b4d911734 harden PTY test timeouts (5s -> 20s)
test / unit + widget + golden + a11y (push) Failing after 27s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 24s
The real-shell PTY tests gave a child + reader-isolate only 5s to
deliver first output; under transient scheduling latency that was
occasionally exceeded, flaking the pre-push gate (retry:2 usually but
not always absorbed it). A working PTY echoes in well under a second,
so 20s is pure headroom — a genuinely dead PTY still fails, just later.
Verified 5/5 clean runs after the bump.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 09:48:26 +02:00
jpmschweitzerandClaude Opus 4.7 f65ef50379 fix Claude secondary pane copy + false "session exited"
Two things surfaced in the secondary pane: the tab said "session 1"
while the banner said "secondary 1" — now both say "session N". And the
banner showed "session exited" right after starting, even though Claude
was alive: a transient tmux client process can exit during spawn while
the session itself is fine. pane.exit now verifies via `tmux has-session`
and only reports exited when the session is actually gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 09:48:26 +02:00
jpmschweitzerandClaude Opus 4.7 af483c2c34 add a native startup banner to the empty Claude pane (T-149)
Replaces the bare "Waiting for Claude…" empty state with ClaudeBanner:
clide's own logo, a "Claude" label, the session role (primary /
secondary N), the workspace (home-collapsed), the tmux status line, and
a warming-up hint. ConversationView gains an optional emptyState widget;
the pane supplies the banner from data it already has.

Fully owned — no tmux capture-pane, no Anthropic artwork. The "Claude"
label uses Anthropic's published accent #d97757 nominatively; recorded
under a new trademark_notices section in assets/licenses.yaml (clide is
unaffiliated, bundles no Anthropic logo/artwork). Also fixes a stale
forkpty->pty reference in that file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 09:26:07 +02:00
jpmschweitzerandClaude Opus 4.7 b52283b7c0 self-heal Claude panes stuck on an unconnectable session (T-147)
test / unit + widget + golden + a11y (push) Failing after 27s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 28s
After session-id binding (T-146), a pane that attached (via new-session
-A) to a session created before the change — which has no --session-id,
so its transcript is under a different id — would wait forever for a
transcript that never appears. Same on any unconnectable session.

Before spawning, if no transcript exists for our deterministic session
id, kill the clide tmux session of that exact name so new-session
creates a clean one with our --session-id. This self-heals the stuck
state on next launch and makes clean-install/first-run robust.

Safe by construction: only clide's own session is killed — by its exact
clide-claude-<slug> name on the private -L clide socket (a terminal
claude never runs there) — and no transcript file is ever deleted. A
healthy session's transcript already exists, so re-attach continuity
(D-41) is preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 09:05:27 +02:00
jpmschweitzerandClaude Opus 4.7 b224502658 rename stale forkpty test tag to pty
test / unit + widget + golden + a11y (push) Failing after 32s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 36s
forkpty was replaced by posix_openpt + posix_spawn in T-96, but the
test tag, ci/test.sh segregation, and dart_test.yaml comment kept the
forkpty name. The segregation is still required — verified the PTY
tests fail under the flutter-test runner (the master fd doesn't
reliably deliver output there) but pass under dart test — only the
name was wrong. Rename to `pty` and correct the rationale comment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 08:49:13 +02:00
jpmschweitzerandClaude Opus 4.7 caf8fd95cf make the transcript session-switch test deterministic
The "session switch: newer file triggers replay" test relied on
wall-clock mtimes to decide which .jsonl was newest. Under timing
pressure the two files' mtimes could tie, so the reader never switched
and the test timed out — it failed ~60% of full-suite runs (measured),
the source of the intermittent red I'd been waving off as "a flake".

Backdate session A to a fixed past time so the newer file is
unambiguously newer; the switch is now guaranteed regardless of load.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 08:48:59 +02:00
jpmschweitzerandClaude Opus 4.7 e1cd4653c0 bind each Claude pane to its own session id (T-146)
A regression from T-137: every pane rendered the newest .jsonl in the
workspace dir, so concurrent sessions collided — a secondary tab showed
the primary's conversation. Each pane now spawns claude with its own
--session-id (a transcript is named <session-id>.jsonl), tails that
exact file via TranscriptReader's file: param, and uses a per-session
MessageBus channel so controllers don't cross-talk.

The primary's id is deterministic from its session name (stable → it
resumes across restarts, like /resume off the same history file);
secondaries get a fresh random id so a clean session is always available.
The reader now waits for the bound file to appear rather than throwing.

Migration: an existing tmux session created before this (no --session-id,
claude chose its own id) must be killed once (claude.kill-all-sessions)
so the next spawn binds the controlled id.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 08:28:55 +02:00
jpmschweitzerandClaude Opus 4.7 24f68e7aaa surface tmux teammates as a tile grid beside the lead (T-140)
test / unit + widget + golden + a11y (push) Failing after 27s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 27s
TeamPanelHost wraps the lead Claude surface and, on TeamMemberJoined,
shows a resizable right pane with one tile per live teammate in a grid
that wraps 1->2->3 columns by count. Each tile renders the teammate's
conversation from its per-agent MessageBus channel; tiles drop on
TeamMemberLeft. With no team, only the lead shows (unchanged).

The Claude extension now starts a TeamObserver for the open workspace
(restarting as the project changes) — wiring T-139 into the running app.
ConversationView gains a wrapInSelectionArea flag so the whole grid
shares one selection area (nested SelectionAreas are illegal), letting a
drag-select span tiles. Member colours map to tile accents.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 01:30:00 +02:00
jpmschweitzerandClaude Opus 4.7 4a59ea4137 rename team lifecycle events to Joined/Left
test / unit + widget + golden + a11y (push) Failing after 27s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 28s
TeamMemberBorn -> TeamMemberJoined, TeamMemberDied -> TeamMemberLeft
(kinds member-joined/member-left). Less morbid and a better fit for
teammates coming and going. No consumers yet, so a plain rename.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 01:19:59 +02:00
jpmschweitzerandClaude Opus 4.7 fc55f58e6b add tmux team observer + member lifecycle events (T-139, D-75)
test / unit + widget + golden + a11y (push) Failing after 27s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 24s
team_observer.dart is the single drift-containment point for Claude
Code's experimental tmux team mode. It discovers the active team for a
workspace (~/.claude/teams/<team>/config.json, matched by member cwd),
polls `tmux -L clide list-panes -a`, and correlates live panes with the
config's tmuxPaneId to emit TeamMemberBorn / TeamMemberDied — identity
(name, agentType, model, colour, pane) comes from the config, so it's
reliable regardless of transcript drift.

Each teammate's subagent transcript is resolved best-effort and streamed
on a per-agent MessageBus channel via TranscriptPublisher (TranscriptReader
gains an explicit `file:` for this). The config<->transcript join is the
fragile part: no shared key, so it uses a sibling .meta.json agentType
when present, else zips members-by-joinedAt against files-by-mtime. This
join needs validation against a live team run.

App wiring + visible surfacing land with the teammate tiles (T-140).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 01:13:46 +02:00
jpmschweitzerandClaude Opus 4.7 cda289b8b1 give Claude turns a brand-orange message card (T-144)
test / unit + widget + golden + a11y (push) Failing after 28s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 23s
Extends the card treatment to Claude's text responses: _userCard becomes
a shared _messageCard(label, accent, body) used by both turns. The user
stripe stays the theme focus colour; Claude's stripe + label use Claude's
brand coral-orange (#D97757) — a fixed brand accent, not a theme token —
so the two speakers are accent-coded at a glance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 00:42:19 +02:00
jpmschweitzerandClaude Opus 4.7 9783e6bd2c render user messages as a distinct card in the conversation (T-143)
test / unit + widget + golden + a11y (push) Failing after 30s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 32s
User turns rendered flat (a "you" label + text on the canvas), the same
shape as Claude's responses, so prompts were hard to pick out when
scanning. UserMessage now renders in a card: a left accent stripe
(focus colour) and a filled background distinct from the panel canvas.
Claude's text responses stay flat markdown — better for reading long
answers, and the asymmetry makes "what I asked" easy to spot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 00:34:19 +02:00
jpmschweitzerandClaude Opus 4.7 ce5996f869 show pasted files/images as removable chips in the composer (T-142)
test / unit + widget + golden + a11y (push) Failing after 31s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 31s
Pasting a file or image now adds a chip above the input instead of
inserting the raw @path as editable text: an image thumbnail
(Image.file of the cache/temp file, with an icon fallback) or a file
icon plus the basename, each with a × to cancel it before sending. On
submit the chips' @path tokens are appended to the typed text and the
chips clear.

resolveClipboardAttachment now returns ComposerAttachment descriptors
(path + isImage) rather than a pre-joined token string, so the composer
can render and manage each one. No new package dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 00:16:01 +02:00
jpmschweitzerandClaude Opus 4.7 6910c79e5b submit composer input via tmux server, not the detached client PTY
test / unit + widget + golden + a11y (push) Failing after 28s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 30s
The composer sent input with pane.write, which writes to the PTY of the
tmux client the app spawned. That client detaches (we no longer render
or drain its PTY since T-137), leaving the session alive on the server
with no client — so keystrokes written to the dead PTY vanished and
Claude never saw the message.

Submit now goes through the tmux server: load the text into a named
paste buffer, paste it bracketed (multi-line and special chars arrive as
one block, not a stream of submits), then send Enter. Verified against a
live session — paste-buffer -p reaches Claude's input with no client
attached. The no-tmux fallback still uses pane.write (claude runs
directly in our PTY there).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 00:01:43 +02:00
jpmschweitzerandClaude Opus 4.7 0abc14ed1a parse only large transcript chunks off-isolate; deflake stream tests
test / unit + widget + golden + a11y (push) Failing after 27s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 24s
The hang fix offloaded every transcript parse to Isolate.run, including
the small per-poll appends. Spawning a one-shot isolate each tick is pure
overhead and, under concurrent test load, the spawn+round-trip latency
blew the streaming tests' fixed-delay window — transcript_reader_test
flaked intermittently in the full suite.

Only chunks >= 64KB now go off-isolate (the initial-tail case that
actually janks a frame); small appends parse inline. The streaming tests
poll until the expected items arrive instead of waiting a fixed delay, so
they're robust regardless of parse latency or scheduler load.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 23:47:25 +02:00
jpmschweitzerandClaude Opus 4.7 8fd251ac48 add file/image paste to the Claude composer via native clipboard
Flutter's clipboard is text-only and tmux/send-keys carry text only, so
a pasted file or image must reach Claude as an @path reference (per the
T-134 spike). A native clide/clipboard MethodChannel reads the non-text
clipboard: GTK (gtk_clipboard_wait_for_image/uris) on Linux, NSPasteboard
on macOS. The composer overrides PasteTextIntent — Ctrl/Cmd+V resolves a
file path or writes a clipboard image to a cache dir, inserts the @path,
and falls back to plain-text paste otherwise. No new package dependency.

macOS handler is written but unverified on this Linux box — needs a build
on a Mac. Linux path builds and is covered by tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 23:36:18 +02:00
jpmschweitzerandClaude Opus 4.7 b3fa69c982 add native composer to the Claude pane (T-138)
A no-Material EditableText below the ConversationView. Enter submits,
Shift+Enter inserts a newline (a Shortcuts override maps plain Enter to
a submit intent; shifted Enter falls through to the default newline).

Submitted text goes to Claude over pane.write — the same input verb the
terminal pane uses, so D-6 parity holds and there's no Claude-only path.
Multi-line input is wrapped in bracketed-paste markers so the embedded
newlines submit as a single message instead of a stream of submits.

Paste handling is stubbed via an injectable resolver (file/image @path
support lands in the next commit); plain-text paste falls through.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 23:30:33 +02:00
jpmschweitzerandClaude Opus 4.7 0b942db251 route Claude transcript content through the MessageBus
test / unit + widget + golden + a11y (push) Failing after 26s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 23s
The Claude pane owned a TranscriptReader directly via the controller.
Insert a TranscriptPublisher that tails the transcript and republishes
each ConversationItem onto the kernel MessageBus; the view's controller
subscribes through ConversationController.fromBus. The subscription is
attached before the reader's first poll, so the initial tail isn't
missed on the broadcast bus.

This decouples reading from rendering: the team work (T-139/T-140) can
run one observer that publishes per-agent channels while the lead tile
and each teammate tile subscribe independently.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 23:03:17 +02:00
jpmschweitzerandClaude Opus 4.7 a6ed22405f fix Claude pane hang: cap tail, parse off-isolate, coalesce notifies
test / unit + widget + golden + a11y (push) Failing after 27s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 27s
The native Claude pane (T-137) picks the newest transcript JSONL by
mtime — which on a live session is this multi-MB active file. On attach
it read and parsed the whole file synchronously on the UI isolate and
fired notifyListeners per item, freezing the app.

Three fixes: cap the initial read to a recent tail (256KB, injectable
for tests); run JSON parsing in a background isolate via Isolate.run;
coalesce the controller's notifications with a zero-duration Timer so a
burst collapses into one rebuild (a microtask-scheduled notify wouldn't
— stream events deliver one per microtask and the notify interleaves).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 20:30:30 +02:00
jpmschweitzerandClaude 76c1d24904 render the Claude pane natively from the transcript (T-137)
test / unit + widget + golden + a11y (push) Failing after 27s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 27s
The Phase-1 wedge of epic T-132 (D-75): the Claude pane no longer
renders the PTY's TUI. It runs claude in tmux as before (so the
transcript is written) but displays the conversation as native cards
read from the transcript via TranscriptReader — user / assistant
markdown / thinking / tool-use / tool-result. The whole list sits under
a new no-Material ClideSelectionArea (SelectableRegion-based, since
Flutter's SelectionArea is Material and D-7 bans it), so text selects
and copies across cards — recovering the terminal's one real advantage.

ClaudePane drops its Terminal model and the resize-driven spawn trigger
(spawn now fires once on didChangeDependencies with a fixed tmux size,
since the TUI isn't shown); pane.output is no longer consumed. The
terminal builtin is untouched and still available as a general tool.
Input/composer is the next ticket (T-138).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-22 20:12:30 +02:00
jpmschweitzerandClaude 3f2e5ad9a9 spike T-134: Claude Code team/transcript internals (pinned to 2.1.148)
test / unit + widget + golden + a11y (push) Failing after 31s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 31s
Findings note for the tmux agent-team work (epic T-132), validated from
real on-disk artifacts (42 past team configs, real team + subagent
transcripts, current-version .meta.json) plus a synthetic control-mode
test and the tmux manual — no live team run / quota needed.

Key results: teammates get tmux panes (config tmuxPaneId) and write
transcripts at <munged>/<sid>/subagents/agent-<hex>.jsonl + .meta.json;
tmux 3.6a has no %pane-died (use %window-close/%layout-change), and
polling list-panes -a is the reliable lifecycle baseline; the real risk
is identity linkage — pane/teammate (config: name@team, agentType,
%pane) joins to the transcript only via agentType (config <-> .meta.json),
ambiguous for same-type teammates, so T-139 needs a joinedAt/mtime or
lead-transcript tiebreaker.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-22 19:52:00 +02:00
jpmschweitzerandClaude 0eee456426 add TranscriptReader: tail + parse Claude transcript JSONL (T-136)
test / unit + widget + golden + a11y (push) Failing after 27s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 30s
Data layer for native Claude rendering (T-132). Resolves the munged
project dir, picks the newest session .jsonl by mtime (Claude doesn't
expose its session id), tails it append-only via a dart:io byte cursor,
and emits a sealed ConversationItem stream (user / tool-result /
assistant text / thinking / tool-use). Skips bookkeeping record types
and degrades gracefully on an unfamiliar transcript `version`. Pure
dart:io + dart:convert, Flutter-free, zero new deps.

The parser is a pure public parseLine(line) -> List<ConversationItem>
so tests exercise the real code (an injectable projectsBase lets the
streaming tests point the real reader at a temp dir) — no shadow
re-implementation. 31 tests under dart test.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-22 19:36:53 +02:00
jpmschweitzerandClaude 9fed749084 make native text widgets selection-aware (T-135)
Foundation for rendering the Claude conversation natively (T-132) with
the cross-widget select+copy the terminal gives today. Converts the raw
RichText in clide_markdown + clide_code_block to Text.rich, which
registers with a Flutter SelectionArea's selection machinery (raw
RichText does not). Adds a selectionBackground surface token
(globalFocus at ~40% alpha, matching the terminal's selection tint) via
tokens + resolver default; bundled palettes are untouched (D-69).

Text and code blocks now select across each other under a SelectionArea;
tables and tappable link-spans remain non-selectable islands for now.
The selection contrast pair is intentionally not added to the WCAG gate:
the tint is semi-transparent and the gate's neutral-grey compositor would
false-fail it (documented in contrast.dart); deferred to the -hc/-cb pass.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-22 19:36:43 +02:00
jpmschweitzerandClaude 04aed1e851 record D-75: render Claude natively from transcripts (T-133)
Decision for epic T-132: clide renders the Claude conversation as native
Flutter widgets driven by Claude Code's transcript JSONL, not by scraping
the PTY/TUI. Claude still runs under tmux for process persistence (D-41),
but its content is sourced from the transcript. The terminal emulator is
retained as a general IDE tool only. Records the Claude-centric > CLI-first
ordering (D-6 surfaces preserved) and the accepted, version-pinned coupling
to Claude Code internal contracts, isolated behind one reader/observer.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-22 19:36:31 +02:00