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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-22 19:36:43 +02:00
jpmschweitzerandClaude d7c935977e fix daemon-not-connected race on startup
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>
2026-05-22 15:39:59 +02:00
jpmschweitzerandClaude f1f12e7d79 add multi-file editor tabs
The editor pane showed a single buffer — opening a second file
replaced the first, even though the daemon's EditorRegistry has
always been multi-buffer (editor.list / activate / close). This wires
the UI up to that: EditorController now tracks the full open-buffer
list (via editor.list on hydrate, kept in sync by editor.opened /
closed / saved / edited events), and EditorView renders the buffers
as tabs through the shared MultitabPane — the same strip the Claude
pane uses. The daemon stays the source of truth: the local tab
controller is reconciled from it, and tab select / close route back
as editor.activate / editor.close. Reorder is disabled for now
(daemon order is authoritative).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-22 12:24:35 +02:00
jpmschweitzerandClaude 498bbc1205 add typed IPC command-schema framework (T-120)
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
Per D-74: commands register an argument schema beside their handler
instead of hand-validating args inline. DaemonDispatcher accumulates a
cmd->schema registry and, before invoking a handler, normalises the
argv-translator shape ({positional, flags}) into named args, coerces
types, and checks per-arg constraints (charset/pattern, leading-dash
rejection, numeric range, list caps). Violations return userError so no
handler sees malformed input. Schema adoption is opt-in per command —
unschema'd commands dispatch unchanged.

panel.resize adopts a schema (dropping the _ResizeArgs hand-lift from
T-119); git.checkout and git.push gain schemas that reject leading-dash
refs at the dispatcher and, via positional ordering, fix the C-client
CLI path — `clide git checkout <branch>` now reaches the handler, where
the positional token previously never mapped to `branch`. The T-104
validateGitRef + count/path caps stay in place as defense-in-depth
because the git client is reachable directly from the UI, not only
through the dispatcher.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-20 18:02:30 +02:00
jpmschweitzerandClaude 7764c896fe add panel.resize CLI verb (T-119)
test / unit + widget + golden + a11y (push) Failing after 36s
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
The keyboard half of panel resizing landed in T-111 (arrow-key
splitters); this completes D-6 user/Claude parity with a CLI verb now
that T-99's IPC dispatch path exists. `clide panel resize <slot> --to N`
sets an absolute pixel size, `--by N` nudges relative to current, and
the reserved `editor` slot drives the editor/bottom-panel split ratio.

The handler lives in panel_commands.dart and stays Flutter-free (so
test/daemon/ keeps running under `dart test`) by talking to an abstract
PanelResizer; the kernel bridge in panel_resizer_kernel.dart wraps
LayoutArrangement and reuses T-111's bumpedSlotSize so the CLI's
relative deltas honour the same right-edge sign-flip as the drag/arrow
handlers. Arguments are lifted from both the direct call shape and the
argv-translator's positional/flags shape pending the typed schema in
T-120. The daemonClientFactory now receives the LayoutArrangement so
the dispatcher can reach it.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-20 09:00:03 +02:00
jpmschweitzerandClaude d0e324ee42 drop @immutable from events/types.dart
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 28s
The prior commit swapped Flutter→meta to keep dart-test compiling, but
`meta` isn't a direct dep and `depend_on_referenced_packages` rightly
flags the import. Adding meta as a top-level dependency would violate
the prefer-zero-deps policy for what is purely a linter hint — the
classes are already const with final fields, so dropping @immutable
costs nothing at runtime and the analyzer never complained when the
annotation was absent.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-19 15:42:45 +02:00
jpmschweitzerandClaude fad539ca63 keep events/types.dart Flutter-free
`@immutable` lives in `package:meta/meta.dart` (transitive via the
flutter SDK); the file was importing `package:flutter/foundation.dart`
purely for that annotation, which pulls all of Flutter into the
transitive graph. Test files reaching DaemonBus / DaemonEvent through
`dart test` (not `flutter test`) then fail to compile because Flutter's
foundation depends on `dart:ui` types absent from the standalone VM.
Same spirit as 5cad982 — types that get touched by the IPC layer must
not drag the engine in.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-19 15:40:47 +02:00
jpmschweitzerandClaude 8b10130c87 T-130: MCP server over HTTP+SSE for /ide integration
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
Seventh slice of T-99. clide now advertises itself to Claude Code's
/ide command and serves a working MCP endpoint over HTTP+SSE per
D-73 (the Q-33 transport decision, locked in this commit).

What lands:
* D-73 — MCP transport for /ide is SSE over HTTP. Resolves Q-33;
  references D-68 + D-72.
* lib/src/ipc/mcp_server.dart — McpServer class. localhost HTTP
  listener on a random port; GET /sse opens a long-lived SSE stream
  with an initial endpoint event carrying the session id; POST
  /messages?sessionId=... accepts JSON-RPC requests and replies via
  the matching SSE stream. JSON-RPC handlers for initialize,
  tools/list, tools/call.
* Discovery file at $HOME/.claude/ide/<pid>.lock with the workspace
  + url so `/ide` can find us. Removed on stop.
* The two /ide minimum tools (mcp__ide__getDiagnostics,
  mcp__ide__executeCode) ship as stubs — real implementations need
  the analyzer integration / a clide eval surface, both follow-ups.
* main.dart starts the MCP server alongside the unix IPC server on
  daemonClientFactory and project switch. Failure non-fatal — the
  UI runs without MCP.
* 12 server tests cover lifecycle (start/stop, lock file), unknown
  paths, full JSON-RPC round-trip for all four methods, error
  responses, and edge cases (unknown session, malformed JSON,
  notification without id).

The "Claude Code's /ide discovers and connects" smoke is deferred to
T-131 wrap-up since it needs a real Claude Code session against the
running app — out of scope for unit/widget tests.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-19 14:34:09 +02:00
jpmschweitzerandClaude e194f02802 T-129: event streaming over the socket — clide tail --events
Sixth slice of T-99. Long-lived event subscription path, the second
half of D-6.

Wire shape:
- Client sends `{cmd:"tail", args:{flags:{events:true, filter:X}}}`.
- Server responds with `{ok:true, data:{streaming:true, filter:X}}`.
- Server pushes `{type:"event", subsystem, kind, ts, data}` lines
  until the client closes.

Server (lib/src/ipc/server.dart):
- Takes a DaemonBus, subscribes to DaemonEvent on start.
- Per-subsystem ring buffer (replayDepth=16 per D-6) populated on
  every emit.
- `tail --events` connection: send ack, replay matching events from
  ring, register the client for future fanout.
- _argv envelope now unwrapped at the server layer so the streaming
  check sees the inner `tail` cmd (not just `_argv`).
- Broken subscriber writes drop the subscriber cleanly; the bus
  doesn't block on a stalled client.

