ADR 0005 supersedes ADR 0002. The "sidecar vs app" split was load-bearing on Go-vs-Dart being a meaningful boundary. Going all-Dart collapses that — the daemon becomes clide --daemon mode of the same AOT binary the CLI lives in, both sharing lib/ with the Flutter app. The one native gap (Dart's multi-threaded VM can't safely fork+exec) is filled by a small C supporter tool — ptyc, Project Terminal Controller, peer of pql — rather than introducing a second core language. ADR 0006 defines the CLI/event surface on top of that Dart core: subsystem list (pane/tab/editor/panel/tree/git/pql/canvas/graph/ theme/settings/project), command shape, versioned JSON event schema, pql-parity exit codes, and command-event duality as the operational form of user/Claude parity. Deleted: sidecar/cmd, sidecar/go.mod, every sidecar/internal package. Rewritten: Makefile (dart compile exe, flutter analyze/format/test, build-linux/build-macos, ptyc-build), ci/*.sh, .githooks/pre-push (no more GOBIN PATH dance), .gitignore (Flutter/Dart at repo root, ptyc section), project.yaml (drop module: and go_version:), CLAUDE.md (guardrails, dependencies, commands refreshed). Co-Authored-By: Claude <noreply@anthropic.com>
7.4 KiB
ADR 0005 — Dart core; sidecar directory dissolved; ptyc as pql-peer
Status: accepted Date: 2026-04-20 Supersedes: ADR 0002
Context
ADR 0002 picked Go for the sidecar/CLI on the reasoning that (a) the heavy work (PTYs, subprocesses, file watchers, git, IPC) belongs in a language separate from the UI layer, and (b) pql is Go so the muscle memory transfers. The Flutter app would talk to a Go binary over a unix socket.
On reassessment, two facts broke that reasoning:
-
What the "heavy work" actually is. Stripped of the PTY layer, the sidecar is I/O-bound glue around shell-outs (
git,claude,pql), a unix-socket server, JSON-lines framing, and a process table.dart:iocovers all of this cleanly. The Go-versus-Rust debate implicit in 0002 was the wrong axis — the real choice was separate process vs shared language, and separate-process is what matters (session persistence needs the daemon to outlive the app), not language. -
PTY is the one place Dart is genuinely weak — and not because of ecosystem, but because Dart's multi-threaded VM can't safely
fork(). That single constraint forces a native helper regardless of what language wraps it. Once you accept a small native helper, the question is whether everything else needs to be in that same native language. It doesn't.
So the "sidecar" directory stopped carrying weight. It existed to justify the Go/Dart split. With the split gone, the directory is ceremony.
Decision
Three moves.
1. Dart is the core language.
Everything that used to live under sidecar/ — IPC server, CLI
dispatch, process management, file watching, git shell-outs, pql
wrapper — is written in Dart. Two execution modes of one Dart AOT
binary:
clide <subcommand>— one-shot, pql-style. Parses args, connects to the running daemon socket, sends a request, prints JSON on stdout, exits with the pql exit-code contract.clide --daemon— long-running. Owns PTYs, subscriptions, file watchers, subprocess lifecycles. Started by the app on load; survives app restarts so Claude sessions persist.
The Flutter app imports the Dart core as a library for in-process state (views, widgets, models) and connects to the daemon over the same IPC the CLI uses. One protocol, two clients.
2. The sidecar directory dissolves.
app/ # Flutter UI (Linux / macOS primary)
lib/ # Dart core shared by app + CLI + daemon:
# ipc/, pty/, proc/, git/, pql/, events/, panes/
bin/clide.dart # Dart AOT entry: subcommand dispatch + --daemon
tool/ # Dart scripts used by the Makefile
tests/ # integration tests that span app + daemon
No sidecar/. No Go module. project.yaml drops module:; the
Dart package name replaces it.
3. ptyc is a pql-peer supporter tool.
The PTY helper — a small C binary that does posix_openpt + fork
exec+ fd-passing viaSCM_RIGHTS— graduates to the same status as pql: single-purpose, language-appropriate, standalone, reusable outside Clide. It lives in its own directory (eventually its own repo) and Clide wraps it the same way it wraps pql. Working name:ptyc.
- Clide shells out to
ptycto spawn every PTY (terminal pane, tmux session, claude, LSP server, debug adapter — all one code path). ptycwrites only what it needs to write (a PTY + forked child) and does nothing else. No IPC protocol of its own, no long-running state. One-shot per pane.- Consumers other than Clide (a Python script, another Dart app, a
Go tool) can use
ptycstandalone with no Clide dependency.
This mirrors ADR 0003's pql contract: wrap, don't duplicate; supporter tools stay independent and reusable.
Consequences
- ADR 0002 is superseded. Go sidecar removed. Existing
sidecar/contents (Go skeleton —cmd/clide/,internal/*,go.mod) are deleted; the ideas it encoded (exit-code contract, ldflag-stamped version, JSON diagnostics) are reimplemented in Dart. The supersession note stays in 0002 so the history reads correctly. - One toolchain for the IDE proper. Flutter + Dart. The C
toolchain is needed only to build
ptyc— a tiny, rarely-changing artifact. project.yamlsimplifies.module:andgo_version:go away. Aptyc_version:pin joins the existingdart_sdk:andflutter_channel:keys.- Supply-chain gates stay, shape changes. The Go gate
(
govulncheck) is removed. The Dart gate stays (advisories review- exact-pin
pubspec.yaml).ptycgets its own tiny gate: it has no deps beyond libc, so the review is "read the 150 lines before every bump."make securitybecomesmake security= Dart advisories +ptycreview checklist.
- exact-pin
- IPC stays. The daemon / app / CLI split is unchanged — unix socket, token auth, JSON-lines. It was never about language.
- Session persistence stays. PTY master fds live in the Dart daemon process, not the app process. App restart does not kill Claude.
- Pql continues as-is. Wrapped via shell-out from
lib/src/pql/(the Dart equivalent of the deletedsidecar/internal/pql/). No protocol change to pql. - CLAUDE.md and the Makefile need updates. Commands, directory references, and the "sidecar language: Go" guardrail all shift.
- Rust remains an escape hatch, not a plan. If a Dart limit
later forces a second native helper (file-watching at scale on
macOS, a tree-sitter host, etc.), the precedent set here is: new
native need → new supporter tool, peer of pql and
ptyc. Never a second "core language."
Notes
-
Name:
ptyc(pronounced "p-tic"). Three honest readings, all pointing at the same tool:- Project Terminal Controller — parallel to pql's Project
Query Language. Clide's supporter tools follow a
p*pattern wherep= project: pql handles project queries, ptyc handles project terminals. Future supporter tools that fit the "small single-purpose peer of pql" slot should follow the same pattern when the fit is natural. - PTY + child — domain vocabulary (PTY parent/child pair). This is what a reader seeing the name on a command line will decode it as, and it's exactly what the tool does: run a child process under a PTY.
- PTY + C — the implementation language. Accurate and
non-limiting; Unix has a long tradition of tools advertising
their implementation (
gcc,libc,musl). If we ever rewrote it in another language it would become a new tool with a new name, same as pql would if rewritten.
Crucially, none of the readings tie the caller to any ecosystem —
ptycis usable from Dart, Python, Go, shell, anywhere a subprocess can be spawned and a fd can be received. Alternatives considered and rejected:clide-pty-spawn(too clide-specific for a peer tool),dpty(already taken on crates.io),ptyspawn(verbose),dartmx(falsely signals caller-ecosystem + implies multiplex),ptyx(arbitrary suffix, no domain reading),ptycas read-only "PTY C" (works but sells the name short). - Project Terminal Controller — parallel to pql's Project
Query Language. Clide's supporter tools follow a
-
The helper's wire contract (stdin JSON → stdout JSON + SCM_RIGHTS fd transfer) is intentionally small so wrapping it is trivial from any language.
-
This ADR does not define the Clide CLI / event surface itself — that is ADR 0006.