Now that the editor split actually opens (T-197), it exposed latent
issues, plus a coincidental Claude-session crash in the same log:
- T-203: the _EditorDragHandle's slider Semantics had value +
onIncrease/onDecrease but no increased/decreasedValue, so Flutter
asserted on every semantics flush — add them. And opening the split
reparented the Claude pane (direct child → Column/Expanded), tearing
down its SelectableRegion mid selection-update ('selectable not in
this registrar' / 'inactive element'); a stable GlobalKey on the
workspace primary makes Flutter move the element instead.
- T-202: rate_limit_event.resetsAt arrives as a unix-epoch number but
was cast `as String?`, throwing in the stream-json line parser. Accept
a num (epoch) or an ISO string.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The first T-197 fix flipped the wrong lever: it called
activateTab(Slots.workspace, 'editor.active'), but _WorkspaceSlot
renders its editor split off arrangement.editorOpen — not the active
tab — so clicking a file still showed nothing. Call arrangement
.openEditor() on editor.opened / active-changed(non-null), and
closeEditor() on active-changed(null) so the split collapses when the
last buffer closes. Test now asserts arrangement.editorOpen, the lever
the UI actually reads.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Search tab's Find mode stacked four ClideFilterBoxes (search,
replace, include, exclude) that all looked identical: every box drew the
magnifying glass and the hint was only a semantics label, never visible
text — so they read as four blank search boxes. Render the hint as
placeholder text while empty, and make the leading icon optional (the
replace + glob fields pass icon: null). General win — every filter box
now shows its placeholder.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pql sidebar panel and the find-in-files tab were duplicate search
surfaces. Consolidate into one Search tab with a mode switch: Find
(content grep), Vault (pql ranked search), Query (PQL DSL), Markdown
(the synced markdown-file listing, keeping focus-highlight + live
refresh). SearchPanelView holds both FindInFilesController and
PqlController; the pql body + result rows move into a reusable
PqlSearchBody. The standalone builtin.pql sidebar tab is removed (one
fewer tab — eases the rail); the pql extension keeps the Backlinks
context panel. No D-79 conflict — grep vs ranked search remain distinct
backends, this is UI consolidation.
Adds the pql builtin's first widget/controller tests (it was untested,
so folding it into the tested Search panel required covering the
Vault/Query/Markdown modes + PqlController).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The rail was a fixed Row(center, max) — one button per tab — so adding
the Search tab pushed it 54px past its width and threw a RenderFlex
overflow. Center the icons when they fit and scroll horizontally when
they don't (LayoutBuilder + SingleChildScrollView + a minWidth floor),
so the rail stays correct at any tab count. (T-200)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reworded entry exceeded the 60-word changelog-gate limit; tighten
it. No code change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pin/unpin toggle is a mode control, not navigation — grouping it
with back/forward/jump implied they work alike. Pull it out of
ReaderActionBar into a standalone ReaderPinButton placed before the
title (ClidePaneChrome.leading), leaving the right-hand navigator to
back/forward/jump-to-pin/edit. Applies to all three readers. (T-198)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring the tickets detail in line with markdown/decisions (D-81). The
controller loads on 'load' (the channel the retained ReaderNav emits),
the extension reveals the static tickets.detail tab on selection instead
of the per-click uncontribute/contribute churn (the T-188 anti-pattern),
and the view grabs nav.current on mount and wraps in ClidePaneChrome
with a ReaderActionBar — pin toggle left, back/forward + jump-to-pin
right, no edit pencil (tickets are pql records, not files). The
controller drops its now-unused panels dependency.
Also adds the tickets builtin's first tests — the sidebar list
(load/sections/filter/select/empty/error/refresh) and the detail reader
(load, nav, pin, parents/decisions/status) — covering a pre-existing gap
exposed by bringing these files under test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per user feedback on the reader action bar: use the push-pin glyph (not
the chain/link), make the pin button toggle the pinned state (tap to pin
current, tap again to unpin) via ReaderNav.togglePin, and split the
layout so the pin/unpin toggle sits on the left while jump-to-pin joins
the navigator (back/forward) on the right — left toggles, right
navigates. The action button gains an active (accent) state for the
pinned indicator.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two reveal-on-open bugs:
Decisions opened only on the second click (T-196): the detail view
subscribed in didChangeDependencies, which runs after the tab is
revealed, so the broadcast 'selection' that triggered the reveal was
already gone. Hoist the back/forward history out of per-view State into
a retained per-reader ReaderNav (kernel ChangeNotifier in a
ReaderNavRegistry, D-81). The nav records selections, emits 'load' (the
single channel readers display from), and survives mount/unmount — the
reader grabs nav.current on mount, so the first selection lands. Both
the markdown and decisions readers move to this model; the per-view
ReaderHistoryMixin and the markdown post-frame forward hack are gone.
The editor pane never opened (T-197): EditorExtension contributed a
workspace tab but nothing activated it on editor.open. Add an activate()
that reveals the tab on editor.opened / editor.active-changed; the
view's hydrate() pulls the active buffer on mount.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Documents why the markdown/decisions readers load from a retained
per-reader nav-history (grab-current-on-mount + single 'load' path)
rather than per-view state (dies with the widget — the T-196 bug) or
MessageBus retention (wrong layer). The user chose the nav-history
helper over a bus fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reader opened repo-local .claude markdown but rejected user-scope
files under ~/.claude with "path outside workspace" — that dir is
global, outside the repo, and files.read was repo-confined (T-102).
Per D-76 the Claude config surface is clide-managed, so files.read now
resolves a path under an allow-list: the workspace root plus trusted
extra read roots (FilesService.extraReadRoots), wired in main.dart to
~/.claude when present. Reads widen; writes stay repo-confined, and the
symlink re-check still refuses a config-root symlink that escapes. Off-
root paths and `..` traversal are rejected as before.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
resolveUnderRoot joined an absolute input onto the workspace root
(/repo + /repo/x → /repo/repo/x), so files.read 404'd on a file that
exists. The Claude Config tab hands the reader a skill's absolute
SKILL.md path, which hit this. Normalize an absolute input as-is; the
existing containment check still rejects absolute paths outside the
root, so the T-102 boundary is preserved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds on the find-in-files engine. A replace engine applies the
query's replacement to each matching file — literal or regex with
capture-group expansion ($1, $&, $$) — and reports per-file, per-line
before/after edits computed with the same logic the apply uses, so
preview and apply never disagree.
The search.replace command previews (no disk writes) or applies
(writing each changed file through the workspace path-safety guard).
The panel gains a Replace field: each match row previews its rewritten
line, and Replace all is gated on a clean git working tree (git is the
undo) plus a confirmation before it writes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add tests for the quick-open overlay's keymap-intent handlers (nav,
accept, dismiss), the no-match / truncated / walk-failure hints, the
search panel's error + no-results states and toggle re-run, the
controller's failed-grep and exclude paths, and engine glob/regex-group
cases. Restores the coverage floor (95.20%).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The find-in-files UI on top of the search.grep engine. A
FindInFilesController drives search.grep, accumulates streamed
search.match events (scoped to the active searchId, stale ids
ignored) grouped by file, and opens a match in the editor at its line.
The SearchPanelView contributes a sidebar tab: a debounced query box,
regex + case toggles, include/exclude glob fields, and a grouped
results list with the matched span highlighted.
findInFiles.open (Ctrl/Cmd+Shift+F) reveals and activates the search
tab.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pure-Dart content-search engine behind find-in-files (D-79): walks
the ignore-pruned workspace, fans files across worker isolates
(Isolate.run) for parallelism, matches each line with a literal
indexOf fast-path or a RegExp, and streams match batches with
cooperative cancellation. No ripgrep dependency; the search.grep IPC
contract is engine-agnostic so an rg accelerator can slot in later.
search.grep returns a searchId and streams search.match / search.done
(or search.error) events, mirroring files.watch; search.cancel stops
an in-flight search. The service reuses the files service's resolved
ignore set so both honour the same ignore_files: layering.
editor.open gains an optional 1-based line argument: it converts the
line to a byte offset and sets the initial selection, enabling
click-to-line from search results.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A file picker overlay over the whole workspace, distinct from the
command palette. QuickOpenController holds the file list + a
subsequence fuzzy filter; the overlay loads the list via files.walk on
open, shows RecentFilesService entries on an empty query, and opens the
selection through a shared openWorkspaceFile helper (.md → markdown
reader bus, else editor.open) that the files panel now also routes
through, so recents stay in sync from every open site.
Bound to ctrl+p / meta+p with `when: !palette.open` so it never
collides with the palette's ctrl+p navigation; in-overlay arrows/enter/
escape reuse the palette's keymap-driven model via quickOpen.* intents.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the hardcoded .gitignore + .clideignore read with the ordered
ignore_files: chain from .pql/config.yaml (D-4) — the single ignore
knob clide owns (D-3). readIgnoreFiles defaults to .gitignore (plus
.clideignore when present) when the config is absent or malformed, and
honours an explicit [] as "no file-based exclusions".
Add walkFiles + the files.walk command: a recursive, ignore-pruned,
capped flat file listing reused by quick-open (T-51) and the search
engine (T-52). Closes the never-filed ignore-layering placeholder in
files_commands.dart.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Find-in-files / replace (T-52/T-53) run as an in-process isolate-pool
grep engine behind an engine-agnostic search.grep verb — not pql (its
search is a ranked document index, with no line numbers, regex, or
glob) and not a ripgrep shell-out (unvendored, not guaranteed
cross-platform). ripgrep is kept as a future optional accelerator
behind the same verb. Clarifies the D-3 wrap-pql boundary: content
grep is a code-navigation primitive pql does not offer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Generalizes the pty split into an explicit "parallel=false" opt-out: a
`serial` tag (declared in dart_test.yaml). The parallel flutter run now
excludes `pty || serial`; a separate `flutter test --tags serial
--concurrency=1` pass runs the vulnerable ones. For the coverage gate the
two passes are real-merged by ci/merge_lcov.py (union DA, max hits, recompute
LF/LH) — a plain concat would double-count and corrupt the total.
Tag transcript_publisher's bus-republish test serial (it flaked in the
parallel pool). Gate verified green end-to-end at 95.08%.
T-193.
Co-Authored-By: Claude <noreply@anthropic.com>
The pty-tagged tests spawn real PTYs and flaked when dart test ran them in
parallel (fd contention) — papered over with retry: 2. Run that pass with
--concurrency=1 and drop the retries: serialization is the correct fix for
resource-bound tests. Verified stable across repeated runs.
T-193.
Co-Authored-By: Claude <noreply@anthropic.com>
pql is a hard dependency (the pre-push gate runs `pql decisions validate`;
the governance + ticket workflow is built on it) but the build setup only
mentioned Flutter. Add it as a prerequisite + the `pql init` setup step.
Co-Authored-By: Claude <noreply@anthropic.com>
The git-hooks line listed only pre-commit + post-merge; the load-bearing
one is the pre-push gate (core.hooksPath = .githooks). "The five commands"
listed seven. And push-check now runs test-coverage (a11y folded into it),
not a separate fast suite + test-a11y pass.
Co-Authored-By: Claude <noreply@anthropic.com>
The markdown and decision sidebar readers gain a shared action bar. A new
lib/builtin/shared/reader_chrome.dart provides ReaderHistory (browser-style
back/forward stack — push truncates forward), a ReaderHistoryMixin that also
holds a single pin slot, and a ReaderActionBar widget. Both readers push to
history only on external selection; back/forward and jump-to-pin reload
in-place without re-publishing a selection (no bus churn / no decision-tab
re-trigger). The edit pencil opens the current doc in the editor
(editor.open) — the markdown path, or the decision's file_path.
T-189, T-190, T-191.
Co-Authored-By: Claude <noreply@anthropic.com>
Wave A's widget tests pulled previously-untested files into the coverage
denominator (the decision extension loads decisions_view; the file-tree
tests load file_tree_controller), dropping total coverage to 93.6%. Add
tests for DecisionsView (list render + tap-to-select, the T-188 publisher
side), FileTreeController, and the remaining DecisionDetailView branches,
restoring the total to 95.03%.
Co-Authored-By: Claude <noreply@anthropic.com>
`make test` is now the fast dev inner loop: no coverage, parallel
(--concurrency=12), ~21s warm (down from ~36s). Coverage moves to a new
`make test-coverage`, which push-check runs to feed coverage-gate. Drop the
separate test-a11y pass from push-check — the coverage run already executes
test/a11y. Both runs get --timeout 60s so a hung test fails fast instead of
wedging the runner ~10min and stalling the gate.
Measured: coverage is the floor (~36s) and concurrency-insensitive, so the
gate keeps coverage without --concurrency; only the no-coverage dev path
benefits from parallelism.
T-192.
Co-Authored-By: Claude <noreply@anthropic.com>
The composer sourced its slash suggestions only from the CLI probe
(activeClaudeConfig.slashCommands), which never advertises clide-owned
commands, so /resume and /fork were missing from the typeahead. Union
kClideOwnedCommands onto the command source unconditionally — whether a
caller supplies a resolver or the default probe is used — de-duped via a
Set so /clear (in both) appears once.
T-162.
Co-Authored-By: Claude <noreply@anthropic.com>
The decisions extension tore down and re-contributed the decisions.detail
context-panel tab on every selection, racing the view's own subscription and
leaving the panel unrevealed — so clicking a decision often did nothing. Match
the working ticket panel: contribute the tab once (static), and on selection
just reveal the context panel and activateTab; DecisionDetailView loads via its
existing subscription.
T-188.
Co-Authored-By: Claude <noreply@anthropic.com>
The files panel and the Claude Config tab called ipc.request('editor.open')
for every file, which targets the editor — so a .md click never reached the
right-side markdown reader (it opens only when something publishes
('builtin.markdown','selection')). Route .md clicks from the files panel
(tree + filtered rows), the Config tab's file-backed rows, and .md wiki links
in the viewer to that channel; non-.md files still open in the editor. Also
remove the dead DaemonEvent fallback that listened for 'editor.buffer_activated'
(the registry emits 'editor.active-changed').
T-187.
Co-Authored-By: Claude <noreply@anthropic.com>
A reusable helper in the shared harness that drains microtasks + advances
one short fake-time tick, replacing the two patterns that have repeatedly
wedged the suite (and the pre-push gate) for ~10 minutes each: pumpAndSettle
(loops until quiescent — hangs on perpetual animation / overlapping async)
and `await Future.delayed(Duration.zero)` inside testWidgets (a real timer
that never fires under fake-async). Bounded by construction — cannot hang.
Co-Authored-By: Claude <noreply@anthropic.com>
Regenerated by `pql decisions sync` — moves resolved questions (Q-6, Q-19,
Q-21, Q-22) into a Resolved section.
Co-Authored-By: Claude <noreply@anthropic.com>
Dogfooding the skill surfaced it: `--status backlog,ready` is not a comma
list — it matches nothing and silently returns [], which would make batch
selection lie. Use a single `--status backlog` and note the `--unblocked`
filter still surfaces prose-"blocked on upstream" tickets (e.g. T-158).
Co-Authored-By: Claude <noreply@anthropic.com>
Batch selection no longer walks blockers per ticket or post-processes
JSON: read the landscape with `ticket show --tree`, select actionable
work with the composable `ticket list --under <epic> --leaf --unblocked
--status backlog,ready`, and record refinements with `ticket append`
instead of re-sending the whole description through `refine write`.
Co-Authored-By: Claude <noreply@anthropic.com>
The team cockpit / chat / config-tab work landed under-tested and pulled
total line coverage to 94.32%. Add tests for the team chat sidebar + pane
(@-completion, overlay, interrupt, message rows), the config loaders, the
stream-json MCP/streaming/rate-limit paths, and the conversation/prompt
card variants — restoring the total to 95.06%.
Co-Authored-By: Claude <noreply@anthropic.com>
A --fork-session branch is spawned without --session-id, so claude mints
a new session id that only arrives in the init event; the ManagedSession
was left holding its placeholder. StreamJsonSession now captures
session_id from the first event that carries it and exposes it via
claudeSessionId / sessionIdResolved; the orchestrator folds that back into
ManagedSession.sessionId (idempotent for normal sessions). A fork can now
itself be resumed or forked.
T-185.
Co-Authored-By: Claude <noreply@anthropic.com>
A live capture against claude 2.1.150 (both --print and the interactive
stream-json transport clide uses) confirms --include-partial-messages
emits the in-progress reply as stream_event envelopes wrapping Anthropic
streaming deltas — NOT assistant events with partial:true, which is what
T-168 assumed, so that handler never fired and streaming was inert.
Replace it: accumulate content_block_delta text per message id (tracked
from message_start, since deltas carry no id) and emit a placeholder under
a stable partial-<id> uuid the controller upserts in place; the matching
single-text-block assistant event reuses that uuid to finalize, while
tool_use / thinking blocks keep their own uuids and append in order. Tests
rewritten against the captured shape; spike doc records it.
T-184.
Co-Authored-By: Claude <noreply@anthropic.com>
The Config sub-tab grows from a static settings table into a browser of
the Claude environment: the pinned settings table stays, and below it
expandable accordions list the full (never-truncated) sets of skills,
agents, commands, hooks, permissions (grouped + colour-coded by allow/
ask/deny), and MCP servers. File-backed entries are clickable and open
their .md via editor.open. ClaudeConfig gains agents, hooks, and
mcpServers loaders plus path fields on skills/commands, kept live by the
existing .claude file watcher.
T-183.
Co-Authored-By: Claude <noreply@anthropic.com>
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 meta sidebar's Team tab becomes a control surface for clide-managed
agents instead of a read-only roster. Each row gains show/hide, mute,
close, and inject-a-message; a live task list renders from the broker
with reassign. The broker grows a Dart change-stream (kept Flutter-free
for dart test) plus tasks/reassign; the orchestrator gains mute/unmute,
injectMessage, and member-name session resolution. Every new UI action
has a matching clide command (D-6 parity).
T-171.
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>
Session lifecycle now runs entirely on the stream-json model: argv
selection picks --resume <id> for an existing transcript and
--session-id <uuid> for a fresh one, and the managed-session orchestrator
owns spawn/close. With the transport off tmux, remove the tmux session
lifecycle (reaping, kill-all-for-repo) and the tmux-polling team observer;
kill-all-sessions now closes sessions through the orchestrator. Team
membership is orchestrator-driven since the coordination broker landed.
Amends D-41 (tmux persistence -> --resume). T-167.
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>
_kOther carried a literal NUL byte so a real option labelled "other"
could never collide with the sentinel. The raw byte made the source
read as binary: git showed a binary diff and grep/file treated it as
data. Write it as a unicode escape instead — identical runtime value,
plain-text source again.
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 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
A runaway turn had no escape: Escape was unbound once the slash typeahead
was closed, and there was no Stop affordance. Now the composer interrupts
the in-flight turn — Escape (when no typeahead is open) or a Stop button
shown while busy — over the stream-json control channel.
StreamJsonSession gains interrupt() (writes a {subtype: interrupt}
control_request; claude cancels the turn and ends it with a result) and a
busy/busyStream signal driven true on send and false on the next result.
The pane binds onInterrupt to the session and reflects busy reactively.
D-78.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pane no longer spawns/owns its StreamJsonSession — it spawns-or-binds
through the app-wide ClaudeSessionOrchestrator by a stable pane key, and
the orchestrator owns the session + conversation. Consequences: disposing
a pane no longer kills its session (a kept-alive/hidden pane keeps it);
the primary re-binds to its live session on remount (conversation
survives); closing a secondary tab closes that session; /clear and
/resume close + respawn through the orchestrator. The extension owns the
orchestrator (set on activate, disposed on deactivate).
Remaining for T-169: re-point TeamObserver from tmux-polling to
orchestrating managed sessions, and roster-driven show/hide.
T-169.
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>
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 31s
pql 1.5 returns exit 0 with an empty `[]` for zero matches (older pql
used exit 2), so the wrapper's "exit 2 = empty, not an error" carve-out
is obsolete — and risky, since a future exit 2 could mean a real error.
Any non-zero exit is now an error.
Also removed the repo's vendored .claude/skills/pql: it's generated by
`pql init` (which CONTRIBUTING already lists in setup, installing at user
scope), so a committed snapshot just shadows the current global skill
with stale content and drifts on every pql bump — this copy was a whole
version behind. Other vendored skills are clide-owned; pql's is pql's.
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 33s
The native-prompts Added entry ran over the changelog gate's per-bullet
word ceiling; tightened it to the user-facing essentials.
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>
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>
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>
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>
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>
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>
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>
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>
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>
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 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>
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>
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>
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>
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>
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>
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 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>
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>