Client (native/clide-cli/clide.c):
- Sniffs `data.streaming:true` in the ack. If set, loops reading
  JSON-line events to stdout (with fflush per line) until EOF.

Tests:
- test/ipc/server_streaming_test.dart — 8 cases covering ack shape,
  filter, replay buffer (size + ordering), multi-subscriber fanout,
  broken-subscriber cleanup.
- test/cli/clide_cli_e2e_test.dart gets a tail --events test that
  spawns the C client, emits two events on the bus, asserts they
  print on stdout.

T-99 children remaining: T-130 (MCP), T-131 (wrap-up).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-19 14:20:07 +02:00
jpmschweitzerandClaude f987cd3bb1 T-128: delete IsolateClient / Backend / backend_entry.dart
test / unit + widget + golden + a11y (push) Failing after 37s
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
Fifth slice of T-99. After T-127 the socket-loopback DaemonClient
is the only IPC path; the isolate-backed third implementation
(IsolateClient + Backend + backend_entry.dart) was never wired
through and has no remaining references. Removed wholesale; the
single service-registration site lives in main.dart's
buildDispatcher.

flutter analyze + the kernel and ipc suites stay green.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-19 12:06:54 +02:00
jpmschweitzerandClaude 70c293b163 T-127: replace InProcessClient with socket loopback
test / unit + widget + golden + a11y (push) Failing after 2m16s
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
Fourth slice of T-99. The UI's DaemonClient now talks to its own
IpcServer through the same per-workspace Unix socket the C `clide`
client uses — one transport, one wire contract, no second path
through the dispatch tree.

Changes:
* lib/kernel/src/ipc/in_process.dart deleted. Nothing imports it.
* DaemonClient.socketPath becomes mutable + new `reconnectAt(path)`
  method swaps an active client onto a different socket without
  restart. Project switch in main.dart uses it — the dispatcher
  + IpcServer are rebuilt for the new workspace, and the client
  reconnects to the new path.
* main.dart's daemonClientFactory now builds a real DaemonClient
  pointed at workspaceSocketPath(workRoot); swapIpcServer kicks
  off server.start() then client.start() in sequence.
* lib/test_app.dart's pane.spawn smoke test uses dispatcher.dispatch
  directly instead of InProcessClient — same coverage, no dead-end
  import.
* DaemonClient client_test gets a reconnectAt round-trip test.

T-128 (delete IsolateClient + Backend + backend_entry.dart) unblocked.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-19 12:03:44 +02:00
jpmschweitzerandClaude 42955b6417 T-126: C clide shell client + _argv unwrap in the dispatcher
Third slice of T-99. After this `clide status` actually does
something when typed in a shell.

* native/clide-cli/clide.c — ~250 LOC C. Walks CWD up to .git,
  hashes the workspace root with FNV-1a 64-bit (byte-for-byte
  identical to the Dart side, pinned via reference vectors in
  paths_test.dart), opens the per-workspace socket, and ships argv
  across the wire as `{cmd:"_argv", args:{argv:[...]}}`.
* lib/src/cli/argv_dispatch.dart — registers the `_argv` sentinel
  command on the dispatcher. The handler runs the T-125 parser on
  the embedded argv and either re-dispatches the unwrapped request
  through the same dispatcher or returns the pre-built error
  response. Keeps the parser in Dart so the C side stays dumb.
* lib/src/ipc/paths.dart — fnv1a64Hex hoisted to a public helper +
  fixed to format as unsigned (Dart `int` is signed int64; the high
  bit lit a leading minus that broke the cross-language compare).
  Reference-vector tests added against the FNV reference.
* `make clide-cli` builds it via the host `cc`; output lands at
  native/<platform>/clide and is gitignored. Test
  test/cli/clide_cli_e2e_test.dart compiles + exercises the full
  round-trip; skips cleanly when no cc is on PATH.
* CONTRIBUTING.md gets a "C clide shell client" section.

T-128 (delete legacy IPC) unblocked.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-18 18:04:50 +02:00
jpmschweitzerandClaude 1147bfac0e T-125: argv→IpcRequest translator (CLI grammar in Dart)
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 1m2s
Second slice of T-99. Pure Dart function that takes the argv tail of
a `clide ...` invocation and returns either an IpcRequest ready to
dispatch or an ArgvError carrying a pre-built userError response.

The grammar — `SUBSYSTEM VERB [pos...] [--flag value] [--flag=val]
[-- passthrough...]` plus the umbrella commands `status`, `tail`,
`version`, `ping` — sits here so the C client (T-126) is a dumb
pipe: it sends argv as JSON and the server runs the translator
before dispatch.

Wire envelope: cmd is `subsystem.verb` (or just `subsystem` for
umbrella commands). Args is a generic envelope —
`positional: [...]`, `flags: {...}`, `passthrough: [...]` — none
required, all omitted when empty so the dispatch surface stays
minimal. Per-command typed schemas land later as each CLI verb
gets wired end-to-end.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-18 17:51:17 +02:00
jpmschweitzerandClaude c4bccd35a3 T-124: unix-domain IPC server, wired into Flutter app boot
First slice of T-99 (the D-56-path-a IPC server). What this lands:

* lib/src/ipc/paths.dart rewritten — `workspaceSocketPath(root)`
  returns the per-workspace path per D-70 (FNV-1a 64-bit hash, hex,
  no crypto dep — D-70 amended in this commit to record the hash
  choice). Old `defaultSocketPath()` removed; the lone fallback in
  facade.dart kept with a clear placeholder pending T-127.
* lib/src/ipc/server.dart — IpcServer class. ServerSocket.listen
  accept loop (D-72), 0600 socket + 0700 parent (D-71), stale-node
  probe + unlink on start, refuses to clobber a live listener.
* lib/main.dart — IpcServer started after the first dispatcher is
  built and swapped on project open (workspace path changes).
  Failure logged but non-fatal so the UI still works without IPC.
* 11 server tests + 5 path tests cover socket modes, multi-conn,
  stale unlink, live-conflict, idempotent start/stop.

T-99 children downstream of T-124 (T-125 / T-126 / T-127 / T-130)
are now unblocked.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-18 14:51:35 +02:00
jpmschweitzerandClaude ae8d774529 forward mouse wheel as xterm wheel escapes when TUI asks for it (T-74)
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 1m0s
Previously every PointerScrollEvent fell straight to PgUp/PgDown
keyInput as a "universal scroll" workaround. That kept plain shells
scrolling but starved vim mouse=a / htop / less of the wheel events
they expect.

