Exercise the previously-untested branches of the settings category renderer:
number-field commit/clamp/unparseable-revert, text-field commit, the file
control's command button, the select raw-value fallback, scope move-to-app,
field help text, and the cross-category search empty state. Restores the
coverage floor after the D-101 facade migration shifted the ratio.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Settings → Appearance gains a Monospace font select (JetBrains Mono / Fira
Mono) on the schema engine, writing app.mono.font. It applies live: root_shell
resolves the family into the ClideSettingsScope (D-101) and rebuilds on the
settings change, so the ~93 migrated mono sites — terminal, diffs, code, IDs —
re-read the new family at once.
Vendor Fira Mono (Regular + Bold, OFL) under assets/fonts/fira_mono/ with the
pubspec font family + licenses.yaml entry per D-42; JetBrains Mono stays the
default.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Vendor Inter (variable + italic, OFL) under assets/fonts/inter/ and make it
the default application UI face, replacing Josefin Sans (which stays bundled
as a selectable option). pubspec font family + licenses.yaml entry per D-42.
Settings → Appearance gains a UI-font select (Inter / Josefin Sans). The root
DefaultTextStyle reads app.ui.font (kUiFontSettingKey) over the default and
re-applies live on settings change, so a pick takes effect immediately. Bump
the default UI weight to w400 — Inter reads better at Regular than Josefin's
Light.
The monospace picker is deferred to T-471: clideMonoFamily is a const at ~38
call sites (not inherited), so a live mono picker needs the family routed
through context first.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A Settings → Claude category sets per-user defaults for NEW sessions: model,
effort, and permission mode (generic schema selects persisting
app.claude.default*). The pane reads them at spawn — effort flows through the
existing --effort flag (SpawnSpec.effort); model and permission mode are sent
as control requests right after a fresh (non-resume, non-fork) session starts.
'default'/unset values are no-ops, leaving the CLI's own defaults.
The optional "settings changed — apply to current sessions?" prompt is filed
as T-470: it needs a custom control (a generic select can't prompt), so it's
a clean follow-up on the T-452 escape hatch.
Tests: defaultEffortFlag sentinel handling; the category contributes the three
default fields. The thin pane-side applySessionDefaults is covered by the gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reflow lines `dart format` collapses — the T-451/T-452 test additions were
committed before `make format` ran, so the committed copies were unformatted.
No behavior change; keeps the push format gate green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Built-in extensions are always on and there's no third-party install path yet,
so the Extensions tab is a notice rather than a toggle list — a toggle list
would let users brick the app by disabling load-bearing builtins. It explains
that installing/enabling/disabling arrives with third-party (Lua) extensions
and points at the records that pick it up (D-16 / T-8).
Fleshes out the builtin.extensions-ui stub via the custom-control hatch. Adds
two renderer guards — empty section/field labels render no chrome — so a
notice-only category shows just its card.
Tests: contributes the Extensions category + notice control; the notice renders.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the one bespoke control the schema engine defers to. New
SettingsControlContribution routes a WidgetBuilder into a kernel
SettingsControlRegistry under a customId; a SettingsFieldKind.custom field
names that id, and the renderer draws the registered widget full-width
(label on top, no scope tag — the control owns its own apply + scope).
The theme-picker extension uses it: an Appearance category whose theme field
is custom, backed by AppearanceThemeControl — base-theme chips + a
high-contrast toggle that apply live through ThemeController (persisted by
theme_persistence). Reuses the shared theme_families helpers.
Tests: control registry (register/dup/unregister), the renderer's custom-field
path, and the Appearance contribution + live theme apply.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The keybindings-ui extension contributes a Keymap SettingsCategory: a preset
select (Default / Vim / VS Code / JetBrains) reading the active preset from
kKeymapPresetSetting. Picking one routes through a new schema affordance —
SettingsField.applyCommandPrefix — running `keymap.preset.<value>`, which calls
KeymapService.setPreset to persist and reload the layered keymap live. The
prefix path keeps the engine generic: settings a subsystem applies via a
command (rather than a bare key write) declare the prefix; the scope tag and
current-value read still use the key.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The first real settings tab. The Claude extension contributes an Activity
SettingsCategory whose fold-level select binds to app.claude.activityFoldLevel;
the activity panes already rebuild off the settings notifier, so a pick applies
live. Proves the schema-driven engine end to end with a production setting —
opening Settings now shows a populated, working category.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A search box (ClideFilterBox) atop the rail filters fields across every
registered category. While searching, the panel swaps to a results view that
groups the matching fields under category subheaders — rendered with the same
carded rows and editable inline — and each rail row shows its match count with
zero-match categories dimmed. Completes the settings-UI infra spine (T-444):
shell, engine, rail, scope tags, search.
Tests: search filters across categories, hides non-matches, and surfaces the
per-category rail count.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each settings field gains a scope tag showing where its value lives — folder
= Project (.clide), globe = Always (~/.clide), circle-dashed = Default/unset —
colour-coded (statusSuccess / statusWarning / muted) with a tooltip. Tapping
opens a menu to move the value between the scopes the key supports, or reset
to default; the tag's menu replaces the interim reset button.
Backs it with scope-explicit SettingsStore access — rawAt / setAt / removeAt /
effectiveLayer / writableLayers — over the two storage files (app ~/.clide,
project .clide). ext.* keys layer project-over-app; app.*/project.* keys live
only in their prefix's file, so their menu offers that one scope + reset.
Tests: store scope ops (layering, reload, guards) and the tag (Default vs
All-clide rendering, menu reset).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The settings modal's left rail now lists the registered categories (icon +
title, data-driven from the SettingsRegistry) with an accent left-stripe +
surfaceHi selection per ui-design surface.md. Selecting a category drives the
modal's selection state and swaps the right panel; the rail scrolls when the
set exceeds the height. The cross-category search box atop the rail lands in
T-450.
Tests: rail lists categories and selecting one swaps the panel.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The core of the settings panel (epic T-444). Subsystems describe a category
as data — a SettingsCategory of carded SettingsSections of SettingsFields
(toggle / select / text / number / opens-external-file), each bound to a
SettingsStore key with help text, a default, and reset-to-default.
Registration is declarative: a new SettingsCategoryContribution carries the
category; the extension manager routes it into a new kernel SettingsRegistry
(exposed on KernelServices), which the panel reads via ClideKernel. Adding a
category is now pure data + a contribution — no widget code.
SettingsCategoryView renders a category into carded sections per ui-design
surface.md: panelHeader card fill, dividerColor border, inputs receding to
panelBackground; select reuses the anchored-overlay menu, text/number commit
on Enter or blur (numeric clamps to bounds). The modal panel now shows the
selected/first registered category, falling back to the empty state.
Tests: registry (sort / dedup / notify), contribution routing on activation,
renderer (render + toggle/select write-through + reset), modal-with-category.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lay the foundation for the schema-driven settings UI (epic T-444). A new
`settings.open` command (⌘`,`, plus a File-menu and command-palette entry)
opens a centered Settings modal over the dimmed app via the dialog router,
built from the modalSurface* tokens (D-7, no Material). The shell frames the
two regions later tickets fill in — the category rail (T-447) and the
scrolling carded panel (T-448) — and dismisses on ✕, Esc, or barrier tap.
With no category registered yet it shows its empty state, which is the
correct runtime state.
Flesh out the `builtin.settings-ui` stub (was 0.0.0-stub) into a real
extension; ship its en-US i18n catalog. Relabel the theme picker's
`theme.pick` command title from "Settings…" to "Theme…" so the two no
longer collide in the palette (the picker folds into the new panel's
Appearance category in T-452).
Tests: command + ⌘`,` binding registered, shell renders, Esc and close
both dismiss.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Under the Vim preset, `:` opens a transient one-line ex overlay running a
fixed v1 table; ZZ runs :wq directly. Completes the last built child of the
T-403 cross-pane vim layer (T-405 part 2 gt/gT still open).
- ExLineController + parseExCommand grammar + editor-targeted executors
(lib/kernel/src/ex_line.dart); the overlay (lib/widgets/src/ex_line_overlay
.dart) reuses the quick-open chrome, mounts in the root_shell Stack, and
publishes the exline.open scope flag. Unknown commands flash + stay open;
with no active buffer every command no-ops (2026-06-13 decision).
- :q closes the active tab via editor.close on its id — the registry promotes
the next buffer and the split self-collapses on the last (2026-06-12
decision); :w/:wq/:x/ZZ save (+close) the active buffer.
- :e <path> seeds quick-open (new QuickOpenController.open(seed:)); :N adds the
editor.goto-line IPC/CLI verb (reuses _offsetForLine). Goto needs caret sync:
EditorController now handles editor.selection-changed and the editor view
moves the caret on a selection-only change.
- `:` and ZZ are typed intents; the editor matcher and PaneKeyNav now bubble
unhandled typed intents to the app-root Actions, so they fire from any focus.
vim.yaml binds `:`, ZZ (shift+z shift+z), and Esc-dismiss.
Tests: parser/controller/executors, editor.goto-line daemon tests,
selection-changed (controller + view), full overlay widget test. make test
green; analyze + format clean.
Also files T-441 (drop bold from the ticket-id card label) and T-442
(sub-agent renders as 3 cards instead of one bundle) under the T-276 UI epic.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
/clear tore the session down and respawned on the same deterministic
--session-id BEFORE the old claude process had actually exited. The
orchestrator's close() ran conversation.dispose() unawaited and kill()
only sent SIGTERM without awaiting exitCode, so the respawn raced a
still-alive holder of the id — claude 2.1.177 rejects it as "Session ID
… is already in use" and exits 1.
Root cause confirmed from clide's own crash log + isolated probes against
2.1.177: the id frees the instant the holder dies (SIGTERM cleans the new
~/.claude/sessions/<pid>.json registry), so awaiting real death is the
fix — and it preserves T-268's deterministic-id continuity (chosen over
minting a fresh id, which would change the continuity model).
- stream_json_session: kill() awaits exitCode (SIGTERM → 2s → SIGKILL →
await); dispose() idempotent (shared cached future); new
SessionEnd.reason getter (last non-empty stderr line, capped).
- session_orchestrator: close() awaits session.dispose() so teardown
returns only once the process is truly dead, before clear + respawn.
- claude_pane: surface end.reason in the status line — no more opaque
"code 1".
- session_naming: correct the stale clearSessionTranscript doc (real
sidecar is the shared memory/ dir) + the await-death precondition.
- tests: close() blocks until process exit; SessionEnd.reason.
CLI 2.1.177 re-probe (folded-in scope): sessions/ registry characterized
(PID-keyed, cleaned on exit); init cache auto-refreshes; advertised
slash_commands show no routing-table drift. No further code change needed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The boot-time toggle (CLIDE_LOG / app.log.level) existed; this makes it
adjustable at runtime, from the two surfaces that fit — D-6 parity — and NOT a
Claude-composer slash command (log verbosity isn't a Claude-session concept).
- Output dock Level chip: was a view-only filter; now also drives the kernel
Logger.minLevel and persists app.log.level, so the choice is real (changes
what's captured) and survives restart. Initialized from the logger's current
level so it reflects a CLI change. A null callback keeps it a pure view
filter (tests / no kernel).
- `clide log level [<level>]` (lib/src/daemon/log_commands.dart): no arg reports
the level + vocabulary; a valid level sets the live logger and persists;
unknown → bad_arg (code 64), logger untouched. Wired in buildDispatcher with
the kernel logger + settings (captured post-boot).
Tested: the command (get/set/case-insensitive/bad-arg, live + persist) and the
controller (initial level, callback fires, same-level no-op, filter-only when
unwired). Coverage gate 95.05%.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
~173 `await Future<void>.delayed(Duration.zero)` async-settle waits across 25
test files yield the microtask queue exactly once; when an event→handler chain
needs more than one hop they lose the race under CI's parallel load, so the
failing set varied run to run. Replace with `await pumpEventQueue()` (the
deterministic drain already used elsewhere in the suite); rewired the shared
settle()/tick() helpers in one shot. menu_bar's toggle-close test gets a bounded
extra pump. Verified green under CI=true.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Nothing has spawned tmux since D-77 moved Claude session persistence to
`--resume`; Claude and terminal panes spawn `claude` or the shell directly.
But the toolchain still resolved tmux and listed it in `missing`, so on
mac/linux a box without tmux showed a spurious "tmux not found" warning in
the welcome view + status bar. The windows-support branch had special-cased
that away with a `!Platform.isWindows` guard — the tell that the requirement
was dead everywhere, not platform-specific.
Drop tmux from ResolvedPaths / ToolchainView / Toolchain (field, getter,
`missing`, PATH resolution) on every platform, removing the Windows guards
with it. Strip the testmode tmux probes and the comments / CLAUDE.md line
that claimed clide spawns tmux. (The dead ToolCheck class that also gated on
tmux was already deleted on main and dropped in the preceding merge.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The structural piece: a global SequenceMatcher in root_shell, at the
HardwareKeyboard level so a focused editor/pane can't swallow the second
chord. It only STARTS on a modified chord that prefixes a sequence (ctrl+w),
so bare-key sequences (gg, dd) stay editor/pane-local and single-chord presets
are untouched; bare ctrl+w still fires editor.close after the D-82 timeout.
vim.yaml binds the window family under vim.normal||vim.visual: ctrl+w h/l →
panel.focus.left/right, j → dock.toggle, w / ctrl+w → focus.nextPanel,
shift+w → focus.previousPanel, o → panel.focusMode, q/c → editor.close.
Tests: ctrl+w sequence resolution at the keymap layer, plus app-level
integration (ctrl+w o toggles focus mode; bare ctrl+w closes the editor after
the timeout; a bare g is not grabbed globally).
This is the global matcher T-405 part 2 (gt/gT) was waiting on — though bare-g
sequences need more thought (g is editor-local), noted for that follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add workspace.tab.next / workspace.tab.previous commands that cycle the
Slots.workspace tab strip with wraparound (no-op under two tabs), bound
ctrl+pagedown / ctrl+pageup across every preset via defaultBindings. Single-
chord, so no global matcher needed. Activating a tab also focuses the
workspace slot.
Part 2 (vim gt/gT) is deferred — it needs the global multi-chord matcher
T-404 introduces. T-405 stays open for that follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The structural T-403 child: make vim normal mode mean navigation in panes
that were mouse-only. The passive global key path can't run multi-chord
sequences (D-82), so each pane hosts its own SequenceMatcher — factored into
a reusable PaneKeyNav that resolves the live keymap and dispatches nav.*
intents while a pane holds focus under the vim preset.
- nav.* intents (down/up/pageDown/pageUp/top/bottom/expandOrRight/
collapseOrLeft/activate) — preset-neutral; vim.yaml binds j/k/ctrl+d/ctrl+u/
gg/G/l/h/[o,enter] under `vim.normal && !editor.focused`.
- The editor publishes an `editor.focused` scope flag from its focus node, so
the same keys stay buffer motions while the editor is focused and become nav
when a pane is — resolved by file order + the guard (no change to the editor
motion bindings).
- File tree: a flattened visible-index selection cursor in FileTreeController
(j/k move, h collapse-or-out, l expand-or-into, o/enter open), with a focus
ring + scroll-into-view.
- Conversation: j/k line-scroll, ctrl+d/u half-page, gg top, G bottom — G
re-arms follow-tail.
Foundation for T-404/T-405/T-407, which build on the per-pane matcher and the
editor.focused guard. Git panel + ticket board list nav deferred to a
follow-up (the ticket says lists can trail). Tests: keymap resolution under
both scopes, PaneKeyNav dispatch, the controller selection model, and
end-to-end key-driven nav in both panes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A `Workflow` tool-use launches its multi-agent run in the background and
returns immediately; the real fan-out arrives out-of-band on stream-json
`type:"system"` task_* events (task_started / task_progress / task_updated /
task_notification) keyed by the launching tool-use id — which clide was
dropping. (Wire shape captured by two live stream-json probes; recorded on
the ticket.)
- workflow_run.dart: a pure, Flutter-free WorkflowRun/WorkflowAgent model
that folds those events (phases, per-agent start→progress→done deltas,
usage) into a snapshot.
- StreamJsonSession recognises the events, accumulates a
Map<toolUseId, WorkflowRun>, and exposes `workflows` + `workflowsStream`.
- A `Workflow` tool-use with a live run renders a dedicated run card —
phase groups, per-agent rows with spinner/check status, usage, and the
script — falling back to the generic tool card pre-progress or on reload.
The run breaks the activity cluster so it's always first-class (like T-342).
- The sidebar Activity tab adds a WORKFLOWS section: one row per run with its
done/total agent count, tinted by running/done state.
Closes T-416 and the T-410 epic (all children done).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The epic's new lib surface dropped coverage to 94.87% (floor 95). Add
the missing tests: the command-bus → _send path end-to-end in the pane
(effort respawn with --effort, invalid-level notice, both pickers,
set_permission_mode write, sidebar navigation messages, /memory
editor.open, /help summary, TUI-only notice without a session write)
and ActivityTabView's USAGE block + placeholder branch. 95.14% after.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
T-413 added 'mcp' (among others) to kClideOwnedCommands, which the
composer unions onto the suggestion list (T-162) — '/m' now yields
[mcp, memory, model], so reaching 'model' takes two arrow-downs. The
test's intent (selection moves; Enter completes, never submits) is
unchanged.
Board: T-158 annotated — /usage is answerable headless on 2.1.175,
unblocking its upstream blocker (see T-415).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Activity tab gains the power-panel's session strip and a usage block:
- SESSION controls (clear / compact / fork / resume + refresh-usage)
publish their slash command on builtin.claude/command — the same path
as typing it, so /clear semantics (and any future confirm behavior)
live in exactly one place.
- The usage block revisits T-158's "blocked on upstream": probed against
claude 2.1.175, a forwarded /usage IS answered headless, free
(num_turns 0), as parseable text. parseUsageText() extracts session /
week / week-Sonnet percentages (timezone parentheticals stripped); the
sidebar watches the primary session's synthetic output for
usage-shaped responses and renders them as a USAGE section. Refresh is
user-initiated (the control sends /usage) — no polling, no background
calls (D-64).
- The runtime row gains the session's effort level (T-412's status
field).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Claude sidebar's settings table was read-only 12px rows. It becomes
the power panel's core:
- model / effort / permission-mode rows are popover controls on the
owned anchored-menu primitive (ClideAnchoredOverlay + ClideMenu),
showing the LIVE session values (SessionStatus, falling back to the
probe/settings) with the active option marked.
- Picking an option publishes the explicit slash command (`/effort
xhigh`) on builtin.claude/command; the PRIMARY pane subscribes and
executes it through the same _send routing the composer uses — the
control and the typed command are one code path (D-6), which is also
what lets the sidebar drive /effort's respawn flow without reaching
into the pane. Only the primary pane listens (controls target the
primary session; a second listener would double-execute).
- Styling pass (user request): shared meta tables move from 12px-
everything to 13px labels/values, accent-coloured section headers,
wider row pitch; control rows get hover affordance + caret.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/permissions, /status, /config, /mcp, /agents, /hooks, /memory, and
/help move from the TUI-only notice catalog to clide-owned commands
with real behavior:
- /permissions <mode> sets the mode over set_permission_mode; bare
/permissions opens a picker in the interaction zone — the same card
/model and /effort use (kPermissionModes, bypass last and explicit
per T-181).
- /status → Claude sidebar Activity tab; /config, /mcp, /agents,
/hooks → Config tab. The pane activates the claude.meta sidebar tab
and publishes a meta.tab message; the sidebar subscribes and switches
its sub-tab — the same MessageBus addressing `clide ui open` uses
(D-6), so the CLI can drive it too.
- /memory opens the workspace CLAUDE.md via editor.open.
- /help renders a local summary card (clide-owned + advertised
commands) — the CLI's TUI help doesn't exist headless.
The catalog keeps empty-hint entries for these tokens as safety nets if
they're ever removed from owned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Spike result (probed claude 2.1.175 over stream-json): there is NO
set_effort/set_thinking_effort control subtype — both are rejected. The
lever is the `--effort <level>` spawn flag (low/medium/high/xhigh/max;
settings.json effortLevel is the persisted default). So changing effort
restarts the process: respawn-with-resume keeps the conversation and
carries the flag — the same continuity /clear and /resume already rely on.
- SpawnSpec.effort → orchestrator appends `--effort <level>`.
- claude_pane: /effort <level> validates and respawns (toast explains the
restart); bare /effort opens a picker; the pane re-applies its effort on
every later respawn. Invalid level → local notice listing levels.
- ModelPickerCard generalised minimally (title + isCurrent predicate) so
the effort picker reuses it; effort needs exact matching because `high`
is a substring of `xhigh` and alias-containment would mis-mark it.
- SessionStatus.effort + StreamJsonSession.noteEffort: the wire never
reports effort, so the spawner records what it set; status/sidebar read
it from the normal status stream.
- Routing: effort moves from the TUI-only catalog to kClideOwnedCommands.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
clide forwards composer input to a headless (stream-json) CLI, where the
TUI's interactive commands don't exist. A known-but-TUI-only command
errored raw ("/x isn't available in this environment", rendered as fake
claude prose); an un-advertised one (e.g. /effort on 2.1.175) was worse —
bracket-pasted to the model as literal text, burning a real turn.
Probed claude 2.1.175 for ground truth: the initialize handshake's
slash_commands advertises skills + the headless builtins only; forwarded
local-command output comes back as an assistant message with model
"<synthetic>"; set_effort is not a control subtype; /usage works headless.
- slash_commands.dart: SlashRoute routing table (owned > advertised >
TUI-only catalog > forward) + kTuiOnlyCommands with clide-native hints
+ tuiOnlyNotice(). One source of truth replacing ad-hoc checks.
- claude_pane._send routes 'unavailable' to a local notice card; nothing
reaches the session.
- transcript_reader: AssistantTextMessage.synthetic ("<synthetic>" model)
so CLI-local output is distinguishable; "<synthetic>" no longer
clobbers the tracked model in SessionStatus (latent /usage bug).
- conversation_view: synthetic output renders as a muted framed "clide"
card (T-306 styling), never coral Claude prose.
- kFallbackSlashCommands trimmed to the genuinely-headless builtin set —
it doubles as the router's advertised fallback, and the old list's
TUI-only entries would have routed to a raw CLI error.
Board (rides this commit): T-414 gains the user's sidebar styling-pass
note; T-416 filed — surface Claude Code Workflow runs in convo/status.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Typed into the conversation view, /model was forwarded to the session's
stdin as message text — the CLI's interactive picker only exists in its
own TUI, so nothing happened. clide now owns it like /clear//resume//fork
(T-156).
/model <name> sends a set_model control_request (verified against
claude 2.1.175: subtype accepted alongside set_permission_mode;
"default" resets to the CLI's configured model) with an optimistic
status merge, rolled back with a toast if the CLI rejects the name.
Bare /model swaps a picker card into the interaction zone (D-78) —
numbers / arrows + Enter / Esc, mirroring the prompt card's shortcuts.
The model list comes from the `initialize` handshake response, which
the session now always sends — the spike verified it is side-effect-
free, and it previously went out only when MCP servers were hosted.
Until the response lands the picker falls back to the stable aliases.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
No production code constructed it since the stream-json pivot (D-77)
— only its own test did. The ClaudeConversation bus-addressing
constants stay; the meta sidebar and team panel host still consume
them for member-status messages. The companion finding — the team
roster surfaces listening to TeamMemberJoined events nothing emits —
is real rewiring work, split out as T-396 (drive the roster from
TeamBroker membership, then delete the ghost event types).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
claude extension lifecycle
make test-integration failed at widget-tree finalization: the
palette's dispose() clears its scope flag, which during app teardown
runs AFTER KernelServices.dispose() has disposed the KeymapService —
notifyListeners asserted. Scope-flag mutations now use the same
fire-and-forget guard SettingsStore established. Same family in
ClaudeConfig: activation's unawaited load() could notify (and start
watchers on) a disposed notifier when a teardown raced it.
The claude extension's activation lifecycle and command success paths
are now exercised end-to-end through the kernel fixture — the file
entered the coverage denominator with the T-391 failure-path tests,
so per the ratchet discipline the rest of it gets covered too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sixteen claude.* handlers reported ok with an `error` field buried in
the payload — `clide claude.agent.set-permission-mode bogus` exited 0,
so scripts could not detect failure, drifting from the D-6 exit-code
contract every other subsystem honors. Missing/invalid args are now
userError, missing sessions notFound, a missing orchestrator
toolError, and a failed task reassign no longer reports ok:false as a
success. No UI consumer read the old payloads. Table-driven test
walks every failure path asserting non-zero codes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Clone-from-git and Start-a-Claude-session were inert onTap: () {}
stubs whose printed shortcuts were never registered — dead UI on the
first screen a new user sees. No advertised dead ends: the tiles are
removed until their flows exist. The tips card was also fiction
(four of six shortcuts unregistered, ⌘ glyphs for a ctrl-based
default keymap) — it now lists six bindings that exist in the shipped
default preset / contributed commands, and the Open-folder glyph
matches the real ctrl+o binding.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
widget.forkSourceId took precedence over the fresh/resume logic on
EVERY (re)bind, so /clear in a fork pane re-forked the original
conversation instead of clearing, and /resume re-forked the same way.
The source is now copied into one-shot pane state and cleared after
the first successful fork spawn; later respawns operate on the pane's
own session.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Orchestrator.spawn() check-then-acts on the session registry across
two awaits (transcript-tail read, process start) — two racing callers
for the same id both passed the check and the loser's live claude
process was orphaned, never killed, never observed. The first caller
now installs the spawn future synchronously; later callers await the
same future, and a failed spawn clears the entry so a retry proceeds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The shell spawned with Directory.current — $HOME for desktop-entry
launches, and stale after a project switch since the process CWD
never moves. Use the kernel project root, falling back to the
process CWD only when no project is open.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Broadcast streams drop the current value for late subscribers — the
shape behind T-274: the init event fires while spawn() is still
awaiting the transcript-tail read, before the pane subscribes, so the
status bar stayed blank. New pure-Dart ValueStream<T> (no rxdart —
prefer-zero-deps) replays the latest value to each new subscriber;
statusStream, busyStream, and pendingPromptStream in the claude
builtin now use it. busyStream subscribers see the current state
first (seeded false), which the busy test now asserts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The session observed its child only via stdout. Two failure modes:
with --verbose the CLI chats on stderr, and an undrained 64KB pipe
blocks the child mid-turn with zero diagnostics; and nothing watched
the exit code, so a crashed process just looked thoughtful forever.
ClaudeStreamJsonProcess now drains stderr from construction into a
bounded tail buffer, and StreamJsonSession watches exitCode: on death
it flips busy off, clears any unanswerable pending prompt, and emits
a SessionEnd (exit code + stderr tail) — replayed via session.end for
late binders. The pane reports the exit in its status line and logs
the stderr tail; a deliberate dispose suppresses the watch so /clear
and teardown don't read as crashes. Test fakes extend the process
base instead of implementing it, so its defaults carry.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New items arrive on every streamed token, and _onChanged jumped to
maxScrollExtent unconditionally — so a reader who scrolled up was
dragged back to the tail continuously for the whole reply. The
_atBottom pin already existed for viewport resizes (T-297); apply it
to the new-item path too, re-checking after layout since the user can
scroll during the frame. Twin tests added beside the T-297 pair:
pinned view keeps following, scrolled-up view stays put.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ClideKernel.of(context) is an illegal ancestor lookup inside
dispose(); both panes wrapped it in catch (_) and silently did
nothing. The terminal pane therefore never sent pane.close (backend
PTY + daemon pane leaked per closed pane) and the Claude pane never
removed its settings listener. Both now cache KernelServices in
didChangeDependencies and the swallow-everything helpers are gone.
New terminal_pane_test covers the close-on-dispose path; note in it
why the whole tree must unmount (harness Overlay keeps
initialEntries across rebuilds).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wire the detection + follower core into the Bash tool card. A Bash card
with a follow intent (`tail -f …`) gains a "live tail" segment below the
result: an embedded read-only TerminalView fed by FileTailFollower on the
file the command follows, resolved against the open workspace.
Lazy lifecycle for free: the collapser builds its children only when
expanded (clide_collapser_card.dart), so _BashLiveTail starts the follower
in didChangeDependencies on expand and stops it in dispose on collapse —
no follower runs until the card is expanded. No resolvable file-backed
source → a muted "no independent source to follow" note, never an empty
terminal. The workspace root comes from kernel.project.current, so no new
plumbing through the conversation widget tree.
Tests: a tail Bash card surfaces the segment (+ the muted note when no
project/source); an ordinary `ls` card gets no segment; the segment only
builds on expand.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The detection/follow core for the live-tail sub-card, with the UI wiring
to follow. Claude Code runs every Bash tool itself and clide only sees the
final tool_result block — we can't mirror the running process, so instead
we detect a file-backed source the command follows and open our own
read-only follower on the same file.
- bash_tail_source.dart: detectBashTailSource() parses a Bash command for a
single, safe, file-backed source (tail/cat/less with one file arg, inside
the workspace via resolveUnderRoot). Returns null for a pipe-into-tail, a
redirect, two files, or a path outside the repo — the caller then shows a
"nothing to follow" note. bashHasTailIntent() gates WHEN the segment
appears: v1 triggers on `tail`/follow-flags only, so ordinary cat/ls/git
cards stay clean (cat/less remain detectable for later).
- file_tail_follower.dart: a polling, read-only `tail -f`-style follower
(no subprocess, no touching Claude's command) that emits the trailing
window then appended deltas, and re-reads from the top on truncation.
Tested: 19 parser cases (incl. the `git push | tail -25` and outside-
workspace null cases), the intent predicate, and the follower (initial
window / appended delta / missing file / rotation / start / stop).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A fan-out of N agents (Task/Agent) merged into one shared "Activity / N
steps" cluster — groupConversation folded an Agent spawn like any Bash/
Read call. Now an Agent spawn is a cluster boundary, rendering as its own
first-class collapsing card (reusing the existing sticky-agent path: folded
prompt T-263 + nested run T-264), while adjacent non-agent foldables keep
clustering into the normal Activity card.
Two changes:
- activity_cluster: a shared isAgentTool() predicate; _isFoldable returns
false for agent spawns at every level (incl. L3), so parallel agents
never merge. Only the grouping boundary changes; fold mechanics are
unchanged.
- conversation_view: harden resolveOwner. Its nearest-preceding-agent
fallback is safe with one agent but mis-routes under a parallel fan-out
(an unattributable item lands in whichever agent was emitted last —
a sibling's card). With >1 agent, drop the fallback so the item orphans
(rendered inline) instead of cross-attributed. The T-338 direct route
(parent_tool_use_id) still attributes interleaved items correctly.
Tests: two consecutive agents → two cards (not one cluster); agent breaks
a sibling cluster; agents first-class at L3; regression — consecutive
Bash still one cluster; interleaved parallel-agent runs route to their own
card; an unattributable item orphans instead of being swept into the last
agent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ExitPlanMode arrives as a can_use_tool permission prompt and was approved
like any other tool — the control_response was sent but the tracked
SessionStatus.permissionMode never changed, so the mode indicator and
composer stayed on "plan" after the plan was accepted.
On approving an ExitPlanMode prompt, sync the tracked mode to 'default'
(the CLI performs the transition itself, so no set_permission_mode control
request is sent — we only mirror it). The change rides the existing
statusStream → claude_pane._status plumbing, so the permission-mode
control and status indicator update with no extra wiring. Deny, and any
non-ExitPlanMode tool, leave the mode untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Raise the declared minimums in pubspec.yaml to what our deps already
require: Flutter >=3.35.0 / Dart >=3.9.0 (was 3.19.0 / 3.5.0). alchemist
0.12 needs Flutter 3.32; Dart 3.9 first ships in Flutter 3.35, so 3.35 is
the binding floor. Pin the exact build toolchain in .fvmrc (Flutter
3.44.1).
Moving to the Dart 3.9 language level switches `dart format` to the new
"tall" style and enables two new lints. This commit is the resulting
mechanical churn, isolated from any behaviour change:
- whole-tree `dart format` reformat (tall style)
- `dart fix` for unnecessary_underscores + use_null_aware_elements
No runtime behaviour change; `make test` green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On a desktop launch the daemon's PqlClient boots with workDir set to the
launch CWD (e.g. HOME), not the repo — swapIpcServer only rewires it once
the project opens. The tickets and decisions panes fire their first pql
fetch before that swap, so pql runs in the wrong directory against a
stale/global pql.db and the pane errors (observed:
"ticket_deps.blocker_record_id missing — pql.db is from an earlier
schema"). A manual refresh worked because by then the workspace was open.
This is a wrong-workDir timing issue, not db-busy, so the T-350 retry
doesn't catch it. Both panes now re-fetch on ProjectOpened, which fires
after the IPC server swaps to the project workRoot.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A row of toggle chips below the filter box — one per pql ticket type,
ordered large→small (Initiative, Epic, Story, Task, Bug), each with its
TicketTypeColors dot + border. Single-click toggles a type; double-click
isolates it (chart-legend solo, fully reversible); disabling the last
enabled type snaps all back on so the list is never blank. ANDed with the
text filter; all on by default, nothing persisted.
One GestureDetector owns both onTap + onDoubleTap so Flutter disambiguates
single vs double. Wireframe updated + approved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The T-339 pick-up test was the only importer of the ~450-line claude
extension.dart, pulling its (mostly UI-wiring, untestable) lines into
the coverage denominator and dropping the suite below the 95% floor.
Move applyTicketPickUp into its own ticket_pick_up.dart and the T-300
path resolver into a pure resolveWorkspaceFilePath() — both small, fully
covered, and imported by the tests instead of the whole extension. No
behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>