Commit Graph
395 Commits
Author SHA1 Message Date
jpmschweitzerandClaude Opus 4.8 606d3df98f harden the ConPTY backend (Linux-verifiable pre-VM work)
Pre-Windows-VM hardening — the parts validatable on Linux, leaving the
unrunnable FFI (Job Object, T-424) for the VM session:

- Clamp PTY cols/rows to >= 2 in both backends' spawn + resize (new
  pty_size.dart). A 1-column ConPTY makes conhost spin emitting CRLF
  (microsoft/terminal#19922); 0 is invalid on both platforms.
- ci/test.sh: --timeout 60s on the dart-test pty line (matches the flutter
  lines) so a wedged ConPTY reader fails fast instead of hanging the run.
- Make windows_pty.dart's pure helpers public + testable off-Windows:
  quoteArg (MSVCRT quoting), composeEnvironmentBlock, and resolveExecutable
  (now takes an injectable existence probe). New windows_pty_args_test.dart
  + pty_size_test.dart give 15 cross-platform assertions over the trickiest
  Windows logic the on-Windows smoke suite can't reach off-platform.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 20:54:50 +02:00
jpmschweitzerandClaude Opus 4.8 431e26d659 remove tmux as a required tool (dead since D-77)
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>
2026-06-14 18:22:04 +02:00
jpmschweitzerandClaude Opus 4.8 84a6ef7c77 Merge main into windows-support
Brings windows-support up to date with main (T-404/405/406, T-413–416,
T-421, the T-422 workspace-lifecycle epic, and the 2.4.0 release).

Conflict resolutions:
- terminal_pane.dart: keep the Windows PowerShell shell selection and
  main's workspace-cwd fix (T-381) together.
- tool_check.dart: accept main's deletion (dead, unreferenced code).
- CHANGELOG.md: keep both Unreleased sections.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 18:21:41 +02:00
dehlakandClaude 03cc0603b0 port clide runtime to Windows (ConPTY, AF_UNIX, PATHEXT)
Bring the runtime up on Windows without disturbing the POSIX paths.

PTY: introduce a platform-neutral PtySession contract with a factory
that picks NativePty (posix_openpt/posix_spawn) or the new WindowsPty
(ConPTY via CreatePseudoConsole). The pane registry programs against
the interface; NativePty now implements it.

IPC: the per-workspace AF_UNIX socket lives under %LOCALAPPDATA% and
is hashed from a canonical workspace key (backslash + ASCII-folded
case) so the Dart server and the C client agree despite NTFS case-
insensitivity. The C client grows a Win32 shim (winsock afunix);
chmod is a no-op on Windows where the per-user ACL is the gate.

Toolchain: PATH probing splits on ';' and tries PATHEXT extensions;
the shell defaults to PowerShell (pwsh, then powershell); tmux is
treated as optional since it has no Windows build; dugite falls back
to PATH git for now.

Build: add `make build-windows`, a clide-cli MSVC build wrapped by
ci/build_cli_windows.sh, and a ConPTY smoke-test suite that self-
skips off-platform.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-14 17:42:55 +02:00
jpmschweitzerandClaude Opus 4.8 cbe76938a8 vim ctrl+w window-command family + global multi-chord matcher (T-404)
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>
2026-06-13 18:19:32 +02:00
jpmschweitzerandClaude Opus 4.8 5a21ea949a workspace tab cycle commands + ctrl+pagedown/up (T-405 part 1)
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>
2026-06-13 18:06:36 +02:00
jpmschweitzerandClaude Opus 4.8 25db19fc0e vim normal-mode navigation in non-editor panes (T-406)
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>
2026-06-13 14:46:04 +02:00
jpmschweitzerandClaude Opus 4.8 b399acedbf surface Claude Code Workflow runs in convo + sidebar (T-416)
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>
2026-06-12 21:44:41 +02:00
jpmschweitzerandClaude Fable 5 57978c54c5 cover the T-410 epic's pane handlers and Activity tab
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>
2026-06-12 14:34:07 +02:00
jpmschweitzerandClaude Fable 5 ac21f5cb69 adjust composer typeahead test for the grown owned-command set
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>
2026-06-12 14:11:28 +02:00
jpmschweitzerandClaude Fable 5 0fd8a241db Activity tab: session controls + live usage block (T-415)
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>
2026-06-12 14:04:55 +02:00
jpmschweitzerandClaude Fable 5 1674d3021a tint the Claude sidebar tab icon with the Claude accent (T-418)
TabContribution gains an optional iconColor honoured by the icon rail:
full-strength when active/hovered, dimmed (70%) when idle, so the tint
reads as identity without outshouting the active-state border. The
Claude Activity tab sets claudeAccent (#D97757) — nominative use per the
licenses.yaml trademark note (it marks Claude's own panel).

Filed and closed as a try-it-out (user request); trivially revertible if
the accent doesn't land visually.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:30:22 +02:00
jpmschweitzerandClaude Fable 5 959ec7ddc4 Config tab → live control panel; sidebar styling pass (T-414)
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>
2026-06-12 13:25:49 +02:00
jpmschweitzerandClaude Fable 5 52d90be730 own the open-in-clide command family (T-413)
/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>
2026-06-12 13:06:10 +02:00
jpmschweitzerandClaude Fable 5 1bdd88f4ab own /effort: respawn-with-resume carrying --effort, picker UX (T-412)
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>
2026-06-12 12:56:09 +02:00
jpmschweitzerandClaude Fable 5 02c6dd4cf0 route slash commands: TUI-only builtins become local notices (T-411)
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>
2026-06-12 12:25:19 +02:00
jpmschweitzerandClaude Fable 5 1d6682df18 intercept /model: arg sets the model, bare opens a picker (T-408)
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>
2026-06-12 09:04:29 +02:00
jpmschweitzerandClaude Fable 5 8966a159db require a clean release for double-tap modifier detection (T-409)
Typing Shift+; opened quick-open instead of a colon. Two flaws in the
T-341 detector: it counted a tap on the Shift keydown (so a chorded
press could complete the gesture before the chord key arrived), and it
relied on the chorded key bubbling to the root KeyboardListener to
break the gesture — but a focused editor or text field consumes that
event, so the tracker never saw it.

The tracker now models press/release: a tap is a press with no other
key going down while the modifier is held, and the gesture fires on
the second clean release. The root shell feeds it from a
HardwareKeyboard handler, which observes every event before focus
dispatch regardless of who consumes it, and treats a modifier pressed
while a non-modifier is already held as a chord.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 08:49:20 +02:00
jpmschweitzerandClaude Fable 5 6817abaf96 add WorkspaceRef + remote identity on RecentProject (T-332)
The model-independent half of the ssh:// open scheme. WorkspaceRef is
the value type for "where a workspace lives" — a local path or
ssh://[user@]host[:port]/abs/path, with parse/uri round-tripping and a
host:path display form. RecentProject carries host/port/user
(back-compatible JSON: absent keys deserialize as local) so remote
recents survive restarts and render with their host badge.

The remaining T-332 scope — ProjectManager.current off bare Directory,
open() branching, remote resolveProject — is gated on the execution
layer (T-336), which is itself blocked on the T-330 footprint pick;
the epic's blocker graph now encodes that gating.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 03:11:57 +02:00
jpmschweitzerandClaude Fable 5 051ceea3b2 route DaemonClient through a DaemonTransport seam (T-331)
The UI's backend client connected straight to the workspace unix
socket, hard-coding the local shape. It now talks JSON-lines through a
DaemonTransport (new lib/src/ipc/transport.dart, Flutter-free), with
LocalSocketTransport reproducing today's connect byte-for-byte — zero
behavior change, proven by the untouched client test suite plus new
seam tests driving the client over an in-memory transport.

This is the slot the SSH-remote backend (T-329/Q-23) plugs into:
request correlation, reconnect/backoff, and event forwarding live
above the seam and won't change when the endpoint is remote.
main.dart's swapIpcServer becomes swapBackend per the same plan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 03:04:58 +02:00
jpmschweitzerandClaude Fable 5 c3dd1d3e3f capture CSI intermediate bytes; stop bare-final mis-dispatch (T-123)
_consumeCsi silently discarded intermediate bytes (0x20-0x2f), so an
intermediate-bearing sequence dispatched on its bare final byte —
`CSI 5 SP @` (VT420 scroll-left) ran as "insert 5 blank characters",
and `CSI Ps SP q` (DECSCUSR) could collide with any future bare-q
handler. The parser now records intermediates on the CSI scratch
object and routes any sequence carrying them to unknownCSI, since no
intermediate form is implemented yet.

Implementing DECSCUSR itself (cursor shape + renderer support) is
filed as T-397.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 02:55:58 +02:00
jpmschweitzerandClaude Fable 5 37f3ad0796 remove the tmux-era TranscriptPublisher class (T-385)
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>
2026-06-12 02:17:37 +02:00
jpmschweitzerandClaude Fable 5 06c2e76be4 remove the dead ColumnHat widget; keep hatHeight (T-385)
ColumnHat was superseded by the hat bar in app.dart and survived only
through a zero-coverage smoke test. Its file also carried the live
hatHeight constant (D-57's 24px hats) consumed by the hat bar and the
menu bar — that moves to widgets/src/chrome_metrics.dart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 02:17:15 +02:00
jpmschweitzerandClaude Fable 5 a59c3658a9 remove the legacy free-function git API (T-385)
operations.dart carried a full second git operation surface
(gitStage/gitCommit/gitStash/gitPush/...) that duplicated GitClient
verb-for-verb, was kept alive only by its own tests, and hid a latent
pipe deadlock in _applyPatch (stdin written without draining stderr).
The file keeps the genuinely shared plumbing — gitBin resolution,
GitException, validateGitRef, GitLogEntry — which GitClient, the
status/diff readers, and the git command handlers consume.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 02:16:06 +02:00
jpmschweitzerandClaude Fable 5 99dc52052a dispose-safe teardown for KeymapService + ClaudeConfig; cover the
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>
2026-06-12 01:55:56 +02:00
jpmschweitzerandClaude Fable 5 4443e1c834 run dart format over the scorpion-fix files
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 01:40:09 +02:00
jpmschweitzerandClaude Fable 5 bc3c47ee81 make extension activation transactional (T-377)
Three lifecycle gaps, benign among curated builtins but hazardous the
day Tier-6 Lua extensions land: a throw mid-contribution left earlier
contributions mounted while the extension recorded as failed (a retry
then double-applied them); deactivate ignored active dependents; and
the panel/command registries silently clobbered on id collision.

Activation now tracks what it mounted and unwinds it all on failure
(including the extension's own deactivate when its activate had
succeeded); deactivate refuses with a logged warning while active
dependents exist — disable the dependents first; duplicate
contribution/command ids throw, which the transactional path turns
into a clean failed activation with first-wins semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 01:35:24 +02:00
jpmschweitzerandClaude Fable 5 c31f5bfb14 add Terminal.writeBytes with a persistent UTF-8 decoder (T-373)
The terminal's only ingestion API was write(String), so both byte
consumers decoded per chunk — a multi-byte rune split across PTY
reads (or a tail window starting mid-character, which FileTailFollower
does by construction) rendered as U+FFFD garbage. writeBytes feeds a
per-instance chunked Utf8Decoder that carries partial-rune state
across calls; the terminal pane and the Bash live-tail follower now
use it, and write(String) stays for tests and programmatic writes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 01:32:13 +02:00
jpmschweitzerandClaude Fable 5 928dede847 return error envelopes from failed claude commands (T-391)
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>
2026-06-12 01:28:50 +02:00
jpmschweitzerandClaude Fable 5 638869621e remove dead welcome tiles; advertise only real shortcuts (T-383)
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>
2026-06-12 01:24:58 +02:00
jpmschweitzerandClaude Fable 5 d9ae2d7585 surface kernel notifications as toasts (T-382)
The Notifications service had zero widget consumers — anything pushed
through ctx.notify (cli_install's dogfood warnings, install results)
accumulated in a list nothing rendered. The service now takes the
kernel MessageBus and publishes each notification to the toast
channel with mapped severity, so the existing ToastOverlay renders
them; the active list stays for API compatibility. Chose routing over
building a notifications tray nobody asked for.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 01:21:16 +02:00
jpmschweitzerandClaude Fable 5 01c3de37e1 render markdown hard breaks and image placeholders (T-379)
Both node types fell through the inline-span switch to an empty
textContent span: words on either side of a hard break glued
together, and images vanished with no trace. A br now emits a
newline; an img renders a muted italic "[image: alt]" placeholder
(falling back to the src) — no inline network loading in the owned
renderer; live-pane images keep going through clide image show.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 01:18:41 +02:00
jpmschweitzerandClaude Fable 5 e413380ea9 make settings persistence safe for nested data and crashes (T-376)
Three failure modes in the YAML store: maps nested inside lists (the
documented keymap-overlay shape) fell through _emitScalar to
toString() and corrupted on the next read; writes went straight to
the live file, so a crash mid-write truncated every setting; and a
parse failure silently returned an empty map that the next set()
wrote over the user's file. Maps in lists now emit as YAML flow
mappings, writes are temp-file + rename, and an unparseable file is
preserved as .broken with a warning through the kernel Logger (new
onError hook, wired in the facade).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 01:16:48 +02:00
jpmschweitzerandClaude Fable 5 5d52694889 serve IPC clients with one await-for read loop (T-372)
The async onData handler never paused its subscription, so pipelined
requests interleaved mid-handler — violating D-72's serial-dispatch
contract — while the shared StringBuffer could re-frame underneath an
in-flight await and the per-chunk utf8.decode corrupted runes split
across reads. One `await for` over a persistent Utf8Decoder +
LineSplitter fixes framing, decoding, and serialization at once.
Tests: two frames pipelined in one write dispatch strictly in order;
a frame split mid-rune across writes decodes intact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 01:13:43 +02:00
jpmschweitzerandClaude Fable 5 bbc6899df2 consume the fork source on first bind (T-375)
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>
2026-06-12 01:11:17 +02:00
jpmschweitzerandClaude Fable 5 51957eb0ac coalesce concurrent session spawns onto one future (T-374)
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>
2026-06-12 01:08:33 +02:00
jpmschweitzerandClaude Fable 5 5d9443d9f7 spawn terminal panes in the open workspace root (T-381)
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>
2026-06-12 01:06:45 +02:00
jpmschweitzerandClaude Fable 5 5f9c054420 replay-latest ValueStream for session state streams (T-386, T-274)
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>
2026-06-12 01:05:08 +02:00
jpmschweitzerandClaude Fable 5 0e7353bf9c run dart format over the dragon-fix files
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 00:58:37 +02:00
jpmschweitzerandClaude Fable 5 9889afdc35 require a bearer token on the MCP HTTP server (T-362)
D-71's threat model — another user on the same host must not drive my
IDE — was enforced with 0600 on the unix socket and then bypassed
wholesale by the unauthenticated localhost SSE port, which since D-86
serves every clide verb as a tool. The server now mints 32 bytes of
CSPRNG token per start, publishes it via the /ide discovery lock
file's authToken slot (the field Claude Code's client reads), chmods
the lock to 0600, and rejects any request that doesn't present the
token in x-claude-code-ide-authorization with 401.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 00:58:01 +02:00
jpmschweitzerandClaude Fable 5 a919d79ce1 watch the claude process: drain stderr, surface exit (T-361)
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>
2026-06-12 00:54:12 +02:00
jpmschweitzerandClaude Fable 5 77c4341318 scope collapser-card semantics exclusion to the header (T-370)
The summarized button semantics (label, expanded/collapsed state)
wrapped the entire card with excludeSemantics, so every expanded
child vanished from the a11y tree — a screen-reader user could expand
a run and hear nothing inside it. The exclusion now wraps only the
header (ticker when collapsed, header row when expanded); inner item
cards stay readable, and the redundant background-toggle tappable is
explicitly excluded so the header stays the single AT stop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 00:47:06 +02:00
jpmschweitzerandClaude Fable 5 664a8da72e tear down the previous workspace's services on project switch (T-367)
buildDispatcher composed a fresh PaneRegistry, FilesService,
SearchService, and EditorRegistry per workspace, but their shutdown()
methods had zero callers — every project switch left the old set's
file watcher emitting into the new workspace's bus and its PTYs
alive. The dispatcher now pairs with a teardown closure that the
serialized swap invokes after the old server stops; the same-path
reuse fast-path drops the unused new set without teardown since its
services are inert until a command starts them. SearchService gains
the shutdown() it was missing (cancels in-flight searches).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 00:44:24 +02:00
jpmschweitzerandClaude Fable 5 ba6ab51118 confine editor.open/editor.save to the workspace (T-363)
The editor registry resolved buffer paths with a string join that
passed absolute paths through verbatim and never normalized `..` —
an unconfined read and write primitive over IPC while files.read was
carefully guarded. Buffer paths now resolve through
resolveUnderRootFollowingSymlinks: traversal, absolute escapes, and
symlinks-out are rejected at open, and re-checked at save so a
symlink swapped in under an open buffer's path can't redirect the
write. D-80's extra read roots deliberately do not apply — a buffer
is a write surface. Handlers map PathOutsideRoot to the same error
files.read uses. Also merges a duplicate Added heading that had crept
into the Unreleased changelog section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 00:39:23 +02:00
jpmschweitzerandClaude Fable 5 88d72789f4 apply include/exclude globs in search.replace (T-364)
computeReplacements accepted the query's glob filters and silently
dropped them — replace could rewrite files the equivalent search
would never have matched. The grep engine's glob helpers are now
public and shared, so search and replace can't disagree on scope;
both the preview and the apply path go through the filtered list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 00:34:47 +02:00
jpmschweitzerandClaude Fable 5 8477e302ef detect symlinks from the lister entity, never descend them (T-365)
stat() follows links, so `stat.type == link` was always false: every
FileEntry reported isSymlink=false and walkFiles happily descended
symlinked directories — an escape hatch out of the workspace and a
cycle risk for the search engine built on the walk. The lister already
runs with followLinks: false, so the Link entity itself is the signal.
listDir keeps reporting the target type for the UI; walkFiles skips
descent into symlinked dirs and still emits file symlinks as entries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 00:32:24 +02:00
jpmschweitzerandClaude Fable 5 34821fbc39 guard SGR 38/48 lookahead; parse colon sub-parameters (T-369)
printf '\e[38m' was a RangeError inside Terminal.write — the
extended-color branches indexed params[i+1..i+4] unguarded. An
emulator must never throw on hostile bytes. Both branches now share a
bounds-checked helper that ignores truncated sequences.

Colons were silently dropped mid-CSI, fusing 38:2:255:0:0 into one
bogus parameter; the consumer now records ECMA-48 sub-parameter
links, so ITU T.416 colon-form truecolor/256-color (with or without
the colorspace slot) parses identically to the semicolon form, and a
malformed colon group is dropped whole instead of bleeding into
neighbouring SGR codes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 00:30:41 +02:00
jpmschweitzerandClaude Fable 5 390ab2b64e gate conversation auto-scroll on the bottom pin (T-368)
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>
2026-06-12 00:26:32 +02:00
jpmschweitzerandClaude Fable 5 75fc2719a0 cache the kernel ref so dispose() can actually clean up (T-366)
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>
2026-06-12 00:24:27 +02:00
jpmschweitzerandClaude Fable 5 466383671d close the PTY master fd when the child exits naturally (T-360)
_reap() flipped _dead without releasing the master fd, and close()
short-circuits on _dead — so every naturally-exited child leaked its
fd and pty device for the life of the app. The reader isolate sends
EOF only after leaving its poll loop, so releasing the fd inside
_reap() cannot race the reader. Regression test counts /dev/ptmx
entries in /proc/self/fd across a natural exit; verified to fail
against the unfixed code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 00:18:47 +02:00