Now `_onPointerSignal` checks `terminal.mouseMode.reportScroll`
first (the cascade of mouse handlers cares about this flag). If
the inner program declared ?1000h / ?1002h / ?1003h (optionally
+?1006h SGR), the wheel forwards as `wheelUp` / `wheelDown` button
events through the existing `renderTerminal.mouseEvent` path. Plain
shells stay on PgUp/PgDown because their mouse mode is `none` —
the existing test for that path keeps passing unchanged.

Click + drag forwarding through the gesture handler was already
wired (renderTerminal.mouseEvent for taps), so T-74's acceptance
list is met by this scroll fix alone.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-18 13:51:55 +02:00
jpmschweitzerandClaude 74f9a45539 extend build-info bake to name + tagline + repository
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 1m2s
User asked for the rest of the pubspec-derived strings to share the
same path the version went down. gen-build-info now also writes
`clideName`, `clideTagline`, `clideRepository` to
lib/src/build_info.g.dart from pubspec.yaml. Added a `tagline:`
field to pubspec for the short user-facing line (the welcome
subtitle, future web meta) — pubspec stays the single source of
truth for every name/tagline/version/repository string the app
shows.

Consumers swept:
* welcome banner ('clide' / 'IDE for Claude Code CLI') and status
  line version label read from the constants.
* app.dart WidgetsApp title + project-switcher label use clideName.
* clide_column_hat uses clideName for the empty-projects fallback.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-18 13:32:15 +02:00
jpmschweitzerandClaude 7c2eae42f1 fix theme picker integration test; one bake for build-time facts (T-116)
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 1m1s
The test was awaiting services.commands.execute('theme.pick') whose
Future doesn't complete until the dialog is dismissed — deadlock.
Fire-and-forget around pumpAndSettle, then tap Cancel, then await
the original future. Also tear the widget tree down before
services.dispose() so listening widgets unsubscribe first.

Pre-existing layout overflow in the welcome _StatusLine surfaced
when running the test at narrower viewports. Switched to a whole-
row FittedBox(scaleDown) — uniform shrink on narrow screens, no-op
at standard widths.

User flagged the hardcoded 'clide 2.0.0-dev' string. Replaced with
one generated lib/src/build_info.g.dart (gitignored, regenerated
by `make gen-build-info` from pubspec.yaml + git short SHA + UTC
clock). The same target re-syncs assets/licenses.yaml self.version
in place — no second source. Every make build/run/test depends on
it implicitly. Welcome status line now reads `clideVersion`. Stale
fontSize literals in welcome_view swept to typography constants;
clideFontMeta=13, clideFontDialogTitle=16, clideFontWelcomeBanner=52
added to fill gaps in the scale.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-18 13:22:55 +02:00
jpmschweitzerandClaude 68aa34e9dd clean lib/src/terminal/ to the project bar (T-107)
test / unit + widget + golden + a11y (push) Failing after 32s
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
T-107 (b): treat the in-tree terminal as ours, not vendored.

* custom_text_edit.dart — drop the row of commented-out `// print(...)`
  debugging stubs that shipped with the fork.
* parser.dart — the "TODO: G2/G3" lines for unimplemented VT220
  charset designators become a clear "not implemented" note; the
  stale "TODO: Normal/Application Keypad" tags on `>` / `=` get
  removed since the handlers ARE wired.
* keytab.dart — the bare "TODO: support VT52" turns into a comment
  explaining that ANSI=false records are intentionally skipped
  (no clide consumer asks for VT52).
* terminal_view.dart — the lone `// ignore:
  invalid_use_of_protected_member` keeps the suppression but gets
  an inline justification per CLAUDE.md (TerminalView owns its own
  ShortcutManager so terminal keybindings fire before the app's
  Shortcuts ancestor; wrapping in Shortcuts would invert that).

parser.dart's 1139-LOC size is parked as T-123 — split is too
invasive to fold here without conflicting with T-91's coverage
sweep on the same area.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-18 12:29:13 +02:00
jpmschweitzerandClaude 78b38e389d T-115 finishing touches + D-66 amendment for justified floor drops
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 1m2s
* Adds `make t T=...` and `make verify` (no-tests gate sweep), plus a
  gitignored test/.test-output/ that the new tee target writes to.
* loadRecents() now notifies listeners so the welcome view reflects
  recents loaded on cold boot.
* _StickyToggle gets a ValueKey('welcome.sticky.<path>') for testing.
* D-66 amended: a downward floor change is allowed iff (a) the commit
  explains the drop, (b) a follow-up ticket is filed in the same
  commit, (c) the new floor rounds down to the nearest whole percent
  of current actual coverage.
* coverage_floor: 95 -> 94. T-115's new _StickyToggle widget is
  uncovered because pumpWidget(WelcomeView) with a non-empty recents
  list strands the test until the 10-min Flutter timeout — even after
  ruling out ClideTooltip and tap shape. Tracked as T-122; next
  test-adding commit re-bumps the floor.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-18 11:29:41 +02:00
jpmschweitzerandClaude 7046bf9c70 picker-first startup with per-project sticky override (T-115)
Boot used to auto-open app.lastProject and fall back to the CWD; new
default is the welcome screen as the project picker. Sticky-open is
opt-in: a checkbox on each recent-projects row toggles a
startupSticky flag, and clide auto-opens iff exactly one row has it.
Two-or-more, or none, ⇒ picker (unambiguous user intent).

RecentProject gains the boolean (persisted in app.recentProjects);
ProjectManager exposes stickyProjectPath, openStickyOrNothing,
setStickyStartup, isStickyStartup, and preserves the flag across
reopens.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-18 10:26:12 +02:00
jpmschweitzerandClaude 31d40ad8ce harden IPC: reject -prefixed git refs, cap files.read / git.log (T-104)
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 1m0s
Three security fixes the consultant flagged:

* git.checkout, git.push now reject branch/remote arguments starting
  with `-` via a top-level validateGitRef helper. `git push` also
  gets a `--` option terminator; checkout can't use `--` without
  changing semantics (it would be parsed as a pathspec), so the
  validator is the only line of defence there.
* files.read caps responses at 10 MB so a single call can't OOM the
  UI on a multi-gigabyte log.
* git.log caps `count` at 1000; git.diff / git.stage cap paths at
  256. Excess is a userError rather than burning subprocess time.

