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>
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>
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>
Two tests compared a raw Directory.systemTemp path against an OS-resolved
one, which diverge on macOS where /tmp is a symlink to /private/tmp:
FilesService.atCwd resolves the CWD, and the clide-cli e2e server must hash
the same canonical workspace the C client sees via getcwd. Resolve symlinks
on both sides.
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>
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 31s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 1m0s
Three security fixes the consultant flagged:
* git.checkout, git.push now reject branch/remote arguments starting
with `-` via a top-level validateGitRef helper. `git push` also
gets a `--` option terminator; checkout can't use `--` without
changing semantics (it would be parsed as a pathspec), so the
validator is the only line of defence there.
* files.read caps responses at 10 MB so a single call can't OOM the
UI on a multi-gigabyte log.
* git.log caps `count` at 1000; git.diff / git.stage cap paths at
256. Excess is a userError rather than burning subprocess time.
The bigger typed-schema framework (item 1 in T-104) is split out as
T-120 since it needs design discussion alongside T-99.
Co-Authored-By: Claude <noreply@anthropic.com>
resolveUnderRoot already blocked path-layer traversal but explicitly
did NOT follow symlinks — a repo symlink config -> /etc/shadow
passed the containment check because the link path was under root.
clide would then read the target.
Add resolveUnderRootFollowingSymlinks: resolves any symlinks at the
target and re-verifies containment against the resolved real root.
The split keeps pure path math testable without filesystem access.
files.read and files.ls now route through it.
Tests cover: plain non-symlink passthrough, non-existent target
(returns path-layer result so caller surfaces not-found cleanly),
single-hop and chained symlinks whose targets escape the workspace,
and tolerance of symlinks in the root path itself (macOS /tmp).
Also adds the T-101 CHANGELOG entry that the docs commit missed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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 1m1s
- editor_commands_test: insert / replace / set-content / save with no
active buffer return not-found (covers the _resolveId null branch
in each handler).
- files_commands_test: files.watch emits a files.changed event when
a file is created (covers the watcher.stream → events.emit wiring),
FilesService.atCwd's parent-walk fallback when no .git is found in
any ancestor.
Coverage: src/daemon/editor_commands.dart 88/100 -> ~95+;
files_commands.dart 64/70 -> 70/70.
Total coverage 92.33% -> 92.44%.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test / unit + widget + golden + a11y (push) Failing after 37s
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
Pane is now a pure data class — id, kind, pid, argv, cwd, title,
isClosed. The daemon-side PaneRegistry holds a parallel map of
PtySession keyed on id; registry methods look up both sides when
writing / resizing / closing.
The `clide.dart` barrel no longer re-exports `src/pty/*`,
`src/panes/registry.dart`, or the `*_commands.dart` modules — all
three transitively import `dart:ffi` which isn't available when
compiling to WebAssembly. The daemon entrypoint (bin/clide.dart) +
core tests import them via deep paths now. Pane / PaneKind /
DaemonEventSink / RecordingEventSink stay in the barrel since
they're pure data the Flutter app references over IPC.
Verified: `dart analyze` clean, 53 core tests green, 174 app tests
green, `make ui-smoke` compiles + serves + Playwright smoke passes,
daemon boots + ping round-trips + SIGTERMs cleanly.
Co-Authored-By: Claude <noreply@anthropic.com>
Flutter sidebar tab that lazy-loads the workspace tree via IPC
files.ls and refreshes subtrees on files.changed events. Click-to-
open routes through a future editor.open command; until Tier 2
registers it, the execute call no-ops gracefully.
Daemon side adds a new files subsystem:
- files.root returns the resolved workspace root (git root if
present, otherwise cwd)
- files.ls lists a directory with ignore filtering applied
- files.watch starts a recursive Directory.watch and fans
FileSystemEvents out as files.changed IPC events
- FilesService owns the watcher + ignore set lifecycle
IgnoreSet + IgnorePattern implement the common gitignore subset:
anchored (/foo), directory-only (foo/), negation (!foo), **
crossing dirs, ** at trailing position. Built-in layer hides clide-
owned dirs (.git, .pql, .clide, .dart_tool, build, node_modules);
.gitignore + .clideignore at the root layer on top per D-004. Full
multi-file ignore_files: layering from .pql/config.yaml is future
work.
11 new ignore-matcher tests + 5 files.* dispatcher tests.
Co-Authored-By: Claude <noreply@anthropic.com>