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>
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>
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>
Add tests for `keybindings.dart` (KeyEvent → Keybinding mapping,
parse-error edges, resolver entries view), `toolchain_paths.dart`
(the Flutter-free `ToolchainView.resolved` static view), and several
`widgets/src/` primitives: tooltip hover/overlay, palette filter +
submit, multitab controller `copyWith` + size getters, and additional
markdown branches (h3–h6 headings, tables, strikethrough, default
block fallback, record-link tap).
Unfreezes the pre-push coverage floor that was held at 90 on
2026-05-14 by mistake and ratchets to 93. Tidies eight test files
that had accumulated unused imports flagged by `unnecessary_import`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cover the remaining catch / no-op branches in
kernel/src/extensions_manager.dart (duplicate register, unknown
activate, activate-throws, deactivate-throws, keybinding unbind
on deactivate, `all` getter passthrough) and exercise the
SchedulerService event surface (ProjectOpened starts the ticker,
ProjectClosed stops it cleanly). Pushes line coverage to 92.73%.
Co-Authored-By: Claude <noreply@anthropic.com>
One small addition to facade_test: ClideKernel.of called from a
Builder with no ClideKernel ancestor throws the documented
FlutterError. Mirrors the existing ClideTheme.of coverage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three small additions to push coverage toward the 90% target:
- test/widgets/icons_test.dart: one sweep test calling .paint() on
every custom ClideIconPainter (Check, ChevronRight, ChevronDown,
Dot, Folder, Gear, GitBranch, Plug, Search, Terminal, Warning).
- log_test: Logger.trace covered at minLevel.trace + filtered out
at minLevel.info.
- services_bigger_test: DialogRouter.current getter; DialogHost
widget rendered with backdrop + inner builder, then dismissed
through the router. Plus FilesDropped subsystem/kind getters
exercised through the existing notifyDropped test.
Coverage 89.08% -> 89.93%.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test / unit + widget + golden + a11y (push) Failing after 32s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 1m3s
Seven new tests in test/kernel/src/settings_test.dart covering the
SettingsStore branches the existing 6-test suite didn't reach:
- project-scoped set + get round-trip when projectDir is configured
(covers the project-file readFile branch + the project-scope set
path).
- setting a project key without a project throws StateError.
- ext.* keys default to app scope; project overrides app when both
are set (covers the project-overrides-app branch in _lookup).
- setProjectDir(null) clears the in-memory project values.
- YAML emitter handles every scalar branch: bool, num, string
(simple + special-chars + empty), list (mixed types incl null),
and round-trips through reload (covers _emitScalar + _emit
branches for null / List / unknown types).
- load tolerates a malformed YAML file (_readFile catch path).
- load returns empty when file is missing or blank.
Coverage: kernel/src/settings.dart 104/123 -> 112/123 (91%).
Remaining lines are a couple of corner-case formatting paths in
_emit when the top-level value isn't a Map (the public API always
serializes a Map, so they're effectively unreachable through normal
use).
Total coverage 83.85% -> 83.92%.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test / unit + widget + golden + a11y (push) Failing after 31s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 1m2s
Five new tests in test/kernel/src/extensions_manager_test.dart
covering the contribution-type case branches and the _ExtensionContext
passthrough surface that the existing 8-test suite didn't reach:
- TrayItemContribution lands in TrayRegistry on activate; deactivate
removes it.
- StatusItem + ToolbarButton contributions activate/deactivate
cleanly through PanelRegistry.
- LayoutPresetContribution exercises the no-kernel-side-wiring case
branch (consumed by default-layout's own activate()).
- Every _ExtensionContext getter (log / events / messages / settings
/ theme / i18n / panels / arrangement / commands / palette /
clipboard / files / notify / dialog / tray / secrets / os / net /
focus / project / ipc) returns the same instance the kernel
exposes — locks in the passthrough contract.
Coverage: kernel/src/extensions_manager.dart 85/130 -> 122/130
(94%). Remaining 8 lines are the disabled-by-default + persisted-
disabled paths plus the deactivate-during-failed-activate cleanup —
edge cases reachable only when extensions throw.
Total coverage 83.39% -> 83.85%.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test / unit + widget + golden + a11y (push) Failing after 29s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 1m2s
services_bigger_test.dart's openURL + reveal tests called the real
OsBridge methods, which Process.run xdg-open / open / explorer.
On a desktop session the OS dispatcher (KIO on KDE, etc.) surfaces
a "Could not read file …" error dialog for the bogus URL, owned by
the desktop session not the test process — it persists after the
test exits and a fresh run spawns another one. The user has to
manually close each popup, blocking CI from running unattended.
Coverage value was minimal (anyOf(true, false)). Leaving fire() and
the rest of the OsBridge bus-event surface; command-shape coverage
of openURL/reveal can come from integration tests where a real OS
dispatcher is desired.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test / unit + widget + golden + a11y (push) Failing after 31s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 1m3s
18 tests in test/kernel/src/project_test.dart driving ProjectManager
with an injected onValidateProject hook so the manager doesn't shell
out to git in tests. Covers:
- RecentProject: relativePath HOME collapsing, timeAgo bucketing
(minutes / hours / yesterday / days / weeks / months), toJson /
fromJson round-trip, fromJson tolerance for missing fields.
- ProjectManager.open: non-git path (returns false), valid path
(sets current, emits ProjectOpened, registers in recents),
re-open deduplication, recents capped at 10.
- ProjectManager.close: resets current, emits ProjectClosed, no-op
when already closed.
- ProjectManager.loadRecents: happy path, malformed-value tolerance,
no-setting clears to empty.
- ProjectManager.openLast: no-stored, stored-but-missing,
stored-and-exists.
- ProjectManager.resolveProject without injected validator falls
back to Process.run.
Coverage: kernel/src/project.dart 2/72 -> 66/72 (92%). Remaining
6 lines are the _currentBranch helper + ProjectOpened-event-time
branch shake (need a real git in the sandbox to materialise the
branch).
Total coverage 81.64% -> 82.42%; floor bumped to 82.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test / unit + widget + golden + a11y (push) Failing after 31s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 1m2s
Two test files covering the pure-Dart and fallback paths of the
tree-sitter integration. The native-FFI parsing depth stays
untested — DynamicLibrary.open('libtree-sitter.so') doesn't resolve
under the flutter test runner because the bundled lib sits at
native/linux-x64/ rather than on the linker search path. Real
parsing coverage would need an integration-test harness that copies
or symlinks the lib into the runner's working directory.
- test/kernel/src/syntax/language_map_test.dart (6 tests): every
branch of grammarForPath — common extensions, case-insensitive
matching, special filenames (Makefile / Dockerfile / .gitignore /
justfile), no-dot/no-special-match fallthrough, unknown extension.
- test/kernel/src/syntax/tree_sitter_service_test.dart (9 tests):
graceful-fallback paths of TreeSitterService when the library
can't load — hasGrammar / languageFor / highlight all short-
circuit cleanly, plus SyntaxResult.empty and SyntaxSpan field
storage.
Coverage: language_map.dart 0/7 -> 7/7; tree_sitter_service.dart
1/131 -> 22/131 (the rest is deep FFI work); tree_sitter_ffi.dart
0/15 -> 1/15 (init entry only).
Total coverage 75.11% -> 75.42%.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds one test covering the currentLocale / defaultLocale /
availableLocales getters on the I18n class. Existing suite exercised
the lookup behaviour but never read back the three locale fields,
leaving them as the last reachable gaps in lib/kernel/src/i18n/.
Coverage: kernel/src/i18n/i18n.dart 54/58 -> 57/58. The 1 remaining
line is the I18nReplacer const-constructor body (lcov phantom).
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 1m2s
10 small tests in test/kernel/src/theme/mop_up_test.dart covering the
scattered last-mile gaps across the theme subsystem:
- ClideTheme.of and ClideTheme.controllerOf inherited-widget lookups
(both happy path + the FlutterError-throw branch when no ancestor).
- contrast._composite alpha-mix branch via a partially-transparent
foreground in contrastRatio.
- ContrastFailure.toString format with embedded pair name / ratio /
minimum.
- ThemeLoader.fromYamlString FormatException on a non-map root.
- ThemeLoader.fromFile happy path with a tempdir YAML.
- Palette.names and SemanticRoles.roles iterables.
Coverage: theme/controller.dart 37/42 -> 42/42; theme/contrast.dart
54/64 -> 62/64 (remaining 2 lines are the failingPairs add-to-list
path, needs a constructed-broken SurfaceTokens to fire);
theme/loader.dart 48/54 -> 53/54 (1 const-ctor phantom);
theme/palette.dart 9/10 -> 10/10; theme/semantic.dart 2/3 -> 3/3.
Total coverage 74.55% -> 74.79%.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test / unit + widget + golden + a11y (push) Failing after 32s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 1m1s
Two test files / batches:
- test/kernel/src/i18n/catalog_loader_test.dart (11 tests): all three
CatalogLoader implementations end-to-end. AssetCatalogLoader against
an in-memory AssetBundle covers happy path, FlutterError catch
(missing asset), FormatException catch (malformed JSON), blank
asset, non-object JSON. FileCatalogLoader against a tempdir covers
happy path, missing file, malformed, blank. InMemoryCatalogLoader
covers lookup + namespace-miss.
- Extends test/builtin/theme_picker/widget_test.dart with two tests
for the _pick command callback: defensive not-activated branch
(drive the contribution's run() before activate runs, _ctx is still
null) and the dialog-resolution happy path (register + activate,
invoke theme.pick, manually drive dialog.dismiss('forest'), assert
IpcResponse data['selected']).
Coverage: kernel/src/i18n/catalog_loader.dart 14/26 -> 26/26;
builtin/theme_picker/src/extension.dart 9/18 -> 15/18 (remaining 3
lines are the dialog builder body — needs a DialogHost in the test
harness, out of scope).
Total coverage 74.08% -> 74.55%; floor bumped to 74.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test / unit + widget + golden + a11y (push) Failing after 29s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
Revert priority sort in tabsFor() — registration order in
main.dart is the intended sidebar order, not priority. The
priority fields on extensions were dead code.
Remove ptyc from toolchain missing list since NativePty
replaced it.
Co-Authored-By: Claude <noreply@anthropic.com>
tabsFor() sorts by contribution priority when no user order is
set. Test expectations updated for sidebar defaultSize 400 and
decision ID D-1 (no zero-padding). PTY tests tagged forkpty and
run via dart test (forkpty output unreliable inside flutter test
runner). CI script adds --no-fatal-infos and --exclude-tags.
Co-Authored-By: Claude <noreply@anthropic.com>
DaemonBus (was EventBus): typed events for system/IPC layer.
MessageBus: channel-based pub/sub for UI/extension coordination.
Messages carry publisher (auto-stamped from extension ID),
channel (required), timestamp, and payload. Subscribe by
publisher, channel, or both — zero collision across extensions.
Extension context gains publish() and subscribe() convenience
methods that auto-stamp the extension's ID as publisher.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Single Flutter package at the repo root. All code, tests, assets,
and platform directories moved from app/ to root. Package renamed
from clide_app to clide — all imports rewritten. Merged pubspec
combines core (ffi) and app (flutter, yaml, xterm) dependencies.
Makefile simplified: no APP_PRESENT conditionals, no cd, no daemon
lifecycle. 317 tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>