The bigger typed-schema framework (item 1 in T-104) is split out as
T-120 since it needs design discussion alongside T-99.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-18 09:53:20 +02:00
jpmschweitzerandClaude 9249b1511d extract bumpedSlotSize for direct test coverage (T-111)
Pulled the slot-relative sign flip out of `_DragResizeHandleState._bump`
into a top-level `bumpedSlotSize` helper so the direction logic (the
bug-prone half) gets unit tests without piping through the keyboard
focus machinery. Adds a slot-label assertion for the context-panel
branch.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-18 09:45:47 +02:00
jpmschweitzerandClaude 5d1237501c panel splitters get keyboard parity and Semantics (T-111)
Drag handles for sidebar / context / editor split were pure
pointer-Listeners — no Tab focus, no arrow-key adjust, no Semantics.
Each now wraps in a FocusableActionDetector with arrow shortcuts (10
px fine / 50 px coarse for the column handles, 2% / 10% for the
editor split) and a slider Semantics node that announces the current
size. The CLI verb half is split out as T-119 and waits on T-99.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-18 09:43:56 +02:00
jpmschweitzerandClaude 044d1b2ff1 lift text-zoom into kernel, surface it in the palette (T-114)
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 1m1s
Workspace text-zoom (Ctrl +/-/0) was local state on _RootShellState,
reachable only via the keymap intent path. Lifted to a kernel TextZoom
ChangeNotifier so the new `view.zoomIn/Out/Reset` palette commands
mutate the same number the keymap does — closing T-114's "discoverable
in the palette" item.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-18 09:04:43 +02:00
jpmschweitzerandClaude 6d4a642773 tokenise window-control colours + palette shadow
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 1m2s
Hard-coded Windows close-button red (#E81123), white close glyph, and
the palette's 0x40000000 drop shadow were the three colour sites the
UX consultant flagged as not adapting per theme. Now they're
`windowControl.closeHoverBackground` / `closeHoverForeground` /
`shadow.ambient`. Paper themes override the shadow to a softer ink so
it doesn't read as a CRT halo on cream.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-18 09:00:52 +02:00
jpmschweitzerandClaude 18cbb4e47b re-flow contrast.dart per dart format
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 1m2s
Pre-push hook rewrote the two single-line `failingPairs` /
`failingExtendedPairs` getters; landing the formatter's choice.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-18 08:56:23 +02:00
jpmschweitzerandClaude cbbbc526f9 split contrast gate + ship -hc theme variants (T-114, T-118)
The expanded canonicalPairs from T-114 (muted text, status chips,
syntax tokens on the code-block surface, panel focus border) made the
four named themes fail WCAG-AA. Retuning their palettes to pass would
have changed the look users picked them for, so the gate is split
instead.

`canonicalPairs` shrinks back to the baseline every named theme passes;
the new `extendedPairs` carries the stricter set and only runs against
themes whose name ends `-hc` or `-cb`. Sibling files (`clide-hc`,
`midnight-hc`, `paper-hc`, `terminal-hc`) ship today; the policy lives
in D-69 with a back-ref from D-22.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-18 08:56:02 +02:00
jpmschweitzerandClaude Opus 4.7 d4f8f89016 code-quality batch (T-112)
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 1m7s
Seven small consultant findings, one commit:

1. TreeSitterLib stores last dlopen error + path in static fields
   instead of swallowing them. Callers that observe a null instance
   can now read the diagnostic.

2. Drop the Cmsghdr alias in libc.dart — back-compat shim with no
   callers; CLAUDE.md forbids those in a solo repo.

3. Drop EditorController._events field + the unused_field
   suppression. The constructor still subscribes via `events.on<...>`
   for _eventSub; the field itself was speculative retention.

4. Replace inline hex / errno literals in native_pty.dart with
   PosixErrno.{eintr,ebadf,epipe} and new libc.{pollin, pollAnyErr,
   sighup, sigkill, sigwinch}. PosixErrno gains eintr.

5. ExtensionManager records activate/deactivate exceptions in a
   `_failed` map exposed as `failedExtensions` + `didFail(id)`.
   Listeners are notified on entry/exit; cleared on a clean
   activate. UI surfaces the degraded state instead of pretending
   everything is fine.

6. file_tree_view imports FileEntry via the clide.dart barrel
   instead of `package:clide/src/files/listing.dart` directly — the
   leak the consultant flagged (barrel already re-exports it).

7. test_app branch in main.dart wrapped in `if (kDebugMode)` so
   release tree-shaker elides the test harness from shipping
   binaries. Source import stays; tree-shake handles the rest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:11:23 +02:00
jpmschweitzerandClaude Opus 4.7 b66e8f6cc0 event-driven test waits, fail-loud on timeout (T-108)
Replaces the fixed Future.delayed sleeps the consultant flagged
with stream-based waits that complete when the awaited event
arrives. Timeout callbacks call fail() with a diagnostic instead
of `onTimeout: () {}` swallowing the signal — a never-producing
pty now reports "pty did not produce X within 5s" instead of an
unhelpful "Actual: ''".

session_test.dart:
  - _readUntil helper subscribes to s.output, completes when a
    marker substring appears (or onDone), fails on timeout.
  - _waitForBuffer polls a buffer the listener is already filling
    after a write; 25ms tick, 5s ceiling, fail-loud on miss.
  - Drops the 500ms settle + 50×100ms polling pattern in the write
    test; uses a "first-byte" completer for prompt-readiness.
  - retry: 2 restored on the four read-dependent forkpty tests
    (the underlying flutter-test-runner pty-output flake hasn't
    fully gone away; recovers cleanly on a fresh spawn).

watcher_test.dart:
  - "emits a created event" awaits stream.firstWhere instead of two
    fixed sleeps.
  - "filters ignored paths" uses pre + post sentinel markers to
    bracket the inotify-delivery window event-driven; the negative
    assertion only runs after the post marker is observed.

event_sink.dart:
  - RecordingEventSink gains a broadcast `stream` for the same
    event-await pattern. PaneRegistry's output test subscribes
    BEFORE spawn so first bytes aren't lost.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:05:49 +02:00
jpmschweitzerandClaude Opus 4.7 7937da1734 panel-to-panel focus traversal via F6 / Shift+F6 (T-105)
Each SlotHost now owns a FocusScopeNode and registers it with
FocusTracker on mount. The render is wrapped in
FocusScope + FocusTraversalGroup so Tab stays within a panel and
slot-level focus is observable.

When a slot's scope gains focus, SlotHost pushes
(slot, activeContributionId) to FocusTracker — this collapses the
parallel-tracker model the consultant flagged. FocusTracker keeps
its setActive surface for explicit callers (palette, etc.) but
slot-scoped tab activation feeds it automatically.

Two new intents, two new bindings:
  FocusNextPanelIntent     → F6
  FocusPreviousPanelIntent → Shift+F6
(VS Code convention; preset YAML.)

The cycle skips slots without a registered scope, so a layout that
hides the context panel doesn't strand focus on a missing target.
Fewer than two registered → no-op.

SlotHost split into a stateful outer (scope + registry) and a
stateless `_SlotBody` (the existing slot-specific rendering),
keeping the build straightforward.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:56:35 +02:00
jpmschweitzerandClaude Opus 4.7 12e0509fa3 keyboard-operable ClideTappable + palette nav (T-100)
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 1m5s
Plug widgets into the keymap layer landed in T-117.

ClideTappable:
  - Wrap in `Actions(ActivateIntent → onTap)` outside a `Focus` so
    dispatch from the focused context walks up and hits the action.
  - Add a focus ring via `tokens.globalFocus` (DecoratedBox foreground
    overlay, transparent border when unfocused, no layout shift).
  - Disabled (`onTap == null`) skips focus traversal and shows the
    forbidden cursor.

ClidePalette:
  - Register Actions for the four palette intents
    (selectNext / selectPrev / accept / dismiss).
  - Publish `palette.open` scope flag via `KeymapService.setScopeFlag`
    so when-clauses can scope future bindings to "palette only".
  - Highlight the selected row with `listItemSelectedBackground`;
    scroll it into view on nav.
  - `PaletteController` grows `selectedIndex` + `selectNext` /
    `selectPrevious` / `acceptSelected`; index resets on open /
    filter change.

Intents.dart drops the `ClideIntent` base — `ActivateIntent` and
`DismissIntent` come from Flutter; clide owns the palette and text-
scale and command-bridge subclasses. `parseIntentId('activate')` →
Flutter's class; same for dismiss. Widget code uses the canonical
Flutter Intent types where they fit.

App root grows a PaletteOpenIntent action that calls
`services.palette.open()`, completing the ctrl/cmd+shift+p path
end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:48:03 +02:00
jpmschweitzerandClaude Opus 4.7 798ba524f1 keystroke mapper layer — intents, presets, when-clauses (T-117)
Build the upstream of every keyboard-driven feature: widgets bind
to typed Intents, the keymap resolves chord+context to an Intent,
and Flutter's Actions dispatches. The widget never touches a key.

Layers (low → high precedence):
  1. preset YAML in assets/keymaps/<preset>.yaml
  2. extension-registered command bindings (via
     KeymapService.registerCommandBinding from ExtensionManager)
  3. user file at <appDir>/keybindings.yaml
  4. settings JSON overlay at app.keymap.overrides

The when-clause grammar is a tiny recursive-descent parser over
boolean expressions on a named context bag — VS-Code style
`palette.open && !textInputFocused`. Producing services publish
scope flags via setScopeFlag.

Keys reference LogicalKeyboardKey.keyId (stable across keyboard
layouts), not the locale-aware keyLabel the consultant flagged.

Ships:
  - lib/kernel/src/keymap/{key_chord, when_clause, intents, keymap,
    keymap_service}.dart
  - assets/keymaps/default.yaml (the baseline preset)
  - 90+ unit tests covering parser precedence, layering precedence,
    scope evaluation, register/unregister, settings overlay,
    malformed-input tolerance
  - app.dart root handler routes through KeymapService → Actions
  - ExtensionManager mirrors every legacy defaultBinding into the
    keymap as a contribution layer

KeybindingResolver kept temporarily as a back-compat shim for
callers we haven't migrated yet; safe to delete once the last
caller goes through Actions.

Closes T-110 (consultant: scoped Shortcuts/Actions; off keyLabel).
Annotates T-23 with what's left for T-100. Unblocks T-64 / T-65 /
T-66 (preset data tickets).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:40:02 +02:00
jpmschweitzerandClaude Opus 4.7 2768f11767 fix SchedulerService isolate-spawn race (T-106)
_startTicker fired Isolate.spawn(...).then((iso) => _isolate = iso)
and returned. If _stopTicker landed before the spawn future resolved,
_isolate was still null at kill time and the just-spawned isolate
(with its Timer.periodic) leaked forever.

Track the spawn as _isolateReady and have _stopTicker await it before
killing. Same shape as the NativePty fix from T-96.

dispose() is now async; the single caller in facade.dart already
sat inside an async dispose chain and just needed the await.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:19:20 +02:00
jpmschweitzerandClaude Opus 4.7 06b08b7388 reject symlinks pointing outside the workspace (T-102)
resolveUnderRoot already blocked path-layer traversal but explicitly
did NOT follow symlinks — a repo symlink config -> /etc/shadow
passed the containment check because the link path was under root.
clide would then read the target.

Add resolveUnderRootFollowingSymlinks: resolves any symlinks at the
target and re-verifies containment against the resolved real root.
The split keeps pure path math testable without filesystem access.
files.read and files.ls now route through it.

Tests cover: plain non-symlink passthrough, non-existent target
(returns path-layer result so caller surfaces not-found cleanly),
single-hop and chained symlinks whose targets escape the workspace,
and tolerance of symlinks in the root path itself (macOS /tmp).

Also adds the T-101 CHANGELOG entry that the docs commit missed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:01:56 +02:00
jpmschweitzerandClaude Opus 4.7 70ce6c270e fix untrusted-workspace RCE in dugite git resolution (T-98)
Drop the workspaceRoot parameter from resolveToolchainPaths /
Toolchain.resolvePaths entirely. The old code resolved
\`<workspaceRoot>/native/dugite/bin/git\` as the git binary before
falling back to PATH — a malicious repo could commit an executable
at that path and clide would run it on the first auto-fired
git.status (which fires automatically on workspace open).

Dugite now resolves against trusted locations only:
1. CLIDE_DUGITE_DIR env var (dev override).
2. <exe-parent>/dugite/bin/git (production bundle).
3. <exe-parent>/lib/dugite/bin/git (alternate bundle layout).

Test plants `native/dugite/bin/git` in a temp workspace and asserts
the resolved git path is NOT inside the workspace.

Callers updated (8 sites): main.dart, backend_entry.dart twice,
test_app.dart three times (compute now wraps a no-arg call), plus
five test fixtures. backend.dart's now-vestigial hintRoot left in
the struct for cleanup under T-99.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 20:56:02 +02:00
jpmschweitzerandClaude Opus 4.7 8074bf4201 replace forkpty() with posix_openpt() + posix_spawn() (T-96)
`forkpty` calls `fork()` underneath. `fork()` in a multithreaded
process is unsafe: only the calling thread survives in the child,
but libc locks held by other threads remain "locked forever." With
the multi-threaded Dart VM as parent, ~5% of spawns deadlocked in
the child before `execve` (forensic probe: child stuck in S state
with comm=`DartWorker`, master fd never sees POLLIN).

`posix_spawn` uses `vfork` on glibc/musl/macOS, keeping the parent
suspended until execve completes — no Dart code runs in the child.
Pty pair built via the POSIX-standard `posix_openpt` / `grantpt` /
`unlockpt` / `ptsname` sequence. Probed: zero hangs in 300
sequential spawns vs ~5% before.

Behavior change: missing executable / missing workingDirectory now
surface as a `PtyException` thrown by `NativePty.start` rather than
a diagnostic written from the child to the slave PTY. Cleaner error
path for callers.

Side benefit: drops the `libutil.so.1` dynamic-library dependency.
PTY now resolves entirely against libc via `DynamicLibrary.process()`.

Splits the library-level `@Tags(['forkpty'])` on session_test.dart
into a per-test tag, so the now-runnable-under-flutter-test cases
contribute to coverage. `dart_test.yaml` declares the tag so the
exclude-tags filters honor it. Drops the `retry: 2` workaround from
the formerly-flaky registry test.

D-5 amended. Trims session-introduced CHANGELOG entries that were
over-verbose for the Keep-a-Changelog format.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 20:21:41 +02:00
jpmschweitzerandClaude Opus 4.7 ab2e5e618b tree_sitter test coverage + DI seam, ratchet floor to 95
Add `colorForRole` switch-arm tests (every role → token mapping plus
the unknown-role fallback). Introduce a DI seam in `TreeSitterService`
and `TreeSitterLib` so tests can substitute the FFI surface and asset
loaders without dlopen'ing `libtree-sitter.so` —
`TreeSitterLib.testing(...)` takes named per-function overrides with
safe no-op defaults, and `TreeSitterLib.fromDynamicLibrary(...)` lets
the smoke test load the vendored library explicitly. Production
paths (`TreeSitterService.shared`, `TreeSitterLib.instance`) are
unchanged.

Fake-FFI tests walk every branch of `_init`, `_loadGrammar`,
`highlight`, and `dispose`. The smoke test catches FFI-signature
regressions the fakes can't, by exercising the real native library
end-to-end on Linux. Together this takes `tree_sitter_service.dart`
from 17% to 96% and crosses the global 95% target — closing out the
D-66 line-coverage epic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 20:21:05 +02:00
jpmschweitzerandClaude 5cad98224f keep the clide.dart barrel Flutter-free
`lib/kernel/src/toolchain.dart` is a `ChangeNotifier`, so it pulls in
`package:flutter/foundation.dart`. `GitClient` and `PqlClient` imported
it for the resolved binary paths, which leaked Flutter through the
`package:clide/clide.dart` barrel — breaking `dart test` on every core
subsystem suite (`ci/test_core.sh`), since pure Dart can't compile
Flutter packages.

Split the Flutter-free pieces into `toolchain_paths.dart`: `ResolvedPaths`,
`resolveToolchainPaths`, and a new read-only `ToolchainView` interface
with a `ToolchainView.resolved()` const factory. `Toolchain` now
implements `ToolchainView`; the clients depend on the interface. Core
test setups that built a `Toolchain` just to call `applyResolved`
switch to the factory.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-14 21:21:09 +02:00
jpmschweitzerandClaude Opus 4.7 889058db1b test sweep: cover kernel/src/{events,ipc} (T-91)
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 1m2s
Three new test files + a small DaemonClient dispose-safety fix:

- test/kernel/src/events/types_test.dart (7 tests): every ClideEvent
  subclass's subsystem / kind / payload contract + the
  ClideEventEnvelope v1 JSON shape.
- test/kernel/src/events/message_bus_test.dart (6 tests): Message
  shape, MessageBus publish/subscribe/dispose, filter-by-publisher,
  filter-by-channel, intersection.
- test/kernel/src/ipc/client_test.dart (9 tests): real Unix-socket
  roundtrip via a _TestDaemon helper — connect + correlate request/
  response, event forwarding to the DaemonBus, malformed-line skip,
  daemon-disconnect failing pending requests, stop cleanup, dispose,
  connect-failure-then-reconnect, daemon-sent-Request warn-and-skip,
  DaemonConnectionChanged emission.

Fix in lib/kernel/src/ipc/client.dart: _setConnected now skips
notifyListeners / event emit when _disposed. The socket stream's
onDone can fire after dispose runs, which previously hit
ChangeNotifier's "used after disposed" assertion. State flip stays
unconditional so stop()'s explicit transition still works.

Coverage: ipc/client.dart 14% -> 92% (79/86; remaining 7 lines are
the socket onError callback + 1 const ctor phantom); events/types
.dart 95% (37/39 — 2 const-ctor phantoms); events/message_bus.dart
100%; events/bus.dart stays 100%.

Total coverage 71.93% -> 73.34%; floor bumped to 73.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:06:17 +02:00
jpmschweitzerandClaude f90ddc345f update decisions/ → governance/ refs after D-21 migration
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 1m1s
Stale path references from the move in 63195d1:
- CLAUDE.md: 8 D-record links, the layout tree, the open-questions
  pointer, all rewritten to governance/.
- docs/design/multitab-pane.md + docs/claude-design/README.md:
  cross-references updated.
- lib/clide.dart: doc-comment refs.
- lib/builtin/problems: user-facing message string.
- Makefile: decisions-validate target docstring.

Note: lib/builtin/decisions/ (the in-app decisions panel package)
keeps its name — it's a feature name, not a filesystem-path mirror.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-11 18:38:39 +02:00
jpmschweitzerandClaude Opus 4.7 94ff623708 remove dead Scrollable-era widgets + simulateScroll param
Three widgets under lib/src/terminal/src/ui/ and one TerminalView
parameter were leftovers from the era when TerminalView wrapped its
viewport in a real Scrollable. The Scrollable path was replaced with
PointerScrollEvent → PgUp/PgDown translation (alive, well-tested in
terminal_view_test.dart); these helpers stayed behind with tests but
zero production callers.

Drops:
- TerminalScrollGestureHandler (scroll_handler.dart, 100 LOC)
- InfiniteScrollView (infinite_scroll_view.dart, 117 LOC)
- KeyboardVisibilty (keyboard_visibility.dart, 59 LOC) — last
  production caller was removed in 048e835
- TerminalView.simulateScroll parameter — declared, never read
- The matching test groups + imports in ui_widget_test.dart
- The KeyboardVisibilty export from the terminal barrel

Net: -585 lines from lib/ + test/, no behavior change, and
infinite_scroll_view.dart stops being the 90%-coverage outlier we
were apologising for in the previous test sweep.

Same shape as T-93 (dead onTapUp wiring), T-95 (dead tertiary tap),
and 048e835 (dead scrollController plumbing). Public-or-tested
surface that no caller exercised.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:20:50 +02:00
jpmschweitzerandClaude Opus 4.7 048e835b31 remove dead Scrollable plumbing from TerminalView
TerminalView's build tree doesn't wrap content in a Scrollable —
scroll is handled by translating PointerScrollEvent into PgUp/PgDown
keyInput. The ScrollController parameter, _scrollableKey, internal
_scrollController, _scrollToBottom helper, and its five call sites
were all dead: _scrollableKey.currentState was always null because no
Scrollable in the tree carried the key, so _scrollToBottom's jumpTo
never fired.

Drops:
- public scrollController parameter on TerminalView
- _scrollableKey + _scrollController fields
- the didUpdateWidget swap block and dispose call
- _scrollToBottom + the five call sites
- KeyboardVisibilty wrapper (its only callback was _scrollToBottom,
  now a no-op; the widget remains a reusable primitive under ui/ for
  future use)
- the matching tests in terminal_view_test.dart

Same shape as T-93 (dead onTapUp wiring) and T-95 (dead tertiary tap
surface) — public API that no caller used + internal state that no
path executed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:12:23 +02:00
jpmschweitzerandClaude Opus 4.7 0a43a1f7d2 remove dead tertiary-tap surface (T-95)
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 1m1s
The middle-click ("tertiary tap") path in TerminalGestureHandler was
wired wrong: build() bound onTertiaryTapDown to the secondary state
method, so a middle-click fired as if it were a right-click. The
state's onTertiaryTapDown/Up methods were unreachable, and the
onTertiaryTapUp body had a copy-paste bug (button=right instead of
middle). No production caller passed onTertiaryTapDown / onTertiaryTapUp
through, and TerminalView didn't expose them either, so the public
parameters were dead too.

Drops both layers of dead surface — option B of T-95. Same shape as
T-93's resolution (delete unused, restore later when a real consumer
needs it). Also collapses the unreachable onDragStart selectWord
branch (PanGestureRecognizer is mouse-only, so the touch path can't
fire) into a single selectCharacters call with a comment.

Companion: refines the reflow-padding test in coverage_trivials_test
to use narrow→wide reflow setup (more honest about intent, also
actually exercises the padding branch — Buffer.resize now 100%) and
clears two unnecessary_import warnings surfaced by the deletion.

Coverage: gesture_handler 55/59 -> 59/59; gesture_detector 50/50;
buffer/buffer 260/261 -> 261/261.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:56:24 +02:00
jpmschweitzerandClaude Opus 4.7 eb32422e05 drop dead pql.decisions.coverage IPC command
The 'pql decisions coverage' subcommand was removed in pql 1.4.x — the
replacement is the 'coverage_gaps' field on 'pql plan status'. No
production callers used this IPC surface; only the unit test referenced
it.

Removes the IPC registration, the PqlClient helper, and the matching
test case. Net negative LOC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:03:40 +02:00
Jeroen SchweitzerandClaude Opus 4.6 a6eca2561b remove dissolved daemon, retire ptyc, fix golden cross-platform
Complete three overdue cleanups discovered during macOS health check:

D-56 daemon dissolution: delete bin/clide.dart, DaemonServer,
and orphaned tests (test/cli/, subprocess_test, in_process_test).
Update stale "clide --daemon" references in i18n catalogs, error
messages, editor_commands, CI scripts, and decision records.

ptyc retirement: delete ptyc/ source tree, PtySession, scm_rights.
Remove from Toolchain resolution, ToolCheck gate, backend
serialization, testmode harness, Makefile, CI, and sandbox
entitlements. PTY spawning uses NativePty (Dart FFI forkpty) since
the terminal was absorbed in-tree. D-5 amended.

Golden tests: wire the existing but never-applied clideGoldenConfig
via flutter_test_config.dart. Disable CI goldens (Skia anti-aliasing
differs between macOS/Linux even with Ahem). Keep platform-keyed
goldens only — goldens/linux/ and goldens/macos/ each run on their
own OS.

Test suite: 826 pass, 0 fail on macOS (was 829 pass, 11 fail).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 18:40:01 +02:00
jpmschweitzerandClaude 6b7290dc42 fix dead-wired TerminalView.onTapUp callback (T-93)
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 59s
`TerminalView.onTapUp` was documented as "Callback for when the
user taps on the terminal" but was wired to a code path nothing
ever invoked:

- `TerminalView.build` passed it via `onTapUp:` to
  `TerminalGestureHandler`,
- which forwarded via `onTapUp:` to `TerminalGestureDetector`,
- whose `onTapUp` field was declared and accepted but never called
  by `_handleTapUp` (which only fires `onSingleTapUp`).

Net: every caller that registered an `onTapUp` callback on
`TerminalView` got silent failure. zero in-tree callers depended
on it (clide_pty_view.dart is the only TerminalView callsite and
doesn't pass any tap callback), but the public API said one thing
and did another.

Fix: wire `_onTapUp` (the cell-resolving state-method) through the
detector's working `onSingleTapUp` slot. The user-facing semantics
("fires on confirmed single tap with the resolved cell offset")
match the only sane interpretation of the docstring, and don't
overlap with the existing `onSecondaryTap*` (which were already
correctly wired through TapGestureRecognizer's secondary callbacks).

Also drops the dead surface that surfaced the bug:
- `TerminalGestureHandler.onTapUp` parameter + field — no caller
  passes it after the fix; was only used to forward into the dead
  detector field.
- `TerminalGestureDetector.onTapUp` parameter + field — never
  invoked by `_handleTapUp`. Pure dead code.

Tests: extends `terminal_view_test.dart` with a primary-tap
regression case + paired tests for selection-clearing and
secondary-tap callback routing. The double-tap recognizer's
300 ms timer is flushed via `pump(const Duration(seconds: 1))`
(pumpAndSettle waits for animations, not arbitrary timers).

Coverage delta:
- terminal_view.dart: 151/188 → 180/188 (95.7%; remaining gaps
  are IME `_onComposing`/`_onEditableRect`/`_onKeyboardShow`
  body branches that need deeper IME mocking).
- gesture_handler.dart: 18/60 → 39/59.
- gesture_detector.dart: 30/50 → 42/50.
- Total project: 65.76% → 66.97%; coverage_floor 65 → 66.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-07 08:49:09 +02:00
jpmschweitzerandClaude 4642a2f25b test sweep: cover Terminal orchestrator (T-91)
Adds test/terminal/terminal_test.dart — 54 unit tests covering the
`Terminal` class as a pure-Dart orchestrator: construction +
TerminalState defaults, the Observable mixin, write/writeChar,
keyInput / charInput / textInput / paste (with bracketed-paste +
ctrl/alt encodings, including macOS reservation), mouseInput
gating, resize (clamping + onResize callback + alt-buffer
scrollback clear), buffer switching (use{Alt,Main}Buffer +
clearAltBuffer), every SBC handler (bell / backspace / lineFeed /
CR / SO / SI / unknown), tab-stop manipulation (tab jump +
saturation, clearTabStopUnderCursor, clearAllTabStops, setTapStop),
every ANSI escape handler (save/restore cursor, index, nextLine,
reverseIndex, designateCharset), CSI cursor + erase + line/char
insert/delete + scroll + repeatPreviousCharacter (incl. no-op when
no preceding char), device-attribute and status reports, every
mode setter mirroring into its getter, every SGR set/unset attr +
colour setter, OSC handlers (setTitle / setIconName / unknownOSC),
and all the documented no-op fallbacks (unknownSBC, unkownEscape,
unknownCSI, setUnknownMode, setUnknownDecMode, setColumnMode,
unsupportedStyle).

Also fixes a real production bug surfaced while writing tests:
`BufferLine.eraseRange(0, 0, ...)` panicked with a `RangeError`
because the right-side wide-char guard read `_data[-1]` via
`getWidth(end - 1)` when `end == 0`. The left guard already had a
`start > 0` check; the right guard was missing the symmetric
`end > 0`. Real trigger path: `Terminal.eraseDisplayAbove`
(`ESC[1J`) with the cursor at column 0 — common after `ESC[H\x1b[1J`
home-then-erase-above sequences that many TUIs emit on redraw.
Regression test added in line_test.dart.

Coverage delta:
- terminal.dart: 0/283 → 291/291 (file grew by 8 LF for the
  fix's comment lines).
- base/observable.dart: 0/7 → 7/7 (covered transitively via
  Terminal's listener tests).
- Total project: 56.40% → 59.82%; coverage_floor bumped 56 → 59.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-07 08:21:18 +02:00
jpmschweitzerandClaude 6008b4914c test sweep: cover utils/ + core/mouse/ (T-91)
Adds two test files closing the last two pure-Dart sub-areas in
`lib/src/terminal/src/`:

`test/terminal/utils/utils_test.dart` — 51 tests covering:
- Ascii.isNonPrintable (control chars + DEL).
- hashValues (every optional arg slot, 2..20 inclusive) +
  hashList (including the documented zero-on-empty short-circuit).
- ByteConsumer (single-block + cross-block consume, peek's
  consume-rollback path, rollback within block + across consumed
  blocks, rollbackTo, unrefConsumedBlocks, reset).
- IndexAwareCircularBuffer (push/trim, pushAll, pop, [] / []=,
  clear, forEach, remove with count clamp / no-op, insert in the
  middle / at end / at full ring, insertAll, trimStart, replaceWith
  with truncate-from-head, swap, maxLength setter incl. error +
  no-op cases, debugDump, IndexedItem mixin attach/detach/index).
- UnicodeV11.wcwidth (control chars, printable ASCII, DEL+C1,
  combining marks BMP+high-plane, wide chars BMP+high-plane,
  unmapped high-plane default, version field).
- The push wrap branch where _startIndex resets to 0 after a full
  revolution (last circular_buffer line).

`test/terminal/mouse/mouse_test.dart` — 21 tests covering:
- TerminalMouseButton ids + isWheel for the 7 enum values.
- MouseReporter for all four MouseReportMode shapes (normal with
  >223 null-byte clamp, utf with the 2015-limit clamp, sgr's M/m
  pair, urxvt's button+32 / 3-on-up encoding).
- TerminalMouseEvent constructor.
- CascadeMouseHandler first-non-null semantics.
- ClickMouseHandler — only fires on clickOnly + down + button id
  < 3; null otherwise. UpDownMouseHandler — fires on every
  upDownScroll* mode; drops wheel-up; passes wheel-down. Default
  defaultMouseHandler routes through both.

Coverage delta:
- utils/ascii.dart: 0/2 → 2/2.
- utils/byte_consumer.dart: 28/42 → 42/42.
- utils/circular_buffer.dart: 71/130 → 130/130.
- utils/hash_values.dart: 16/36 → 36/36.
- utils/unicode_v11.dart: 16/27 → 27/27.
- core/mouse/handler.dart: 3/34 → 34/34.
- core/mouse/reporter.dart: 0/17 → 17/17.
- (utils/char_code.dart, utils/lookup_table.dart already at 100%
  from earlier reflow + parser work; mouse/button.dart,
  mouse/button_state.dart, mouse/mode.dart are pure enums with no
  executable lines.)
- Total project: 54.62% → 56.40%; coverage_floor bumped 54 → 56.

Two pre-existing `// ignore_for_file: constant_identifier_names`
suppressions that lacked documented reasons get inline
justifications:
- `lib/src/terminal/src/utils/ascii.dart` — RFC 20 / ISO 646
  control-character names; lowerCamelCase would diverge from every
  spec/man-page reference.
- `lib/src/terminal/src/utils/unicode_v11.dart` — Unicode 11
  wcwidth tables vendored as-is; future re-vendoring stays a
  verbatim paste.

Both fall under the same "FFI / spec-shaped names" pattern as the
libc.dart suppression (D-66 era).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-07 08:13:07 +02:00
jpmschweitzerandClaude 6caa82597e fix dangling tail anchors in reflow on partially-filled lines (T-92)
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 1m0s
`_LineReflow._addPart`'s post-loop block reparents anchors past the
source line's trimmed content onto whatever `_builder._result` was
active at that moment. When no further content lands in the builder
(non-wrapped lines, or the last logical line of a wrapped run),
`finish()` was emitting only when `_builder.isNotEmpty` — leaving
the empty result line with the reparented anchor unappended. The
anchor then pointed to a `BufferLine` that the reflow output never
included, `lines.replaceWith(reflowResult)` discarded it, and
`CellAnchor.attached` returned false. The selection controller's
`extent.attached` null-check then dropped the selection silently
on resize.

The fix adds a `_LineBuilder.hasAnchors` getter and uses it in
`finish()` so the builder line is also emitted when it's carrying
an anchor — even when otherwise empty. Trade-off: an extra trailing
line in the reflow output when (and only when) a tail anchor would
have dangled. `Buffer.resize` already pads the result to `newHeight`
afterward, so for the common case (resize fits inside view height)
the total ring length is unchanged; only when the result already
meets / exceeds `newHeight` does the buffer grow by one. Acceptable
in exchange for selections surviving a width change.

User-visible trigger paths:
- `SelectAllTextIntent` (Ctrl+A) creates an end anchor at
  `x = viewWidth` on the last buffer line — exactly the past-
  trimmed-length position. Resizing narrower while the selection
  was active dropped it.
- Mouse drag selections past the end of a partially-filled line
  hit the same shape.

Tests:
- The pre-existing `reflow anchors on the source line tail (past
  trimmedLength) get reparented` test was originally written to
  document the buggy behaviour ("anchor moves off the source onto
  a dangling builder line"). Updated to assert the post-fix
  contract: `out.contains(tail.line)` is true.
- New `SelectAllTextIntent-shaped end anchor survives shrink`
  regression test that mirrors the actual production trigger
  (anchor at `x = viewWidth` on a partially-filled line, narrower
  reflow).

reflow.dart 71/71 → 72/72 (the new getter is a one-liner). Project
coverage 54.62% unchanged within rounding.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-07 07:58:44 +02:00