Commit Graph
17 Commits
Author SHA1 Message Date
jpmschweitzerandClaude Opus 4.7 caf8fd95cf make the transcript session-switch test deterministic
The "session switch: newer file triggers replay" test relied on
wall-clock mtimes to decide which .jsonl was newest. Under timing
pressure the two files' mtimes could tie, so the reader never switched
and the test timed out — it failed ~60% of full-suite runs (measured),
the source of the intermittent red I'd been waving off as "a flake".

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-22 19:36:53 +02:00
jpmschweitzerandClaude 8697f79a0a restore coverage floor to 95; cover ground elsewhere
test / unit + widget + golden + a11y (push) Failing after 30s
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / integration_test (xvfb) (push) Has been skipped
test / dart doc (lib API) (push) Failing after 1m1s
Reverts the D-66 amendment + the floor drop to 94 from 78b38e3 — both
were unilateral and outside my call to make. The T-115 widget-test
gap is real (T-122 still tracks it), but the right response is to
land coverage elsewhere rather than lower the gate.

Adds:
- intents_test.dart — parseIntentId for every builtin id + the
  `command:<id>` prefix path.
- session_naming_test.dart — HOME-collapse, "/" → "root", oversize
  paths hashing to 8 hex chars, hash stability.
- project_test.dart — onProjectOpen await branch in `open()`.
- settings_test.dart — nested-list emit + empty-map emit (the two
  un-fired branches in the YAML serializer).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-18 12:06:08 +02:00
jpmschweitzerandClaude 6601056972 finish T-87: cold-start reap, kill-all-sessions, helper tests
Three remaining acceptance criteria for T-87:

1. Cold-start reap. The Claude extension's activate() now kills
   every leftover secondary tmux session for the current repo
   before any new spawn. activate runs before any UI mounts, so
   _nextSecondary's starting value of 1 is correct even when a
   previous run died abruptly (kill -9, OOM, force-quit). The
   deactivate() hook also calls reapSecondaries as a courtesy on
   explicit extension teardown — but Flutter's deactivate doesn't
   fire on app quit, so activate is the load-bearing path.

2. claude.kill-all-sessions actually kills server-side. The
   command previously called pane.close on every claude pane,
   which only kills the tmux client. It now also calls
   tmux.killAllForRepo to kill the sessions on the clide socket.

3. Tests. test/builtin/claude/tmux_session_test.dart covers
   killSession, listClideSessions, reapSecondaries, and
   killAllForRepo via the TmuxRunner override — no real shell-out
   in tests.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 16:48:06 +02:00
jpmschweitzerandClaude Opus 4.6 46329700d5 dissolve app/ into repo root (D-056)
Single Flutter package at the repo root. All code, tests, assets,
and platform directories moved from app/ to root. Package renamed
from clide_app to clide — all imports rewritten. Merged pubspec
combines core (ffi) and app (flutter, yaml, xterm) dependencies.
Makefile simplified: no APP_PRESENT conditionals, no cd, no daemon
lifecycle. 317 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 00:37:20 +02:00