Renders broker traffic as a chat timeline and makes the user a first-class
participant. The broker grows a Stream<TeamMessage> and a recipient field,
auto-registers a virtual `user` member, and gains sendAsUser. A Flutter-free
TeamChatModel (owned by the orchestrator) accumulates the feed and exposes
postAsUser with @-routing (a new at_commands helper mirroring slash) and an
optional interrupt that cancels the target's turn before delivery. One model
backs two surfaces: a compact cockpit widget that pops out into a full
workspace chat pane. CLI parity via clide.team-chat.open / .post.
T-180.
Co-Authored-By: Claude <noreply@anthropic.com>
Adds fork-into-a-pane: /fork in the composer, a roster Fork button, and a
clide.agent.fork command all branch a session via
--resume <source> --fork-session, so the branch gets its own claude
session id and diverges without touching the original. SpawnSpec/
ManagedSession gain forkSourceSessionId; the orchestrator selects the
fork argv via a new forkSessionArgs helper; the session host opens the
fork as a new secondary pane.
The branch's real claude session-id (assigned by --fork-session, arriving
in the init event) is not yet captured back — tracked as T-185.
T-172.
Co-Authored-By: Claude <noreply@anthropic.com>
Each roster row shows a D/A/P mode badge reflecting the session's live
permission mode. Click cycles the safe trio default -> acceptEdits ->
plan and sends a set_permission_mode control_request to that session
(mirrors interrupt(); fire-and-forget). bypassPermissions is a footgun,
so it is reachable only on Shift-click and behind an inline confirm. A
clide.agent.set-permission-mode command gives the CLI parity.
T-181.
Co-Authored-By: Claude <noreply@anthropic.com>
The copy button and custom message actions only rendered on hover, so
they were unreachable by keyboard or assistive tech. Keep them in the
tree always — revealed via opacity on hover OR focus — and route each
through ClideTappable (Tab traversal + Enter/Space activation) with a
Semantics button label and onTap so AT can discover and invoke them.
alwaysIncludeSemantics keeps them in the semantics tree while hidden.
T-174.
Co-Authored-By: Claude <noreply@anthropic.com>
The conversation pane now exploits the structured stream instead of
dumping tool input as JSON. ConversationController indexes tool_use by id
so a tool_result pairs back to its call and renders the Edit/Write diff or
is_error failure in place; per-tool bodies (Bash command+output, Read/Grep
file/query) reuse the shared renderers factored out of the permission
card. SessionStatus gains cost + contextWindow + rate-limit, read straight
off the init/result/rate_limit_event events, so the in-pane status line
reflects live state without the config probe.
Partial-message streaming is wired behind --include-partial-messages but
its event shape is unverified against the live binary and degrades to a
no-op if it differs — see T-184.
T-168.
Co-Authored-By: Claude <noreply@anthropic.com>
StatusbarHost laid every item at intrinsic width, so once the focused-pane
context line grew long (a model/mode/context/skills summary) the row's
content exceeded the bar width and overflowed instead of letting the slot
shrink. Add an opt-in flex factor to StatusItemContribution; the host wraps
flex>0 items in Flexible(loose) so they yield width when the bar is tight,
and the marquee then receives a bounded viewport and scrolls. Drops the
fixed maxWidth cap on the pane-context item.
T-160.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude often sends the file path itself as the tool description for
Write/Edit. The card body already renders that path, so printing the
description line above it showed the same path twice. Suppress the
description when it just repeats file_path.
Co-Authored-By: Claude <noreply@anthropic.com>
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 34s
The sidebar had outgrown one scroll (stats + roster + config don't fit). A
sub-tab strip now switches between three surfaces: Activity (usage stats +
the primary session's live runtime), Team (the member roster, auto-fronted
when a team spawns and otherwise quiet), and Config (the Claude-environment
settings table over ClaudeConfig).
Activity and Config render their key→value rows through one shared table
(same label column + row pitch + header style) so toggling tabs doesn't move
anything. The expandable skills/agents/commands/permissions/MCP browser on
the Config tab is the follow-up (T-183). Exposes StreamJsonSession.status so
the runtime row can seed from the session's current state.
T-182, D-77.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Claude's tmux team mode let teammates message each other and share a task
list; that mode is undocumented and unavailable headless. clide rebuilds
the same behavior over its own managed sessions, as the broker.
Verified live against claude 2.1.150 that a spawner can host an in-process
("SDK") MCP server entirely over the stream-json control channel — no
subprocess, no --mcp-config, no socket: declare the server name in the
initialize handshake's sdkMcpServers, answer the mcp_message JSON-RPC
round-trips (initialize / tools/list / tools/call) under
response.response.mcp_response. SDK tool calls are permission-gated through
the existing can_use_tool path. Documented in the 2.1.150 spike §6.
StreamJsonSession gains an McpServer hosting seam; TeamBroker + TeamMcpServer
expose send_message / broadcast / list_teammates / inbox / claim_task /
task_status, all routed through one shared broker. The orchestrator owns the
broker, registers each team session, delivers a message into the target's
next turn on its stdin, and injects roster + role via --append-system-prompt.
Solo sessions are unchanged (no MCP server, no initialize handshake).
T-170, D-77.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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 29s
ClaudeSessionOrchestrator owns a registry of ManagedSessions, decoupling
a session's lifecycle from any pane: spawn() starts + registers a
stream-json process, show()/hide() toggle visibility WITHOUT killing the
process, and close() tears it down. This is the one primitive Phase 2's
teammate / secondary-tab / forked-branch panes all become (D-77). The
process factory is injectable so the lifecycle is unit-tested without a
real claude. Not yet wired into the pane — that re-pointing is the next
T-169 step.
T-169.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
So toggling Activity↔Config doesn't visually jump: both render on one
two-column table (label left, value at a shared x, same row pitch +
section headers). T-182 notes the shared geometry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Settings is a fixed, short set, so it's a key->value table pinned at the
top, not an accordion. Variable-length groups (skills, agents, commands,
hooks, permissions, MCP servers) expand to their COMPLETE list rather than
a truncated first-N + "…" — a truncated list falsely prioritises its first
entries. Permissions expand grouped by allow/ask/deny. T-183 scope synced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Config tab is a browser, not a summary: each category (skills,
agents, commands, hooks, …) is an expandable accordion of the full list,
and file-backed entries open their .md in the right-side reader rather
than truncating to a one-line "…". Updates the T-183 scope to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase-2 interaction-model wireframes (D-77): the team cockpit sidebar and
the expanded team-chat pane (message inbox, @-routing, interrupt tickbox,
per-agent permission-mode badge), and the Claude sidebar reorganised into
Activity / Team / Config sub-tabs. Referenced by T-180..T-183.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two spot-check fixes (T-178, T-179), both grounded in a boundary test of
the stream-json wire (findings folded into the spike doc):
- Harness-injected user messages (skill loads, slash-command expansions,
system reminders) carry isSynthetic on the wire (isMeta in the
transcript). They were rendering as blue "you" cards though the user
never typed them; now UserMessage.injected flags them and the view
shows a muted, collapsed "context" card instead.
- Permission prompts now show the command/input being permitted (a
capped, scrollable code block) so you can see what you approve. Instead
of fully hiding a prompted tool-use, once resolved it collapses to a
one-line summary with a green (approved) or red (denied) border; the
session tracks per-tool_use_id outcome and the view colours it. The
result is kept.
Corrects an earlier wrong assumption: the Skill tool is auto-allowed
(no permission prompt); the inject only appears once the Skill tool is
actually invoked, which is why deny-captures missed it.
T-178, T-179, D-78.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
The socket-loopback DaemonClient (T-127) raced the UI on first launch:
panels queried before the socket finished connecting and cached a
"daemon not connected" error, and the Claude pane's spawn gate tripped,
leaving an empty terminal. Three fixes in the startup/connection path:
- DaemonClient.request() now waits briefly (5s) for an in-flight
connection instead of failing instantly, gated on _started so a
never-started client still fails fast. start() sets _started
synchronously so the gate is armed before the UI builds.
- swapIpcServer reuses the live server when the opened project matches
the workspace it already serves, instead of tearing it down — the
project-open flow fired right as the Claude pane spawned, dropping
the connection mid-spawn.
- _connect bails if already connected, so start() arming the reconnect
loop and swapIpcServer's reconnectAt can't open a second socket
(which had been double-delivering events).
This whole orchestration had no automated coverage — integration tests
stub a FakeDaemonClient. Adds a real wait-then-connect client test.
Co-Authored-By: Claude <noreply@anthropic.com>