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>
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>
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>
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>
_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>
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>
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>
`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>
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>
`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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Adds test/terminal/core/core_test.dart — 36 unit tests across the
small standalone files that sit directly under
`lib/src/terminal/src/core/*.dart`:
- CellData (constructor + empty + getHash + toString),
- CursorStyle (default ctor, every set/unset attr getter pair, all
three colour-mode setters per channel, reset, the .empty
singleton) + CursorPosition,
- Charset (translate, designate/use, save/restore, asciiTranslator,
decSpecGraphicsTranslator with in-table, out-of-table, and high-
codepoint paths),
- TabStops (default 8-column grid, find with empty-range / out-of-
bounds / no-stop-in-range cases, setAt/clearAt/clearAll/reset),
- reflow (empty input, single-line passthrough, grow, shrink-with-
split, wrapped-run continuation, wide-char boundary on the new
width, inner wide-char clamp during _addPart, anchor reparent on
the main path, anchor reparent past trimmedLength).
Two source-side cleanups folded in:
- `CursorStyle.isItalis` was a defined-but-never-called getter
with a typo. No external callers reference it; renamed to
`isItalic` in the same change as the test that exercises it.
- `_LineBuilder.isEmpty` in reflow.dart was dead — the only callers
use `isNotEmpty` or check `_lines.isNotEmpty` separately.
Removed.
Coverage delta:
- cell.dart: 3/7 → 7/7.
- charset.dart: 12/25 → 25/25.
- cursor.dart: 2/62 → 62/62.
- tabs.dart: 0/23 → 23/23.
- reflow.dart: 24/72 → 71/71 (file shrank by one line after the
isEmpty getter removal).
- Total project: 52.72% → 54.62%; coverage_floor bumped 52 → 54.
Note for follow-up (not blocking): the post-loop "anchor.x >= to"
branch in reflow's `_addPart` reparents anchors past trimmedLength
onto whatever builder line is active at that moment. If no
subsequent content is added (no wrapped continuations after the
last shrink iteration), that builder line is never emitted by
`finish()` and the anchor lands on a dangling reference. The path
is exercised by the new test, but the contract it implements is
arguably broken — anchors that should follow the source content
end up off the visible buffer. Worth a separate ticket if real
terminals trip it.
Co-Authored-By: Claude <noreply@anthropic.com>
test / unit + widget + golden + a11y (push) Failing after 35s
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
Clears the 19 misc lint hits left after the test_app print sweep
+ libc.dart suppression. By rule:
- `withOpacity(α)` → `withValues(alpha: α)` (deprecated_member_use)
in `painter.dart:187` and `terminal_view.dart:318`.
- `Pointer.elementAt(n)` → `Pointer + n` (deprecated_member_use)
in `native_pty.dart:306` and `session.dart:187`.
- Brace single-statement for/if bodies in `native_pty.dart`
(×3) and `decisions_view.dart` (curly_braces_in_flow_control_
structures).
- `IsolateClient` and `InProcessClient` constructors switched to
`super.log` / `super.events` parameters (use_super_parameters);
associated unused imports of `kernel/src/log.dart` and
`kernel/src/events/bus.dart` removed in the same files.
- `InProcessClient._dispatcher` field + getter/setter pair folded
into a single mutable public `dispatcher` field
(unnecessary_getters_setters).
- `_buildDispatcher` local in `lib/main.dart` renamed to
`buildDispatcher` (no_leading_underscores_for_local_identifiers).
- `_onTapDown(_)` in `terminal_view.dart` typed as
`TapDownDetails _` (strict_top_level_inference).
- `operator []=(...)` in `circular_buffer.dart` given an explicit
`void` return type (strict_top_level_inference).
- `CustomKeyboardListener` and `TerminalGestureDetector` callsites
reordered so `child:` lands last (sort_child_properties_last).
- `CustomTextEdit` constructor declared `const`
(prefer_const_constructors_in_immutables).
- `LinkedHashMap<K, V>()` in `paragraph_cache.dart` collapsed to a
`<K, V>{}` literal (prefer_collection_literals); the now-unused
`dart:collection` import dropped.
Project analyze: 19 → 0 issues. `make test` stays green; coverage
unchanged at 52.72%.
Co-Authored-By: Claude <noreply@anthropic.com>
`lib/src/pty/ffi/libc.dart` carried 34 analyze infos:
- 26 × `non_constant_identifier_names` on struct field names
(`msg_name`, `iov_base`, `msg_controllen`, etc.) that map 1:1
to POSIX (`man 2 socketpair`, `recvmsg`, `iovec`, `msghdr`).
- 8 × `library_private_types_in_public_api` on the C / Dart
function-signature typedefs (`_SocketpairC`, `_SocketpairDart`,
etc.) consumed only by the `lookupFunction<...>()` calls in
this same file.
Renaming the field names to lowerCamelCase would diverge from the
spec the file documents itself against; promoting the typedefs to
public would just add noise to the import surface. This is the
textbook FFI-binding case where the lints work against the file's
purpose.
Adds a file-wide `// ignore_for_file:` directive — explicitly
approved per the no-lint-suppression rule, with the reason
written inline above the directive so a future reader can
re-evaluate it.
Project analyze drops 65 → 31 issues.
Co-Authored-By: Claude <noreply@anthropic.com>
12 imports flagged by `unnecessary_import` because the symbols
they bring in are also re-exported by the umbrella import already
present in the same file:
- bin/clide.dart: src/git/client.dart, src/pql/client.dart
(covered by package:clide/clide.dart).
- lib/builtin/decisions/, lib/builtin/tickets/ (4 files):
kernel/src/events/message_bus.dart (covered by kernel.dart).
- lib/kernel/src/ipc/in_process.dart: src/daemon/dispatcher.dart
(covered by clide.dart).
- lib/main.dart: kernel/src/toolchain.dart (covered by kernel.dart).
- test/builtin/ipc_status/widget_test.dart:
builtin/ipc_status/src/status_item.dart (covered by
ipc_status.dart).
- test/daemon/{git,pql}_commands_test.dart: src/git/client.dart and
src/pql/client.dart (covered by clide.dart).
- test/widgets/multitab_pane_test.dart: widgets/src/icons/x.dart
(covered by widgets.dart).
Mechanical change — every removed line was already a no-op for
symbol resolution; the umbrella imports define the public surface
each file is actually using.
Co-Authored-By: Claude <noreply@anthropic.com>
`lib/test_app.dart` printed [testmode] / [testmode:json] lines via
the bare `print` builtin, which tripped the `avoid_print` analyze
rule 38 times — by far the loudest source of analyze noise in the
tree.
Routes everything through a `Logger()` instance held on
`_ClideTestAppState`, with a small `_say(msg)` helper for
human-readable lines and a separate `'testmode:json'` source for
the structured summary the harness greps. The default Logger sink
is stderr; `make run-testmode` already pipes `2>&1`, so the
existing `grep -q '"failed":0'` check is unaffected.
Also drops the now-redundant kernel sub-imports (events/bus,
events/types, log, toolchain) — `kernel/kernel.dart` re-exports
them, and the analyzer flagged the doubles as unnecessary.
Project analyze: 107 → 65 issues. test_app.dart is now clean
(0 issues, was 42).
Co-Authored-By: Claude <noreply@anthropic.com>
The file's only purpose is to expose the default-keytab string
constant, but it carried a `void main()` at the end that parsed
that constant and printed the result. That entry point:
- doesn't belong in `lib/` (Dart entry points live in `bin/` or
`tool/`),
- pulls in `keytab_parse` and `keytab_token` imports that are
unused everywhere else in the file,
- emits one of the pre-existing `avoid_print` analyze infos,
- only ever ran when a contributor manually invoked
`dart lib/src/terminal/src/core/input/keytab/keytab_default.dart`,
which the build never does.
Removing it unblocks the file from the coverage report (no
executable lines remain, just the string constant), drops the
unused imports, and shaves an analyze info off the pre-existing
total. If the dump-to-stdout helper turns out to be useful again,
the right home is a `tool/dump_keytab.dart` outside the package's
runtime surface.
Co-Authored-By: Claude <noreply@anthropic.com>
Four `throw` sites in `core/input/keytab/` were unreachable through
the public API:
- `keytab_token.dart` `_parseKeyboardNameDefine` and `_parseKeyDefine`
each tested `reader.readString() == 'keyboard'` / `'key'` after
the caller in the same file (`tokenize`) had already gated entry
on `_isKeyboardNameDefine` / `_isKeyDefine`. Both checks
redundantly re-derived a fact already established a function
call earlier; the `else { throw }` was dead code.
- `keytab_parse.dart` `_parseName` and `_parseKeyDefine` checked
the first token's type, but `addTokens` only delegates to those
functions after `peek().type` matches the expected kind. Same
pattern: the throw protects an invariant the caller already
enforces.
Surfaced while bringing `core/input/` to ~100% coverage. Per the
"near-perfect discipline" / "no carve-outs" rules, dead defensive
code is cleaned, not skipped — the surrounding callers in the same
file are tight enough that introducing a real callsite gap would
be a localised and obvious bug, not a silent failure rescued by
these guards.
The two `else`-throw sites in keytab_token.dart fold into a single
unconditional `reader.readString()` (consume the leading word) +
`yield` of the matching token type. The two type-check throws in
keytab_parse.dart fold into an unconditional `reader.take()` to
skip the already-validated token.
All public-API ParseError paths exercised by `core/input/`'s
unit tests still throw correctly — they're guarded by the second
check in each function (the action-token type check after
modeStatus loops, and the input-token check in _parseName).
After cleanup:
- keytab_token.dart: 80 / 80
- keytab_parse.dart: 63 / 63
Co-Authored-By: Claude <noreply@anthropic.com>
`_csiHandleSgr` carried a `// ignore: dead_code` directive with the
note "workaround for a bug in the analyzer". Re-running the
analyzer with the suppression removed produces no warning — Dart's
flow analysis has caught up since the comment was added.
Per the no-lint-suppression rule the suppression needed to be
either removed or given a more substantive justification; the
analyzer's silence makes the call easy.
Co-Authored-By: Claude <noreply@anthropic.com>
`removeCells`, `insertCells`, and `dispose` each iterate over
`_anchors` while invoking `anchor.dispose()` on entries inside the
loop — but `dispose()` removes the anchor from the same list, which
shifts later indexes left and causes the for-loop to skip them.
Symptoms (no user-facing report yet, but real correctness bug):
- After `removeCells` with multiple anchors past the start, anchors
that should be repositioned were silently left at their old `x`.
- After `insertCells` with anchors getting pushed past `_length`,
ones meant to be disposed could survive.
- `BufferLine.dispose` would throw `ConcurrentModificationError` as
soon as more than one anchor was attached.
Fix: iterate `_anchors.toList()` (a snapshot) in all three sites.
Cheap, safe, and matches the expected anchor-management semantics.
Surfaced by the unit tests added under T-91; that commit covers the
fix with regression tests.
Co-Authored-By: Claude <noreply@anthropic.com>
Bold attributes from terminal escapes now render in a real bold
weight instead of being silently flattened.
- pubspec.yaml: register JetBrainsMono Bold + BoldItalic at
weight 700 under family JetBrainsMono. Files already shipped on
disk; only the registration was missing.
- assets/licenses.yaml: bump JetBrainsMono weights_bundled to
[Regular, Italic, Bold, BoldItalic] per D-42 (the entry must
match what is actually wired into the family).
- lib/src/terminal/src/ui/painter.dart: revert the `bold: false`
override and drop the workaround comment. Bold now flows from
CellFlags.bold to TextStyle.fontWeight.
- test/terminal/painter_bold_metrics_test.dart: load Regular and
Bold via FontLoader and assert paragraph maxIntrinsicWidth is
identical (cell-grid drift = 0). JetBrainsMono Bold's monospace
by spec; this test is the canary for the day someone swaps the
font.
- test/goldens/goldens/{ci,linux}/clide_button.png: regenerate.
ClideButton's label renders slightly heavier on the bold variant
(expected — 0.28% pixel diff before regen).
Earlier perception of over-bolding in the Claude pane was
synthetic-bold smearing (Flutter overpaints when no Bold.ttf is
registered for the family), not legitimate bold rendering. Visual
A/B confirms a real Bold face renders crisp emphasis without the
smear, so no per-pane renderer config is needed.
Co-Authored-By: Claude <noreply@anthropic.com>
`mouse/button.dart` and `mouse/button_state.dart` were imported but
nothing in terminal_view referenced their symbols — analyzer
warnings, not infos. Removed.
Probable origin: a half-landed mouse-forwarding refactor (the actual
work is now scoped under T-74); the imports can come back when the
real wiring lands. Removing them in the meantime keeps the analyze
gate clean.
Co-Authored-By: Claude <noreply@anthropic.com>
Mechanical `dart format` sweep across files that drifted from the
formatter's output (mostly trailing-comma and line-wrap differences
from a Dart SDK / formatter version bump). No semantic changes.
Caught because the pre-push gate now actually fires.
Co-Authored-By: Claude <noreply@anthropic.com>
Eight unresolved doc references and broken README-rewritten links
that surfaced under `dart doc --validate-links`:
- Library-scope refs `[spawn]`, `[openProject]` qualified to
`[Backend.spawn]` / `[Backend.openProject]`; same treatment for
`[resolvePaths]` / `[applyResolved]` on Toolchain.
- `[D-41]` was a decision ID, not a Dart symbol — drop the brackets.
- `[from]` from I18n.interpolated qualified to `[I18nReplacer.from]`.
- `[DefaultSurfaceMap]` was a stale name (private `_defaultSurfaceMap`
in resolver.dart); switch to backticked path reference since
dartdoc can't link private members.
- `[icons/]` was a directory, not a symbol; backticked path.
- README links to `legacy/`, `docs/initial-plan.md`, `decisions/`,
`LICENSE` rewritten as absolute github.com/postmeridiem/clide URLs
so dartdoc stops re-rooting them into the doc tree.
Co-Authored-By: Claude <noreply@anthropic.com>
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>
Adds lib/builtin/claude/src/tmux_session.dart with helpers for the
clide-socket tmux server: killSession, listClideSessions,
reapSecondaries, killAllForRepo. The runner is overrideable via a
TmuxRunner typedef so tests don't shell out for real.
Wires ClaudePane.dispose() to call killSession(sessionName) for
secondary panes. Primary panes are left alone — D-41 keeps the
primary's tmux session alive across clide restarts so the next
launch re-attaches via `tmux new-session -A`.
Imports the helpers in the Claude extension as groundwork for the
app-shutdown reap and the existing claude.kill-all-sessions
command — wiring those uses lands separately.
Co-Authored-By: Claude <noreply@anthropic.com>
Adds lib/widgets/src/spacing.dart with three categories of named
constants — insets (clideInsetHairline / Tight / Icon / Standard /
Text), gaps (clideGapTight / Standard / Section / SectionLarge /
Major / Column), and sizes (clideIconMicro / Caption / Standard /
HitTarget / Emphatic, clideControlHeight).
Migrates MultitabPane to consume the constants and updates the
ui-design geometry reference to point at them. Inline pixel
literals in widget code were drifting (12 here, 6 there, 28
elsewhere) — pulling them through named symbols makes the
"uniform inner spacing" rule enforceable instead of eyeballed.
Co-Authored-By: Claude <noreply@anthropic.com>
ClaudeSessionHost replaces its bespoke tab strip / add button /
close handler with a MultitabPane<_Session> in keepAlive mode.
The primary tab is seeded as non-closeable and non-reorderable
per D-41; secondaries spawn via the existing addSecondary()
entry point and gain drag-to-reorder for free.
Drops ~100 lines of custom _TabRow / _Tab / _AddButton code in
favour of the shared widget. Behaviour is preserved: primary
persists across clide restarts, secondaries can be closed, and
PTY state survives tab switches because IndexedStack keeps every
ClaudePane mounted.
Co-Authored-By: Claude <noreply@anthropic.com>
Adds keepAlive: when true, all entry bodies stay mounted via
IndexedStack so switching tabs doesn't tear down their state.
Hosts that own PTY-backed sessions or any long-lived widget
state opt in; callers that want fresh state on each switch use
the default single-body mode.
Polishes the tab strip itself for production use:
- bottom divider so the strip visually anchors to the body below
- Column.crossAxisAlignment.stretch so the strip fills the pane
width instead of sizing to its content
- close button: replace the text × glyph with the CloseIcon
painter (clean cross strokes, font-independent)
- two-column tab layout — Expanded title on the left, fixed
16x16 close button on the right; uniform 12px left padding,
6px right padding to match the 6px top/bottom breathing room
around the close button
Two new widget tests cover keepAlive (state preserved across
switches) and default mode (inactive bodies disposed).
Co-Authored-By: Claude <noreply@anthropic.com>
Each tab is wrapped in a Draggable (when allowReorder is true and the
entry itself is reorderable) and a DragTarget (always — the controller's
barrier logic decides whether the move actually happens). Drops insert
the dragged entry at the target tab's index. A 2px leading insertion
indicator highlights the active drop target.
The widget harness now wraps children in an Overlay so Draggable's
feedback can mount without each test re-wrapping. Sized by the test
view's bounds to avoid disturbing existing tests that query
find.byType(SizedBox).first.
Four widget tests cover the gesture path: drop reorders, pinned
barrier blocks, pinned tabs aren't draggable, and allowReorder=false
disables drag entirely.
Co-Authored-By: Claude <noreply@anthropic.com>
MultitabController<T> is a Flutter-free ChangeNotifier owning the
tab list, active selection, and reorder/close invariants:
- pinned (non-reorderable) entries form barriers that other tabs
cannot cross
- non-closeable entries silently no-op on remove() so hosts don't
need to gate the call site
- closing the active tab falls right, then left, then to null
- duplicate ids are rejected
MultitabPane<T> is the widget shell: a horizontal tab strip
followed by the active entry's body. Active tab gets the
panelHeader background and a panelActiveBorder top accent;
inactive tabs blend into the tab bar. Close × is hidden until
hover. Add button only renders when onAddRequested is wired.
Hosts route the user's add/close intent through callbacks so the
widget stays domain-free — for the Claude pane, add will spawn a
new tmux session and close will kill one. Drag-to-reorder is
controller-side only for now (the gesture wiring lands with T-24).
19 controller tests + 9 widget tests.
Co-Authored-By: Claude <noreply@anthropic.com>
Six common keybindings (Quick open, Command palette, Toggle
sidebar, Toggle context, Switch theme, New Claude session) shown
as a 3x2 grid card spanning the same 850px content column as the
two action columns above. LayoutBuilder gates the card on viewport
height (>640px) so on shorter windows the centered START / RECENT
columns stay the focus and the tips drop out cleanly.
Co-Authored-By: Claude <noreply@anthropic.com>
clide is an IDE for the Claude Code CLI. The previous tagline
"Flutter desktop IDE for Claude Code" overemphasized the host
toolkit (Flutter is implementation detail, immediately obvious to
contributors) and was ambiguous about whether the integration
target is the CLI specifically.
Updates the welcome subtitle (i18n catalog + widget test + view),
the project description in pubspec.yaml, README and CLAUDE.md, the
CLI banner, and the web manifest/index.
Co-Authored-By: Claude <noreply@anthropic.com>
pane.spawn (via PtyException.errno) and editor.open (via
FileSystemException.osError.errorCode) now route ENOENT to
not_found, EACCES/EPERM to user_error with a permissions hint,
EISDIR/ENOTDIR/EEXIST to distinct user-error/conflict, and
EMFILE/ENFILE to tool_error with a "fd limit hit" hint. The
mapping lives in lib/src/ipc/errno_mapping.dart so other handlers
can adopt the same surface as they pick up errno-bearing failures.
Co-Authored-By: Claude <noreply@anthropic.com>
Three hardening fixes:
- 60s per-request timeout (configurable via DaemonServer constructor)
prevents a misbehaving handler from blocking the connection's
read pipeline indefinitely. On timeout the client gets a clean
tool_error response.
- broadcast() and the per-request response writeln are wrapped in
try/catch with stderr logging. Previously write failures silently
dropped clients with no diagnostic; events going missing was
invisible.
- start() probes for a live daemon before unlinking a stale socket.
If something answers within 200ms, refuse to start. Previously
two daemons racing to bind would let the second rip the first's
live socket out.
Co-Authored-By: Claude <noreply@anthropic.com>
NativePty.close() now awaits the reader-isolate spawn, kills the
child first to drive EOF on the master fd, awaits the isolate's
EOF acknowledgement, and only then closes the fd. Previously the
fd-close racing with the polling isolate left a window where the
fd number could be reused and the isolate would briefly target the
wrong file.
Both NativePty and PtySession now surface reader-isolate spawn
errors via the output stream's addError instead of silently
swallowing them.
PtySession.spawn closes the master fd on any post-receive failure,
closes parentSock in finally (was leaking on every spawn), and
kills the ptyc process if recvFd fails.
PtySession._recvFdAsync uses try/finally to close the ReceivePort
and kill the spawn isolate even when Isolate.spawn itself throws.
Co-Authored-By: Claude <noreply@anthropic.com>
forkpty failures throw PtyException with the captured errno
(previously a generic StateError). The spawned child's chdir/execve
failures write a diagnostic line to its slave PTY before _exit, so
the parent's reader sees "exec failed: <path>" instead of an
indistinguishable EOF.
NativePty.write and PtySession.write loop on short writes and throw
PtyException on hard errors (with errno). NativePty.resize sets
_dead on EBADF so subsequent calls short-circuit cleanly.
Co-Authored-By: Claude <noreply@anthropic.com>
Both handlers concatenated the request path onto the workspace root
without validating containment, letting `path: "../../../etc/passwd"`
escape the workspace. resolveUnderRoot normalizes the path and
checks containment under root.absolute.path before any filesystem
access.
Co-Authored-By: Claude <noreply@anthropic.com>
Flutter falls back to synthetic bold when JetBrainsMono-Bold isn't
registered, and synthetic bold drifts glyph advance widths enough
to break the monospace cell grid (cursor block lands between
characters, prompts wrap mid-word). Color is enough to convey
emphasis in TUIs; semantic italic and underline still render.
Also drop the temporary `tmux -L clide kill-server` from the
install target — the rapid-iteration loop is no longer needed.
Co-Authored-By: Claude <noreply@anthropic.com>