143 Commits
Author SHA1 Message Date
jpmschweitzerandClaude Opus 4.8 1e8ee2c412 test(coverage): lift os/code-block/deeplink for gate headroom
The three worst-covered files were genuinely untested, not edge cases:
os.dart 27%→~85% (inject the process runner so openURL/reveal don't spawn a
real browser), clide_code_block 39%→~90% (expose the byte→char span mapper
as a top-level fn + render tests), deeplink 29%→~75% (the confirm-opens and
not-activated paths). Buys buffer above the 95% floor so a feature batch
doesn't immediately trip the gate. 95.01% → 95.22%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 22:01:08 +02:00
jpmschweitzerandClaude Opus 4.8 1d9000988d test(draw): cover the 2.9.0 drawing-card additions to clear the coverage floor
The drawing-card feature batch (icon/image/compare/graph/d2 cards, --stdin,
tool resolution) added widget + wiring code that dipped total coverage under
the 95% floor — surfaced by `make release` (push-check skips coverage). Cover
the gaps: ProblemsController.refresh, every SVG shape-type bbox + the style
vocabulary, _spawnD2 via a real /bin/cat, the icon-show bus path + error
branches, and quad/arc/close marker paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 22:00:56 +02:00
jpmschweitzer 9abcb0c30c release v2.9.0 2026-06-30 00:36:07 +02:00
jpmschweitzerandClaude Opus 4.8 2150a85b2f fix(cli): make clide draw reachable as an umbrella command (T-318)
draw is single-token (no verb), so `clide draw --file x` mis-parsed as cmd
"draw.--file" — the dispatcher registered draw but the argv parser never
learned it was umbrella (unlike icon/image which use subsystem.verb). Found
driving the live install; the handler unit tests bypassed the argv path so
they missed it. Added draw to _umbrellaCommands + a parse regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 00:28:28 +02:00
jpmschweitzer b0cc60735c chore(plan): split Tier-5 interactive panes (T-322/T-323) into epic T-506 2026-06-30 00:18:11 +02:00
jpmschweitzerandClaude Opus 4.8 bcef3a6fca feat(graph): deterministic force-directed layout solver (T-323)
Fruchterman-Reingold (clide-owned, no layout package): nodes repel, edges
attract, cooled over iterations into a readable layout. Deterministic — a
fixed circular seed, no RNG — so the graph view is stable across rebuilds
and the solver is unit-tested (6 cases). Flutter-free; the interactive graph
pane (render, pan/zoom, hover, filter, pql wiring) builds on this.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 00:03:44 +02:00
jpmschweitzerandClaude Opus 4.8 c167a37ea7 feat(draw): graph card template — circular node/edge layout (T-321)
graphTemplateHandler lowers a {nodes,edges} payload to SVG: a deterministic
circular layout of labelled <circle> nodes with <line> edges, self-contained
(own light backdrop + content colors, like a d2 card) and painted by the
shared renderer. Display-only (D-78); decoupled from the interactive graph
pane (T-323) since a static card only needs a layout->SVG. Honest error on
a duplicate id or an edge to an unknown node.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 00:00:44 +02:00
jpmschweitzerandClaude Opus 4.8 64410e8047 feat(cli): --stdin piped JSON payloads for structured commands (T-315)
The C client slurps stdin when it sees --stdin, strips the flag, and ships
the payload alongside the argv; the Dart unwrap folds it into the request
as a `stdin` arg (undeclared keys pass the schema untouched). icon.show and
image.show now read that payload as the peer of --file (stdin wins). Bounded
slurp + the envelope's existing size guard keep a huge payload from
corrupting the wire. C client builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 23:56:33 +02:00
jpmschweitzerandClaude Opus 4.8 1f6d31ac73 feat(draw): wire compare card image loading + register the template (T-319)
The drawing card now loads any <image> hrefs its SVG references into
ui.Images before painting (loadDrawingImages, decoder injectable for
tests) — the renderer and the lightbox both paint through the resolver, so
a compare card shows real images and zooms. main registers the `compare`
template with a workspace path resolver. A drawing with no images is
unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 15:02:21 +02:00
jpmschweitzerandClaude Opus 4.8 a651c9e026 feat(draw): compare template + aspect-fit image painting (T-319)
compareTemplateHandler lowers an `images` array to an SVG of side-by-side
<image> cells, each with the per-object data-label/description caption and
data-lightbox; paths resolve to absolute up front (injected, honest error
on a miss). _paintImage now aspect-fits (contain, centered) so cells of
differing-shape images don't distort. Flutter-free handler + painter pixel
tests. Card image-loading + registration next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 13:57:47 +02:00
jpmschweitzerandClaude Opus 4.8 281fb2a869 feat(icon): Phosphor glyph card — hero + real-UI-size strip (T-313)
The icon card renders each glyph at a hero size plus a continuous sample
strip (10–48px) so legibility is judged at the sizes the app uses, with the
entry's optional label + description and a per-entry or card-level color.
Wired end to end: icon.show publishes on the `icon` bus, the Claude
extension injects an IconMessage, and conversation_view paints the card
(display-only, D-78). en+nl catalogs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 13:10:53 +02:00
jpmschweitzerandClaude Opus 4.8 9a2b4e9eca feat(icon): icon.show handler + variadic positional schema (T-313)
icon.show resolves Phosphor glyphs by name (injected resolver) or a 0xNNNN
codepoint, reads a --file JSON array of {icon,label,description,color}
entries, validates colors via parseSvgColor (hex or CSS name), and
publishes on the `icon` bus channel. Honest userError on an unknown glyph,
a bad color, or a malformed payload. A trailing stringList positional is
now variadic so `icon show gear folder gauge` collects every token.
Flutter-free, dart-tested. Card widget + wiring next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 13:04:44 +02:00
jpmschweitzerandClaude Opus 4.8 f0fb5a5134 feat(image): --file metadata payload for annotated image cards (T-316)
`clide image show --file meta.json` reads a {path,label,description,caption}
payload so an image can carry a title and a longer description, not just a
one-line caption. ImageMessage + the image card render the richer metadata;
the bare `image show <path> [--caption]` form is unchanged. Honest userError
on a malformed/missing payload. Text annotation only (option a) — visual
marker overlays stay a follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 12:58:30 +02:00
jpmschweitzerandClaude Opus 4.8 5f8fa6af10 feat(draw): fold the d2 source into a "view source" disclosure (T-494)
The rendered diagram leads; the d2 source carries through the draw bus and
folds into a collapsed ClideCollapserCard beneath the card (D-78 display-
only). en+nl catalogs. Also switches the d2 test to a null-aware element.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 12:08:33 +02:00
jpmschweitzerandClaude Opus 4.8 64d77ec5dc feat(draw): d2 diagram template — compile d2 source to SVG (T-494)
The d2 drawing template compiles a diagram's source to SVG through the d2
binary (resolved via the D-104 path layer), then paints it with the same
renderer the svg card uses. `clide draw --file x.d2` infers the type from
the extension; `.svg` files render directly. Template handlers now return
a DrawResult so a compile failure or an unresolved d2 surface as an honest
userError with an install hint, not a generic "no SVG". Real d2 0.7.1
verified end to end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 11:19:48 +02:00
jpmschweitzerandClaude Opus 4.8 67edf732f1 style(main): dart format the supporter-binaries boot call (T-495)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 09:29:21 +02:00
jpmschweitzerandClaude Opus 4.8 eac9612a5b feat(tools): Tools settings category + re-detect for supporter binaries (T-495)
ToolsSettingsExtension adds a Tools settings category — a path field per
tool (app.tools.<name>) plus a Re-detect action (tools.detect) — and keeps
the live resolver in sync as paths are edited (supporterBinariesFrom). en
+ nl catalogs. Completes the D-104 UI surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 09:28:46 +02:00
jpmschweitzerandClaude Opus 4.8 bd4a8332af refactor(env): per-tool override keys for supporter binaries (T-495)
Store each tool's override under its own key (app.tools.<name>) instead of
one map, so the settings panel can bind a plain text field per tool.
loadSupporterBinaries reads the per-tool keys plus a first-run marker and
keeps an explicit path; redetectSupporterBinaries backs the Re-detect
action. The resolver itself is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 09:22:41 +02:00
jpmschweitzerandClaude Opus 4.8 3be063d6da feat(problems): surface stale supporter-binary pins (T-495)
The Problems panel flags a configured tool path (app.tools) that no longer
points at a file — a real misconfig, echoing D-58's soft floor. A merely-
unfound optional tool isn't flagged (its use-time userError covers that).
Extracted to a pure supporterToolProblems(), flutter-tested (3 cases).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 09:15:05 +02:00
jpmschweitzerandClaude Opus 4.8 62418b4010 feat(env): wire supporter-binary resolution at boot (T-495)
main resolves the override map (and first-run auto-detects, using the
primed login-shell PATH) right after bootSettings.load(), exposing the
process-wide activeSupporterBinaries that tool consumers read — the d2
template (T-494) will resolve its binary through it. Analyze clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 23:07:52 +02:00
jpmschweitzerandClaude Opus 4.8 0adccc5cd1 feat(env): first-run auto-detect + persist for supporter binaries (T-495)
loadSupporterBinaries reads the persisted tool→path override map; on first
run (key absent) it probes the known tools once and persists the result —
pinned thereafter, not re-detected each launch (D-104). Injected read/write
so it stays Flutter-free and dart-test covered (3 new cases). Boot wiring
next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 23:05:49 +02:00
jpmschweitzerandClaude Opus 4.8 dab0b3194e feat(env): supporter-binary resolver — explicit override → PATH → well-known (T-495)
SupporterBinaries resolves claude/d2/etc. to absolute paths (D-104): an
explicit override map first, then the login-shell/process PATH, then the
well-known bin dirs incl. Homebrew-on-Linux — the dir the standard PATH
expansion omits and a .bashrc-only brew shellenv misses (the D-104 gap).
detect() probes those to seed the override map (pinned, not re-probed each
launch); isStalePin flags a moved tool. Pure Dart, dart test (11 cases).
Settings storage + first-run detect wire on next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 23:03:12 +02:00
jpmschweitzerandClaude Opus 4.8 b1ec4afe15 feat(draw): data-lightbox tap-to-zoom on the drawing card (T-318)
A data-lightbox element gets a tap target (click cursor) over its region;
DrawingCard exposes an onLightbox callback, which the conversation wires to
open the whole drawing in a ClideLightbox (zoomable). Completes the
per-object overlay — captions + lightbox — and T-318's defined scope.
Widget-tested (the tap fires the callback).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:40:26 +02:00
jpmschweitzerandClaude Opus 4.8 3e43746c17 feat(draw): per-object data-* caption overlay on the drawing card (T-318)
DrawingCard stacks a caption layer over the SVG: each data-label /
data-description annotation renders a themed caption just below its
element, mapped to pixel space via a shared svgViewportFit (refactored out
of the painter so the overlay and the paint use identical viewBox-fit
math). Display-only (IgnorePointer). Widget-tested. data-lightbox is
captured in the model; the tap-to-zoom interaction is the last follow-on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:35:12 +02:00
jpmschweitzerandClaude Opus 4.8 78b0a7b8a4 feat(svg): extract per-object data-* annotations for the drawing-card overlay (T-318)
The builder captures data-label / data-description / data-lightbox on SVG
elements into SvgDocument.annotations, each with the element's AABB in
viewBox coordinates (accumulated transform applied — rect/ellipse/image/
line/poly/path bounded; groups skipped, text degenerates to its anchor
point). Feeds the Flutter caption/lightbox overlay next. Flutter-free,
covered by dart test (6 new cases).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:31:04 +02:00
jpmschweitzer 75f378ff20 chore(governance): Add D-105 to decision index in README.md
Auto-generated by pql when creating decisions/llm.md.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-28 22:18:53 +02:00
jpmschweitzerandClaude Opus 4.8 408fe01648 fix(draw): drop now-redundant draw_dispatch import in the command test (T-318)
draw_commands.dart re-exports DrawingRegistry, so the direct import is
unnecessary; cleared the analyzer warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:18:53 +02:00
jpmschweitzer 6ebb4ab32b chore(docs): Move Vibe CLI analysis to docs/spikes/
Relocate online-mistral-analysis.md → docs/spikes/vibe-cli-integration-analysis.md
Update D-105 references to new path.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-28 22:18:53 +02:00
jpmschweitzerandClaude Opus 4.8 7acea8baa0 feat(draw): wire the drawing card into the conversation — end to end (T-318)
The last wire: a DrawingMessage conversation item + the Claude-extension
subscriber on the `draw` channel (buildSvgDocument → inject), the
conversation renderer (DrawingCard), the fold/summary switches, the i18n
`drawing` label, and registering `clide draw` at boot (empty template
registry for now — primitive SVG works; d2/icon/compare/image handlers
plug in as they land). `clide draw --file doc.json` now renders a card in
the conversation. Render widget-tested; analyze + format clean across the
repo. Templates + per-object overlay remain; the CHANGELOG entry lands
with the user-complete feature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:18:53 +02:00
jpmschweitzer a5529577f2 chore(vibe): Add AGENTS.md for Mistral Vibe hybrid operation
Distills CLAUDE.md guardrails and .claude/skills/ into Vibe's
instruction hierarchy, preserving Claude Code punch.

Includes full pql skill integration for vault queries and
project planning workflow.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-28 22:18:53 +02:00
jpmschweitzer cb8840aff3 feat(llm): Add Vibe CLI integration initiative (T-497, D-105)
- Add D-105: Support Vibe CLI as opt-in alternative to Claude Code CLI
- Add T-497 initiative with 6 children:
  - T-498: Adapt StreamJsonProcess + session management
  - T-500: Update config system for per-repo LLM selection
  - T-501: LLMDriver abstraction + per-repo config schema
  - T-502: Verify Vibe CLI permission + tool behavior
  - T-503: Hybrid LLM test suite + regression checks
  - T-504: pql improvements for hybrid agent workflow

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-28 22:18:53 +02:00
jpmschweitzerandClaude Opus 4.8 14bbd1fd2d feat(draw): clide draw command — drive a drawing card from the CLI (T-318)
Mirrors image.show: a Flutter-free `draw` handler reads + JSON-parses the
document (injected reader), lowers it to SVG via the template registry,
and publishes {svg, label, description} on the `draw` MessageBus channel
for the Claude extension to inject. Honest errors — notFound (missing
file), userError (bad JSON / unknown template / no --file), toolError (no
live UI). Covered by dart test (7 cases). The extension subscription that
injects the card remains to wire.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:18:53 +02:00
jpmschweitzerandClaude Opus 4.8 ed5cd6e312 feat(draw): DrawingCard widget — SVG region + themed caption (T-318)
Display-only card (D-78): renders an SvgDocument in a framed, aspect-
ratio-sized region with an optional clide-themed label/description caption
beneath. The SVG is content (its own palette); the frame + captions are
chrome (SurfaceTokens). Widget-tested via the kernel fixture. Per-object
data-* overlay and lightbox remain follow-ons.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:18:53 +02:00
jpmschweitzerandClaude Opus 4.8 1fdba4aba5 feat(draw): template dispatch — lower a drawing-card doc to SVG (T-318)
resolveDrawingSvg lowers a DrawingCardDoc to an SVG string: primitive docs
use inline svg or read svgPath (via an injected reader); template docs use
a registered DrawingRegistry handler, so the d2/icon/compare/image children
plug in. Honest DrawErr on no source / unknown template / unreadable path /
empty output. Flutter-free, covered by dart test (7 cases). Not yet wired.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:18:53 +02:00
jpmschweitzerandClaude Opus 4.8 1a0a69df5f feat(draw): drawing-card document envelope model + parser (T-318)
DrawingCardDoc + parseDrawingCardDoc read the `clide draw` JSON: card
label/description, primitive svg/svgPath, or a template name plus its
fields (for a handler to lower to SVG). Tolerant — a non-object payload
is null, blank strings are absent, never throws. Flutter-free, covered by
dart test (10 cases). First slice of the drawing-card core (D-103); not
yet wired.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:18:53 +02:00
jpmschweitzerandClaude Opus 4.8 406da1e0c5 feat(svg): paint <image> via an injected resolver — completes the renderer (T-320)
paintSvg/SvgScenePainter/SvgView take an optional SvgImageResolver
(href → decoded ui.Image); image nodes draw into their dest rect. The
caller owns loading (file/asset/network), so the painter stays pure
rendering. Threaded marker + resolver through a small paint context.
Pixel-probe tested (image draws via the resolver; nothing without one).
The clide-owned SVG renderer is now feature-complete for T-320.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:18:53 +02:00
jpmschweitzerandClaude Opus 4.8 718d2b39c2 feat(svg): marker/arrowhead rendering for the drawing-card renderer (T-320)
Collect <marker> defs (refX/refY, orient=auto, markerUnits) and the path
marker-start/mid/end refs in the builder, then paint them at the path
endpoints rotated to the path tangent (orient=auto). The viewBox→viewport
scale is approximated 1:1 (holds for d2's markers). d2 edges now render
with arrowheads. Covered by dart test (builder) + a flutter-test pixel
probe (the green arrowhead draws). image painting remains the last T-320
follow-on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:18:53 +02:00
jpmschweitzerandClaude Opus 4.8 91607d54d1 feat(svg): CustomPaint renderer for the drawing-card SVG engine (T-320)
paintSvg walks the typed SvgDocument and draws it: shapes → ui.Path
(rect/rrect/ellipse/line/poly/path incl. arcs), text → TextPainter
(anchor + baseline), per-node Affine transforms, group opacity via
saveLayer, and viewBox fit (xMidYMid meet). Exposes SvgScenePainter and
the SvgView widget. image-href (async) and markers/arrowheads are deferred
follow-ons. Pixel-probe tested under flutter test (8 cases, incl.
rendering the real d2 fixture). Completes the clide-owned SVG renderer
core (D-103); not yet wired into a card.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:18:53 +02:00
jpmschweitzerandClaude Opus 4.8 1a3486708c feat(svg): typed scene model + document builder for the drawing card (T-320)
The builder ties the five parsers together: parseXml → inlineStyles →
a typed SvgNode tree (group/rect/ellipse/line/poly/path/text/image) with
viewBox, per-node Affine transforms, and inheritance-flattened SvgStyle
(fill/stroke/font resolved to ARGB; opacity per-node). Tolerant — a
non-svg root yields an empty doc; defs/marker deferred. An end-to-end test
runs the whole pipeline against a real d2-rendered SVG fixture. Flutter-
free, covered by dart test (17 cases). No user-visible behaviour yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:18:53 +02:00
jpmschweitzerandClaude Opus 4.8 ac387796c8 feat(svg): SVG colour + transform parsers for the drawing card (T-320)
parseSvgColor → packed ARGB (#rgb/#rgba/#rrggbb/#rrggbbaa, rgb()/rgba()
integer or %, common names, none/transparent → 0x0); null on unrecognised
so the caller can inherit. parseTransform → a composed 2-D Affine
(translate/scale/rotate[/about-point]/matrix/skew), applied left-to-right.
Both tolerant and Flutter-free, covered by dart test (22 cases). These feed
the typed node model next. No user-visible behaviour yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:18:53 +02:00
jpmschweitzerandClaude Opus 4.8 fb96e5238f feat(svg): XML tokenizer + inline-style normalizer for the drawing card (T-320)
Zero-dependency, tolerant XML reader (elements/attrs/text/comments/prolog/
CDATA/entities; <style> read as raw text) producing a generic element tree.
The normalizer folds d2/graphviz's class-based <style> rules into inline
presentation attributes — cascade presentation-attr < tag < class < style —
then drops <style>/class/style, so the painter only ever sees inline attrs
(D-103). Flutter-free, covered by dart test (26 cases). No user-visible
behaviour yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:18:53 +02:00
jpmschweitzerandClaude Opus 4.8 9cc3d68862 feat(svg): SVG path-data parser for the drawing-card renderer (T-320)
Parse the `d` attribute into an absolute-coordinate SvgPathSeg list,
lowering SVG shorthands to a small command set the painter can replay:
relative→absolute, H/V→lineTo, S→cubic and T→quad with control
reflection, arcs kept as arcTo. Tolerant — malformed tail returns the
understood prefix and never throws, so a bad diagram can't crash the
conversation. Flutter-free, covered by dart test. First slice of the SVG
engine (D-103); no user-visible behaviour yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:18:53 +02:00
jpmschweitzerandClaude Opus 4.8 87634898c0 docs(design): finalize SVG subset + inline-normalize CSS for T-320
Pin the bounded SVG subset from a real d2 sample + our templates (markers
for arrowheads in; mask deferred to v1-ignore; foreignObject/filters/
gradients out, so mermaid isn't a launch target). Class styling resolves
via an inline-normalize preprocess (flatten <style> classes to inline
attrs) so the painter stays a pure attribute renderer. Tool-PATH for the
d2 binary now resolves via D-104.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:18:53 +02:00
jpmschweitzerandClaude Opus 4.8 ed22160bbb chore(plan): persist T-495 id mapping
The pre-commit changelog export left ticket_idmap unstaged; commit it so
the T-495 label↔record_id mapping survives a rebuild.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:18:53 +02:00
jpmschweitzerandClaude Opus 4.8 99cebc6d11 docs(governance): D-104 — explicit supporter-binary path overrides (T-495)
clide resolves external supporter binaries (claude, d2, future tools)
via an explicit user-scope tools: map (name → absolute path), consulted
first. First run auto-detects each tool once and writes the discovered
absolute paths into the map — a one-time populate, pinned thereafter, not
a per-launch heuristic. Escapes the login-shell-probe brittleness (T-439
misses brew when shellenv lives only in ~/.bashrc) while staying zero-
config for standard installs. User-scope only (D-93); generalizes D-58's
CLIDE_PQL_BIN override. Filed T-495; gates the d2 template (T-494).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:18:53 +02:00
jpmschweitzerandClaude Opus 4.8 d036e6b9b0 chore(plan): persist drawing-card epic re-sequencing (D-103)
The pre-commit changelog export didn't re-stage the dependency edits from
this turn (blocker flip T-320↔T-318, template blockers, T-494). Stage
them explicitly so the re-sequencing persists.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:18:53 +02:00
jpmschweitzerandClaude Opus 4.8 e2d9542fe8 docs(design): drawing-card wireframe set + SVG-substrate schema (T-317)
Seven Frame0 wireframes for the conversation drawing-card family — image
annotation, icon glyph, svg, d2, compare, graph, and the core dispatch
shell — plus the draft JSON schema refined from them. Schema follows the
SVG-substrate model (D-103): document envelope, bounded SVG subset, a
data-* Flutter overlay for captions/lightbox, templates lower to SVG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:18:53 +02:00
jpmschweitzerandClaude Opus 4.8 e9f394ff0f docs(governance): D-103 — drawing-card primitive layer is SVG (amends D-91)
The unified drawing card's primitive layer is SVG, not a bespoke
{type:"rect"} JSON vocabulary; the clide-owned CustomPaint SVG renderer
(T-320) is the engine the rest builds on. Templates lower to SVG; a thin
Flutter overlay carries label/description captions + lightbox, anchored
via data-* attributes; the graph template stays a live widget. Inverts
the former T-320 → T-318 dependency. Raised by the user: "if we are close
to svg, are we not better off extending svg instead?"

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:18:53 +02:00
jpmschweitzer ab9f93b541 Update README.md
changed readme to mention added windows build
2026-06-28 15:37:57 +02:00
jpmschweitzerandClaude Opus 4.8 89b7f99a8d build(ci): make dugite-check — track dugite-native upstream for security (T-88)
D-59 requires tracking dugite-native (bundled git) releases for security
updates, but it lived only in the D-record prose with no mechanism (clean-house
flagged RULE-SUNSET-WITHOUT-TICKET). Add ci/check_dugite_version.sh + a
`make dugite-check` target: compares the Makefile DUGITE_VERSION pin against the
latest desktop/dugite-native release and loudly flags CVE / security mentions in
the notes. Informational (not a push gate) — the bump itself is manual per D-63
and automated by T-25.

The script header is the maintenance home (dugite is fetched, not built, and
native/dugite/ is gitignored, so there's no BUILD.md to record it): cadence
(quarterly / on a git CVE), advisory subscriptions, and the bump procedure.
D-59's cost line + a Makefile comment now point at the mechanism. T-88 stays
open as the recurring calendar; first check (2026-06-28) shows v2.53.0-3 current.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 15:12:50 +02:00
jpmschweitzerandClaude Opus 4.8 066197c5e1 fix(i18n): stop warning on missing tool.name.* keys for proper-name tools (T-493)
Tool display names are open-ended (Bash, Grep, Glob, ScheduleWakeup, MCP tools,
…) — they intentionally have no catalog key and fall back to the raw name, so a
miss isn't a gap to fix. The conversation pane was logging an i18n "missing key"
warning for each, cluttering the output dock.

Adds a `warnIfMissing` flag to I18nService.string (default true, so fixed UI
strings still warn on a real gap) threaded through the ClideSettings facade;
_toolNameLabel passes false. Display is unchanged — the placeholder already
rendered the raw name.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 14:58:12 +02:00
jpmschweitzerandClaude Opus 4.8 e5976ad6b1 test(menubar): cover githubGet against a loopback server (T-47 P1)
The injectable-fetch tests skip the real HTTP path; exercise githubGet's
200-body and non-200-throws branches against a localhost HttpServer so no test
hits the network and the gate stays green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 12:18:28 +02:00
jpmschweitzerandClaude Opus 4.8 7c7a91545e feat(menubar): manual "Check for updates" in the About box (T-47 P1)
Help → About gains a "Check for updates" button that fetches the latest GitHub
release, semver-compares it to clideVersion, and shows the result inline:
up-to-date, available (with a tappable link to the release notes), or a clear
error. clide's first and only outbound HTTP call — a plain GET with no user
data, run ONLY on this explicit tap, never on a launch path or a timer. So it's
D-64-clean with no amendment; a background/periodic poll stays deferred (would
need the narrow opt-in amendment first).

The fetch is injectable so no test touches the network. compareSemver handles
2.3.10 > 2.3.9 and ranks pre-releases below their release. Closes T-492 (P1);
the release-channel CI for downloadable signed packages is T-491, and download/
apply (P2/P3) depend on it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 12:17:29 +02:00
jpmschweitzerandClaude Opus 4.8 095c45a023 feat(markdown): live-sync read-mirror of the open editor buffer (T-36)
D-50 behavior 4: opening a renderable .md in the editor now auto-reveals the
context-panel reader, which mirrors the buffer read-only and re-renders as the
user types — rather than a one-shot disk read.

The extension reveals the tab on editor.opened (renderable only — D-50 behavior
5 leaves non-.md files alone). The viewer owns the mirror: on mount it picks up
the active buffer (editor.read, no id), enters mirror mode on editor.opened /
active-changed for a renderable file, re-reads the in-memory buffer on
editor.edited, and drops the mirror (with its edit affordance back) on a disk
load or a switch to a non-renderable buffer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:08:31 +02:00
jpmschweitzerandClaude Opus 4.8 84f4b20cb8 feat(claude): nudge fresh sessions to load the pql + clide skills (T-490)
A fresh Claude session now gets a one-line preamble (clideSkillsNote) telling it
to reach for the bundled pql + clide skills from its first turn rather than
rediscovering the workflows. Layered onto clideContextNote in the single
--append-system-prompt.

Gated on a NEW session (!spec.resume && !spec.isFork): new tabs and the
post-/clear respawn spawn with resume:false and get it; the account-change
respawn (T-480) and real resumes carry prior context (resume:true), and forks
inherit their source — none are re-nagged. Both skills already ship (pql
user-scope, clide repo-scope), so no precursor was needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 10:53:08 +02:00
jpmschweitzerandClaude Opus 4.8 a46677facf feat(welcome): initialize a non-repo folder as a project (T-489)
Closes the new-project story (T-486). The dead-end "not a git repo" dialog now
offers to initialize the folder: project.init runs git init + a non-clobbering
scaffold, then opens + announces on projectCreatedChannel so the account
roadblock fires — the same path a brand-new project takes. Adds initExistingProject
+ the `clide project init [--dir]` verb (default: the current workspace).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 08:42:02 +02:00
jpmschweitzerandClaude Opus 4.8 25430686cc feat(welcome): New project flow + per-repo account roadblock (T-488)
The UI half of the new-project flow (story T-486). A "New project…" welcome
action opens a dialog (location + name) that dispatches project.new (T-487),
opens the result, and announces it on projectCreatedChannel. The Claude
extension consumes that and shows the account roadblock — the embedded
per-workspace picker + accounts list, so a fresh project gets bound to an
account (or Default) right at birth.

The two halves stay decoupled: the welcome builtin only publishes the event
(no claude import); the claude builtin owns the account dialog. Only freshly-
created projects announce, so existing opens are never prompted.

Closes T-488. The non-repo "initialize as a project" path (T-489) is next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 08:35:30 +02:00
jpmschweitzerandClaude Opus 4.8 adaf8966dc feat(cli): clide project new — create + git-init a new project (T-487)
The backend half of the new-project flow (story T-486). createNewProject
validates the name, makes <parent>/<name>/, runs git init (injected from the
toolchain in main.dart so the handler stays Flutter-free), and writes a minimal
scaffold (.gitignore + a CLAUDE.md stub). The project.new verb wraps it; --dir
defaults to the current workspace's parent so a new project lands beside it.

Create-only by design — opening the new workspace and the account roadblock are
the UI flow's job (T-488). Closes T-487.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 08:23:18 +02:00
jpmschweitzerandClaude Opus 4.8 b9fc720f97 feat(git): GitClient.init for the new-project flow (T-487)
clide treats a git repo as the workspace, so creating a new project starts with
`git init`. Add GitClient.init — `git init -b <main>` in workDir, deterministic
default branch, idempotent on an existing repo. The first primitive of the
new-project flow (T-486); the create-dir + scaffold service and the
`clide project new` verb build on it next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 08:19:10 +02:00
jpmschweitzerandClaude Opus 4.8 6d7b96f115 chore(plan): re-scope T-486 — account picker at new-project init, not welcome
Per the design pivot: the account choice is only a real decision at project
birth, so it belongs as a roadblock in the new-project / init-in-a-new-dir flow
(evolving the dead-end _NotARepoDialog), not a welcome-screen list. Opening an
existing project never prompts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 08:06:42 +02:00
jpmschweitzerandClaude Opus 4.8 8a9167bdfd test(claude): cover remove-nonexistent + list live-update for the 95% margin
Two more real branches the account feature left uncovered: removing an
unregistered account (the no-such-account error) and the registry list
rebuilding when a CLI-side registration notifies the shared settings store.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 01:19:50 +02:00
jpmschweitzerandClaude Opus 4.8 da82b48f9b test(claude): cover the per-repo account branches to clear the 95% gate
Adds the missing-line coverage the epic's new code left uncovered: Account /
DetectedAccount value equality, the set/unset no-workspace error branches, the
login dialog's escape-to-close, and the settings controls' live-update /
no-workspace / duplicate-add / re-login paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 01:13:03 +02:00
jpmschweitzerandClaude Opus 4.8 53b93f092b test(claude): scope the Claude-category select assertion to the defaults fields
The Account section (T-482) adds custom-kind controls to the Claude settings
category, so the T-457 test can no longer assert that *every* field in the
category is a select. Scope the kind/options check to the new-session-default
fields (model/effort/permission) it was actually about.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 00:57:26 +02:00
jpmschweitzerandClaude Opus 4.8 996b10d3ec feat(claude): account login pane — host claude login in a modal terminal (T-485)
Completes the `login` verb + the UI add/re-login affordances. The
accountActionChannel 'login' action opens ClaudeLoginDialog — a modal hosting a
TerminalPane that runs `CLAUDE_CONFIG_DIR=<dir> claude login`, so the CLI drives
the OAuth browser flow and credentials land in that account's config dir (D-64:
one CLI-initiated browser flow, on explicit action).

TerminalPane gains optional argv/env/cwdOverride (default stays the login
shell), and its pane.spawn carries env when set. The login pane reuses the
TerminalPane *widget* — a code import, not a runtime dependency, since it spawns
through the always-present pane.spawn IPC rather than the terminal extension.

Tests cover the parameterized spawn args and the dialog's host wiring (title,
CLAUDE_CONFIG_DIR, close). Closes T-485.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 00:52:41 +02:00
jpmschweitzerandClaude Opus 4.8 222d4ae5e3 feat(claude): per-repo account badge in the Claude pane chrome (T-481)
The always-visible affordance for the multi-account epic: a compact badge in
the pane header showing which account this workspace is bound to (or "default"),
colour-tinted per account so two windows are distinguishable at a glance. Tapping
opens a picker of the registered accounts + Default; selecting binds/unbinds via
the shared bindWorkspaceAccount helper (respawn + lock-sync follow on the bus).
Hidden when no accounts are registered, so it adds no chrome for users not using
the feature.

accountAccent derives the tint by hashing the name into a fixed set of theme
tokens — never an arbitrary colour, so the palette stays theme-owned.

The welcome-view accounts section (the other half of T-481) is split to T-486:
the welcome builtin is intentionally decoupled from feature builtins, so it needs
a welcome-section contribution point rather than importing claude directly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 00:43:40 +02:00
jpmschweitzerandClaude Opus 4.8 5c31190c4f feat(claude): Settings → Claude accounts registry list (T-482 part 2)
Completes the account settings surface with the global Accounts list under the
Claude category: each registered account shows a sign-in dot, name, and config
dir, with re-login and remove affordances plus an inline add-account field.

- Sign-in status: accountIsSignedIn, a read-only probe (a .credentials.json, or
  an oauthAccount marker in .claude.json; under-reports on macOS keychain).
- Add: registers ~/.claude-<name> and publishes a login action (T-485 opens the
  pane).
- Remove: registry-remove, guarded while a workspace is bound — matching the CLI
  `account remove`. --purge dir deletion stays on the CLI flag.

All management routes through the AccountRegistry + accountActionChannel, and
the list rebuilds live off the settings notifier. en/nl strings + widget/probe
tests added. Closes T-482.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 00:35:47 +02:00
jpmschweitzerandClaude Opus 4.8 c3efcf3855 feat(claude): Settings → Claude per-workspace account picker (T-482 part 1)
A custom settings control under the Claude category: a dropdown of the
registered accounts plus Default. Picking one binds (or unbinds) this
workspace via the AccountRegistry and publishes set/unset on
accountActionChannel — the same channel the CLI verbs use, so the session
respawns onto the account (T-480) and the IDE lock re-syncs (T-479). It reads
live off the shared settings notifier, so a CLI `account set` updates the
dropdown too. Empty states cover no-workspace and no-accounts, each pointing at
the CLI.

Registry writes set the in-memory binding synchronously then flush, so the
control publishes the bus event before the disk write completes.

Part 2 (the global Accounts registry CRUD list + sign-in probe) is still open
on T-482. en/nl strings added; widget test covers the states + bind/unbind.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 23:46:43 +02:00
jpmschweitzerandClaude Opus 4.8 e86543d476 fix(ipc): write the /ide discovery lock into bound-account config dirs (T-479)
A `claude` started with CLAUDE_CONFIG_DIR=<dir> looks for clide's /ide
discovery lock under <dir>/ide, not ~/.claude/ide — so today a per-repo
account session (T-476) can't reach clide's IDE bridge.

McpServer now reconciles a SET of lock paths instead of one: the default
~/.claude/ide always, plus the bound account's <dir>/ide when this workspace
is bound (boundConfigDir, injected from main.dart via the AccountRegistry).
syncDiscoveryLocks() writes/removes locks to match the active set; it runs on
start, once post-boot when the registry becomes resolvable, and on every
accountActionChannel event, so binding/unbinding adds/removes the account lock
at runtime. stop() sweeps every path written — no orphans. Lock content +
0600 are identical across dirs.

Acceptance 1-4 and 6 are covered by mcp_server_test (multi-dir write, no-binding
regression, dynamic add/remove, full cleanup); #5 (a live claude resolving the
bridge end-to-end) is manual.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 21:25:19 +02:00
jpmschweitzerandClaude Opus 4.8 34b9690005 feat(claude): respawn panes on account set/unset + safe --purge (T-480 part 2)
The extension consumer for the per-repo account verbs, making set/unset
fully functional. The Claude extension subscribes to accountActionChannel:

- set / unset → ClaudeSessionOrchestrator.respawnForWorkspace(cwd): closes the
  workspace's solo sessions (awaiting real process death, T-437) and re-spawns
  each on the same id with --resume, so the conversation continues under the
  newly-bound CLAUDE_CONFIG_DIR (resolved at spawn by agentBootstrap). Team and
  forked sessions are skipped — re-joining the broker / re-forking on an account
  swap is out of scope; they adopt the account on their next natural spawn.
- remove --purge → deletes the config dir behind isPurgeableAccountDir, a strict
  guard that only ever removes a ~/.claude-* directory that is a direct child of
  $HOME. The purge payload now carries the dir (the account is gone from the
  registry by publish time).

login still only publishes its action — spawning the `claude login` terminal
pane needs argv+env terminal-pane support and is split to T-485.

Covered: respawnForWorkspace (respawn solo, skip fork/other-repo) and the purge
guard's accept/reject matrix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 21:18:49 +02:00
jpmschweitzerandClaude Opus 4.8 744b6cff43 feat(cli): clide claude account verbs — add/list/login/set/unset/remove (T-480 part 1)
The CLI half of the multi-account feature (epic T-476; D-6 parity). A new
`claude.account` dispatcher command multiplexes the six sub-verbs over an
injected, Flutter-free AccountStore port (runs under `dart test`):

- add <name> [--dir]   register (default ~/.claude-<name>); idempotent, clear
                       conflict error
- list                 {accounts, boundAccount (this workspace), detected}
- set <name>           bind this workspace (persists)
- unset                clear this workspace's binding
- remove <name> [--purge]  registry-remove; refuses while any workspace is
                       bound
- login <name>         (publishes the login action)

Registry reads/writes go through the user-scope SettingsStore; side-effects
that only the UI layer can do — respawn on set/unset, the `claude login`
terminal pane, and the --purge rm — are published on accountActionChannel for
the Claude extension to consume (that consumer is T-480 part 2). main.dart
adapts the real AccountRegistry to the port and registers the command alongside
image.show / status.

Adds SettingsStore.keysAt (binding enumeration) and AccountRegistry.boundName /
boundAccountNames. No changelog yet — set/unset don't auto-respawn until part 2,
so the feature isn't user-complete. Verb behaviour + payloads + the in-use
guard are unit-tested against a fake store.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 17:30:01 +02:00
jpmschweitzerandClaude Opus 4.8 1fa7dee863 feat(claude): spawn hosted sessions under the bound account's CLAUDE_CONFIG_DIR (T-484)
The load-bearing piece of the multi-account epic (T-476): a bound workspace's
hosted claude now spawns with CLAUDE_CONFIG_DIR set to that account's dir, so
it runs under the bound account end-to-end. Every hosted session (primary /
secondary / fork / teammate) inherits it — the orchestrator already routes all
spawns through agentBootstrap.

- New pure resolver claudeConfigDirForWorkspace(cwd, boundConfigDir, env):
  bound account dir > parent CLAUDE_CONFIG_DIR (respect the launcher) > null
  (Claude defaults to ~/.claude). The registry is injected as a plain lookup
  so agent_bootstrap stays Flutter-free (its tests run under `dart test`).
- agentBootstrap merges CLAUDE_CONFIG_DIR BEFORE base, so an explicit
  SpawnSpec.env override still wins (override > binding > parent > unset); the
  key is omitted entirely when the resolver returns null.
- Orchestrator carries an optional AccountRegistry; the claude extension builds
  it from ctx.settings. Null in tests → no injection (unchanged behaviour).

No way to SET a binding yet (that's the CLI T-480 / settings UI T-482), so no
changelog entry — the mechanism is in place, the surface lands next. Unit tests
cover the resolver's four states and the envDelta precedence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 17:00:06 +02:00
jpmschweitzerandClaude Opus 4.8 a8d321f344 feat(claude): per-repo account registry + storage foundation (T-483)
Foundation for the multi-account epic (T-476): a Flutter-free AccountRegistry
over the app-scope SettingsStore (per-user, never committed to a repo).

- `app.claude.accounts` — the {name, dir} account list (dir = CLAUDE_CONFIG_DIR).
- `app.claude.account.<hash>` — per-workspace binding to an account name; the
  hash is the SAME FNV-1a 64-bit hex D-70 uses for the socket path, trailing
  separators stripped so /repo and /repo/ map alike.
- accountForWorkspace(cwd) resolves an account or null (a binding to a removed
  account degrades to null = Claude's default, never errors).
- probeExistingAccountDirs(home): read-only bootstrap probe for adoptable
  ~/.claude-* config dirs (welcome-view UX is T-481).

Keys are app.-prefixed because SettingsStore requires a scope prefix; app scope
already provides the per-user persistence T-483 wants (T-356 will consolidate
more state later but isn't a hard blocker). No spawn/UI/CLI — downstream
tickets. Unit tests cover CRUD, the disk round-trip, hash/trailing-slash
mapping, independent bindings, and probe filtering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 16:42:39 +02:00
jpmschweitzerandClaude Opus 4.8 c6adccc5de build(make): route build through build-<os> instead of duplicating the flutter build
`build` ran `flutter build $(FLUTTER_OS)` inline while build-linux/-macos/
-windows ran the same command separately — two places to keep in sync. Make
`build` depend on `build-$(FLUTTER_OS)` so the per-OS target is the single
source of the flutter-build command; `build` just adds the C-client bundling
on top. `make install` → build → build-<os>, and `make build-linux` still
works standalone (release.sh + CI reference it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 16:32:44 +02:00
jpmschweitzerandClaude Opus 4.8 679b5ba9d9 feat(cli): clide instances / instance verbs for instance discovery (T-247)
Closes the observability half of T-247: a way to find and identify running
clides. `clide instances` scans the runtime socket dir, probes each live
*.sock, and prints its identity (version/pid/workspace/socketPath) as jsonl;
dead sockets are skipped. `clide instance` reports the one you're connected
to. Combined with CLIDE_SOCK honoring (this same ticket), you can now list
instances and pin the CLI to a chosen one.

Server: a new `instance` dispatcher command (registered in buildDispatcher
with the live workspace/pid/socket) returns the identity map; added to the
argv translator's umbrella set so a bare `instance` token routes to it.
Client: a POSIX dir-scan in clide.c (Windows stub until it ships).

Tests: e2e `instances` lists the live test server with its identity; a
cc-free unit test covers the `instance` command shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 15:51:13 +02:00
jpmschweitzerandClaude Opus 4.8 ceab497ba8 fix(ipc): sweep dead orphan sockets from the runtime dir on startup (T-247)
The runtime socket dir accumulated orphaned *.sock nodes from crashed
instances — only the current workspace's own path was ever cleaned. Add a
best-effort startup sweep that probes every *.sock in the dir and unlinks
only the dead ones; live instances (something answers) and unresponsive
nodes (possibly hung) are left alone. Runs before bind, alongside the
existing per-workspace stale-unlink. Never blocks our own startup on a
sweep failure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 08:15:05 +02:00
jpmschweitzerandClaude Opus 4.8 6af980f31c fix(cli): honor CLIDE_SOCK as an explicit target, fail loudly if dead (T-247)
The `clide` shell client only ever computed the socket path from the
workspace's FNV-1a hash — it never read CLIDE_SOCK, so the var the app
exports to spawned agents was a no-op and a bogus CLIDE_SOCK was silently
ignored (it still hit the discovered instance). Now: when CLIDE_SOCK is set
the client connects to it directly, beating discovery, and a dead socket
aborts with EX_UNAVAILABLE rather than falling back to a different instance
(the split-brain footgun). Unset → the deterministic per-workspace path
(D-70) as before. The server keeps deterministic binding; this is a
client-side targeting override only.

Also fixes a pre-existing -Wstringop-truncation warning in
find_workspace_root (strncpy+manual-null → snprintf).

Tests: the e2e suite now clears the inherited CLIDE_SOCK so discovery tests
stay hermetic (the suite may run inside a clide instance), plus two new
tests — a valid CLIDE_SOCK pins from a non-git dir, a dead one fails loudly
and returns no data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 08:11:55 +02:00
jpmschweitzerandClaude Opus 4.8 3cbe0e6ec6 refactor(claude): drop the redundant Team-tab account budget (T-158)
After live review: the shared account /usage budget read as redundant on
both the Activity and Team tabs. Usage is per-account and can't be split per
member, so one place is enough — it lives only on the Activity tab, next to
the refresh control that fetches it (T-415). Removes the Team-tab account
card, its usage prop wiring, the team.section.usage/team.usage.shared i18n
keys, and the now-obsolete team_tab test. The three-tab card facelift stays.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 07:56:09 +02:00
jpmschweitzerandClaude Opus 4.8 a72906d25d chore(plan): reconcile T-477/T-478 label collision from the merge
The merged T-476 breakdown reused T-477/T-478 (already held by the
window-size fix + Summer Night theme). Relabel the two per-repo-account
children to T-483/T-484 (identities/record_ids unchanged), preserving the
labels already referenced in CHANGELOG and pushed commits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 07:45:10 +02:00
jpmschweitzerandClaude Opus 4.8 026156261c Merge origin/main: T-476 per-repo-account breakdown
Union-resolved the .pql/changelog logs (append-only, idempotent INSERT … ON
CONFLICT DO UPDATE — latest-updated wins on replay), keeping both sides:
this session's T-158 changes and origin's T-476 6-child breakdown. pql.db
rebuilt clean (2785 statements).

NOTE — label collision to reconcile (record_ids intact, labels only):
the breakdown reused T-477 and T-478, already held by the window-size bug
and the Summer Night theme. Reconcile with `pql ticket relabel`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 11:36:03 +02:00
jpmschweitzerandClaude Opus 4.8 763d628fc9 feat(claude): shared account budget in Team tab + meta-sidebar card facelift (T-158)
Implements T-158 option A: the forwarded /usage budget (already parsed for
the Activity tab, T-415) now also renders in the Team tab as a single
ACCOUNT card, captioned "shared across the team". Usage is per-account —
every team session shares one ~/.claude login — so it's shown once, not
split per member (which would just repeat identical numbers).

Facelift: the Activity, Team, and Config tabs now render their sections as
elevated cards (panelHeader fill, dividerColor hairline, 6px radius) under
small-caps mono headers, matching the settings overlay's card design
(settings_category_view). Shared helpers — metaSectionHeader / metaCard /
metaCardRow — live in meta_sidebar/models.dart; the SESSION control strip
and the pinned Config SETTINGS block adopt the same card. The shared
ClideAccordion (also used by decisions/tickets) is left untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 11:31:50 +02:00
Jeroen SchweitzerandClaude Opus 4.7 3f627c5b17 chore(plan): break T-476 down into 6 children under an epic
Convert T-476 from story to epic and file the implementation slices:
T-477 storage/registry/migration, T-478 spawn-time CLAUDE_CONFIG_DIR
injection, T-479 multi-dir /ide lock writer, T-480 clide CLI verbs
(D-6 parity), T-481 pane badge + welcome accounts UI, T-482 settings
rows. Blocker graph wired (T-356 -> T-477 -> fan-out -> T-480 -> UI).
Epic body refined with the proven mechanism, surveyed alternatives,
concrete code touchpoints, and acceptance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-26 09:50:57 +02:00
jpmschweitzerandClaude Opus 4.8 c4be179a87 chore(plan): record T-158 decision — option A (shared-account team-tab budget)
Locks scope to the single account-wide budget section in the team tab; declines
the per-member split (not meaningful under one shared account) and the
close-as-done option. Not started.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 09:01:21 +02:00
jpmschweitzerandClaude Opus 4.8 367792490d chore(plan): refine T-158 (team-sidebar usage budget) implementation plan
Grounded against the current tree: T-415 already ships the account budget
(parseUsageText/ClaudeUsage + Activity tab). Key finding — usage is
per-ACCOUNT (one shared ~/.claude login across all sessions), so a literal
per-member split would repeat identical numbers and isn't meaningful even
under T-476 (per-repo, not per-session, login). Refinement recommends a
single shared-account section in the team tab (resolves Q-34), lists the
exact files to touch, alternatives, and acceptance. No code yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 08:59:50 +02:00
jpmschweitzerandClaude Opus 4.8 259562f8cc docs(git-commit): document the release tagging step (T-393)
The "Cutting a release" ritual moved the changelog and bumped pubspec but
never mentioned tagging — which is how tags silently lapsed from v2.2.0
through v2.8.0. Add step 6: run `make release` (verify + gate + annotated
tag), then push with --follow-tags, and state the rule that every released
version must have a matching git tag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 23:27:13 +02:00
jpmschweitzerandClaude Opus 4.8 9a5f5b2ab5 build(hooks): widen pre-push fast path to test/ci/.githooks + guard test (T-393)
The fast-path regex matched only `^(lib/|pubspec\.)`, so a push that ONLY
changed a test, a ci/ gate script, or the pre-push hook itself skipped the
entire ~2min suite — exactly the paths most able to break the suite or the
gate went unchecked. Widen the trigger to include test/, ci/, and
.githooks/, and extract it to a single `trigger_re` variable. Also drop the
header comment's false claim that "release CI runs it forced on a tagged
version" — there is no release CI.

Add test/tooling/pre_push_hook_test.dart: it reads the live trigger_re from
the hook and asserts the load-bearing dirs force the full gate while
docs/assets ride along — so the regex can't be silently narrowed again.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 23:26:52 +02:00
jpmschweitzerandClaude Opus 4.8 c1d91e57d2 build(ci): real release finalizer + make release target (T-393)
ci/release.sh was a stub that printed a goreleaser/sidecar TODO and
exited 64 — both the sidecar and goreleaser are dissolved (D-56), so it
could never run. Replace it with a finalizer for the single-process app:
read the version from pubspec, assert CHANGELOG has a dated section for it
and the tree is clean, run the full gate (make push-check), then create
the annotated vX.Y.Z tag if missing. Never pushes — prints the
--follow-tags push + build commands. Add a `make release` wrapper so it's
driven through the Makefile like the other gates (CLAUDE.md tooling rule).

This closes the loop that left releases untagged: running `make release`
after the release commit can no longer forget the tag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 23:26:37 +02:00
jpmschweitzerandClaude Opus 4.8 b7db22924d feat(theme): canonical theme/namespace lists + ship Summer Night (T-371, T-478)
The bundled-theme list and the Tier-0 i18n namespace list were each
hand-maintained in 3+ places that had drifted: the testmode harness loaded
8 themes (catppuccin silently unvalidated) while the app and contrast gate
loaded 10, and the i18n gate checked 4 of the shipped namespaces. Export
one canonical const each — kBundledThemePaths and kTier0Namespaces — and
have the app, the testmode harness, and the a11y gates iterate them.

Drift-proof meta-assertions:
- contrast gate fails if any theme YAML on disk is absent from
  kBundledThemePaths (so a theme can't sit unvalidated).
- i18n gate derives its subjects from assets/i18n/en_us/ at test time and
  asserts en/nl key parity for every shipped catalog (26, was 4), plus that
  every kTier0Namespaces entry has a shipped catalog.

Surfaced summer-night.yaml: a legacy-ported palette on disk, unbundled and
never contrast-checked. Per the maintainer's call, ship it: flesh the
palette to full token + syntax coverage (honouring the v1.2 colours, clide
derivation pattern for the new keys) and add a structurally identical
summer-night-hc sibling that clears the strict extended gate. Both pass.

No licenses.yaml change: summer-night is clide's own (ported from legacy
clide v1.2 under legacy/, MIT); the only third-party palette, Catppuccin,
is already acknowledged; no fonts added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:31:16 +02:00
jpmschweitzerandClaude Opus 4.8 1e49f3e1d9 fix(editor): collapse the workspace split when the last buffer closes (T-459)
EditorRegistry.close() guarded its active-changed emit on `_activeId !=
null`, so closing the LAST buffer (active clears to null) emitted only
editor.closed — never the active-changed(id:null) the editor extension
listens for to call closeEditor(). editorOpen stayed true and the top
split sat orphaned over the Claude pane. Always emit active-changed when
the active buffer is removed, including the cleared-to-null case; the
slot renderer already collapses correctly once editorOpen flips false.

The existing extension test fabricated the null active-changed event, so
it passed despite the registry never emitting it — that gap is why the
bug shipped. Add a registry test that drives the real close() path, plus
a slot_host widget test asserting the split (drag handle) drops out and
the primary pane fills the column.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:49:12 +02:00
jpmschweitzerandClaude Opus 4.8 8c9c881d81 fix(welcome): open Linux window at 1600x900 to clear footer overlap (T-477)
At the 720p default the vertically-centred welcome content slid under the
floating version/theme status line (Positioned bottom:24), overlapping the
tips card. The taller default gives the centred block clear bottom margin
and matches the macOS default size.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 09:14:05 +02:00
jpmschweitzerandClaude Opus 4.8 3aebfa7ec3 release v2.8.1
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 07:46:10 +02:00
jpmschweitzerandClaude Opus 4.8 ea340ef141 fix(i18n): localize remaining Claude-pane strings (T-462)
Completes i18n coverage of the conversation surface — the strings the
T-464 migration left as "decorative" English:

- running-indicator verbs (Pondering/Conjuring/… → catalog, Dutch added)
- pane chrome title (primary / secondary N) and banner role
- tool-card titles (Edit → Bewerken) via a tool.name.* key set, with
  command/proper-name tools (Bash, Grep, Glob, LS) falling back to the
  raw name
- collapser step/edit/agent counters
- the folded-activity ticker (result/error/thinking/image + tool name)

English degrades to the placeholder where a key is absent, so untranslated
or MCP tool names render unchanged. en_us and nl_nl stay at key parity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 00:24:34 +02:00
jpmschweitzerandClaude Opus 4.8 c6db2fa7dc release v2.8.0
The i18n epic (T-462): every user-facing label — panels, dialogs, command
palette, menus, settings, framework chrome — now resolves through the i18n
catalog instead of a hardcoded string. Catalogs are bundled per locale under
assets/i18n/<locale>/<namespace>.json. Adds a live UI-language selector
(Settings → Appearance → Language) and a full Dutch (nl-NL) pack; English
stays the default. Architecture in D-102.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 19:46:45 +02:00
jpmschweitzerandClaude Opus 4.8 9d7ab35a19 test(i18n): cover the i18n facade, selector, manifest routing + locale (T-462)
Tests for the epic's new code so the 95% floor holds: ClideSettings.i18n
string/interpolated null-safe + localizedCommandTitle; settings-renderer
localization (section/field/help/select/file, project-scope tag, didUpdateWidget);
extension-activation rollback + settings-contribution unregister; the Dutch
gate check; RootShell applying app.locale on boot; and prompt/conversation
tool-body + file-ref coverage. Also harden the menu toggle re-tap (capture the
button point while stable, then tapAt) so it no longer throws getCenter under
the serial coverage load.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:26:30 +02:00
jpmschweitzerandClaude Opus 4.8 328902a3d8 feat(i18n): Dutch (nl-NL) translation pack (T-462)
A full nl_NL catalog set under assets/i18n/nl_nl/ — all 26 namespaces, key-for-
key parity with en_us. Proper nouns (clide, Claude, Git, font names) and the
Vim mode names stay as-is; everything else is translated. Selectable via
Settings → Appearance → Language. The a11y i18n gate now also asserts the
Dutch pack covers every Tier-0 key, so a locale switch never falls back to
English for a built-in label.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 17:34:48 +02:00
jpmschweitzerandClaude Opus 4.8 43c650de10 feat(i18n): UI language selector (Settings → Appearance → Language) (T-462)
Make the localization usable: an app.locale select (English / Nederlands) in
the Appearance category, applied live by root_shell — it parses app.locale and
calls i18n.setLocale on boot + on settings change (setLocale is a no-op when
unchanged). nl_NL registered in availableLocales. The ui-design skill now
documents the locale-dir config and the rule to design for string-length
variation (translations run ~20% longer; never hard-size to the English label).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 17:34:08 +02:00
jpmschweitzerandClaude Opus 4.8 bee7f98e45 docs(i18n): record D-102 (i18n routing) + ui-design skill rule
Document the T-462 i18n architecture: ext-id namespaces auto-loaded on
activation, a 'core' catalog for framework chrome, the null-safe
ClideSettings.i18n read facade, contribution titleKey/labelKey fields, and the
assets/i18n/<locale>/<namespace>.json locale-dir layout. Add the "route
user-facing strings through the catalog" rule to the ui-design skill.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 15:57:05 +02:00
jpmschweitzerandClaude Opus 4.8 1bdfd1be1c refactor(i18n): bundle catalogs under assets/i18n/<locale>/ (T-462)
Move the catalogs out of lib/kernel/src/i18n/catalog/ into assets/i18n/, and
switch the layout from a `{namespace}_{locale}.json` filename to a per-locale
DIRECTORY: assets/i18n/en_us/{namespace}.json. Adding a language is now just a
new folder (assets/i18n/nl_nl/, nl_be, en_eu, …) of the same namespace files —
no renames — which is cleaner to maintain and mirrors how an external
extension ships its own catalog. AssetCatalogLoader/FileCatalogLoader resolve
`<root>/<locale>/<namespace>.json`; pubspec bundles the locale folder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 15:54:14 +02:00
jpmschweitzerandClaude Opus 4.8 37fa255a63 refactor(i18n): route command + settings manifest labels through the catalog (T-462)
Apply the manifest-i18n foundation across every extension: each
CommandContribution (45, command-palette + menu) gains titleKey + i18nNamespace,
and each SettingsCategory/section/field/option gains its key — with the English
text added to the owning extension's catalog. The settings renderer
(settings_category_view + settings_modal) threads the category's i18nNamespace
down and resolves every label/help/option through it; new catalogs created for
view, cli-install, keybindings-ui. No en_US behaviour change — the command
palette, menus, and settings panel now localize from the catalog (D-21).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:20:19 +02:00
jpmschweitzerandClaude Opus 4.8 198c8b18b5 feat(i18n): i18n key fields on command + settings contributions (T-462)
Lets manifest labels (command-palette/menu titles, settings labels) localize,
not just displayed widget strings. Adds optional titleKey/i18nNamespace to
CommandContribution and labelKey/helpKey/titleKey + a category i18nNamespace to
the settings schema. The command palette and menu bar now resolve titles via a
shared localizedCommandTitle helper — and the palette's fuzzy search matches
the localized title too (PaletteController.titleResolver). No behaviour change
until the per-extension keys + catalog entries land (placeholder == English).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 09:44:35 +02:00
jpmschweitzerandClaude Opus 4.8 33a8a74240 refactor(i18n): route framework + shared chrome through a 'core' catalog (T-469)
Framework strings outside any extension — widget primitives (collapser, toast,
lightbox, multitab, ex-line, spine, pane chrome), the shared reader chrome, the
markdown 'Open in editor' tooltip, and the drag-resize handle a11y labels — now
resolve under a new 'core' namespace (preloaded at boot). Settles the T-469
namespace question: framework chrome gets one 'core' catalog.

Makes ClideSettings.i18n.string/.interpolated null-safe (ClideKernel.maybeOf):
primitives render kernel-less in isolated tests, returning the placeholder —
matching the D-101 fallback contract for fonts. The markdown tooltip threads
via the ClideMarkdownHooks carrier like mono/ui; drag_resize reads the kernel
i18n directly to avoid a kernel→widgets layering inversion. No en_US change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 23:26:02 +02:00
jpmschweitzerandClaude Opus 4.8 17bc907415 fix(test): settle the menu overlay before re-tapping the top button
The "tapping the same top button toggles closed" case re-tapped File before
the open overlay finished laying out, so getCenter intermittently threw on a
not-yet-positioned hit box under load. Settle first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 23:21:06 +02:00
jpmschweitzerandClaude Opus 4.8 c5500dc235 refactor(i18n): route remaining extension labels through the catalog (T-468)
Migrate the user-facing strings in menubar, output, search, deeplink, editor,
files, terminal, markdown, and vim to ClideSettings.i18n.string/.interpolated;
extend the existing catalogs and create the missing ones (deeplink, output,
search, markdown, menubar, vim). vim mode labels now resolve through
builtin.vim (default still `-- NORMAL --`, now localizable). A dock-status test
seeds builtin.output so the widget's own i18n lookups resolve instead of
warning into the logRing it counts. No en_US behaviour change (D-21).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:16:20 +02:00
jpmschweitzerandClaude Opus 4.8 17d22b9571 refactor(i18n): route planning-pane labels through the catalog (T-467)
Migrate the user-facing strings in builtin.tickets, builtin.pql,
builtin.decisions, and builtin.problems (list/detail views, section headers,
filters, empty/loading/error states, type chips) to ClideSettings.i18n; extend
each extension's catalog. The detail/links tabs that had a bare title now wire
titleKey + i18nNamespace so the tab title resolves too. Context threaded into
the static label helpers. No en_US behaviour change (D-21).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:14:01 +02:00
jpmschweitzerandClaude Opus 4.8 f33cd603be refactor(i18n): route git + diff panel labels through the catalog (T-466)
Migrate the user-facing strings in builtin.git (panel, status groups, commit
bar, branch picker, discard dialog, row a11y) and builtin.diff (toolbar,
empty states, file meta) to ClideSettings.i18n.string/.interpolated; extend
both catalogs. _stateLabel threaded a BuildContext from its caller. No en_US
behaviour change (D-21).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:13:35 +02:00
jpmschweitzerandClaude Opus 4.8 16fe71cb3f refactor(i18n): route claude pane labels through the catalog (T-464)
Migrate the hardcoded user-facing strings across builtin.claude — composer,
conversation cards/segments, permission + AskUserQuestion prompts, task dock,
meta-sidebar (activity/config/team/roster), session/model pickers — to
ClideSettings.i18n.string/.interpolated, English kept as the placeholder;
builtin.claude_en_us.json extended to cover them. Context threaded into the
context-free render helpers (toolInputBody chain, _ConversationTurn) the same
way the mono family already is. No en_US behaviour change (D-21).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 21:19:19 +02:00
jpmschweitzerandClaude Opus 4.8 93aa9bc332 fix(i18n): re-export I18nReplacer with the ClideSettings facade (T-462)
ClideSettings.i18n.interpolated takes I18nReplacer, but the type didn't travel
with the facade — every templated-lookup call site had to separately import it
from the kernel. Re-export it from clide_settings so it ships via the widgets
barrel alongside ClideSettings; the interpolated API is now self-contained.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 21:14:20 +02:00
jpmschweitzerandClaude Opus 4.8 e1d57e7964 refactor(i18n): route welcome labels through the catalog (T-465)
Migrate the ~25 hardcoded user-facing strings in builtin.welcome (section
headers, tips, action + status lines, both dialogs) to
ClideSettings.i18n.string(...), keeping the English as the placeholder; extend
builtin.welcome_en_us.json to cover them. The _tips const widened to carry
(key, English, glyph) so the labels resolve at render. No en_US behaviour
change — strings now resolve through the catalog so they can be localized
(D-21).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 15:52:05 +02:00
jpmschweitzerandClaude Opus 4.8 3e83373258 fix(i18n): close theme-picker + diff catalog gaps (T-463)
theme-picker referenced section.appearance + toggle.highContrast with no
catalog entries (silently fell to the placeholder); add both. builtin.diff
contributes a localized tab but shipped no catalog file at all; create it with
tab.title. Both namespaces now resolve from the catalog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 09:56:25 +02:00
jpmschweitzerandClaude Opus 4.8 6d2361cd61 feat(i18n): load every extension's own namespace + facade lookup (T-462)
Foundation for routing hardcoded UI labels through the catalog (D-21). Two
enablers:

- ExtensionManager now eagerly loads each activated extension's own-id catalog
  namespace, not just the namespaces of localized tabs. An extension's id IS
  its catalog namespace (ClideExtension.t), so labels resolve through the
  catalog even for extensions that contribute no tab; a missing catalog file
  loads as an empty map, so it's harmless.
- ClideSettings.i18n gains string()/interpolated() lookups so widget call
  sites route through the one D-101 facade (like theme/fonts) instead of each
  re-deriving ClideKernel.of(context).i18n.

No user-visible change yet — placeholders equal the en_US catalog values; the
per-extension label migrations follow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 09:54:56 +02:00
jpmschweitzerandClaude Opus 4.8 2fe5d6d3e8 release v2.7.1
Patch on 2.7.0: Josefin Sans is the default UI font again (Inter stays
selectable), and Claude's markdown-rendered prose + links now honour the
live UI-font setting instead of pinning the bundled default (T-475).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 09:37:59 +02:00
jpmschweitzerandClaude Opus 4.8 8130d6b43a feat(fonts): default UI font back to Josefin Sans
Reverts the Inter default introduced in 2.7.0 — Josefin Sans (Light, w300) is
the default UI face again. Inter stays bundled and selectable in Settings →
Appearance; JetBrains Mono remains the default monospace with Fira Mono
selectable. Flips clideUiFamily/default weight, the picker's default + option
order, and the licenses.yaml purpose notes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 09:36:30 +02:00
jpmschweitzerandClaude Opus 4.8 90546cb446 fix(fonts): markdown prose + links honour the live UI font (T-475)
The markdown span builders are context-free statics, so they pinned the
bundled clideUiFamily const and ignored the Appearance UI-font setting —
Claude's conversation prose stayed on the default face regardless of the
pick. Thread the resolved UI family through the same ClideMarkdownHooks
carrier already used for mono (T-472): build() resolves it from context and
every prose/link span reads hooks.ui. Adds a regression test asserting prose
and inline code follow the families from the ClideSettingsScope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 09:35:51 +02:00
jpmschweitzerandClaude Opus 4.8 258090926d release v2.7.0
Settings panel epic: schema-driven settings engine (modal shell, category
rail, per-field scope tags, cross-category search) with Activity, Keymap,
Appearance, Extensions, and Claude categories; Inter as the default UI font
plus live UI + monospace font pickers (Inter, Fira Mono bundled). Backed by
the ClideSettings facade (D-101) unifying theme, i18n, and fonts behind one
widget-facing read entry, with consumer reads migrated onto it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 22:37:00 +02:00
jpmschweitzerandClaude Opus 4.8 160cc81741 fix(test): de-flake theme_persistence project-write assertion under load
The "persists project.theme into that repo" case asserted the .clide
settings file existed after a single pumpEventQueue, but the write is
fire-and-forget real I/O — one event-queue drain doesn't guarantee the disk
flush, so it flaked in the loaded coverage pool. Poll for the file (bounded)
instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 22:30:47 +02:00
jpmschweitzerandClaude Opus 4.8 18c8e702fb test(settings): cover the schema-engine edit/file/scope/search paths
Exercise the previously-untested branches of the settings category renderer:
number-field commit/clamp/unparseable-revert, text-field commit, the file
control's command button, the select raw-value fallback, scope move-to-app,
field help text, and the cross-category search empty state. Restores the
coverage floor after the D-101 facade migration shifted the ratio.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 22:27:59 +02:00
jpmschweitzerandClaude Opus 4.8 a5d6559d24 fix(test): pty_log_test imports package:test, not flutter_test
The breadcrumb unit test (T-434) pulled package:flutter_test, which imports
dart:ui — unavailable under plain `dart test`. That broke the flutter-free
core batch (ci/test_core.sh) for every file compiled after it. It uses only
package:test APIs, so the import was simply wrong; siblings already use
package:test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 22:27:12 +02:00
jpmschweitzerandClaude Opus 4.8 18cd945380 refactor(theme): route consumer reads through ClideSettings.theme (T-473)
Migrate ClideTheme.of(context) reads across the widget, feature, and shell
layers to the unified ClideSettings.theme.of(context) facade (D-101), so
theme/i18n/fonts/settings share one widget-facing entry. The facade delegates
straight to ClideTheme, so behaviour is unchanged — goldens are unmoved.

The low-level theme provider keeps its direct ClideTheme.of: the facade is
built on it, and the two kernel sites (ClideTheme's own definition + the
panels drag-resize widget) stay direct to avoid a widgets→kernel import cycle.
Dead controller.dart/kernel.dart imports left by the sweep removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 21:47:15 +02:00
jpmschweitzerandClaude Opus 4.8 1c79c9c4dc refactor(i18n): route consumer reads through ClideSettings.i18n (T-474)
Migrate widget i18n reads from ClideKernel.of(context).i18n to the unified
ClideSettings.i18n.of(context) facade (D-101), so settings/theme/fonts/i18n
share one widget-facing entry point. The facade delegates to the same I18n
service, so behaviour is unchanged. Infra plumbing (KernelServices /
ExtensionManager fields, the slot-host Listenable.merge) keeps its direct
kernel reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 21:37:51 +02:00
jpmschweitzerandClaude Opus 4.8 c4248e3228 fix(fonts): honour the monospace setting in context-free render helpers (T-472)
Eleven leaf sites rendered monospace text from top-level/static helpers that
take no BuildContext, so they hard-coded clideMonoFamily and ignored the
Settings → Appearance Monospace choice that the D-101 facade made live
everywhere else.

Thread the resolved family in from the nearest context-bearing caller:
- claude tool bodies/results: a required `mono` field on _ConversationTurn +
  a `mono` arg on the shared toolInputBody chain;
- markdown inline `code`/record/file-ref spans: carried on ClideMarkdownHooks,
  which build() already constructs from context and threads to every static;
- search preview styles and welcome tips: a `mono` parameter on the helpers.

No behaviour change when the setting is default; these surfaces now switch
live with the rest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 21:35:38 +02:00
jpmschweitzerandClaude Opus 4.8 6b5317342b chore(plan): file T-472/T-473/T-474 (ClideSettings migration follow-ups)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 21:00:28 +02:00
jpmschweitzerandClaude Opus 4.8 b8e6629df5 feat(settings): monospace font picker + bundle Fira Mono (T-471)
Settings → Appearance gains a Monospace font select (JetBrains Mono / Fira
Mono) on the schema engine, writing app.mono.font. It applies live: root_shell
resolves the family into the ClideSettingsScope (D-101) and rebuilds on the
settings change, so the ~93 migrated mono sites — terminal, diffs, code, IDs —
re-read the new family at once.

Vendor Fira Mono (Regular + Bold, OFL) under assets/fonts/fira_mono/ with the
pubspec font family + licenses.yaml entry per D-42; JetBrains Mono stays the
default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 20:59:47 +02:00
jpmschweitzerandClaude Opus 4.8 a7ea38acde refactor(settings): ClideSettings — one live-preferences facade (D-101)
Introduce ClideSettings, a single widget-facing facade for the app's live
user preferences, namespaced by concern: ClideSettings.fonts.monoOf(context)
/ .fonts.uiOf(context), ClideSettings.theme.of(context),
ClideSettings.i18n.of(context). "Plumb once, use many."

Fonts are carried by a new root-provided ClideSettingsScope (resolved from the
font settings in root_shell, rebuilt on change); theme and i18n delegate to
their existing live providers (ClideTheme / the I18n service) so there's one
source of truth and their many consumers migrate incrementally rather than in
a big-bang. Reads outside a scope fall back to the bundled font defaults, so a
widget renders without a provider (isolated tests).

Migrate ~93 monospace-font call sites across 33 files from the clideMonoFamily
const to ClideSettings.fonts.monoOf(context) — pure refactor, identical family
when no override is set. 11 context-less helper sites (markdown static spans,
a few top-level/static builders) keep the const for now and are tracked in
T-472. Records D-101; updates the ui-design skill's font-family rule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 20:57:16 +02:00
jpmschweitzerandClaude Opus 4.8 8ad5c9ba6e chore(plan): file T-471 (monospace font picker follow-up)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 20:16:38 +02:00
jpmschweitzerandClaude Opus 4.8 21d435d1f1 feat(settings): Inter as default UI font + UI font picker (T-460)
Vendor Inter (variable + italic, OFL) under assets/fonts/inter/ and make it
the default application UI face, replacing Josefin Sans (which stays bundled
as a selectable option). pubspec font family + licenses.yaml entry per D-42.

Settings → Appearance gains a UI-font select (Inter / Josefin Sans). The root
DefaultTextStyle reads app.ui.font (kUiFontSettingKey) over the default and
re-applies live on settings change, so a pick takes effect immediately. Bump
the default UI weight to w400 — Inter reads better at Regular than Josefin's
Light.

The monospace picker is deferred to T-471: clideMonoFamily is a const at ~38
call sites (not inherited), so a live mono picker needs the family routed
through context first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 20:15:01 +02:00
jpmschweitzerandClaude Opus 4.8 b1057b41c8 chore(plan): file T-470 (Claude apply-to-current prompt follow-up)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 20:00:46 +02:00
jpmschweitzerandClaude Opus 4.8 ea670fd077 feat(settings): Claude category — new-session defaults (T-457)
A Settings → Claude category sets per-user defaults for NEW sessions: model,
effort, and permission mode (generic schema selects persisting
app.claude.default*). The pane reads them at spawn — effort flows through the
existing --effort flag (SpawnSpec.effort); model and permission mode are sent
as control requests right after a fresh (non-resume, non-fork) session starts.
'default'/unset values are no-ops, leaving the CLI's own defaults.

The optional "settings changed — apply to current sessions?" prompt is filed
as T-470: it needs a custom control (a generic select can't prompt), so it's
a clean follow-up on the T-452 escape hatch.

Tests: defaultEffortFlag sentinel handling; the category contributes the three
default fields. The thin pane-side applySessionDefaults is covered by the gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:58:11 +02:00
jpmschweitzerandClaude Opus 4.8 d9eadf295a chore(plan): sync settings-epic ticket statuses
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:11:15 +02:00
jpmschweitzerandClaude Opus 4.8 de1e96c510 style(test): dart format settings + keymap category tests
Reflow lines `dart format` collapses — the T-451/T-452 test additions were
committed before `make format` ran, so the committed copies were unformatted.
No behavior change; keeps the push format gate green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:07:47 +02:00
jpmschweitzerandClaude Opus 4.8 79c5294d44 feat(settings): Extensions tab — "watch this space" notice (T-456)
Built-in extensions are always on and there's no third-party install path yet,
so the Extensions tab is a notice rather than a toggle list — a toggle list
would let users brick the app by disabling load-bearing builtins. It explains
that installing/enabling/disabling arrives with third-party (Lua) extensions
and points at the records that pick it up (D-16 / T-8).

Fleshes out the builtin.extensions-ui stub via the custom-control hatch. Adds
two renderer guards — empty section/field labels render no chrome — so a
notice-only category shows just its card.

Tests: contributes the Extensions category + notice control; the notice renders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:45:01 +02:00
jpmschweitzerandClaude Opus 4.8 757d6f71fa feat(settings): Appearance category + custom-control escape hatch (T-452)
Add the one bespoke control the schema engine defers to. New
SettingsControlContribution routes a WidgetBuilder into a kernel
SettingsControlRegistry under a customId; a SettingsFieldKind.custom field
names that id, and the renderer draws the registered widget full-width
(label on top, no scope tag — the control owns its own apply + scope).

The theme-picker extension uses it: an Appearance category whose theme field
is custom, backed by AppearanceThemeControl — base-theme chips + a
high-contrast toggle that apply live through ThemeController (persisted by
theme_persistence). Reuses the shared theme_families helpers.

Tests: control registry (register/dup/unregister), the renderer's custom-field
path, and the Appearance contribution + live theme apply.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:04:52 +02:00
jpmschweitzerandClaude Opus 4.8 727fe8fdd4 feat(settings): Keymap category — preset select (T-451)
The keybindings-ui extension contributes a Keymap SettingsCategory: a preset
select (Default / Vim / VS Code / JetBrains) reading the active preset from
kKeymapPresetSetting. Picking one routes through a new schema affordance —
SettingsField.applyCommandPrefix — running `keymap.preset.<value>`, which calls
KeymapService.setPreset to persist and reload the layered keymap live. The
prefix path keeps the engine generic: settings a subsystem applies via a
command (rather than a bare key write) declare the prefix; the scope tag and
current-value read still use the key.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 12:51:14 +02:00
jpmschweitzerandClaude Opus 4.8 7aab93e776 chore(config): deny bulk git add (-A / --all / .) in project settings
Deny Bash(git add -A*), Bash(git add --all*), and Bash(git add .) so the
bulk-stage forms can't run; deny overrides the existing git add * allow. The
git-commit skill mandates specific-path staging, and a shared working tree
makes -A actively dangerous (it sweeps another agent's files into the commit).
Exact-match the bare dot rather than git add .* so git add .gitignore and
other dotfile paths still work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 12:44:05 +02:00
jpmschweitzerandClaude Opus 4.8 291f997d31 feat(settings): Activity category — conversation fold level (T-453)
The first real settings tab. The Claude extension contributes an Activity
SettingsCategory whose fold-level select binds to app.claude.activityFoldLevel;
the activity panes already rebuild off the settings notifier, so a pick applies
live. Proves the schema-driven engine end to end with a production setting —
opening Settings now shows a populated, working category.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 12:41:20 +02:00
jpmschweitzerandClaude Opus 4.8 76d8dadc25 docs: forbid git add -A in CLAUDE.md, require git-commit skill
A concurrent session's `git add -A` swept this turn's uncommitted doc
edits into unrelated feat(settings) commits (f56ad88, f643d6e). Add an
explicit-staging guardrail to the Git workflow section and route all
commits through the git-commit skill.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 12:33:41 +02:00
jpmschweitzerandClaude Opus 4.8 f643d6e747 feat(settings): cross-category settings search (T-450)
A search box (ClideFilterBox) atop the rail filters fields across every
registered category. While searching, the panel swaps to a results view that
groups the matching fields under category subheaders — rendered with the same
carded rows and editable inline — and each rail row shows its match count with
zero-match categories dimmed. Completes the settings-UI infra spine (T-444):
shell, engine, rail, scope tags, search.

Tests: search filters across categories, hides non-matches, and surfaces the
per-category rail count.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 12:24:24 +02:00
jpmschweitzerandClaude Opus 4.8 f56ad88439 feat(settings): per-field scope tags + scope resolution (T-449)
Each settings field gains a scope tag showing where its value lives — folder
= Project (.clide), globe = Always (~/.clide), circle-dashed = Default/unset —
colour-coded (statusSuccess / statusWarning / muted) with a tooltip. Tapping
opens a menu to move the value between the scopes the key supports, or reset
to default; the tag's menu replaces the interim reset button.

Backs it with scope-explicit SettingsStore access — rawAt / setAt / removeAt /
effectiveLayer / writableLayers — over the two storage files (app ~/.clide,
project .clide). ext.* keys layer project-over-app; app.*/project.* keys live
only in their prefix's file, so their menu offers that one scope + reset.

Tests: store scope ops (layering, reload, guards) and the tag (Default vs
All-clide rendering, menu reset).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 12:18:38 +02:00
jpmschweitzerandClaude Opus 4.8 6c6b0c731e feat(settings): category rail + navigation (T-447)
The settings modal's left rail now lists the registered categories (icon +
title, data-driven from the SettingsRegistry) with an accent left-stripe +
surfaceHi selection per ui-design surface.md. Selecting a category drives the
modal's selection state and swaps the right panel; the rail scrolls when the
set exceeds the height. The cross-category search box atop the rail lands in
T-450.

Tests: rail lists categories and selecting one swaps the panel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 12:09:56 +02:00
jpmschweitzerandClaude Opus 4.8 4bbb0ee4b3 feat(settings): schema-driven settings engine (T-448)
The core of the settings panel (epic T-444). Subsystems describe a category
as data — a SettingsCategory of carded SettingsSections of SettingsFields
(toggle / select / text / number / opens-external-file), each bound to a
SettingsStore key with help text, a default, and reset-to-default.

Registration is declarative: a new SettingsCategoryContribution carries the
category; the extension manager routes it into a new kernel SettingsRegistry
(exposed on KernelServices), which the panel reads via ClideKernel. Adding a
category is now pure data + a contribution — no widget code.

SettingsCategoryView renders a category into carded sections per ui-design
surface.md: panelHeader card fill, dividerColor border, inputs receding to
panelBackground; select reuses the anchored-overlay menu, text/number commit
on Enter or blur (numeric clamps to bounds). The modal panel now shows the
selected/first registered category, falling back to the empty state.

Tests: registry (sort / dedup / notify), contribution routing on activation,
renderer (render + toggle/select write-through + reset), modal-with-category.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 12:07:51 +02:00
jpmschweitzerandClaude Opus 4.8 643f40d7b2 feat(settings): settings panel shell + settings.open command (T-445)
Lay the foundation for the schema-driven settings UI (epic T-444). A new
`settings.open` command (⌘`,`, plus a File-menu and command-palette entry)
opens a centered Settings modal over the dimmed app via the dialog router,
built from the modalSurface* tokens (D-7, no Material). The shell frames the
two regions later tickets fill in — the category rail (T-447) and the
scrolling carded panel (T-448) — and dismisses on ✕, Esc, or barrier tap.
With no category registered yet it shows its empty state, which is the
correct runtime state.

Flesh out the `builtin.settings-ui` stub (was 0.0.0-stub) into a real
extension; ship its en-US i18n catalog. Relabel the theme picker's
`theme.pick` command title from "Settings…" to "Theme…" so the two no
longer collide in the palette (the picker folds into the new panel's
Appearance category in T-452).

Tests: command + ⌘`,` binding registered, shell renders, Esc and close
both dismiss.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:41:20 +02:00
jpmschweitzerandClaude Opus 4.8 50d10c792a chore(plan): file T-461 (red error border on API-error cards)
Flag API-error conversation cards with a red statusError border (the card
already has an error status + bordered variant; just needs wiring). UI
tracker T-276.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 09:42:54 +02:00
jpmschweitzerandClaude Opus 4.8 f5fab2f4c2 chore(plan): file T-460 (Appearance font pickers + bundle Inter)
UI + monospace font pickers in the Appearance settings tab; bundle Inter
(SIL OFL) as the default UI font. Child of the settings-UI epic T-444,
renders alongside the theme picker (T-452).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 09:40:32 +02:00
jpmschweitzerandClaude Opus 4.8 c661f76c3b sync licenses.yaml self.version to 2.6.0
self.version is auto-synced from pubspec by make gen-build-info; pubspec
is already at 2.6.0 but the generated value had lagged at 2.5.0. Commit
the regenerated file so the working tree matches.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:20:17 +02:00
jpmschweitzerandClaude Opus 4.8 f6a3c527f4 chore(plan): file T-458 EOL/EOF fidelity, T-459 split-collapse bug
Both filed under the T-276 UI epic.

T-458: the editor records neither the original EOL style nor the
trailing-newline state when it reads a file (registry.dart), and only
normalizes on save when .editorconfig asks. Any layer that rewrites
newlines therefore produces cross-platform commit churn with no
.gitattributes backstop. Ticket captures the record-on-read /
preserve-on-save / gitattributes plan.

T-459: closing the last editor panel in the main column's top split
does not collapse it. Suspect the asymmetric top/bottom split logic in
slot_host.dart and closeEditor() not moving the active tab off
editor.active. Root cause not yet confirmed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:20:05 +02:00
364 changed files with 27028 additions and 2081 deletions
+4 -1
View File
@@ -59,7 +59,10 @@
"Bash(git checkout -- *)", "Bash(git checkout -- *)",
"Bash(git restore .*)", "Bash(git restore .*)",
"Bash(chmod -R 777 *)", "Bash(chmod -R 777 *)",
"Bash(chmod 777 *)" "Bash(chmod 777 *)",
"Bash(git add -A*)",
"Bash(git add --all*)",
"Bash(git add .)"
] ]
} }
} }
+8 -3
View File
@@ -14,12 +14,16 @@ Follow these conventions whenever you create a commit in this repository. These
## Message style ## Message style
- **First line:** imperative mood, ≤ 70 characters. Examples: `add sidecar PTY scaffold`, `fix IPC reconnect after app reload`, `update CLI exit-code contract`. This repo uses [Conventional Commits 1.0](https://www.conventionalcommits.org/en/v1.0.0/) (per [D-37](../../../governance/decisions/process.md#d-37)).
- **First line:** `type(scope): imperative subject`, ≤ 72 characters **including** the prefix. Examples: `feat(settings): add Appearance font picker (T-460)`, `fix(ipc): reconnect after app reload`, `docs(readme): drop brittle version line`.
- **Type:** one of `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `chore`. Use `feat`/`fix` for user-visible behavior; `chore` for bookkeeping (`chore(plan)` is the convention for pql ticket housekeeping). Append `!` after the scope for a breaking change (`feat(ipc)!: …`).
- **Scope (optional but preferred):** the subsystem the change lives in — `settings`, `vim`, `pty`, `git`, `plan`, etc. Lower-case, no spaces.
- **Ticket ref:** keep a trailing `(T-NNN)` on the subject when the work has a ticket — `feat(settings): category rail + navigation (T-447)`.
- **Body (optional):** wrap at ~72 chars. Explain the *why* — the reason this change exists. The diff already shows the *what*; don't restate it in prose. - **Body (optional):** wrap at ~72 chars. Explain the *why* — the reason this change exists. The diff already shows the *what*; don't restate it in prose.
- **No emojis.** Anywhere. - **No emojis.** Anywhere.
- **Don't prefix with types** like `feat:` or `fix:` — this repo isn't Conventional Commits. (The Python-era clide under `legacy/` used Conventional Commits; the Flutter rebuild at the repo root does not.)
- **Don't reference the current task or flow** (`for the v2.0 milestone`, `used by the canvas panel`) — that context belongs in the PR description and rots as the repo evolves. - **Don't reference the current task or flow** (`for the v2.0 milestone`, `used by the canvas panel`) — that context belongs in the PR description and rots as the repo evolves.
- **Naming:** the project is `clide`. The Flutter desktop app lives at the repo root; the Go sidecar/CLI binary is `clide`. The supporter project is `pql` (referenced, not part of this repo). The archived Python implementation lives under `legacy/`. - **Naming:** the project is `clide`. The Flutter desktop app lives at the repo root; the `clide` CLI is a thin C client (`native/clide-cli/`). The supporter project is `pql` (referenced, not part of this repo). The archived Python implementation lives under `legacy/`.
## Logically-separated commits ## Logically-separated commits
@@ -95,6 +99,7 @@ Cutting a release is its own commit. In a single commit:
3. Bump `pubspec.yaml` `version:` to `X.Y.Z` (drop the `-dev` suffix for the tag; re-add it on the next development commit if desired). 3. Bump `pubspec.yaml` `version:` to `X.Y.Z` (drop the `-dev` suffix for the tag; re-add it on the next development commit if desired).
4. Run `make gen-build-info` so `assets/licenses.yaml` `self.version:` re-syncs from pubspec (it's auto-rewritten by every build but commit the fresh state). Stage the resulting diff alongside step 3. 4. Run `make gen-build-info` so `assets/licenses.yaml` `self.version:` re-syncs from pubspec (it's auto-rewritten by every build but commit the fresh state). Stage the resulting diff alongside step 3.
5. Commit subject: `release vX.Y.Z`. 5. Commit subject: `release vX.Y.Z`.
6. **Tag it.** After the commit lands, run `make release` — it verifies the version/changelog/clean-tree invariants, runs the full gate, and creates the annotated `vX.Y.Z` tag. Then `git push origin main --follow-tags` to publish the commit and the tag together. **Every released version must have a matching `git tag`** — a CHANGELOG heading without a tag is an incomplete release (T-393: tagging had silently lapsed from v2.2.0 through v2.8.0). Don't hand-roll the tag; `make release` keeps the tag, gate, and version in lockstep. If you tag by hand, use an annotated tag (`git tag -a vX.Y.Z -m "clide vX.Y.Z"`); never push a release commit without its tag.
`pubspec.yaml` is the single source of truth for the version. Every `make` build/run/test target regenerates `lib/src/build_info.g.dart` (gitignored) and rewrites `assets/licenses.yaml` `self.version:` from it — so the Flutter app sees the current version everywhere without manual sync. Bumping `pubspec.yaml` and the changelog out of sync is the mistake this rule prevents. `pubspec.yaml` is the single source of truth for the version. Every `make` build/run/test target regenerates `lib/src/build_info.g.dart` (gitignored) and rewrites `assets/licenses.yaml` `self.version:` from it — so the Flutter app sees the current version everywhere without manual sync. Bumping `pubspec.yaml` and the changelog out of sync is the mistake this rule prevents.
+34 -1
View File
@@ -36,8 +36,41 @@ These apply across every reference and every surface:
- Never use `Material*` or `Cupertino*` widgets or color constants — clide - Never use `Material*` or `Cupertino*` widgets or color constants — clide
is `WidgetsApp` only (D-7). is `WidgetsApp` only (D-7).
- Use `ClideText` for themed text; never bare `Text` in production widgets. - Use `ClideText` for themed text; never bare `Text` in production widgets.
- Typography: `clideFontMono` for code/paths/IDs, `clideFontCaption` for - Typography sizes: `clideFontMono` for code/paths/IDs, `clideFontCaption` for
status/section headers, body inherits from `DefaultTextStyle`. status/section headers, body inherits from `DefaultTextStyle`.
- Font *family* comes from the user-selectable facade, not a const: a
monospace surface uses `fontFamily: ClideSettings.fonts.monoOf(context)`
(and `fontFamilyFallback: clideMonoFamilyFallback`); the UI face is inherited
via the root `DefaultTextStyle`, or `ClideSettings.fonts.uiOf(context)` when a
widget must set it explicitly. `clideMonoFamily` / `clideUiFamily` are the
facade's defaults — don't read them directly in new widgets (D-101). Same
facade exposes `ClideSettings.theme.of(context)` and `.i18n.of(context)`.
- User-facing strings resolve through the catalog, never a hardcoded literal
(D-21/D-102): `ClideSettings.i18n.string(context, 'dotted.key', namespace:
<ext id or 'core'>, placeholder: '<English>')` (or `.interpolated` for
templated). Add the key→English to `assets/i18n/en_us/<namespace>.json`. The
`placeholder` is the English fallback; the extension's own id is its
namespace (framework chrome uses `core`). Contribution manifests carry
`titleKey`/`labelKey` for the same reason.
## Localization & string length (D-21/D-102)
- **Config / layout.** Catalogs are bundled assets at
`assets/i18n/<locale>/<namespace>.json` — the locale is a *directory*
(`en_us`, `nl_nl`, `nl_be`, `en_eu`, …); a new language is a new folder of the
same namespace files. The active language is `app.locale` (Settings →
Appearance → Language), applied live by `root_shell` via `i18n.setLocale`;
add the `Locale` to `availableLocales` in `main.dart` and a folder under
`assets/i18n/`. `en_US` is default; `nl_NL` ships.
- **Design for length variation.** Translations are not the same width — Dutch
runs ~20% longer than English, German more. So **never hard-size a surface to
its English label.** Tight surfaces (status-bar items, chips, buttons, tab
titles, menu items) must tolerate ~30% growth: let them wrap, ellipsis, or
`Flexible`/`Expanded`, not a fixed width tuned to English. When you add or
translate a label, sanity-check the length delta on those tight surfaces (an
`*.semantics` label is screen-reader-only, so its length never deforms
layout). A quick audit: compare `len(nl)/len(en)` per key and eyeball the
short-but-grew cases on real (non-semantics) surfaces.
## Conversation-panel cards (T-305) ## Conversation-panel cards (T-305)
+20 -12
View File
@@ -5,14 +5,16 @@
# Bypass: never. If this runs slowly, fix the slow test; don't reach # Bypass: never. If this runs slowly, fix the slow test; don't reach
# for --no-verify (git-commit skill forbids it). # for --no-verify (git-commit skill forbids it).
# #
# Fast path (T-348): run the full ~2min test suite only when the push touches # Fast path (T-348, widened T-393): run the full ~2min test suite when the push
# lib/ (app + runtime Dart source) or pubspec.* (deps / version). test/, # touches lib/ (app + runtime Dart source), pubspec.* (deps / version), or the
# assets/, docs, and tooling changes ride along with a lib change in practice, # things that can themselves break the suite or this gate — test/, ci/, and
# and an otherwise-skipped push is covered by the next one that does touch lib. # .githooks/. (The old regex matched only lib/ and pubspec.*, so a push that
# The full suite is always available via `make push-check`, and the release CI # ONLY changed a test, a ci/ gate script, or this hook skipped the whole suite.)
# runs it forced on a tagged version. So a lib/pubspec-free push runs just the # Pure assets/docs changes still ride along with the next lib-touching push.
# instant decisions + changelog gates. A state we can't classify (unfetched # There is no release CI — the full suite is only ever run here or via
# remote, new branch) runs the full gate. # `make push-check`. So a push touching none of the above runs just the instant
# decisions + changelog gates. A state we can't classify (unfetched remote, new
# branch) runs the full gate.
set -euo pipefail set -euo pipefail
cd "$(git rev-parse --show-toplevel)" cd "$(git rev-parse --show-toplevel)"
@@ -40,16 +42,22 @@ while read -r _local_ref local_sha _remote_ref remote_sha; do
changed+=$'\n'"$(git diff --name-only "$base" "$local_sha")" changed+=$'\n'"$(git diff --name-only "$base" "$local_sha")"
done done
# Run the full gate when lib/ (app + runtime source) or pubspec.* (deps / # Paths that force the full gate: source (lib/), deps/version (pubspec.*), and
# version) is touched, or when we couldn't classify above. # the dirs that can themselves break the suite or this gate (test/, ci/,
# .githooks/). Single source of truth — test/tooling/pre_push_hook_test.dart
# reads this exact pattern, so narrowing it fails that test (the T-393 guard).
trigger_re='^(lib/|test/|ci/|\.githooks/|pubspec\.)'
# Run the full gate when a trigger path is touched, or when we couldn't classify
# above.
needs_gate=1 needs_gate=1
if [[ "$force_full" -eq 0 ]]; then if [[ "$force_full" -eq 0 ]]; then
trigger_files="$(printf '%s\n' "$changed" | grep -E '^(lib/|pubspec\.)' || true)" trigger_files="$(printf '%s\n' "$changed" | grep -E "$trigger_re" || true)"
[[ -z "$trigger_files" ]] && needs_gate=0 [[ -z "$trigger_files" ]] && needs_gate=0
fi fi
if [[ "$needs_gate" -eq 0 ]]; then if [[ "$needs_gate" -eq 0 ]]; then
echo "==> pre-push: no lib/ or pubspec change — decisions + changelog gates, skipping tests" echo "==> pre-push: no source/test/ci/hook/pubspec change — decisions + changelog gates, skipping tests"
make decisions-validate changelog-gate make decisions-validate changelog-gate
else else
echo "==> pre-push: make push-check" echo "==> pre-push: make push-check"
+21
View File
@@ -35,3 +35,24 @@ INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updat
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKMV7Y13PAYKZC0WB4FQXKC', '06FBKN09R21H3AWR2Q2ZTSGNSW', '2026-06-12 03:16:40', '2026-06-12 03:16:40', NULL, '7e00348c10432c65b03f9dce36520e48', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash); INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKMV7Y13PAYKZC0WB4FQXKC', '06FBKN09R21H3AWR2Q2ZTSGNSW', '2026-06-12 03:16:40', '2026-06-12 03:16:40', NULL, '7e00348c10432c65b03f9dce36520e48', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKMXSVCE98K1H76N00TYCQR', '06FBKN2MP35NPPK1BRDYY2M428', '2026-06-12 03:16:44', '2026-06-12 03:16:44', NULL, 'a6b1c20279ac30f692a30c802cf8f3e5', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash); INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKMXSVCE98K1H76N00TYCQR', '06FBKN2MP35NPPK1BRDYY2M428', '2026-06-12 03:16:44', '2026-06-12 03:16:44', NULL, 'a6b1c20279ac30f692a30c802cf8f3e5', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKMV7Y13PAYKZC0WB4FQXKC', '06FBKN4QVFVE51MY2N0CWCVXHM', '2026-06-12 03:16:49', '2026-06-12 03:16:49', NULL, '9063328f18ba32c0737997ac9af0911a', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash); INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKMV7Y13PAYKZC0WB4FQXKC', '06FBKN4QVFVE51MY2N0CWCVXHM', '2026-06-12 03:16:49', '2026-06-12 03:16:49', NULL, '9063328f18ba32c0737997ac9af0911a', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBDSM0PRGYR61R0NWYAT9VDC', '06FFW49VYMPF18PYQXD9PCMHN8', '2026-06-25 10:24:38', '2026-06-25 10:24:38', NULL, 'a5146a3cfb2f6a3fd904224150836336', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49VYMPF18PYQXD9PCMHN8', '06FFW49W3V175EM4F8ZHC6JFM4', '2026-06-25 10:24:38', '2026-06-25 10:24:38', NULL, 'd57042b235b73ed2d25252d5253e6e4a', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49VYMPF18PYQXD9PCMHN8', '06FFW49W6GP535GR8XD1XEHYWG', '2026-06-25 10:24:38', '2026-06-25 10:24:38', NULL, '07fe1945cb3e60c43cce0989366af8fb', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49VYMPF18PYQXD9PCMHN8', '06FFW49W95HJK08BCRQSWFZBF4', '2026-06-25 10:24:38', '2026-06-25 10:24:38', NULL, 'e4484230e2202044baaa5ce7a2438bbb', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W3V175EM4F8ZHC6JFM4', '06FFW49W95HJK08BCRQSWFZBF4', '2026-06-25 10:24:38', '2026-06-25 10:24:38', NULL, '24cc5602737e75d30377522f38b3e7b5', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W95HJK08BCRQSWFZBF4', '06FFW49WBTT3ESK6QG0XZ1VN2G', '2026-06-25 10:24:38', '2026-06-25 10:24:38', NULL, '51f4759153b30d2a62ed6c69ab62b84d', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49VYMPF18PYQXD9PCMHN8', '06FFW49WEE5N4PESF5G1G5JRHR', '2026-06-25 10:24:38', '2026-06-25 10:24:38', NULL, 'b348cfc357b73fb28366459795752d8b', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W95HJK08BCRQSWFZBF4', '06FFW49WEE5N4PESF5G1G5JRHR', '2026-06-25 10:24:38', '2026-06-25 10:24:38', NULL, 'e114eff56fef8c92fe3b19df2b8b6e3c', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSQ789MDB00EQAXTXBVJDXM', '06FGSQ8B7WDKZ174GJDGWW0GS0', '2026-06-28 06:14:17.763', '2026-06-28 06:14:17.763', NULL, '973169c6f426d9b4bcfd925ce0f36f32', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSQ8B7WDKZ174GJDGWW0GS0', '06FGSQ8YSZA5YNP1JE8SJHXWBG', '2026-06-28 06:14:17.799', '2026-06-28 06:14:17.799', NULL, '60769ef20f951c01c5d20f512913b946', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2AD3HPR3HXSVASVZEX8PK0', '06FB2ACSDBDZARV3NNGYD9NYYR', '2026-06-28 13:28:39.768', '2026-06-28 13:28:39.768', NULL, 'd5a7084fa717846475fad88d8f30febd', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB234WP4Y6Q16A0HFW8BSXMG', '06FB2ACSDBDZARV3NNGYD9NYYR', '2026-06-28 13:28:39.808', '2026-06-28 13:28:39.808', NULL, '79027c30d19f6f7d43c3068fd80ba8ed', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ETJQP0CT6X7W3CWZ6NS9G', '06FGX26B0NAC9WRJMVB0QE6CV8', '2026-06-28 14:01:13.773', '2026-06-28 14:01:13.773', NULL, '68cfce9178bbb310b87c76a908781c76', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', '06FB2ETJQP0CT6X7W3CWZ6NS9G', '2026-06-10 11:12:20', '2026-06-28 15:29:40.913', '2026-06-28 15:29:40.913', '16d8a936aa3083cd16ca25a57a2e1b86', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ETJQP0CT6X7W3CWZ6NS9G', '06FB2ERREMEEF26KKHGNZBWW64', '2026-06-28 15:29:44.904', '2026-06-28 15:29:44.904', NULL, '545d2ce4f863fd5bb2eee240b696be0c', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', '06FB234WP4Y6Q16A0HFW8BSXMG', '2026-06-10 11:12:11', '2026-06-28 15:29:57.491', NULL, '6e43179e6b211be30766ca56f5ed5885', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', '06FB2AD3HPR3HXSVASVZEX8PK0', '2026-06-28 15:30:00.399', '2026-06-28 15:30:00.399', NULL, 'e0961be9550747530f530a7fd300583f', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', '06FGX26B0NAC9WRJMVB0QE6CV8', '2026-06-28 15:30:03.803', '2026-06-28 15:30:03.803', NULL, 'fa47a4a4aff706215efa3a449681d18f', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2G2KHKT5CJYR0TK1WQGMD0', '06FB2EV29HSK6EJ5VF50R87VC4', '2026-06-28 15:30:10.567', '2026-06-28 15:30:10.567', NULL, '52cd02f3a176cd078db479ef850cbcf4', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGYPJ6FTEPP4JK3D7D01ZSMM', '06FGX26B0NAC9WRJMVB0QE6CV8', '2026-06-28 17:50:13.767', '2026-06-28 17:50:13.767', NULL, '3e0ac1d814ac13a12b68714c7320075f', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2G2KHKT5CJYR0TK1WQGMD0', '06FB2EV29HSK6EJ5VF50R87VC4', '2026-06-28 15:30:10.567', '2026-06-29 21:58:13.907', '2026-06-29 21:58:13.907', '3b7ec3277f971c3646a7587b06606335', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
File diff suppressed because it is too large Load Diff
+56
View File
@@ -283,3 +283,59 @@ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD15S1A0WRTY9WWFG0R4AAZ4', 'T-455', '2026-06-16 13:20:25', '2026-06-16 13:20:25', NULL, '370e913c62eab739cf39bda08089e033', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD15S1A0WRTY9WWFG0R4AAZ4', 'T-455', '2026-06-16 13:20:25', '2026-06-16 13:20:25', NULL, '370e913c62eab739cf39bda08089e033', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD15TY946689VZK3MNABSMDW', 'T-456', '2026-06-16 13:20:41', '2026-06-16 13:20:41', NULL, '6041202d66dd93ee3d22206ece3a7db7', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD15TY946689VZK3MNABSMDW', 'T-456', '2026-06-16 13:20:41', '2026-06-16 13:20:41', NULL, '6041202d66dd93ee3d22206ece3a7db7', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD15W7SBVAD3QR5NVE67PKWR', 'T-457', '2026-06-16 13:20:52', '2026-06-16 13:20:52', NULL, 'c9bef415fb7d9d03444fde53ecac3bb1', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD15W7SBVAD3QR5NVE67PKWR', 'T-457', '2026-06-16 13:20:52', '2026-06-16 13:20:52', NULL, 'c9bef415fb7d9d03444fde53ecac3bb1', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD1HK7YKJTEK1WV4VHK0RT8R', 'T-458', '2026-06-16 14:12:04.212', '2026-06-16 14:12:04.212', NULL, '070f1def635ee013df639cb9fe81e357', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD1JBRABJHX804CPMZJDC444', 'T-459', '2026-06-16 14:15:25.011', '2026-06-16 14:15:25.011', NULL, '44d5db70ef13ead00c4dcc437c387cec', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD91A7VEW3VCY7QX1END2Z3G', 'T-460', '2026-06-17 07:39:25.019', '2026-06-17 07:39:25.019', NULL, 'e7e749d35dc413a560d3347be5f4a9ee', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD91RWDWZPHFPJ72FHHHWS08', 'T-461', '2026-06-17 07:41:24.975', '2026-06-17 07:41:24.975', NULL, '4e3fba7aaad762b7b7cd854459cf664a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0TPYSWEM10RP0Q76XAP58', 'T-462', '2026-06-17 09:57:06.422', '2026-06-17 09:57:06.422', NULL, '4be5f7eb00ca81eb07015af05ed144d3', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0VPB99NGS4J6Z5B2RJM2M', 'T-463', '2026-06-17 09:57:14.458', '2026-06-17 09:57:14.458', NULL, 'd5d02453175411c13d21361b084494bd', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0WCPG458GF5FXTXM9VY98', 'T-464', '2026-06-17 09:57:20.180', '2026-06-17 09:57:20.180', NULL, 'c787ef61befc6698286ebdaf613e2a41', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0X5WFME83GS0DKYG3WVJ8', 'T-465', '2026-06-17 09:57:26.627', '2026-06-17 09:57:26.627', NULL, '116b1a377950dfcef4e5b6db90141faa', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0XMA9BQV4RHJ55BFN4JR4', 'T-466', '2026-06-17 09:57:30.322', '2026-06-17 09:57:30.322', NULL, 'd25bc26eb29445fc35cf53f3d54049e5', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0YCY2WGSG8W46RHQ5Z9MW', 'T-467', '2026-06-17 09:57:36.624', '2026-06-17 09:57:36.624', NULL, 'a47c675ca67b86ca3c8d4c1a8acc5b80', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0YYCJ0NK8C4HMDYKJHRDG', 'T-468', '2026-06-17 09:57:41.092', '2026-06-17 09:57:41.092', NULL, 'b988d9578b1ca079c040f07265a008b4', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA10DRQE3SED10JAW2CZDCR', 'T-469', '2026-06-17 09:57:53.221', '2026-06-17 09:57:53.221', NULL, '2a72e3e647afc1b5b235ab1a97d298f8', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDB1VFADV4QK1YG29T658T8C', 'T-470', '2026-06-17 12:21:23.411', '2026-06-17 12:21:23.411', NULL, '0f95e5a1faa7a7e902c765f449cceb15', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDDJ17C3GZWNE98NRVXP189C', 'T-471', '2026-06-17 18:11:42.049', '2026-06-17 18:11:42.049', NULL, '10a9a957d647a63ce69dc4b6b564f6aa', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDDTCGG5Z0KNQKF89W1VZSQ8', 'T-472', '2026-06-17 18:48:11.649', '2026-06-17 18:48:11.649', NULL, '097120cff851cb2dac9f54937f4c7b17', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDDX5GVH3FTCVDEC1QAFACY4', 'T-473', '2026-06-17 19:00:20.828', '2026-06-17 19:00:20.828', NULL, '647782edc3fbb4e7285e4f82a1fb84be', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDDX5KEAJ8KRWVV7ZSG6H7A0', 'T-474', '2026-06-17 19:00:21.490', '2026-06-17 19:00:21.490', NULL, '79576f18553ed885d3a2b745becdf2f5', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDEMY3DQWDNY535PMMSB0CSW', 'T-475', '2026-06-17 20:44:11.501', '2026-06-17 20:44:11.501', NULL, 'bb2b56ea9a76d967123238f588c70aba', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'T-476', '2026-06-19 07:42:08.735', '2026-06-19 07:42:08.735', NULL, 'f75a0a8c00b0ec40c110c1169e42fe2c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FEWB9JA5CMTXB5D7G1NSHAV4', 'T-477', '2026-06-22 07:13:19.958', '2026-06-22 07:13:19.958', NULL, '0314d96a690bdadc122ce0a1e9ed0f5c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFKFYBZFKBX0WV574QJEH2BC', 'T-478', '2026-06-24 13:09:16.923', '2026-06-24 13:09:16.923', NULL, 'f1ea48c2cfa08915841d99eea06a9974', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49VYMPF18PYQXD9PCMHN8', 'T-477', '2026-06-25 09:16:42', '2026-06-25 09:16:42', NULL, '64605163f24e3846463f3afb4ed62fd1', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W3V175EM4F8ZHC6JFM4', 'T-478', '2026-06-25 09:16:42', '2026-06-25 09:16:42', NULL, '1c2e59e6220c0b288103c4a71a8282ca', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W6GP535GR8XD1XEHYWG', 'T-479', '2026-06-25 09:16:42', '2026-06-25 09:16:42', NULL, '2a6162f26c4c0b6e7bceb1f1a2ffe167', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W95HJK08BCRQSWFZBF4', 'T-480', '2026-06-25 09:16:42', '2026-06-25 09:16:42', NULL, '8a42c8c1fc2036d6ea73521266e87235', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49WBTT3ESK6QG0XZ1VN2G', 'T-481', '2026-06-25 09:16:42', '2026-06-25 09:16:42', NULL, 'd1d616d020e6ea8661c1f5553e3001eb', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49WEE5N4PESF5G1G5JRHR', 'T-482', '2026-06-25 09:16:42', '2026-06-25 09:16:42', NULL, '557f92392db9b5c1e6b6840fe1293b7d', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49VYMPF18PYQXD9PCMHN8', 'T-483', '2026-06-25 09:16:42', '2026-06-26 21:35:04.076', NULL, '2841b381bbb03e6a9cdb970280cd2207', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W3V175EM4F8ZHC6JFM4', 'T-484', '2026-06-25 09:16:42', '2026-06-26 21:35:07.645', NULL, 'd90df1d5e2da44e81f60edb8c5fb2823', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGN13H42TP2MP3T27CTSCDRW', 'T-485', '2026-06-27 19:17:59.713', '2026-06-27 19:17:59.713', NULL, 'b345ba60d55cfdbc7c2e9bb9187ee171', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGPG4WNNH3BWDRVZXYQTW7Z0', 'T-486', '2026-06-27 22:43:31.629', '2026-06-27 22:43:31.629', NULL, 'cd344c35e0eca5673be26790aa8d292c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSQ789MDB00EQAXTXBVJDXM', 'T-487', '2026-06-28 06:13:51.821', '2026-06-28 06:13:51.821', NULL, '184f63035c0a014dc4fb863e512f8779', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSQ8B7WDKZ174GJDGWW0GS0', 'T-488', '2026-06-28 06:14:00.768', '2026-06-28 06:14:00.768', NULL, '69e3283ee3c10bcd2ca5b5cbad6a8b1f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSQ8YSZA5YNP1JE8SJHXWBG', 'T-489', '2026-06-28 06:14:05.776', '2026-06-28 06:14:05.776', NULL, '0b37f24529eb3bb4f2b295dda3bb04ea', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSQP1YPG70RPQJ4XZDH2T1M', 'T-490', '2026-06-28 06:15:53.077', '2026-06-28 06:15:53.077', NULL, '586b7d8495ad019b436fddbf973c9e63', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGVCY4M0ZK72HZT2FSZF968W', 'T-491', '2026-06-28 10:08:32.933', '2026-06-28 10:08:32.933', NULL, '3895f296d80984ccd613ca9853a05eda', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGVDBD0F14FV5CDJQB063YSR', 'T-492', '2026-06-28 10:10:21.572', '2026-06-28 10:10:21.572', NULL, '341150811f14eb3f4486067ffb1197c4', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGVNCZS0JZP5Y7596GBV4RFC', 'T-493', '2026-06-28 10:45:31.721', '2026-06-28 10:45:31.721', NULL, 'eec40f4714242cf343035481684f2aba', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGX26B0NAC9WRJMVB0QE6CV8', 'T-494', '2026-06-28 14:01:13.733', '2026-06-28 14:01:13.733', NULL, '37bf4baec69cb3d06072793cd3d60a46', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGYPJ6FTEPP4JK3D7D01ZSMM', 'T-495', '2026-06-28 17:50:02.366', '2026-06-28 17:50:02.366', NULL, '40dbd25d323a8192e8f65a6e1c2dd33d', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGYS06SDJGZ4W3Z9V9HDEATW', 'T-496', '2026-06-28 18:00:41.420', '2026-06-28 18:00:41.420', NULL, 'd2e472745c710eb00555b2b824d4e214', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZK6PWBRBK9J2XPJ55Y3630', 'T-497', '2026-06-28 19:55:10.439', '2026-06-28 19:55:10.439', NULL, '56f8bf5d0e9a52d9ce3c7a293b5f8972', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZKEHPT1TMFMR0KYFGR087W', 'T-498', '2026-06-28 19:56:14.646', '2026-06-28 19:56:14.646', NULL, 'c62ee89f4aa203262fab74da8604777a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZKEHPSSHEG6ZBM0B4DMM6C', 'T-498', '2026-06-28 19:56:14.655', '2026-06-28 19:56:14.655', NULL, '1a47f2a97f13a7565080893758424fec', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZKEHPW9GMXNACA38MK56HR', 'T-498', '2026-06-28 19:56:14.655', '2026-06-28 19:56:14.655', NULL, '2c606da2b11071cee3bfcee4855d4e66', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZKJKW1721NKWY6XZG3KYB8', 'T-499', '2026-06-28 19:56:47.968', '2026-06-28 19:56:47.968', NULL, '135e7af3ddf32a676fcfadd150061dd6', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZKJKW6EDH8TR45M86EWB5M', 'T-499', '2026-06-28 19:56:47.977', '2026-06-28 19:56:47.977', NULL, 'fc370ccee9cd94d0de9988f08963b8f1', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZKJKW9P697RTQ68R3NKBR8', 'T-499', '2026-06-28 19:56:47.978', '2026-06-28 19:56:47.978', NULL, '99eb5554507cf5b29a158838f156d544', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZKEHPW9GMXNACA38MK56HR', 'T-500', '2026-06-28 19:56:14.655', '2026-06-28 19:57:02.538', NULL, '6b529f95abd005b73e20b293dcba765c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZKEHPT1TMFMR0KYFGR087W', 'T-501', '2026-06-28 19:56:14.646', '2026-06-28 19:57:25.895', NULL, '4e1059be29e69c094168bb55e06704a4', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZKJKW1721NKWY6XZG3KYB8', 'T-502', '2026-06-28 19:56:47.968', '2026-06-28 19:57:25.904', NULL, '2c08cf1dfc5c7ba16f756db7c80bc300', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZKJKW6EDH8TR45M86EWB5M', 'T-503', '2026-06-28 19:56:47.977', '2026-06-28 19:57:25.905', NULL, '5177977a9215f17efa101e50b03ad8eb', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZKJKW9P697RTQ68R3NKBR8', 'T-504', '2026-06-28 19:56:47.978', '2026-06-28 19:57:25.906', NULL, 'cb7f70a279326ff6e29a8c7bdeb78c0f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZKJKW9P697RTQ68R3NKBR8', 'T-504', '2026-06-28 19:56:47.978', '2026-06-28 19:57:25.906', NULL, 'cb7f70a279326ff6e29a8c7bdeb78c0f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FH621XBYJBGE1XPSFFK4YZGM', 'T-505', '2026-06-29 10:58:54.943', '2026-06-29 10:58:54.943', NULL, 'b402cc72b921d446a6e4f6c759d72d54', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FHAX7FV5KWGZQ31617R63W94', 'T-506', '2026-06-29 22:16:52.953', '2026-06-29 22:16:52.953', NULL, 'fdeccc75d6a70fae57de814d11695473', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
File diff suppressed because it is too large Load Diff
+582
View File
@@ -0,0 +1,582 @@
# AGENTS.md for clide — Mistral Vibe operating as Claude Code peer
This file configures Mistral Vibe to operate in this repo with the same
effectiveness as Claude Code. It distills the CLAUDE.md guardrails and all
`.claude/skills/` into Vibe's instruction hierarchy, preserving "Claude punch"
in a hybrid workflow.
---
## Identity
You are operating in **clide** — a Flutter/Dart IDE for Claude Code CLI.
Your role: **peer to Claude Code**, not replacement. Maintain Claude's
behavioral standards, tool discipline, and architectural rigor.
**Primary directive:** Never lose "Claude punch" — the combination of
strict guardrails, CLI-first interaction, and parity between UI and CLI
that defines effective operation in this repo.
---
## Non-Negotiable Guardrails (from CLAUDE.md)
These are load-bearing. Violating any means the design is wrong, not the rule.
- **Flutter desktop is the host. No Electron, ever.**
- **Single process.** The Flutter app hosts everything in-process: IPC server,
subsystem handlers (pane, files, editor, git, pql), extensions.
- **CLI-first, not MCP.** Drive via `clide <subsystem> <verb>` Bash commands.
- **Dart is the core; pql fills the query gap.** PTY spawning is native Dart FFI
(`posix_openpt` + `posix_spawn`). `pql` (Go) handles vault queries.
- **Own the rendering stack.** PTY, markdown, graph, canvas — all clide-owned.
- **User/Claude parity (D-6).** Every CLI subcommand has a UI affordance,
and every UI action has a CLI equivalent.
- **pql: wrap, don't duplicate.** Clide wraps pql; never re-implements it.
- **Repo-is-the-workspace.** Git repo root is the workspace.
- **Decision discipline.** All architectural choices live in
`governance/decisions/<domain>.md` as `D-NNN` records. Open questions as
`Q-NNN` under `governance/questions/<domain>.md`. Rejected as `R-NNN`.
- **No pre-existing excuse.** Solo-dev repo — if `make test` is red, fix it
first, then your work. Surface blockers; don't push on top of broken state.
---
## Tool Discipline (from CLAUDE.md)
### Make targets are the entry points
| Purpose | Command | Never call directly |
|---------|---------|---------------------|
| Launch app | `make run` | `flutter run` |
| Static analysis | `make analyze` | `flutter analyze` |
| Format | `make format` | `dart format` |
| Fast test suite | `make test` | `flutter test` |
| Core subsystem tests | `make test-core` | underlying scripts |
| Accessibility tests | `make test-a11y` | underlying scripts |
| Integration tests | `make test-integration` | underlying scripts |
| Pre-push gate | `make push-check` | `ci/*` scripts |
| Setup hooks | `make hooks` | `cp .githooks/* .git/hooks/` |
| Clean | `make clean` | `rm -rf build/` |
### Shell hygiene
- **Working directory is repo root** — never `cd /path/to/clide` or `git -C`
- **One command per invocation** — no `&&`/`;` chaining
- Exception: `git commit -F` HEREDOC for multi-line messages
- Prefer Read/Edit/Grep tools over `cat`/`sed`/`grep` for file inspection
---
## Git Workflow (from git-commit skill)
### Commit message format: Conventional Commits 1.0
```
<type>(<scope>): <imperative subject> (<T-NNN>)
Optional body: explain WHY, not WHAT. Wrap at ~72 chars.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
```
**Type:** `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `chore`
- Use `feat`/`fix` for user-visible behavior
- Use `chore` for bookkeeping (`chore(plan)` for pql ticket housekeeping)
- Append `!` after scope for breaking changes: `feat(ipc)!: ...`
**Scope:** Optional but preferred — subsystem: `settings`, `vim`, `pty`, `git`, `plan`
- Lower-case, no spaces
**Subject:** ≤ 72 characters INCLUDING prefix. No emojis. No "and".
**Ticket ref:** Trailing `(T-NNN)` when work has a ticket.
**Body:** Explain the *why*. The diff shows the *what*. Don't restate it.
- Hard cap: **60 words per bullet** for CHANGELOG entries
- No multi-paragraph bullets
- No sub-headers inside bullets
- No probe numbers, latency stats, or %-coverage deltas
### Logically-separated commits
1. **Read `git status` + `git diff` first** — never stage blind
2. **Group by concern, not location:**
- Bookkeeping: `.gitignore`, lockfiles, config
- Documentation: `README.md`, ADRs, design notes
- Tooling/skills: reusable, non-project-specific
- Project conventions: repo's own rules
- Feature/subsystem: one cohesive change
- Layer changes: app, sidecar CLI, daemon, IPC, pql wrapper, canvas, git panel
3. **Prefer many small focused commits over one large mixed one**
4. **Use `git add <specific paths>`** — NEVER `git add -A`, `git add .`, `git add -u`
5. **Verify between commits** with `git status`, `git diff --staged`, `git log -1`
### Changelog discipline
- **Every user-visible commit must touch CHANGELOG.md** under `## [Unreleased]`
- Use Keep a Changelog 1.1.0 format with sections: Added, Changed, Deprecated, Removed, Fixed, Security
- Entries: short imperative phrases describing user-facing impact
- **60 words hard cap per bullet** — verify with `make changelog-gate`
- What skips changelog: pure bookkeeping with no user-visible effect
### Safety rules (reinforced)
- **Never** `--no-verify`
- **Never** `--amend` unless user explicitly asks
- **Never** force-push to `main` or `master`
- **Always** check `git status` before staging, `git diff --staged` before committing
- Commit message via HEREDOC:
```bash
git commit -m "$(cat <<'EOF'
<message>
EOF
)"
```
### What NOT to commit
- `.env`, `*.env.local`
- `.claude/settings.local.json`
- Build artefacts: `sidecar/bin/`, `sidecar/dist/`, `build/`, `app/.dart_tool/`
- SQLite index files: `*.sqlite`, `*.sqlite-wal`, `*.sqlite-shm`, `*.db`
- Coverage/test output: `*.out`, `coverage.*`, `*.test`
- `.pql/changelog/` — auto-staged by pre-commit hook from pql DB
---
## pql — Vault Queries + Project Planning
`pql` indexes a vault into SQLite and exposes structural queries plus a
planning layer for decision records and tickets. One binary, two surfaces.
### Precondition
```bash
command -v pql
```
If absent, tell the user to install from
https://github.com/postmeridiem/pql/releases/latest. Don't install it
yourself. Don't fall back to grep unless the user explicitly asks.
### First touch: learn the vault
```bash
pql schema
```
Returns one row per frontmatter key with observed types and file counts.
Run once per session before writing queries.
---
### Surface 1: Vault queries
#### Subcommands
| Command | Purpose |
|---|---|
| `pql files [glob]` | List indexed files; optional glob filter |
| `pql tags [--sort count]` | Distinct tags with counts |
| `pql backlinks <path>` | Files linking TO a path |
| `pql outlinks <path>` | Links FROM a file |
| `pql meta <path>` | Frontmatter + tags + outlinks + headings for one file |
| `pql schema` | Typed frontmatter schema |
| `pql base <name>` | Execute an Obsidian .base file |
| `pql shell` | Interactive REPL (indexes once, then query per line) |
| `pql query "<DSL>"` | SQL-derived DSL for complex queries |
| `pql doctor` | Resolved vault/config/DB/index state |
#### DSL examples
```sql
SELECT name, fm.date WHERE fm.type = 'meeting' ORDER BY fm.date DESC LIMIT 10
SELECT path WHERE 'project' IN tags ORDER BY path
SELECT name, fm.prior_job WHERE fm.type = 'council-member' ORDER BY name
```
Use `--file q.pql` or `--stdin` for long queries. Don't interpolate vault
content into the command line.
#### Query cookbook
- **Files in folder** → `pql files 'sessions/*'`
- **Top tags** → `pql tags --sort count --limit 20`
- **What links to X?** → `pql backlinks members/vaasa/persona.md`
- **Date range** → `pql query "SELECT name, fm.date WHERE fm.date BETWEEN '2024-01-01' AND '2024-12-31'"`
- **Run a Base** → `pql base council-sessions`
- **Inspect one file** → `pql meta members/vaasa/persona.md --pretty`
---
### Surface 2: Planning (decisions + tickets)
Planning state lives in `<vault>/.pql/pql.db` (user-authored state, not a
cache). Decision records come from the DQR tree — `governance/{decisions,
questions,rejected}/<domain>.md` by default (D-21), configurable via
`dqr_dir` in `.pql/config.yaml` or the `PQL_DQR_DIR` env var (env > file >
default); a legacy flat `decisions/` is auto-detected as a fallback.
Tickets are SQLite-native.
#### Decision subcommands
| Command | Purpose |
|---|---|
| `pql decisions sync [--no-style]` | Parse the DQR tree → upsert into pql.db; surfaces style warnings (filename, subdir-type, domain pairing/conflicts) unless `--no-style` |
| `pql decisions validate [--no-style]` | Dry-run parse; structural errors exit non-zero, style issues warn (suppress with `--no-style`) |
| `pql decisions claim <D\|Q\|R> <domain> "title"` | Print next available ID |
| `pql decisions list [--type X] [--domain X] [--status X]` | List decisions |
| `pql decisions show <id> [--with-refs] [--with-tickets]` | Show with joins |
| `pql decisions coverage` | Confirmed decisions without tickets |
| `pql decisions refs <id>` | Cross-references involving a decision |
Always `pql decisions sync` before querying if decisions/*.md may have changed.
#### Ticket subcommands
| Command | Purpose |
|---|---|
| `pql ticket new <type> "title" [--parent T-NNN] [--decision D-NNN] [--priority P] [--id-only]` | Create (emits T-NNN; `--parent` files it under an epic/story in one step; `--id-only` prints the bare id for tree-creation scripts) |
| `pql ticket list [--status S] [--team T] [--assigned A] [--label L] [--under T-NNN] [--leaf] [--unblocked]` | List with filters. `--under` = recursive descendants of a ticket; `--leaf` = no children; `--unblocked` = blockers all reached a terminal status |
| `pql ticket show <id[,id,...]> [--with-context] [--with-blockers] [--with-children] [--tree] [--depth N]` | Show one or more (comma-batch → array of show-trees). `--with-children` = direct children; `--tree` = nested descendant subtree + direct parent (cap with `--depth N`) |
| `pql ticket status <id> <new-status> [--force]` | Change status. Closing (terminal status) is blocked while the ticket has open children; `--force` cascades that status to all not-yet-closed descendants and lists them |
| `pql ticket statuslist` | List the configured status vocabulary (name, label, class, order, is_default, is_terminal) — what a UI reads to render columns |
| `pql ticket relabel <id\|record_id> [--new-label T-NNN] [--fix-prose]` | Reassign a ticket's friendly T-NNN label (reconcile a duplicate-label collision). Identity (record_id) and the structural graph are untouched; only the label moves. `--fix-prose` rewrites stale T-NNN mentions in DQR markdown |
| `pql ticket assign <id> <agent>` | Set assignee |
| `pql ticket setparent <id[,id,...]> <parent-id \| none>` | Set (or clear with `none`) a ticket's **parent** — the hierarchy link (epic→story→task). Positional, not a flag. This is the parent/child relationship, distinct from blockers |
| `pql ticket append <id> <text\|--file\|--stdin>` | Append to the description (blank-line separated); never round-trips existing text |
| `pql ticket block <id> --by <other>` | Add a **blocker** (a dependency: <id> can't start until <other> is done) — NOT a parent/child link; use `setparent` or `new --parent` for hierarchy |
| `pql ticket unblock <id> --from <other>` | Remove blocker |
| `pql ticket team <id> <team>` | Set team |
| `pql ticket label <id> add\|rm <label>` | Manage labels |
| `pql ticket board [--team T]` | Kanban board view |
| `pql ticket refine list` | Tickets with empty descriptions, status-priority-sorted |
| `pql ticket refine next [--skip N]` | Head of the unrefined queue with full show-tree + remaining count |
| `pql ticket refine write <id> <json\|--file\|--stdin>` | Patch writable fields (title, description, priority, type) |
Ticket types: initiative, epic, story, task, bug.
The `id` you type and see (T-NNN) is a friendly label backed by a stable
underwater `record_id` (also in output); two clones never collide on identity,
and a duplicate label is fixed with `pql ticket relabel` (D-26).
Statuses are a per-vault vocabulary (`ticket_statuses` in `.pql/config.yaml`),
defaulting to: backlog, ready, in_progress, review, done, cancelled. Each status
has a class — initial, active, review, terminal — that the engine reasons about.
Run `pql ticket statuslist` to discover the live set. Any status can transition
to any other — pql does not enforce a state machine — except that a ticket
cannot reach a terminal status while it has open children (use `--force` to
cascade the close down the subtree).
#### Plan subcommands
| Command | Purpose |
|---|---|
| `pql plan status` | Dashboard: decision counts, open Qs, ticket summary, coverage gaps |
| `pql plan whatsnext` | Next ticket to work on (active work, then the "ready" lane) with full context bundle |
| `pql plan review` | Next ticket awaiting review with full context bundle |
| `pql plan export [--stage]` | Append changed planning rows to `.pql/changelog/<table>/<YYYY-MM>.sql` (the git-tracked log of record); `--stage` also `git add`s them. Normally a no-op — mutations already write through |
| `pql plan import [--legacy FILE]` | Replay `.pql/changelog/` into `pql.db` (or one-time `--legacy pql-plan.json` migration from the pre-D-15 snapshot) |
| `pql plan rebuild` | Drop replicated tables and replay `.pql/changelog/` from scratch. Warns on stderr (`changelog.ticket_id_collision`) + lists `collisions` in the result if one ticket id was filed twice across clones |
#### Versioning planning state
`pql.db` is gitignored — the durable, git-tracked artifact is
`.pql/changelog/` (D-15/D-16). Ticket mutations **write through** to the
changelog synchronously, so it is always current; you never have to
remember to "export". The hooks installed by `pql init` do the rest:
- `pre-commit` stages `.pql/changelog/` so it lands in the same commit as
the change that produced it.
- `post-merge` replays incoming changelog edits (`pql plan import`) and
re-syncs decisions from their markdown.
- `post-checkout` / `post-rewrite` rebuild `pql.db` from the changelog.
On a fresh clone, `pql plan import` (run automatically on first open)
replays the changelog into a new `pql.db`. There is **no** `pql-plan.json`
snapshot — that artifact is retired; `pql plan export` is now only a
manual catch-up/reconcile.
A ticket mutation (create / status transition / any write) leaves
`.pql/changelog/` dirty by design — the `pre-commit` hook stages it onto
the next `git commit`. This is expected, not a problem to flag. Don't
narrate "the ticket won't persist until committed" on every edit; either
fold the bookkeeping into a commit or trust the normal commit flow.
#### Planning cookbook
- **Sync and list confirmed** → `pql decisions sync && pql decisions list --type confirmed`
- **Show with refs** → `pql decisions show D-5 --with-refs --pretty`
- **Read full body** → `pql decisions read D-5`
- **Create ticket** → `pql ticket new task "implement X" --decision D-5`
- **Create ticket, capture id for a script** → `id=$(pql ticket new task "implement X" --id-only)` — prints just `T-NNN`
- **File a ticket under an epic** → `pql ticket new bug "fix X" --parent T-276` (one step), or reparent an existing one → `pql ticket setparent T-9 T-276` (clear with `none`). Parent = hierarchy; use `block` only for blocking dependencies
- **Batch close** → `pql ticket status T-1,T-2,T-3 done`
- **Full context** → `pql ticket show T-5 --with-context --pretty`
- **Batch show** → `pql ticket show T-1,T-2,T-3 --pretty`
- **Refine next ticket** → `pql ticket refine next --pretty`, then `pql ticket refine write T-N '{"description":"..."}'`
- **Append a note** → `pql ticket append T-5 "benchmarked; TTL now 5m"` — blank-line separated, never overwrites; use `--file note.md` or `--stdin` for longer content
- **Subtree of an epic** → `pql ticket show T-2 --tree --pretty` — nested `subtree` + direct parent in `ancestors`; add `--depth N` to cap levels
- **Ready leaf work under an epic** → `pql ticket list --under T-2 --leaf --unblocked` — leaf tickets beneath T-2 whose blockers have all reached a terminal status; the batch complement to `plan whatsnext`
- **What's next?** → `pql plan whatsnext --pretty`
- **Review queue** → `pql plan review --pretty`
- **Coverage gaps** → `pql decisions coverage`
- **Dashboard** → `pql plan status --pretty`
- **Force a changelog catch-up** → `pql plan export` (normally a no-op; mutations already write through to `.pql/changelog/`)
---
### Output contract (both surfaces)
- **stdout:** JSON array (default); `--jsonl` for one object/line; `--pretty`; `--limit N`.
- **stderr:** JSON diagnostics `{"level":"...","code":"pql.<phase>.<kind>","msg":"..."}`.
- **Exit codes:**
- `0` — success, including zero matches (empty `[]` — say "no matches", not "failed")
- `64` — bad flag
- `65` — parse/compile error (pass stderr back)
- `66` — vault/config not found
- `69` — unavailable
- `70` — internal error
### Anti-patterns
- Don't pipe to `jq` for simple projections — use `--limit`, `--pretty`, `--jsonl`.
- Don't chain `pql files` + `pql meta` — one `pql query` with WHERE.
- Don't parse errors — pass stderr diagnostics back directly.
- Don't forget `pql decisions sync` before querying decisions.
- Don't try to install or upgrade pql — instruct the user if missing.
### When NOT to use
- **Body text search** → `grep`/`rg`.
- **Reading file contents** → `Read` tool.
- **Code structure** → tree-sitter / LSP.
- **Modifying vault files** → `Write`/`Edit`. pql doesn't write to vault content.
---
## Driving clide UI via CLI (from clide skill)
**Core principle: Every UI action has a CLI equivalent (D-6).**
Discover the live surface; don't hard-code it.
### Discover capabilities
```bash
clide capabilities
```
Returns JSON: every registered command, split into `subsystem` + `verb`,
with argument schema. **This is authoritative** — re-run it, don't trust
remembered lists.
### Slots (layout areas)
- `sidebar` — left
- `workspace` — center (where Claude lives)
- `context` — right
- `statusbar` — bottom
### Observe state
```bash
# One-shot orientation
clide status
# Narrower snapshots
clide pane list
clide editor active
clide git status
```
### Drive UI
```bash
# Open a doc in a GUI reader
clide ui open <reader> <ref>
# readers: tickets, decisions, markdown, diff
# examples:
clide ui open tickets T-123
clide ui open decisions D-456
clide ui open diff lib/src/foo.dart
clide ui open markdown docs/bar.md
# Show diff and scroll to file
clide ui diff lib/src/foo.dart
# Raise a toast
clide ui toast "message" --severity success|warning|error|info
# General: clide <subsystem> <verb> [args]
clide files list
clide editor open <path>
clide pane focus <pane-id>
```
**Convention:** If a drive verb has no live GUI, return toolError
("no live UI to drive"), not hang. Exit code conveys ok/usage/tool error.
JSON on stdout.
### You only see what flows through clide
Your own non-`clide` shell work (plain file reads, `make test`, `git`) is
outside clide's view by design (D-83). Run it **through** `clide ...` if
you want clide to observe it.
---
## Testmode (from testmode skill)
`ClideTestApp` is a lightweight Flutter app for integration testing.
Catches regressions unit tests cannot: missing binaries, broken subprocess
wiring, IPC dispatch failures, extension activation order, theme parse errors.
### Running
```bash
make run-testmode # all categories, 60s timeout
make run-testmode TESTMODE_CATEGORY=toolchain
make run-testmode TESTMODE_CATEGORY=ipc
make run-testmode TESTMODE_CATEGORY=extensions
make run-testmode TESTMODE_TIMEOUT=120
```
Results: stdout + `/tmp/clide-testmode.log`
Last line is machine-readable JSON: `{"passed":N,"failed":M,"total":N+M,"failures":[...]}`
### When to run
| Changed area | Category | Why |
|-------------|----------|-----|
| Toolchain, PATH, ptyc | `toolchain` | Binary resolution + exec |
| IPC envelope, dispatcher | `ipc` | Round-trip + error contract |
| Extension manifest, activate | `extensions` | Register + activate lifecycle |
| Theme YAML, loader | `extensions` | Theme parse is in this category |
| Platform config | `all` | Full rebuild validates everything |
| Any doubt | `all` | ~30s, cheap insurance |
### Interpreting output
- `[testmode] exec | ... | OK` — subprocess ran, exit 0 or 1
- `[testmode] PASS | ...` — assertion passed
- `[testmode] FAIL | ...` — assertion failed
- `[testmode] exec | ... | EXCEPTION` — binary not found or not executable
- `[testmode] exec | ... | TIMEOUT` — subprocess hung
If no `[testmode]` lines appear, the testmode gate didn't fire — verify
`CLIDE_TESTMODE` is set to a non-empty string.
### Adding tests
All test logic in `lib/test_app.dart`. Pattern:
```dart
await _testExec('label', binary, ['args'], workDir);
// or
_addResult('label', boolCondition, 'detail string');
```
---
## Repo Layout
```
lib/
main.dart # Flutter entry point
app.dart # Root layout, workspace, panels
clide.dart # Barrel: shared types
src/ # Core subsystems (IPC, PTY, git, files, pql, panes)
kernel/ # Kernel services (theme, i18n, settings, panels)
builtin/ # Built-in extensions
widgets/ # Custom widget primitives
extension/ # Extension contract and registration
lua/ # Lua runtime support
test/ # All tests (core + widgets + goldens + a11y)
assets/ # Fonts, themes, grammars, licenses, logo
linux/, macos/, web/ # Flutter platform directories
native/ # Vendored native libs (tree-sitter, dugite)
governance/ # D/Q/R records
docs/ # Design docs, wireframes
legacy/ # Python Textual clide v1.2 (frozen)
```
---
## Dependency & Supply Chain Discipline
- **Prefer-zero-deps.** Flutter-SDK widgets first; third-party needs justification
- **Exact-pinned in pubspec.yaml** — no caret ranges
- **Advisories reviewed** before every bump
- **pubspec.lock committed**
- **Document every bundled dependency** in `assets/licenses.yaml`:
- name, kind, version, homepage, license, purpose
- Adding a dep: two-step commit — artefact AND `licenses.yaml` entry
- Native deps (dugite, libtree-sitter): vendored in `native/`, pinned by SHA
---
## Pre-push Gate
```bash
make push-check
```
Runs: decisions validation + core tests + fast suite + a11y + coverage + changelog
**Never bypass.** If it fails, fix the underlying issue.
---
## Session Setup
One-time setup on fresh clone:
```bash
make hooks && flutter pub get
pql init # wires up pql skill + perms
```
---
## Mistral-Specific Notes
### What to preserve from Claude
- CLI-first interaction model
- Strict guardrails enforcement
- Logically-separated commits
- Changelog discipline (60-word cap)
- pql as single source of truth for decisions/tickets
- UI/CLI parity
- Testmode for integration validation
### What to adapt
- **Tool names:** Claude's `Bash(...)` → my `bash` tool
- **Agent spawning:** Claude's `Agent{}` → my `task` tool for subagents
- **Permissions:** `.claude/settings.json` allow/deny → my system prompt + your instructions
### When in doubt
Ask: "What would Claude do?" Then do that. The AGENTS.md is the bridge,
not a replacement.
---
## Quick Reference Card
| Task | Command |
|------|---------|
| Discover clide commands | `clide capabilities` |
| See current UI state | `clide status` |
| Open ticket in UI | `clide ui open tickets T-NNN` |
| Toast notification | `clide ui toast "msg" --severity info` |
| List actionable tickets | `pql ticket list --under <id> --leaf --unblocked --status backlog --pretty` |
| Sync decisions | `pql decisions sync` |
| Run integration tests | `make run-testmode` |
| Fast test suite | `make test` |
| Full pre-push | `make push-check` |
| Commit with message | `git commit -m "$(cat <<'EOF'\n<message>\nEOF\n)"` |
| Verify changelog | `make changelog-gate` |
---
*Generated for Mistral Vibe. Preserves Claude Code punch for clide repo.*
+227
View File
@@ -16,6 +16,233 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
## [Unreleased] ## [Unreleased]
### Added
### Changed
### Fixed
## [2.9.0] — 2026-06-30
### Added
- **Graph cards.** `clide draw --file graph.json` (template `graph`) renders a
nodes/edges graph in the conversation as a circular layout — labelled nodes,
lines between them. Honest error on a duplicate id or an edge to an unknown
node. (T-321)
- **Piped `--stdin` payloads.** `cat icons.json | clide icon show --stdin` (and
`image show`) accept a JSON payload on stdin — the ergonomic peer of `--file`
for structured commands. (T-315)
- **Before/after compare cards.** `clide draw --file compare.json` (template
`compare`) renders two or more images side by side, each with its own
label/description and tap-to-zoom; images aspect-fit so differing shapes
don't distort. (T-319)
- **Phosphor glyph cards.** `clide icon show gear folder` (or `--file` entries
with label/description/color) renders glyphs in the conversation at a hero
size plus a real-UI-size strip (1048), for previewing and comparing icons.
Resolves by name or 0xNNNN codepoint; honest error on an unknown glyph. (T-313)
- **Annotated image cards.** `clide image show --file meta.json` attaches a
title/label and a longer description to an image card (alongside the existing
one-line caption); the bare `image show <path> --caption` form is unchanged.
Visual marker overlays remain a follow-up. (T-316)
- **D2 diagram cards.** `clide draw --file diagram.d2` compiles a d2 diagram to
SVG (via the `d2` binary) and renders it in the conversation; `template:"d2"`
with inline source works too, and a `.svg` file renders directly. Honest error
if d2 isn't installed or the source doesn't compile. (T-494, D-103)
- **Tool path resolution + settings.** clide resolves supporter binaries (claude,
d2) via an explicit per-tool path, else PATH and the common install dirs — now
including Homebrew-on-Linux. A Tools settings category edits the paths and
re-detects; a broken path shows in Problems. (T-495, D-104)
- **Check for updates (About box).** Help → About has a manual "Check for
updates" button that compares your version to the latest GitHub release and
links to the notes — explicit and user-initiated, no background polling (the
first and only outbound call clide makes, on your action). (T-47)
- **Live-sync markdown read-mirror.** Editing a `.md` in the editor auto-opens a
read-only preview in the context panel that mirrors the buffer and re-renders
as you type; non-renderable files get no auto-viewer. (T-36, D-50)
- **New project flow.** A "New project…" action in the welcome view creates +
opens a project, then prompts for the Claude account to bind it to — the
per-repo account roadblock fires only for freshly-created projects. (T-488,
story T-486)
- **Initialize a non-repo folder.** Opening a folder that isn't a git repo now
offers to initialize it as a clide project (`git init` + scaffold + the
account roadblock) instead of dead-ending. Also `clide project init`. (T-489)
- **`clide project new <name> [--dir <parent>]`.** Create a new clide project —
a fresh dir, `git init`, and a minimal scaffold. `--dir` defaults to the
current workspace's parent. (T-487, story T-486)
- **Claude account login pane.** `account login` (and the UI add/re-login
affordances) open a modal terminal running `CLAUDE_CONFIG_DIR=<dir> claude
login`; the CLI owns the OAuth flow, credentials land in that account's dir.
(T-485, epic T-476)
- **Claude pane account badge.** The pane header shows which account this repo
is bound to (colour-tinted per account); tap to switch. Hidden when no
accounts are registered. (T-481, epic T-476)
- **Settings → Claude → Accounts.** A registry list (sign-in status, dir,
re-login / remove, add) plus a per-workspace picker that binds this repo to an
account (or Default); switching respawns the pane onto it. (T-482, epic T-476)
- **Per-repo Claude accounts — `clide claude account` verbs.** Manage named
Claude config dirs and bind one per workspace: `add`/`list`/`set`/`unset`/
`remove [--purge]`. `set`/`unset` respawn the workspace's Claude pane(s) onto
the bound account (resuming the conversation under its `CLAUDE_CONFIG_DIR`).
(T-480, epic T-476)
- **`clide instances` / `clide instance` CLI verbs.** `instances` lists every
live clide on the machine with its version, pid, workspace, and socket path
(probing the runtime dir); `instance` reports the one you're connected to — so
you can find and target a specific instance via `CLIDE_SOCK`. (T-247)
- **Summer Night theme + high-contrast sibling.** The legacy v1.2 palette is
fleshed out to full token + syntax coverage and ships alongside a
contrast-hardened `summer-night-hc`, both selectable in Settings → Appearance.
(T-478)
### Changed
- **Fresh Claude sessions are nudged to use the bundled skills.** New tabs and
post-`/clear` respawns get a one-line prompt to load the `pql` + `clide`
skills from the first turn; resumed/forked sessions are left alone. (T-490)
- **Claude meta sidebar facelift.** The Activity, Team, and Config tabs render
their sections as elevated cards with small-caps headers, matching the
settings overlay's card design. (T-158)
- **Bundled themes and Tier-0 i18n namespaces resolve from one canonical list
each.** The app, testmode harness, and contrast/i18n gates iterate the shared
lists instead of drifting copies; a new theme or catalog is validated
automatically, and the i18n gate now checks every catalog for en/nl parity.
(T-371)
### Fixed
- **No more i18n "missing key" log spam for tool names.** Proper-name tools
(Bash, ScheduleWakeup, MCP tools, …) intentionally have no catalog entry and
fall back to the raw name; the conversation pane no longer logs a warning for
each. (T-493)
- **`clide` CLI honors `CLIDE_SOCK`.** The shell client now connects to the
socket named by `CLIDE_SOCK` when set — an explicit target (e.g. a spawned
agent pinning its parent instance) that beats workspace auto-discovery — and
fails loudly if that socket is dead instead of silently driving a different
instance. (T-247)
- **Orphaned IPC sockets are swept on startup.** The app probes the runtime
socket dir on launch and unlinks dead `*.sock` nodes left by crashed
instances (live instances are left untouched), so the dir no longer
accumulates stale sockets. (T-247)
- **Default window opens larger (1600×900) on Linux.** 720p was short enough
that the welcome screen's version/theme footer overlapped the tips card;
the taller default clears it, matching the macOS default. (T-477)
- **Editor split collapses when the last buffer closes.** Closing the final
editor buffer left the top split orphaned over the Claude pane; the daemon
now emits the buffer-cleared event it was suppressing, so the split drops
out and the primary pane fills the column. (T-459)
## [2.8.0] — 2026-06-19
### Added
- **Language selector + Dutch (nl-NL).** Settings → Appearance → Language
switches the UI language live (persisted as `app.locale`); a full Dutch
translation ships. English (en-US) stays the default. (T-462)
### Changed
- **App is fully localizable (i18n everywhere).** Every user-facing label —
panels, dialogs, command palette, menus, and settings — now resolves through
the i18n catalog instead of a hardcoded string; catalogs are bundled per
locale under `assets/i18n/<locale>/`. A new language is a drop-in folder;
en_US behaviour is unchanged. (T-462)
## [2.8.1] — 2026-06-22
### Fixed
- **Localized the remaining Claude-pane strings.** Running-indicator verbs, the
primary/secondary session titles and banner role, tool-card titles, the
step/edit/agent counters, and the folded-activity ticker now resolve through
the catalog — completing i18n coverage of the conversation surface. (T-462)
## [2.7.1] — 2026-06-18
### Changed
- **Josefin Sans is the default UI font again.** Reverts the Inter default from
2.7.0; Inter stays bundled and selectable in Settings → Appearance. JetBrains
Mono remains the default monospace face (Fira Mono selectable). (T-460)
### Fixed
- **Markdown prose honours the UI-font setting.** Claude's conversation prose
and inline links render through the markdown engine, which pinned the bundled
UI face and ignored the Appearance UI-font pick; it now follows the setting
live, like the rest of the app. (T-475)
## [2.7.0] — 2026-06-17
### Added
- **Settings panel shell.** A new `settings.open` command (⌘`,`, plus a File
menu and command-palette entry) opens a centered Settings modal over the
dimmed app — the foundation of the schema-driven settings UI. It frames the
category rail and scrolling carded panel that later work fills in; closes on
✕, Esc, or a barrier tap. (T-445)
- **Schema-driven settings engine.** Subsystems register a `SettingsCategory`
(via `SettingsCategoryContribution` → the kernel `SettingsRegistry`); the
panel renders it as carded sections of toggle/select/text/number/file rows,
each bound to a `SettingsStore` key with help and reset-to-default.
Registering a category surfaces a new tab. (T-448)
- **Settings category rail.** The modal's left rail lists the registered
categories (icon + title, data-driven from the registry) with an accent
left-stripe + surfaceHi selection; picking one swaps the panel. (T-447)
- **Per-field scope tags.** Each field shows where its value lives — folder =
Project (`.clide`), globe = Always (`~/.clide`), circle-dashed =
Default/unset — with a tooltip and a menu to move the value between scopes or
reset it. Backed by scope-explicit `SettingsStore` access. (T-449)
- **Cross-category settings search.** A search box atop the rail filters fields
across every category; the panel shows the matches grouped under category
subheaders (editable inline), and each rail row shows its match count with
zero-match categories dimmed. (T-450)
- **Settings → Activity category.** The first real settings tab: the
conversation fold level (none / tools / thinking / everything) as a schema
field; picking a level applies live to the activity stream. (T-453)
- **Settings → Keymap category.** A preset select (Default / Vim / VS Code /
JetBrains); picking one switches the active keymap live via the preset
command. (T-451)
- **Settings → Appearance category.** A theme picker in the panel — base-theme
chips + a high-contrast toggle, applied live. Adds the engine's custom-control
escape hatch (`SettingsControlContribution` / `SettingsControlRegistry`) for
one-off controls the generic field kinds can't express. (T-452)
- **Settings → Extensions tab.** A "watch this space" notice — installing and
toggling extensions arrives with third-party (Lua) support; built-ins stay
always-on for now. (T-456)
- **Settings → Claude category.** New-session defaults — model, effort, and
permission mode — seed fresh sessions (effort via `--effort` at spawn;
model and permission applied right after start). (T-457)
- **Inter is the default UI font + a UI-font picker.** Bundled Inter (variable)
as the default interface typeface — Josefin Sans stays selectable — and
Settings → Appearance gains a UI-font picker that applies live. (T-460)
- **Monospace font picker.** Settings → Appearance adds a monospace font select
(JetBrains Mono / Fira Mono) that applies live to the terminal, diffs, code,
and IDs. Bundles Fira Mono. (T-471)
### Changed
- **Theme picker relabelled "Theme…".** The ⌘K theme picker's command title
changed from "Settings…" to "Theme…" so it no longer collides with the new
Settings panel in the palette; behaviour is unchanged. (T-445)
### Fixed
- **Monospace setting now applies everywhere.** Eleven context-free render
helpers (Claude tool bodies + results, inline markdown code/refs, search
previews, welcome tips) hard-coded JetBrains Mono and ignored the Monospace
font setting; they now honour it live. (T-472)
## [2.6.0] — 2026-06-16 ## [2.6.0] — 2026-06-16
### Added ### Added
+2
View File
@@ -89,6 +89,8 @@ Shell hygiene (keeps commands inside the permission allowlist, so they don't get
Commit and push directly to `main` for routine work — this is a solo-dev repo and does not use a branch-first / feature-branch flow. Do **not** create a working branch just to land a change. (This overrides the generic "branch before committing on the default branch" assistant default.) The usual safety rules still hold: never `--no-verify`, never force-push `main`, and let the pre-push gate run. Commit and push directly to `main` for routine work — this is a solo-dev repo and does not use a branch-first / feature-branch flow. Do **not** create a working branch just to land a change. (This overrides the generic "branch before committing on the default branch" assistant default.) The usual safety rules still hold: never `--no-verify`, never force-push `main`, and let the pre-push gate run.
**Never `git add -A` or `git add .` — stage explicit paths every time (`git add <file> …`), no exceptions.** This worktree can host concurrent Claude sessions: a blanket add vacuums another session's in-progress files — and your own unrelated edits — into your commit, mislabeling work and entangling history (this has happened). If `git status` shows files you didn't touch this turn, they are not yours to stage. **Always create commits through the [`git-commit` skill](.claude/skills/git-commit/SKILL.md)** — it encodes the message format (Conventional Commits, per [D-37](governance/decisions/process.md#d-37)), the explicit-staging rule, changelog discipline, and the safety reminders. Don't hand-roll a commit that skips it.
The pre-commit hook auto-exports and stages `.pql/changelog/` (the pql ticket DB) on every commit — don't hand-stage it. A ticket change only persists if the turn makes at least one commit; with no commit the hook never fires and a later branch switch can drop it. The pre-commit hook auto-exports and stages `.pql/changelog/` (the pql ticket DB) on every commit — don't hand-stage it. A ticket change only persists if the turn makes at least one commit; with no commit the hook never fires and a later branch switch can drop it.
## Changelog discipline ## Changelog discipline
+8 -4
View File
@@ -181,14 +181,18 @@ tickets before the diff lands. Trivial typo fixes don't need one.
See [D-37](governance/decisions/process.md#d-37) and the bundled See [D-37](governance/decisions/process.md#d-37) and the bundled
[`git-commit` skill](.claude/skills/git-commit/SKILL.md). In short: [`git-commit` skill](.claude/skills/git-commit/SKILL.md). In short:
- Imperative subject ≤ 70 chars, no Conventional Commits prefix - [Conventional Commits 1.0](https://www.conventionalcommits.org/en/v1.0.0/):
(this isn't a Conventional Commits repo — the archived Python `type(scope): imperative subject`, ≤ 72 chars including the prefix.
predecessor under [`legacy/`](legacy/) is, but the rebuild isn't). Types are `feat`, `fix`, `docs`, `style`, `refactor`, `perf`,
`test`, `build`, `chore`; scope is the subsystem (`settings`,
`vim`, `pty`…); keep a trailing `(T-NNN)` ticket ref where one
applies — e.g. `feat(settings): category rail + navigation (T-447)`.
- One logical change per commit. If the subject needs "and", split it. - One logical change per commit. If the subject needs "and", split it.
- Every user-visible commit adds an entry to `CHANGELOG.md` under - Every user-visible commit adds an entry to `CHANGELOG.md` under
`[Unreleased]` in the right subsection (Added, Changed, Deprecated, `[Unreleased]` in the right subsection (Added, Changed, Deprecated,
Removed, Fixed, Security). Keep entries to one or two short Removed, Fixed, Security). Keep entries to one or two short
sentences — the 60-word cap is enforced by `ci/changelog_gate.sh`. sentences — the 60-word cap is enforced by the pre-push gate
(`make changelog-gate`).
- Co-author trailer: - Co-author trailer:
`Co-Authored-By: Claude <noreply@anthropic.com>` when Claude wrote `Co-Authored-By: Claude <noreply@anthropic.com>` when Claude wrote
any of the diff. any of the diff.
+11 -2
View File
@@ -169,8 +169,7 @@ ui-smoke: ## Build + serve + run Playwright smoke + stop.
@sh -c 'trap "tools/ui/stop.sh >/dev/null 2>&1" EXIT; cd tools/ui && npx playwright test smoke.spec.ts' @sh -c 'trap "tools/ui/stop.sh >/dev/null 2>&1" EXIT; cd tools/ui && npx playwright test smoke.spec.ts'
.PHONY: build .PHONY: build
build: gen-build-info clide-cli ## flutter build for the current OS (incl. the C CLI client). build: clide-cli build-$(FLUTTER_OS) ## flutter build for the current OS (via build-<os>) + bundle the C CLI client.
flutter build $(FLUTTER_OS)
@install -m 755 $(CLIDE_CLI_BIN) $(CLI_BUNDLE_DEST) @install -m 755 $(CLIDE_CLI_BIN) $(CLI_BUNDLE_DEST)
@echo "==> bundled C client at $(CLI_BUNDLE_DEST)" @echo "==> bundled C client at $(CLI_BUNDLE_DEST)"
@@ -186,6 +185,10 @@ build-macos: gen-build-info ## flutter build macos (desktop bundle).
build-windows: gen-build-info ## flutter build windows (desktop bundle). build-windows: gen-build-info ## flutter build windows (desktop bundle).
flutter build windows flutter build windows
.PHONY: release
release: ## Finalize a release: verify version/changelog/tree, run the gate, tag vX.Y.Z. Run after the `release vX.Y.Z` commit.
ci/release.sh
# -- install / uninstall ----------------------------------------------------- # -- install / uninstall -----------------------------------------------------
# Install prefix. Bundle lands at $(INSTALL_PREFIX)/clide/ with a # Install prefix. Bundle lands at $(INSTALL_PREFIX)/clide/ with a
@@ -261,6 +264,8 @@ else ifeq ($(FLUTTER_OS),macos)
endif endif
# -- dugite-native (bundled git) ------------------------------------------ # -- dugite-native (bundled git) ------------------------------------------
# Security tracking (D-59): run `make dugite-check` quarterly, or on a git CVE,
# to compare this pin against the latest upstream release. T-88 is the calendar.
DUGITE_VERSION := v2.53.0-3 DUGITE_VERSION := v2.53.0-3
DUGITE_COMMIT := f49d009 DUGITE_COMMIT := f49d009
@@ -327,6 +332,10 @@ clide-cli-clean: ## Remove the compiled C `clide` client.
security: ## Supply-chain gate — osv-scanner over pubspec.lock (CI PR-merge pipeline; run locally on demand). Fails on a known advisory. security: ## Supply-chain gate — osv-scanner over pubspec.lock (CI PR-merge pipeline; run locally on demand). Fails on a known advisory.
ci/osv_scan.sh ci/osv_scan.sh
.PHONY: dugite-check
dugite-check: ## Track dugite-native (bundled git) upstream releases for security drift (T-88 / D-59). Run quarterly, or on a git CVE. Informational, not a gate.
ci/check_dugite_version.sh
# -- pre-push gate -------------------------------------------------------- # -- pre-push gate --------------------------------------------------------
.PHONY: decisions-validate .PHONY: decisions-validate
+6 -6
View File
@@ -2,7 +2,7 @@
This document governs what clide is allowed to do at runtime, what it's allowed to depend on, and how contributors — human and agent — introduce code into the project. It is binding on all contributors. When in doubt, stop and ask. This document governs what clide is allowed to do at runtime, what it's allowed to depend on, and how contributors — human and agent — introduce code into the project. It is binding on all contributors. When in doubt, stop and ask.
Rationale for specific architectural choices referenced here and in code comments (the D-### markers) lives in `decisions/`. This document sets the rules; `decisions/` records why the rules produced the code they did in a given case. If the two ever disagree, the rule in this document wins until the document itself is changed. Rationale for specific architectural choices referenced here and in code comments (the D-### markers) lives in `governance/decisions/`. This document sets the rules; `governance/decisions/` records why the rules produced the code they did in a given case. If the two ever disagree, the rule in this document wins until the document itself is changed.
## Why this document exists ## Why this document exists
@@ -124,9 +124,9 @@ When removing a dependency:
1. **Grep the entire repository** for references to the package, its exports, and any type names it contributed. `rg '<package>|<PackageType>|<prefix_>'` across the repo. Zero hits outside git history is the goal. A single lingering import will break the build; a single lingering FFI stub or type alias will compile fine and fail at runtime. 1. **Grep the entire repository** for references to the package, its exports, and any type names it contributed. `rg '<package>|<PackageType>|<prefix_>'` across the repo. Zero hits outside git history is the goal. A single lingering import will break the build; a single lingering FFI stub or type alias will compile fine and fail at runtime.
2. **Regenerate the lockfile** as part of the same PR. A `pubspec.yaml` with the dep removed but a `pubspec.lock` that still pins it is a partial removal, and CI or a fresh clone will happily continue installing the package. 2. **Regenerate the lockfile** as part of the same PR. A `pubspec.yaml` with the dep removed but a `pubspec.lock` that still pins it is a partial removal, and CI or a fresh clone will happily continue installing the package.
3. **Update `app/assets/licenses.yaml`** to drop the removed package and any transitive deps it brought in that aren't pulled by anything else. If the license manifest is auto-generated on release, verify the generation script sees the change; if it's maintained by hand, edit it in the same PR. 3. **Update `assets/licenses.yaml`** to drop the removed package and any transitive deps it brought in that aren't pulled by anything else. If the license manifest is auto-generated on release, verify the generation script sees the change; if it's maintained by hand, edit it in the same PR.
4. **Remove any vendored artifacts** tied to the dep — binaries, prebuilt assets, generated bindings — and delete their `BUILD.md` records. An orphaned vendored binary is worse than a removed one because it looks legitimate. 4. **Remove any vendored artifacts** tied to the dep — binaries, prebuilt assets, generated bindings — and delete their `BUILD.md` records. An orphaned vendored binary is worse than a removed one because it looks legitimate.
5. **Check for architectural assumptions** that the dep was carrying. If the removed package was the thing that justified a specific data flow, build step, or platform strategy, either the replacement picks up those responsibilities or the architecture has actually changed and the relevant design decision (see `decisions/`) needs updating. 5. **Check for architectural assumptions** that the dep was carrying. If the removed package was the thing that justified a specific data flow, build step, or platform strategy, either the replacement picks up those responsibilities or the architecture has actually changed and the relevant design decision (see `governance/decisions/`) needs updating.
A dependency is not removed until all five are true. "I deleted the line from pubspec.yaml" is the start of the removal, not the end. A dependency is not removed until all five are true. "I deleted the line from pubspec.yaml" is the start of the removal, not the end.
@@ -188,18 +188,18 @@ When in doubt about a license, the dependency does not land until the question i
### Attribution requirements ### Attribution requirements
- The license manifest at `app/assets/licenses.yaml` lists every dependency with its license, copyright notice, and upstream URL. - The license manifest at `assets/licenses.yaml` lists every dependency with its license, copyright notice, and upstream URL.
- Transitive dependencies are listed, not just direct ones. If `wasm_run` pulls in `wasmtime` which pulls in `cranelift`, all three appear. - Transitive dependencies are listed, not just direct ones. If `wasm_run` pulls in `wasmtime` which pulls in `cranelift`, all three appear.
- Apache-2.0 dependencies get their `NOTICE` file content preserved verbatim, not summarized. - Apache-2.0 dependencies get their `NOTICE` file content preserved verbatim, not summarized.
- Apache-2.0-with-LLVM-exception (e.g., Cranelift, parts of LLVM) requires the LLVM exception text specifically, not just the Apache-2.0 boilerplate. - Apache-2.0-with-LLVM-exception (e.g., Cranelift, parts of LLVM) requires the LLVM exception text specifically, not just the Apache-2.0 boilerplate.
- Fonts and icon sets get attributed even if the license doesn't strictly require it. It's the right thing to do. - Fonts and icon sets get attributed even if the license doesn't strictly require it. It's the right thing to do.
- `app/assets/licenses.yaml` is regenerated as part of the release build, not maintained by hand. A release that ships a stale manifest is a release defect. - `assets/licenses.yaml` is regenerated as part of the release build, not maintained by hand. A release that ships a stale manifest is a release defect.
Adding a dependency means updating the license manifest in the same PR. No exceptions. Adding a dependency means updating the license manifest in the same PR. No exceptions.
## Changelog and commit conventions ## Changelog and commit conventions
clide follows [Keep a Changelog 1.1](https://keepachangelog.com/en/1.1.0/) for `CHANGELOG.md` and [Conventional Commits 1.0](https://www.conventionalcommits.org/en/v1.0.0/) for commit messages. Enforcement is handled by the project's git skill; this section exists so human contributors know the standard before their first PR, and so the connection between these conventions and the rest of the policy is explicit. clide follows [Keep a Changelog 1.1](https://keepachangelog.com/en/1.1.0/) for `CHANGELOG.md` and [Conventional Commits 1.0](https://www.conventionalcommits.org/en/v1.0.0/) for commit messages (see [D-37](governance/decisions/process.md#d-37)). Enforcement is handled by the project's git skill; this section exists so human contributors know the standard before their first PR, and so the connection between these conventions and the rest of the policy is explicit.
Security-relevant changes — CVE responses, dependency-driven vulnerability fixes, the removal of a phoning-home transitive dep, anything where the rules in this document were the reason for the change — go under the `Security` heading of the release's changelog entry, regardless of whether the code change itself looks security-shaped. That heading is the trail future-us follows to reconstruct why a dep was bumped or removed. Lumping security fixes under `Fixed` because the diff looks like a normal bug fix loses that signal and is the wrong choice even when it's technically accurate. Security-relevant changes — CVE responses, dependency-driven vulnerability fixes, the removal of a phoning-home transitive dep, anything where the rules in this document were the reason for the change — go under the `Security` heading of the release's changelog entry, regardless of whether the code change itself looks security-shaped. That heading is the trail future-us follows to reconstruct why a dep was bumped or removed. Lumping security fixes under `Fixed` because the diff looks like a normal bug fix loses that signal and is the wrong choice even when it's technically accurate.
+4 -4
View File
@@ -1,10 +1,10 @@
# clide # clide
An IDE for Claude Code CLI. Native rendering, terminal-first interaction, pql-powered queries, canvas and graph surfaces. Linux and macOS. An IDE for Claude Code CLI. Native rendering, terminal-first interaction, pql-powered queries, canvas and graph surfaces. Linux, macOS and Windows.
## Architecture ## Architecture
Single Flutter package at the repo root. The app hosts everything in-process: IPC server, subsystem handlers (pane, files, editor, git, pql), and the extension framework. tmux owns Claude session persistence (D-41). Single Flutter package at the repo root. The app hosts everything in-process: IPC server, subsystem handlers (pane, files, editor, git, pql), and the extension framework. Claude session persistence is `--resume <session-id>` against Claude Code's transcript files (D-77, superseding the tmux-backed D-41).
- **`lib/`** — all Dart code. Core subsystems (`lib/src/`), kernel services (`lib/kernel/`), UI widgets (`lib/widgets/`), built-in extensions (`lib/builtin/`), the extension framework (`lib/extension/`). - **`lib/`** — all Dart code. Core subsystems (`lib/src/`), kernel services (`lib/kernel/`), UI widgets (`lib/widgets/`), built-in extensions (`lib/builtin/`), the extension framework (`lib/extension/`).
- **PTY**`lib/src/pty/` spawns child processes via Dart FFI `posix_openpt()` + `posix_spawn()` directly; no external helper binary. - **PTY**`lib/src/pty/` spawns child processes via Dart FFI `posix_openpt()` + `posix_spawn()` directly; no external helper binary.
@@ -15,7 +15,7 @@ Claude drives the UI through a `clide` CLI surface (Bash, not MCP). Every CLI su
## Built-in extensions ## Built-in extensions
canvas, claude, claude_control, decisions, diff, editor, extensions_ui, files, git, graph, grammars_core, ipc_status, keybindings_ui, markdown, pql, problems, settings_ui, terminal, theme_picker, tickets, todos, welcome. canvas, claude, claude_control, cli_install, decisions, deeplink, default_layout, diff, editor, extensions_ui, files, git, grammars_core, graph, ipc_status, keybindings_ui, markdown, menubar, output, pql, problems, search, settings_ui, terminal, theme_picker, tickets, todos, view, vim, welcome.
## Building ## Building
@@ -44,7 +44,7 @@ make push-check # pre-push gate: decisions + core + fast + a11y + coverage
## Status ## Status
Pre-v2.0 (`2.0.0-dev`). Interaction model and panel system landed. The Python Textual v1.2.0 predecessor is archived under [`legacy/`](https://github.com/postmeridiem/clide/tree/main/legacy). Active development; the interaction model, panel system, and settings engine have landed. The Python Textual predecessor is archived under [`legacy/`](https://github.com/postmeridiem/clide/tree/main/legacy).
## Documentation ## Documentation
Binary file not shown.
Binary file not shown.
+93
View File
@@ -0,0 +1,93 @@
Copyright (c) 2012-2013, The Mozilla Corporation and Telefonica S.A.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
+93
View File
@@ -0,0 +1,93 @@
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+249
View File
@@ -0,0 +1,249 @@
{
"tab.title": { "translation": "Claude" },
"status.attaching": { "translation": "attaching…" },
"status.no-tmux": { "translation": "no-tmux · fresh every launch" },
"status.exited": { "translation": "session exited" },
"status.primary-exited": { "translation": "session exited — restart clide to retry" },
"banner.title": { "translation": "Claude" },
"banner.warmingUp": { "translation": "Warming up — your conversation will appear here." },
"banner.role.primary": { "translation": "primary" },
"banner.role.secondary": { "translation": "session {index}" },
"composer.hint": { "translation": "Message Claude… (Enter to send · Shift+Enter for newline)" },
"composer.stop": { "translation": "Stop ⎋" },
"composer.stop.hint": { "translation": "Interrupt the running turn (Escape)" },
"composer.removeAttachment": { "translation": "Remove {name}" },
"pane.title.primary": { "translation": "claude — primary" },
"pane.title.secondary": { "translation": "claude — secondary {index}" },
"pane.starting": { "translation": "starting…" },
"pane.modeBadge.semantics": { "translation": "permission mode: {mode}" },
"running.semantics": { "translation": "Claude is running" },
"running.verb.pondering": { "translation": "Pondering" },
"running.verb.conjuring": { "translation": "Conjuring" },
"running.verb.brewing": { "translation": "Brewing" },
"running.verb.tinkering": { "translation": "Tinkering" },
"running.verb.noodling": { "translation": "Noodling" },
"running.verb.percolating": { "translation": "Percolating" },
"running.verb.computing": { "translation": "Computing" },
"running.verb.wrangling": { "translation": "Wrangling" },
"running.verb.untangling": { "translation": "Untangling" },
"running.verb.synthesizing": { "translation": "Synthesizing" },
"running.verb.cogitating": { "translation": "Cogitating" },
"running.verb.whirring": { "translation": "Whirring" },
"running.verb.mincing": { "translation": "Mincing" },
"running.verb.boiling": { "translation": "Boiling" },
"running.verb.humming": { "translation": "Humming" },
"running.verb.buzzing": { "translation": "Buzzing" },
"running.verb.magicking": { "translation": "Magicking" },
"running.verb.cliding": { "translation": "Cliding" },
"running.verb.zooming": { "translation": "Zooming" },
"running.verb.bouncing": { "translation": "Bouncing" },
"conversation.empty": { "translation": "Waiting for Claude…" },
"conversation.label.you": { "translation": "you" },
"conversation.label.claude": { "translation": "claude" },
"conversation.label.clide": { "translation": "clide" },
"conversation.label.agent": { "translation": "agent" },
"conversation.label.agentPrompt": { "translation": "agent prompt" },
"conversation.label.context": { "translation": "context" },
"conversation.label.thinking": { "translation": "thinking" },
"conversation.label.agentThinking": { "translation": "agent thinking" },
"conversation.label.image": { "translation": "image" },
"conversation.label.icon": { "translation": "icons" },
"conversation.label.drawing": { "translation": "drawing" },
"conversation.draw.viewSource": { "translation": "view d2 source" },
"conversation.label.agentRun": { "translation": "agent run" },
"conversation.label.workflow": { "translation": "workflow" },
"conversation.label.error": { "translation": "error" },
"conversation.label.result": { "translation": "result" },
"conversation.label.denied": { "translation": "denied" },
"conversation.segment.prompt": { "translation": "prompt" },
"conversation.segment.result": { "translation": "result" },
"conversation.segment.liveTail": { "translation": "live tail" },
"conversation.segment.usage": { "translation": "usage" },
"conversation.segment.script": { "translation": "script" },
"conversation.workflow.launching": { "translation": "Launching…" },
"conversation.imagePlaceholder": { "translation": "could not load {path}" },
"conversation.bashTail.empty": { "translation": "no independent source to follow" },
"conversation.bashTail.label": { "translation": "live tail" },
"conversation.cluster.activity": { "translation": "Activity" },
"conversation.cluster.edits": { "translation": "Edits" },
"conversation.counter.step": { "translation": "1 step" },
"conversation.counter.steps": { "translation": "{count} steps" },
"conversation.counter.edit": { "translation": "1 edit" },
"conversation.counter.edits": { "translation": "{count} edits" },
"conversation.counter.starting": { "translation": "starting" },
"conversation.counter.agents": { "translation": "{done}/{total} agents" },
"prompt.permission.allow": { "translation": "1. Allow" },
"prompt.permission.allowRemember": { "translation": "2. Allow & don't ask again" },
"prompt.permission.deny": { "translation": "{n}. Deny" },
"prompt.permission.denySimplify": { "translation": "{n}. Deny & simplify" },
"prompt.permission.denySimplify.tooltip": { "translation": "Deny and ask Claude to retry this action in a simpler format — complex interactions don't work well with the permission system." },
"prompt.permission.note.placeholder": { "translation": "add a note (optional) — sent to Claude" },
"prompt.permission.label": { "translation": "permission · {name}" },
"prompt.question.label": { "translation": "question" },
"prompt.review.label": { "translation": "review" },
"prompt.review.title": { "translation": "Review your answers" },
"prompt.submit": { "translation": "Submit" },
"prompt.submitAnswers": { "translation": "Submit answers" },
"prompt.back": { "translation": " Back" },
"prompt.next": { "translation": "Next " },
"prompt.reviewNav": { "translation": "Review " },
"prompt.nav.review": { "translation": "Review" },
"prompt.option.other": { "translation": "Other…" },
"prompt.other.placeholder": { "translation": "type your answer…" },
"prompt.note.placeholder": { "translation": "+ note (optional)" },
"prompt.chatInstead": { "translation": "chat instead" },
"tool.edit.before": { "translation": "— before" },
"tool.edit.after": { "translation": "+ after" },
"tool.bash.background": { "translation": "background" },
"tool.name.Read": { "translation": "Read" },
"tool.name.Edit": { "translation": "Edit" },
"tool.name.MultiEdit": { "translation": "MultiEdit" },
"tool.name.Write": { "translation": "Write" },
"tool.name.NotebookEdit": { "translation": "NotebookEdit" },
"tool.name.WebFetch": { "translation": "WebFetch" },
"tool.name.WebSearch": { "translation": "WebSearch" },
"tool.name.Task": { "translation": "Task" },
"tool.name.TodoWrite": { "translation": "TodoWrite" },
"tool.name.ExitPlanMode": { "translation": "ExitPlanMode" },
"permissionControl.semantics": { "translation": "permission mode: {mode}. Activate to change." },
"permissionControl.tooltip": { "translation": "Permission mode: {mode} — change (Ctrl/Cmd+M cycles)" },
"permissionBadge.tooltip": { "translation": "Permission mode: {mode}. Click to cycle default/acceptEdits/plan; Shift-click for bypassPermissions." },
"permissionBadge.semantics": { "translation": "Permission mode: {label}" },
"card.expand": { "translation": "Expand" },
"card.collapse": { "translation": "Collapse" },
"card.succeeded": { "translation": "succeeded" },
"card.failed": { "translation": "failed" },
"card.copy": { "translation": "copy" },
"taskDock.summary": { "translation": "{count} {tasks} · {done} done" },
"taskDock.task.singular": { "translation": "task" },
"taskDock.task.plural": { "translation": "tasks" },
"taskDock.collapse": { "translation": "Collapse tasks" },
"taskDock.expand": { "translation": "Expand tasks" },
"taskDock.semantics": { "translation": "Claude task list, {summary}, {state}" },
"taskDock.state.expanded": { "translation": "expanded" },
"taskDock.state.collapsed": { "translation": "collapsed" },
"taskDock.status.done": { "translation": "done" },
"taskDock.status.inProgress": { "translation": "in progress" },
"taskDock.status.pending": { "translation": "pending" },
"taskDock.row.semantics": { "translation": "{text}, {status}" },
"sessionPicker.title": { "translation": "Resume a Claude session" },
"sessionPicker.empty": { "translation": "No sessions found for this workspace." },
"modelPicker.cancel": { "translation": "cancel" },
"image.semantics": { "translation": "Image {name}" },
"activity.section.session": { "translation": "SESSION" },
"activity.control.clear": { "translation": "clear" },
"activity.control.compact": { "translation": "compact" },
"activity.control.fork": { "translation": "fork" },
"activity.control.resume": { "translation": "resume" },
"activity.control.refreshUsage": { "translation": "refresh usage" },
"activity.control.semantics": { "translation": "{label} session" },
"activity.control.tooltip": { "translation": "{label} · {command}" },
"activity.empty": { "translation": "No activity recorded yet." },
"activity.section.workflows": { "translation": "WORKFLOWS" },
"activity.workflow.fallback": { "translation": "workflow" },
"activity.workflow.done": { "translation": "done" },
"activity.workflow.starting": { "translation": "starting" },
"activity.section.usage": { "translation": "USAGE" },
"activity.row.session": { "translation": "session" },
"activity.row.weekAll": { "translation": "week (all)" },
"activity.row.weekSonnet": { "translation": "week (sonnet)" },
"activity.section.today": { "translation": "TODAY" },
"activity.row.messages": { "translation": "messages" },
"activity.row.sessions": { "translation": "sessions" },
"activity.row.toolCalls": { "translation": "tool calls" },
"activity.section.lifetime": { "translation": "LIFETIME" },
"activity.section.runtime": { "translation": "RUNTIME · primary" },
"activity.row.model": { "translation": "model" },
"activity.row.effort": { "translation": "effort" },
"activity.row.context": { "translation": "context" },
"activity.row.mode": { "translation": "mode" },
"activity.row.skills": { "translation": "skills" },
"config.empty": { "translation": "Claude environment not loaded." },
"config.section.settings": { "translation": "SETTINGS" },
"config.row.model": { "translation": "model" },
"config.row.effort": { "translation": "effort" },
"config.row.permissionMode": { "translation": "permission mode" },
"config.row.outputStyle": { "translation": "output style" },
"config.row.source": { "translation": "source" },
"config.row.source.value": { "translation": "~/.claude + .claude" },
"config.footer": { "translation": "expand a list to see all · click a skill/agent/command → opens its .md" },
"config.section.skills": { "translation": "SKILLS" },
"config.section.agents": { "translation": "AGENTS" },
"config.section.commands": { "translation": "COMMANDS" },
"config.section.hooks": { "translation": "HOOKS" },
"config.section.permissions": { "translation": "PERMISSIONS" },
"config.section.mcpServers": { "translation": "MCP SERVERS" },
"config.perm.allow": { "translation": "allow" },
"config.perm.ask": { "translation": "ask" },
"config.perm.deny": { "translation": "deny" },
"config.control.semantics": { "translation": "{label}: {value}. Click to change." },
"config.control.tooltip": { "translation": "change {label}" },
"config.control.option.semantics": { "translation": "{label}: {name}" },
"roster.bypass.confirmBody": { "translation": "Enable bypassPermissions? All tool calls will be auto-allowed." },
"roster.bypass.confirm.semantics": { "translation": "Confirm bypass" },
"roster.bypass.confirm.tooltip": { "translation": "Confirm" },
"roster.bypass.ok": { "translation": "OK" },
"roster.bypass.cancel.semantics": { "translation": "Cancel bypass" },
"roster.bypass.cancel.tooltip": { "translation": "Cancel" },
"roster.bypass.cancel": { "translation": "Cancel" },
"roster.hidePane": { "translation": "Hide pane" },
"roster.showPane": { "translation": "Show pane" },
"roster.unmute": { "translation": "Unmute messages" },
"roster.mute": { "translation": "Mute messages" },
"roster.inject": { "translation": "Inject message" },
"roster.fork": { "translation": "Fork session" },
"roster.close": { "translation": "Close session" },
"roster.inject.cancel": { "translation": "Cancel" },
"taskRow.reassign": { "translation": "Reassign task" },
"team.empty": { "translation": "No team active." },
"team.section.tasks": { "translation": "TASKS" },
"tabStrip.activity": { "translation": "Activity" },
"tabStrip.team": { "translation": "Team" },
"tabStrip.team.count": { "translation": "Team · {count}" },
"tabStrip.config": { "translation": "Config" },
"teamChat.section.messages": { "translation": "MESSAGES" },
"teamChat.popOut.semantics": { "translation": "Open full chat pane" },
"teamChat.popOut.tooltip": { "translation": "Open full chat" },
"teamChat.empty": { "translation": "No messages yet." },
"teamChat.composer.placeholder": { "translation": "@name or @team …" },
"teamChat.pane.title": { "translation": "Team Chat" },
"teamChat.interrupt.semantics": { "translation": "Interrupt target session" },
"teamChat.interrupt.label": { "translation": "Interrupt" },
"command.newSecondary": { "translation": "Claude: open a secondary session" },
"command.killAllSessions": { "translation": "Claude: kill all sessions for this repo" },
"command.sessionStorage": { "translation": "Claude: session storage (disk usage + cleanup)" },
"command.activity.foldLevel": { "translation": "Claude: cycle activity fold level" },
"command.agent.show": { "translation": "Claude: show an agent session pane" },
"command.agent.hide": { "translation": "Claude: hide an agent session pane" },
"command.agent.close": { "translation": "Claude: close (kill) an agent session" },
"command.agent.mute": { "translation": "Claude: mute broker delivery to an agent session" },
"command.agent.unmute": { "translation": "Claude: unmute broker delivery to an agent session" },
"command.agent.injectMessage": { "translation": "Claude: inject a text turn into an agent session" },
"command.agent.setPermissionMode": { "translation": "Claude: set permission mode for an agent session" },
"command.mode.cycle": { "translation": "Claude: Cycle permission mode" },
"command.task.reassign": { "translation": "Claude: reassign a shared task to an agent" },
"command.teamChat.open": { "translation": "Claude: open the team chat pane" },
"command.teamChat.post": { "translation": "Claude: post a message into the team channel as the user" },
"command.agent.fork": { "translation": "Claude: fork a managed session into a new branch session" },
"settings.activity.title": { "translation": "Activity" },
"settings.activity.conversation.label": { "translation": "Conversation" },
"settings.activity.foldLevel.label": { "translation": "Fold level" },
"settings.activity.foldLevel.help": { "translation": "How aggressively the conversation folds tool calls, thinking, and results." },
"settings.activity.opt.none": { "translation": "Show everything" },
"settings.activity.opt.tools": { "translation": "Fold tool calls" },
"settings.activity.opt.thinking": { "translation": "Fold tools + thinking" },
"settings.activity.opt.everything": { "translation": "Fold all but prose" },
"settings.claude.title": { "translation": "Claude" },
"settings.claude.newSessionDefaults.label": { "translation": "New session defaults" },
"settings.claude.model.label": { "translation": "Model" },
"settings.claude.model.help": { "translation": "Model for new sessions." },
"settings.claude.effort.label": { "translation": "Effort" },
"settings.claude.effort.help": { "translation": "Reasoning effort for new sessions (applied via --effort at spawn)." },
"settings.claude.permissionMode.label": { "translation": "Permission mode" },
"settings.claude.permissionMode.help": { "translation": "Starting permission mode for new sessions." },
"settings.claude.account.label": { "translation": "Account" },
"settings.claude.account.registry.label": { "translation": "Accounts" },
"settings.claude.account.registry.help": { "translation": "Registered Claude accounts (each a separate config dir + login)." },
"settings.claude.account.workspace.label": { "translation": "Account for this workspace" },
"settings.claude.account.workspace.help": { "translation": "Which Claude account this repo runs under; Default uses the system login." }
}
@@ -0,0 +1,3 @@
{
"command.clide.installCli": { "translation": "clide: Install 'clide' command in PATH" }
}
+16
View File
@@ -0,0 +1,16 @@
{
"tab.title": { "translation": "Decisions" },
"tab.detail.title": { "translation": "Decision" },
"loading": { "translation": "Loading decisions..." },
"error.load": { "translation": "failed to load decisions" },
"empty": { "translation": "No decisions found.\nRun `pql decisions sync` to index." },
"filter.hint": { "translation": "Filter decisions…" },
"refresh.tooltip": { "translation": "Refresh decisions" },
"section.confirmed": { "translation": "CONFIRMED" },
"section.questions": { "translation": "QUESTIONS" },
"section.rejected": { "translation": "REJECTED" },
"badge.resolved": { "translation": "resolved" },
"detail.loading": { "translation": "Loading…" },
"detail.empty": { "translation": "Select a decision to view details." },
"detail.section.refs": { "translation": "CROSS-REFERENCES" }
}
+8
View File
@@ -0,0 +1,8 @@
{
"dialog.title": { "translation": "Open an external link?" },
"dialog.body": { "translation": "A clide:// link from outside the app is asking to:" },
"dialog.warning": { "translation": "Only allow this if you trust where the link came from." },
"button.cancel": { "translation": "Cancel" },
"button.open": { "translation": "Open" },
"command.deeplink.invoke": { "translation": "Open a clide:// deep link" }
}
@@ -0,0 +1,21 @@
{
"command.reset": { "translation": "Layout: Reset to Classic" },
"preset.classic": { "translation": "Classic" },
"command.palette.toggle": { "translation": "Command Palette" },
"command.sidebar.collapse": { "translation": "Toggle Sidebar Collapse" },
"command.context.collapse": { "translation": "Toggle Context Panel Collapse" },
"command.panel.focus.left": { "translation": "Focus Left Panel" },
"command.panel.focus.middle": { "translation": "Focus Middle Panel" },
"command.panel.focus.right": { "translation": "Focus Right Panel" },
"command.panel.focusMode": { "translation": "Toggle Focus Mode" },
"command.panel.focusMode.exit": { "translation": "Exit Focus Mode" },
"command.editor.open": { "translation": "Open Editor" },
"command.editor.close": { "translation": "Close Editor" },
"command.workspace.tab.next": { "translation": "Next Workspace Tab" },
"command.workspace.tab.previous": { "translation": "Previous Workspace Tab" },
"command.sidebar.section.1": { "translation": "Sidebar: Section 1" },
"command.sidebar.section.2": { "translation": "Sidebar: Section 2" },
"command.sidebar.section.3": { "translation": "Sidebar: Section 3" },
"command.sidebar.section.4": { "translation": "Sidebar: Section 4" },
"command.sidebar.section.5": { "translation": "Sidebar: Section 5" }
}
+15
View File
@@ -0,0 +1,15 @@
{
"tab.title": { "translation": "Diff" },
"view.semantics": { "translation": "diff view" },
"status.loading": { "translation": "Loading…" },
"empty.staged": { "translation": "No staged changes." },
"empty.unstaged": { "translation": "No unstaged changes." },
"toolbar.unstaged": { "translation": "Unstaged" },
"toolbar.unstaged.semantics": { "translation": "show unstaged changes" },
"toolbar.staged": { "translation": "Staged" },
"toolbar.staged.semantics": { "translation": "show staged changes" },
"meta.newFile": { "translation": "new file" },
"meta.deleted": { "translation": "deleted" },
"meta.renamedFrom": { "translation": "renamed from {path}" },
"meta.binary": { "translation": "binary" }
}
@@ -1,5 +1,7 @@
{ {
"tab.title": { "translation": "Editor" }, "tab.title": { "translation": "Editor" },
"chrome.title": { "translation": "editor" },
"empty": { "translation": "Open a file to begin editing." }, "empty": { "translation": "Open a file to begin editing." },
"subtitle.no-buffer": { "translation": "no buffer · use `clide open <path>` or pick a file in the tree" } "subtitle.no-buffer": { "translation": "no buffer · use `clide open <path>` or pick a file in the tree" },
"a11y.text-area": { "translation": "editor text area" }
} }
@@ -0,0 +1,8 @@
{
"notice.title": { "translation": "Extension management is coming" },
"notice.body": { "translation": "Installing, enabling, and disabling extensions arrives with third-party (Lua) extension support. For now the built-in extensions are always on." },
"notice.tracked": { "translation": "Tracked in T-8 (Tier 6) · D-16" },
"settings.extensions.title": { "translation": "Extensions" },
"settings.extensions.section.notice": { "translation": "" },
"settings.extensions.field.notice.label": { "translation": "" }
}
+10
View File
@@ -0,0 +1,10 @@
{
"tab.title": { "translation": "Files" },
"loading": { "translation": "Loading…" },
"empty": { "translation": "No visible files" },
"filter.hint": { "translation": "Filter files…" },
"a11y.tree": { "translation": "file tree — {name}" },
"a11y.collapse": { "translation": "Collapse {name}" },
"a11y.expand": { "translation": "Expand {name}" },
"a11y.open": { "translation": "Open {name}" }
}
+38
View File
@@ -0,0 +1,38 @@
{
"tab.title": { "translation": "Git" },
"panel.semantics": { "translation": "git panel" },
"filter.hint": { "translation": "Filter changes…" },
"status.loading": { "translation": "Loading…" },
"status.clean": { "translation": "Nothing to commit, working tree clean." },
"group.conflicts": { "translation": "Merge conflicts" },
"group.staged": { "translation": "Staged" },
"group.changes": { "translation": "Changes" },
"group.untracked": { "translation": "Untracked" },
"action.unstageAll": { "translation": "Unstage all" },
"action.stageAll": { "translation": "Stage all" },
"commit.message.semantics": { "translation": "commit message" },
"commit.button": { "translation": "Commit" },
"commit.button.semantics": { "translation": "commit staged changes" },
"branch.detached": { "translation": "(detached)" },
"action.pull": { "translation": "Pull" },
"action.pull.semantics": { "translation": "git pull" },
"action.push": { "translation": "Push" },
"action.push.semantics": { "translation": "git push" },
"state.added": { "translation": "added" },
"state.modified": { "translation": "modified" },
"state.deleted": { "translation": "deleted" },
"state.renamed": { "translation": "renamed" },
"state.copied": { "translation": "copied" },
"state.untracked": { "translation": "untracked" },
"row.stage.semantics": { "translation": "stage {name}" },
"row.unstage.semantics": { "translation": "unstage {name}" },
"row.discard.semantics": { "translation": "discard changes to {name}" },
"discard.title": { "translation": "Discard changes?" },
"discard.body": { "translation": "Unstaged changes to {name} will be permanently lost." },
"button.cancel": { "translation": "Cancel" },
"button.discard": { "translation": "Discard" },
"branch.switch.semantics": { "translation": "switch branch — {branch}" },
"branchPicker.title": { "translation": "Switch branch" },
"branchPicker.empty": { "translation": "No branches found." },
"branchPicker.loadFailed": { "translation": "failed to load branches" }
}
@@ -0,0 +1,14 @@
{
"command.preset.default": { "translation": "Keymap: Default" },
"command.preset.vim": { "translation": "Keymap: Vim" },
"command.preset.vscode": { "translation": "Keymap: VS Code" },
"command.preset.jetbrains": { "translation": "Keymap: JetBrains" },
"settings.keymap.title": { "translation": "Keymap" },
"settings.keymap.section.preset": { "translation": "Preset" },
"settings.keymap.field.preset.label": { "translation": "Active preset" },
"settings.keymap.field.preset.help": { "translation": "Keyboard layout for the whole app." },
"settings.keymap.field.preset.option.default": { "translation": "Default" },
"settings.keymap.field.preset.option.vim": { "translation": "Vim" },
"settings.keymap.field.preset.option.vscode": { "translation": "VS Code" },
"settings.keymap.field.preset.option.jetbrains": { "translation": "JetBrains" }
}
+5
View File
@@ -0,0 +1,5 @@
{
"empty": { "translation": "Select a .md file to preview it here." },
"chrome.title": { "translation": "viewer" },
"subtitle.lines": { "translation": "{count} lines" }
}
+31
View File
@@ -0,0 +1,31 @@
{
"menu.file": { "translation": "File" },
"menu.view": { "translation": "View" },
"menu.help": { "translation": "Help" },
"about.version": { "translation": "Version" },
"about.commit": { "translation": "Commit" },
"about.built": { "translation": "Built" },
"about.repository": { "translation": "Repository" },
"about.checkUpdates": { "translation": "Check for updates" },
"about.checking": { "translation": "Checking…" },
"about.upToDate": { "translation": "You're on the latest version." },
"about.updateAvailable": { "translation": "clide {version} is available — release notes" },
"about.updateFailed": { "translation": "Couldn't check for updates" },
"licenses.heading": { "translation": "Bundled dependencies" },
"licenses.unavailable": { "translation": "Licenses unavailable." },
"licenses.loading": { "translation": "Loading…" },
"button.close": { "translation": "Close" },
"button.cancel": { "translation": "Cancel" },
"button.open": { "translation": "Open" },
"button.opening": { "translation": "Opening…" },
"button.ok": { "translation": "OK" },
"dialog.openProject.title": { "translation": "Open project" },
"dialog.openProject.body": { "translation": "Enter the path to a git repository." },
"dialog.openProject.error": { "translation": "Not a git repository" },
"dialog.notRepo.title": { "translation": "No git repo found" },
"dialog.notRepo.body": { "translation": "A clide project root requires a git repository." },
"command.file.openFolder": { "translation": "File: Open Folder…" },
"command.file.newWindow": { "translation": "File: New Window" },
"command.file.closeWorkspace": { "translation": "File: Close Project" },
"command.help.about": { "translation": "Help: About clide" }
}
+15
View File
@@ -0,0 +1,15 @@
{
"a11y.log": { "translation": "output log" },
"empty": { "translation": "No output yet." },
"empty.filtered": { "translation": "No output matches the filter." },
"filter.hint": { "translation": "Filter…" },
"chip.level": { "translation": "Level: {level}" },
"chip.source": { "translation": "Source: {source}" },
"chip.clear": { "translation": "Clear" },
"source.all": { "translation": "all" },
"a11y.jumpToLatest": { "translation": "jump to latest" },
"jump.label": { "translation": "Jump to latest ↓" },
"a11y.toggleDock": { "translation": "toggle output dock" },
"dock.label": { "translation": "Output" },
"command.dock.toggle": { "translation": "Toggle output dock" }
}
+16
View File
@@ -0,0 +1,16 @@
{
"tab.title": { "translation": "pql" },
"tab.links.title": { "translation": "Links" },
"filter.markdown.hint": { "translation": "Filter markdown…" },
"loading": { "translation": "Loading…" },
"empty.markdown": { "translation": "No markdown files found." },
"search.vault.hint": { "translation": "Search vault…" },
"search.query.hint": { "translation": "PQL query…" },
"backlinks.empty": { "translation": "Open a file to see its links." },
"backlinks.loading": { "translation": "Loading…" },
"backlinks.semantics": { "translation": "backlinks for {path}" },
"group.backlinks": { "translation": "Backlinks" },
"group.outlinks": { "translation": "Outlinks" },
"group.label": { "translation": "{label} ({count})" },
"group.none": { "translation": "None" }
}
+10
View File
@@ -0,0 +1,10 @@
{
"tab.title": { "translation": "Problems" },
"semantics.panel": { "translation": "problems panel" },
"filter.hint": { "translation": "Filter problems…" },
"count": { "translation": "Problems ({count})" },
"refresh.label": { "translation": "Refresh" },
"refresh.semantics": { "translation": "refresh problems" },
"scanning": { "translation": "Scanning…" },
"empty": { "translation": "No problems found." }
}
+23
View File
@@ -0,0 +1,23 @@
{
"mode.find": { "translation": "Find" },
"mode.vault": { "translation": "Vault" },
"mode.query": { "translation": "Query" },
"mode.markdown": { "translation": "Markdown" },
"find.hint": { "translation": "Search" },
"replace.hint": { "translation": "Replace" },
"include.hint": { "translation": "files to include (e.g. *.dart)" },
"exclude.hint": { "translation": "files to exclude" },
"toggle.regex": { "translation": "Regular expression" },
"toggle.caseInsensitive": { "translation": "Case insensitive" },
"status.searching": { "translation": "Searching…" },
"status.noResults": { "translation": "No results" },
"status.counts": { "translation": "{matches} in {files}" },
"a11y.openMatch": { "translation": "Open {path} line {line}" },
"button.replaceAll": { "translation": "Replace all" },
"button.ok": { "translation": "OK" },
"button.cancel": { "translation": "Cancel" },
"button.confirm": { "translation": "Confirm" },
"dialog.dirty.title": { "translation": "Working tree not clean" },
"dialog.dirty.body": { "translation": "Commit or stash your changes before replacing — git is the only undo." },
"dialog.confirm.body": { "translation": "Replace {matches} match(es) across {files} file(s)? This cannot be undone in clide." }
}
@@ -0,0 +1,17 @@
{
"command.open": { "translation": "Settings…" },
"modal.title": { "translation": "Settings" },
"modal.close": { "translation": "Close" },
"modal.close.hint": { "translation": "Close settings without changing anything" },
"rail.header": { "translation": "Categories" },
"panel.empty": { "translation": "No settings categories are registered yet." },
"search.hint": { "translation": "Search settings…" },
"search.empty": { "translation": "No settings match your search." },
"scope.project": { "translation": "This project" },
"scope.always": { "translation": "All clide" },
"scope.default": { "translation": "Default" },
"scope.reset": { "translation": "Reset to default" },
"scope.tip.project": { "translation": "Stored in this project (.clide)" },
"scope.tip.always": { "translation": "Stored for all clide (~/.clide)" },
"scope.tip.default": { "translation": "Unset — using the default" }
}
@@ -1,7 +1,9 @@
{ {
"tab.title": { "translation": "Terminal" }, "tab.title": { "translation": "Terminal" },
"chrome.title": { "translation": "terminal" },
"subtitle.spawning": { "translation": "spawning shell…" }, "subtitle.spawning": { "translation": "spawning shell…" },
"subtitle.exited": { "translation": "Shell exited." }, "subtitle.exited": { "translation": "Shell exited." },
"error.unavailable": { "translation": "Terminal unavailable" }, "error.unavailable": { "translation": "Terminal unavailable" },
"error.daemon": { "translation": "Backend not connected." } "error.daemon": { "translation": "Backend not connected." },
"a11y.label": { "translation": "terminal — {subtitle}" }
} }
@@ -0,0 +1,25 @@
{
"command.pick": { "translation": "Theme…" },
"modal.title": { "translation": "Select theme" },
"modal.cancel": { "translation": "Cancel" },
"modal.cancel.hint": { "translation": "Close the theme picker without changing the current theme" },
"row.select.hint": { "translation": "Activate this theme" },
"section.appearance": { "translation": "Appearance" },
"toggle.highContrast": { "translation": "High contrast" },
"settings.appearance.title": { "translation": "Appearance" },
"settings.appearance.section.theme": { "translation": "Theme" },
"settings.appearance.section.typography": { "translation": "Typography" },
"settings.appearance.field.theme.label": { "translation": "Theme" },
"settings.appearance.field.theme.help": { "translation": "Color theme; high contrast switches to the accessible variant." },
"settings.appearance.field.uiFont.label": { "translation": "UI font" },
"settings.appearance.field.uiFont.help": { "translation": "Typeface for the app interface; applies live." },
"settings.appearance.field.uiFont.option.josefinSans": { "translation": "Josefin Sans" },
"settings.appearance.field.uiFont.option.inter": { "translation": "Inter" },
"settings.appearance.field.monoFont.label": { "translation": "Monospace font" },
"settings.appearance.field.monoFont.help": { "translation": "Terminal, diffs, code, and IDs; applies live." },
"settings.appearance.field.monoFont.option.jetBrainsMono": { "translation": "JetBrains Mono" },
"settings.appearance.field.monoFont.option.firaMono": { "translation": "Fira Mono" },
"settings.appearance.section.language": { "translation": "Language" },
"settings.appearance.field.language.label": { "translation": "Language" },
"settings.appearance.field.language.help": { "translation": "Language for the app interface; applies live." }
}
+36
View File
@@ -0,0 +1,36 @@
{
"tab.title": { "translation": "Tickets" },
"tab.detail.title": { "translation": "Ticket" },
"type.initiative": { "translation": "Initiative" },
"type.epic": { "translation": "Epic" },
"type.story": { "translation": "Story" },
"type.task": { "translation": "Task" },
"type.bug": { "translation": "Bug" },
"loading": { "translation": "Loading tickets..." },
"error.load": { "translation": "failed to load tickets" },
"empty": { "translation": "No tickets.\nRun `pql ticket new` to create one." },
"filter.hint": { "translation": "Filter tickets…" },
"refresh.tooltip": { "translation": "Refresh tickets" },
"section.in_progress": { "translation": "IN PROGRESS" },
"section.review": { "translation": "REVIEW" },
"section.ready": { "translation": "READY" },
"section.backlog": { "translation": "BACKLOG" },
"section.done": { "translation": "DONE" },
"section.cancelled": { "translation": "CANCELLED" },
"chip.label": { "translation": "{type} type filter" },
"chip.tooltip": { "translation": "Click to toggle · double-click to isolate" },
"badge.wip": { "translation": "WIP" },
"badge.review": { "translation": "REVIEW" },
"badge.cancelled": { "translation": "CANCELLED" },
"pickUp.tooltip": { "translation": "Pick up — hand this ticket to the Claude pane" },
"detail.loading": { "translation": "Loading…" },
"detail.empty": { "translation": "Select a ticket to view details." },
"detail.assigned": { "translation": "assigned: {name}" },
"detail.section.parents": { "translation": "PARENT TREE" },
"detail.section.decisions": { "translation": "REFERENCED DECISIONS" },
"status.backlog": { "translation": "BACKLOG" },
"status.ready": { "translation": "READY" },
"status.in_progress": { "translation": "WIP" },
"status.review": { "translation": "REVIEW" },
"status.done": { "translation": "DONE" }
}
@@ -0,0 +1,11 @@
{
"command.detect": { "translation": "Re-detect tool paths" },
"settings.title": { "translation": "Tools" },
"settings.section.binaries": { "translation": "Supporter binaries" },
"settings.field.claude.label": { "translation": "Claude CLI" },
"settings.field.claude.help": { "translation": "Absolute path to the claude binary; blank to auto-resolve." },
"settings.field.d2.label": { "translation": "d2" },
"settings.field.d2.help": { "translation": "Absolute path to the d2 diagram compiler; blank to auto-resolve." },
"settings.field.detect.label": { "translation": "Re-detect" },
"settings.field.detect.help": { "translation": "Re-scan PATH and the common install dirs, overwriting the paths above." }
}
+5
View File
@@ -0,0 +1,5 @@
{
"command.view.zoomIn": { "translation": "View: Zoom In" },
"command.view.zoomOut": { "translation": "View: Zoom Out" },
"command.view.zoomReset": { "translation": "View: Reset Zoom" }
}
+8
View File
@@ -0,0 +1,8 @@
{
"mode.normal": { "translation": "NORMAL" },
"mode.insert": { "translation": "INSERT" },
"mode.visual": { "translation": "VISUAL" },
"command.vim.mode.normal": { "translation": "Vim: Normal mode" },
"command.vim.mode.insert": { "translation": "Vim: Insert mode" },
"command.vim.mode.visual": { "translation": "Vim: Visual mode" }
}
+42
View File
@@ -0,0 +1,42 @@
{
"title": { "translation": "clide" },
"subtitle": { "translation": "IDE for Claude Code CLI" },
"open-project": { "translation": "Open project" },
"open-project.hint": { "translation": "Pick a git repository to open as the workspace" },
"tab.title": { "translation": "Welcome" },
"section.tips": { "translation": "TIPS" },
"section.start": { "translation": "START" },
"section.recent": { "translation": "RECENT" },
"tips.quickOpen": { "translation": "Quick open" },
"tips.commandPalette": { "translation": "Command palette" },
"tips.toggleSidebar": { "translation": "Toggle sidebar" },
"tips.toggleContext": { "translation": "Toggle context" },
"tips.findInFiles": { "translation": "Find in files" },
"tips.focusMode": { "translation": "Focus mode" },
"action.openFolder": { "translation": "Open folder…" },
"action.newProject": { "translation": "New project…" },
"dialog.newProject.title": { "translation": "New project" },
"dialog.newProject.body": { "translation": "Creates a git repo + a CLAUDE.md, then opens it." },
"button.create": { "translation": "Create" },
"button.creating": { "translation": "Creating…" },
"recent.empty": { "translation": "No recent projects." },
"sticky.label": { "translation": "always open this project on launch" },
"sticky.tooltip": { "translation": "Always open this project on launch" },
"sticky.tooltip.active": { "translation": "Always open this project on launch (uncheck to restore picker)" },
"status.checking": { "translation": "checking…" },
"status.ok": { "translation": "application ok" },
"status.notFound": { "translation": "{tool} not found" },
"status.theme": { "translation": "theme: " },
"dialog.openProject.title": { "translation": "Open project" },
"dialog.openProject.body": { "translation": "Enter the path to a git repository." },
"dialog.openProject.error": { "translation": "Not a git repository" },
"button.cancel": { "translation": "Cancel" },
"button.open": { "translation": "Open" },
"button.opening": { "translation": "Opening…" },
"button.ok": { "translation": "OK" },
"dialog.notRepo.title": { "translation": "No git repo found" },
"dialog.notRepo.body": { "translation": "A clide project needs a git repository. Initialize this folder as one?" },
"dialog.notRepo.initialize": { "translation": "Initialize project" },
"button.initializing": { "translation": "Initializing…" },
"command.workspace.open-project": { "translation": "Workspace: Open project…" }
}
+27
View File
@@ -0,0 +1,27 @@
{
"collapser.expand": { "translation": "Expand" },
"collapser.collapse": { "translation": "Collapse" },
"collapser.expanded": { "translation": "expanded" },
"collapser.collapsed": { "translation": "collapsed" },
"toast.dismiss": { "translation": "Dismiss notification" },
"lightbox.close": { "translation": "close" },
"lightbox.hint": { "translation": "scroll to zoom · double-click to reset · Esc to close" },
"tab.new": { "translation": "New tab" },
"exline.notCommand": { "translation": "Not an editor command" },
"spine.expandSuffix": { "translation": "click to expand" },
"pane.close": { "translation": "Close pane" },
"pane.header": { "translation": "pane header: {title}" },
"reader.back": { "translation": "Back" },
"reader.forward": { "translation": "Forward" },
"reader.jumpToPin": { "translation": "Jump to pin" },
"reader.edit": { "translation": "Edit in editor" },
"reader.pin": { "translation": "Pin" },
"reader.unpin": { "translation": "Unpin" },
"link.openInEditor": { "translation": "Open in editor" },
"resize.axis.width": { "translation": "width" },
"resize.axis.height": { "translation": "height" },
"resize.sidebar": { "translation": "Sidebar {axis}" },
"resize.contextPanel": { "translation": "Context panel {axis}" },
"resize.slot": { "translation": "{slot} {axis}" },
"resize.pixels": { "translation": "{n} pixels" }
}
+249
View File
@@ -0,0 +1,249 @@
{
"tab.title": { "translation": "Claude" },
"status.attaching": { "translation": "verbinden…" },
"status.no-tmux": { "translation": "no-tmux · elke start opnieuw" },
"status.exited": { "translation": "sessie beëindigd" },
"status.primary-exited": { "translation": "sessie beëindigd — herstart clide om opnieuw te proberen" },
"banner.title": { "translation": "Claude" },
"banner.warmingUp": { "translation": "Opstarten — je gesprek verschijnt hier." },
"banner.role.primary": { "translation": "primair" },
"banner.role.secondary": { "translation": "sessie {index}" },
"composer.hint": { "translation": "Bericht aan Claude… (Enter om te verzenden · Shift+Enter voor een nieuwe regel)" },
"composer.stop": { "translation": "Stop ⎋" },
"composer.stop.hint": { "translation": "Onderbreek de lopende beurt (Escape)" },
"composer.removeAttachment": { "translation": "{name} verwijderen" },
"pane.title.primary": { "translation": "claude — primair" },
"pane.title.secondary": { "translation": "claude — secundair {index}" },
"pane.starting": { "translation": "starten…" },
"pane.modeBadge.semantics": { "translation": "permissiemodus: {mode}" },
"running.semantics": { "translation": "Claude is bezig" },
"running.verb.pondering": { "translation": "Peinzen" },
"running.verb.conjuring": { "translation": "Toveren" },
"running.verb.brewing": { "translation": "Brouwen" },
"running.verb.tinkering": { "translation": "Knutselen" },
"running.verb.noodling": { "translation": "Prutsen" },
"running.verb.percolating": { "translation": "Pruttelen" },
"running.verb.computing": { "translation": "Rekenen" },
"running.verb.wrangling": { "translation": "Worstelen" },
"running.verb.untangling": { "translation": "Ontwarren" },
"running.verb.synthesizing": { "translation": "Synthetiseren" },
"running.verb.cogitating": { "translation": "Overpeinzen" },
"running.verb.whirring": { "translation": "Snorren" },
"running.verb.mincing": { "translation": "Hakken" },
"running.verb.boiling": { "translation": "Koken" },
"running.verb.humming": { "translation": "Neuriën" },
"running.verb.buzzing": { "translation": "Zoemen" },
"running.verb.magicking": { "translation": "Goochelen" },
"running.verb.cliding": { "translation": "Cliden" },
"running.verb.zooming": { "translation": "Zoeven" },
"running.verb.bouncing": { "translation": "Stuiteren" },
"conversation.empty": { "translation": "Wachten op Claude…" },
"conversation.label.you": { "translation": "jij" },
"conversation.label.claude": { "translation": "claude" },
"conversation.label.clide": { "translation": "clide" },
"conversation.label.agent": { "translation": "agent" },
"conversation.label.agentPrompt": { "translation": "agent-prompt" },
"conversation.label.context": { "translation": "context" },
"conversation.label.thinking": { "translation": "nadenken" },
"conversation.label.agentThinking": { "translation": "agent denkt na" },
"conversation.label.image": { "translation": "afbeelding" },
"conversation.label.icon": { "translation": "iconen" },
"conversation.label.drawing": { "translation": "tekening" },
"conversation.draw.viewSource": { "translation": "d2-bron tonen" },
"conversation.label.agentRun": { "translation": "agent-uitvoering" },
"conversation.label.workflow": { "translation": "workflow" },
"conversation.label.error": { "translation": "fout" },
"conversation.label.result": { "translation": "resultaat" },
"conversation.label.denied": { "translation": "geweigerd" },
"conversation.segment.prompt": { "translation": "prompt" },
"conversation.segment.result": { "translation": "resultaat" },
"conversation.segment.liveTail": { "translation": "live tail" },
"conversation.segment.usage": { "translation": "verbruik" },
"conversation.segment.script": { "translation": "script" },
"conversation.workflow.launching": { "translation": "Starten…" },
"conversation.imagePlaceholder": { "translation": "kon {path} niet laden" },
"conversation.bashTail.empty": { "translation": "geen onafhankelijke bron om te volgen" },
"conversation.bashTail.label": { "translation": "live tail" },
"conversation.cluster.activity": { "translation": "Activiteit" },
"conversation.cluster.edits": { "translation": "Wijzigingen" },
"conversation.counter.step": { "translation": "1 stap" },
"conversation.counter.steps": { "translation": "{count} stappen" },
"conversation.counter.edit": { "translation": "1 wijziging" },
"conversation.counter.edits": { "translation": "{count} wijzigingen" },
"conversation.counter.starting": { "translation": "starten" },
"conversation.counter.agents": { "translation": "{done}/{total} agents" },
"prompt.permission.allow": { "translation": "1. Toestaan" },
"prompt.permission.allowRemember": { "translation": "2. Toestaan en niet meer vragen" },
"prompt.permission.deny": { "translation": "{n}. Weigeren" },
"prompt.permission.denySimplify": { "translation": "{n}. Weigeren en vereenvoudigen" },
"prompt.permission.denySimplify.tooltip": { "translation": "Weiger en vraag Claude deze actie in een eenvoudigere vorm opnieuw te proberen — complexe interacties werken niet goed met het permissiesysteem." },
"prompt.permission.note.placeholder": { "translation": "voeg een notitie toe (optioneel) — wordt naar Claude gestuurd" },
"prompt.permission.label": { "translation": "permissie · {name}" },
"prompt.question.label": { "translation": "vraag" },
"prompt.review.label": { "translation": "controleren" },
"prompt.review.title": { "translation": "Controleer je antwoorden" },
"prompt.submit": { "translation": "Verzenden" },
"prompt.submitAnswers": { "translation": "Antwoorden verzenden" },
"prompt.back": { "translation": " Terug" },
"prompt.next": { "translation": "Volgende " },
"prompt.reviewNav": { "translation": "Controleren " },
"prompt.nav.review": { "translation": "Controleren" },
"prompt.option.other": { "translation": "Anders…" },
"prompt.other.placeholder": { "translation": "typ je antwoord…" },
"prompt.note.placeholder": { "translation": "+ notitie (optioneel)" },
"prompt.chatInstead": { "translation": "in plaats daarvan chatten" },
"tool.edit.before": { "translation": "— voor" },
"tool.edit.after": { "translation": "+ na" },
"tool.bash.background": { "translation": "achtergrond" },
"tool.name.Read": { "translation": "Lezen" },
"tool.name.Edit": { "translation": "Bewerken" },
"tool.name.MultiEdit": { "translation": "Meervoudig bewerken" },
"tool.name.Write": { "translation": "Schrijven" },
"tool.name.NotebookEdit": { "translation": "Notebook bewerken" },
"tool.name.WebFetch": { "translation": "Web ophalen" },
"tool.name.WebSearch": { "translation": "Web zoeken" },
"tool.name.Task": { "translation": "Taak" },
"tool.name.TodoWrite": { "translation": "Takenlijst" },
"tool.name.ExitPlanMode": { "translation": "Planmodus verlaten" },
"permissionControl.semantics": { "translation": "permissiemodus: {mode}. Activeer om te wijzigen." },
"permissionControl.tooltip": { "translation": "Permissiemodus: {mode} — wijzigen (Ctrl/Cmd+M wisselt)" },
"permissionBadge.tooltip": { "translation": "Permissiemodus: {mode}. Klik om te wisselen tussen default/acceptEdits/plan; Shift-klik voor bypassPermissions." },
"permissionBadge.semantics": { "translation": "Permissiemodus: {label}" },
"card.expand": { "translation": "Uitvouwen" },
"card.collapse": { "translation": "Invouwen" },
"card.succeeded": { "translation": "gelukt" },
"card.failed": { "translation": "mislukt" },
"card.copy": { "translation": "kopiëren" },
"taskDock.summary": { "translation": "{count} {tasks} · {done} klaar" },
"taskDock.task.singular": { "translation": "taak" },
"taskDock.task.plural": { "translation": "taken" },
"taskDock.collapse": { "translation": "Taken invouwen" },
"taskDock.expand": { "translation": "Taken uitvouwen" },
"taskDock.semantics": { "translation": "Takenlijst van Claude, {summary}, {state}" },
"taskDock.state.expanded": { "translation": "uitgevouwen" },
"taskDock.state.collapsed": { "translation": "ingevouwen" },
"taskDock.status.done": { "translation": "klaar" },
"taskDock.status.inProgress": { "translation": "bezig" },
"taskDock.status.pending": { "translation": "in wachtrij" },
"taskDock.row.semantics": { "translation": "{text}, {status}" },
"sessionPicker.title": { "translation": "Een Claude-sessie hervatten" },
"sessionPicker.empty": { "translation": "Geen sessies gevonden voor deze workspace." },
"modelPicker.cancel": { "translation": "annuleren" },
"image.semantics": { "translation": "Afbeelding {name}" },
"activity.section.session": { "translation": "SESSIE" },
"activity.control.clear": { "translation": "wissen" },
"activity.control.compact": { "translation": "compact" },
"activity.control.fork": { "translation": "fork" },
"activity.control.resume": { "translation": "hervatten" },
"activity.control.refreshUsage": { "translation": "verbruik vernieuwen" },
"activity.control.semantics": { "translation": "{label} sessie" },
"activity.control.tooltip": { "translation": "{label} · {command}" },
"activity.empty": { "translation": "Nog geen activiteit vastgelegd." },
"activity.section.workflows": { "translation": "WORKFLOWS" },
"activity.workflow.fallback": { "translation": "workflow" },
"activity.workflow.done": { "translation": "klaar" },
"activity.workflow.starting": { "translation": "starten" },
"activity.section.usage": { "translation": "VERBRUIK" },
"activity.row.session": { "translation": "sessie" },
"activity.row.weekAll": { "translation": "week (alles)" },
"activity.row.weekSonnet": { "translation": "week (sonnet)" },
"activity.section.today": { "translation": "VANDAAG" },
"activity.row.messages": { "translation": "berichten" },
"activity.row.sessions": { "translation": "sessies" },
"activity.row.toolCalls": { "translation": "tool-aanroepen" },
"activity.section.lifetime": { "translation": "TOTAAL" },
"activity.section.runtime": { "translation": "RUNTIME · primair" },
"activity.row.model": { "translation": "model" },
"activity.row.effort": { "translation": "effort" },
"activity.row.context": { "translation": "context" },
"activity.row.mode": { "translation": "modus" },
"activity.row.skills": { "translation": "skills" },
"config.empty": { "translation": "Claude-omgeving niet geladen." },
"config.section.settings": { "translation": "INSTELLINGEN" },
"config.row.model": { "translation": "model" },
"config.row.effort": { "translation": "effort" },
"config.row.permissionMode": { "translation": "permissiemodus" },
"config.row.outputStyle": { "translation": "uitvoerstijl" },
"config.row.source": { "translation": "bron" },
"config.row.source.value": { "translation": "~/.claude + .claude" },
"config.footer": { "translation": "vouw een lijst uit om alles te zien · klik op een skill/agent/command → opent de .md" },
"config.section.skills": { "translation": "SKILLS" },
"config.section.agents": { "translation": "AGENTS" },
"config.section.commands": { "translation": "COMMANDS" },
"config.section.hooks": { "translation": "HOOKS" },
"config.section.permissions": { "translation": "PERMISSIES" },
"config.section.mcpServers": { "translation": "MCP SERVERS" },
"config.perm.allow": { "translation": "toestaan" },
"config.perm.ask": { "translation": "vragen" },
"config.perm.deny": { "translation": "weigeren" },
"config.control.semantics": { "translation": "{label}: {value}. Klik om te wijzigen." },
"config.control.tooltip": { "translation": "{label} wijzigen" },
"config.control.option.semantics": { "translation": "{label}: {name}" },
"roster.bypass.confirmBody": { "translation": "bypassPermissions inschakelen? Alle tool-aanroepen worden automatisch toegestaan." },
"roster.bypass.confirm.semantics": { "translation": "Bypass bevestigen" },
"roster.bypass.confirm.tooltip": { "translation": "Bevestigen" },
"roster.bypass.ok": { "translation": "OK" },
"roster.bypass.cancel.semantics": { "translation": "Bypass annuleren" },
"roster.bypass.cancel.tooltip": { "translation": "Annuleren" },
"roster.bypass.cancel": { "translation": "Annuleren" },
"roster.hidePane": { "translation": "Paneel verbergen" },
"roster.showPane": { "translation": "Paneel tonen" },
"roster.unmute": { "translation": "Berichten weer aanzetten" },
"roster.mute": { "translation": "Berichten dempen" },
"roster.inject": { "translation": "Bericht injecteren" },
"roster.fork": { "translation": "Sessie forken" },
"roster.close": { "translation": "Sessie sluiten" },
"roster.inject.cancel": { "translation": "Annuleren" },
"taskRow.reassign": { "translation": "Taak opnieuw toewijzen" },
"team.empty": { "translation": "Geen team actief." },
"team.section.tasks": { "translation": "TAKEN" },
"tabStrip.activity": { "translation": "Activiteit" },
"tabStrip.team": { "translation": "Team" },
"tabStrip.team.count": { "translation": "Team · {count}" },
"tabStrip.config": { "translation": "Config" },
"teamChat.section.messages": { "translation": "BERICHTEN" },
"teamChat.popOut.semantics": { "translation": "Volledig chatpaneel openen" },
"teamChat.popOut.tooltip": { "translation": "Volledige chat openen" },
"teamChat.empty": { "translation": "Nog geen berichten." },
"teamChat.composer.placeholder": { "translation": "@naam of @team …" },
"teamChat.pane.title": { "translation": "Teamchat" },
"teamChat.interrupt.semantics": { "translation": "Doelsessie onderbreken" },
"teamChat.interrupt.label": { "translation": "Onderbreken" },
"command.newSecondary": { "translation": "Claude: een secundaire sessie openen" },
"command.killAllSessions": { "translation": "Claude: alle sessies voor deze repo afsluiten" },
"command.sessionStorage": { "translation": "Claude: sessieopslag (schijfgebruik + opschonen)" },
"command.activity.foldLevel": { "translation": "Claude: doorloop het invouwniveau van de activiteit" },
"command.agent.show": { "translation": "Claude: een agent-sessiepaneel tonen" },
"command.agent.hide": { "translation": "Claude: een agent-sessiepaneel verbergen" },
"command.agent.close": { "translation": "Claude: een agent-sessie sluiten (afsluiten)" },
"command.agent.mute": { "translation": "Claude: broker-aflevering naar een agent-sessie dempen" },
"command.agent.unmute": { "translation": "Claude: broker-aflevering naar een agent-sessie weer aanzetten" },
"command.agent.injectMessage": { "translation": "Claude: een tekstbeurt in een agent-sessie injecteren" },
"command.agent.setPermissionMode": { "translation": "Claude: permissiemodus voor een agent-sessie instellen" },
"command.mode.cycle": { "translation": "Claude: Permissiemodus doorlopen" },
"command.task.reassign": { "translation": "Claude: een gedeelde taak opnieuw aan een agent toewijzen" },
"command.teamChat.open": { "translation": "Claude: het teamchatpaneel openen" },
"command.teamChat.post": { "translation": "Claude: een bericht als gebruiker in het teamkanaal plaatsen" },
"command.agent.fork": { "translation": "Claude: een beheerde sessie forken naar een nieuwe branch-sessie" },
"settings.activity.title": { "translation": "Activiteit" },
"settings.activity.conversation.label": { "translation": "Gesprek" },
"settings.activity.foldLevel.label": { "translation": "Invouwniveau" },
"settings.activity.foldLevel.help": { "translation": "Hoe agressief het gesprek tool-aanroepen, nadenken en resultaten invouwt." },
"settings.activity.opt.none": { "translation": "Alles tonen" },
"settings.activity.opt.tools": { "translation": "Tool-aanroepen invouwen" },
"settings.activity.opt.thinking": { "translation": "Tools + nadenken invouwen" },
"settings.activity.opt.everything": { "translation": "Alles invouwen behalve tekst" },
"settings.claude.title": { "translation": "Claude" },
"settings.claude.newSessionDefaults.label": { "translation": "Standaardwaarden voor nieuwe sessies" },
"settings.claude.model.label": { "translation": "Model" },
"settings.claude.model.help": { "translation": "Model voor nieuwe sessies." },
"settings.claude.effort.label": { "translation": "Effort" },
"settings.claude.effort.help": { "translation": "Reasoning effort voor nieuwe sessies (toegepast via --effort bij het starten)." },
"settings.claude.permissionMode.label": { "translation": "Permissiemodus" },
"settings.claude.permissionMode.help": { "translation": "Begin-permissiemodus voor nieuwe sessies." },
"settings.claude.account.label": { "translation": "Account" },
"settings.claude.account.registry.label": { "translation": "Accounts" },
"settings.claude.account.registry.help": { "translation": "Geregistreerde Claude-accounts (elk een eigen configmap + login)." },
"settings.claude.account.workspace.label": { "translation": "Account voor deze werkmap" },
"settings.claude.account.workspace.help": { "translation": "Welk Claude-account deze repo gebruikt; Standaard gebruikt de systeemlogin." }
}
@@ -0,0 +1,3 @@
{
"command.clide.installCli": { "translation": "clide: Installeer 'clide'-opdracht in PATH" }
}
+16
View File
@@ -0,0 +1,16 @@
{
"tab.title": { "translation": "Beslissingen" },
"tab.detail.title": { "translation": "Beslissing" },
"loading": { "translation": "Beslissingen laden..." },
"error.load": { "translation": "laden van beslissingen mislukt" },
"empty": { "translation": "Geen beslissingen gevonden.\nVoer `pql decisions sync` uit om te indexeren." },
"filter.hint": { "translation": "Beslissingen filteren…" },
"refresh.tooltip": { "translation": "Beslissingen vernieuwen" },
"section.confirmed": { "translation": "BEVESTIGD" },
"section.questions": { "translation": "VRAGEN" },
"section.rejected": { "translation": "AFGEWEZEN" },
"badge.resolved": { "translation": "opgelost" },
"detail.loading": { "translation": "Laden…" },
"detail.empty": { "translation": "Selecteer een beslissing om de details te bekijken." },
"detail.section.refs": { "translation": "KRUISVERWIJZINGEN" }
}
+8
View File
@@ -0,0 +1,8 @@
{
"dialog.title": { "translation": "Een externe link openen?" },
"dialog.body": { "translation": "Een clide://-link van buiten de app vraagt om:" },
"dialog.warning": { "translation": "Sta dit alleen toe als je vertrouwt waar de link vandaan komt." },
"button.cancel": { "translation": "Annuleren" },
"button.open": { "translation": "Openen" },
"command.deeplink.invoke": { "translation": "Een clide://-deeplink openen" }
}
@@ -0,0 +1,21 @@
{
"command.reset": { "translation": "Indeling: Terugzetten naar Klassiek" },
"preset.classic": { "translation": "Klassiek" },
"command.palette.toggle": { "translation": "Opdrachtenpalet" },
"command.sidebar.collapse": { "translation": "Zijbalk samenvouwen aan/uit" },
"command.context.collapse": { "translation": "Contextpaneel samenvouwen aan/uit" },
"command.panel.focus.left": { "translation": "Focus op linkerpaneel" },
"command.panel.focus.middle": { "translation": "Focus op middelste paneel" },
"command.panel.focus.right": { "translation": "Focus op rechterpaneel" },
"command.panel.focusMode": { "translation": "Focusmodus aan/uit" },
"command.panel.focusMode.exit": { "translation": "Focusmodus afsluiten" },
"command.editor.open": { "translation": "Editor openen" },
"command.editor.close": { "translation": "Editor sluiten" },
"command.workspace.tab.next": { "translation": "Volgend werkruimtetabblad" },
"command.workspace.tab.previous": { "translation": "Vorig werkruimtetabblad" },
"command.sidebar.section.1": { "translation": "Zijbalk: Sectie 1" },
"command.sidebar.section.2": { "translation": "Zijbalk: Sectie 2" },
"command.sidebar.section.3": { "translation": "Zijbalk: Sectie 3" },
"command.sidebar.section.4": { "translation": "Zijbalk: Sectie 4" },
"command.sidebar.section.5": { "translation": "Zijbalk: Sectie 5" }
}
+15
View File
@@ -0,0 +1,15 @@
{
"tab.title": { "translation": "Diff" },
"view.semantics": { "translation": "diff-weergave" },
"status.loading": { "translation": "Laden…" },
"empty.staged": { "translation": "Geen gestagede wijzigingen." },
"empty.unstaged": { "translation": "Geen niet-gestagede wijzigingen." },
"toolbar.unstaged": { "translation": "Niet gestaged" },
"toolbar.unstaged.semantics": { "translation": "niet-gestagede wijzigingen tonen" },
"toolbar.staged": { "translation": "Gestaged" },
"toolbar.staged.semantics": { "translation": "gestagede wijzigingen tonen" },
"meta.newFile": { "translation": "nieuw bestand" },
"meta.deleted": { "translation": "verwijderd" },
"meta.renamedFrom": { "translation": "hernoemd vanaf {path}" },
"meta.binary": { "translation": "binair" }
}
+7
View File
@@ -0,0 +1,7 @@
{
"tab.title": { "translation": "Editor" },
"chrome.title": { "translation": "editor" },
"empty": { "translation": "Open een bestand om te beginnen met bewerken." },
"subtitle.no-buffer": { "translation": "geen buffer · gebruik `clide open <path>` of kies een bestand in de boom" },
"a11y.text-area": { "translation": "tekstgebied editor" }
}
@@ -0,0 +1,8 @@
{
"notice.title": { "translation": "Extensiebeheer komt eraan" },
"notice.body": { "translation": "Het installeren, in- en uitschakelen van extensies arriveert samen met ondersteuning voor externe (Lua-)extensies. Voorlopig staan de ingebouwde extensies altijd aan." },
"notice.tracked": { "translation": "Bijgehouden in T-8 (Tier 6) · D-16" },
"settings.extensions.title": { "translation": "Extensies" },
"settings.extensions.section.notice": { "translation": "" },
"settings.extensions.field.notice.label": { "translation": "" }
}
+10
View File
@@ -0,0 +1,10 @@
{
"tab.title": { "translation": "Bestanden" },
"loading": { "translation": "Laden…" },
"empty": { "translation": "Geen zichtbare bestanden" },
"filter.hint": { "translation": "Bestanden filteren…" },
"a11y.tree": { "translation": "bestandsboom — {name}" },
"a11y.collapse": { "translation": "{name} samenvouwen" },
"a11y.expand": { "translation": "{name} uitvouwen" },
"a11y.open": { "translation": "{name} openen" }
}
+38
View File
@@ -0,0 +1,38 @@
{
"tab.title": { "translation": "Git" },
"panel.semantics": { "translation": "git-paneel" },
"filter.hint": { "translation": "Wijzigingen filteren…" },
"status.loading": { "translation": "Laden…" },
"status.clean": { "translation": "Niets om vast te leggen, werkmap is schoon." },
"group.conflicts": { "translation": "Samenvoegconflicten" },
"group.staged": { "translation": "Klaargezet" },
"group.changes": { "translation": "Wijzigingen" },
"group.untracked": { "translation": "Niet gevolgd" },
"action.unstageAll": { "translation": "Alles terugnemen" },
"action.stageAll": { "translation": "Alles klaarzetten" },
"commit.message.semantics": { "translation": "commitbericht" },
"commit.button": { "translation": "Vastleggen" },
"commit.button.semantics": { "translation": "klaargezette wijzigingen vastleggen" },
"branch.detached": { "translation": "(losgekoppeld)" },
"action.pull": { "translation": "Pullen" },
"action.pull.semantics": { "translation": "git pull" },
"action.push": { "translation": "Pushen" },
"action.push.semantics": { "translation": "git push" },
"state.added": { "translation": "toegevoegd" },
"state.modified": { "translation": "gewijzigd" },
"state.deleted": { "translation": "verwijderd" },
"state.renamed": { "translation": "hernoemd" },
"state.copied": { "translation": "gekopieerd" },
"state.untracked": { "translation": "niet gevolgd" },
"row.stage.semantics": { "translation": "{name} klaarzetten" },
"row.unstage.semantics": { "translation": "{name} terugnemen" },
"row.discard.semantics": { "translation": "wijzigingen aan {name} ongedaan maken" },
"discard.title": { "translation": "Wijzigingen ongedaan maken?" },
"discard.body": { "translation": "Niet-klaargezette wijzigingen aan {name} gaan definitief verloren." },
"button.cancel": { "translation": "Annuleren" },
"button.discard": { "translation": "Ongedaan maken" },
"branch.switch.semantics": { "translation": "branch wisselen — {branch}" },
"branchPicker.title": { "translation": "Branch wisselen" },
"branchPicker.empty": { "translation": "Geen branches gevonden." },
"branchPicker.loadFailed": { "translation": "laden van branches mislukt" }
}
@@ -0,0 +1,6 @@
{
"connected": { "translation": "verbonden" },
"connected.hint": { "translation": "backend-isolate is bereikbaar" },
"disconnected": { "translation": "niet verbonden" },
"disconnected.hint": { "translation": "backend-isolate draait niet" }
}
@@ -0,0 +1,14 @@
{
"command.preset.default": { "translation": "Toetsindeling: Standaard" },
"command.preset.vim": { "translation": "Toetsindeling: Vim" },
"command.preset.vscode": { "translation": "Toetsindeling: VS Code" },
"command.preset.jetbrains": { "translation": "Toetsindeling: JetBrains" },
"settings.keymap.title": { "translation": "Toetsindeling" },
"settings.keymap.section.preset": { "translation": "Voorinstelling" },
"settings.keymap.field.preset.label": { "translation": "Actieve voorinstelling" },
"settings.keymap.field.preset.help": { "translation": "Toetsenbordindeling voor de hele app." },
"settings.keymap.field.preset.option.default": { "translation": "Standaard" },
"settings.keymap.field.preset.option.vim": { "translation": "Vim" },
"settings.keymap.field.preset.option.vscode": { "translation": "VS Code" },
"settings.keymap.field.preset.option.jetbrains": { "translation": "JetBrains" }
}
+5
View File
@@ -0,0 +1,5 @@
{
"empty": { "translation": "Selecteer een .md-bestand om het hier te bekijken." },
"chrome.title": { "translation": "weergave" },
"subtitle.lines": { "translation": "{count} regels" }
}
+31
View File
@@ -0,0 +1,31 @@
{
"menu.file": { "translation": "Bestand" },
"menu.view": { "translation": "Beeld" },
"menu.help": { "translation": "Help" },
"about.version": { "translation": "Versie" },
"about.commit": { "translation": "Commit" },
"about.built": { "translation": "Gebouwd" },
"about.repository": { "translation": "Repository" },
"about.checkUpdates": { "translation": "Controleer op updates" },
"about.checking": { "translation": "Bezig met controleren…" },
"about.upToDate": { "translation": "Je hebt de nieuwste versie." },
"about.updateAvailable": { "translation": "clide {version} is beschikbaar — releaseopmerkingen" },
"about.updateFailed": { "translation": "Kon niet op updates controleren" },
"licenses.heading": { "translation": "Meegeleverde afhankelijkheden" },
"licenses.unavailable": { "translation": "Licenties niet beschikbaar." },
"licenses.loading": { "translation": "Laden…" },
"button.close": { "translation": "Sluiten" },
"button.cancel": { "translation": "Annuleren" },
"button.open": { "translation": "Openen" },
"button.opening": { "translation": "Openen…" },
"button.ok": { "translation": "OK" },
"dialog.openProject.title": { "translation": "Project openen" },
"dialog.openProject.body": { "translation": "Voer het pad naar een git-repository in." },
"dialog.openProject.error": { "translation": "Geen git-repository" },
"dialog.notRepo.title": { "translation": "Geen git-repo gevonden" },
"dialog.notRepo.body": { "translation": "Een clide-projecthoofdmap vereist een git-repository." },
"command.file.openFolder": { "translation": "Bestand: Map openen…" },
"command.file.newWindow": { "translation": "Bestand: Nieuw venster" },
"command.file.closeWorkspace": { "translation": "Bestand: Project sluiten" },
"command.help.about": { "translation": "Help: Over clide" }
}
+15
View File
@@ -0,0 +1,15 @@
{
"a11y.log": { "translation": "uitvoerlogboek" },
"empty": { "translation": "Nog geen uitvoer." },
"empty.filtered": { "translation": "Geen uitvoer komt overeen met het filter." },
"filter.hint": { "translation": "Filteren…" },
"chip.level": { "translation": "Niveau: {level}" },
"chip.source": { "translation": "Bron: {source}" },
"chip.clear": { "translation": "Wissen" },
"source.all": { "translation": "alle" },
"a11y.jumpToLatest": { "translation": "naar nieuwste springen" },
"jump.label": { "translation": "Naar nieuwste ↓" },
"a11y.toggleDock": { "translation": "uitvoerdok aan/uit" },
"dock.label": { "translation": "Uitvoer" },
"command.dock.toggle": { "translation": "Uitvoerdok aan/uit" }
}
+16
View File
@@ -0,0 +1,16 @@
{
"tab.title": { "translation": "pql" },
"tab.links.title": { "translation": "Koppelingen" },
"filter.markdown.hint": { "translation": "Markdown filteren…" },
"loading": { "translation": "Laden…" },
"empty.markdown": { "translation": "Geen markdown-bestanden gevonden." },
"search.vault.hint": { "translation": "Vault doorzoeken…" },
"search.query.hint": { "translation": "PQL-query…" },
"backlinks.empty": { "translation": "Open een bestand om de koppelingen te zien." },
"backlinks.loading": { "translation": "Laden…" },
"backlinks.semantics": { "translation": "backlinks voor {path}" },
"group.backlinks": { "translation": "Backlinks" },
"group.outlinks": { "translation": "Uitgaande koppelingen" },
"group.label": { "translation": "{label} ({count})" },
"group.none": { "translation": "Geen" }
}
+10
View File
@@ -0,0 +1,10 @@
{
"tab.title": { "translation": "Problemen" },
"semantics.panel": { "translation": "problemenpaneel" },
"filter.hint": { "translation": "Problemen filteren…" },
"count": { "translation": "Problemen ({count})" },
"refresh.label": { "translation": "Vernieuwen" },
"refresh.semantics": { "translation": "problemen vernieuwen" },
"scanning": { "translation": "Scannen…" },
"empty": { "translation": "Geen problemen gevonden." }
}
+23
View File
@@ -0,0 +1,23 @@
{
"mode.find": { "translation": "Zoeken" },
"mode.vault": { "translation": "Kluis" },
"mode.query": { "translation": "Query" },
"mode.markdown": { "translation": "Markdown" },
"find.hint": { "translation": "Zoeken" },
"replace.hint": { "translation": "Vervangen" },
"include.hint": { "translation": "te includeren bestanden (bijv. *.dart)" },
"exclude.hint": { "translation": "uit te sluiten bestanden" },
"toggle.regex": { "translation": "Reguliere expressie" },
"toggle.caseInsensitive": { "translation": "Hoofdletterongevoelig" },
"status.searching": { "translation": "Zoeken…" },
"status.noResults": { "translation": "Geen resultaten" },
"status.counts": { "translation": "{matches} in {files}" },
"a11y.openMatch": { "translation": "{path} regel {line} openen" },
"button.replaceAll": { "translation": "Alles vervangen" },
"button.ok": { "translation": "OK" },
"button.cancel": { "translation": "Annuleren" },
"button.confirm": { "translation": "Bevestigen" },
"dialog.dirty.title": { "translation": "Werkboom niet schoon" },
"dialog.dirty.body": { "translation": "Commit of stash je wijzigingen voordat je vervangt — Git is de enige manier om ongedaan te maken." },
"dialog.confirm.body": { "translation": "{matches} overeenkomst(en) in {files} bestand(en) vervangen? Dit kan niet ongedaan worden gemaakt in clide." }
}
@@ -0,0 +1,17 @@
{
"command.open": { "translation": "Instellingen…" },
"modal.title": { "translation": "Instellingen" },
"modal.close": { "translation": "Sluiten" },
"modal.close.hint": { "translation": "Sluit instellingen zonder iets te wijzigen" },
"rail.header": { "translation": "Categorieën" },
"panel.empty": { "translation": "Er zijn nog geen instellingscategorieën geregistreerd." },
"search.hint": { "translation": "Instellingen zoeken…" },
"search.empty": { "translation": "Geen instellingen komen overeen met je zoekopdracht." },
"scope.project": { "translation": "Dit project" },
"scope.always": { "translation": "Heel clide" },
"scope.default": { "translation": "Standaard" },
"scope.reset": { "translation": "Terugzetten naar standaard" },
"scope.tip.project": { "translation": "Opgeslagen in dit project (.clide)" },
"scope.tip.always": { "translation": "Opgeslagen voor heel clide (~/.clide)" },
"scope.tip.default": { "translation": "Niet ingesteld — standaard wordt gebruikt" }
}
+9
View File
@@ -0,0 +1,9 @@
{
"tab.title": { "translation": "Terminal" },
"chrome.title": { "translation": "terminal" },
"subtitle.spawning": { "translation": "shell starten…" },
"subtitle.exited": { "translation": "Shell afgesloten." },
"error.unavailable": { "translation": "Terminal niet beschikbaar" },
"error.daemon": { "translation": "Backend niet verbonden." },
"a11y.label": { "translation": "terminal — {subtitle}" }
}
@@ -0,0 +1,25 @@
{
"command.pick": { "translation": "Thema…" },
"modal.title": { "translation": "Thema selecteren" },
"modal.cancel": { "translation": "Annuleren" },
"modal.cancel.hint": { "translation": "Sluit de themakiezer zonder het huidige thema te wijzigen" },
"row.select.hint": { "translation": "Dit thema activeren" },
"section.appearance": { "translation": "Weergave" },
"toggle.highContrast": { "translation": "Hoog contrast" },
"settings.appearance.title": { "translation": "Weergave" },
"settings.appearance.section.theme": { "translation": "Thema" },
"settings.appearance.section.typography": { "translation": "Typografie" },
"settings.appearance.field.theme.label": { "translation": "Thema" },
"settings.appearance.field.theme.help": { "translation": "Kleurthema; hoog contrast schakelt over naar de toegankelijke variant." },
"settings.appearance.field.uiFont.label": { "translation": "UI-lettertype" },
"settings.appearance.field.uiFont.help": { "translation": "Lettertype voor de app-interface; wordt direct toegepast." },
"settings.appearance.field.uiFont.option.josefinSans": { "translation": "Josefin Sans" },
"settings.appearance.field.uiFont.option.inter": { "translation": "Inter" },
"settings.appearance.field.monoFont.label": { "translation": "Monospace-lettertype" },
"settings.appearance.field.monoFont.help": { "translation": "Terminal, diffs, code en ID's; wordt direct toegepast." },
"settings.appearance.field.monoFont.option.jetBrainsMono": { "translation": "JetBrains Mono" },
"settings.appearance.field.monoFont.option.firaMono": { "translation": "Fira Mono" },
"settings.appearance.section.language": { "translation": "Taal" },
"settings.appearance.field.language.label": { "translation": "Taal" },
"settings.appearance.field.language.help": { "translation": "Taal voor de app-interface; wordt direct toegepast." }
}
+36
View File
@@ -0,0 +1,36 @@
{
"tab.title": { "translation": "Tickets" },
"tab.detail.title": { "translation": "Ticket" },
"type.initiative": { "translation": "Initiatief" },
"type.epic": { "translation": "Epic" },
"type.story": { "translation": "Story" },
"type.task": { "translation": "Taak" },
"type.bug": { "translation": "Bug" },
"loading": { "translation": "Tickets laden..." },
"error.load": { "translation": "laden van tickets mislukt" },
"empty": { "translation": "Geen tickets.\nVoer `pql ticket new` uit om er een aan te maken." },
"filter.hint": { "translation": "Tickets filteren…" },
"refresh.tooltip": { "translation": "Tickets vernieuwen" },
"section.in_progress": { "translation": "IN BEHANDELING" },
"section.review": { "translation": "BEOORDELING" },
"section.ready": { "translation": "GEREED" },
"section.backlog": { "translation": "BACKLOG" },
"section.done": { "translation": "KLAAR" },
"section.cancelled": { "translation": "GEANNULEERD" },
"chip.label": { "translation": "filter op type {type}" },
"chip.tooltip": { "translation": "Klik om te wisselen · dubbelklik om te isoleren" },
"badge.wip": { "translation": "WIP" },
"badge.review": { "translation": "BEOORDELING" },
"badge.cancelled": { "translation": "GEANNULEERD" },
"pickUp.tooltip": { "translation": "Oppakken — geef dit ticket door aan het Claude-paneel" },
"detail.loading": { "translation": "Laden…" },
"detail.empty": { "translation": "Selecteer een ticket om de details te bekijken." },
"detail.assigned": { "translation": "toegewezen aan: {name}" },
"detail.section.parents": { "translation": "BOVENLIGGENDE BOOM" },
"detail.section.decisions": { "translation": "GEREFEREERDE BESLISSINGEN" },
"status.backlog": { "translation": "BACKLOG" },
"status.ready": { "translation": "GEREED" },
"status.in_progress": { "translation": "WIP" },
"status.review": { "translation": "BEOORDELING" },
"status.done": { "translation": "KLAAR" }
}
@@ -0,0 +1,11 @@
{
"command.detect": { "translation": "Tool-paden opnieuw detecteren" },
"settings.title": { "translation": "Tools" },
"settings.section.binaries": { "translation": "Hulpprogramma's" },
"settings.field.claude.label": { "translation": "Claude-CLI" },
"settings.field.claude.help": { "translation": "Absoluut pad naar het claude-binary; leeg om automatisch te bepalen." },
"settings.field.d2.label": { "translation": "d2" },
"settings.field.d2.help": { "translation": "Absoluut pad naar de d2-diagramcompiler; leeg om automatisch te bepalen." },
"settings.field.detect.label": { "translation": "Opnieuw detecteren" },
"settings.field.detect.help": { "translation": "Scan PATH en de gangbare installatiemappen opnieuw; overschrijft de paden hierboven." }
}
+5
View File
@@ -0,0 +1,5 @@
{
"command.view.zoomIn": { "translation": "Weergave: Inzoomen" },
"command.view.zoomOut": { "translation": "Weergave: Uitzoomen" },
"command.view.zoomReset": { "translation": "Weergave: Zoom resetten" }
}
+8
View File
@@ -0,0 +1,8 @@
{
"mode.normal": { "translation": "NORMAL" },
"mode.insert": { "translation": "INSERT" },
"mode.visual": { "translation": "VISUAL" },
"command.vim.mode.normal": { "translation": "Vim: Normale modus" },
"command.vim.mode.insert": { "translation": "Vim: Invoegmodus" },
"command.vim.mode.visual": { "translation": "Vim: Visuele modus" }
}
+42
View File
@@ -0,0 +1,42 @@
{
"title": { "translation": "clide" },
"subtitle": { "translation": "IDE voor Claude Code CLI" },
"open-project": { "translation": "Project openen" },
"open-project.hint": { "translation": "Kies een git-repository om als werkruimte te openen" },
"tab.title": { "translation": "Welkom" },
"section.tips": { "translation": "TIPS" },
"section.start": { "translation": "STARTEN" },
"section.recent": { "translation": "RECENT" },
"tips.quickOpen": { "translation": "Snel openen" },
"tips.commandPalette": { "translation": "Opdrachtenpalet" },
"tips.toggleSidebar": { "translation": "Zijbalk in-/uitschakelen" },
"tips.toggleContext": { "translation": "Context in-/uitschakelen" },
"tips.findInFiles": { "translation": "Zoeken in bestanden" },
"tips.focusMode": { "translation": "Focusmodus" },
"action.openFolder": { "translation": "Map openen…" },
"action.newProject": { "translation": "Nieuw project…" },
"dialog.newProject.title": { "translation": "Nieuw project" },
"dialog.newProject.body": { "translation": "Maakt een git-repo + een CLAUDE.md, en opent het." },
"button.create": { "translation": "Aanmaken" },
"button.creating": { "translation": "Aanmaken…" },
"recent.empty": { "translation": "Geen recente projecten." },
"sticky.label": { "translation": "dit project altijd openen bij opstarten" },
"sticky.tooltip": { "translation": "Dit project altijd openen bij opstarten" },
"sticky.tooltip.active": { "translation": "Dit project altijd openen bij opstarten (vink uit om de kiezer te herstellen)" },
"status.checking": { "translation": "controleren…" },
"status.ok": { "translation": "applicatie ok" },
"status.notFound": { "translation": "{tool} niet gevonden" },
"status.theme": { "translation": "thema: " },
"dialog.openProject.title": { "translation": "Project openen" },
"dialog.openProject.body": { "translation": "Voer het pad naar een git-repository in." },
"dialog.openProject.error": { "translation": "Geen git-repository" },
"button.cancel": { "translation": "Annuleren" },
"button.open": { "translation": "Openen" },
"button.opening": { "translation": "Openen…" },
"button.ok": { "translation": "OK" },
"dialog.notRepo.title": { "translation": "Geen git-repo gevonden" },
"dialog.notRepo.body": { "translation": "Een clide-project heeft een git-repository nodig. Deze map als project initialiseren?" },
"dialog.notRepo.initialize": { "translation": "Project initialiseren" },
"button.initializing": { "translation": "Initialiseren…" },
"command.workspace.open-project": { "translation": "Werkruimte: Project openen…" }
}
+27
View File
@@ -0,0 +1,27 @@
{
"collapser.expand": { "translation": "Uitvouwen" },
"collapser.collapse": { "translation": "Samenvouwen" },
"collapser.expanded": { "translation": "uitgevouwen" },
"collapser.collapsed": { "translation": "samengevouwen" },
"toast.dismiss": { "translation": "Melding sluiten" },
"lightbox.close": { "translation": "sluiten" },
"lightbox.hint": { "translation": "scrollen om te zoomen · dubbelklik om te resetten · Esc om te sluiten" },
"tab.new": { "translation": "Nieuw tabblad" },
"exline.notCommand": { "translation": "Geen editoropdracht" },
"spine.expandSuffix": { "translation": "klik om uit te vouwen" },
"pane.close": { "translation": "Paneel sluiten" },
"pane.header": { "translation": "paneelkop: {title}" },
"reader.back": { "translation": "Terug" },
"reader.forward": { "translation": "Vooruit" },
"reader.jumpToPin": { "translation": "Naar speld springen" },
"reader.edit": { "translation": "Bewerken in editor" },
"reader.pin": { "translation": "Vastmaken" },
"reader.unpin": { "translation": "Losmaken" },
"link.openInEditor": { "translation": "Openen in editor" },
"resize.axis.width": { "translation": "breedte" },
"resize.axis.height": { "translation": "hoogte" },
"resize.sidebar": { "translation": "Zijbalk {axis}" },
"resize.contextPanel": { "translation": "Contextpaneel {axis}" },
"resize.slot": { "translation": "{slot} {axis}" },
"resize.pixels": { "translation": "{n} pixels" }
}
+27 -4
View File
@@ -39,7 +39,7 @@ self:
# Auto-synced from pubspec.yaml `version:` by `make gen-build-info` # Auto-synced from pubspec.yaml `version:` by `make gen-build-info`
# (runs implicitly on every build/run/test). Don't hand-edit; bump # (runs implicitly on every build/run/test). Don't hand-edit; bump
# pubspec instead. # pubspec instead.
version: "2.5.0" version: "2.9.0"
homepage: https://github.com/postmeridiem/clide homepage: https://github.com/postmeridiem/clide
license: MIT license: MIT
license_file: assets/LICENSE license_file: assets/LICENSE
@@ -54,10 +54,32 @@ dependencies:
license: OFL-1.1 license: OFL-1.1
license_file: assets/fonts/jetbrains_mono/OFL.txt license_file: assets/fonts/jetbrains_mono/OFL.txt
purpose: >- purpose: >-
Monospace face for terminal panes, diff views, code editors, and Default monospace face for terminal panes, diff views, code editors, and
any other monospace surface. any other monospace surface.
weights_bundled: [Regular, Italic, Bold, BoldItalic] weights_bundled: [Regular, Italic, Bold, BoldItalic]
- name: Fira Mono
kind: font
version: "3.206"
homepage: https://github.com/mozilla/Fira
license: OFL-1.1
license_file: assets/fonts/fira_mono/OFL.txt
purpose: >-
Selectable monospace face (Settings → Appearance, T-471); JetBrains Mono
remains the default.
weights_bundled: [Regular, Bold]
- name: Inter
kind: font
version: "variable"
homepage: https://github.com/rsms/inter
license: OFL-1.1
license_file: assets/fonts/inter/OFL.txt
purpose: >-
Selectable application UI face (bundled T-460); Josefin Sans is the
default. Variable font with optical-size and weight axes.
weights_bundled: [VariableFont, Italic-VariableFont]
- name: Josefin Sans - name: Josefin Sans
kind: font kind: font
version: "variable" version: "variable"
@@ -65,8 +87,9 @@ dependencies:
license: OFL-1.1 license: OFL-1.1
license_file: assets/fonts/josefin_sans/OFL.txt license_file: assets/fonts/josefin_sans/OFL.txt
purpose: >- purpose: >-
Application UI face. Default weight Light (300); full 100-700 Default application UI face; default weight Light (300), full 100-700
range available via the variable-font weight axis. range via the variable-font weight axis. Inter is bundled as a
selectable alternative.
weights_bundled: [VariableFont, Italic-VariableFont] weights_bundled: [VariableFont, Italic-VariableFont]
- name: Phosphor Icons - name: Phosphor Icons
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# T-88 — track dugite-native (bundled git) upstream releases for security
# updates (D-59). Compares the pinned DUGITE_VERSION against the latest
# desktop/dugite-native release and flags CVE / security mentions.
#
# WHY THIS SCRIPT IS THE MAINTENANCE HOME: dugite's binary is FETCHED at build
# time (`make dugite-fetch`), not built, and native/dugite/ is gitignored — so
# there is no `native/dugite/BUILD.md` (D-63) to record it. The pin lives in the
# Makefile (DUGITE_VERSION / DUGITE_COMMIT); this script + its `make dugite-check`
# target are the version-tracking calendar D-59 requires.
#
# CADENCE: run quarterly during normal operation. Run IMMEDIATELY on a git or
# dugite-native security advisory — subscribe to:
# https://github.com/git/git/security/advisories
# https://github.com/desktop/dugite-native/security/advisories
#
# This check is INFORMATIONAL (not a push gate): it reports drift and flags CVE
# mentions. The bump itself is manual per D-63 (reproducibility record) and is
# automated by T-25 (CI). A bump updates: Makefile DUGITE_VERSION/COMMIT, the
# fetched binary in native/dugite/, and assets/licenses.yaml if the bundled git
# or dugite version changed.
set -euo pipefail
cd "$(dirname "$0")/.."
REPO="desktop/dugite-native"
CURRENT="$(grep -E '^DUGITE_VERSION[[:space:]]*:=' Makefile | head -1 | sed -E 's/.*:=[[:space:]]*//')"
# Public read — no auth needed for a quarterly check. gh would raise the rate
# limit but isn't required; curl keeps this dependency-free.
json="$(curl -fsSL -H 'Accept: application/vnd.github+json' "https://api.github.com/repos/$REPO/releases/latest" 2>/dev/null || true)"
LATEST="$(printf '%s' "$json" | sed -nE 's/.*"tag_name":[[:space:]]*"([^"]+)".*/\1/p' | head -1)"
if [[ -z "$LATEST" ]]; then
echo "==> dugite-check: couldn't reach the dugite-native releases API." >&2
echo " Check manually: https://github.com/$REPO/releases" >&2
exit 2
fi
echo "==> dugite-check (T-88): bundled '$CURRENT' vs latest release '$LATEST'"
if [[ "$CURRENT" == "$LATEST" ]]; then
echo " OK — up to date."
else
echo " DRIFT — a newer dugite-native release exists: $CURRENT -> $LATEST"
echo " Bump (D-63 record; machine: T-25): update Makefile DUGITE_VERSION/COMMIT,"
echo " refresh native/dugite/, and assets/licenses.yaml if the git/dugite version changed."
fi
# Loud flag when the latest release notes mention a CVE / security fix — those
# jump the queue regardless of the quarterly cadence.
if printf '%s' "$json" | grep -qiE 'cve-[0-9]{4}|security (fix|advisory|release|update)|vulnerab'; then
echo " !! SECURITY: the latest release notes mention a CVE / security fix — schedule a bump NOW." >&2
fi
+54 -5
View File
@@ -1,9 +1,58 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# CI entry: release pipeline. Stub — wire goreleaser + flutter build # Release finalizer for the single-process Flutter app (T-393).
# artifacts later. #
# Run AFTER the `release vX.Y.Z` commit is in place (version bump + changelog
# move — see .claude/skills/git-commit/SKILL.md "Cutting a release"). It:
# 1. reads the version from pubspec.yaml,
# 2. asserts CHANGELOG.md has a dated section for it (not still Unreleased),
# 3. asserts the working tree is clean,
# 4. runs the full gate (make push-check),
# 5. creates the annotated vX.Y.Z tag if missing — closing the loop that
# previously left every release since v2.1.0 untagged.
#
# No goreleaser, no sidecar (both dissolved, D-56). Artifact builds are
# `make build-linux` / `make build-macos`. This never pushes — it prints the
# push command for you to run.
#
# Invoke via `make release`, not directly.
set -euo pipefail set -euo pipefail
cd "$(dirname "$0")/.." cd "$(git rev-parse --show-toplevel)"
echo "TODO: goreleaser release (sidecar) + flutter build (app) + publish" version="$(grep -E '^version:' pubspec.yaml | awk '{print $2}')"
exit 64 if [[ -z "$version" ]]; then
echo "release: could not read 'version:' from pubspec.yaml" >&2
exit 1
fi
tag="v$version"
# 1. The release commit must already have moved Unreleased → [version] — DATE.
if ! grep -qE "^## \[${version//./\\.}\] — [0-9]{4}-[0-9]{2}-[0-9]{2}" CHANGELOG.md; then
echo "release: CHANGELOG.md has no dated section for [$version]." >&2
echo " Cut the release commit first (move Unreleased → '## [$version] — YYYY-MM-DD')." >&2
exit 1
fi
# 2. Clean tree — the release commit is in, nothing dangling.
if [[ -n "$(git status --porcelain)" ]]; then
echo "release: working tree not clean — commit the release first." >&2
exit 1
fi
# 3. Full gate.
echo "release: running the full gate (make push-check)…"
make push-check
# 4. Tag (idempotent), annotated, on the current release commit.
if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then
echo "release: tag $tag already exists — leaving it."
else
git tag -a "$tag" -m "clide $tag"
echo "release: created tag $tag at $(git rev-parse --short HEAD)."
fi
echo
echo "release: $tag verified and tagged. Next:"
echo " git push origin main --follow-tags # publish the commit + tag"
echo " make build-linux # desktop bundle (Linux)"
echo " make build-macos # desktop bundle (macOS, on a Mac)"
-776
View File
@@ -1,776 +0,0 @@
# clide — External Consultant Review
**Date:** 2026-05-14
**Scope:** Full-repository assessment of clide at `main` (commit `9030e56`).
**Method:** Six independent specialist reviewers, each given read-only access and a
brief covering best practice, clean code, architecture, usability, stability,
expandability, style, consistency, and general quality. Reviewers did not see each
other's findings; cross-cutting themes below are genuine independent agreement.
**Panel:**
| Lens | Reviewer |
|---|---|
| Architecture | Software Architect |
| Tests & quality gates | Test / QA Analyst |
| UX & accessibility | UX & Accessibility Expert |
| Code quality & craft | Senior Dart/Flutter Engineer |
| Security & supply chain | Security Engineer |
| Docs, governance & DX | TPM / Developer-Experience Consultant |
---
## Overall verdict
clide is, for a solo-dev pre-v2.0 project, **unusually disciplined** — every reviewer
said so independently. The governance system is alive, the core subsystems are small
and well-typed, the FFI/PTY layer shows real systems-programming care, and the quality
gates are genuine rather than ornamental. The codebase is in good shape.
The weaknesses cluster into a handful of themes, and several are **load-bearing**: a
central guardrail (CLI-first IPC) has no runtime implementation, keyboard operability —
the core requirement of a power-user dev tool — is largely unbuilt, an untrusted
workspace can achieve code execution, and the two primary onboarding documents describe
an architecture that no longer exists.
None of these are fatal; all are fixable; most have quick-win first steps. But they
should be addressed before a public v2.0.
---
## Cross-cutting themes (independent agreement)
These were each flagged by **two or more** reviewers who did not coordinate:
1. **The IPC layer is mid-migration and contradicts itself.** The Architect found no
Unix-socket *server* anywhere in `lib/` — D-56's "app hosts an in-process IPC server"
and D-1's "CLI-first, not MCP" have no runtime path; three IPC clients
(`DaemonClient`, `InProcessClient`, `IsolateClient`) coexist with two duplicated
service-wiring sites. The Security reviewer independently noted `DaemonClient`'s
socket code is still live as an unvalidated attack surface. **Pick one IPC model,
implement or amend D-56, delete the other two.**
2. **The "no pre-existing excuse / clean board" guardrail is being violated right now.**
`flutter analyze` reports 9 `unnecessary_import` issues in `test/`; `ci/test.sh` runs
analyze with `--no-fatal-infos`, which silently tolerates them. Flagged by the
Architect, Code Quality, and Test reviewers. The repo's own rules say fix-first.
3. **`lib/src/terminal/` is in an undeclared middle state.** ~7k LOC forked from
xterm.dart, carrying commented-out `print`s, dangling TODOs, a 1137-line `parser.dart`,
and the only `invalid_use_of_protected_member` suppression in the repo. MEMORY says
"code under `lib/` is owned, not vendored" — so it must either be formally vendored
(frozen, documented, decision-recorded) or cleaned to the project bar. Flagged by
Code Quality; the Architect's "consistency" deduction points at the same seam.
4. **Documentation describes a dissolved architecture.** `README.md` and
`docs/initial-plan.md` still describe a Go sidecar, `ptyc/` C helper, `app/`
subdirectory, and a separate `clide --daemon` process — all removed by D-5, D-56, and
the FFI pivot. A new contributor's first read builds a wrong mental model.
---
## Consolidated scorecard
Scores are each reviewer's, 15, on their own dimensions.
| Domain | Dimension | Score |
|---|---|---|
| **Architecture** | Layering & dependency direction | 4 |
| | Separation of concerns | 4 |
| | Expandability | 5 |
| | Consistency | 4 |
| | Guardrail adherence | 3 |
| **Tests** | Coverage quality | 4 |
| | Test reliability / flakiness | 3 |
| | Gate trustworthiness | 3 |
| | Test maintainability | 5 |
| | Regression-catching power | 4 |
| **UX / a11y** | Interaction model | 2 |
| | Accessibility | 3 |
| | Visual consistency | 4 |
| | Discoverability | 2 |
| | State coverage (loading/error/empty) | 3 |
| **Code quality** | Idiomatic Dart | 4 |
| | Error handling | 4 |
| | Naming & readability | 4 |
| | Consistency across subsystems | 3 |
| | Resource / lifecycle safety | 4 |
| **Security** | Subprocess safety | 2 |
| | IPC input validation | 3 |
| | Path / filesystem safety | 3 |
| | Dependency / supply-chain hygiene | 3 |
| | Secrets & sandboxing | 3 |
| **Docs / governance** | Governance discipline | 4 |
| | Documentation accuracy | 2 |
| | Changelog hygiene | 3 |
| | Contributor onboarding | 2 |
| | Convention adherence | 4 |
**Highest marks:** expandability (5), test maintainability (5). The extension contract
and test-helper design are genuine standouts.
**Lowest marks:** interaction model (2), discoverability (2), subprocess safety (2),
documentation accuracy (2), contributor onboarding (2).
---
## Prioritized action list
Synthesized across all six reviews. Severity is the highest any reviewer assigned.
### Critical — address before public v2.0
1. **Fix untrusted-workspace code execution.** `toolchain_paths.dart:79` resolves
`native/dugite/bin/git` relative to the *workspace root*; a malicious repo can plant
an executable there that clide runs on the first auto-fired `git.status`. Resolve
`native/dugite` against `Platform.resolvedExecutable`'s directory, never the
workspace. *(Security)*
2. **Resolve the IPC story.** Either implement the in-process Unix-socket server per
D-56 so the `clide` CLI / C client actually works, or amend D-56 to make in-process
direct dispatch the design and delete `DaemonClient`'s socket code, `IsolateClient`,
`Backend`, and `backend_entry.dart`. Today the code claims three models and runs one,
and a load-bearing guardrail (D-1/D-6) is unmet. *(Architecture, Security)*
3. **Make the tool keyboard-operable.** `ClideTappable` (base of nearly every
interactive widget) is mouse-only — no `Focus`, no Enter/Space. The command palette
has no arrow-key navigation and no Escape. For a keyboard-first dev tool this is a
functional gap, not a polish item. *(UX)*
4. **Fix the onboarding docs.** Rewrite `README.md`'s `ptyc/` / `make ptyc-build`
sections, fix its dead `decisions/` link, and banner `docs/initial-plan.md` as
historical (or split out a current `docs/architecture.md`). *(Docs)*
### Major — should land soon
5. Add symlink re-resolution + containment re-check in `files.read` / `files.ls` — a
repo symlink `config -> /etc/shadow` currently passes path-safety. *(Security)*
6. Add `test-integration` (and ideally `smoke-bundle`) to `make push-check` — the gate
that catches "app won't boot" is currently omitted from the pre-push gate. *(Tests)*
7. Schema-validate the IPC argument surface; reject `-`-prefixed `branch`/`remote`/`path`
values; add size/count bounds. *(Security)*
8. Establish a real focus-traversal model (`FocusTraversalGroup` per slot, a documented
"focus next panel" keybinding) and integrate `FocusTracker` with Flutter's focus
system instead of paralleling it. *(UX)*
9. Fix the `SchedulerService._startTicker` isolate-spawn race — a `_stopTicker()` before
the spawn future resolves leaks a forever-ticking isolate. Mirror `NativePty`'s
`_readerReady` pattern. *(Code quality)*
10. Decide the status of `lib/src/terminal/` — formally vendor (and decision-record) it,
or do the cleanup sweep. *(Code quality)*
11. Replace fixed wall-clock `Future.delayed` sleeps in `watcher_test.dart` /
`session_test.dart` with event-driven waits; make swallowed `onTimeout` callbacks
`fail()` loudly. *(Tests)*
12. Write a human-facing `CONTRIBUTING.md`; cut an interim release to drain the ~80-commit
`[Unreleased]` backlog; merge duplicate changelog subsection headings. *(Docs)*
13. Single global `KeyboardListener` → scoped `Shortcuts`/`Actions`; move
`KeybindingResolver` off layout-dependent `keyLabel`. *(UX)*
### Quick wins — hours each
- Clear the 9 `unnecessary_import` analyzer issues; drop `--no-fatal-infos` from
`ci/test.sh`. *(Architecture, Tests, Code quality)*
- Run the `forkpty` PTY tests with `--coverage` so `native_pty.dart` — the riskiest file
— is honestly measured. *(Tests)*
- Add a `Focus` + Enter/Space wrapper and a focus-ring inside `ClideTappable`; this fixes
the keyboard gap for every button and list item at once. *(UX)*
- Add arrow-key + Escape + selected-index to `ClidePalette` (copy the existing
`_ProjectSwitcherDropdown` `onKeyEvent` pattern). *(UX)*
- Amend D-66 to reflect the coverage floor's real location (`pubspec.yaml`), mechanism,
and value (90%) — it currently disagrees with the changelog and the code. *(Docs)*
- Reconcile `licenses.yaml` with `pubspec.yaml` (`test` version drift, phantom `lints`
entry); add a `native/SHA256SUMS` manifest. *(Security, Docs)*
- Replace silent `catch (_)` in `tree_sitter_ffi.dart` with a logged last-error.
*(Code quality)*
- Fix the `clide.dart` barrel leak in `file_tree_view.dart:8`; narrow the barrel (drop
the `dispatcher.dart` export); move `test_app.dart` out of the production `main.dart`
import graph. *(Architecture)*
- Expand the contrast gate's `canonicalPairs` to cover `globalTextMuted`, the `status*`
colors, and `panelActiveBorder`. *(UX)*
- Triage stale governance Q-records (Q-1/2/3/25 overtaken by shipped Tier-1 work).
*(Docs)*
---
# Full reviews
## 1. Architecture — Software Architect
### Executive summary
clide is an unusually disciplined solo-dev codebase. The governance system (67
D-records, tracked Q/R) is real and largely honored in code, the kernel/extension split
is coherent, and the feature-first layout with barrel files is consistently applied. The
single biggest strength is the **extension contract**: every built-in — including layout
itself — passes the same `ClideExtension` + `ContributionPoint` contract, which is the
best possible proof the contract is usable. The single biggest risk is **architectural
drift in the IPC layer**: D-56 mandates the Flutter app host an in-process IPC server
reachable by a thin C client over a unix socket, but no socket server exists anywhere in
`lib/` — the "CLI-first, not MCP" guardrail (D-1) has no runtime path today. Compounding
this, three parallel IPC client implementations (`DaemonClient` socket,
`InProcessClient`, `IsolateClient` + `Backend`) coexist with two competing
service-wiring sites (`main.dart` and `backend_entry.dart`), suggesting an unfinished
migration.
### Strengths
- **Extension contract is clean and scales**`lib/extension/src/extension.dart` +
`contribution.dart`: sealed `ContributionPoint` hierarchy, `ClideExtensionContext`
lists services explicitly (deliberately avoiding a `KernelServices` import cycle —
`extension.dart:50-52`). `ExtensionManager` does dependency topo-sort,
dependency-gated activation, and contribution apply/remove symmetrically
(`extensions_manager.dart:164-202`). Adding a pane = new extension file + one
`register()` line in `main.dart`.
- **Kernel admission rule is enforced, not aspirational** — D-12's "mandatory shared
singleton" test visibly shaped `KernelServices` (`facade.dart:38-93`); ~25 services,
each defensibly cross-cutting. The two-tier disable model (D-14) is honored:
`default_layout` is itself an extension.
- **Feature-first layout with barrel discipline** (D-8) is consistent — every
`builtin/<name>/` and `kernel/` has a barrel; builtins import
`package:clide/kernel/kernel.dart`, not deep paths. Only one leak found.
- **Governance-to-code traceability is genuine**`WidgetsApp` root (D-7) at
`app.dart:38`, `ChangeNotifier`/`ListenableBuilder` state (D-10) everywhere, git
hardcoded in toolchain/project loader (D-13), terminal correctly tagged
`inlined-source` in `licenses.yaml` with modifications documented.
- **Git subsystem cohesion**`lib/src/git/` cleanly split into `client` / `status` /
`diff` / `operations` (~250 lines each), each a single responsibility.
### Findings
- **[Critical] No IPC socket server exists** — D-56 specifies the app hosts an
in-process IPC server with a C client connecting over a unix socket. `grep` for
`ServerSocket`/unix-domain `bind` in `lib/` returns nothing. `DaemonClient._connect`
(`client.dart:72-94`) *connects* to a socket, but nothing *serves* one. Today the only
working path is `InProcessClient` (`in_process.dart`), which calls the dispatcher
directly in-process. **Claude cannot drive clide via `clide ...` — the CLI-first
guardrail (D-1, D-6) has no implementation.** This is the load-bearing contract of the
whole project and it is absent.
- **[Major] Three IPC clients + two wiring sites = unfinished migration** —
`DaemonClient` (socket), `InProcessClient`, and `IsolateClient`+`Backend`/
`backend_entry.dart` all coexist. `main.dart:76-113` wires subsystems via
`buildDispatcher`; `backend_entry.dart:40-110` wires the *same* five subsystems again
inside an isolate. `Backend.spawn` is referenced only by `facade.dart` but `main.dart`
uses `autoStartDaemonClient: false` + `daemonClientFactory` (the in-process path).
Dead-or-dormant isolate infrastructure with duplicated registration logic — pick one
and delete the others.
- **[Major] `main.dart` (production entry) imports `test_app.dart`** — `main.dart:2` and
`:51-55`. The production binary carries the test harness and branches on
`CLIDE_TESTMODE`. Test scaffolding should not be reachable from the shipping entry
point; gate it behind a separate entrypoint or `kDebugMode`.
- **[Minor] `flutter analyze` reports 9 issues** — all `unnecessary_import` in `test/`,
but CLAUDE.md's "no pre-existing excuse" / "clean board" guardrail makes this a
fix-first item.
- **[Minor] Barrel leak** — `lib/builtin/files/src/file_tree_view.dart:8` imports
`package:clide/src/files/listing.dart` directly instead of via
`package:clide/clide.dart` (which already re-exports `FileEntry`).
- **[Minor] `clide.dart` barrel exports the daemon dispatcher** — `clide.dart:15`
exports `src/daemon/dispatcher.dart`. The barrel is described as "shared types"; the
dispatcher is server-side machinery.
- **[Minor] `ExtensionManager.activate` swallows exceptions** (`extensions_manager.dart:
141-143`) — a failed `activate()` logs and continues, leaving the extension
un-activated but `_known`, with no surfaced "degraded" state for the UI.
### Recommendations
**Quick wins:** clear the 9 analyzer issues; fix the `file_tree_view.dart` barrel leak
(consider a CI grep gate for `package:clide/src/` imports outside their feature); move
`test_app.dart` out of the production import graph; drop the `dispatcher.dart` export
from `clide.dart`.
**Larger efforts:** resolve the IPC story (implement the socket server per D-56, or
amend D-56 and delete `DaemonClient`/`IsolateClient`/`Backend`/`backend_entry.dart`);
collapse subsystem wiring into one `registerAllSubsystems(...)` function; give
`ExtensionManager` a surfaced failure state so the UI can show degraded built-ins.
### Scorecard
| Dimension | Score | Justification |
|---|---|---|
| Layering & dependency direction | 4/5 | Kernel→extension direction clean, context-vs-aggregate split avoids cycles; docked for the `src/``kernel/src/` barrel leak and the dispatcher export. |
| Separation of concerns | 4/5 | Feature-first layout, single-responsibility subsystems; duplicated subsystem registration is the blemish. |
| Expandability | 5/5 | New pane = one extension file + one `register()` line; sealed contribution hierarchy; layout itself is data and extension-shaped. |
| Consistency | 4/5 | Barrels, naming, D-record back-references uniform; three coexisting IPC clients and 9 analyzer issues break the bar. |
| Guardrail adherence | 3/5 | `WidgetsApp`, single-process, no-Material, governance, zero-deps all honored — but D-1/D-6/D-56 (CLI-first via socket server) have no runtime implementation. |
---
## 2. Tests & quality gates — Test / QA Analyst
### Executive summary
The clide test suite is, for a solo-dev pre-2.0 project, in genuinely good shape. ~104
test files against 276 lib files, ~92.75% line coverage, and — critically — the coverage
was *not* bought with assertion-free filler. Even the alarmingly-named files
(`coverage_trivials_test.dart`, `zero_coverage_widgets_test.dart`,
`services_stubs_test.dart`, `mop_up_test.dart`) contain real behavioral assertions. The
biggest strength is a sensibly layered pyramid with a real boot-path integration gate
and a startup smoke test that catches the "tests pass but app won't launch" class. The
biggest risk is **flakiness from wall-clock-dependent tests** — fixed `Future.delayed`
sleeps in file-watcher and PTY tests will eventually produce intermittent CI failures,
and the PTY tests are run via `dart test` so they are **excluded from the coverage
measurement entirely**.
### Strengths
- **Test pyramid is sound.** Pure-Dart unit, widget tests with a shared harness, golden
tests (Alchemist), an a11y contract layer, and 3 real-boot `integration_test/` files —
correctly separated by runner (`ci/test.sh` vs `ci/test_core.sh` vs
`ci/test_integration.sh`).
- **Helpers are well-designed.** `test/helpers/kernel_fixture.dart` boots a real
`KernelServices` with in-memory themes/i18n and `autoStartDaemonClient: false` — no
real socket, temp-dir scoped, proper `dispose()`. `FakeDaemonClient` is a clean stub.
- **Error-branch discipline.** `pql_commands_errors_test.dart` /
`git_commands_errors_test.dart` deliberately point the toolchain at a non-existent
binary to drive catch-branches the happy path can't reach — table-driven, with
`reason:` tags.
- **OS-dialog avoidance is handled correctly.** `welcome/dialog_test.dart` mocks the
`clide/window` MethodChannel to throw `MissingPluginException`, exercising the fallback
path *without* spawning a native file picker.
- **Startup gate.** `ci/smoke_bundle.sh` builds the real release bundle and runs it
under xvfb for 5s, correctly interpreting `timeout` exit codes (124/143 = healthy).
- **Coverage gate is honest.** `ci/coverage_gate.sh` is a self-contained awk parser (no
`lcov` dependency), ratchets only upward, and `exit 2` distinguishes "missing data"
from "below floor."
### Findings
- **[Major] PTY tests are excluded from coverage.** `ci/test.sh:13` runs
`flutter test --coverage --exclude-tags forkpty`; the `forkpty` tests run separately
via `dart test` with no `--coverage`. So `lib/src/pty/native_pty.dart` — the
highest-risk native code in the repo — is barely in the measured denominator. The
92.75% number overstates coverage of the riskiest file.
- **[Major] Wall-clock sleeps will flake.** `test/files/watcher_test.dart:67-82` uses
fixed `Future.delayed`; `test/pty/session_test.dart:71` polls 50×100ms and `:65` uses a
bare `500ms` settle. `session_test.dart`'s `timeout(5s, onTimeout: () {})` (`:48`)
*swallows* the timeout — a never-producing PTY proceeds to a confusing assertion
failure rather than a clear timeout.
- **[Major] `make push-check` does not run integration tests.** `push-check:
decisions-validate test-core test test-a11y coverage-gate` — `test-integration` and
`smoke-bundle` are omitted. A boot-order regression sails through.
- **[Minor] `flutter analyze --no-fatal-infos` in `ci/test.sh:9`** contradicts the
stated "fail-on-warning, clean board" discipline.
- **[Minor] Integration tests run one-file-at-a-time** to dodge a batch "Unable to start
the app" error — each invocation re-boots the engine (slow), and the workaround masks
whether the batch failure is environmental or a real teardown leak.
- **[Minor] Golden CI config disabled.** Only platform goldens run; a Linux-only CI
never validates the macOS goldens, and stale `test/goldens/failures/*.png` artifacts
are committed to the repo.
- **[Minor] `test_core.sh` timeout kill is best-effort** — the `pkill -9 -f` pattern
match is redundant noise next to the real `setsid` + `timeout --kill-after` safety net.
- **[Minor] `git/client_test.dart` depends on the ambient `git` binary**, not the
vendored dugite — the suite passes/fails on the host git version.
### Recommendations
**Quick wins:** add `test-integration` (and `smoke-bundle`) to `push-check` — the single
highest-value change; run the `forkpty` tests with `--coverage`; drop `--no-fatal-infos`;
gitignore `test/goldens/failures/`; make `onTimeout` callbacks `fail()`.
**Larger efforts:** replace fixed sleeps with event-driven waits
(`expectLater(stream, emits(...))`); add a macOS golden CI matrix entry or document
goldens as advisory; consider a coverage-exclusion allowlist for genuinely-unreachable
defensive branches rather than chasing the last lines with filler tests.
### Scorecard
| Dimension | Score | Justification |
|---|---|---|
| Coverage quality | 4/5 | Tests are meaningful even in "mop-up" files; docked because PTY/FFI is outside the measured number. |
| Test reliability / flakiness | 3/5 | Fixed wall-clock sleeps and a swallowed timeout are latent intermittent failures. |
| Gate trustworthiness | 3/5 | Coverage gate and smoke bundle are well-built, but `push-check` omits integration tests. |
| Test maintainability | 5/5 | Shared fixtures, consistent structure, table-driven error suites, clear doc comments. |
| Regression-catching power | 4/5 | Real boot-path integration + smoke + a11y + goldens; weakened by single-OS goldens and PTY coverage gaps. |
---
## 3. UX & accessibility — UX & Accessibility Expert
### Executive summary
clide has an unusually disciplined *foundation* for a solo pre-v2.0 project: a coherent
semantic design-token system, a WCAG-AA contrast gate wired into CI, and i18n/semantic
contract tests. That foundation is the biggest strength. The biggest risk is that
**keyboard operability is largely unimplemented below the foundation** — the project's
own core interaction primitive (`ClideTappable`) is mouse-only, the command palette has
no arrow-key navigation or Escape, and there is no focus-traversal wiring across panels.
For a keyboard-first power-user dev tool, this is a critical gap that the a11y test suite
does not catch because the tests assert *structural* presence (Semantics nodes exist)
rather than *operability* (can you actually drive it from the keyboard).
### Strengths
- **Semantic token system is real and enforced.** `lib/kernel/src/theme/tokens.dart`
defines ~65 named surface tokens; widgets consume `ClideTheme.of(context).surface`
rather than raw colors. The resolver provides defaults so partial themes still produce
a complete `SurfaceTokens`.
- **Contrast gate is genuine WCAG math, run per-theme.** `lib/kernel/src/theme/
contrast.dart` implements real relative-luminance ratio with alpha pre-compositing
against neutral grey (`contrast.dart:31-37`) — semi-transparent tokens can't spuriously
pass.
- **Semantics are present on composed widgets.** `ClideButton` wraps
`Semantics(button: true, enabled:, label:, hint:, onTap:)`; panels set
`container: true, explicitChildNodes: true` with landmark labels.
- **State coverage exists in data panels.** `git_panel_view.dart:86-104` handles error,
loading, and empty ("working tree clean") states distinctly; `file_tree_view.dart`
handles error + loading.
- **Manual a11y discipline is documented.** `docs/testing/a11y-manual.md` prescribes a
per-tier Orca/VoiceOver pass and is honest about why prose quality can't be automated.
- **Disabled state is handled at the cursor level.** `clide_button.dart:41` switches to
`SystemMouseCursors.forbidden` and drops the semantic `onTap` when `onPressed == null`.
### Findings
- **[Critical] `ClideTappable` is mouse-only — no `Focus`, no keyboard activation.**
`lib/widgets/src/clide_tappable.dart:37-54` is `MouseRegion` + `GestureDetector` only.
It is the base for `ClideButton`, `_WinBtn`, `_RecentProjectRow`, `_ActionRow`, and
most builtin list items. None can receive Tab focus or be activated with Enter/Space.
The keyboard-traversal test only passes because it manually wraps the button in an
external `Focus` node — it tests that the widget doesn't *block* focus, not that it
*accepts* it.
- **[Critical] Command palette is not keyboard-navigable.** `clide_palette.dart`
`onSubmitted` only ever invokes `filtered.first` (`:77-80`); no up/down handling, no
selected index, no selection highlight, no Escape handler.
- **[Major] No focus-traversal wiring between panels.** `FocusTracker`
(`lib/kernel/src/focus.dart`) tracks an active *contribution id* for the `clide active`
CLI, but is not Flutter `FocusScope`/`FocusTraversalGroup` integration. Nothing
establishes Tab order across sidebar → workspace → context.
- **[Major] Drag-resize handles have no keyboard equivalent — parity gap.**
`drag_resize.dart` and `app.dart:870-912` are pure `Listener` pointer handlers, with no
Semantics node at all. Per "User/Claude parity", panel sizing should have a CLI
affordance; none is evident.
- **[Major] Single global `KeyboardListener` is a fragile keybinding architecture.**
`app.dart:90-148` routes all shortcuts through one root `KeyboardListener` — no
per-context scoping, will conflict with text-input fields.
`KeybindingResolver.fromKeyEvent` keys off layout-dependent `logicalKey.keyLabel`.
- **[Major] Text scale is the *only* in-app a11y accommodation, and it's hidden.**
`app.dart:122-138` implements Ctrl +/-/0 text scaling but it's undiscoverable. No
high-contrast toggle, no reduced-motion handling, no focus-ring rendering anywhere.
- **[Minor] Contrast gate covers only 11 token pairs** — omits `globalTextMuted` (muted
text is everywhere), the `status*` foregrounds, syntax tokens on `panelBackground`, and
`panelActiveBorder`.
- **[Minor] ~43 hardcoded-color sites bypass the token system** — some defensible (ANSI
palette), but the modal/palette shadow and window-control colors won't adapt to the
`paper` light theme.
- **[Minor] Hover state is inconsistent and not paired with focus** — every interactive
widget reimplements its own `_hover` bool; none render a focus indicator.
- **[Minor] `_LeftHatContent` is dead code** — `app.dart:281-292` always returns
`SizedBox.shrink()`.
### Recommendations
**Quick wins:** add a `Focus` + `Actions`/`Shortcuts` (Enter/Space → onTap) wrapper and
a focus-ring inside `ClideTappable` — fixes the [Critical] for every button/list-item at
once; add arrow-key + Escape + selected-index to `ClidePalette` (copy the existing
`_ProjectSwitcherDropdown` `onKeyEvent` pattern at `app.dart:446-452`); expand
`canonicalPairs`; surface text-zoom and theme switching in the palette; tokenize the
modal shadow and window-control colors.
**Larger efforts:** establish a real focus-traversal model and integrate `FocusTracker`
with Flutter's focus system; replace the root `KeyboardListener` with scoped
`Shortcuts`/`Actions` and move off `keyLabel`; add keyboard operability + Semantics to
drag-resize handles plus a `clide panel resize` CLI; add an a11y test tier that asserts
*operability*, not just Semantics presence.
### Scorecard
| Dimension | Score | Justification |
|---|---|---|
| Interaction model | 2/5 | Coherent slot/panel structure and good drag-resize *with a mouse*, but keyboard operability is largely unbuilt. |
| Accessibility | 3/5 | Genuine contrast gate, Semantics on composed widgets, i18n contract tests — but keyboard operability and focus order are not implemented. |
| Visual consistency | 4/5 | Strong semantic token system consumed consistently; a few hardcoded-color sites are real theme-adaptation bugs. |
| Discoverability | 2/5 | Command palette isn't keyboard-navigable; accommodations are undiscoverable; no in-app keybinding reference. |
| State coverage | 3/5 | Data panels and dialogs handle loading/error/empty; but no focus states anywhere and no reduced-motion handling. |
---
## 4. Code quality & craft — Senior Dart/Flutter Engineer
### Executive summary
clide is, for a solo pre-v2.0 project, in genuinely good shape. The core subsystems (IPC
envelope, daemon dispatch, git client, PTY) are small, single-responsibility,
well-typed, and consistent. `flutter analyze` is clean for `lib/` — the 9 reported issues
are all in `test/`, none are suppressions. The biggest strength is the FFI/PTY layer:
`lib/src/pty/native_pty.dart` shows real systems-programming discipline (pre-fork
allocation, errno captured before `free`, isolate-teardown ordering documented and
correct). The biggest risk is concentrated in two places: a genuine isolate-leak race in
`SchedulerService`, and the large vendored-but-owned `lib/src/terminal/` xterm.dart fork
(~7k LOC) which carries a different style, commented-out `print`s, and dangling TODOs
that the project's own "lib is owned, not vendored" rule says must be held to the same
bar.
### Strengths
- **PTY/FFI layer is excellent.** `native_pty.dart:129-145` force-resolves FFI
trampolines and pre-allocates *all* native memory before `forkpty()`.
`native_pty.dart:171-177` captures `errno` before `_freeAll` because `free()` can
clobber it. `close()` (367-397) documents and implements the kill→EOF→close ordering
to avoid fd-reuse races. The child branch touches no Dart heap.
- **IPC envelope is clean and idiomatic**`lib/src/ipc/envelope.dart` uses a `sealed`
class hierarchy, named constructors, a private unifying constructor, and conditional
map keys. Decode is total over the type discriminant.
- **Typed, meaningful errors.** `PtyException` carries `op` + optional `errno`;
`GitException` carries `stderr`; `errnoToIpcError` maps POSIX errno to actionable IPC
error kinds. Errors are values, not strings.
- **Resource lifecycle is taken seriously across most subsystems.** `FileWatcher.stop()`
cancels the subscription *and* closes the controller; `withBuffer`/`setWinsize` in
`libc.dart` use `try/finally` around every native allocation. 45 files define
`dispose`/`close`.
- **The `DaemonEventSink` interface** keeps the dependency graph pointing the right way
(server→subsystems) and is documented as such.
- **The one `ignore_for_file` (`libc.dart:11-27`) is exemplary** — textbook FFI case,
multi-paragraph justification exactly as CLAUDE.md requires.
### Findings
- **[Major] Isolate-leak race in `SchedulerService._startTicker`** — `scheduler.dart:71`:
`Isolate.spawn(...).then((iso) => _isolate = iso)`. If `_stopTicker()` runs before the
spawn future completes, `_isolate` is still null, nothing is killed, and the
just-spawned isolate (with its `Timer.periodic`) leaks. `native_pty.dart` solved
exactly this with `_readerReady`.
- **[Major] `lib/src/terminal/` held below the project's own bar.** Carries
commented-out `print()` debugging (`custom_text_edit.dart:244-275`), dangling TODOs
(`parser.dart:110-113`, `keytab.dart:91`), a 1137-line `parser.dart`, and the only
`// ignore: invalid_use_of_protected_member` in the repo (`terminal_view.dart:363`).
Either it's genuinely vendored (belongs in `native/` or documented as frozen) or it's
owned (needs the cleanup pass).
- **[Minor] Empty `catch (_) {}` swallows in `tree_sitter_ffi.dart:197,206`** —
`DynamicLibrary.open` failures silently discarded; caller gets a bare `null` with no
diagnostic about *why*. Syntax highlighting silently not working is a support
headache.
- **[Minor] Empty `catch (_) {}` in `test_app.dart:271,311`** — `:271` swallows a
theme-load failure the harness exists to detect.
- **[Minor] Dead alias in `libc.dart:201-202`** — `typedef Cmsghdr = CmsghdrLinux;`
flagged "backward compatibility"; CLAUDE.md forbids backwards-compat hacks in a solo
repo.
- **[Minor] `// ignore: unused_field` in `editor_controller.dart:25`** "kept for future
subscription changes" — speculative retention; the no-suppression rule wants it fixed,
not silenced.
- **[Minor] Magic numbers in hot FFI paths.** `native_pty.dart` inlines `0x0001
// POLLIN`, `28 /* SIGWINCH */`, `4 /* EINTR */`, `9 /* EBADF */` — but `libc.dart`
already has a constants section and `errno_mapping.dart` has `PosixErrno.ebadf`.
- **[Minor] `git_commands.dart` has ~16 near-identical handler bodies** — a
`_guarded(req, () async {...})` helper would remove ~60 lines of structural
duplication. Borderline.
### Recommendations
**Quick wins:** fix the `SchedulerService` spawn race (track the spawn future like
`NativePty._readerReady`); replace the three silent `catch (_)` in `tree_sitter_ffi.dart`
with a logged last-error; delete the `Cmsghdr` alias and the `unused_field` suppression;
have the PTY layer consume `libc.dart` constants / `PosixErrno` instead of inline hex.
**Larger efforts:** decide the status of `lib/src/terminal/` — formally vendor it
(freeze, document, decision-record) or do the cleanup sweep; optionally a `_guarded`
helper for `git_commands.dart` (check whether `files_commands` / `editor_commands` share
the shape).
### Scorecard
| Dimension | Score | Justification |
|---|---|---|
| Idiomatic Dart | 4/5 | Sealed classes, named ctors, records, `const`, immutability used well; the vendored terminal tree pulls the average down. |
| Error handling | 4/5 | Typed errors with context everywhere in core; a few silent `catch (_)` in the FFI loader and test harness cost the 5th point. |
| Naming & readability | 4/5 | Clear, intention-revealing names; comments earn their place; inline magic numbers are the main blemish. |
| Consistency across subsystems | 3/5 | IPC/git/files/pty are uniform; `lib/src/terminal/` is a different codebase in style; PTY duplicates constants `libc.dart` owns. |
| Resource/lifecycle safety | 4/5 | `try/finally` around native allocs, controllers closed, subscriptions cancelled; the one real defect is the `SchedulerService` race. |
---
## 5. Security & supply chain — Security Engineer
### Executive summary
clide's security posture is **above average for a solo pre-v2.0 project**. All
subprocess calls use `Process.run`/`Process.start` with argument *lists* (no shell
interpolation), the IPC transport is a per-user Unix socket (not a TCP port), and there
is an explicit `path_safety` module with a containment check. The single biggest strength
is the disciplined no-shell subprocess layer. The single biggest risk is
**untrusted-workspace code execution via toolchain resolution**
(`toolchain_paths.dart:79`): a malicious repo can ship a `native/dugite/bin/git`
executable that clide will resolve and run. Secondary real issues: path-safety does not
defend against symlink escape, and IPC command args are largely unvalidated/un-bounded.
Supply-chain hygiene is mostly good but `licenses.yaml` has drifted from `pubspec.yaml`
and native binaries are committed without SHA pinning.
### Strengths
- **No-shell subprocess execution.** `GitClient._run` (`client.dart:210`),
`PqlClient._run` (`client.dart:165`), and the PTY layer all pass `List<String>` args
directly. Classic command injection is structurally prevented.
- **Toolchain uses resolved absolute paths** — git/pql/tmux resolved once to absolute
paths and reused.
- **Path containment check exists and is used.** `resolveUnderRoot`
(`path_safety.dart:21`) collapses `..`/`.` without touching the filesystem and enforces
a prefix check with a separator guard. `files.read`/`files.ls` both call it.
- **IPC is a per-user Unix socket, not a network listener.** No `ServerSocket` over TCP
anywhere; the default runtime path is in-process, eliminating the socket attack
surface in the shipped app.
- **PTY FFI memory discipline** — all native memory allocated before `forkpty()`, `errno`
captured before `free()`, freed on every path.
- **`pubspec.lock` is committed**, deps use exact pins (no carets), `licenses.yaml`
exists with per-dep purpose/license.
### Findings
- **[Critical] Malicious workspace can plant a git binary that clide executes.**
`toolchain_paths.dart:79-84` builds `'$workspaceRoot/native/dugite/bin'` and runs
`_firstExisting(['$dugite/git'])`; if that file exists it becomes the git binary for
all `GitClient` calls, **before** falling back to PATH. An attacker commits an
executable at `native/dugite/bin/git`; clide runs it on the first `git.status` (which
fires automatically on workspace open). Arbitrary code execution from merely opening a
repo. The `native/dugite` convention should resolve relative to the *clide install
dir*, never the workspace root.
- **[Major] Path-safety does not defend against symlink escape.** `path_safety.dart:
35-51` explicitly does not resolve symlinks, and the filesystem layer
(`files_commands.dart:81-85`) never does either. A repo symlink `config -> /etc/shadow`
passes the containment check (the *link path* is under root) and clide reads the
target. Fix: after `resolveUnderRoot`, `resolveSymbolicLinksSync()` and re-verify
containment.
- **[Major] IPC command arguments are unvalidated and unbounded.**
`DaemonDispatcher.dispatch` (`dispatcher.dart:26`) and `IpcRequest.fromJson`
(`envelope.dart:49`) do no schema validation. No size limit on `files.read`, no count
cap on `git.log`, no check that `git.checkout`'s `branch` (`git_commands.dart:240`)
isn't a `-`-prefixed flag. `git diff`/`stage` use `--` separators (good), but
`checkout(branch)` and `push(remote, branch)` do not — argument injection
(`git checkout --upload-pack=...`) is possible.
- **[Minor] macOS entitlements disable library validation.**
`macos/Runner/Release.entitlements` sets `disable-library-validation` = true with no
App Sandbox entitlement. Arguably needed for the `dlopen` of `libtree-sitter.so`, but
combined with no sandbox a compromised process has full user-level filesystem access.
- **[Minor] `licenses.yaml` has drifted from `pubspec.yaml`.** Lists dev-dep `test` at
`1.25.8` but `pubspec.yaml:60` pins `1.30.0`; lists a `lints 5.0.0` not in
`pubspec.yaml` at all. The two-step-commit guardrail is being violated.
- **[Minor] Native binaries committed without SHA pinning.** `native/linux-x64/` has
`libtree-sitter.so` (24 MB) and `ptyc` (22 KB) committed with no `SHA256SUMS` manifest.
CLAUDE.md says native deps are "pinned by SHA"; that pinning is not evidenced.
- **[Informational] No secrets service** — clide stores no tokens; git auth is delegated
to the system credential helper. The right call; noted so the absence reads as
deliberate.
- **[Informational] Lua runtime is a stub** — `lib/lua/src/host.dart` is Tier-0. Design
intent (strip `io`/`os.execute`/`package.loadlib`/`debug`) is sound; re-assess at Tier
6 — sandbox-escape via FFI re-entry will be the concern.
### Recommendations
**Quick wins:** fix toolchain resolution to resolve `native/dugite` against
`Platform.resolvedExecutable`'s directory, never `workspaceRoot` (closes the Critical);
add symlink re-check in `files.read`/`files.ls`; reconcile `licenses.yaml` with
`pubspec.yaml`; reject `-`-prefixed values for `branch`/`remote`/`path` args (or use
`--` everywhere, including `checkout`).
**Larger efforts:** schema-validate the IPC surface with typed arg schemas + size/count
bounds; add a committed `native/SHA256SUMS` verified by `make` and CI; revisit macOS
sandboxing (App Sandbox with explicit exceptions); security-review the Lua FFI boundary
and capability table before Tier 6 ships.
### Scorecard
| Area | Rating | Justification |
|---|---|---|
| Subprocess safety | 2/5 | No-shell arg lists are excellent, but the workspace-relative dugite path is a real RCE; argument-injection on `checkout`/`push` unmitigated. |
| IPC input validation | 3/5 | Per-user Unix socket + in-process default sharply limits exposure, but zero arg-schema validation and no size/count bounds. |
| Path/filesystem safety | 3/5 | Real containment check that's actually wired in, undermined by the unhandled symlink-escape gap. |
| Dependency/supply-chain hygiene | 3/5 | Exact pins, committed lockfile, documented deps — but `licenses.yaml` drift and missing SHA manifest for committed native binaries. |
| Secrets & sandboxing | 3/5 | Correctly delegates secrets; Lua sandbox is only a stub; macOS runs with library validation off and no App Sandbox. |
---
## 6. Docs, governance & DX — TPM / Developer-Experience Consultant
### Executive summary
clide runs an unusually disciplined governance system for a solo-dev pre-v2.0 project:
67 decision records across six domains, with a parser-validated DQR structure, anchored
cross-references, and a `make decisions-validate` gate wired into pre-push. The biggest
strength is that the DQR system is genuinely *alive* — questions get resolved with dated
amendments, superseded decisions are marked, and decisions cite the commits that
implement them. The biggest risk is **documentation drift in the narrative docs**:
`README.md` and `docs/initial-plan.md` describe an architecture (Go sidecar, `ptyc/` C
helper, `app/` subdirectory, separate daemon) that three major decisions (D-5, D-56, the
FFI pivot) have since dissolved. A new contributor reading the README first would build
a wrong mental model.
### Strengths
- **DQR system is maintained, not ornamental.** Resolved questions carry dated
resolution lines pointing to the deciding D-record (`questions/architecture.md:39`
Q-6→D-57). D-40 carries a `[SUPERSEDED]` tag and an amendment line.
- **Decisions are linked to code and commits.** D-67 (`decisions/process.md:61`) cites
implementing commits `01a99ed`, `d162ba2`. D-66 references `ci/test.sh` by path.
- **Governance migration was done cleanly** — the `decisions/``governance/`
restructure updated cross-references and the auto-generated index.
- **Commit discipline is real.** `git log` shows imperative subjects, no Conventional
Commits prefixes, ticket refs, logical scoping — exactly what `git-commit/SKILL.md`
prescribes.
- **`licenses.yaml` is thorough** — all six runtime Dart deps present, plus fonts/native
libs, with purpose justifications. *(Note: the Security reviewer found version drift
in this file — see Finding above; the two reviewers examined different rows.)*
- **Makefile is self-documenting** (`##` help annotations) and matches `CLAUDE.md`.
### Findings
- **[Critical] `docs/initial-plan.md` is badly stale.** The "north-star" doc (linked
from `CLAUDE.md:14` and `README.md:44`) still describes a Go sidecar
(`initial-plan.md:4,55,189`), `clide --daemon` long-running process (`:162-164`),
`app/` subdirectory layout (`:184-204`), and `project.yaml` (`:172`) — all contradicted
by D-5, D-56, and the single-package-at-root reality. Nothing flags it as historical.
- **[Critical] `README.md` describes a dissolved architecture.** `README.md:10`
documents `ptyc/` as a live component; `README.md:36` lists `make ptyc-build`. The
`ptyc/` directory does not exist, the Makefile has no such target, and the CHANGELOG's
own Unreleased section records ptyc's removal.
- **[Major] `README.md:44` links to `decisions/`** — a directory that no longer exists
(migrated to `governance/`). Dead link in the primary onboarding doc.
- **[Major] CHANGELOG has duplicate subsection headings in `[Unreleased]`.** Three
`### Changed` blocks (`CHANGELOG.md:100, 114, 168`), two `### Fixed`, two `### Removed`
in the 2.0.0 section. Keep a Changelog 1.1.0 expects one of each per release.
- **[Major] No `CONTRIBUTING.md` or onboarding doc.** For a project "intended to ship
publicly to other developers," there is no contributor guide; the build/test story is
scattered across `CLAUDE.md` (Claude-oriented), `README.md` (partly wrong), and
Makefile help.
- **[Major] Coverage-floor governance contradicts itself.** D-66 (`testing.md:65`) says
the floor lives at `coverage/floor.txt` starting "≈35%"; `CHANGELOG.md:44-46` says it's
in `pubspec.yaml` `coverage_floor:` starting at 34%; the latest commit is `9030e56
hold coverage_floor fixed at 90`. Three sources, three mechanisms/values. D-66 was
never amended.
- **[Minor] `ci/release.sh` is a stub that still references goreleaser/sidecar** — Go
tooling for a project with no Go.
- **[Minor] CHANGELOG `[2.0.0] — 2026-05-03` dating** — the v2.0.0 tag is dated
2026-05-03 but the enormous Unreleased section represents ~80 commits of post-tag work
with no interim version.
- **[Minor] Stale-ish open questions** — Q-25 (body text face) is de facto resolved by
D-43/D-44 and the shipped impl; Q-1/Q-2/Q-3 ("defer until Tier 1 is in real use") are
due for triage now that Tier 1 has shipped.
- **[Minor] Skills system is coherent but undocumented as a set** — eight skills under
`.claude/skills/`, no index.
### Recommendations
**Quick wins:** rewrite `README.md`'s `ptyc/` sections and fix the `decisions/` link;
banner `docs/initial-plan.md` as historical (or split out a current
`docs/architecture.md`); merge the duplicate changelog subsection headings; amend D-66 to
reflect the floor's actual location/mechanism/value with a dated amendment line; triage
Q-1/2/3/25.
**Larger efforts:** write a human-facing `CONTRIBUTING.md` (clone →
`make hooks && flutter pub get``make test` → DQR workflow → commit conventions); cut
an interim release to drain the ~80-commit Unreleased backlog; add a
`.claude/skills/README.md` inventory; establish a periodic governance sweep (the repo
even has a `clean-house` skill for exactly this).
### Scorecard
| Area | Score | Justification |
|---|---|---|
| Governance discipline | 4/5 | DQR system genuinely maintained — but D-66 drift and untriaged Tier-1-era questions show the sweep cadence lags the code. |
| Documentation accuracy | 2/5 | Both primary onboarding docs describe a dissolved Go-sidecar/ptyc/daemon architecture; `CLAUDE.md` is accurate by contrast. |
| Changelog hygiene | 3/5 | Per-commit discipline is followed, but duplicate subsection headings violate the standard and an 80-commit Unreleased backlog undermines the format. |
| Contributor onboarding | 2/5 | No `CONTRIBUTING.md`; build story split across three docs, one wrong; `CLAUDE.md` is Claude-addressed, not human-addressed. |
| Convention adherence | 4/5 | Commit style, DQR claiming, `licenses.yaml` two-step rule demonstrably followed; docked for the changelog defects and the README gap. |
---
## Closing note
The recurring pattern across all six reviews: **clide's foundations are excellent and
its finishing is incomplete.** The extension contract, test helpers, FFI discipline,
governance system, and token system are all things most projects never get right. The
gaps — IPC not wired, keyboard not operable, docs describing a dead architecture, a
workspace-relative binary path — are all the kind of thing that happens when a fast-moving
solo project's implementation outruns its connective tissue. They are concentrated, not
diffuse, and the quick-win column above would close most of the critical ones in a few
focused days.
+130
View File
@@ -0,0 +1,130 @@
# Drawing-card JSON schema (T-317 / D-91 / D-103) — draft
Status: **draft for build**, SVG-substrate model (D-103), refined from the
T-317 wireframe set, 2026-06-28.
## Model — what this is, and what it is NOT
- **SVG is the substrate.** The card's primitive / scene-graph layer **is SVG**;
the clide-owned `CustomPaint` **SVG renderer (T-320) is the engine** the rest
builds on (D-103).
- It is **not** the HTML Canvas 2D API and **not** a third-party package. SVG is
a document *format* we render ourselves — "own the rendering stack" holds.
("HTML `<canvas>`" in D-91 was only a mental model, chosen to reject Obsidian's
`.canvas` schema; never an API to port.)
- The card is **two layers**:
1. **SVG content** — painted by the SVG renderer.
2. A thin **Flutter overlay** — the clide chrome that is *not* content
(per-object label/description captions, lightbox affordance), anchored to
SVG elements via `data-*` attributes.
- **Display-only** (D-78); re-rendered from the document. The **graph template
is the live-widget exception** (below).
## Document envelope
```json
{
"card": { "label": "Build pipeline", "description": "…" }, // optional caption (overlay)
"template": "icon", // optional → template sugar
"…template fields…": "…",
"svg": "<svg viewBox='0 0 480 360'>…</svg>" // primitive mode = raw SVG
// or "svgPath": "diagram.svg"
}
```
- **Template mode**`template` names a component; clide lowers it to SVG
(+ overlay anchors).
- **Primitive mode**`svg` (inline) or `svgPath` — arbitrary SVG, the escape
hatch. One less invented format; external SVG / graphviz / mermaid render free.
- Card size comes from the SVG `viewBox` (or `width`/`height`); the painter scales
to the pane width.
## Primitive layer = a bounded SVG subset
Grounded in a real d2 sample + our own templates — **not** a full SVG engine:
- **structure:** `<svg>` (viewBox/width/height, incl. nested `<svg>`), `<g>`
(transform, opacity, class), `<defs>`, `<marker>` (+ marker-start/mid/end,
`orient="auto"`, refX/refY, viewBox) — edge **arrowheads**
- **shapes:** `rect` (rx/ry), `circle`, `ellipse`, `line`, `polyline`,
`polygon`, `path` (full data — `M L H V C S Q T A Z` + relatives)
- **text:** `text` + `tspan` (x/y/dx/dy, font-family incl. **Phosphor**,
font-size/weight, text-anchor, dominant-baseline)
- **raster:** `image` (`href`/`xlink:href`, x/y/w/h, preserveAspectRatio)
- **styling:** presentation attrs (fill, fill-opacity, stroke, stroke-width,
stroke-linecap/linejoin, stroke-dasharray, opacity, color), `transform`
(translate/scale/rotate/matrix); `class=` resolved by the normalizer below
- **deferred v1:** `<mask>` (d2 masks connections for clean edge/node joins) —
ignore and lean on node-over-edge paint order; add only if output looks wrong
- **out:** `foreignObject`, filters, `<animate>`/SMIL, scripting,
`<use>`/`<symbol>`, gradients, patterns, `clipPath` → **mermaid is not a
launch target** (it leans on `foreignObject`)
`color` everywhere is an **arbitrary value** (hex / named) — content, not a clide
`SurfaceTokens` token (D-7 governs clide chrome, not rendered content).
### Class styling → inline-normalize (not a render-time CSS engine)
d2 / graphviz emit a `<style>` block of flat single-class selectors
(`.fill-B1`, `.shape`, `.connection`, `.text-bold` → presentation props), not
inline attributes. A **preprocessing normalizer** parses `<style>` into
class→props and merges each element's class props into inline presentation
attributes (inline wins), then drops `<style>`. The painter therefore only ever
sees inline attrs — a pure, testable presentation-attribute renderer. The
normalizer is a bounded, fixture-testable transform (real d2 + graphviz output).
## Overlay (clide chrome, layered over the SVG)
Flutter widgets anchored to SVG elements that carry:
- `data-label` → themed caption beneath the element's bounding box
- `data-description` → secondary caption line
- `data-lightbox` (on `<image>`) → click-to-zoom affordance
Templates emit these attributes; raw-SVG authors may add them. The icon
template's `data-label` is also the bridge to the interaction-zone choice list.
## Templates (lower to SVG + overlay)
| template | lowers to | ticket |
|----------|-----------|--------|
| `image` | `<image href>` + `data-lightbox` + caption attrs | T-316 |
| `icon` | `<text font=Phosphor>` glyphs at 10,11,12,13,14,15,18,20,24,32,48 + hero 52; per-item `data-label`/`data-description`/color | T-313 |
| `compare` | two+ `<image>` side by side in a `<g>`, per-image `data-lightbox` + captions | T-319 |
| `svg` | identity — the source *is* the SVG | T-320 |
| `d2` | compile d2 → SVG → render | T-494 |
| `graph` | **exception** — hosts the live native graph subsystem widget (D-46 / T-323), not static SVG | T-321 |
## CLI / transport (D-6 parity)
`clide draw --file doc.json` (or inline JSON). **Flutter-free** handler validates,
publishes on a `draw` MessageBus channel; the Claude extension injects the card —
mirroring `image.show`. The shipped `image show` (T-249/T-252) stays as
convenience and migrates onto this card later (D-91). `--stdin` deferred (T-315);
`--file` is the path.
## Error contract
Unknown `template`, unparseable / unsupported SVG, bad `href` / glyph / `color`
→ honest `IpcError` (`userError` / `notFound`), surfaced like `image.show`
**never a blank card**.
## Build sequence (D-103)
1. **T-320 — the SVG renderer (engine):** parse + paint the bounded SVG subset.
2. **T-318 — envelope + template dispatch + the Flutter overlay** (`data-*`
captions / lightbox) on top of the renderer.
3. **Templates:** image / icon (T-316 / T-313) → compare (T-319) → d2 (T-494) →
graph (T-321, after the graph subsystem T-323).
## Decisions
- **SVG subset boundary (T-320): RESOLVED** — see the subset above, grounded in
a real d2 sample; expand deliberately.
- **Class styling (T-320): RESOLVED** — inline-normalize, not a render-time CSS
engine (above).
- **Tool-PATH resolution: RESOLVED** — explicit user-scope override + first-run
auto-detect (D-104 / T-495); the d2 binary resolves through it.
- **D2 compiler delivery (T-494): open** — shell out to a `d2` binary as a
pql-style supporter tool (resolution now handled by D-104), vs. vendor.
- **Graph (T-321): gated** on the native graph subsystem (D-46 / T-323).
@@ -0,0 +1,64 @@
{
"name": "Drawing Card Core (T-318)",
"shapes": {
"title": { "type": "Text", "left": 60, "top": 38, "text": "clide drawing card — core: canvas engine + JSON schema + template dispatch (T-318)", "fontColor": "#c8d0e0", "fontSize": 14 },
"subtitle": { "type": "Text", "left": 60, "top": 60, "text": "the dispatch shell under every card type — a JSON document renders as raw primitives OR via a named template that lowers onto the same primitive scene (hybrid)", "fontColor": "#6a7280", "fontSize": 11 },
"cliLine": { "type": "Text", "left": 60, "top": 84, "text": "clide draw --file doc.json — Flutter-free handler → MessageBus → Claude-extension injection (mirrors image.show)", "fontColor": "#6a7280", "fontSize": 11 },
"inputBox": { "type": "Rectangle", "left": 400, "top": 110, "width": 300, "height": 58, "fillColor": "#14171c", "strokeColor": "#333340", "corners": [6, 6, 6, 6] },
"inT": { "type": "Text", "parent": "inputBox", "left": 418, "top": 122, "text": "Drawing-card JSON document", "fontColor": "#e2e8f5", "fontSize": 13 },
"inT2": { "type": "Text", "parent": "inputBox", "left": 418, "top": 144, "text": "{ \"template\"?: \"…\", \"objects\": [ … ] }", "fontColor": "#9aa3b5", "fontSize": 11 },
"coreBox": { "type": "Rectangle", "left": 360, "top": 200, "width": 380, "height": 60, "fillColor": "#1a1e24", "strokeColor": "#4a5570", "corners": [8, 8, 8, 8] },
"coreT": { "type": "Text", "parent": "coreBox", "left": 384, "top": 212, "text": "Drawing-card core (T-318)", "fontColor": "#e2e8f5", "fontSize": 14 },
"coreT2": { "type": "Text", "parent": "coreBox", "left": 384, "top": 234, "text": "JSON schema · primitive renderer · template dispatch", "fontColor": "#8a93a6", "fontSize": 11 },
"primBox": { "type": "Rectangle", "left": 110, "top": 312, "width": 360, "height": 150, "fillColor": "#161a20", "strokeColor": "#333340", "corners": [8, 8, 8, 8] },
"primH": { "type": "Text", "parent": "primBox", "left": 130, "top": 326, "text": "PRIMITIVE scene-graph", "fontColor": "#e2e8f5", "fontSize": 13 },
"primS": { "type": "Text", "parent": "primBox", "left": 130, "top": 348, "text": "objects drawn at coordinates", "fontColor": "#8a93a6", "fontSize": 11 },
"pc1": { "type": "Rectangle", "parent": "primBox", "left": 130, "top": 376, "width": 56, "height": 24, "fillColor": "#2a3040", "strokeColor": "#3a4250", "corners": [4, 4, 4, 4] },
"pc1t": { "type": "Text", "parent": "pc1", "left": 144, "top": 382, "text": "rect", "fontColor": "#c8d0e0", "fontSize": 10 },
"pc2": { "type": "Rectangle", "parent": "primBox", "left": 194, "top": 376, "width": 56, "height": 24, "fillColor": "#2a3040", "strokeColor": "#3a4250", "corners": [4, 4, 4, 4] },
"pc2t": { "type": "Text", "parent": "pc2", "left": 208, "top": 382, "text": "line", "fontColor": "#c8d0e0", "fontSize": 10 },
"pc3": { "type": "Rectangle", "parent": "primBox", "left": 258, "top": 376, "width": 56, "height": 24, "fillColor": "#2a3040", "strokeColor": "#3a4250", "corners": [4, 4, 4, 4] },
"pc3t": { "type": "Text", "parent": "pc3", "left": 272, "top": 382, "text": "text", "fontColor": "#c8d0e0", "fontSize": 10 },
"pc4": { "type": "Rectangle", "parent": "primBox", "left": 322, "top": 376, "width": 56, "height": 24, "fillColor": "#2a3040", "strokeColor": "#3a4250", "corners": [4, 4, 4, 4] },
"pc4t": { "type": "Text", "parent": "pc4", "left": 334, "top": 382, "text": "glyph", "fontColor": "#c8d0e0", "fontSize": 10 },
"pc5": { "type": "Rectangle", "parent": "primBox", "left": 386, "top": 376, "width": 56, "height": 24, "fillColor": "#2a3040", "strokeColor": "#3a4250", "corners": [4, 4, 4, 4] },
"pc5t": { "type": "Text", "parent": "pc5", "left": 398, "top": 382, "text": "image", "fontColor": "#c8d0e0", "fontSize": 10 },
"primNote": { "type": "Text", "parent": "primBox", "left": 130, "top": 420, "text": "{ \"type\": \"rect\", \"x\":.., \"y\":.., \"w\":.., \"h\":.. }", "fontColor": "#6a7280", "fontSize": 10 },
"tmplBox": { "type": "Rectangle", "left": 640, "top": 312, "width": 360, "height": 212, "fillColor": "#161a20", "strokeColor": "#333340", "corners": [8, 8, 8, 8] },
"tmplH": { "type": "Text", "parent": "tmplBox", "left": 660, "top": 326, "text": "TEMPLATE dispatch", "fontColor": "#e2e8f5", "fontSize": 13 },
"tmplS": { "type": "Text", "parent": "tmplBox", "left": 660, "top": 348, "text": "named component → predefined renderer (children)", "fontColor": "#8a93a6", "fontSize": 11 },
"tc1": { "type": "Rectangle", "parent": "tmplBox", "left": 660, "top": 374, "width": 150, "height": 26, "fillColor": "#2a3040", "strokeColor": "#3a4250", "corners": [4, 4, 4, 4] },
"tc1t": { "type": "Text", "parent": "tc1", "left": 674, "top": 381, "text": "image · T-316", "fontColor": "#c8d0e0", "fontSize": 10 },
"tc2": { "type": "Rectangle", "parent": "tmplBox", "left": 822, "top": 374, "width": 150, "height": 26, "fillColor": "#2a3040", "strokeColor": "#3a4250", "corners": [4, 4, 4, 4] },
"tc2t": { "type": "Text", "parent": "tc2", "left": 836, "top": 381, "text": "icon · T-313", "fontColor": "#c8d0e0", "fontSize": 10 },
"tc3": { "type": "Rectangle", "parent": "tmplBox", "left": 660, "top": 408, "width": 150, "height": 26, "fillColor": "#2a3040", "strokeColor": "#3a4250", "corners": [4, 4, 4, 4] },
"tc3t": { "type": "Text", "parent": "tc3", "left": 674, "top": 415, "text": "svg · T-320", "fontColor": "#c8d0e0", "fontSize": 10 },
"tc4": { "type": "Rectangle", "parent": "tmplBox", "left": 822, "top": 408, "width": 150, "height": 26, "fillColor": "#2a3040", "strokeColor": "#3a4250", "corners": [4, 4, 4, 4] },
"tc4t": { "type": "Text", "parent": "tc4", "left": 836, "top": 415, "text": "d2 · T-494", "fontColor": "#c8d0e0", "fontSize": 10 },
"tc5": { "type": "Rectangle", "parent": "tmplBox", "left": 660, "top": 442, "width": 150, "height": 26, "fillColor": "#2a3040", "strokeColor": "#3a4250", "corners": [4, 4, 4, 4] },
"tc5t": { "type": "Text", "parent": "tc5", "left": 674, "top": 449, "text": "compare · T-319", "fontColor": "#c8d0e0", "fontSize": 10 },
"tc6": { "type": "Rectangle", "parent": "tmplBox", "left": 822, "top": 442, "width": 150, "height": 26, "fillColor": "#2a3040", "strokeColor": "#3a4250", "corners": [4, 4, 4, 4] },
"tc6t": { "type": "Text", "parent": "tc6", "left": 836, "top": 449, "text": "graph · T-321", "fontColor": "#c8d0e0", "fontSize": 10 },
"tmplNote": { "type": "Text", "parent": "tmplBox", "left": 660, "top": 486, "text": "{ \"template\": \"svg\", \"source\": … }", "fontColor": "#6a7280", "fontSize": 10 },
"lowerLab": { "type": "Text", "left": 486, "top": 380, "text": "lower onto the", "fontColor": "#c8d8f0", "fontSize": 10 },
"lowerLab2": { "type": "Text", "left": 486, "top": 396, "text": "primitive scene (hybrid)", "fontColor": "#c8d8f0", "fontSize": 10 },
"canvasBox": { "type": "Rectangle", "left": 320, "top": 556, "width": 400, "height": 64, "fillColor": "#1a1e24", "strokeColor": "#4a5570", "corners": [8, 8, 8, 8] },
"cvT": { "type": "Text", "parent": "canvasBox", "left": 344, "top": 568, "text": "CustomPaint canvas — in the conversation pane", "fontColor": "#e2e8f5", "fontSize": 12 },
"cvT2": { "type": "Text", "parent": "canvasBox", "left": 344, "top": 590, "text": "+ shared per-object label / description widget (only when present)", "fontColor": "#8a93a6", "fontSize": 10 },
"foot": { "type": "Text", "left": 60, "top": 638, "text": "Display-only (D-78). Unknown template / primitive → clear userError. Templates (image · icon · svg · d2 · compare · graph) are separate children of this engine.", "fontColor": "#6a7280", "fontSize": 11 }
},
"connectors": {
"c1": { "tailId": "inputBox", "headId": "coreBox", "strokeColor": "#c8d8f0" },
"c2": { "tailId": "coreBox", "headId": "primBox", "strokeColor": "#6a7a98" },
"c3": { "tailId": "coreBox", "headId": "tmplBox", "strokeColor": "#6a7a98" },
"c4": { "tailId": "tmplBox", "headId": "primBox", "strokeColor": "#c8d8f0" },
"c5": { "tailId": "primBox", "headId": "canvasBox", "strokeColor": "#c8d8f0" }
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

@@ -0,0 +1,40 @@
{
"name": "Compare / Before-After Card (T-319)",
"shapes": {
"title": { "type": "Text", "left": 60, "top": 40, "text": "clide drawing card — compare / before-after (T-319)", "fontColor": "#c8d0e0", "fontSize": 14 },
"subtitle": { "type": "Text", "left": 60, "top": 62, "text": "two (or more) images side by side; each with an optional label + description and lightbox-on-click — shares the image template", "fontColor": "#6a7280", "fontSize": 11 },
"jsonBox": { "type": "Rectangle", "left": 60, "top": 92, "width": 600, "height": 116, "fillColor": "#14171c", "strokeColor": "#333340", "corners": [6, 6, 6, 6] },
"j1": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 104, "text": "{ \"template\": \"compare\", \"items\": [", "fontColor": "#9aa3b5", "fontSize": 11 },
"j2": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 126, "text": " { \"path\": \"before.png\", \"label\": \"Before\", \"description\": \"…\" },", "fontColor": "#9aa3b5", "fontSize": 11 },
"j3": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 148, "text": " { \"path\": \"after.png\", \"label\": \"After\", \"description\": \"…\" }", "fontColor": "#9aa3b5", "fontSize": 11 },
"j4": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 170, "text": "] }", "fontColor": "#9aa3b5", "fontSize": 11 },
"cli": { "type": "Text", "left": 60, "top": 226, "text": "clide draw --file compare.json", "fontColor": "#c8d8f0", "fontSize": 12 },
"card": { "type": "Rectangle", "left": 60, "top": 262, "width": 960, "height": 278, "fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [8, 8, 8, 8] },
"cardLabel": { "type": "Text", "parent": "card", "left": 84, "top": 282, "text": "HUD — before / after", "fontColor": "#e2e8f5", "fontSize": 16 },
"vdiv": { "type": "Rectangle", "parent": "card", "left": 540, "top": 300, "width": 1, "height": 216, "fillColor": "#333340", "strokeColor": "#333340" },
"labelL": { "type": "Text", "parent": "card", "left": 84, "top": 312, "text": "Before", "fontColor": "#e2e8f5", "fontSize": 14 },
"descL": { "type": "Text", "parent": "card", "left": 84, "top": 334, "text": "cramped status row", "fontColor": "#8a93a6", "fontSize": 11 },
"imgL": { "type": "Rectangle", "parent": "card", "left": 84, "top": 356, "width": 430, "height": 150, "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] },
"imgLname": { "type": "Text", "parent": "imgL", "left": 256, "top": 424, "text": "before.png", "fontColor": "#6a7280", "fontSize": 12 },
"zoomL": { "type": "Rectangle", "parent": "imgL", "left": 470, "top": 364, "width": 36, "height": 18, "fillColor": "#1a1e24", "strokeColor": "#3a4250", "corners": [3, 3, 3, 3] },
"zoomLt": { "type": "Text", "parent": "zoomL", "left": 476, "top": 367, "text": "zoom", "fontColor": "#8a93a6", "fontSize": 9 },
"labelR": { "type": "Text", "parent": "card", "left": 566, "top": 312, "text": "After", "fontColor": "#e2e8f5", "fontSize": 14 },
"descR": { "type": "Text", "parent": "card", "left": 566, "top": 334, "text": "roomy, aligned status row", "fontColor": "#8a93a6", "fontSize": 11 },
"imgR": { "type": "Rectangle", "parent": "card", "left": 566, "top": 356, "width": 430, "height": 150, "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] },
"imgRname": { "type": "Text", "parent": "imgR", "left": 740, "top": 424, "text": "after.png", "fontColor": "#6a7280", "fontSize": 12 },
"zoomR": { "type": "Rectangle", "parent": "imgR", "left": 952, "top": 364, "width": 36, "height": 18, "fillColor": "#1a1e24", "strokeColor": "#3a4250", "corners": [3, 3, 3, 3] },
"zoomRt": { "type": "Text", "parent": "zoomR", "left": 958, "top": 367, "text": "zoom", "fontColor": "#8a93a6", "fontSize": 9 },
"lightboxCap": { "type": "Text", "parent": "card", "left": 84, "top": 516, "text": "click either image → lightbox (shared with the image card)", "fontColor": "#6a7280", "fontSize": 10 },
"foot": { "type": "Text", "left": 60, "top": 562, "text": "Two or more items, side by side. Per-object label + description via the shared widget (T-318). Display-only (D-78).", "fontColor": "#6a7280", "fontSize": 11 }
},
"connectors": {
"flow": { "tailId": "jsonBox", "headId": "card", "strokeColor": "#3a4250" }
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

@@ -0,0 +1,58 @@
{
"name": "D2 Diagram Card",
"shapes": {
"title": { "type": "Text", "left": 60, "top": 40, "text": "clide drawing card — D2 diagram (T-494 · separate type, reuses the SVG widget T-320)", "fontColor": "#c8d0e0", "fontSize": 14 },
"subtitle": { "type": "Text", "left": 60, "top": 62, "text": "the rendered diagram leads; the d2 source folds into a collapsed “view d2 source” disclosure (collapser pattern, T-305)", "fontColor": "#6a7280", "fontSize": 11 },
"card1": { "type": "Rectangle", "left": 60, "top": 100, "width": 460, "height": 302, "fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [8, 8, 8, 8] },
"label1": { "type": "Text", "parent": "card1", "left": 84, "top": 122, "text": "Build pipeline", "fontColor": "#e2e8f5", "fontSize": 16 },
"region1": { "type": "Rectangle", "parent": "card1", "left": 84, "top": 154, "width": 412, "height": 180, "fillColor": "#14171c", "strokeColor": "#2a3040", "corners": [4, 4, 4, 4] },
"wm1": { "type": "Text", "parent": "region1", "left": 462, "top": 314, "text": "SVG", "fontColor": "#39424f", "fontSize": 11 },
"n1": { "type": "Rectangle", "parent": "region1", "left": 108, "top": 232, "width": 72, "height": 30, "fillColor": "#2a3040", "strokeColor": "#6a7a98", "corners": [4, 4, 4, 4] },
"n1t": { "type": "Text", "parent": "n1", "left": 124, "top": 241, "text": "fetch", "fontColor": "#c8d0e0", "fontSize": 11 },
"n2": { "type": "Rectangle", "parent": "region1", "left": 212, "top": 232, "width": 72, "height": 30, "fillColor": "#2a3040", "strokeColor": "#6a7a98", "corners": [4, 4, 4, 4] },
"n2t": { "type": "Text", "parent": "n2", "left": 228, "top": 241, "text": "build", "fontColor": "#c8d0e0", "fontSize": 11 },
"n3": { "type": "Rectangle", "parent": "region1", "left": 340, "top": 196, "width": 72, "height": 30, "fillColor": "#2a3040", "strokeColor": "#6a7a98", "corners": [4, 4, 4, 4] },
"n3t": { "type": "Text", "parent": "n3", "left": 360, "top": 205, "text": "test", "fontColor": "#c8d0e0", "fontSize": 11 },
"n4": { "type": "Rectangle", "parent": "region1", "left": 340, "top": 268, "width": 72, "height": 30, "fillColor": "#2a3040", "strokeColor": "#6a7a98", "corners": [4, 4, 4, 4] },
"n4t": { "type": "Text", "parent": "n4", "left": 352, "top": 277, "text": "deploy", "fontColor": "#c8d0e0", "fontSize": 11 },
"disc1": { "type": "Rectangle", "parent": "card1", "left": 84, "top": 348, "width": 412, "height": 34, "fillColor": "#181c22", "strokeColor": "#2a3040", "corners": [4, 4, 4, 4] },
"chev1": { "type": "Text", "parent": "disc1", "left": 98, "top": 356, "text": "▸", "fontColor": "#8a93a6", "fontSize": 13 },
"disc1t": { "type": "Text", "parent": "disc1", "left": 118, "top": 358, "text": "view d2 source", "fontColor": "#8a93a6", "fontSize": 12 },
"stateCap1": { "type": "Text", "left": 84, "top": 412, "text": "collapsed — diagram only", "fontColor": "#6a7280", "fontSize": 11 },
"card2": { "type": "Rectangle", "left": 560, "top": 100, "width": 460, "height": 384, "fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [8, 8, 8, 8] },
"label2": { "type": "Text", "parent": "card2", "left": 584, "top": 122, "text": "Build pipeline", "fontColor": "#e2e8f5", "fontSize": 16 },
"region2": { "type": "Rectangle", "parent": "card2", "left": 584, "top": 154, "width": 412, "height": 180, "fillColor": "#14171c", "strokeColor": "#2a3040", "corners": [4, 4, 4, 4] },
"wm2": { "type": "Text", "parent": "region2", "left": 962, "top": 314, "text": "SVG", "fontColor": "#39424f", "fontSize": 11 },
"m1": { "type": "Rectangle", "parent": "region2", "left": 608, "top": 232, "width": 72, "height": 30, "fillColor": "#2a3040", "strokeColor": "#6a7a98", "corners": [4, 4, 4, 4] },
"m1t": { "type": "Text", "parent": "m1", "left": 624, "top": 241, "text": "fetch", "fontColor": "#c8d0e0", "fontSize": 11 },
"m2": { "type": "Rectangle", "parent": "region2", "left": 712, "top": 232, "width": 72, "height": 30, "fillColor": "#2a3040", "strokeColor": "#6a7a98", "corners": [4, 4, 4, 4] },
"m2t": { "type": "Text", "parent": "m2", "left": 728, "top": 241, "text": "build", "fontColor": "#c8d0e0", "fontSize": 11 },
"m3": { "type": "Rectangle", "parent": "region2", "left": 840, "top": 196, "width": 72, "height": 30, "fillColor": "#2a3040", "strokeColor": "#6a7a98", "corners": [4, 4, 4, 4] },
"m3t": { "type": "Text", "parent": "m3", "left": 860, "top": 205, "text": "test", "fontColor": "#c8d0e0", "fontSize": 11 },
"m4": { "type": "Rectangle", "parent": "region2", "left": 840, "top": 268, "width": 72, "height": 30, "fillColor": "#2a3040", "strokeColor": "#6a7a98", "corners": [4, 4, 4, 4] },
"m4t": { "type": "Text", "parent": "m4", "left": 852, "top": 277, "text": "deploy", "fontColor": "#c8d0e0", "fontSize": 11 },
"disc2": { "type": "Rectangle", "parent": "card2", "left": 584, "top": 348, "width": 412, "height": 34, "fillColor": "#181c22", "strokeColor": "#2a3040", "corners": [4, 4, 4, 4] },
"chev2": { "type": "Text", "parent": "disc2", "left": 598, "top": 356, "text": "▾", "fontColor": "#c8d0e0", "fontSize": 13 },
"disc2t": { "type": "Text", "parent": "disc2", "left": 618, "top": 358, "text": "view d2 source", "fontColor": "#c8d0e0", "fontSize": 12 },
"srcBox": { "type": "Rectangle", "parent": "card2", "left": 584, "top": 388, "width": 412, "height": 82, "fillColor": "#14171c", "strokeColor": "#2a3040", "corners": [4, 4, 4, 4] },
"sd1": { "type": "Text", "parent": "srcBox", "left": 600, "top": 398, "text": "direction: right", "fontColor": "#9aa3b5", "fontSize": 11 },
"sd2": { "type": "Text", "parent": "srcBox", "left": 600, "top": 420, "text": "fetch -> build -> test", "fontColor": "#9aa3b5", "fontSize": 11 },
"sd3": { "type": "Text", "parent": "srcBox", "left": 600, "top": 442, "text": "build -> deploy", "fontColor": "#9aa3b5", "fontSize": 11 },
"stateCap2": { "type": "Text", "left": 584, "top": 494, "text": "expanded — d2 source revealed", "fontColor": "#6a7280", "fontSize": 11 },
"expandLab": { "type": "Text", "left": 528, "top": 330, "text": "expand", "fontColor": "#c8d8f0", "fontSize": 10 },
"foot": { "type": "Text", "left": 60, "top": 520, "text": "D2 → SVG compile in front; renders via the T-320 SVG widget. clide draw --file pipeline.d2 (type inferred from .d2)", "fontColor": "#6a7280", "fontSize": 11 }
},
"connectors": {
"e1": { "tailId": "n1", "headId": "n2", "strokeColor": "#6a7a98" },
"e2": { "tailId": "n2", "headId": "n3", "strokeColor": "#6a7a98" },
"e3": { "tailId": "n2", "headId": "n4", "strokeColor": "#6a7a98" },
"f1": { "tailId": "m1", "headId": "m2", "strokeColor": "#6a7a98" },
"f2": { "tailId": "m2", "headId": "m3", "strokeColor": "#6a7a98" },
"f3": { "tailId": "m2", "headId": "m4", "strokeColor": "#6a7a98" },
"expand": { "tailId": "disc1", "headId": "disc2", "strokeColor": "#3a4250" }
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

@@ -0,0 +1,45 @@
{
"name": "Graph Render Card (T-321)",
"shapes": {
"title": { "type": "Text", "left": 60, "top": 40, "text": "clide drawing card — graph render (T-321)", "fontColor": "#c8d0e0", "fontSize": 14 },
"subtitle": { "type": "Text", "left": 60, "top": 62, "text": "renders a node/edge graph from JSON — embeds clide's native graph subsystem (CustomPaint, D-46 / T-323), not a new renderer", "fontColor": "#6a7280", "fontSize": 11 },
"jsonBox": { "type": "Rectangle", "left": 60, "top": 110, "width": 400, "height": 196, "fillColor": "#14171c", "strokeColor": "#333340", "corners": [6, 6, 6, 6] },
"j1": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 124, "text": "{ \"template\": \"graph\",", "fontColor": "#9aa3b5", "fontSize": 11 },
"j2": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 146, "text": " \"nodes\": [\"app\",\"ipc\",\"pty\",\"git\",\"pql\"],", "fontColor": "#9aa3b5", "fontSize": 11 },
"j3": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 168, "text": " \"edges\": [[\"app\",\"ipc\"],[\"ipc\",\"pty\"],", "fontColor": "#9aa3b5", "fontSize": 11 },
"j4": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 190, "text": " [\"ipc\",\"git\"],[\"git\",\"pql\"],[\"app\",\"git\"]],", "fontColor": "#9aa3b5", "fontSize": 11 },
"j5": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 212, "text": " \"label\": \"subsystem links\" }", "fontColor": "#9aa3b5", "fontSize": 11 },
"cli": { "type": "Text", "left": 60, "top": 328, "text": "clide draw --file graph.json", "fontColor": "#c8d8f0", "fontSize": 12 },
"cliNote": { "type": "Text", "left": 60, "top": 350, "text": "(nodes + edges inline, or a path to a graph file)", "fontColor": "#6a7280", "fontSize": 11 },
"card": { "type": "Rectangle", "left": 560, "top": 110, "width": 480, "height": 360, "fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [8, 8, 8, 8] },
"region": { "type": "Rectangle", "parent": "card", "left": 584, "top": 134, "width": 432, "height": 256, "fillColor": "#14171c", "strokeColor": "#2a3040", "corners": [4, 4, 4, 4] },
"wm": { "type": "Text", "parent": "region", "left": 980, "top": 372, "text": "graph", "fontColor": "#39424f", "fontSize": 11 },
"na": { "type": "Ellipse", "parent": "region", "left": 644, "top": 168, "width": 46, "height": 46, "fillColor": "#2a3040", "strokeColor": "#6a7a98" },
"nat": { "type": "Text", "parent": "na", "left": 657, "top": 184, "text": "app", "fontColor": "#c8d0e0", "fontSize": 11 },
"ni": { "type": "Ellipse", "parent": "region", "left": 766, "top": 206, "width": 46, "height": 46, "fillColor": "#2a3040", "strokeColor": "#6a7a98" },
"nit": { "type": "Text", "parent": "ni", "left": 781, "top": 222, "text": "ipc", "fontColor": "#c8d0e0", "fontSize": 11 },
"np": { "type": "Ellipse", "parent": "region", "left": 898, "top": 166, "width": 46, "height": 46, "fillColor": "#2a3040", "strokeColor": "#6a7a98" },
"npt": { "type": "Text", "parent": "np", "left": 911, "top": 182, "text": "pty", "fontColor": "#c8d0e0", "fontSize": 11 },
"ng": { "type": "Ellipse", "parent": "region", "left": 760, "top": 306, "width": 46, "height": 46, "fillColor": "#2a3040", "strokeColor": "#6a7a98" },
"ngt": { "type": "Text", "parent": "ng", "left": 773, "top": 322, "text": "git", "fontColor": "#c8d0e0", "fontSize": 11 },
"nq": { "type": "Ellipse", "parent": "region", "left": 904, "top": 298, "width": 46, "height": 46, "fillColor": "#2a3040", "strokeColor": "#6a7a98" },
"nqt": { "type": "Text", "parent": "nq", "left": 917, "top": 314, "text": "pql", "fontColor": "#c8d0e0", "fontSize": 11 },
"label": { "type": "Text", "parent": "card", "left": 584, "top": 402, "text": "subsystem links", "fontColor": "#e2e8f5", "fontSize": 14 },
"desc": { "type": "Text", "parent": "card", "left": 584, "top": 424, "text": "how the core pieces connect", "fontColor": "#8a93a6", "fontSize": 11 },
"foot": { "type": "Text", "left": 60, "top": 500, "text": "Embeds clide's native graph subsystem (CustomPaint, D-46 / T-323) — the card hosts it, doesn't fork it. Label + description beneath via T-318. Display-only (D-78).", "fontColor": "#6a7280", "fontSize": 11 }
},
"connectors": {
"flow": { "tailId": "jsonBox", "headId": "card", "strokeColor": "#3a4250" },
"ea": { "tailId": "na", "headId": "ni", "strokeColor": "#6a7a98" },
"eb": { "tailId": "ni", "headId": "np", "strokeColor": "#6a7a98" },
"ec": { "tailId": "ni", "headId": "ng", "strokeColor": "#6a7a98" },
"ed": { "tailId": "ng", "headId": "nq", "strokeColor": "#6a7a98" },
"ee": { "tailId": "na", "headId": "ng", "strokeColor": "#6a7a98" }
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

@@ -0,0 +1,78 @@
{
"name": "Icon Glyph Card (T-313)",
"shapes": {
"title": { "type": "Text", "left": 60, "top": 44, "text": "clide icon show — Phosphor glyph card (T-313)", "fontColor": "#c8d0e0", "fontSize": 14 },
"subtitle": { "type": "Text", "left": 60, "top": 66, "text": "per-entry label + description · hero (52) + a strip of size samples from 10 up to 48 px", "fontColor": "#6a7280", "fontSize": 11 },
"cli1": { "type": "Text", "left": 60, "top": 98, "text": "clide icon show gear folder gauge", "fontColor": "#c8d8f0", "fontSize": 12 },
"cli1n": { "type": "Text", "left": 372, "top": 98, "text": "— bare variadic preview", "fontColor": "#6a7280", "fontSize": 11 },
"cli2": { "type": "Text", "left": 60, "top": 118, "text": "clide icon show --file icons.json", "fontColor": "#c8d8f0", "fontSize": 12 },
"cli2n": { "type": "Text", "left": 372, "top": 118, "text": "— labelled entries: [{icon, label, description}, …]", "fontColor": "#6a7280", "fontSize": 11 },
"glyphNote": { "type": "Text", "left": 60, "top": 140, "text": "(wireframe: neutral marks stand in for glyphs — Frame0 has no Phosphor font; real card paints via PhosphorIconPainter)", "fontColor": "#566070", "fontSize": 10 },
"card": { "type": "Rectangle", "left": 60, "top": 168, "width": 960, "height": 350, "fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [8, 8, 8, 8] },
"hero1": { "type": "Rectangle", "left": 84, "top": 192, "width": 92, "height": 92, "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [8, 8, 8, 8] },
"hero1g": { "type": "Ellipse", "left": 106, "top": 214, "width": 48, "height": 48, "fillColor": "#c8d8f0", "strokeColor": "#c8d8f0" },
"hero1cap": { "type": "Text", "left": 90, "top": 290, "text": "hero · 52", "fontColor": "#6a7280", "fontSize": 10 },
"label1": { "type": "Text", "left": 200, "top": 192, "text": "Settings", "fontColor": "#e2e8f5", "fontSize": 16 },
"desc1": { "type": "Text", "left": 200, "top": 216, "text": "global scope", "fontColor": "#8a93a6", "fontSize": 12 },
"sizecap1": { "type": "Text", "left": 200, "top": 240, "text": "samples (px)", "fontColor": "#6a7280", "fontSize": 10 },
"a1": { "type": "Rectangle", "left": 200, "top": 302, "width": 10, "height": 10, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
"a1l": { "type": "Text", "left": 200, "top": 318, "text": "10", "fontColor": "#6a7280", "fontSize": 9 },
"a2": { "type": "Rectangle", "left": 218, "top": 301, "width": 11, "height": 11, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
"a2l": { "type": "Text", "left": 218, "top": 318, "text": "11", "fontColor": "#6a7280", "fontSize": 9 },
"a3": { "type": "Rectangle", "left": 237, "top": 300, "width": 12, "height": 12, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
"a3l": { "type": "Text", "left": 238, "top": 318, "text": "12", "fontColor": "#6a7280", "fontSize": 9 },
"a4": { "type": "Rectangle", "left": 257, "top": 299, "width": 13, "height": 13, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
"a4l": { "type": "Text", "left": 258, "top": 318, "text": "13", "fontColor": "#6a7280", "fontSize": 9 },
"a5": { "type": "Rectangle", "left": 278, "top": 298, "width": 14, "height": 14, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
"a5l": { "type": "Text", "left": 280, "top": 318, "text": "14", "fontColor": "#6a7280", "fontSize": 9 },
"a6": { "type": "Rectangle", "left": 300, "top": 297, "width": 15, "height": 15, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
"a6l": { "type": "Text", "left": 302, "top": 318, "text": "15", "fontColor": "#6a7280", "fontSize": 9 },
"a7": { "type": "Rectangle", "left": 323, "top": 294, "width": 18, "height": 18, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
"a7l": { "type": "Text", "left": 327, "top": 318, "text": "18", "fontColor": "#6a7280", "fontSize": 9 },
"a8": { "type": "Rectangle", "left": 349, "top": 292, "width": 20, "height": 20, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
"a8l": { "type": "Text", "left": 354, "top": 318, "text": "20", "fontColor": "#6a7280", "fontSize": 9 },
"a9": { "type": "Rectangle", "left": 377, "top": 288, "width": 24, "height": 24, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
"a9l": { "type": "Text", "left": 384, "top": 318, "text": "24", "fontColor": "#6a7280", "fontSize": 9 },
"a10": { "type": "Rectangle", "left": 409, "top": 280, "width": 32, "height": 32, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
"a10l": { "type": "Text", "left": 420, "top": 318, "text": "32", "fontColor": "#6a7280", "fontSize": 9 },
"a11": { "type": "Rectangle", "left": 449, "top": 264, "width": 48, "height": 48, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
"a11l": { "type": "Text", "left": 468, "top": 318, "text": "48", "fontColor": "#6a7280", "fontSize": 9 },
"divider": { "type": "Rectangle", "left": 84, "top": 336, "width": 912, "height": 1, "fillColor": "#333340", "strokeColor": "#333340" },
"hero2": { "type": "Rectangle", "left": 84, "top": 360, "width": 92, "height": 92, "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [8, 8, 8, 8] },
"hero2g": { "type": "Ellipse", "left": 106, "top": 382, "width": 48, "height": 48, "fillColor": "#9fb0c8", "strokeColor": "#9fb0c8" },
"hero2cap": { "type": "Text", "left": 90, "top": 458, "text": "hero · 52", "fontColor": "#6a7280", "fontSize": 10 },
"label2": { "type": "Text", "left": 200, "top": 360, "text": "Folder", "fontColor": "#e2e8f5", "fontSize": 16 },
"desc2": { "type": "Text", "left": 200, "top": 384, "text": "workspace tree", "fontColor": "#8a93a6", "fontSize": 12 },
"sizecap2": { "type": "Text", "left": 200, "top": 408, "text": "same scale, per entry", "fontColor": "#6a7280", "fontSize": 10 },
"b1": { "type": "Rectangle", "left": 200, "top": 470, "width": 10, "height": 10, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
"b2": { "type": "Rectangle", "left": 218, "top": 469, "width": 11, "height": 11, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
"b3": { "type": "Rectangle", "left": 237, "top": 468, "width": 12, "height": 12, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
"b4": { "type": "Rectangle", "left": 257, "top": 467, "width": 13, "height": 13, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
"b5": { "type": "Rectangle", "left": 278, "top": 466, "width": 14, "height": 14, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
"b6": { "type": "Rectangle", "left": 300, "top": 465, "width": 15, "height": 15, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
"b7": { "type": "Rectangle", "left": 323, "top": 462, "width": 18, "height": 18, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
"b8": { "type": "Rectangle", "left": 349, "top": 460, "width": 20, "height": 20, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
"b9": { "type": "Rectangle", "left": 377, "top": 456, "width": 24, "height": 24, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
"b10": { "type": "Rectangle", "left": 409, "top": 448, "width": 32, "height": 32, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
"b11": { "type": "Rectangle", "left": 449, "top": 432, "width": 48, "height": 48, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
"tagMeta": { "type": "Text", "left": 880, "top": 196, "text": "label + description", "fontColor": "#c8d8f0", "fontSize": 11 },
"tagHero": { "type": "Text", "left": 880, "top": 226, "text": "hero · 52", "fontColor": "#c8d8f0", "fontSize": 11 },
"tagSizes": { "type": "Text", "left": 880, "top": 286, "text": "size samples 10→48", "fontColor": "#c8d8f0", "fontSize": 11 },
"foot1": { "type": "Text", "left": 60, "top": 536, "text": "Display-only (D-78): the card shows options; selection happens in the convo box. Per-entry labels bridge card → interaction-zone choice list.", "fontColor": "#6a7280", "fontSize": 11 },
"foot2": { "type": "Text", "left": 60, "top": 556, "text": "Without label/description, entries collapse to a bare glyph grid.", "fontColor": "#566070", "fontSize": 11 }
},
"connectors": {
"cMeta": { "tailId": "tagMeta", "headId": "label1", "strokeColor": "#3a4250" },
"cHero": { "tailId": "tagHero", "headId": "hero1", "strokeColor": "#3a4250" },
"cSizes": { "tailId": "tagSizes", "headId": "a11", "strokeColor": "#3a4250" }
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

@@ -0,0 +1,71 @@
{
"name": "Image Annotation Flow (T-316)",
"shapes": {
"title": {
"type": "Text",
"left": 60, "top": 56,
"text": "clide image show — annotated image card (T-316, variant a: text metadata)",
"fontColor": "#c8d0e0", "fontSize": 14
},
"jsonBox": {
"type": "Rectangle",
"left": 60, "top": 110, "width": 380, "height": 200,
"fillColor": "#14171c", "strokeColor": "#333340", "corners": [6, 6, 6, 6]
},
"j1": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 124, "text": "{", "fontColor": "#9aa3b5", "fontSize": 11 },
"j2": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 146, "text": " \"path\": \"docs/shot.png\",", "fontColor": "#9aa3b5", "fontSize": 11 },
"j3": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 168, "text": " \"label\": \"HUD v3\",", "fontColor": "#9aa3b5", "fontSize": 11 },
"j4": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 190, "text": " \"description\": \"status row cramped\",", "fontColor": "#9aa3b5", "fontSize": 11 },
"j5": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 212, "text": " \"caption\": \"before the fix\"", "fontColor": "#9aa3b5", "fontSize": 11 },
"j6": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 234, "text": "}", "fontColor": "#9aa3b5", "fontSize": 11 },
"cli": {
"type": "Text",
"left": 60, "top": 330,
"text": "clide image show docs/shot.png --file meta.json",
"fontColor": "#c8d8f0", "fontSize": 12
},
"cliNote": {
"type": "Text",
"left": 60, "top": 354,
"text": "(same payload via --stdin once T-315 lands; --caption form still works)",
"fontColor": "#6a7280", "fontSize": 11
},
"card": {
"type": "Rectangle",
"left": 560, "top": 110, "width": 480, "height": 430,
"fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [8, 8, 8, 8]
},
"cardLabel": { "type": "Text", "parent": "card", "left": 584, "top": 134, "text": "HUD v3", "fontColor": "#e2e8f5", "fontSize": 16 },
"cardDesc": { "type": "Text", "parent": "card", "left": 584, "top": 164, "text": "Status row cramped — the clock overlaps the battery at narrow widths.", "fontColor": "#8a93a6", "fontSize": 12 },
"cardImage": {
"type": "Rectangle",
"parent": "card",
"left": 584, "top": 200, "width": 432, "height": 250,
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
},
"cardImageName": { "type": "Text", "parent": "cardImage", "left": 730, "top": 318, "text": "docs/shot.png", "fontColor": "#6a7280", "fontSize": 12 },
"cardCaption": { "type": "Text", "parent": "card", "left": 584, "top": 468, "text": "before the fix", "fontColor": "#8a93a6", "fontSize": 11 },
"tagLabel": { "type": "Text", "left": 1052, "top": 134, "text": "label", "fontColor": "#c8d8f0", "fontSize": 11 },
"tagDesc": { "type": "Text", "left": 1052, "top": 164, "text": "description", "fontColor": "#c8d8f0", "fontSize": 11 },
"tagImage": { "type": "Text", "left": 1052, "top": 318, "text": "path", "fontColor": "#c8d8f0", "fontSize": 11 },
"tagCaption": { "type": "Text", "left": 1052, "top": 468, "text": "caption", "fontColor": "#c8d8f0", "fontSize": 11 },
"followup": {
"type": "Text",
"left": 560, "top": 558,
"text": "Variant (b) follow-up: visual markers / numbered callouts painted at {x,y} over the image (CustomPaint).",
"fontColor": "#6a7280", "fontSize": 11
}
},
"connectors": {
"flow": { "tailId": "jsonBox", "headId": "card", "strokeColor": "#c8d8f0" },
"mLabel": { "tailId": "tagLabel", "headId": "cardLabel", "strokeColor": "#3a4250" },
"mDesc": { "tailId": "tagDesc", "headId": "cardDesc", "strokeColor": "#3a4250" },
"mImage": { "tailId": "tagImage", "headId": "cardImage", "strokeColor": "#3a4250" },
"mCaption": { "tailId": "tagCaption", "headId": "cardCaption", "strokeColor": "#3a4250" }
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

@@ -0,0 +1,42 @@
{
"name": "SVG Render Card (T-320)",
"shapes": {
"title": { "type": "Text", "left": 60, "top": 44, "text": "clide drawing card — SVG render (T-320)", "fontColor": "#c8d0e0", "fontSize": 14 },
"subtitle": { "type": "Text", "left": 60, "top": 66, "text": "a drawing-card template (T-318 dispatch): renders a vector SVG natively in the conversation pane — separate type from the raster image card", "fontColor": "#6a7280", "fontSize": 11 },
"srcBox": { "type": "Rectangle", "left": 60, "top": 110, "width": 380, "height": 178, "fillColor": "#14171c", "strokeColor": "#333340", "corners": [6, 6, 6, 6] },
"s1": { "type": "Text", "parent": "srcBox", "left": 76, "top": 124, "text": "{", "fontColor": "#9aa3b5", "fontSize": 11 },
"s2": { "type": "Text", "parent": "srcBox", "left": 76, "top": 146, "text": " \"template\": \"svg\",", "fontColor": "#9aa3b5", "fontSize": 11 },
"s3": { "type": "Text", "parent": "srcBox", "left": 76, "top": 168, "text": " \"title\": \"Build pipeline\",", "fontColor": "#9aa3b5", "fontSize": 11 },
"s4": { "type": "Text", "parent": "srcBox", "left": 76, "top": 190, "text": " \"source\": \"<svg>…</svg>\" // or a .svg path", "fontColor": "#9aa3b5", "fontSize": 11 },
"s5": { "type": "Text", "parent": "srcBox", "left": 76, "top": 212, "text": "}", "fontColor": "#9aa3b5", "fontSize": 11 },
"cli": { "type": "Text", "left": 60, "top": 308, "text": "clide draw --file pipeline.json", "fontColor": "#c8d8f0", "fontSize": 12 },
"cliNote": { "type": "Text", "left": 60, "top": 330, "text": "(inline SVG string or a .svg path; clide owns the SVG renderer — vector, no raster rasterise)", "fontColor": "#6a7280", "fontSize": 11 },
"card": { "type": "Rectangle", "left": 560, "top": 110, "width": 480, "height": 362, "fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [8, 8, 8, 8] },
"cardLabel": { "type": "Text", "parent": "card", "left": 584, "top": 132, "text": "Build pipeline", "fontColor": "#e2e8f5", "fontSize": 16 },
"region": { "type": "Rectangle", "parent": "card", "left": 584, "top": 168, "width": 432, "height": 248, "fillColor": "#14171c", "strokeColor": "#2a3040", "corners": [4, 4, 4, 4] },
"rgWatermark": { "type": "Text", "parent": "region", "left": 968, "top": 392, "text": "SVG", "fontColor": "#39424f", "fontSize": 11 },
"n1": { "type": "Rectangle", "parent": "region", "left": 612, "top": 214, "width": 96, "height": 40, "fillColor": "#2a3040", "strokeColor": "#6a7a98", "corners": [4, 4, 4, 4] },
"n1t": { "type": "Text", "parent": "n1", "left": 636, "top": 226, "text": "fetch", "fontColor": "#c8d0e0", "fontSize": 12 },
"n2": { "type": "Rectangle", "parent": "region", "left": 772, "top": 214, "width": 96, "height": 40, "fillColor": "#2a3040", "strokeColor": "#6a7a98", "corners": [4, 4, 4, 4] },
"n2t": { "type": "Text", "parent": "n2", "left": 798, "top": 226, "text": "build", "fontColor": "#c8d0e0", "fontSize": 12 },
"n3": { "type": "Rectangle", "parent": "region", "left": 772, "top": 320, "width": 96, "height": 40, "fillColor": "#2a3040", "strokeColor": "#6a7a98", "corners": [4, 4, 4, 4] },
"n3t": { "type": "Text", "parent": "n3", "left": 802, "top": 332, "text": "test", "fontColor": "#c8d0e0", "fontSize": 12 },
"caption": { "type": "Text", "parent": "card", "left": 584, "top": 430, "text": "rendered SVG — crisp at any zoom (vector)", "fontColor": "#8a93a6", "fontSize": 11 },
"tagRender": { "type": "Text", "left": 1058, "top": 286, "text": "in-pane\nSVG widget", "fontColor": "#c8d8f0", "fontSize": 11 },
"foot": { "type": "Text", "left": 60, "top": 500, "text": "The D2 card (next frame) reuses THIS widget — D2 compiles to SVG, then renders through the same path.", "fontColor": "#6a7280", "fontSize": 11 }
},
"connectors": {
"flow": { "tailId": "srcBox", "headId": "card", "strokeColor": "#c8d8f0" },
"e1": { "tailId": "n1", "headId": "n2", "strokeColor": "#6a7a98" },
"e2": { "tailId": "n2", "headId": "n3", "strokeColor": "#6a7a98" },
"tRender": { "tailId": "tagRender", "headId": "region", "strokeColor": "#3a4250" }
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

@@ -0,0 +1,320 @@
The **Vibe CLI** (Mistral's open-source CLI tool for their AI models) **significantly changes the math** for replacing Claude in **clide**. Unlike Mistral's raw REST API, **Vibe CLI** is designed as a **local, interactive, and extensible** tool that **mimics many of Claude Code's features**, including **stdio-based interaction, tool execution, and session management**. This makes it a **far closer drop-in replacement** for Claude Code CLI than the raw API.
Heres how **Vibe CLI** compares to **Claude Code CLI** and how it impacts the migration effort:
---
---
---
## **🔹 Key Differences: Vibe CLI vs. Mistral API vs. Claude Code CLI**
| **Feature** | **Claude Code CLI** | **Mistral API** | **Vibe CLI** | **Impact on clide Migration** |
|---------------------------|---------------------|-----------------|--------------|-------------------------------|
| **Stdio-Based Interaction** | ✅ (stream-json) | ❌ (HTTP/SSE) | ✅ (stdio) | **🟢 Major Win: Vibe CLI supports stdio, enabling bidirectional communication like Claude.** |
| **Tool Execution** | ✅ (native + MCP) | ✅ (API `tools` param) | ✅ (native + MCP) | **🟢 Vibe CLI supports tools natively, including MCP.** |
| **Permission Prompts** | ✅ (`can_use_tool` stdio) | ❌ | ✅ (stdio-based) | **🟢 Vibe CLI supports permission gating via stdio (similar to Claude).** |
| **Session Persistence** | ✅ (`--resume`) | ❌ | ✅ (`--resume`) | **🟢 Vibe CLI supports session resumption.** |
| **Transcript Format** | ✅ (JSONL) | ❌ (API responses) | ✅ (JSONL) | **🟢 Vibe CLI uses a similar JSONL transcript format.** |
| **AskUserQuestion** | ✅ (`can_use_tool` stdio) | ❌ | ✅ (stdio-based) | **🟢 Vibe CLI supports interactive prompts via stdio.** |
| **Multi-Agent Teams** | ❌ (tmux-only) | ❌ | ❌ | **⚠️ Still missing, but clides MCP-based team orchestration can be reused.** |
| **Config System** | ✅ (`.claude/`) | ❌ | ✅ (`.vibe/`) | **🟢 Vibe CLI has its own config system (`.vibe/`).** |
| **Local Inference** | ❌ (Cloud-only) | ✅ | ✅ | **🟢 Vibe CLI supports local models (e.g., `mistral-large`, `codestral`).** |
| **MCP Support** | ✅ | ✅ | ✅ | **🟢 Vibe CLI supports MCP servers.** |
| **Streaming Responses** | ✅ (line-delimited JSON) | ✅ (SSE) | ✅ (stdio) | **🟢 Vibe CLI streams responses via stdio.** |
---
---
---
## **🔹 How Vibe CLI Changes the Migration Math**
### **1. Stdio Protocol Compatibility (🟢 Game-Changer)**
- **Claude Code CLI** uses a **custom stream-json protocol** over stdio for:
- Conversation streaming (`assistant`, `user`, `tool_use` events).
- Control requests (`can_use_tool` for permissions, `AskUserQuestion`).
- Session management (`--resume`, `--session-id`).
- **Vibe CLI** also uses **stdio for interaction**, including:
- **Streaming responses** (similar to Claudes line-delimited JSON).
- **Tool execution** (native and MCP-based).
- **Permission prompts** (stdio-based gating, like Claudes `can_use_tool`).
- **Session resumption** (`--resume` flag).
- **Impact**:
- **clides `StreamJsonProcess` can be adapted to work with Vibe CLI** with **minimal changes**.
- **No need for a custom wrapper** (unlike Mistral API).
- **Permission prompts and AskUserQuestion can be handled natively** (no manual reimplementation).
---
### **2. Session Persistence (🟢 Major Win)**
- **Claude Code CLI**:
- Stores sessions in `~/.claude/projects/<munged-cwd>/<session-id>.jsonl`.
- Supports `--resume <session-id>` to restore a session.
- **Vibe CLI**:
- Stores sessions in `~/.vibe/sessions/<session-id>.jsonl`.
- Supports `--resume <session-id>` to restore a session.
- **Impact**:
- **clide can reuse its existing session management logic** (e.g., `SessionStorage`, `TranscriptReader`).
- **No need to manually store/replay transcripts** (Vibe CLI handles it).
---
### **3. Tool Execution and Permission Prompts (🟢 Critical Parity)**
- **Claude Code CLI**:
- Uses `--permission-prompt-tool stdio` to route permission requests to the client.
- Emits `can_use_tool` control requests for tools like `Write`, `Bash`, etc.
- Supports `AskUserQuestion` via the same channel.
- **Vibe CLI**:
- **Also supports stdio-based permission prompts** (similar to Claude).
- Tools can be **allowed, denied, or gated** via stdio.
- Supports **interactive questions** (e.g., "Should I proceed?").
- **Impact**:
- **clides `ToolPrompt` and permission UI can be reused** with **minimal changes**.
- **No need to reimplement permission logic** from scratch.
---
### **4. Config System (🟢 Close Enough)**
- **Claude Code CLI**:
- Uses `.claude/` for skills, agents, hooks, and settings.
- clides `ClaudeConfig` service watches `.claude/` and probes the CLI for built-in commands.
- **Vibe CLI**:
- Uses `.vibe/` for config, tools, and MCP servers.
- Supports **custom commands, tools, and MCP integrations**.
- **Impact**:
- **clides config system can be adapted** to watch `.vibe/` instead of `.claude/`.
- **Minimal changes** to `ClaudeConfig` (rename paths, adjust probes).
---
### **5. Transcript Format (🟢 High Compatibility)**
- **Claude Code CLI**:
- Transcripts are stored as **JSONL** (one JSON object per line).
- Each line represents an event (`assistant`, `user`, `tool_use`, etc.).
- **Vibe CLI**:
- **Also uses JSONL for transcripts** (similar structure).
- Events include `assistant`, `user`, `tool_call`, etc.
- **Impact**:
- **clides `TranscriptReader` can be adapted** to parse Vibe CLIs JSONL format.
- **Minimal changes** to the parsing logic.
---
### **6. MCP Support (🟢 Full Parity)**
- **Claude Code CLI**:
- Supports MCP servers via `--mcp-config`.
- clide hosts its own MCP server for team orchestration.
- **Vibe CLI**:
- **Natively supports MCP servers** (via `--mcp` or config).
- **Impact**:
- **clides MCP broker can be reused** for Vibe CLI.
- **Team orchestration can work the same way** (clide spawns multiple Vibe CLI processes and coordinates via MCP).
---
### **7. Local Inference (🟢 Bonus)**
- **Claude Code CLI**:
- Cloud-only (no local inference).
- **Vibe CLI**:
- Supports **local models** (e.g., `mistral-large`, `codestral`, or any Ollama-compatible model).
- **Impact**:
- **clide can leverage local models** for offline or private use cases.
- **No dependency on cloud APIs** (if using local models).
---
---
---
## **🔹 Updated Migration Complexity with Vibe CLI**
| **Feature** | **Claude Code CLI** | **Mistral API** | **Vibe CLI** | **Migration Effort** |
|-------------|---------------------|-----------------|--------------|----------------------|
| **Stdio Protocol** | ✅ | ❌ | ✅ | **Low** (adapt `StreamJsonProcess`) |
| **Streaming Responses** | ✅ | ✅ (SSE) | ✅ (stdio) | **Low** |
| **Tool Execution** | ✅ | ✅ | ✅ | **Low** |
| **Permission Prompts** | ✅ | ❌ | ✅ | **Low** (reuse existing UI) |
| **AskUserQuestion** | ✅ | ❌ | ✅ | **Low** |
| **Session Persistence** | ✅ | ❌ | ✅ | **Low** (reuse existing logic) |
| **Transcript Format** | ✅ | ❌ | ✅ | **Low** (minor parsing adjustments) |
| **Config System** | ✅ | ❌ | ✅ | **Medium** (adapt `.vibe/` instead of `.claude/`) |
| **Multi-Agent Teams** | ✅ (clide-orchestrated) | ❌ | ❌ | **Medium** (reuse clides MCP broker) |
| **MCP Support** | ✅ | ✅ | ✅ | **Low** |
| **Local Inference** | ❌ | ✅ | ✅ | **Low** (bonus feature) |
---
---
## **🔹 Revised Effort Estimate with Vibe CLI**
| **Task** | **Mistral API** | **Vibe CLI** | **Savings** |
|----------|----------------|--------------|-------------|
| Replace stream-json with Mistral API | 35 days | **12 days** | **23 days** |
| Reimplement permission prompts | 57 days | **0 days** (reuse existing) | **57 days** |
| Reimplement AskUserQuestion | 35 days | **0 days** (reuse existing) | **35 days** |
| Session persistence | 35 days | **0 days** (reuse existing) | **35 days** |
| Update config system | 35 days | **12 days** (adapt `.vibe/`) | **23 days** |
| Update team orchestration | 23 days | **12 days** (reuse MCP broker) | **1 day** |
| Testing & debugging | 57 days | **35 days** | **2 days** |
| **Total** | **34 weeks** | **12 weeks** | **~2 weeks** |
---
---
## **🔹 Updated Recommendations with Vibe CLI**
### **🟢 Option 1: Direct Vibe CLI Integration (Recommended)**
**Approach**: Replace `claude` with `vibe` in clides spawn logic and adapt the existing protocol handlers.
**Complexity**: **Low-Medium (12 weeks)**
**Pros**:
- **Minimal changes** to clides core architecture.
- **Full parity** for **stdio protocol, permissions, sessions, and tools**.
- **Leverages Vibe CLIs native features** (MCP, local inference, config).
**Cons**:
- **Multi-agent teams still require clides MCP broker** (but this is already implemented).
- **Minor adjustments** to `TranscriptReader` and `ClaudeConfig`.
#### **Implementation Steps**:
1. **Update `ClaudeStreamJsonProcess.start()`**:
- Replace `claude` with `vibe` in the spawn command.
- Adjust flags (e.g., `--resume` instead of `--session-id` if needed).
```dart
// Before:
Process.start('claude', ['--input-format', 'stream-json', ...]);
// After:
Process.start('vibe', ['--resume', sessionId, '--stdio', ...]);
```
2. **Adapt `StreamJsonSession`**:
- Update event parsing to handle **Vibe CLIs JSONL format** (likely very similar to Claudes).
- Ensure `can_use_tool` and `AskUserQuestion` are handled the same way.
3. **Update `ClaudeConfig`**:
- Replace `.claude/` with `.vibe/` for config watching.
- Adjust the **slash command probe** to use `vibe --help` or similar.
4. **Update Session Management**:
- Change session storage paths from `~/.claude/` to `~/.vibe/`.
5. **Test Extensively**:
- Validate **all control requests** (permissions, prompts).
- Test **session resumption** and **transcript parsing**.
---
### **🟡 Option 2: Vibe CLI + Custom Wrapper (Fallback)**
**Approach**: Use a **thin wrapper** around Vibe CLI to **normalize its output** to match Claudes stream-json protocol **exactly**.
**Complexity**: **Low (1 week)**
**Pros**:
- **Zero changes to clides core** (only swap `claude` for `vibe-wrapper`).
- **Guarantees 100% protocol compatibility**.
**Cons**:
- **Adds an extra process** (minor latency).
- **Maintenance burden** (wrapper must stay in sync with Vibe CLI updates).
#### **Implementation Steps**:
1. **Create `vibe-wrapper`**:
- Written in **Rust, Go, or Python** (for performance).
- **Input**: Reads Claude-style stream-json from stdin.
- **Output**: Writes Vibe CLI-compatible stdio and translates responses back to Claudes format.
- **Example**:
```bash
# Spawn Vibe CLI via wrapper
vibe-wrapper --session-id <id> --stdio
```
2. **Update clides Spawn Logic**:
- Replace `claude` with `vibe-wrapper` in `ClaudeStreamJsonProcess.start()`.
3. **Test**:
- Ensure **all events and control requests** are translated correctly.
---
### **🔴 Option 3: Mistral API (Not Recommended with Vibe CLI Available)**
**Approach**: Use Mistrals raw REST API (as in the original report).
**Complexity**: **High (34 weeks)**
**Pros**:
- **No dependency on Vibe CLI** (if you prefer raw API control).
**Cons**:
- **Loses stdio protocol, permissions, and sessions** (must reimplement).
- **Higher effort** than Vibe CLI.
---
---
## **🔹 Feature Parity with Vibe CLI**
| **Feature** | **Claude Code CLI** | **Vibe CLI** | **Parity** | **Notes** |
|-------------|---------------------|--------------|------------|-----------|
| **Stdio Protocol** | ✅ | ✅ | **100%** | Vibe CLI supports stdio like Claude. |
| **Streaming Responses** | ✅ | ✅ | **100%** | Both use line-delimited JSON. |
| **Tool Execution** | ✅ | ✅ | **100%** | Vibe CLI supports native and MCP tools. |
| **Permission Prompts** | ✅ | ✅ | **100%** | Both use stdio for `can_use_tool`. |
| **AskUserQuestion** | ✅ | ✅ | **100%** | Both support interactive prompts. |
| **Session Persistence** | ✅ | ✅ | **100%** | Both support `--resume`. |
| **Transcript Format** | ✅ | ✅ | **95%** | Minor differences, easily adaptable. |
| **Config System** | ✅ | ✅ | **90%** | `.vibe/` vs `.claude/`, but similar structure. |
| **Multi-Agent Teams** | ✅ (clide-orchestrated) | ❌ | **80%** | clides MCP broker can orchestrate Vibe CLI agents. |
| **MCP Support** | ✅ | ✅ | **100%** | Both support MCP servers. |
| **Local Inference** | ❌ | ✅ | **Bonus** | Vibe CLI supports local models. |
---
---
## **🔹 Risks & Mitigations with Vibe CLI**
| **Risk** | **Likelihood** | **Impact** | **Mitigation** |
|----------|---------------|------------|----------------|
| **Vibe CLI protocol differences** | Low | Medium | Test thoroughly; adapt `StreamJsonSession` for minor differences. |
| **Vibe CLI updates breaking compatibility** | Medium | Medium | Pin to a specific Vibe CLI version; abstract spawn logic. |
| **Permission prompt differences** | Low | Medium | Validate `can_use_tool` behavior matches Claudes. |
| **Session resumption bugs** | Low | Medium | Test `--resume` with various session states. |
| **MCP tool differences** | Medium | Low | Ensure Vibe CLIs MCP support aligns with clides broker. |
---
---
## **🔹 Final Verdict: Vibe CLI is a Drop-In Replacement**
### **🟢 Why Vibe CLI is a Game-Changer**
1. **Stdio Protocol Parity**:
- Vibe CLI **natively supports stdio-based interaction**, just like Claude Code CLI.
- **No need for a custom wrapper** (unlike Mistral API).
2. **Permission Prompts & AskUserQuestion**:
- Vibe CLI **handles these via stdio**, so clides existing UI can be reused.
3. **Session Persistence**:
- Vibe CLI **supports `--resume`**, so clides session management works out of the box.
4. **Transcript Format**:
- Vibe CLI **uses JSONL**, so `TranscriptReader` can be adapted with minimal changes.
5. **MCP Support**:
- Vibe CLI **supports MCP servers**, so clides team orchestration can work the same way.
6. **Local Inference**:
- **Bonus**: Vibe CLI supports **local models**, enabling offline use.
### **📌 Migration Effort Summary**
| **Approach** | **Complexity** | **Effort** | **Feature Parity** | **Recommended?** |
|--------------|---------------|------------|--------------------|------------------|
| **Vibe CLI (Direct Integration)** | Low-Medium | **12 weeks** | **~95%** | ✅ **Yes (Best Option)** |
| **Vibe CLI + Wrapper** | Low | **1 week** | **100%** | ⚠️ **Only if protocol differences exist** |
| **Mistral API** | High | **34 weeks** | **~70%** | ❌ **Not recommended** |
### **🚀 Recommended Path Forward**
1. **Start with Direct Vibe CLI Integration**:
- Replace `claude` with `vibe` in clides spawn logic.
- Adapt `StreamJsonSession` and `TranscriptReader` for Vibe CLIs output format.
- Update `ClaudeConfig` to watch `.vibe/` instead of `.claude/`.
- **Effort**: **12 weeks**.
- **Outcome**: **Near-100% feature parity** with minimal changes.
2. **Test Thoroughly**:
- Validate **all control requests** (permissions, prompts).
- Test **session resumption** and **multi-agent teams** (via MCP).
3. **Fallback to Wrapper (If Needed)**:
- If **protocol differences** are significant, use a **thin wrapper** to normalize Vibe CLIs output.
- **Effort**: **1 additional week**.
---
---
## **🔹 Conclusion: Vibe CLI Makes Migration Trivial**
**Vibe CLI changes the math dramatically**:
- **Original (Mistral API)**: **34 weeks**, **major reimplementation**, **lost features**.
- **With Vibe CLI**: **12 weeks**, **minimal changes**, **~95% feature parity**.
**Vibe CLI is the closest thing to a drop-in replacement for Claude Code CLI** in clide. The **stdio protocol, permission prompts, session persistence, and MCP support** make it **almost identical** to Claude from clides perspective. The migration would primarily involve:
1. **Swapping `claude` for `vibe`** in spawn commands.
2. **Adapting paths** (`.claude/``.vibe/`).
3. **Minor adjustments** to event parsing and config probing.
**Final Answer**:
**Yes, Vibe CLI changes the math entirely. With Vibe CLI, replacing Claude in clide is a low-effort, high-parity migration (12 weeks). Without Vibe CLI (raw Mistral API), its a high-effort, partial-parity migration (34 weeks). Vibe CLI is the clear winner.**
+5
View File
@@ -142,6 +142,11 @@ You might also want, project-permitting:
- [D-98: Remote-tool contract + connect preflight](decisions/architecture.md#d-98-remote-tool-contract--connect-preflight) — _architecture_ - [D-98: Remote-tool contract + connect preflight](decisions/architecture.md#d-98-remote-tool-contract--connect-preflight) — _architecture_
- [D-99: Remote session identity keyed on (host, workspace)](decisions/architecture.md#d-99-remote-session-identity-keyed-on-host-workspace) — _architecture_ - [D-99: Remote session identity keyed on (host, workspace)](decisions/architecture.md#d-99-remote-session-identity-keyed-on-host-workspace) — _architecture_
- [D-100: Fence `dart:ffi` behind conditional imports + web stubs to keep the web/WASM target compiling](decisions/tooling.md#d-100-fence-dartffi-behind-conditional-imports--web-stubs-to-keep-the-webwasm-target-compiling) — _tooling_ - [D-100: Fence `dart:ffi` behind conditional imports + web stubs to keep the web/WASM target compiling](decisions/tooling.md#d-100-fence-dartffi-behind-conditional-imports--web-stubs-to-keep-the-webwasm-target-compiling) — _tooling_
- [D-101: ClideSettings — one live-preferences access facade](decisions/architecture.md#d-101-clidesettings--one-live-preferences-access-facade) — _architecture_
- [D-102: i18n routing — ext-id namespaces, `core` catalog, ClideSettings.i18n facade, contribution keys](decisions/accessibility.md#d-102-i18n-routing--ext-id-namespaces-core-catalog-clidesettingsi18n-facade-contribution-keys) — _accessibility_
- [D-103: Drawing-card primitive layer is SVG; the SVG renderer is the engine](decisions/architecture.md#d-103-drawing-card-primitive-layer-is-svg-the-svg-renderer-is-the-engine) — _architecture_
- [D-104: Explicit supporter-binary path overrides in user-scope settings](decisions/tooling.md#d-104-explicit-supporter-binary-path-overrides-in-user-scope-settings) — _tooling_
- [D-105: Support Vibe CLI as opt-in alternative to Claude Code CLI](decisions/llm.md#d-105-support-vibe-cli-as-opt-in-alternative-to-claude-code-cli) — _llm_
## Open questions ## Open questions
+11
View File
@@ -32,4 +32,15 @@ A11y + i18n are Tier-0 contracts, not Tier-6 polish.
- **Cost:** Two extra theme files per "named" theme when we add a11y variants. The bundled-theme contrast gate ([D-22](#d-22-wcag-aa-contrast-gate-on-bundled-themes)) needs a baseline/extended split so the named themes don't fail the strict pairs. - **Cost:** Two extra theme files per "named" theme when we add a11y variants. The bundled-theme contrast gate ([D-22](#d-22-wcag-aa-contrast-gate-on-bundled-themes)) needs a baseline/extended split so the named themes don't fail the strict pairs.
- **Raised by:** 2026-05-17 — user intervened mid-T-114 when I had retuned `clide`/`midnight`/`paper`/`terminal` palette entries to satisfy the expanded `canonicalPairs`; reverted, decision written, T-114 will follow this rule. - **Raised by:** 2026-05-17 — user intervened mid-T-114 when I had retuned `clide`/`midnight`/`paper`/`terminal` palette entries to satisfy the expanded `canonicalPairs`; reverted, decision written, T-114 will follow this rule.
### D-102: i18n routing — ext-id namespaces, `core` catalog, ClideSettings.i18n facade, contribution keys
- **Date:** 2026-06-19
- **Decision:** Implements [D-21](#d-21-i18n-is-a-tier-0-contract-fframe-pattern--locale-fallback-chain) across the whole app (epic T-462).
- **Namespaces:** an extension's catalog namespace IS its id (`builtin.<name>`); the ExtensionManager eager-loads it on activation, so a built-in localizes with no hand-maintained registry. Framework chrome outside any extension (`lib/widgets`, `lib/kernel`, the shared reader chrome) resolves under one **`core`** namespace, preloaded at boot.
- **Read path:** widgets resolve through the single [D-101](architecture.md) facade — `ClideSettings.i18n.string(context, key, namespace:, placeholder:)` (+ `.interpolated`) — null-safe (returns the placeholder when no kernel is in scope, so primitives render in isolated tests).
- **Manifest labels:** `CommandContribution` carries `titleKey`/`i18nNamespace`; the palette and menu resolve via a shared `localizedCommandTitle`, and the palette's fuzzy search matches the localized title. The settings schema carries `i18nNamespace`+`titleKey` on the category and `labelKey`/`helpKey`/option `labelKey` beneath it, threaded down by the renderer.
- **Storage:** catalogs are bundled assets at `assets/i18n/<locale>/<namespace>.json` — the locale is a *directory* (`en_us`, future `nl_nl`, `nl_be`, `en_eu`, …), so a new language is a new folder of the same namespace files, no renames.
- **Rationale:** makes a complete translation set (e.g. a Dutch pack) a pure data drop — no code. ext-id namespaces need no registry; the facade keeps one widget-facing read path for theme/fonts/i18n (D-101); the locale-dir layout is cleaner to maintain and mirrors how an external extension ships its own catalog.
- **Cost:** every extension's manifest gains optional key fields, and framework primitives now depend on the (null-safe) facade. The pure-data search matcher (`settingsFieldMatches`) still matches the English label — display localizes, search-by-translation does not (acceptable refinement).
- **Raised by:** 2026-06-19, epic T-462 (i18n everywhere). Builds on [D-101](architecture.md).
--- ---
+17
View File
@@ -534,4 +534,21 @@ Core, rendering, IPC, kernel, panel manager.
- **Cross-reference:** [D-41](#d-41-claude-panes-one-primary-per-repo-tmux-backed), [D-70](#d-70-ipc-socket-path-is-per-workspace-deterministic), [D-77](#d-77-drive-claude-via-the-stream-json-control-protocol-teams-become-a-clide-owned-coordination-layer), [D-93](#d-93-clide-writes-no-directories-of-its-own-into-the-workspace), [D-96](#d-96-remote-execution-footprint--no-install-ssh-exec), [D-97](#d-97-ssh-workspace-uri--system-ssh-auth). Implemented across T-332 (identity carrier) and T-333 (session re-key). - **Cross-reference:** [D-41](#d-41-claude-panes-one-primary-per-repo-tmux-backed), [D-70](#d-70-ipc-socket-path-is-per-workspace-deterministic), [D-77](#d-77-drive-claude-via-the-stream-json-control-protocol-teams-become-a-clide-owned-coordination-layer), [D-93](#d-93-clide-writes-no-directories-of-its-own-into-the-workspace), [D-96](#d-96-remote-execution-footprint--no-install-ssh-exec), [D-97](#d-97-ssh-workspace-uri--system-ssh-auth). Implemented across T-332 (identity carrier) and T-333 (session re-key).
- **Raised by:** 2026-06-12 — T-330 spike artifacts ("session identity keyed on (host, repo) amending D-41/D-77"). - **Raised by:** 2026-06-12 — T-330 spike artifacts ("session identity keyed on (host, repo) amending D-41/D-77").
### D-101: ClideSettings — one live-preferences access facade
- **Date:** 2026-06-17
- **Decision:** Live, user-selectable preferences are read through a single widget-facing facade, `ClideSettings`, namespaced by concern: `ClideSettings.fonts.monoOf(context)` / `.fonts.uiOf(context)`, `ClideSettings.theme.of(context)`, `ClideSettings.i18n.of(context)`. Values originate in the kernel `SettingsStore`; the app root (`root_shell`) resolves them and provides a `ClideSettingsScope` InheritedWidget (carrying the font families), rebuilding it on a settings change so dependents re-read live. Theme and i18n **delegate** to their existing live providers (`ClideTheme` / the `I18n` service) rather than being duplicated — one source of truth. Reads outside a scope fall back to the bundled font defaults, so a widget renders without a provider (isolated tests). "Plumb once, use many."
- **Rationale:** Before this, each live setting had its own ad-hoc read path — theme via `ClideTheme.of`, i18n via `ClideKernel.of(context).i18n`, fonts as a compile-time `const` that couldn't change at runtime at all. A new preference meant inventing another path. One facade gives every current and future preference a uniform, discoverable read site and a single root resolution point — without a mutable global (the shortcut rejected during the T-471 font-flow design) and without a big-bang rewrite of the established theme/i18n providers (they delegate, so their consumers migrate incrementally).
- **Cost:** Two read paths coexist during migration — `ClideTheme.of` / `i18n.string` still work (the facade delegates to them), so their hundreds of call sites move to `ClideSettings.theme` / `.i18n` incrementally rather than at once. The font consts (`clideMonoFamily` / `clideUiFamily`) remain as the facade's defaults; ~11 context-less helper sites still read the const directly, migrated in a follow-up (T-472). The facade lives in `widgets` and reaches into `kernel` for the theme/i18n delegates (a dependency already present).
- **Cross-reference:** Fonts landed it: T-460 (Inter default + UI picker) and T-471 (mono picker) migrated ~93 sites onto `ClideSettings.fonts`. Consumer migration of theme + i18n onto the facade, and the context-less font stragglers (T-472), are staged follow-ups. Values live in the kernel `SettingsStore`.
- **Raised by:** 2026-06-17 — user, during T-471 font-flow design: "I do see reason in nesting them all in one settings object that dynamically loads so we can extend it in the future… plumb once, use many."
### D-103: Drawing-card primitive layer is SVG; the SVG renderer is the engine
- **Date:** 2026-06-28
- **Decision:** The unified drawing card's primitive / scene-graph layer is **SVG**, not a bespoke `{type:"rect",…}` JSON vocabulary. The clide-owned `CustomPaint` SVG renderer (T-320) **is** the card's rendering engine — the foundation the rest builds on, not one template among many. The low-level escape hatch is "send SVG" (`template:"svg"` with inline `source`, or a `.svg` path); the high-level **templates** (image / icon / compare / d2) stay as JSON sugar but **lower to SVG** rather than to a custom primitive scene. A thin **Flutter overlay** renders the clide chrome that is *not* content — the per-object label/description caption widgets and the lightbox affordance — layered over the rendered SVG, anchored to elements via `data-label` / `data-description` / `data-lightbox` attributes. The **graph template is the exception:** it embeds the interactive native graph subsystem ([D-46], T-323) as a live widget rather than lowering to static SVG. Driven via `clide draw --file` (D-6); Flutter-free handler → `draw` MessageBus channel → Claude-extension injection.
- **Rationale:** SVG already *is* a declarative, standard scene-graph with precisely the primitives [D-91] wanted (rect/line/text/image/path at coordinates, transforms). Since a clide-owned SVG renderer is being built for the `svg` template regardless, inventing a parallel primitive JSON + a second renderer duplicates the work for one job. Making SVG the substrate collapses two renderers and two schemas into one, makes external SVG / graphviz / mermaid / d2 output renderable for free, and keeps the `.canvas`-viewer reuse ([D-91]) on the same path. Owning the SVG painter (`CustomPaint`, no package) honors "own the rendering stack" — SVG here is a document *format*, not a third-party renderer, and explicitly **not** the imperative HTML Canvas 2D API ("HTML `<canvas>`" in D-91 was a mental model to reject Obsidian's `.canvas` schema, never an API to port).
- **Cost:** The SVG-subset scope becomes the card's central design surface (the substrate, not one template) — but bounded, since the only SVG that must render is what clide's own templates + d2/graphviz emit; clide controls both ends. Re-sequences epic T-317: the SVG renderer (T-320) lands **before** the core envelope (T-318), inverting the prior `T-320 → T-318` dependency. The card is a hybrid (SVG content + Flutter overlay), so captions/interaction are not expressible in the document SVG itself.
- **Amends [D-91]:** D-91's "raw primitives (rects/lines/text at coordinates)" are now SVG elements; "templates lower onto the same primitive scene" becomes "templates lower to SVG"; the renderer foundation is the SVG painter (T-320), not a separate primitive engine inside T-318 (which becomes the document envelope + template dispatch + the Flutter overlay).
- **Cross-reference:** [D-91](#d-91-unified-conversation-drawing-card-backed-by-a-canvas-renderer), [D-78], [D-46], T-317 (epic), T-318 (envelope/dispatch/overlay), T-320 (SVG engine), T-494 (d2→svg), T-313/T-316/T-319 (templates), T-321/T-323 (graph). Schema: `docs/design/drawing-card-schema.md`.
- **Raised by:** 2026-06-28 — user, during the drawing-card schema draft: "if we are close to svg, are we not better off extending svg instead?" Confirmed the primitive layer should be SVG with templates lowering to it, a Flutter overlay for captions/interaction, and graph as the live-widget exception.
--- ---
+84
View File
@@ -0,0 +1,84 @@
# LLM Integration Decisions
Decisions around Large Language Model integration, driver abstraction, and
multi-LLM support in clide.
---
### D-105: Support Vibe CLI as opt-in alternative to Claude Code CLI
- **Date:** 2026-06-28
- **Status:** confirmed
- **Supersedes:** None
- **See Also:** docs/spikes/vibe-cli-integration-analysis.md, D-6 (CLI/UI parity), D-56 (single process)
- **Raised by:** Mistral Vibe evaluation
#### Decision
**We will support Vibe CLI as an opt-in alternative to Claude Code CLI in clide.**
Vibe CLI provides **stdio protocol, permission prompts, session persistence,
and MCP support** — making it a **~95% feature-parity drop-in replacement** for
Claude Code CLI. This enables users to select their preferred LLM driver on a
per-repo basis while maintaining full backward compatibility with the
existing Claude flow.
#### Context
Claude Code CLI is clide's current and primary LLM. User requested the
ability to switch to Mistral's offerings. Two paths were considered:
1. **Raw Mistral API** — Requires reimplementing stdio protocol, permission
prompts, session management. Effort: 3-4 weeks. Parity: ~70%.
2. **Vibe CLI** — Mistral's open-source CLI that **natively supports** stdio
protocol, permission prompts (`can_use_tool`), session persistence
(`--resume`), MCP servers, and JSONL transcripts. Effort: 1-2 weeks.
Parity: ~95%.
The analysis in `docs/spikes/vibe-cli-integration-analysis.md` confirms Vibe CLI is the
**production-ready path**.
#### Architectural Choice
**Direct Vibe CLI Integration (Option 1 from analysis):**
- Replace `claude` with `vibe` in clide's spawn logic
- Adapt `StreamJsonProcess` and `TranscriptReader` for minor JSONL differences
- Update `ClaudeConfig` to watch `.vibe/` instead of `.claude/`
- Reuse clide's existing MCP broker for team orchestration
**Fallback (Option 2):** Thin wrapper to normalize Vibe CLI output to match
Claude's stream-json exactly. Only if protocol differences prove significant
during testing.
#### Implementation Constraints
- **Claude remains primary** — Vibe CLI is opt-in; default driver stays `claude`
- **Per-repo setting**`llm.driver` in `.clide/config.yaml`, similar to multi-
Claude-account mechanism
- **Zero regression** — All existing Claude functionality preserved
- **Minimal abstraction** — Leverage Vibe CLI's native parity; avoid thick
conversion layers
#### Success Criteria
- User can select `llm.driver: claude | mistral` in per-repo config
- Vibe CLI sessions work end-to-end: streaming, permissions, tools, MCP, session resume
- `make test` passes with both drivers
- No performance regression in Claude flow
- Migration effort: 1-2 weeks
#### Consequences
**Positive:**
- Users can choose their preferred LLM
- Local inference support (offline/private use cases)
- Minimal code changes (~1-2 weeks)
- Full feature parity maintained
**Negative:**
- Additional binary dependency (Vibe CLI)
- Maintenance burden for two drivers (mitigated by abstraction)
- Testing matrix doubles (mitigated by CI feature flags)
---
+1
View File
@@ -31,6 +31,7 @@ Q&D record system itself, kanban, commit conventions, changelog.
- **Rationale:** Python-era clide under `legacy/` used Conventional Commits; the Flutter rebuild does not. Imperative mood reads better for a project-governance log; types are noise when every commit is scoped to a subsystem already. - **Rationale:** Python-era clide under `legacy/` used Conventional Commits; the Flutter rebuild does not. Imperative mood reads better for a project-governance log; types are noise when every commit is scoped to a subsystem already.
- **Cost:** Contributors with Conventional Commits muscle memory adjust. - **Cost:** Contributors with Conventional Commits muscle memory adjust.
- **Raised by:** 2026-04-21 planning. - **Raised by:** 2026-04-21 planning.
- **Amendment (2026-06-17):** Reversed — the rebuild **does** use [Conventional Commits 1.0](https://www.conventionalcommits.org/en/v1.0.0/) after all. Format is `type(scope): imperative subject`, with the standard type set (`feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `chore`); `scope` is the subsystem (`settings`, `vim`, `pty`, `plan`, …); append `!` after the scope for a breaking change; keep a trailing `(T-NNN)` ticket ref where one applies. Subject (prefix included) stays ≤ 72 chars; the no-emoji, body, HEREDOC, and attribution-trailer rules from the original decision are unchanged. Practice had already drifted to this form (`feat(settings): category rail + navigation (T-447)`); the decision now matches it. `.claude/skills/git-commit/SKILL.md`, `CONTRIBUTING.md`, and `POLICY.md` updated to suit. **Why the reversal:** the original "types are noise" call didn't hold up — scoped types make `git log` skimmable and the changelog subsection (Added/Fixed/…) maps cleanly onto the commit type.
### D-38: Changelog discipline — Keep a Changelog 1.1.0 ### D-38: Changelog discipline — Keep a Changelog 1.1.0
- **Date:** 2026-04-21 - **Date:** 2026-04-21

Some files were not shown because too many files have changed in this diff Show More