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>
Flips the Tier-0 stub into a real extension. Contributes a Terminal
tab in the workspace slot that spawns $SHELL -l via IPC pane.spawn,
feeds pane.output events into xterm.dart's Terminal, and routes user
input back through pane.write. Viewport resize propagates via
pane.resize. Surface for disconnected-daemon / exited-shell states so
there's no silent dead tab.
Knows nothing about Claude deliberately — the Claude-specific pane
(primary-per-repo, tmux-backed, D-041) lives under builtin.claude in
the next steps.
Co-Authored-By: Claude <noreply@anthropic.com>
ClidePtyView is a theme-bound wrapper around xterm.dart's TerminalView
— token-derived TerminalTheme, JetBrainsMono as the face, Semantics
live-region label so screen readers + Playwright both hear it. The
consumer (terminal / Claude extensions) owns the `Terminal` model and
wires IPC pane.write / pane.output → terminal.write() themselves; the
widget deliberately has no IPC dependency so it stays trivially
testable.
ClidePaneChrome is the shared pane header — title + subtitle + leading
icon + trailing widgets + optional close button. The close button is
null-conditional so primary Claude panes (D-041, landing in step 7)
can render without one.
xterm 4.0.0 added as a justified runtime dep + logged in
licenses.yaml per D-042. 3 new widget tests cover header rendering,
close-button presence, and the close-tap round-trip.
Q-023 records the SSH-remote-development question so the daemon + IPC
seams don't accrete local-only assumptions during Tier 1-5.
Co-Authored-By: Claude <noreply@anthropic.com>
PtySession wraps the ptyc helper: socketpair + Process.start + recvmsg
with SCM_RIGHTS for master-fd transfer, a background isolate that
loops on blocking read() and posts byte chunks, plus write/resize/
kill/close. close() SIGTERMs the child so the PTY's EOF wakes the
reader naturally; SIGKILL + fd close + isolate kill cover the edge
where the shell ignores SIGTERM — avoids the known Linux quirk where
closing an fd doesn't unblock an in-flight read() on it.
Env defaults stamp TERM=xterm-256color, COLORTERM=truecolor,
CLICOLOR_FORCE=1 so shells + tmux + Claude emit 24-bit sequences
that xterm.dart can render. User env (HOME / USER / SHELL) still
inherits via mergePtyEnv().
ffi: 2.1.3 added as a runtime dep — the FFI bindings for socketpair,
recvmsg, read/write, and ioctl(TIOCSWINSZ) need an allocator we're
not writing by hand. Justified in pubspec + listed in licenses.yaml
per D-042.
make test-core (ci/test_core.sh) runs the Flutter-free core tests
under a 120s hard timeout with setsid + process-group kill, wired
ahead of the fast app tests in push-check so a hung PTY test can't
wedge a pre-push. Current core suite: 24 tests in ~1s.
Co-Authored-By: Claude <noreply@anthropic.com>
test / unit + widget + golden + a11y (push) Successful in 1m30s
test / integration_test (xvfb) (push) Failing after 1m14s
test / bundle smoke (xvfb 5s) (push) Failing after 1m3s
test / daemon subprocess + web WASM smoke (push) Successful in 1m42s
Three sections instead of one flat list. `self:` carries clide's own
MIT license (rendered first in the About screen so the user knows
what they're running before the dependency list). `dependencies:`
covers artefacts that actually ship in the binary — JetBrainsMono,
JosefinSans, yaml. `dev_dependencies:` tracks build-time tooling
(mocktail, alchemist, flutter_lints, lints, test) for audit
completeness without polluting the user-facing About panel.
Also backfilled the two root-package dev deps that were missed the
first pass: `lints: 5.0.0` and `test: 1.25.8` (root pubspec, not
app/). Root LICENSE mirrored into app/assets/LICENSE + declared as
a bundled asset so the About screen can read it at runtime (Flutter
can't reference paths above the package root).
D-042 updated to describe the three-section split and the dev-vs-
runtime distinction.
Co-Authored-By: Claude <noreply@anthropic.com>
Every third-party artefact shipping in the clide binary — fonts,
Dart packages, native tools, bundled assets — now has an entry in
app/assets/licenses.yaml with name, kind, version, homepage, license
identifier, relative path to the bundled license text, and a
one-line purpose. The About screen (Tier 6) will render this file
verbatim. Seeded with the current set: JetBrainsMono, JosefinSans,
yaml, mocktail, alchemist, flutter_lints.
D-042 captures the two-step-commit rule (artefact + licenses.yaml
entry in the same changeset) alongside D-031's prefer-zero-deps
budget: preferring zero deps is the budget; licenses.yaml is the
visible consequence when the budget grows.
CLAUDE.md "Dependencies & supply chain" gains a matching guardrail
line. The per-dep license text files (OFL.txt for both fonts) are
declared as pubspec assets too so the About screen has something to
display, not just a manifest.
Co-Authored-By: Claude <noreply@anthropic.com>
Vendor the JosefinSans variable font (upright + italic, weight range
100-700), OFL licensed. _AppRoot installs it as the ambient
DefaultTextStyle at weight w300 (Light); ClideText inherits rather
than re-specifying the family, so Alchemist's Ahem injection keeps
goldens deterministic per D-024 while production renders the
intended face.
typography.dart gains clideUiFamily + clideUiFamilyFallback
alongside the existing mono constants; .gitignore learns about
Alchemist's failures/ diff output dir so local re-runs don't leak
into `git status`.
Co-Authored-By: Claude <noreply@anthropic.com>
Vendor the 4 core weights (Regular, Italic, Bold, BoldItalic) of
JetBrains Mono v2.304 under app/assets/fonts/jetbrains_mono/, ~1 MB
total. OFL license checked in alongside. Declared as the
JetBrainsMono family in pubspec so Flutter bundles it automatically.
app/lib/widgets/src/typography.dart exposes clideMonoFamily plus a
platform-ordered fallback chain (SF Mono / Menlo / Monaco →
JetBrains Mono system install / Fira Code / Hack / DejaVu / Liberation →
Cascadia / Consolas → monospace) for web builds and test harnesses
that don't load asset fonts. Terminal widgets pick both up via a
single import.
Co-Authored-By: Claude <noreply@anthropic.com>
test / unit + widget + golden + a11y (push) Successful in 4m34s
test / integration_test (xvfb) (push) Failing after 1m4s
test / bundle smoke (xvfb 5s) (push) Failing after 58s
test / daemon subprocess + web WASM smoke (push) Successful in 2m27s
CLAUDE.md gains a "Decision discipline" guardrail pointing at
decisions/<domain>.md, rewrites every inline ADR link to the new
anchor-link style (ADR 0001 → D-001, etc.), collapses the bottom
"Open questions" section to a one-line pointer, and updates the
parent-project note to reference decisions/architecture.md instead
of the deleted docs/ADRs/.
Three extension slots are reserved with id-only stubs, registered
alongside the existing 21 built-ins in main.dart:
- builtin.decisions — future sidebar for D/Q/R records
- builtin.tickets — future sidebar + kanban workspace
- builtin.claude-control — future .claude/ first-class surface
(Settings / Skills / Agents / Hooks / MCP). Distinct from the
existing builtin.claude PTY-pane stub.
`flutter analyze` clean; builtin + a11y test suites still pass.
Co-Authored-By: Claude <noreply@anthropic.com>
Three tests under app/integration_test/, run against the real built
app via the integration_test package. The load-bearing one is
app_starts_test.dart — boots ClideApp, waits for the root shell to
settle, asserts the three-column layout + welcome tab + statusbar
indicator all render. This is the "tests pass and then app fails to
start" gate that widget tests can't see because they never pump the
real bindings.
theme_picker_test.dart invokes theme.pick, asserts the modal opens,
selects a theme, asserts the modal dismisses.
extension_lifecycle_test.dart disables ipc-status via settings,
asserts its statusbar item unmounts, re-enables, asserts it
reappears.
Co-Authored-By: Claude <noreply@anthropic.com>
Four layers under app/test/:
* kernel/, extension/ — unit tests for every kernel service and
the extension contract. i18n fallback chain gets the full
matrix (exact/lang/default/placeholder, namespace isolation,
unknown namespace, interpolation). Theme resolver and
extension-manager topo sort covered.
* widgets/, builtin/ — widget tests for every primitive and each
Tier 0 built-in. Assertions reach into the Semantics node so
a missing label fails structurally, not visually.
* goldens/ — Alchemist + Ahem font for cross-platform pixel
stability; primitives only (button, tab bar, icon set). No
goldens for compositions — they'd churn through every tier.
* a11y/ — contract-level gate: semantic coverage walks the
built-in catalogue, contrast walks every token pair in every
bundled theme against WCAG-AA thresholds (catches real
regressions: the first run caught `tab.inactive_text` at 2.81
in summer-night), i18n coverage asserts every referenced key
exists in its catalog (exists-in-map, not "key == value" — too
ambiguous for keys that happen to equal their translation).
Helpers under helpers/: `KernelFixture.create()` boots a
KernelServices with in-memory stores + a FakeDaemonClient (no
socket, drivable connected-state), `widget_harness` wraps a
widget in the minimum tree that resolves theme + i18n.
Co-Authored-By: Claude <noreply@anthropic.com>
First real content under app/. Lays the whole Tier 0 foundation in one
commit because the pieces depend on each other circularly (kernel →
extension → widgets → built-ins all reference types from the layer
below); splitting would leave intermediate commits that don't compile.
Key shapes:
* bare WidgetsApp root — no Material, no Cupertino, no Scaffold.
ClideTheme InheritedWidget is the only source of tokens.
* ClideKernel InheritedWidget aggregates 18 services (settings,
project, extensions, theme, panels, events, ipc, commands +
palette + keybindings, clipboard, files, notify, dialog, tray,
secrets, os, net, focus, log, i18n). ExtensionContext exposes
them through a stable interface.
* ClideExtension + sealed ContributionPoint hierarchy (Tab,
StatusItem, Toolbar, Command, TrayItem, LayoutPreset). One
manifest ships N contributions into kernel slots.
* Three-tier theme pipeline: palette (named colors) → semantic
roles → ~60 surface tokens. Defaults at each layer so legacy
palette-only themes produce a complete SurfaceTokens.
* A11y baked in from day one — every interactive primitive wraps
in Semantics(label:, hint:, button:); ensureSemantics() at boot;
theme/contrast.dart helper exposes token pairs for the WCAG
gate in the a11y test suite.
* i18n ported from fframe's L10n pattern (text-driven, namespaced
JSON catalogs, caller-supplied placeholders) with a proper
locale fallback chain (exact → language → default → placeholder)
fframe lacks.
* Four Tier-0 built-ins live (default-layout, welcome, ipc-status,
theme-picker) plus 17 id-reserving stubs so later tiers fill in
without rename churn.
.gitignore extended to cover app/ sub-package artefacts and the
Playwright harness scratch dirs.
Co-Authored-By: Claude <noreply@anthropic.com>