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>
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>
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>
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>
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>
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>
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>
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>
_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>
parser.dart (1139 LOC) now keeps only the core — byte queue, dispatch
tables, ESC/CSI consumers — at 373 LOC. The handler groups move to
mixins in part files of the same library, so every private member
stays library-scoped and no public surface is added:
- csi_handlers.dart — cursor/erase/scroll ops, DA/DSR, margins, tab
clear, repeat, window manipulation
- sgr_handlers.dart — SGR incl. the guarded 38/48 extended-color path
(T-369)
- mode_handlers.dart — ANSI + DEC private mode set/reset
- osc_handlers.dart — OSC string parsing + dispatch
An abstract _EscapeParserBase carries the shared state (handler sink,
queue, token bookkeeping, the reusable _Csi scratch) the mixins are
`on`. All 76 parser tests (and the rest of the terminal suite) pass
unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
app.dart was 1187 LOC mixing five concerns. It now keeps ClideApp +
the WidgetsApp root (~60 LOC); the shell moved to lib/src/shell/:
- root_shell.dart — keyboard/intent routing (keymap resolution,
double-tap modifiers, menu mnemonics), the overlay stack, and the
welcome overlay
- hat_bar.dart + project_switcher.dart — the window-chrome bar and
its recents/file-actions dropdown (now in src/shell, not builtin/ —
they're app chrome, not extension-shaped contributions)
- slot_host.dart — slot mounting, focus-scope integration, the
per-slot bodies incl. the workspace split + editor drag handle;
_SlotBody's static title resolver became the shared resolveTabTitle
- layout.dart — the three-column grid, status bar, collapse toggles,
bottom icon rails
app.dart re-exports RootLayout, SlotHost, StatusbarHost, and
StatusbarCollapseToggle, so every existing import (incl. the three
app-level test files) is unchanged. Pure move + minimal publics
(RootShell, HatBar, ProjectSwitcherButton); full suite green with no
test edits.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fd-passing-era surface — recvmsg with the msghdr/cmsghdr/iovec
struct family, raw read/write, ioctl/winsize, the fcntl non-blocking
helpers — had no callers since the daemon dissolution (D-56);
NativePty binds its own symbols. What remains is what's actually
consumed: socketpair + close (the ClideTestApp harness), errno, the
poll event bits, and the two signal numbers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
_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>
Confirmed root cause of the sidebar failure: a desktop launch starts in
HOME, which isn't a git repo, so resolveWorkspaceRoot returns HOME and
the daemon's pql/git/files all target HOME. pql then finds a stale
~/.pql/pql.db (left from earlier HOME-workdir runs) and errors
"pql.db is from an earlier schema" — exactly what the sidebars showed.
A manual refresh worked only because by then the workspace had swapped
to the repo. (Verified directly: `cd $HOME && pql ticket list` reproduces
the schema error against ~/.pql/pql.db.)
Settings confirmed the user is in the picker path (no startupSticky on
any recent), so nothing auto-opens the repo at boot and the daemon sits
on HOME until the project is opened.
Fix: resolve the startup workspace before boot — prefer the launch CWD
when it's a git repo, else fall back to app.lastProject (the repo). The
daemon now points at the real workspace from its first request, so the
sidebars load immediately without depending on swap/refetch timing. The
ProjectOpened refetch and swap serialization from 2.3.x stay in place for
genuine mid-session project switches.
Pure resolveStartupWorkspace() helper is unit-tested; app boot green.
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>
The pql-backed sidebar panes fetch once on first build. If that fetch
fired too early — the planning DB still settling at startup, or a db-busy
SQLite lock under concurrent pql writes (pql exits 69) — the pane showed
"pql … failed" and stayed there until a manual refresh re-fired it.
Retry transient failures at the single chokepoint, PqlClient._run: on a
busy/locked signal (exit 69, or stderr mentioning database is locked /
busy) retry a few times with short backoff before throwing. Genuine
errors aren't busy, so they still surface immediately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The catch-all "dispatch threw" log omitted the request command, so a
handler exception couldn't be correlated to what caused it (PTY/IPC
audit item #26). Track the resolved cmd across the request handler and
include it in the error log.
The audit's other two items were already satisfied: errno/signal magic
numbers are centralized in PosixErrno + ffi/libc.dart, and the IPC
server already logs through the kernel Logger (no stderr.writeln left).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A clide:// link is an untrusted external vector (any webpage can fire one), so it
no longer translates to a command in parseArgv. It routes the raw URL to a new
builtin.deeplink handler that is doubly defensive: a default-deny allowlist
(kDeepLinkSafeActions — only the read-only 'open' verb; run/git/write/passthrough
rejected) AND a mandatory 'an external link wants to: … allow?' confirmation
before anything runs. Records the security boundary as D-90.
The earlier silent editor.open passthrough is replaced; open still works, now
behind the prompt. Tests cover the allowlist (the boundary) + the gating.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
clide://open?path=&line= translates (in parseArgv) to editor.open, so an OS
scheme invocation routes through the existing CLI→IPC path into the running
window — single-instance for free, no new code path. Registered the scheme:
linux/clide.desktop MimeType x-scheme-handler/clide (Exec already passes %U) and
macOS Info.plist CFBundleURLTypes. Parser validates the action/path/line.
Linux works end to end (the OS passes the URL as argv). macOS URL DELIVERY (the
AppDelegate openURLs callback → forward into this path) is a follow-up — the
scheme is registered, but the native hook needs a real macOS machine to verify,
so it's not shipped blind. Also drops a stray import in clide_markdown_test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce EditorSettings as the effective, source-agnostic editor configuration
the editor and save path obey. .editorconfig is demoted to one *source*
(editorconfig.dart now returns EditorSettings); editor_settings_resolver.dart is
the single composition seam where future sources — a settings panel, a clide
settings file — layer in via merge() without the editor changing.
The registry resolves settings on buffer load and, when a .editorconfig is saved
in-app, re-resolves every open buffer and emits editor.settings-changed (a hook
in save, not a filesystem watcher — the realistic case, cheaply). Buffer JSON
carries editorSettings. 100% line coverage on the new model + resolver.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New lib/src/editor/editorconfig.dart: a dependency-free INI parser, an
EditorConfig-flavour glob matcher (*, **, ?, [seq], {a,b}, {m..n}), and
directory-walk resolution honouring root=true with nearest-file / last-section
precedence. EditorRegistry resolves the config when a buffer opens (exposed on
the buffer JSON for the UI) and applies end_of_line / trim_trailing_whitespace /
insert_final_newline on save, reconciling the in-memory buffer when the text
changes. 100% line coverage on the new file.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The sidebar/dock filter fields (the shared ClideFilterBox) had no CLI
peer — a one-way, UI-only affordance that broke D-6 parity. Add the
drive+observe verb `clide ui filter <address> [<text>]`, routed entirely
through the kernel MessageBus pub/sub so a box reacts to a published
message identically whether the trigger was a UI keystroke or the CLI —
keeping extensions first-class (no dispatcher→widget wiring).
- ClideFilterBox gains an `address`; when set it listens on `filter.set`
for its address and republishes its value on `filter.state`. Null
address keeps the box a kernel-free UI widget.
- FilterStateCache (new kernel service) caches the latest `filter.state`
per address — the bus has no retention, so this backs the observe-half.
- ui.filter: with text → publishes `filter.set` (drive); without →
reads the cache (observe). Honest toolError when there is no live UI.
- Address every box: decisions/tickets/files/git/output/problems panes,
the four search boxes, and the pql search/query/markdown inputs.
Addresses are the ids from `clide pane list` (e.g. decisions.panel).
settings.json: allow the `clide` CLI + relevant skills.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The inline image cards (T-249) are often too small to read. Clicking one now
opens a full-screen lightbox: zoom (scroll wheel / pinch), pan when zoomed,
double-click to reset to fit, Esc / close button / backdrop click to dismiss.
ClideLightbox is a reusable primitive (lib/widgets/) over Flutter's
InteractiveViewer with clide-owned zoom gestures, shown via the DialogRouter
(dimmed backdrop, single modal at a time, D-78). The card stays display-only;
the click is a navigation gesture, not an inline control.
CLI parity (D-6): `clide image show <path> --fullscreen` opens straight into
the lightbox instead of injecting a card.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
External MCP clients (Cursor, Windsurf, Copilot) can now drive clide. The
MCP server's tools/list is generated from the co-registered command+schema
registry (D-74) that already feeds the CLI and palette — the full
mcp__clide__* namespace with no hand-maintained second surface (D-86). Each
command's CommandSchema maps to a JSON-Schema inputSchema; tools/call routes
mcp__clide__<cmd> to dispatcher.dispatch and renders the IpcResponse as MCP
content (data as JSON, errors with isError).
register() gains a mcpExpose flag (default true); pane.tail opts out as a
poor request/response fit. tail/events are server-intercepted so they're
naturally absent. The two /ide stubs (getDiagnostics, executeCode) are left
as stubs — making them real (analyzer hook, Jupyter eval) is out of scope
per the ticket. Transport unchanged (SSE, D-73).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The one-shot request/response complement to the never-returning
`tail --events` stream — what an agent poll loop wants. The IPC server now
keeps a single global, arrival-ordered event log keyed by a monotonic
cursor (alongside the per-subsystem replay ring), bounded by eventLogDepth
with drop-oldest back-pressure (D-85: producer never blocks).
`clide events [--since <cursor>] [--filter X]` returns events after the
cursor, a high-water `cursor` to poll from next, and `gap: true` (+
oldestCursor) when the requested cursor predates the retained window so a
caller detects loss instead of silently missing dropped events. Repeated
polls neither drop nor duplicate. No on-disk persistence.
`events` is handled in the IPC server like `tail` (not the dispatcher);
added to the argv umbrella set. bindingWhen/CommandContribution untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds diff as a fourth ui.open target. The diff extension now retains an
app-scoped DiffController and subscribes to its builtin.diff/selection
channel: a selection reveals the diff tab and focuses the file, which
the view scrolls into view and highlights. Retaining the controller in
the extension (not the view) lets a focus survive the tab being
revealed/remounted, mirroring the ReaderNav viewers.
Closes T-233.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drives an image inline into the Claude conversation log over the same
bus-publish path as ui.toast/ui.open, keeping the dispatcher handler
Flutter-free. The card is display-only per D-78; the verb registers a
CommandSchema so it surfaces in clide capabilities for T-248 discovery.
Closes T-249.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Parity guarantees a verb exists for every UI action, but a verb is
unreachable if nothing advertises it. Add `clide capabilities` — it
reflects the live dispatcher registry to JSON (subsystem, verb, arg
schema) so the surface is discoverable and can't drift from what
dispatches. A thin /clide skill points Claude at it rather than
hard-coding a verb list, so new panels become reachable the moment
they register.
Co-Authored-By: Claude <noreply@anthropic.com>
The drive-half complement to the toast system (D-6 parity, like ui.open):
`clide ui toast "msg" [--severity success|warning|error|info] [--duration MS]`
publishes a message on the kernel MessageBus 'toast' channel, which the
ToastService consumes — so a hosted Claude session or any script can surface
"done/failed" on the user's screen. The channel literal is kept in ui_command
(not imported from the kernel) so the daemon command stays Flutter-free.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Parameterized subsystem commands were unreachable from the CLI: the
argv translator emits {positional, flags} but the handlers read named
top-level keys (args['path'], args['id'], ...), and nothing mapped
between them -- so 'clide editor open <path>' returned 'path is
required'. The fix needed no new mechanism: D-74's CommandSchema.normalize
already folds the argv shape into named args by a declared positional
ordering; these commands just never registered a schema.
Adopts it for the navigation/drive surface -- editor.open/activate/read/
save/close, files.read/ls, pane.close/focus/resize/write -- with
non-required positional schemas, so the only effect is positional->named
mapping plus numeric coercion of line/cols/rows. Handlers unchanged;
missing-arg errors unchanged. Edit-mutation verbs, pane.spawn, and git
arg verbs are deferred (noted on the ticket).
Takes effect on app restart (the dispatcher is built once at boot).
Closes T-232 (under T-208 'Give Claude hands').
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The drive-half complement to clide status (observe): an agent can now
open a doc in a GUI reader from the CLI so it can show the user what
it's looking at. 'clide ui open tickets T-48' (decisions by id,
markdown by path) publishes a 'selection' to the kernel MessageBus that
the reader's ReaderNav picks up.
The verb (ui_command.dart) is decoupled from the kernel via a publish
callback, wired in main.dart to the post-boot-captured MessageBus, so it
stays Flutter-free under dart test. Reads CLI positionals or named args.
Also files T-232 (CLI argv args don't reach typed handlers -- the gating
drive-the-IDE bug) and T-233 (diff-panel ui.open follow-up).
Closes T-231 (under T-208).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
clide status returns a one-shot snapshot for an orienting agent: the
workspace root, a git summary (branch/ahead/behind/clean/counts), the
active editor buffer + selection, the read-only readers' viewed docs
(new ReaderNavRegistry.currentByReader, the T-220 fold), focusedFile,
the live view-pane list (T-219), and the layout (slots + visibility +
focus mode). Previously 'status' was an unknown command (exit 3).
The verb handler (status_command.dart) is a thin Flutter-free wrapper;
the snapshot is assembled in main.dart where the live kernel + subsystem
state is in scope, with readerNav captured post-boot. Composite shape is
verified live; the pieces are unit-tested.
Closes T-221 and T-218 (Epic C) under T-208 'Give Claude hands' --
the observe half of D-6 parity is now in place.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per D-6 parity / D-83, make the kernel tabs the user sees (Claude,
Files, Editor, viewers) visible to the CLI. The PTY-backed PaneRegistry
can't model widget panes, so rather than mirror state (and risk drift),
pane.list snapshots the kernel PanelRegistry + LayoutArrangement at
request time via an injected view-pane source.
New Flutter-free ViewPane value type + snapshotViewPanes kernel bridge;
the daemonClientFactory now passes the PanelRegistry through so the
dispatcher can read it. pane.list merges PTY panes (source absent) with
UI tabs (source: ui, with slot/title/active/visible).
Acceptance (GUI-open: pane list enumerates live panes) needs a running
app to confirm end-to-end; unit-tested at the snapshot + dispatch level.
Closes T-219 (under T-218 / T-208 'Give Claude hands').
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
isIgnored only matched a directory path itself, not files beneath it, so
a recursive watch still surfaced changes inside .dart_tool/, build/, etc.
Linux usually hid this because inotify drops the nested creates; macOS
FSEvents delivers them, so the tree reacted to churn it should ignore.
Check each ancestor segment as a directory.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reader opened repo-local .claude markdown but rejected user-scope
files under ~/.claude with "path outside workspace" — that dir is
global, outside the repo, and files.read was repo-confined (T-102).
Per D-76 the Claude config surface is clide-managed, so files.read now
resolves a path under an allow-list: the workspace root plus trusted
extra read roots (FilesService.extraReadRoots), wired in main.dart to
~/.claude when present. Reads widen; writes stay repo-confined, and the
symlink re-check still refuses a config-root symlink that escapes. Off-
root paths and `..` traversal are rejected as before.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
resolveUnderRoot joined an absolute input onto the workspace root
(/repo + /repo/x → /repo/repo/x), so files.read 404'd on a file that
exists. The Claude Config tab hands the reader a skill's absolute
SKILL.md path, which hit this. Normalize an absolute input as-is; the
existing containment check still rejects absolute paths outside the
root, so the T-102 boundary is preserved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds on the find-in-files engine. A replace engine applies the
query's replacement to each matching file — literal or regex with
capture-group expansion ($1, $&, $$) — and reports per-file, per-line
before/after edits computed with the same logic the apply uses, so
preview and apply never disagree.
The search.replace command previews (no disk writes) or applies
(writing each changed file through the workspace path-safety guard).
The panel gains a Replace field: each match row previews its rewritten
line, and Replace all is gated on a clean git working tree (git is the
undo) plus a confirmation before it writes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pure-Dart content-search engine behind find-in-files (D-79): walks
the ignore-pruned workspace, fans files across worker isolates
(Isolate.run) for parallelism, matches each line with a literal
indexOf fast-path or a RegExp, and streams match batches with
cooperative cancellation. No ripgrep dependency; the search.grep IPC
contract is engine-agnostic so an rg accelerator can slot in later.
search.grep returns a searchId and streams search.match / search.done
(or search.error) events, mirroring files.watch; search.cancel stops
an in-flight search. The service reuses the files service's resolved
ignore set so both honour the same ignore_files: layering.
editor.open gains an optional 1-based line argument: it converts the
line to a byte offset and sets the initial selection, enabling
click-to-line from search results.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the hardcoded .gitignore + .clideignore read with the ordered
ignore_files: chain from .pql/config.yaml (D-4) — the single ignore
knob clide owns (D-3). readIgnoreFiles defaults to .gitignore (plus
.clideignore when present) when the config is absent or malformed, and
honours an explicit [] as "no file-based exclusions".
Add walkFiles + the files.walk command: a recursive, ignore-pruned,
capped flat file listing reused by quick-open (T-51) and the search
engine (T-52). Closes the never-filed ignore-layering placeholder in
files_commands.dart.
Co-Authored-By: Claude Opus 4.8 (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 31s
pql 1.5 returns exit 0 with an empty `[]` for zero matches (older pql
used exit 2), so the wrapper's "exit 2 = empty, not an error" carve-out
is obsolete — and risky, since a future exit 2 could mean a real error.
Any non-zero exit is now an error.
Also removed the repo's vendored .claude/skills/pql: it's generated by
`pql init` (which CONTRIBUTING already lists in setup, installing at user
scope), so a committed snapshot just shadows the current global skill
with stale content and drifts on every pql bump — this copy was a whole
version behind. Other vendored skills are clide-owned; pql's is pql's.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>