Compare commits
118
Commits
ad807fb0df
...
v2.1.0
@@ -29,7 +29,8 @@
|
|||||||
"Bash(flutter *)",
|
"Bash(flutter *)",
|
||||||
"Bash(make *)",
|
"Bash(make *)",
|
||||||
"Bash(pql)",
|
"Bash(pql)",
|
||||||
"Bash(pql *)"
|
"Bash(pql *)",
|
||||||
|
"Bash(awk *)"
|
||||||
],
|
],
|
||||||
"deny": [
|
"deny": [
|
||||||
"Bash(rm -rf /*)",
|
"Bash(rm -rf /*)",
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Clide Claude-Code skills
|
||||||
|
|
||||||
|
Skills are reusable instruction packs Claude Code loads on demand. Each one
|
||||||
|
lives in its own directory with a `SKILL.md` (frontmatter + body) and any
|
||||||
|
helper scripts. This index is for humans skimming what's available; Claude
|
||||||
|
discovers skills automatically from the directory structure.
|
||||||
|
|
||||||
|
| Skill | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| [`d2-diagram`](d2-diagram/SKILL.md) | Generate technical diagrams from `.d2` source with the d2 CLI; renders to PNG. |
|
||||||
|
| [`frame0-wireframe`](frame0-wireframe/SKILL.md) | Author UI wireframes as local JSON and sync to the Frame0 desktop app for rendering + export. |
|
||||||
|
| [`git-commit`](git-commit/SKILL.md) | Commit conventions for this repo — message style, CHANGELOG discipline (40/60 word cap), attribution trailer, safety rules. |
|
||||||
|
| [`pql`](pql/SKILL.md) | Query and plan against the markdown vault via the `pql` CLI (decisions, tickets, structural queries). |
|
||||||
|
| [`skill-create`](skill-create/SKILL.md) | Guidance for creating new skills — SKILL.md structure, bundling scripts, packaging. |
|
||||||
|
| [`testmode`](testmode/SKILL.md) | Run and interpret the `ClideTestApp` platform integration harness; smoke-test after toolchain / IPC / theme / native changes. |
|
||||||
|
| [`ui-design`](ui-design/SKILL.md) | Visual design guide — surface tokens, control geometry, Phosphor icons. |
|
||||||
|
| [`whats-next`](whats-next/SKILL.md) | Dependency-driven batch selection against the pql backlog. Walks the initiative/epic tree, filters unblocked work, refines, optionally activates. |
|
||||||
|
|
||||||
|
## Adding a skill
|
||||||
|
|
||||||
|
Use the `skill-create` skill (or follow its SKILL.md by hand). Add a row to
|
||||||
|
the table above so the inventory stays accurate; the index is otherwise just
|
||||||
|
a directory listing.
|
||||||
@@ -53,6 +53,35 @@ This repo follows [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/)
|
|||||||
|
|
||||||
Entries should be short imperative phrases that describe user-facing impact — not implementation detail. "Added sidecar PTY support for terminal pane" beats "Added `internal/pty/session.go`."
|
Entries should be short imperative phrases that describe user-facing impact — not implementation detail. "Added sidecar PTY support for terminal pane" beats "Added `internal/pty/session.go`."
|
||||||
|
|
||||||
|
### Be concise — this is the rule, not a suggestion
|
||||||
|
|
||||||
|
CHANGELOG entries must be **one or two short sentences**. Hard cap: **60 words per bullet** (enforced by `ci/changelog_gate.sh`). Aim for 30 or under; if you can't say it in one line wrapped at ~75 columns, you're writing the wrong document.
|
||||||
|
|
||||||
|
The CHANGELOG is read by humans scanning for what changed between two versions. It is **not** the place for the rationale, the probe results, the implementation detail, the behavior-change deep dive, or the "see also" cross-references. Those belong in:
|
||||||
|
|
||||||
|
- the **commit message body** — explain *why*, list evidence, name the trade-offs;
|
||||||
|
- a **D-record / decision document** — durable architectural rationale;
|
||||||
|
- the **ticket / PR description** — work-context and review notes.
|
||||||
|
|
||||||
|
Hard rules:
|
||||||
|
|
||||||
|
- **No multi-paragraph bullets.** One paragraph max. If you reach for a blank line inside a bullet, stop and split or trim.
|
||||||
|
- **No "Behavior change:" / "Side benefit:" / "Note:" sub-headers inside a bullet.** Those are essay structure; put them in the commit message.
|
||||||
|
- **No probe numbers, latency stats, or %-coverage deltas in entries.** ("hit 95% target" is fine; "0 hangs in 300 spawns vs ~5% before" is commit-body material.)
|
||||||
|
- **No nested function/file lists inside parentheses.** If you find yourself writing `(foo, bar, baz, …)` for more than 3 items, just say "several X" and trust the diff.
|
||||||
|
- **Don't restate the title in the body.** A bullet is its own title.
|
||||||
|
|
||||||
|
Calibration — match the **existing entries** in `CHANGELOG.md`. Open it, look at five recent bullets, write to that length. If your draft is visibly bigger than its neighbors, trim until it isn't.
|
||||||
|
|
||||||
|
Good:
|
||||||
|
> - Mouse wheel scrolling in Claude pane — converts scroll events to PgUp/PgDown so TUI apps scroll naturally.
|
||||||
|
|
||||||
|
Bad (verbose; commit-body material leaked in):
|
||||||
|
> - **PTY spawning switched from `forkpty()` to `posix_openpt()` + `posix_spawn()`** (T-96). `forkpty` calls `fork()` underneath, which is unsafe in the multithreaded Dart VM: about 5% of spawns deadlocked in the child before `execve` because libc locks held by ghost-threads remained "locked forever" in the forked child. `posix_spawn` uses `vfork` (glibc/musl/macOS), keeping the parent suspended until `execve` completes — no Dart code runs in the child. Probed: zero hangs in 300 sequential spawns vs ~5% before. **Behavior change:** missing executable / missing workingDirectory now surface as a `PtyException`…
|
||||||
|
|
||||||
|
Better:
|
||||||
|
> - PTY spawning uses `posix_openpt` + `posix_spawn` instead of `forkpty` — closes a ~5% deadlock window in the multithreaded Dart VM (T-96). Missing exe/cwd now throw `PtyException` at spawn time.
|
||||||
|
|
||||||
**What skips the changelog:** pure bookkeeping commits that have no user-visible effect (typo fix in internal comment, `.gitignore` tweak, lint config change, reformatting). When in doubt, add an entry — the harm of an extra line is zero.
|
**What skips the changelog:** pure bookkeeping commits that have no user-visible effect (typo fix in internal comment, `.gitignore` tweak, lint config change, reformatting). When in doubt, add an entry — the harm of an extra line is zero.
|
||||||
|
|
||||||
When a commit spans multiple entries (e.g. a feature that adds one thing and fixes another), add a line under each applicable subsection rather than cramming both into one.
|
When a commit spans multiple entries (e.g. a feature that adds one thing and fixes another), add a line under each applicable subsection rather than cramming both into one.
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
.pql/changelog/*.sql merge=union
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- run: (cd app && flutter pub get)
|
- run: (cd app && flutter pub get)
|
||||||
- run: ci/test.sh
|
- run: ci/test.sh
|
||||||
- run: ci/test_a11y.sh
|
- run: ci/test_a11y.sh
|
||||||
- run: ci/test_coverage.sh
|
- run: ci/coverage_gate.sh
|
||||||
|
|
||||||
integration:
|
integration:
|
||||||
name: integration_test (xvfb)
|
name: integration_test (xvfb)
|
||||||
|
|||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# pql: source .pql/hooks/post-checkout (rebuild pql.db on branch checkout)
|
||||||
|
. "$(git rev-parse --show-toplevel)/.pql/hooks/post-checkout"
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# pql: source .pql/hooks/post-merge (planning-state auto-import on pull)
|
||||||
|
. "$(git rev-parse --show-toplevel)/.pql/hooks/post-merge"
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# pql: source .pql/hooks/post-rewrite (rebuild pql.db after rebase / amend)
|
||||||
|
. "$(git rev-parse --show-toplevel)/.pql/hooks/post-rewrite"
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# pql: source .pql/hooks/pre-commit (planning-state auto-export)
|
||||||
|
. "$(git rev-parse --show-toplevel)/.pql/hooks/pre-commit"
|
||||||
+7
-7
@@ -26,16 +26,17 @@ tools/ui/test-results/
|
|||||||
tools/ui/playwright-report/
|
tools/ui/playwright-report/
|
||||||
tools/ui/.serve.pid
|
tools/ui/.serve.pid
|
||||||
|
|
||||||
# -- ptyc (C supporter tool) --------------------------------------------
|
|
||||||
/ptyc/bin/
|
|
||||||
/ptyc/*.o
|
|
||||||
|
|
||||||
# -- dugite-native (bundled git, downloaded at build time) ---------------
|
# -- dugite-native (bundled git, downloaded at build time) ---------------
|
||||||
/native/dugite/
|
/native/dugite/
|
||||||
|
|
||||||
# -- dart doc output (generated by `dart doc`, uploaded as CI artefact) -
|
# -- dart doc output (generated by `dart doc`, uploaded as CI artefact) -
|
||||||
/doc/
|
/doc/
|
||||||
|
|
||||||
|
# -- coverage output (regenerated by every `flutter test --coverage`).
|
||||||
|
# Floor lives in pubspec.yaml `coverage_floor:`; nothing under
|
||||||
|
# coverage/ is committed.
|
||||||
|
/coverage/
|
||||||
|
|
||||||
# -- Test, coverage, profile output ------------------------------------
|
# -- Test, coverage, profile output ------------------------------------
|
||||||
*.test
|
*.test
|
||||||
*.out
|
*.out
|
||||||
@@ -51,7 +52,7 @@ test/goldens/failures/
|
|||||||
|
|
||||||
# -- pql per-repo state (index + planning DB, rebuilt from markdown) ----
|
# -- pql per-repo state (index + planning DB, rebuilt from markdown) ----
|
||||||
/.pql/*
|
/.pql/*
|
||||||
!/.pql/pql-plan.json
|
!/.pql/changelog/
|
||||||
|
|
||||||
# -- SQLite index files (defensive; should never land at repo root) ----
|
# -- SQLite index files (defensive; should never land at repo root) ----
|
||||||
*.sqlite
|
*.sqlite
|
||||||
@@ -102,5 +103,4 @@ legacy/**/.coverage.*
|
|||||||
# frame0-wireframe local ID ↔ Frame0 ID mapping; per-machine state.
|
# frame0-wireframe local ID ↔ Frame0 ID mapping; per-machine state.
|
||||||
*.idmap.json
|
*.idmap.json
|
||||||
.pql/*
|
.pql/*
|
||||||
!.pql/pql-plan.json
|
!.pql/changelog/
|
||||||
!.pql/hooks/
|
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
-- Auto-generated by pql init. CREATE TABLE statements
|
||||||
|
-- for the planning schema; per-table dir keeps the changelog
|
||||||
|
-- self-describing per D-15. CREATE TABLE IF NOT EXISTS is
|
||||||
|
-- idempotent so running schema files from each directory in
|
||||||
|
-- replay order is harmless.
|
||||||
|
--
|
||||||
|
-- Importer parses the markers below to detect schema drift
|
||||||
|
-- between the producing pql version and the local one — a
|
||||||
|
-- bumped canonical_version means projection rules changed
|
||||||
|
-- and replay must refuse rather than silently corrupt state.
|
||||||
|
-- pql:created_by: 1.4.26
|
||||||
|
-- pql:canonical_version: 1
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS decisions (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('confirmed','question','rejected')),
|
||||||
|
domain TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'active'
|
||||||
|
CHECK(status IN ('active','superseded','resolved','open')),
|
||||||
|
date TEXT,
|
||||||
|
file_path TEXT NOT NULL,
|
||||||
|
synced_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS decision_refs (
|
||||||
|
source_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||||
|
target_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||||
|
ref_type TEXT NOT NULL
|
||||||
|
CHECK(ref_type IN ('supersedes','references','resolves','depends_on','amends')),
|
||||||
|
note TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (source_id, target_id, ref_type)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tickets (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('initiative','epic','story','task','bug')),
|
||||||
|
parent_id TEXT REFERENCES tickets(id),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
status TEXT NOT NULL DEFAULT 'backlog'
|
||||||
|
CHECK(status IN ('backlog','ready','in_progress','review','done','cancelled')),
|
||||||
|
priority TEXT DEFAULT 'medium'
|
||||||
|
CHECK(priority IN ('critical','high','medium','low')),
|
||||||
|
assigned_to TEXT,
|
||||||
|
team TEXT,
|
||||||
|
decision_ref TEXT REFERENCES decisions(id),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_deps (
|
||||||
|
blocker_id TEXT NOT NULL REFERENCES tickets(id),
|
||||||
|
blocked_id TEXT NOT NULL REFERENCES tickets(id),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (blocker_id, blocked_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_history (
|
||||||
|
ticket_id TEXT NOT NULL REFERENCES tickets(id),
|
||||||
|
field TEXT NOT NULL,
|
||||||
|
old_value TEXT,
|
||||||
|
new_value TEXT,
|
||||||
|
changed_by TEXT,
|
||||||
|
changed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT UNIQUE,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_labels (
|
||||||
|
ticket_id TEXT NOT NULL REFERENCES tickets(id),
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (ticket_id, label)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS meta (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_team ON tickets(team);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_decision_ref ON tickets(decision_ref);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_assigned ON tickets(assigned_to);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decisions_domain ON decisions(domain);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decisions_type ON decisions(type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decision_refs_target ON decision_refs(target_id);
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
-- Auto-generated by pql init. CREATE TABLE statements
|
||||||
|
-- for the planning schema; per-table dir keeps the changelog
|
||||||
|
-- self-describing per D-15. CREATE TABLE IF NOT EXISTS is
|
||||||
|
-- idempotent so running schema files from each directory in
|
||||||
|
-- replay order is harmless.
|
||||||
|
--
|
||||||
|
-- Importer parses the markers below to detect schema drift
|
||||||
|
-- between the producing pql version and the local one — a
|
||||||
|
-- bumped canonical_version means projection rules changed
|
||||||
|
-- and replay must refuse rather than silently corrupt state.
|
||||||
|
-- pql:created_by: 1.4.26
|
||||||
|
-- pql:canonical_version: 1
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS decisions (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('confirmed','question','rejected')),
|
||||||
|
domain TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'active'
|
||||||
|
CHECK(status IN ('active','superseded','resolved','open')),
|
||||||
|
date TEXT,
|
||||||
|
file_path TEXT NOT NULL,
|
||||||
|
synced_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS decision_refs (
|
||||||
|
source_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||||
|
target_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||||
|
ref_type TEXT NOT NULL
|
||||||
|
CHECK(ref_type IN ('supersedes','references','resolves','depends_on','amends')),
|
||||||
|
note TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (source_id, target_id, ref_type)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tickets (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('initiative','epic','story','task','bug')),
|
||||||
|
parent_id TEXT REFERENCES tickets(id),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
status TEXT NOT NULL DEFAULT 'backlog'
|
||||||
|
CHECK(status IN ('backlog','ready','in_progress','review','done','cancelled')),
|
||||||
|
priority TEXT DEFAULT 'medium'
|
||||||
|
CHECK(priority IN ('critical','high','medium','low')),
|
||||||
|
assigned_to TEXT,
|
||||||
|
team TEXT,
|
||||||
|
decision_ref TEXT REFERENCES decisions(id),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_deps (
|
||||||
|
blocker_id TEXT NOT NULL REFERENCES tickets(id),
|
||||||
|
blocked_id TEXT NOT NULL REFERENCES tickets(id),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (blocker_id, blocked_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_history (
|
||||||
|
ticket_id TEXT NOT NULL REFERENCES tickets(id),
|
||||||
|
field TEXT NOT NULL,
|
||||||
|
old_value TEXT,
|
||||||
|
new_value TEXT,
|
||||||
|
changed_by TEXT,
|
||||||
|
changed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT UNIQUE,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_labels (
|
||||||
|
ticket_id TEXT NOT NULL REFERENCES tickets(id),
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (ticket_id, label)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS meta (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_team ON tickets(team);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_decision_ref ON tickets(decision_ref);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_assigned ON tickets(assigned_to);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decisions_domain ON decisions(domain);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decisions_type ON decisions(type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decision_refs_target ON decision_refs(target_id);
|
||||||
@@ -0,0 +1,483 @@
|
|||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'status', 'backlog', 'ready', NULL, '2026-04-22 09:42:11', '2026-04-22 09:42:11', '2026-04-22 09:42:11', NULL, '66d7c49f6b2ccdd2ef88a635ec717ca2', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'status', 'ready', 'in_progress', NULL, '2026-04-22 09:42:11', '2026-04-22 09:42:11', '2026-04-22 09:42:11', NULL, '87c83225076b22ecbc7fd150b8ccce5f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:45:21', '2026-04-22 11:45:21', '2026-04-22 11:45:21', NULL, '056f21749ff40c14feeb380b2425bb04', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'status', 'review', 'done', NULL, '2026-04-22 11:45:21', '2026-04-22 11:45:21', '2026-04-22 11:45:21', NULL, 'a345d1db748f86fb707b715ce4f45c82', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-2', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:12', '2026-04-22 11:46:12', '2026-04-22 11:46:12', NULL, 'd666982c7578a64cff619807467b0d22', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-2', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:16', '2026-04-22 11:46:16', '2026-04-22 11:46:16', NULL, 'bf092de792c1dba0a724e6c3ef0a2ca3', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-2', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:26', '2026-04-22 11:46:26', '2026-04-22 11:46:26', NULL, '6648704125e10ddf9582b0fc8a91cbb8', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-2', 'status', 'review', 'done', NULL, '2026-04-22 11:46:33', '2026-04-22 11:46:33', '2026-04-22 11:46:33', NULL, '28a5fbab4db244d0cc5c48e7e320ec71', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-3', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, '8e8f387c6cf81e8db06c212be3c75ff7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-3', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, 'b0d4dd5c1779606c861d1fd778ee9d0f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-3', 'status', 'review', 'done', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, 'b93dd369ad1260e6b1f74798544bafd1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-3', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, 'e5b9592394cabd32ce426a3916c27827', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-4', 'status', 'review', 'done', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '140ed6257373fb7d199c67da8452bb82', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-4', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '20cb7969c9c3af6a6deb93ee56a3a8f6', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-4', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '456c32193f208cb4dcd78d722c8e5308', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-4', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '92282e57e514c08df603650c82fc5ad1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-5', 'status', 'review', 'done', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, '2352d517e219af8edf4537f4145b5a9e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-5', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, '37b7881534e16c2f369cdfb4a25ac511', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-5', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, '7e4e0f286bd3bf1687cbbd49fca9e1f4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-5', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, 'c6ccbb7da6b303b5fd6ef4f5273c7d07', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-6', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:52', '2026-04-22 11:46:52', '2026-04-22 11:46:52', NULL, '99d159170d35cb6225a0b9e6f8e5a4f5', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '171dab623718a0cb2a4af914c84e0d30', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '206793ce446e6ba302f209cda04dcb78', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '2a311168aab1126eadcb2e23433ba317', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '3098b6064c8b96eb459c5ed947ca97c4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '366f83e624514e442d539f704a0abbb1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '36c5d3bbd5f027fdb114391abdf1dda6', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '381444444f3e04024e2293f61c774d1d', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '46d32785bb5113ebcb050845e67ea280', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '5fde942d02640b9df4e75d1d745b2de5', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '74dbfcfee3f60d46ba6e910ebb5fed1b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '7d19553f3714932803e565e8ac106220', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '9de91c0384a97d66eaa01e7f8164f389', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'c257d281cc9fc54be99fea49688ff720', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'cf47b1a47c543dabd8fe0c891c535285', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'd34d4be4170d93183c18754a60ea97e1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'db0af7f083e8985026799a9d3334347d', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, '1a24e0b9fbc1472aacf17392a9241b99', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, '7774f4c92e18371341b7c0332022d29a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, '8589b12b48657f3513e801cd96dab11c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, '92508b82168bd20d5a69c63a66271c34', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-6', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, 'dc2b5b3b5b348409f6d719f05130804b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '0dd7c9716ba7dab383c7890822ba9224', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '23c19fa354da9eb201212b23696da027', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '79e01d297b5cf559d37f2c0ee3661a51', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '88a0fb2513eebbd4627abd854eefb484', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '9ad3112f08548b5896fd2252dda48be0', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'acba548461502b88a483649f181a65da', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'bd986c8233a7a251c0403c4df3ce8511', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'cfb6a1faab0ce219609f44055648cda1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'dabc0f13943decd855d39febfd2a18ae', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'e340923ef96f70f142a0bee868bb8e67', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'f179c71abcd89f7ed9fa781220d2018c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'fae80bffb40c02fdf07691ef18837b14', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-6', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:54', '2026-04-22 12:03:54', '2026-04-22 12:03:54', NULL, '1693e0a6448fb98bd03fe0c4c65371fb', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-6', 'status', 'review', 'done', NULL, '2026-04-22 12:03:54', '2026-04-22 12:03:54', '2026-04-22 12:03:54', NULL, '80d695ac3bfc5fe1f3615013dea3b06a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'status', 'ready', 'in_progress', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, '33a14771999bc0062268447b4ffadc73', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'status', 'backlog', 'ready', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, '5a2f1c3d37090ad5791c6a0e8a7aa80a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'status', 'ready', 'in_progress', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, '7d84a743a1b9e1ce5c50172b89318153', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'status', 'backlog', 'ready', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, 'cad02006950fb6fa0d003553437d3bbe', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'status', 'in_progress', 'review', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '10f0a16fa4e4899c7817dd5c44cc3ffe', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'status', 'in_progress', 'review', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '2d697a30a6193eacf2a3ddf1661e861c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'status', 'review', 'done', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '591bb27186744d023df34105884fa58f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'status', 'review', 'done', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '817b9d8e92707eb588cb7f9eae7d424e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-22', 'status', 'backlog', 'cancelled', NULL, '2026-04-22 20:34:17', '2026-04-22 20:34:17', '2026-04-22 20:34:17', NULL, '5460d369f6cdef9c393a37e41481bcb5', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-30', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, '313d4c9dd6444a4573cbe9584ed18f61', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-31', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, '4630269c56784316b975ca56b10a2337', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-34', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, '5a8f7e0eda55bc0893c6597400a8d535', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-33', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, 'fac6b818dc68490a80ea32255f0ba404', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-31', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, '0a7137f82fbb9307496b7e4133b15052', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-33', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, '5aff1ed40d9a68a5b4d59aab0d24dd31', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-34', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, '931bcaabbd64af0612fbe6eb9a4470d6', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-30', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, 'c2fca20d28b2a3a63a56eb2841a4bda9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-31', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, '84e5b99f4fb99f8ae4d189ddaac2de74', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-34', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, '8c7ba0fd4c8acc6065a24b80ae28d526', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-30', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, 'd91c6440f66e683a87be5e61a366cca7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-33', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, 'f9253848d9a95ce1823c8ba78a511fc0', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-35', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:56:58', '2026-04-22 20:56:58', '2026-04-22 20:56:58', NULL, '389d5abb35b3b73ccb38ac8bec3b92c7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-35', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:56:58', '2026-04-22 20:56:58', '2026-04-22 20:56:58', NULL, '7e714e95f89966a13e436d2fe38e7443', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-35', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:56:58', '2026-04-22 20:56:58', '2026-04-22 20:56:58', NULL, 'b1aa8ff1f9b24cdfc9e0bebf21d38b99', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-32', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:02:29', '2026-04-22 21:02:29', '2026-04-22 21:02:29', NULL, '39b407be88dbb31f530c8074f6bcecc7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-32', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:02:29', '2026-04-22 21:02:29', '2026-04-22 21:02:29', NULL, '59df9c23d722ccf1c1ac18baee76c785', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-32', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:02:29', '2026-04-22 21:02:29', '2026-04-22 21:02:29', NULL, '76b48a80abcba29b1047c794fd004234', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-37', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '297763c026010189ea0bd870f77a173e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-37', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '2cd947436be1c156eb77d233aeed5da2', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-38', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '38826870354a1bf51bc2ff21f6da0ea3', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-37', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '5105038efff2e3055213b1b1403b7610', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-39', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '67cc6c0934711b6a592e11e5edc6194c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-39', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '6e9e7d3ce9f74ed261912b1cb099dcad', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-39', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, 'a4e702776f3b553fb276782e539ebfc7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-38', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, 'd294248ba8ea651e78aeb6fed0457eb4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-38', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, 'da8dc2002a03eaee7fcd5504abee6f60', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '08f09f3bb9f1a0001110716930824e62', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-29', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '10005b4803d743aa21be0223c8a85d5c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '11ac23b546d0e22206af4221062f5912', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-21', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '127404fe79dfb79d0297b85d78235c68', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-23', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '15fb6606af61e11b6960799721e7cdcc', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-38', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '19f6209ce4428172fb16941accb13566', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-27', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '212224b7dcc4b991119dc430ae179311', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '2fe8eed15b3ead999a69cdd08df5e14c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '3990623d651c604fec692edb70976d69', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-28', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '44d273b92ad4c93159706c1f17fcb6a7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '46935c5a6d8ef12cf1c2252bd8d30368', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-37', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '551ea05a8d0cc28c0ff7010a139e6fe1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '6748fca610fc8a8503d69846231616fe', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '7843cfda82ec1c1622c1a201f82acaf1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '822790bbace926feb2372b8665080932', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-22', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '9a1ae5742a1230e60279d9c72f8b0f75', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-42', 'parent_id', NULL, 'T-5', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'a414bc8e4d32fa32483228a4e9c7be8c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-24', 'parent_id', NULL, 'T-3', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'a80caae1913b8e9a1d5dab980bf0b77b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-40', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'a895d51a2e6e8b448684933cf0224162', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-43', 'parent_id', NULL, 'T-3', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'ad9a8a85686e33df17ae15631afa1213', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'aedb3c83402245f81cd10f04d15dcf8e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'b6dbb8114cd77c206d3b8bdf054736b9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-26', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'b7b189fe87f61d42e1d0318d23aeb1fe', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'c80049d2c236f0d2512355183b766f0f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-25', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'ccdf7bec2a75f5829eab4fdd57171622', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-36', 'parent_id', NULL, 'T-7', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'd13ed78c92d33e61e448c9e155adf870', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-41', 'parent_id', NULL, 'T-7', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'eb2a2ca41a57604b40556e81bf6480a9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-35', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'f0d27e423d40a998115e10ffd804f791', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-39', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'f5c891c9a5f7d5e3f43ded0dc07eb704', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'ff609c22f964618c84b7c36988e87ae7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:01:08', '2026-04-24 07:01:08', '2026-04-24 07:01:08', NULL, '6ae864936d48565e880f06e984e9d978', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:01:08', '2026-04-24 07:01:08', '2026-04-24 07:01:08', NULL, 'ebebc38301a28001a09c9e7066c9aeb9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:01:48', '2026-04-24 07:01:48', '2026-04-24 07:01:48', NULL, '3cf128b79fe91447f72f42d8cffce9e1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:01:48', '2026-04-24 07:01:48', '2026-04-24 07:01:48', NULL, '68f57f5e3a1005ca3009f944b8b634d7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:05:40', '2026-04-24 07:05:40', '2026-04-24 07:05:40', NULL, '50aa3e362dbe9682d98f6d5f71ea97b1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:05:50', '2026-04-24 07:05:50', '2026-04-24 07:05:50', NULL, '4cb951c3ea697881d8fa5b0e86cfdf96', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:05:51', '2026-04-24 07:05:51', '2026-04-24 07:05:51', NULL, '7516ffa5c72fa4e7fd067249393c3653', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:06:30', '2026-04-24 07:06:30', '2026-04-24 07:06:30', NULL, '268576bd9559485dd2ac5e7ffd1f1a79', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:06:32', '2026-04-24 07:06:32', '2026-04-24 07:06:32', NULL, 'ef46c3a3db4c4ba50ff1f0e1baa4c0e6', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'review', 'done', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, '1cd74d47eb547aa0bef71597eaaa2c5b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'ready', 'in_progress', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, '3b8c17d366d1710d03523303c9df83c8', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'in_progress', 'review', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, '7eb9ca56292802301f93d0e2d680835e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, 'a1a837494c047ade786d79edb75296dd', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'done', 'in_progress', NULL, '2026-04-24 07:08:00', '2026-04-24 07:08:00', '2026-04-24 07:08:00', NULL, 'ac745b5254a0e8f085189c23d95acb7e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'in_progress', 'done', NULL, '2026-04-24 07:08:29', '2026-04-24 07:08:29', '2026-04-24 07:08:29', NULL, '2adaf942a49bfeeac90aee1527a98444', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'done', 'in_progress', NULL, '2026-04-24 07:08:29', '2026-04-24 07:08:29', '2026-04-24 07:08:29', NULL, 'e0d54ffba34677dc6e687be801ccec05', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'in_progress', 'ready', NULL, '2026-04-24 07:08:35', '2026-04-24 07:08:35', '2026-04-24 07:08:35', NULL, '74e6fa2f524c21300467381c2f3ad4e3', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:08:35', '2026-04-24 07:08:35', '2026-04-24 07:08:35', NULL, 'fbd19eabf442e6e0de60028c28956b8f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, '7d3efe4ccaee7d45d1a01d28144c4f0a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'ready', 'in_progress', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, '914a2f5f5ef0807a6743f8811e71804b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'in_progress', 'ready', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, 'a1cd74c76fd10c353aa758cea0331c5c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'cancelled', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, 'cdbe85f80838e781048780db934d0115', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'cancelled', 'backlog', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, 'f24e8d138172597d7ddcf26672b67d5f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, 'f9a83fa1b41d56202c57e6a55b240f35', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'done', NULL, '2026-04-24 07:30:15', '2026-04-24 07:30:15', '2026-04-24 07:30:15', NULL, '99c4e5a68463201e7f1b7764eddfd2ae', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'done', 'backlog', NULL, '2026-04-24 07:30:22', '2026-04-24 07:30:22', '2026-04-24 07:30:22', NULL, 'd0274058b130d8003123881b8a410f7f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 08:08:16', '2026-04-24 08:08:16', '2026-04-24 08:08:16', NULL, '83d70eb5a7f6e7175e0719fdb23243c2', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'backlog', NULL, '2026-04-24 08:08:16', '2026-04-24 08:08:16', '2026-04-24 08:08:16', NULL, 'b5acf95cb059f0c573b78d755b938da2', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'in_progress', NULL, '2026-04-24 08:08:23', '2026-04-24 08:08:23', '2026-04-24 08:08:23', NULL, '4ae08b8af459af0679b05cf1f8ac3950', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'in_progress', 'ready', NULL, '2026-04-24 08:08:38', '2026-04-24 08:08:38', '2026-04-24 08:08:38', NULL, 'c7e4cabf5d1fccad28f22d498e91193a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'in_progress', NULL, '2026-04-24 08:08:40', '2026-04-24 08:08:40', '2026-04-24 08:08:40', NULL, '9b0937bc7eb82785407c367a90365c3a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'in_progress', 'review', NULL, '2026-04-24 08:08:44', '2026-04-24 08:08:44', '2026-04-24 08:08:44', NULL, '90587f61d96b12ce53f2a4edc705fdd8', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'review', 'in_progress', NULL, '2026-04-24 08:08:48', '2026-04-24 08:08:48', '2026-04-24 08:08:48', NULL, 'd02ac7ee61fc4dc420fb7597a4f326db', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'in_progress', 'ready', NULL, '2026-04-24 08:16:15', '2026-04-24 08:16:15', '2026-04-24 08:16:15', NULL, '145613c94b4366bda03fb76c0f0747ce', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'backlog', NULL, '2026-04-24 08:16:19', '2026-04-24 08:16:19', '2026-04-24 08:16:19', NULL, '0d8e318e8060aece6e18788cee56f646', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 08:16:21', '2026-04-24 08:16:21', '2026-04-24 08:16:21', NULL, '3d8635cfdbaf9b0c22dcba9f15f3c1ad', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'in_progress', NULL, '2026-04-24 08:16:47', '2026-04-24 08:16:47', '2026-04-24 08:16:47', NULL, '77f873a7a5fff47ce9b974a6999d03b4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-28', 'status', 'backlog', 'in_progress', NULL, '2026-04-24 08:16:56', '2026-04-24 08:16:56', '2026-04-24 08:16:56', NULL, 'e7610d68ec9871746b4a361e33b18f05', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-43', 'status', 'backlog', 'ready', NULL, '2026-04-24 08:17:11', '2026-04-24 08:17:11', '2026-04-24 08:17:11', NULL, '9c3afbbfca207aeb78541dbd9d90e462', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'status', 'backlog', 'ready', NULL, '2026-04-22 09:42:11', '2026-04-22 09:42:11', '2026-04-22 09:42:11', NULL, '66d7c49f6b2ccdd2ef88a635ec717ca2', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'status', 'ready', 'in_progress', NULL, '2026-04-22 09:42:11', '2026-04-22 09:42:11', '2026-04-22 09:42:11', NULL, '87c83225076b22ecbc7fd150b8ccce5f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:45:21', '2026-04-22 11:45:21', '2026-04-22 11:45:21', NULL, '056f21749ff40c14feeb380b2425bb04', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'status', 'review', 'done', NULL, '2026-04-22 11:45:21', '2026-04-22 11:45:21', '2026-04-22 11:45:21', NULL, 'a345d1db748f86fb707b715ce4f45c82', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-2', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:12', '2026-04-22 11:46:12', '2026-04-22 11:46:12', NULL, 'd666982c7578a64cff619807467b0d22', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-2', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:16', '2026-04-22 11:46:16', '2026-04-22 11:46:16', NULL, 'bf092de792c1dba0a724e6c3ef0a2ca3', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-2', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:26', '2026-04-22 11:46:26', '2026-04-22 11:46:26', NULL, '6648704125e10ddf9582b0fc8a91cbb8', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-2', 'status', 'review', 'done', NULL, '2026-04-22 11:46:33', '2026-04-22 11:46:33', '2026-04-22 11:46:33', NULL, '28a5fbab4db244d0cc5c48e7e320ec71', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-3', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, '8e8f387c6cf81e8db06c212be3c75ff7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-3', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, 'b0d4dd5c1779606c861d1fd778ee9d0f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-3', 'status', 'review', 'done', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, 'b93dd369ad1260e6b1f74798544bafd1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-3', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, 'e5b9592394cabd32ce426a3916c27827', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-4', 'status', 'review', 'done', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '140ed6257373fb7d199c67da8452bb82', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-4', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '20cb7969c9c3af6a6deb93ee56a3a8f6', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-4', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '456c32193f208cb4dcd78d722c8e5308', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-4', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '92282e57e514c08df603650c82fc5ad1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-5', 'status', 'review', 'done', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, '2352d517e219af8edf4537f4145b5a9e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-5', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, '37b7881534e16c2f369cdfb4a25ac511', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-5', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, '7e4e0f286bd3bf1687cbbd49fca9e1f4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-5', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, 'c6ccbb7da6b303b5fd6ef4f5273c7d07', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-6', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:52', '2026-04-22 11:46:52', '2026-04-22 11:46:52', NULL, '99d159170d35cb6225a0b9e6f8e5a4f5', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '171dab623718a0cb2a4af914c84e0d30', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '206793ce446e6ba302f209cda04dcb78', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '2a311168aab1126eadcb2e23433ba317', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '3098b6064c8b96eb459c5ed947ca97c4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '366f83e624514e442d539f704a0abbb1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '36c5d3bbd5f027fdb114391abdf1dda6', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '381444444f3e04024e2293f61c774d1d', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '46d32785bb5113ebcb050845e67ea280', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '5fde942d02640b9df4e75d1d745b2de5', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '74dbfcfee3f60d46ba6e910ebb5fed1b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '7d19553f3714932803e565e8ac106220', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '9de91c0384a97d66eaa01e7f8164f389', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'c257d281cc9fc54be99fea49688ff720', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'cf47b1a47c543dabd8fe0c891c535285', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'd34d4be4170d93183c18754a60ea97e1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'db0af7f083e8985026799a9d3334347d', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, '1a24e0b9fbc1472aacf17392a9241b99', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, '7774f4c92e18371341b7c0332022d29a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, '8589b12b48657f3513e801cd96dab11c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, '92508b82168bd20d5a69c63a66271c34', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-6', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, 'dc2b5b3b5b348409f6d719f05130804b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '0dd7c9716ba7dab383c7890822ba9224', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '23c19fa354da9eb201212b23696da027', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '79e01d297b5cf559d37f2c0ee3661a51', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '88a0fb2513eebbd4627abd854eefb484', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '9ad3112f08548b5896fd2252dda48be0', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'acba548461502b88a483649f181a65da', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'bd986c8233a7a251c0403c4df3ce8511', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'cfb6a1faab0ce219609f44055648cda1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'dabc0f13943decd855d39febfd2a18ae', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'e340923ef96f70f142a0bee868bb8e67', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'f179c71abcd89f7ed9fa781220d2018c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'fae80bffb40c02fdf07691ef18837b14', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-6', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:54', '2026-04-22 12:03:54', '2026-04-22 12:03:54', NULL, '1693e0a6448fb98bd03fe0c4c65371fb', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-6', 'status', 'review', 'done', NULL, '2026-04-22 12:03:54', '2026-04-22 12:03:54', '2026-04-22 12:03:54', NULL, '80d695ac3bfc5fe1f3615013dea3b06a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'status', 'ready', 'in_progress', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, '33a14771999bc0062268447b4ffadc73', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'status', 'backlog', 'ready', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, '5a2f1c3d37090ad5791c6a0e8a7aa80a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'status', 'ready', 'in_progress', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, '7d84a743a1b9e1ce5c50172b89318153', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'status', 'backlog', 'ready', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, 'cad02006950fb6fa0d003553437d3bbe', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'status', 'in_progress', 'review', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '10f0a16fa4e4899c7817dd5c44cc3ffe', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'status', 'in_progress', 'review', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '2d697a30a6193eacf2a3ddf1661e861c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'status', 'review', 'done', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '591bb27186744d023df34105884fa58f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'status', 'review', 'done', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '817b9d8e92707eb588cb7f9eae7d424e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-22', 'status', 'backlog', 'cancelled', NULL, '2026-04-22 20:34:17', '2026-04-22 20:34:17', '2026-04-22 20:34:17', NULL, '5460d369f6cdef9c393a37e41481bcb5', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-30', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, '313d4c9dd6444a4573cbe9584ed18f61', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-31', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, '4630269c56784316b975ca56b10a2337', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-34', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, '5a8f7e0eda55bc0893c6597400a8d535', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-33', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, 'fac6b818dc68490a80ea32255f0ba404', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-31', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, '0a7137f82fbb9307496b7e4133b15052', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-33', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, '5aff1ed40d9a68a5b4d59aab0d24dd31', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-34', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, '931bcaabbd64af0612fbe6eb9a4470d6', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-30', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, 'c2fca20d28b2a3a63a56eb2841a4bda9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-31', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, '84e5b99f4fb99f8ae4d189ddaac2de74', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-34', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, '8c7ba0fd4c8acc6065a24b80ae28d526', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-30', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, 'd91c6440f66e683a87be5e61a366cca7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-33', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, 'f9253848d9a95ce1823c8ba78a511fc0', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-35', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:56:58', '2026-04-22 20:56:58', '2026-04-22 20:56:58', NULL, '389d5abb35b3b73ccb38ac8bec3b92c7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-35', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:56:58', '2026-04-22 20:56:58', '2026-04-22 20:56:58', NULL, '7e714e95f89966a13e436d2fe38e7443', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-35', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:56:58', '2026-04-22 20:56:58', '2026-04-22 20:56:58', NULL, 'b1aa8ff1f9b24cdfc9e0bebf21d38b99', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-32', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:02:29', '2026-04-22 21:02:29', '2026-04-22 21:02:29', NULL, '39b407be88dbb31f530c8074f6bcecc7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-32', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:02:29', '2026-04-22 21:02:29', '2026-04-22 21:02:29', NULL, '59df9c23d722ccf1c1ac18baee76c785', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-32', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:02:29', '2026-04-22 21:02:29', '2026-04-22 21:02:29', NULL, '76b48a80abcba29b1047c794fd004234', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-37', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '297763c026010189ea0bd870f77a173e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-37', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '2cd947436be1c156eb77d233aeed5da2', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-38', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '38826870354a1bf51bc2ff21f6da0ea3', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-37', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '5105038efff2e3055213b1b1403b7610', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-39', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '67cc6c0934711b6a592e11e5edc6194c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-39', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '6e9e7d3ce9f74ed261912b1cb099dcad', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-39', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, 'a4e702776f3b553fb276782e539ebfc7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-38', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, 'd294248ba8ea651e78aeb6fed0457eb4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-38', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, 'da8dc2002a03eaee7fcd5504abee6f60', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '08f09f3bb9f1a0001110716930824e62', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-29', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '10005b4803d743aa21be0223c8a85d5c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '11ac23b546d0e22206af4221062f5912', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-21', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '127404fe79dfb79d0297b85d78235c68', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-23', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '15fb6606af61e11b6960799721e7cdcc', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-38', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '19f6209ce4428172fb16941accb13566', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-27', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '212224b7dcc4b991119dc430ae179311', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '2fe8eed15b3ead999a69cdd08df5e14c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '3990623d651c604fec692edb70976d69', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-28', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '44d273b92ad4c93159706c1f17fcb6a7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '46935c5a6d8ef12cf1c2252bd8d30368', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-37', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '551ea05a8d0cc28c0ff7010a139e6fe1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '6748fca610fc8a8503d69846231616fe', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '7843cfda82ec1c1622c1a201f82acaf1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '822790bbace926feb2372b8665080932', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-22', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '9a1ae5742a1230e60279d9c72f8b0f75', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-42', 'parent_id', NULL, 'T-5', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'a414bc8e4d32fa32483228a4e9c7be8c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-24', 'parent_id', NULL, 'T-3', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'a80caae1913b8e9a1d5dab980bf0b77b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-40', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'a895d51a2e6e8b448684933cf0224162', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-43', 'parent_id', NULL, 'T-3', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'ad9a8a85686e33df17ae15631afa1213', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'aedb3c83402245f81cd10f04d15dcf8e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'b6dbb8114cd77c206d3b8bdf054736b9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-26', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'b7b189fe87f61d42e1d0318d23aeb1fe', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'c80049d2c236f0d2512355183b766f0f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-25', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'ccdf7bec2a75f5829eab4fdd57171622', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-36', 'parent_id', NULL, 'T-7', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'd13ed78c92d33e61e448c9e155adf870', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-41', 'parent_id', NULL, 'T-7', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'eb2a2ca41a57604b40556e81bf6480a9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-35', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'f0d27e423d40a998115e10ffd804f791', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-39', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'f5c891c9a5f7d5e3f43ded0dc07eb704', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'ff609c22f964618c84b7c36988e87ae7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:01:08', '2026-04-24 07:01:08', '2026-04-24 07:01:08', NULL, '6ae864936d48565e880f06e984e9d978', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:01:08', '2026-04-24 07:01:08', '2026-04-24 07:01:08', NULL, 'ebebc38301a28001a09c9e7066c9aeb9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:01:48', '2026-04-24 07:01:48', '2026-04-24 07:01:48', NULL, '3cf128b79fe91447f72f42d8cffce9e1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:01:48', '2026-04-24 07:01:48', '2026-04-24 07:01:48', NULL, '68f57f5e3a1005ca3009f944b8b634d7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:05:40', '2026-04-24 07:05:40', '2026-04-24 07:05:40', NULL, '50aa3e362dbe9682d98f6d5f71ea97b1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:05:50', '2026-04-24 07:05:50', '2026-04-24 07:05:50', NULL, '4cb951c3ea697881d8fa5b0e86cfdf96', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:05:51', '2026-04-24 07:05:51', '2026-04-24 07:05:51', NULL, '7516ffa5c72fa4e7fd067249393c3653', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:06:30', '2026-04-24 07:06:30', '2026-04-24 07:06:30', NULL, '268576bd9559485dd2ac5e7ffd1f1a79', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:06:32', '2026-04-24 07:06:32', '2026-04-24 07:06:32', NULL, 'ef46c3a3db4c4ba50ff1f0e1baa4c0e6', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'review', 'done', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, '1cd74d47eb547aa0bef71597eaaa2c5b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'ready', 'in_progress', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, '3b8c17d366d1710d03523303c9df83c8', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'in_progress', 'review', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, '7eb9ca56292802301f93d0e2d680835e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, 'a1a837494c047ade786d79edb75296dd', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'done', 'in_progress', NULL, '2026-04-24 07:08:00', '2026-04-24 07:08:00', '2026-04-24 07:08:00', NULL, 'ac745b5254a0e8f085189c23d95acb7e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'in_progress', 'done', NULL, '2026-04-24 07:08:29', '2026-04-24 07:08:29', '2026-04-24 07:08:29', NULL, '2adaf942a49bfeeac90aee1527a98444', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'done', 'in_progress', NULL, '2026-04-24 07:08:29', '2026-04-24 07:08:29', '2026-04-24 07:08:29', NULL, 'e0d54ffba34677dc6e687be801ccec05', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'in_progress', 'ready', NULL, '2026-04-24 07:08:35', '2026-04-24 07:08:35', '2026-04-24 07:08:35', NULL, '74e6fa2f524c21300467381c2f3ad4e3', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:08:35', '2026-04-24 07:08:35', '2026-04-24 07:08:35', NULL, 'fbd19eabf442e6e0de60028c28956b8f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, '7d3efe4ccaee7d45d1a01d28144c4f0a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'ready', 'in_progress', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, '914a2f5f5ef0807a6743f8811e71804b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'in_progress', 'ready', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, 'a1cd74c76fd10c353aa758cea0331c5c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'cancelled', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, 'cdbe85f80838e781048780db934d0115', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'cancelled', 'backlog', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, 'f24e8d138172597d7ddcf26672b67d5f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, 'f9a83fa1b41d56202c57e6a55b240f35', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'done', NULL, '2026-04-24 07:30:15', '2026-04-24 07:30:15', '2026-04-24 07:30:15', NULL, '99c4e5a68463201e7f1b7764eddfd2ae', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'done', 'backlog', NULL, '2026-04-24 07:30:22', '2026-04-24 07:30:22', '2026-04-24 07:30:22', NULL, 'd0274058b130d8003123881b8a410f7f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 08:08:16', '2026-04-24 08:08:16', '2026-04-24 08:08:16', NULL, '83d70eb5a7f6e7175e0719fdb23243c2', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'backlog', NULL, '2026-04-24 08:08:16', '2026-04-24 08:08:16', '2026-04-24 08:08:16', NULL, 'b5acf95cb059f0c573b78d755b938da2', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'in_progress', NULL, '2026-04-24 08:08:23', '2026-04-24 08:08:23', '2026-04-24 08:08:23', NULL, '4ae08b8af459af0679b05cf1f8ac3950', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'in_progress', 'ready', NULL, '2026-04-24 08:08:38', '2026-04-24 08:08:38', '2026-04-24 08:08:38', NULL, 'c7e4cabf5d1fccad28f22d498e91193a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'in_progress', NULL, '2026-04-24 08:08:40', '2026-04-24 08:08:40', '2026-04-24 08:08:40', NULL, '9b0937bc7eb82785407c367a90365c3a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'in_progress', 'review', NULL, '2026-04-24 08:08:44', '2026-04-24 08:08:44', '2026-04-24 08:08:44', NULL, '90587f61d96b12ce53f2a4edc705fdd8', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'review', 'in_progress', NULL, '2026-04-24 08:08:48', '2026-04-24 08:08:48', '2026-04-24 08:08:48', NULL, 'd02ac7ee61fc4dc420fb7597a4f326db', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'in_progress', 'ready', NULL, '2026-04-24 08:16:15', '2026-04-24 08:16:15', '2026-04-24 08:16:15', NULL, '145613c94b4366bda03fb76c0f0747ce', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'backlog', NULL, '2026-04-24 08:16:19', '2026-04-24 08:16:19', '2026-04-24 08:16:19', NULL, '0d8e318e8060aece6e18788cee56f646', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 08:16:21', '2026-04-24 08:16:21', '2026-04-24 08:16:21', NULL, '3d8635cfdbaf9b0c22dcba9f15f3c1ad', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'in_progress', NULL, '2026-04-24 08:16:47', '2026-04-24 08:16:47', '2026-04-24 08:16:47', NULL, '77f873a7a5fff47ce9b974a6999d03b4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-28', 'status', 'backlog', 'in_progress', NULL, '2026-04-24 08:16:56', '2026-04-24 08:16:56', '2026-04-24 08:16:56', NULL, 'e7610d68ec9871746b4a361e33b18f05', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-43', 'status', 'backlog', 'ready', NULL, '2026-04-24 08:17:11', '2026-04-24 08:17:11', '2026-04-24 08:17:11', NULL, '9c3afbbfca207aeb78541dbd9d90e462', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'status', 'backlog', 'ready', NULL, '2026-04-22 09:42:11', '2026-04-22 09:42:11', '2026-04-22 09:42:11', NULL, '66d7c49f6b2ccdd2ef88a635ec717ca2', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'status', 'ready', 'in_progress', NULL, '2026-04-22 09:42:11', '2026-04-22 09:42:11', '2026-04-22 09:42:11', NULL, '87c83225076b22ecbc7fd150b8ccce5f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:45:21', '2026-04-22 11:45:21', '2026-04-22 11:45:21', NULL, '056f21749ff40c14feeb380b2425bb04', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'status', 'review', 'done', NULL, '2026-04-22 11:45:21', '2026-04-22 11:45:21', '2026-04-22 11:45:21', NULL, 'a345d1db748f86fb707b715ce4f45c82', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-2', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:12', '2026-04-22 11:46:12', '2026-04-22 11:46:12', NULL, 'd666982c7578a64cff619807467b0d22', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-2', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:16', '2026-04-22 11:46:16', '2026-04-22 11:46:16', NULL, 'bf092de792c1dba0a724e6c3ef0a2ca3', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-2', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:26', '2026-04-22 11:46:26', '2026-04-22 11:46:26', NULL, '6648704125e10ddf9582b0fc8a91cbb8', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-2', 'status', 'review', 'done', NULL, '2026-04-22 11:46:33', '2026-04-22 11:46:33', '2026-04-22 11:46:33', NULL, '28a5fbab4db244d0cc5c48e7e320ec71', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-3', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, '8e8f387c6cf81e8db06c212be3c75ff7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-3', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, 'b0d4dd5c1779606c861d1fd778ee9d0f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-3', 'status', 'review', 'done', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, 'b93dd369ad1260e6b1f74798544bafd1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-3', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, 'e5b9592394cabd32ce426a3916c27827', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-4', 'status', 'review', 'done', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '140ed6257373fb7d199c67da8452bb82', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-4', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '20cb7969c9c3af6a6deb93ee56a3a8f6', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-4', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '456c32193f208cb4dcd78d722c8e5308', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-4', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '92282e57e514c08df603650c82fc5ad1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-5', 'status', 'review', 'done', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, '2352d517e219af8edf4537f4145b5a9e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-5', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, '37b7881534e16c2f369cdfb4a25ac511', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-5', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, '7e4e0f286bd3bf1687cbbd49fca9e1f4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-5', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, 'c6ccbb7da6b303b5fd6ef4f5273c7d07', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-6', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:52', '2026-04-22 11:46:52', '2026-04-22 11:46:52', NULL, '99d159170d35cb6225a0b9e6f8e5a4f5', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '171dab623718a0cb2a4af914c84e0d30', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '206793ce446e6ba302f209cda04dcb78', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '2a311168aab1126eadcb2e23433ba317', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '3098b6064c8b96eb459c5ed947ca97c4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '366f83e624514e442d539f704a0abbb1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '36c5d3bbd5f027fdb114391abdf1dda6', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '381444444f3e04024e2293f61c774d1d', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '46d32785bb5113ebcb050845e67ea280', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '5fde942d02640b9df4e75d1d745b2de5', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '74dbfcfee3f60d46ba6e910ebb5fed1b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '7d19553f3714932803e565e8ac106220', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '9de91c0384a97d66eaa01e7f8164f389', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'c257d281cc9fc54be99fea49688ff720', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'cf47b1a47c543dabd8fe0c891c535285', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'd34d4be4170d93183c18754a60ea97e1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'db0af7f083e8985026799a9d3334347d', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, '1a24e0b9fbc1472aacf17392a9241b99', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, '7774f4c92e18371341b7c0332022d29a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, '8589b12b48657f3513e801cd96dab11c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, '92508b82168bd20d5a69c63a66271c34', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-6', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, 'dc2b5b3b5b348409f6d719f05130804b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '0dd7c9716ba7dab383c7890822ba9224', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '23c19fa354da9eb201212b23696da027', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '79e01d297b5cf559d37f2c0ee3661a51', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '88a0fb2513eebbd4627abd854eefb484', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '9ad3112f08548b5896fd2252dda48be0', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'acba548461502b88a483649f181a65da', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'bd986c8233a7a251c0403c4df3ce8511', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'cfb6a1faab0ce219609f44055648cda1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'dabc0f13943decd855d39febfd2a18ae', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'e340923ef96f70f142a0bee868bb8e67', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'f179c71abcd89f7ed9fa781220d2018c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'fae80bffb40c02fdf07691ef18837b14', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-6', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:54', '2026-04-22 12:03:54', '2026-04-22 12:03:54', NULL, '1693e0a6448fb98bd03fe0c4c65371fb', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-6', 'status', 'review', 'done', NULL, '2026-04-22 12:03:54', '2026-04-22 12:03:54', '2026-04-22 12:03:54', NULL, '80d695ac3bfc5fe1f3615013dea3b06a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'status', 'ready', 'in_progress', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, '33a14771999bc0062268447b4ffadc73', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'status', 'backlog', 'ready', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, '5a2f1c3d37090ad5791c6a0e8a7aa80a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'status', 'ready', 'in_progress', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, '7d84a743a1b9e1ce5c50172b89318153', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'status', 'backlog', 'ready', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, 'cad02006950fb6fa0d003553437d3bbe', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'status', 'in_progress', 'review', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '10f0a16fa4e4899c7817dd5c44cc3ffe', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'status', 'in_progress', 'review', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '2d697a30a6193eacf2a3ddf1661e861c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'status', 'review', 'done', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '591bb27186744d023df34105884fa58f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'status', 'review', 'done', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '817b9d8e92707eb588cb7f9eae7d424e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-22', 'status', 'backlog', 'cancelled', NULL, '2026-04-22 20:34:17', '2026-04-22 20:34:17', '2026-04-22 20:34:17', NULL, '5460d369f6cdef9c393a37e41481bcb5', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-30', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, '313d4c9dd6444a4573cbe9584ed18f61', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-31', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, '4630269c56784316b975ca56b10a2337', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-34', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, '5a8f7e0eda55bc0893c6597400a8d535', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-33', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, 'fac6b818dc68490a80ea32255f0ba404', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-31', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, '0a7137f82fbb9307496b7e4133b15052', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-33', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, '5aff1ed40d9a68a5b4d59aab0d24dd31', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-34', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, '931bcaabbd64af0612fbe6eb9a4470d6', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-30', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, 'c2fca20d28b2a3a63a56eb2841a4bda9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-31', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, '84e5b99f4fb99f8ae4d189ddaac2de74', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-34', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, '8c7ba0fd4c8acc6065a24b80ae28d526', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-30', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, 'd91c6440f66e683a87be5e61a366cca7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-33', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, 'f9253848d9a95ce1823c8ba78a511fc0', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-35', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:56:58', '2026-04-22 20:56:58', '2026-04-22 20:56:58', NULL, '389d5abb35b3b73ccb38ac8bec3b92c7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-35', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:56:58', '2026-04-22 20:56:58', '2026-04-22 20:56:58', NULL, '7e714e95f89966a13e436d2fe38e7443', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-35', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:56:58', '2026-04-22 20:56:58', '2026-04-22 20:56:58', NULL, 'b1aa8ff1f9b24cdfc9e0bebf21d38b99', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-32', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:02:29', '2026-04-22 21:02:29', '2026-04-22 21:02:29', NULL, '39b407be88dbb31f530c8074f6bcecc7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-32', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:02:29', '2026-04-22 21:02:29', '2026-04-22 21:02:29', NULL, '59df9c23d722ccf1c1ac18baee76c785', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-32', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:02:29', '2026-04-22 21:02:29', '2026-04-22 21:02:29', NULL, '76b48a80abcba29b1047c794fd004234', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-37', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '297763c026010189ea0bd870f77a173e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-37', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '2cd947436be1c156eb77d233aeed5da2', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-38', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '38826870354a1bf51bc2ff21f6da0ea3', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-37', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '5105038efff2e3055213b1b1403b7610', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-39', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '67cc6c0934711b6a592e11e5edc6194c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-39', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '6e9e7d3ce9f74ed261912b1cb099dcad', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-39', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, 'a4e702776f3b553fb276782e539ebfc7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-38', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, 'd294248ba8ea651e78aeb6fed0457eb4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-38', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, 'da8dc2002a03eaee7fcd5504abee6f60', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '08f09f3bb9f1a0001110716930824e62', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-29', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '10005b4803d743aa21be0223c8a85d5c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '11ac23b546d0e22206af4221062f5912', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-21', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '127404fe79dfb79d0297b85d78235c68', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-23', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '15fb6606af61e11b6960799721e7cdcc', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-38', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '19f6209ce4428172fb16941accb13566', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-27', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '212224b7dcc4b991119dc430ae179311', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '2fe8eed15b3ead999a69cdd08df5e14c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '3990623d651c604fec692edb70976d69', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-28', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '44d273b92ad4c93159706c1f17fcb6a7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '46935c5a6d8ef12cf1c2252bd8d30368', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-37', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '551ea05a8d0cc28c0ff7010a139e6fe1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '6748fca610fc8a8503d69846231616fe', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '7843cfda82ec1c1622c1a201f82acaf1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '822790bbace926feb2372b8665080932', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-22', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '9a1ae5742a1230e60279d9c72f8b0f75', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-42', 'parent_id', NULL, 'T-5', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'a414bc8e4d32fa32483228a4e9c7be8c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-24', 'parent_id', NULL, 'T-3', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'a80caae1913b8e9a1d5dab980bf0b77b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-40', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'a895d51a2e6e8b448684933cf0224162', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-43', 'parent_id', NULL, 'T-3', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'ad9a8a85686e33df17ae15631afa1213', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'aedb3c83402245f81cd10f04d15dcf8e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'b6dbb8114cd77c206d3b8bdf054736b9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-26', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'b7b189fe87f61d42e1d0318d23aeb1fe', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'c80049d2c236f0d2512355183b766f0f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-25', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'ccdf7bec2a75f5829eab4fdd57171622', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-36', 'parent_id', NULL, 'T-7', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'd13ed78c92d33e61e448c9e155adf870', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-41', 'parent_id', NULL, 'T-7', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'eb2a2ca41a57604b40556e81bf6480a9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-35', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'f0d27e423d40a998115e10ffd804f791', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-39', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'f5c891c9a5f7d5e3f43ded0dc07eb704', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'ff609c22f964618c84b7c36988e87ae7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:01:08', '2026-04-24 07:01:08', '2026-04-24 07:01:08', NULL, '6ae864936d48565e880f06e984e9d978', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:01:08', '2026-04-24 07:01:08', '2026-04-24 07:01:08', NULL, 'ebebc38301a28001a09c9e7066c9aeb9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:01:48', '2026-04-24 07:01:48', '2026-04-24 07:01:48', NULL, '3cf128b79fe91447f72f42d8cffce9e1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:01:48', '2026-04-24 07:01:48', '2026-04-24 07:01:48', NULL, '68f57f5e3a1005ca3009f944b8b634d7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:05:40', '2026-04-24 07:05:40', '2026-04-24 07:05:40', NULL, '50aa3e362dbe9682d98f6d5f71ea97b1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:05:50', '2026-04-24 07:05:50', '2026-04-24 07:05:50', NULL, '4cb951c3ea697881d8fa5b0e86cfdf96', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:05:51', '2026-04-24 07:05:51', '2026-04-24 07:05:51', NULL, '7516ffa5c72fa4e7fd067249393c3653', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:06:30', '2026-04-24 07:06:30', '2026-04-24 07:06:30', NULL, '268576bd9559485dd2ac5e7ffd1f1a79', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:06:32', '2026-04-24 07:06:32', '2026-04-24 07:06:32', NULL, 'ef46c3a3db4c4ba50ff1f0e1baa4c0e6', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'review', 'done', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, '1cd74d47eb547aa0bef71597eaaa2c5b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'ready', 'in_progress', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, '3b8c17d366d1710d03523303c9df83c8', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'in_progress', 'review', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, '7eb9ca56292802301f93d0e2d680835e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, 'a1a837494c047ade786d79edb75296dd', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'done', 'in_progress', NULL, '2026-04-24 07:08:00', '2026-04-24 07:08:00', '2026-04-24 07:08:00', NULL, 'ac745b5254a0e8f085189c23d95acb7e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'in_progress', 'done', NULL, '2026-04-24 07:08:29', '2026-04-24 07:08:29', '2026-04-24 07:08:29', NULL, '2adaf942a49bfeeac90aee1527a98444', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'done', 'in_progress', NULL, '2026-04-24 07:08:29', '2026-04-24 07:08:29', '2026-04-24 07:08:29', NULL, 'e0d54ffba34677dc6e687be801ccec05', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'in_progress', 'ready', NULL, '2026-04-24 07:08:35', '2026-04-24 07:08:35', '2026-04-24 07:08:35', NULL, '74e6fa2f524c21300467381c2f3ad4e3', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:08:35', '2026-04-24 07:08:35', '2026-04-24 07:08:35', NULL, 'fbd19eabf442e6e0de60028c28956b8f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, '7d3efe4ccaee7d45d1a01d28144c4f0a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'ready', 'in_progress', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, '914a2f5f5ef0807a6743f8811e71804b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'in_progress', 'ready', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, 'a1cd74c76fd10c353aa758cea0331c5c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'cancelled', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, 'cdbe85f80838e781048780db934d0115', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'cancelled', 'backlog', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, 'f24e8d138172597d7ddcf26672b67d5f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, 'f9a83fa1b41d56202c57e6a55b240f35', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'done', NULL, '2026-04-24 07:30:15', '2026-04-24 07:30:15', '2026-04-24 07:30:15', NULL, '99c4e5a68463201e7f1b7764eddfd2ae', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'done', 'backlog', NULL, '2026-04-24 07:30:22', '2026-04-24 07:30:22', '2026-04-24 07:30:22', NULL, 'd0274058b130d8003123881b8a410f7f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 08:08:16', '2026-04-24 08:08:16', '2026-04-24 08:08:16', NULL, '83d70eb5a7f6e7175e0719fdb23243c2', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'backlog', NULL, '2026-04-24 08:08:16', '2026-04-24 08:08:16', '2026-04-24 08:08:16', NULL, 'b5acf95cb059f0c573b78d755b938da2', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'in_progress', NULL, '2026-04-24 08:08:23', '2026-04-24 08:08:23', '2026-04-24 08:08:23', NULL, '4ae08b8af459af0679b05cf1f8ac3950', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'in_progress', 'ready', NULL, '2026-04-24 08:08:38', '2026-04-24 08:08:38', '2026-04-24 08:08:38', NULL, 'c7e4cabf5d1fccad28f22d498e91193a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'in_progress', NULL, '2026-04-24 08:08:40', '2026-04-24 08:08:40', '2026-04-24 08:08:40', NULL, '9b0937bc7eb82785407c367a90365c3a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'in_progress', 'review', NULL, '2026-04-24 08:08:44', '2026-04-24 08:08:44', '2026-04-24 08:08:44', NULL, '90587f61d96b12ce53f2a4edc705fdd8', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'review', 'in_progress', NULL, '2026-04-24 08:08:48', '2026-04-24 08:08:48', '2026-04-24 08:08:48', NULL, 'd02ac7ee61fc4dc420fb7597a4f326db', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'in_progress', 'ready', NULL, '2026-04-24 08:16:15', '2026-04-24 08:16:15', '2026-04-24 08:16:15', NULL, '145613c94b4366bda03fb76c0f0747ce', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'backlog', NULL, '2026-04-24 08:16:19', '2026-04-24 08:16:19', '2026-04-24 08:16:19', NULL, '0d8e318e8060aece6e18788cee56f646', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 08:16:21', '2026-04-24 08:16:21', '2026-04-24 08:16:21', NULL, '3d8635cfdbaf9b0c22dcba9f15f3c1ad', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'in_progress', NULL, '2026-04-24 08:16:47', '2026-04-24 08:16:47', '2026-04-24 08:16:47', NULL, '77f873a7a5fff47ce9b974a6999d03b4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-28', 'status', 'backlog', 'in_progress', NULL, '2026-04-24 08:16:56', '2026-04-24 08:16:56', '2026-04-24 08:16:56', NULL, 'e7610d68ec9871746b4a361e33b18f05', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-43', 'status', 'backlog', 'ready', NULL, '2026-04-24 08:17:11', '2026-04-24 08:17:11', '2026-04-24 08:17:11', NULL, '9c3afbbfca207aeb78541dbd9d90e462', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,114 @@
|
|||||||
|
-- Auto-generated by pql init. CREATE TABLE statements
|
||||||
|
-- for the planning schema; per-table dir keeps the changelog
|
||||||
|
-- self-describing per D-15. CREATE TABLE IF NOT EXISTS is
|
||||||
|
-- idempotent so running schema files from each directory in
|
||||||
|
-- replay order is harmless.
|
||||||
|
--
|
||||||
|
-- Importer parses the markers below to detect schema drift
|
||||||
|
-- between the producing pql version and the local one — a
|
||||||
|
-- bumped canonical_version means projection rules changed
|
||||||
|
-- and replay must refuse rather than silently corrupt state.
|
||||||
|
-- pql:created_by: 1.4.26
|
||||||
|
-- pql:canonical_version: 1
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS decisions (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('confirmed','question','rejected')),
|
||||||
|
domain TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'active'
|
||||||
|
CHECK(status IN ('active','superseded','resolved','open')),
|
||||||
|
date TEXT,
|
||||||
|
file_path TEXT NOT NULL,
|
||||||
|
synced_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS decision_refs (
|
||||||
|
source_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||||
|
target_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||||
|
ref_type TEXT NOT NULL
|
||||||
|
CHECK(ref_type IN ('supersedes','references','resolves','depends_on','amends')),
|
||||||
|
note TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (source_id, target_id, ref_type)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tickets (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('initiative','epic','story','task','bug')),
|
||||||
|
parent_id TEXT REFERENCES tickets(id),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
status TEXT NOT NULL DEFAULT 'backlog'
|
||||||
|
CHECK(status IN ('backlog','ready','in_progress','review','done','cancelled')),
|
||||||
|
priority TEXT DEFAULT 'medium'
|
||||||
|
CHECK(priority IN ('critical','high','medium','low')),
|
||||||
|
assigned_to TEXT,
|
||||||
|
team TEXT,
|
||||||
|
decision_ref TEXT REFERENCES decisions(id),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_deps (
|
||||||
|
blocker_id TEXT NOT NULL REFERENCES tickets(id),
|
||||||
|
blocked_id TEXT NOT NULL REFERENCES tickets(id),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (blocker_id, blocked_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_history (
|
||||||
|
ticket_id TEXT NOT NULL REFERENCES tickets(id),
|
||||||
|
field TEXT NOT NULL,
|
||||||
|
old_value TEXT,
|
||||||
|
new_value TEXT,
|
||||||
|
changed_by TEXT,
|
||||||
|
changed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT UNIQUE,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_labels (
|
||||||
|
ticket_id TEXT NOT NULL REFERENCES tickets(id),
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (ticket_id, label)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS meta (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_team ON tickets(team);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_decision_ref ON tickets(decision_ref);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_assigned ON tickets(assigned_to);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decisions_domain ON decisions(domain);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decisions_type ON decisions(type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decision_refs_target ON decision_refs(target_id);
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
-- Auto-generated by pql init. CREATE TABLE statements
|
||||||
|
-- for the planning schema; per-table dir keeps the changelog
|
||||||
|
-- self-describing per D-15. CREATE TABLE IF NOT EXISTS is
|
||||||
|
-- idempotent so running schema files from each directory in
|
||||||
|
-- replay order is harmless.
|
||||||
|
--
|
||||||
|
-- Importer parses the markers below to detect schema drift
|
||||||
|
-- between the producing pql version and the local one — a
|
||||||
|
-- bumped canonical_version means projection rules changed
|
||||||
|
-- and replay must refuse rather than silently corrupt state.
|
||||||
|
-- pql:created_by: 1.4.26
|
||||||
|
-- pql:canonical_version: 1
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS decisions (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('confirmed','question','rejected')),
|
||||||
|
domain TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'active'
|
||||||
|
CHECK(status IN ('active','superseded','resolved','open')),
|
||||||
|
date TEXT,
|
||||||
|
file_path TEXT NOT NULL,
|
||||||
|
synced_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS decision_refs (
|
||||||
|
source_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||||
|
target_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||||
|
ref_type TEXT NOT NULL
|
||||||
|
CHECK(ref_type IN ('supersedes','references','resolves','depends_on','amends')),
|
||||||
|
note TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (source_id, target_id, ref_type)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tickets (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('initiative','epic','story','task','bug')),
|
||||||
|
parent_id TEXT REFERENCES tickets(id),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
status TEXT NOT NULL DEFAULT 'backlog'
|
||||||
|
CHECK(status IN ('backlog','ready','in_progress','review','done','cancelled')),
|
||||||
|
priority TEXT DEFAULT 'medium'
|
||||||
|
CHECK(priority IN ('critical','high','medium','low')),
|
||||||
|
assigned_to TEXT,
|
||||||
|
team TEXT,
|
||||||
|
decision_ref TEXT REFERENCES decisions(id),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_deps (
|
||||||
|
blocker_id TEXT NOT NULL REFERENCES tickets(id),
|
||||||
|
blocked_id TEXT NOT NULL REFERENCES tickets(id),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (blocker_id, blocked_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_history (
|
||||||
|
ticket_id TEXT NOT NULL REFERENCES tickets(id),
|
||||||
|
field TEXT NOT NULL,
|
||||||
|
old_value TEXT,
|
||||||
|
new_value TEXT,
|
||||||
|
changed_by TEXT,
|
||||||
|
changed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT UNIQUE,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_labels (
|
||||||
|
ticket_id TEXT NOT NULL REFERENCES tickets(id),
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (ticket_id, label)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS meta (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_team ON tickets(team);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_decision_ref ON tickets(decision_ref);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_assigned ON tickets(assigned_to);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decisions_domain ON decisions(domain);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decisions_type ON decisions(type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decision_refs_target ON decision_refs(target_id);
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# --- pql plan import ---
|
|
||||||
# Auto-installed by pql init. Imports planning state when the
|
|
||||||
# snapshot file changes on pull/merge. Safe no-op if pql is absent.
|
|
||||||
if command -v pql >/dev/null 2>&1; then
|
|
||||||
changed=$(git diff-tree -r --name-only ORIG_HEAD HEAD -- .pql/pql-plan.json 2>/dev/null)
|
|
||||||
if [ -n "$changed" ]; then
|
|
||||||
pql plan import 2>/dev/null
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
# --- end pql ---
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# --- pql plan export ---
|
|
||||||
# Auto-installed by pql init. Exports planning state so it's
|
|
||||||
# committed alongside code changes. Safe no-op if pql is absent.
|
|
||||||
if command -v pql >/dev/null 2>&1; then
|
|
||||||
pql plan export 2>/dev/null
|
|
||||||
if ! git diff --quiet -- .pql/pql-plan.json 2>/dev/null; then
|
|
||||||
git add .pql/pql-plan.json
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
# --- end pql ---
|
|
||||||
-4262
File diff suppressed because it is too large
Load Diff
+187
-74
@@ -18,102 +18,215 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
## [2.1.0] — 2026-05-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Contrast gate split — baseline `canonicalPairs` every theme passes,
|
||||||
|
strict `extendedPairs` (muted/status/syntax/focus-border) gated to
|
||||||
|
`-hc` / `-cb` variants. Ships `clide-hc`, `midnight-hc`, `paper-hc`,
|
||||||
|
`terminal-hc` siblings of the named themes (D-69, T-114, T-118).
|
||||||
|
- Pre-push coverage gate — `make push-check` runs `ci/coverage_gate.sh`,
|
||||||
|
which fails if total line coverage drops below `coverage_floor:` in
|
||||||
|
`pubspec.yaml`. Floor ratchets up only; target 95% (D-66).
|
||||||
|
- Pre-push changelog gate — `ci/changelog_gate.sh` fails on any
|
||||||
|
`## [Unreleased]` bullet over 60 words. Enforces the Keep-a-Changelog
|
||||||
|
conciseness rule in the git-commit skill.
|
||||||
|
- Keymap layer (`KeymapService`) — typed Intents, YAML presets,
|
||||||
|
VS-Code-style when-clauses, layered preset → user file → settings
|
||||||
|
overlay. Default preset ships; vim/vscode/jetbrains unblocked
|
||||||
|
(T-117, supersedes T-110).
|
||||||
|
- Keyboard operability — `ClideTappable` is now Tab-focusable with a
|
||||||
|
focus ring and Enter/Space activation; `ClidePalette` adds arrow
|
||||||
|
nav, Escape dismiss, and selection highlight (T-100).
|
||||||
|
- Panel-to-panel focus traversal — each `SlotHost` wraps in a
|
||||||
|
`FocusScope` + `FocusTraversalGroup`; `F6` / `Shift+F6` cycle
|
||||||
|
sidebar → workspace → context. `FocusTracker` integrates with
|
||||||
|
Flutter focus rather than paralleling it (T-105).
|
||||||
|
- Event-driven test waits — PTY + watcher tests await stream events
|
||||||
|
instead of fixed sleeps; `onTimeout` callbacks now `fail()` loudly
|
||||||
|
with diagnostic context. `RecordingEventSink` exposes a broadcast
|
||||||
|
stream for the same pattern (T-108).
|
||||||
|
- Code-quality cleanups — `TreeSitterLib` exposes a last-error
|
||||||
|
diagnostic instead of swallowing dlopen failures; `ExtensionManager`
|
||||||
|
surfaces a `failedExtensions` map for UI degradation; PTY constants
|
||||||
|
consolidated in `libc.dart` + `PosixErrno`; `test_app.dart` gated
|
||||||
|
behind `kDebugMode` (T-112).
|
||||||
|
- Test sweep — `keybindings`, `toolchain_paths`, and several
|
||||||
|
`widgets/src/` primitives (tooltip, palette, multitab, markdown).
|
||||||
|
- `tree_sitter_service` sweep — fake-FFI + real-library smoke,
|
||||||
|
17% → 96%. Crosses the 95% global target (T-91).
|
||||||
- Staged `dart doc` CI job — generates and uploads an HTML API
|
- Staged `dart doc` CI job — generates and uploads an HTML API
|
||||||
reference for the public `lib/` surface. The step wraps
|
reference for the public `lib/` surface. The step wraps
|
||||||
`dart doc --validate-links` and grep-fails the build on any warning,
|
`dart doc --validate-links` and grep-fails the build on any warning.
|
||||||
so broken doc refs and dangling links can't accumulate. Inert with
|
Inert with the rest of the workflow until Gitea Actions activates.
|
||||||
the rest of the workflow until Gitea Actions activates.
|
|
||||||
- Mouse wheel scrolling in Claude pane — converts scroll events to
|
- Mouse wheel scrolling in Claude pane — converts scroll events to
|
||||||
PgUp/PgDown so Claude Code (and other TUI apps) scroll their
|
PgUp/PgDown so TUI apps scroll their history naturally.
|
||||||
history naturally.
|
|
||||||
- Welcome screen Tips card — six common keybindings shown below the
|
- Welcome screen Tips card — six common keybindings shown below the
|
||||||
START / RECENT row when the viewport is tall enough.
|
START / RECENT row when the viewport is tall enough.
|
||||||
- `MultitabPane` widget + `MultitabController` for panes that host
|
- `MultitabPane` widget + `MultitabController` for panes that host
|
||||||
N runtime tab instances of the same kind. Generic over a payload
|
N runtime tab instances of the same kind. Generic over a payload
|
||||||
type, supports pinned/non-closeable tabs (primary), drag-reorder,
|
type, supports pinned/non-closeable tabs, drag-reorder, close × on
|
||||||
close × on hover, and an optional `+` add button. Used by the
|
hover, and an optional `+` add button.
|
||||||
Claude pane to render primary + secondaries.
|
- `MultitabPane.keepAlive` mode — entry bodies stay mounted via
|
||||||
- `MultitabPane.keepAlive` mode — when set, all entry bodies stay
|
IndexedStack so switching tabs preserves their state (PTY
|
||||||
mounted via IndexedStack so switching tabs preserves their state
|
connections, scroll position, etc.).
|
||||||
(PTY connections, scroll position, etc.).
|
- `CONTRIBUTING.md` — human-addressed contributor guide covering
|
||||||
|
clone / build / test / DQR / tickets / commit conventions. The
|
||||||
|
`[Unreleased]` section is reorganised to one subsection per kind
|
||||||
|
per Keep a Changelog 1.1.0 (T-109).
|
||||||
|
- `make verify` — no-tests sweep (analyze + format + decisions +
|
||||||
|
changelog gate). For mid-edit checks; `make push-check` stays the
|
||||||
|
full pre-push pipeline.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- Window-control close-button red, white close glyph, and palette
|
||||||
|
ambient shadow are now tokens (`windowControl.closeHover*`,
|
||||||
|
`shadow.ambient`) instead of hard-coded hex. Light themes get a
|
||||||
|
softer ink-tinted shadow (T-114).
|
||||||
|
- Text-zoom (Ctrl +/-/0) is now a kernel `TextZoom` service and shows
|
||||||
|
up in the palette as `View: Zoom In/Out/Reset Zoom` (T-114).
|
||||||
|
- Panel splitters (sidebar / context / editor-split) are tab-focusable;
|
||||||
|
arrow keys nudge by 10 px, Shift+arrow by 50 px (2% / 10% for the
|
||||||
|
editor split). Exposed as slider Semantics nodes so screen readers
|
||||||
|
announce the current size. CLI verb deferred to T-99 (T-111).
|
||||||
|
- Changelog gate is binary — dropped the soft 40-word warning, kept
|
||||||
|
the 60-word hard cap. Warnings that never blocked just normalised
|
||||||
|
drift.
|
||||||
|
- PTY spawning uses `posix_openpt` + `posix_spawn` instead of
|
||||||
|
`forkpty` — closes a ~5% deadlock window in the multithreaded Dart
|
||||||
|
VM (T-96, D-5 amended). Missing exe/cwd now throw `PtyException` at
|
||||||
|
spawn time. Drops the `libutil.so.1` dependency.
|
||||||
|
- Coverage floor ratcheted to 95% — D-66 target hit.
|
||||||
|
- `TreeSitterService` and `TreeSitterLib` accept injectable FFI + asset
|
||||||
|
loaders for fake-driven tests; production paths unchanged.
|
||||||
|
- Tidied test imports flagged by `unnecessary_import`.
|
||||||
|
- `README.md` rewritten to match current architecture;
|
||||||
|
`docs/initial-plan.md` bannered as historical; new
|
||||||
|
`docs/architecture.md` describes today's shape (T-101).
|
||||||
|
- `SchedulerService._stopTicker` now awaits the in-flight isolate spawn
|
||||||
|
before killing — closes the same race shape we fixed in PTY (T-106).
|
||||||
|
- `make push-check-full` added — runs `push-check` plus integration +
|
||||||
|
smoke for pre-release checks. Integration tests skip the hanging
|
||||||
|
theme_picker case until that's fixed (T-103, T-116).
|
||||||
|
- Governance bookkeeping: D-66 amended (floor at `coverage_floor:` in
|
||||||
|
`pubspec.yaml`); `licenses.yaml` reconciled with `pubspec.yaml`;
|
||||||
|
Q-1/Q-2/Q-3/Q-25 triaged; `.claude/skills/README.md` inventory
|
||||||
|
added; `--no-fatal-infos` dropped from `ci/test.sh` (T-113).
|
||||||
|
- Terminal panes render bold attributes with a real bold weight —
|
||||||
|
bundled JetBrainsMono Bold + BoldItalic registered with the
|
||||||
|
`JetBrainsMono` family at `weight: 700`. The painter's bold
|
||||||
|
suppression workaround is gone.
|
||||||
- Claude pane uses `MultitabPane` for primary + secondaries — drops
|
- Claude pane uses `MultitabPane` for primary + secondaries — drops
|
||||||
~100 lines of bespoke tab-strip code, gains drag-to-reorder.
|
~100 lines of bespoke tab-strip code, gains drag-to-reorder.
|
||||||
- UI spacing constants live in `lib/widgets/src/spacing.dart` —
|
- UI spacing constants live in `lib/widgets/src/spacing.dart` —
|
||||||
`clideInset*` for paddings, `clideGap*` for sibling distances,
|
`clideInset*` for paddings, `clideGap*` for sibling distances,
|
||||||
`clideIcon*` / `clideControlHeight` for control sizes. Inline
|
`clideIcon*` / `clideControlHeight` for control sizes.
|
||||||
pixel literals replaced where they were repeated.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Tagline reads "IDE for Claude Code CLI" everywhere (welcome
|
- Tagline reads "IDE for Claude Code CLI" everywhere (welcome
|
||||||
subtitle, README, CLAUDE.md, pubspec, web manifest, CLI banner).
|
subtitle, README, CLAUDE.md, pubspec, web manifest, CLI banner).
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Closing a secondary Claude pane tab now kills its tmux session
|
|
||||||
on the clide socket, honouring D-41's "closing a secondary kills
|
|
||||||
that tmux session" lifecycle. Previously `pane.close` only killed
|
|
||||||
the ptyc-spawned tmux client and the server-side session leaked.
|
|
||||||
- Cold-start reap: every clide launch kills any leftover secondary
|
|
||||||
tmux sessions for the current repo before spawning new ones, so
|
|
||||||
D-41's "secondary numbering resets between runs" holds even after
|
|
||||||
an abrupt previous exit (kill -9, crash, force-quit).
|
|
||||||
- `claude.kill-all-sessions` command now actually kills the
|
|
||||||
server-side tmux sessions for the repo, not just the panes.
|
|
||||||
- Terminal cell grid no longer drifts on bold text — bold rendering
|
|
||||||
is suppressed at the painter level since synthetic bold (with no
|
|
||||||
Bold.ttf registered) shifts glyph advance widths.
|
|
||||||
- PTY surfaces errno on `forkpty`, `write`, and `ioctl` failures
|
|
||||||
instead of swallowing them. `execve` failures in the spawned
|
|
||||||
child now write a diagnostic line to the slave PTY before
|
|
||||||
`_exit`, so the parent's reader sees the cause instead of an
|
|
||||||
immediate EOF that looked indistinguishable from clean exit.
|
|
||||||
PTY `write` loops on short writes; both `NativePty.write` and
|
|
||||||
`PtySession.write` now throw `PtyException` on hard errors.
|
|
||||||
- PTY teardown order fixed — kill the child first so the master
|
|
||||||
fd returns EOF, await the reader isolate exit, then close the
|
|
||||||
fd. Previously closing the fd while the isolate still polled it
|
|
||||||
could briefly target a reused fd. Reader isolate spawn errors
|
|
||||||
in both `NativePty` and `PtySession` are now surfaced via the
|
|
||||||
output stream instead of silently dropped. `_recvFdAsync` no
|
|
||||||
longer leaks the `ReceivePort` when `Isolate.spawn` throws, and
|
|
||||||
`PtySession.spawn` closes the master fd if any post-receive
|
|
||||||
step fails.
|
|
||||||
- IPC server hardening: per-request 60s timeout (configurable),
|
|
||||||
broadcast/response write failures logged instead of swallowed,
|
|
||||||
client dropped on response-write failure, and the stale-socket
|
|
||||||
retry now probes for a live daemon before unlinking the socket
|
|
||||||
(refusing to start if one answers).
|
|
||||||
- `pane.spawn` and `editor.open` now map POSIX errno values to
|
|
||||||
actionable IPC error kinds. ENOENT → `not_found`, EACCES/EPERM
|
|
||||||
→ `user_error` with a permissions hint, EISDIR/ENOTDIR/EEXIST
|
|
||||||
→ distinct user-error/conflict, EMFILE/ENFILE → `tool_error`
|
|
||||||
with a "fd limit hit" hint. Previously every spawn/open failure
|
|
||||||
was an indistinguishable `tool_error`.
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- `files.read` and `files.ls` now reject paths that resolve outside
|
|
||||||
the workspace root. Previously a relative path containing `..`
|
|
||||||
could read arbitrary files via path traversal.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Inline terminal emulator based on xterm.dart v4.0.0 — replaces the
|
- Inline terminal emulator based on xterm.dart v4.0.0 — replaces the
|
||||||
pub.dev dependency with owned code under `lib/src/terminal/`. Drops
|
pub.dev dependency with owned code under `lib/src/terminal/`. Drops
|
||||||
three transitive dependencies (xterm, quiver, zmodem).
|
three transitive dependencies (xterm, quiver, zmodem).
|
||||||
- Bundle clide-specific tmux.conf for Claude pane sessions: no status
|
- Bundle clide-specific tmux.conf for Claude pane sessions: no status
|
||||||
bar, 50k scrollback, mouse on, zero escape delay, isolated socket.
|
bar, 50k scrollback, mouse on, zero escape delay, isolated socket.
|
||||||
- Claude pane spawns `claude` directly inside tmux with
|
- Claude pane spawns `claude` directly inside tmux with
|
||||||
`CLAUDE_CODE_NO_FLICKER=1` to enable Claude's fullscreen TUI mode
|
`CLAUDE_CODE_NO_FLICKER=1` to enable Claude's fullscreen TUI mode.
|
||||||
(input box pinned at the bottom).
|
- PTY read buffer increased from 4 KB to 64 KB.
|
||||||
- PTY read buffer increased from 4KB to 64KB.
|
- Terminal view 2 px padding on all sides.
|
||||||
- Terminal view 2px padding on all sides.
|
|
||||||
- Remove bold JetBrains Mono font registration to prevent glyph width
|
### Removed
|
||||||
mismatch in terminal rendering.
|
|
||||||
|
- **`bin/clide.dart` + `DaemonServer`** — completing the D-56
|
||||||
|
dissolution. The separate daemon process was dissolved on 2026-04-23
|
||||||
|
but the entry point and socket server class were never deleted.
|
||||||
|
Gone now, along with orphaned tests, stale i18n strings, and
|
||||||
|
"start `clide --daemon`" error messages.
|
||||||
|
- **`ptyc/` source tree + `PtySession` + `scm_rights.dart`** — PTY
|
||||||
|
spawning migrated to Dart FFI `forkpty()` (`NativePty`) but the old
|
||||||
|
C helper and its Dart wiring were never cleaned up. Removed from
|
||||||
|
toolchain resolution, `ToolCheck` gate, backend serialization,
|
||||||
|
testmode harness, CI scripts, Makefile, and sandbox entitlements.
|
||||||
|
D-5 amended to record the retirement.
|
||||||
|
- CI golden images (`test/goldens/goldens/ci/`) — Skia anti-aliasing
|
||||||
|
of geometric shapes differs between macOS and Linux even with the
|
||||||
|
Ahem font. Replaced with platform-keyed goldens (`goldens/linux/`,
|
||||||
|
`goldens/macos/`).
|
||||||
|
- Bold JetBrains Mono font registration that prevented glyph-width
|
||||||
|
mismatch in terminal rendering — superseded by the Bold/BoldItalic
|
||||||
|
re-registration above.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `TerminalView.onTapUp` now actually fires on primary tap — was
|
||||||
|
wired to a dead code path (T-93). Dead `onTapUp` surface on
|
||||||
|
`TerminalGestureHandler` / `TerminalGestureDetector` removed.
|
||||||
|
- `BufferLine.eraseRange` no longer panics when called with `end == 0`.
|
||||||
|
Real trigger: `Terminal.eraseDisplayAbove` with the cursor at
|
||||||
|
column 0 — common after `ESC[H\x1b[1J` (home + erase-above).
|
||||||
|
- Terminal selections no longer vanish when resizing narrower —
|
||||||
|
reflow's tail-anchor handler left anchors detached past the
|
||||||
|
trimmed range. Common triggers: Ctrl+A then resize, drag past a
|
||||||
|
partially-filled line (T-92).
|
||||||
|
- `BufferLine.removeCells` / `insertCells` / `dispose` no longer skip
|
||||||
|
anchors due to concurrent list modification during iteration —
|
||||||
|
iteration now snapshots the list first (T-91).
|
||||||
|
- Closing a secondary Claude pane tab now kills its tmux session on
|
||||||
|
the clide socket, honouring D-41's lifecycle. Previously
|
||||||
|
`pane.close` only killed the ptyc-spawned tmux client.
|
||||||
|
- Cold-start reap: every clide launch kills any leftover secondary
|
||||||
|
tmux sessions for the current repo before spawning new ones, so
|
||||||
|
D-41's "secondary numbering resets between runs" holds.
|
||||||
|
- `claude.kill-all-sessions` command now actually kills the
|
||||||
|
server-side tmux sessions for the repo, not just the panes.
|
||||||
|
- Terminal cell grid no longer drifts on bold text — bold rendering
|
||||||
|
is suppressed at the painter level since synthetic bold (with no
|
||||||
|
Bold.ttf registered) shifts glyph advance widths.
|
||||||
|
- PTY surfaces errno on `forkpty` / `write` / `ioctl` failures
|
||||||
|
instead of swallowing. `execve` failures write a diagnostic to the
|
||||||
|
slave before `_exit`. `NativePty.write` and `PtySession.write`
|
||||||
|
loop on short writes; both throw `PtyException` on hard errors.
|
||||||
|
- PTY teardown order fixed — kill child first so the master fd
|
||||||
|
returns EOF, await reader isolate exit, then close the fd.
|
||||||
|
- Reader isolate spawn errors in `NativePty` / `PtySession` are now
|
||||||
|
surfaced via the output stream instead of silently dropped.
|
||||||
|
`_recvFdAsync` no longer leaks the `ReceivePort` on spawn throw.
|
||||||
|
- IPC server hardening: per-request 60s timeout (configurable),
|
||||||
|
broadcast/response write failures logged instead of swallowed,
|
||||||
|
client dropped on response-write failure, and the stale-socket
|
||||||
|
retry now probes for a live daemon before unlinking the socket.
|
||||||
|
- `pane.spawn` and `editor.open` map POSIX errno values to actionable
|
||||||
|
IPC error kinds (ENOENT → `not_found`, EACCES/EPERM → `user_error`,
|
||||||
|
EISDIR/ENOTDIR/EEXIST → distinct kinds, EMFILE/ENFILE →
|
||||||
|
`tool_error` with an fd-limit hint).
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- IPC: `git.checkout`, `git.push` reject branch/remote args starting
|
||||||
|
with `-` (closes the `--upload-pack=...` argv-injection vector).
|
||||||
|
`files.read` rejects files over 10 MB. `git.log` caps `count` at
|
||||||
|
1000; `git.diff` / `git.stage` cap paths at 256 (T-104).
|
||||||
|
- Toolchain no longer resolves the dugite git binary against the open
|
||||||
|
workspace — a malicious repo could plant `native/dugite/bin/git`
|
||||||
|
and clide would run it on auto-fired `git.status`. Dugite now
|
||||||
|
resolves against the install dir + `CLIDE_DUGITE_DIR` env override
|
||||||
|
only (T-98).
|
||||||
|
- `files.read` and `files.ls` reject symlinks whose targets live
|
||||||
|
outside the workspace — closes a path-safety bypass via in-repo
|
||||||
|
symlinks (T-102).
|
||||||
|
- `files.read` and `files.ls` reject paths that resolve outside the
|
||||||
|
workspace root. Previously a relative path containing `..` could
|
||||||
|
read arbitrary files via path traversal.
|
||||||
|
|
||||||
## [2.0.0] — 2026-05-03
|
## [2.0.0] — 2026-05-03
|
||||||
|
|
||||||
|
|||||||
@@ -4,30 +4,30 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## What clide is
|
## What clide is
|
||||||
|
|
||||||
An IDE for Claude Code CLI. Single Flutter package at the repo root, plus small native supporter tools where Dart can't reach.
|
An IDE for Claude Code CLI. Single Flutter package at the repo root.
|
||||||
|
|
||||||
- **`lib/`** — all Dart code. Subsystem handlers (`lib/src/daemon/`, `lib/src/pty/`, `lib/src/ipc/`, `lib/src/git/`, `lib/src/pql/`), kernel services (`lib/kernel/`), UI widgets (`lib/widgets/`), built-in extensions (`lib/builtin/`), and the extension framework (`lib/extension/`). The Flutter app hosts the IPC server in-process (D-56).
|
- **`lib/`** — all Dart code. Subsystem handlers (`lib/src/daemon/`, `lib/src/pty/`, `lib/src/ipc/`, `lib/src/git/`, `lib/src/pql/`), kernel services (`lib/kernel/`), UI widgets (`lib/widgets/`), built-in extensions (`lib/builtin/`), and the extension framework (`lib/extension/`). The Flutter app hosts the IPC server in-process (D-56). PTY spawning uses Dart FFI `posix_openpt()` + `posix_spawn()` directly.
|
||||||
- **[`pql`](https://github.com/postmeridiem/pql)** — external supporter tool. Clide wraps it for every query surface; never re-implements it.
|
- **[`pql`](https://github.com/postmeridiem/pql)** — external supporter tool. Clide wraps it for every query surface; never re-implements it.
|
||||||
- **`ptyc/`** — small C supporter tool, peer of pql. Spawns a PTY + child and hands the master fd back over `SCM_RIGHTS`. Clide shells out to it for every pane (shell, tmux, claude, LSP, debug adapter).
|
|
||||||
|
|
||||||
tmux owns Claude session persistence (D-41) — the app re-attaches on restart via `tmux new-session -A`. Native rendering — markdown, canvas, graph — is Dart/Flutter (`CustomPaint` + widgets), not third-party packages.
|
tmux owns Claude session persistence (D-41) — the app re-attaches on restart via `tmux new-session -A`. Native rendering — markdown, canvas, graph — is Dart/Flutter (`CustomPaint` + widgets), not third-party packages.
|
||||||
|
|
||||||
Design doc: [`docs/initial-plan.md`](docs/initial-plan.md). Decisions: [`decisions/`](decisions/) (`D-NNN` confirmed, `Q-NNN` open, `R-NNN` rejected — see [`decisions/README.md`](decisions/README.md)). Python Textual predecessor under [`legacy/`](legacy/).
|
Design doc: [`docs/initial-plan.md`](docs/initial-plan.md). Decisions: [`governance/`](governance/) (`D-NNN` confirmed, `Q-NNN` open, `R-NNN` rejected — see [`governance/README.md`](governance/README.md)). Python Textual predecessor under [`legacy/`](legacy/).
|
||||||
|
|
||||||
## Guardrails
|
## Guardrails
|
||||||
|
|
||||||
These are load-bearing. Violating any means the design is wrong, not the rule.
|
These are load-bearing. Violating any means the design is wrong, not the rule.
|
||||||
|
|
||||||
- **Flutter desktop is the host. No Electron, ever.** Web target may work as a happy accident — don't compromise desktop fidelity for it. If we ship a web build at all, prefer Flutter's **WebAssembly (CanvasKit/Skwasm) compile** over the JS/HTML renderer. `xterm.dart` is the terminal renderer; markdown, canvas, graph are custom `CustomPaint`/widget components.
|
- **Flutter desktop is the host. No Electron, ever.** Web target may work as a happy accident — don't compromise desktop fidelity for it. If we ship a web build at all, prefer Flutter's **WebAssembly (CanvasKit/Skwasm) compile** over the JS/HTML renderer. `xterm.dart` is the terminal renderer; markdown, canvas, graph are custom `CustomPaint`/widget components.
|
||||||
- **Single process.** The Flutter app hosts everything in-process: IPC server, subsystem handlers (pane, files, editor, git, pql), extensions. No separate daemon binary (D-56 dissolved it). The CLI surface for Claude is a thin C client (ptyc peer).
|
- **Single process.** The Flutter app hosts everything in-process: IPC server, subsystem handlers (pane, files, editor, git, pql), extensions. No separate daemon binary (D-56 dissolved it).
|
||||||
- **CLI-first, not MCP.** Claude talks via Bash (`clide ...`), matching pql's contract. See [`D-1`](decisions/architecture.md#d-1-cli-first-not-mcp).
|
- **CLI-first, not MCP.** Claude talks via Bash (`clide ...`), matching pql's contract. See [`D-1`](governance/decisions/architecture.md#d-1-cli-first-not-mcp).
|
||||||
- **Dart is the core; native supporter tools fill specific gaps.** `ptyc` (C) for PTY spawning. `pql` (Go) for queries. No second "core language." See [`D-5`](decisions/architecture.md#d-5-dart-core-sidecar-dissolved-ptyc-as-pql-peer) (amended by D-56).
|
- **Dart is the core; pql fills the query gap.** PTY spawning is native Dart FFI (`posix_openpt` + `posix_spawn`). `pql` (Go) handles vault queries. No second "core language." See [`D-5`](governance/decisions/architecture.md#d-5-dart-core-sidecar-dissolved-ptyc-as-pql-peer) (amended by D-56).
|
||||||
- **Own the rendering stack.** PTY (via `ptyc`), markdown renderer, graph, canvas — all clide-owned, not pulled from opinionated packages.
|
- **Own the rendering stack.** PTY (via Dart FFI), markdown renderer, graph, canvas — all clide-owned, not pulled from opinionated packages.
|
||||||
- **User/Claude parity.** Every CLI subcommand has a UI affordance, and every UI action has a CLI. See [`D-6`](decisions/architecture.md#d-6-cli-and-event-surface-contract).
|
- **User/Claude parity.** Every CLI subcommand has a UI affordance, and every UI action has a CLI. See [`D-6`](governance/decisions/architecture.md#d-6-cli-and-event-surface-contract).
|
||||||
- **pql: wrap, don't duplicate.** Pql logic only lives in `lib/src/pql/` (pure shell-outs). Clide owns pql's `ignore_files:` config key; it never touches pql's `.pql/` index/cache data. See [`D-3`](decisions/architecture.md#d-3-pql-as-supporter-tool-clide-wraps-never-duplicates).
|
- **pql: wrap, don't duplicate.** Pql logic only lives in `lib/src/pql/` (pure shell-outs). Clide owns pql's `ignore_files:` config key; it never touches pql's `.pql/` index/cache data. See [`D-3`](governance/decisions/architecture.md#d-3-pql-as-supporter-tool-clide-wraps-never-duplicates).
|
||||||
- **Repo-is-the-workspace.** The git repo root is the workspace — no parallel "vault" concept.
|
- **Repo-is-the-workspace.** The git repo root is the workspace — no parallel "vault" concept.
|
||||||
- **Ignore discipline.** Single knob: `ignore_files:` in `.pql/config.yaml`, ordered layering. See [`D-4`](decisions/architecture.md#d-4-ignore-file-strategy).
|
- **Ignore discipline.** Single knob: `ignore_files:` in `.pql/config.yaml`, ordered layering. See [`D-4`](governance/decisions/architecture.md#d-4-ignore-file-strategy).
|
||||||
- **Decision discipline.** All architectural choices live in `decisions/<domain>.md` as `D-NNN` records. Open questions as `Q-NNN`. Rejected alternatives as `R-NNN`. Claim new IDs via `pql decisions claim D <domain> "title"`. See [`decisions/README.md`](decisions/README.md).
|
- **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 alternatives as `R-NNN` under `governance/rejected/<domain>.md`. Claim new IDs via `pql decisions claim D <domain> "title"`. See [`governance/README.md`](governance/README.md).
|
||||||
|
- **No pre-existing excuse.** Solo-dev repo — every failure encountered is yours to fix, regardless of who introduced it. If `make test` is red, a golden is broken, or `flutter analyze` shows a warning when you start working, the order is: **fix it first, then your work**. If you genuinely can't fix it in scope (separate ticket, large sweep, missing context), stop and surface it before continuing — don't push on top of broken state. "It was already broken" is not a reason to add more on top.
|
||||||
|
|
||||||
## Repo layout
|
## Repo layout
|
||||||
|
|
||||||
@@ -45,9 +45,8 @@ lib/
|
|||||||
test/ # All tests (core subsystems + widgets + goldens + a11y)
|
test/ # All tests (core subsystems + widgets + goldens + a11y)
|
||||||
assets/ # Fonts, themes, grammars, licenses, logo
|
assets/ # Fonts, themes, grammars, licenses, logo
|
||||||
linux/, macos/, web/ # Flutter platform directories
|
linux/, macos/, web/ # Flutter platform directories
|
||||||
ptyc/ # C PTY helper
|
native/ # Vendored native libs (libtree-sitter.so, dugite)
|
||||||
native/ # Vendored native libs (libtree-sitter.so)
|
governance/ # D/Q/R records (decisions/, questions/, rejected/ subdirs)
|
||||||
decisions/ # D/Q/R records
|
|
||||||
docs/ # Design docs, wireframes
|
docs/ # Design docs, wireframes
|
||||||
legacy/ # Python Textual clide v1.2 (frozen)
|
legacy/ # Python Textual clide v1.2 (frozen)
|
||||||
```
|
```
|
||||||
@@ -55,8 +54,8 @@ legacy/ # Python Textual clide v1.2 (frozen)
|
|||||||
## Dependencies & supply chain
|
## Dependencies & supply chain
|
||||||
|
|
||||||
- **Prefer-zero-deps.** Flutter-SDK widgets first; third-party packages need justification. What stays is exact-pinned in `pubspec.yaml` (no caret ranges). Advisories reviewed before every bump; `pubspec.lock` committed.
|
- **Prefer-zero-deps.** Flutter-SDK widgets first; third-party packages need justification. What stays is exact-pinned in `pubspec.yaml` (no caret ranges). Advisories reviewed before every bump; `pubspec.lock` committed.
|
||||||
- **Document every bundled dependency.** Listed in [`assets/licenses.yaml`](assets/licenses.yaml) with name, kind, version, homepage, license, and purpose. Adding a dep is a two-step commit: add the artefact **and** the `licenses.yaml` entry. See [`D-42`](decisions/tooling.md#d-42-bundled-dependencies-documented-in-licensesyaml).
|
- **Document every bundled dependency.** Listed in [`assets/licenses.yaml`](assets/licenses.yaml) with name, kind, version, homepage, license, and purpose. Adding a dep is a two-step commit: add the artefact **and** the `licenses.yaml` entry. See [`D-42`](governance/decisions/tooling.md#d-42-bundled-dependencies-documented-in-licensesyaml).
|
||||||
- **`ptyc` and any future native supporter tool:** no dep graph by design (libc-only for `ptyc`). "Audit" is reading the source before each bump.
|
- **Native deps (dugite, libtree-sitter):** vendored in `native/`, pinned by SHA. Bumps follow the same advisory-review + `licenses.yaml` rule.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
@@ -70,7 +69,6 @@ make test-a11y # accessibility contract tests
|
|||||||
make test-integration# real app boot integration tests
|
make test-integration# real app boot integration tests
|
||||||
make build-linux # flutter build linux
|
make build-linux # flutter build linux
|
||||||
make build-macos # flutter build macos
|
make build-macos # flutter build macos
|
||||||
make ptyc-build # build the ptyc PTY-spawn helper
|
|
||||||
make push-check # pre-push gate: decisions + core + fast tests + a11y
|
make push-check # pre-push gate: decisions + core + fast tests + a11y
|
||||||
make hooks # install the repo's git hooks (one-time setup)
|
make hooks # install the repo's git hooks (one-time setup)
|
||||||
make clean # remove build artefacts
|
make clean # remove build artefacts
|
||||||
@@ -84,4 +82,4 @@ One-time setup on a fresh clone: `make hooks && flutter pub get` once Flutter is
|
|||||||
|
|
||||||
## Open questions
|
## Open questions
|
||||||
|
|
||||||
Open questions live under [`decisions/questions-*.md`](decisions/questions.md).
|
Open questions live under [`governance/questions/`](governance/questions/).
|
||||||
|
|||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
# Contributing to clide
|
||||||
|
|
||||||
|
clide is an IDE for the Claude Code CLI, built as a single Flutter
|
||||||
|
package at the repo root. This guide is for people working on clide
|
||||||
|
itself.
|
||||||
|
|
||||||
|
For day-to-day model-driven work in the codebase, see
|
||||||
|
[`CLAUDE.md`](CLAUDE.md) — it documents the same guardrails from the
|
||||||
|
agent's point of view and is the place to look for "why is the code
|
||||||
|
shaped like this?"
|
||||||
|
|
||||||
|
## One-time setup
|
||||||
|
|
||||||
|
```
|
||||||
|
git clone git@github.com:postmeridiem/clide.git
|
||||||
|
cd clide
|
||||||
|
make hooks && flutter pub get
|
||||||
|
```
|
||||||
|
|
||||||
|
`make hooks` installs the repo's git hooks (pre-commit + post-merge).
|
||||||
|
Flutter must be on the stable channel and on `$PATH`.
|
||||||
|
|
||||||
|
If you haven't used [pql](https://github.com/postmeridiem/pql) before,
|
||||||
|
install it and run `pql init` once in the repo root. pql is a hard
|
||||||
|
dependency for the governance + ticket workflow described below.
|
||||||
|
|
||||||
|
## The five commands you'll actually use
|
||||||
|
|
||||||
|
```
|
||||||
|
make run # launch the desktop app
|
||||||
|
make verify # no-tests sweep — analyze + format + decisions + changelog gate
|
||||||
|
make test # fast suite — analyze + format + unit + widget + golden
|
||||||
|
make test-a11y # WCAG-AA contrast + keyboard traversal contracts
|
||||||
|
make push-check # the pre-push gate; what CI runs
|
||||||
|
make build-linux # release artefact for the host platform
|
||||||
|
```
|
||||||
|
|
||||||
|
`make verify` is the lightweight "are the gates green?" check for
|
||||||
|
mid-edit iteration. `make push-check` is the full pre-push pipeline
|
||||||
|
(verify + every test suite + coverage gate).
|
||||||
|
|
||||||
|
`make` with no target prints the full list. `make test-integration`
|
||||||
|
boots a real app process and is slow; reserve it for the rare change
|
||||||
|
that touches startup wiring.
|
||||||
|
|
||||||
|
The pre-push hook runs `make push-check` automatically. Don't bypass
|
||||||
|
it with `--no-verify` — fix the underlying issue and create a new
|
||||||
|
commit. Pre-push includes:
|
||||||
|
|
||||||
|
- `flutter analyze` (zero warnings)
|
||||||
|
- `dart format --set-exit-if-changed`
|
||||||
|
- the fast unit/widget/golden suites
|
||||||
|
- accessibility contract tests
|
||||||
|
- a coverage floor read from `coverage_floor:` in `pubspec.yaml`
|
||||||
|
(currently 95 %; ratchets up only — see
|
||||||
|
[D-66](governance/decisions/testing.md#d-66))
|
||||||
|
- `CHANGELOG.md` `[Unreleased]` bullets ≤ 60 words each
|
||||||
|
|
||||||
|
## Decisions, questions, rejected (DQR)
|
||||||
|
|
||||||
|
clide tracks architectural commitments as durable records under
|
||||||
|
[`governance/`](governance/):
|
||||||
|
|
||||||
|
- `decisions/<domain>.md` — confirmed decisions (`D-NNN`)
|
||||||
|
- `questions/<domain>.md` — open questions (`Q-NNN`)
|
||||||
|
- `rejected/<domain>.md` — rejected proposals (`R-NNN`)
|
||||||
|
|
||||||
|
When you make a non-trivial architectural choice, write it down:
|
||||||
|
|
||||||
|
```
|
||||||
|
pql decisions claim D <domain> "short title"
|
||||||
|
```
|
||||||
|
|
||||||
|
This reserves a fresh ID and tells you where to add the record. The
|
||||||
|
[governance/README.md](governance/README.md) explains the format and
|
||||||
|
the recommended domain list.
|
||||||
|
|
||||||
|
Pre-push validates that every `D-NNN` / `Q-NNN` / `R-NNN` link in
|
||||||
|
the docs and code resolves to an actual record (`pql decisions
|
||||||
|
validate`). Broken references fail the build.
|
||||||
|
|
||||||
|
## Tickets
|
||||||
|
|
||||||
|
All non-trivial work is tracked in `pql ticket`:
|
||||||
|
|
||||||
|
```
|
||||||
|
pql ticket list --status in_progress
|
||||||
|
pql ticket show T-NNN[,T-NNN…] # batch form on pql 1.4.33+
|
||||||
|
pql ticket new task "title" --parent T-NNN --priority medium
|
||||||
|
pql ticket status T-NNN in_progress
|
||||||
|
pql ticket status T-NNN done
|
||||||
|
```
|
||||||
|
|
||||||
|
A ticket exists for any change a reviewer might want to ask "why?"
|
||||||
|
about. Bug fixes, refactors, and consultant findings all become
|
||||||
|
tickets before the diff lands. Trivial typo fixes don't need one.
|
||||||
|
|
||||||
|
## Commit conventions
|
||||||
|
|
||||||
|
See [D-37](governance/decisions/process.md#d-37) and the bundled
|
||||||
|
[`git-commit` skill](.claude/skills/git-commit/SKILL.md). In short:
|
||||||
|
|
||||||
|
- Imperative subject ≤ 70 chars, no Conventional Commits prefix
|
||||||
|
(this isn't a Conventional Commits repo — the archived Python
|
||||||
|
predecessor under [`legacy/`](legacy/) is, but the rebuild isn't).
|
||||||
|
- One logical change per commit. If the subject needs "and", split it.
|
||||||
|
- Every user-visible commit adds an entry to `CHANGELOG.md` under
|
||||||
|
`[Unreleased]` in the right subsection (Added, Changed, Deprecated,
|
||||||
|
Removed, Fixed, Security). Keep entries to one or two short
|
||||||
|
sentences — the 60-word cap is enforced by `ci/changelog_gate.sh`.
|
||||||
|
- Co-author trailer:
|
||||||
|
`Co-Authored-By: Claude <noreply@anthropic.com>` when Claude wrote
|
||||||
|
any of the diff.
|
||||||
|
|
||||||
|
Never `--amend` a commit unless explicitly asked. Never force-push
|
||||||
|
to `main`. Never `git add -A` / `git add .` when staging — name
|
||||||
|
files explicitly so stray secrets or build artefacts don't sneak in.
|
||||||
|
|
||||||
|
## Cutting a release
|
||||||
|
|
||||||
|
A release is a single commit:
|
||||||
|
|
||||||
|
1. Move all `## [Unreleased]` entries under a new `## [X.Y.Z] —
|
||||||
|
YYYY-MM-DD` heading.
|
||||||
|
2. Leave an empty `## [Unreleased]` skeleton at the top.
|
||||||
|
3. Bump `pubspec.yaml` `version:` to `X.Y.Z` (no `-dev` suffix on
|
||||||
|
the release tag; add it back on the next development commit if
|
||||||
|
you like).
|
||||||
|
4. Commit subject: `release vX.Y.Z`.
|
||||||
|
|
||||||
|
`pubspec.yaml` is the single source of truth for the version — the
|
||||||
|
Makefile reads it for ldflag stamping and the app reads it for build
|
||||||
|
info.
|
||||||
|
|
||||||
|
## What goes where
|
||||||
|
|
||||||
|
- **Bug, feature, sweep:** file/claim a ticket, branch, code, test,
|
||||||
|
commit, push. Push triggers `make push-check`.
|
||||||
|
- **Architectural decision:** `pql decisions claim`, write the
|
||||||
|
record, then file the implementation ticket linked via
|
||||||
|
`decision_ref`.
|
||||||
|
- **Open question:** drop a `Q-NNN` under
|
||||||
|
`governance/questions/<domain>.md`. Triage later.
|
||||||
|
- **Rejected proposal:** drop an `R-NNN` under
|
||||||
|
`governance/rejected/<domain>.md`. Future-you (or a reviewer)
|
||||||
|
will be glad it's written down.
|
||||||
|
|
||||||
|
## Reporting issues
|
||||||
|
|
||||||
|
The public issue tracker lives at
|
||||||
|
<https://github.com/postmeridiem/clide/issues>. The Gitea mirror is
|
||||||
|
read-only.
|
||||||
@@ -79,6 +79,9 @@ analyze: ## flutter analyze.
|
|||||||
format: ## dart format --set-exit-if-changed.
|
format: ## dart format --set-exit-if-changed.
|
||||||
dart format --set-exit-if-changed .
|
dart format --set-exit-if-changed .
|
||||||
|
|
||||||
|
.PHONY: verify
|
||||||
|
verify: analyze format decisions-validate changelog-gate ## No-tests sweep — analyze + format + decisions-validate + changelog-gate. For mid-edit "are the gates green?" checks; `push-check` is the full pre-push pipeline.
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test: ## Fast: analyze + format + unit + widget + golden (<60s).
|
test: ## Fast: analyze + format + unit + widget + golden (<60s).
|
||||||
ci/test.sh
|
ci/test.sh
|
||||||
@@ -102,9 +105,13 @@ test-e2e: ## End-to-end Playwright smoke.
|
|||||||
.PHONY: test-all
|
.PHONY: test-all
|
||||||
test-all: test-core test test-a11y test-integration test-e2e ## Everything, sequentially.
|
test-all: test-core test test-a11y test-integration test-e2e ## Everything, sequentially.
|
||||||
|
|
||||||
.PHONY: coverage
|
.PHONY: coverage-gate
|
||||||
coverage: ## flutter test --coverage + lcov summary.
|
coverage-gate: ## Coverage gate — fails if total line % < pubspec.yaml `coverage_floor:` (D-66). Assumes `make test` ran first.
|
||||||
ci/test_coverage.sh
|
ci/coverage_gate.sh
|
||||||
|
|
||||||
|
.PHONY: changelog-gate
|
||||||
|
changelog-gate: ## Changelog concision gate — fails on `## [Unreleased]` bullets over 60 words.
|
||||||
|
ci/changelog_gate.sh
|
||||||
|
|
||||||
.PHONY: smoke-bundle
|
.PHONY: smoke-bundle
|
||||||
smoke-bundle: ## Build Linux release bundle and run it under xvfb for 5s.
|
smoke-bundle: ## Build Linux release bundle and run it under xvfb for 5s.
|
||||||
@@ -234,49 +241,23 @@ dugite-fetch: ## Download and extract the dugite-native git distribution.
|
|||||||
dugite-clean: ## Remove the dugite-native directory.
|
dugite-clean: ## Remove the dugite-native directory.
|
||||||
rm -rf $(DUGITE_DIR)
|
rm -rf $(DUGITE_DIR)
|
||||||
|
|
||||||
# -- ptyc (C supporter tool) ---------------------------------------------
|
|
||||||
|
|
||||||
PTYC_PRESENT := $(shell test -f ptyc/Makefile && echo yes || echo no)
|
|
||||||
|
|
||||||
.PHONY: ptyc-build
|
|
||||||
ptyc-build: ## Build the ptyc PTY-spawn helper.
|
|
||||||
ifeq ($(PTYC_PRESENT),yes)
|
|
||||||
$(MAKE) -C ptyc
|
|
||||||
else
|
|
||||||
@echo "(ptyc/ not scaffolded yet; skipping)"
|
|
||||||
endif
|
|
||||||
|
|
||||||
.PHONY: ptyc-test
|
|
||||||
ptyc-test: ## Run ptyc smoke tests (SCM_RIGHTS round-trip).
|
|
||||||
ifeq ($(PTYC_PRESENT),yes)
|
|
||||||
$(MAKE) -C ptyc test
|
|
||||||
else
|
|
||||||
@echo "(ptyc/ not scaffolded yet; skipping)"
|
|
||||||
endif
|
|
||||||
|
|
||||||
.PHONY: ptyc-clean
|
|
||||||
ptyc-clean: ## Clean ptyc build artefacts.
|
|
||||||
ifeq ($(PTYC_PRESENT),yes)
|
|
||||||
$(MAKE) -C ptyc clean
|
|
||||||
else
|
|
||||||
@echo "(ptyc/ not scaffolded yet; skipping)"
|
|
||||||
endif
|
|
||||||
|
|
||||||
# -- security -------------------------------------------------------------
|
# -- security -------------------------------------------------------------
|
||||||
|
|
||||||
.PHONY: security
|
.PHONY: security
|
||||||
security: ## Dart advisory review + ptyc source review.
|
security: ## Dart advisory review.
|
||||||
@echo "security: Dart advisories reviewed manually before pubspec.yaml bumps;"
|
@echo "security: Dart advisories reviewed manually before pubspec.yaml bumps."
|
||||||
@echo " ptyc is reviewed by reading it (tiny libc-only C)."
|
|
||||||
|
|
||||||
# -- pre-push gate --------------------------------------------------------
|
# -- pre-push gate --------------------------------------------------------
|
||||||
|
|
||||||
.PHONY: decisions-validate
|
.PHONY: decisions-validate
|
||||||
decisions-validate: ## Parser dry-run over decisions/*.md.
|
decisions-validate: ## Parser dry-run over governance/{decisions,questions,rejected}/*.md.
|
||||||
pql decisions validate
|
pql decisions validate
|
||||||
|
|
||||||
.PHONY: push-check
|
.PHONY: push-check
|
||||||
push-check: decisions-validate test-core test test-a11y ## Pre-push gate.
|
push-check: decisions-validate test-core test test-a11y coverage-gate changelog-gate ## Pre-push gate (fast — <2 min target).
|
||||||
|
|
||||||
|
.PHONY: push-check-full
|
||||||
|
push-check-full: push-check test-integration smoke-bundle ## Pre-release gate (push-check + integration + smoke; slower; skips theme_picker per T-116).
|
||||||
|
|
||||||
.PHONY: hooks
|
.PHONY: hooks
|
||||||
hooks: ## Install the repo's git hooks.
|
hooks: ## Install the repo's git hooks.
|
||||||
@@ -288,6 +269,5 @@ hooks: ## Install the repo's git hooks.
|
|||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean: ## Remove build artefacts.
|
clean: ## Remove build artefacts.
|
||||||
rm -rf build .dart_tool
|
rm -rf build .dart_tool
|
||||||
$(MAKE) ptyc-clean
|
|
||||||
|
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ An IDE for Claude Code CLI. Native rendering, terminal-first interaction, pql-po
|
|||||||
|
|
||||||
## 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.
|
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).
|
||||||
|
|
||||||
- **`lib/`** — all Dart code. Kernel services (theme, i18n, settings, panels, commands, focus), UI widgets, built-in extensions, and the extension contract.
|
- **`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/`).
|
||||||
- **`ptyc/`** — small C helper. Spawns a PTY + child and hands the master fd back over `SCM_RIGHTS`. Every pane (shell, tmux, claude, LSP, debug adapter) goes through it.
|
- **PTY** — `lib/src/pty/` spawns child processes via Dart FFI `posix_openpt()` + `posix_spawn()` directly; no external helper binary.
|
||||||
|
- **`native/`** — vendored native libraries (`libtree-sitter.so` with wasmtime embedded). Linux only today.
|
||||||
- **[pql](https://github.com/postmeridiem/pql)** — external supporter tool. Clide wraps it for every query surface; never re-implements it.
|
- **[pql](https://github.com/postmeridiem/pql)** — external supporter tool. Clide wraps it for every query surface; never re-implements it.
|
||||||
|
|
||||||
Claude drives the UI through a `clide` CLI surface (Bash, not MCP). Every CLI subcommand has a UI affordance and every UI action has a CLI equivalent.
|
Claude drives the UI through a `clide` CLI surface (Bash, not MCP). Every CLI subcommand has a UI affordance and every UI action has a CLI equivalent (D-6).
|
||||||
|
|
||||||
## Built-in extensions
|
## Built-in extensions
|
||||||
|
|
||||||
@@ -30,19 +31,25 @@ Then:
|
|||||||
make run # launch the desktop app
|
make run # launch the desktop app
|
||||||
make test # fast suite: analyze + format + unit + widget + golden
|
make test # fast suite: analyze + format + unit + widget + golden
|
||||||
make test-core # core subsystem tests (IPC, PTY, git, pane registry)
|
make test-core # core subsystem tests (IPC, PTY, git, pane registry)
|
||||||
|
make test-a11y # accessibility contract tests
|
||||||
make test-integration # real app boot integration tests
|
make test-integration # real app boot integration tests
|
||||||
make build-linux # flutter build linux
|
make build-linux # flutter build linux
|
||||||
make build-macos # flutter build macos
|
make build-macos # flutter build macos
|
||||||
make ptyc-build # build the ptyc PTY-spawn helper
|
make push-check # pre-push gate: decisions + core + fast + a11y + coverage + changelog
|
||||||
make push-check # pre-push gate: decisions + core + fast tests
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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).
|
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).
|
||||||
|
|
||||||
Design doc: [`docs/initial-plan.md`](https://github.com/postmeridiem/clide/blob/main/docs/initial-plan.md). Architectural decisions: [`decisions/`](https://github.com/postmeridiem/clide/tree/main/decisions).
|
## Documentation
|
||||||
|
|
||||||
|
- [`docs/architecture.md`](docs/architecture.md) — current architecture (read this first).
|
||||||
|
- [`CONTRIBUTING.md`](CONTRIBUTING.md) — how to clone, build, test, file tickets, and write D-records.
|
||||||
|
- [`docs/initial-plan.md`](docs/initial-plan.md) — historical design doc; preserved as a snapshot of the 2026-04 plan, much of it now superseded.
|
||||||
|
- [`governance/decisions/`](governance/decisions/) — confirmed decisions (`D-NNN`), open questions (`Q-NNN`), rejected alternatives (`R-NNN`).
|
||||||
|
- [`CLAUDE.md`](CLAUDE.md) — Claude-addressed working notes (guardrails, repo layout).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT. See [`LICENSE`](https://github.com/postmeridiem/clide/blob/main/LICENSE).
|
MIT. See [`LICENSE`](LICENSE).
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# clide default keymap.
|
||||||
|
#
|
||||||
|
# This is the baseline preset. Vim / VSCode / JetBrains presets are
|
||||||
|
# their own files (T-64/T-65/T-66) and replace bindings via the same
|
||||||
|
# YAML shape.
|
||||||
|
#
|
||||||
|
# Each binding:
|
||||||
|
# intent: <stable intent id> # see kernel/src/keymap/intents.dart
|
||||||
|
# keys: <chord> | [<chord>, ...] # `ctrl+shift+p`, `cmd+enter`, ...
|
||||||
|
# when: <when-clause> # optional; VS-Code-style boolean expr
|
||||||
|
#
|
||||||
|
# Identifiers in `when:` are scope flags published by producing
|
||||||
|
# services (e.g. `palette.open` when ClidePalette is mounted +
|
||||||
|
# visible). Missing flags evaluate to false.
|
||||||
|
|
||||||
|
name: default
|
||||||
|
|
||||||
|
bindings:
|
||||||
|
# -- Activation / focus -----------------------------------------------
|
||||||
|
# ActivateIntent has no when-clause: it dispatches via Actions.maybeInvoke
|
||||||
|
# against the focused context, so only widgets that opt in (ClideTappable's
|
||||||
|
# Actions wrapper) actually catch it. Text-input widgets handle Enter
|
||||||
|
# themselves first.
|
||||||
|
- intent: activate
|
||||||
|
keys: [enter, space]
|
||||||
|
- intent: dismiss
|
||||||
|
keys: escape
|
||||||
|
- intent: focus.next
|
||||||
|
keys: tab
|
||||||
|
- intent: focus.previous
|
||||||
|
keys: shift+tab
|
||||||
|
|
||||||
|
# Panel-to-panel cycling (sidebar → workspace → context).
|
||||||
|
# VS Code convention.
|
||||||
|
- intent: focus.nextPanel
|
||||||
|
keys: f6
|
||||||
|
- intent: focus.previousPanel
|
||||||
|
keys: shift+f6
|
||||||
|
|
||||||
|
# -- Command palette --------------------------------------------------
|
||||||
|
- intent: palette.open
|
||||||
|
keys: [ctrl+shift+p, meta+shift+p]
|
||||||
|
- intent: palette.selectNext
|
||||||
|
keys: [down, ctrl+n]
|
||||||
|
when: palette.open
|
||||||
|
- intent: palette.selectPrevious
|
||||||
|
keys: [up, ctrl+p]
|
||||||
|
when: palette.open
|
||||||
|
- intent: palette.accept
|
||||||
|
keys: enter
|
||||||
|
when: palette.open
|
||||||
|
- intent: dismiss
|
||||||
|
keys: escape
|
||||||
|
when: palette.open
|
||||||
|
|
||||||
|
# -- Text scale -------------------------------------------------------
|
||||||
|
# On most layouts `+` is `shift+equal`; we bind both so users who
|
||||||
|
# think of it as Ctrl+Plus and users who hit Ctrl+= both work.
|
||||||
|
- intent: text.scaleIncrease
|
||||||
|
keys: [ctrl+equal, ctrl+shift+equal, meta+equal, meta+shift+equal]
|
||||||
|
- intent: text.scaleDecrease
|
||||||
|
keys: [ctrl+minus, meta+minus]
|
||||||
|
- intent: text.scaleReset
|
||||||
|
keys: [ctrl+0, meta+0]
|
||||||
+4
-14
@@ -53,7 +53,7 @@ dependencies:
|
|||||||
purpose: >-
|
purpose: >-
|
||||||
Monospace face for terminal panes, diff views, code editors, and
|
Monospace face for terminal panes, diff views, code editors, and
|
||||||
any other monospace surface.
|
any other monospace surface.
|
||||||
weights_bundled: [Regular, Italic]
|
weights_bundled: [Regular, Italic, Bold, BoldItalic]
|
||||||
|
|
||||||
- name: Josefin Sans
|
- name: Josefin Sans
|
||||||
kind: font
|
kind: font
|
||||||
@@ -216,21 +216,11 @@ dev_dependencies:
|
|||||||
license: BSD-3-Clause
|
license: BSD-3-Clause
|
||||||
purpose: Flutter-team-recommended analyzer lint set (app/).
|
purpose: Flutter-team-recommended analyzer lint set (app/).
|
||||||
|
|
||||||
- name: lints
|
|
||||||
kind: dart-package
|
|
||||||
version: "5.0.0"
|
|
||||||
homepage: https://pub.dev/packages/lints
|
|
||||||
license: BSD-3-Clause
|
|
||||||
purpose: >-
|
|
||||||
Dart-team-recommended analyzer lint set for the Flutter-free
|
|
||||||
core package at the repo root.
|
|
||||||
|
|
||||||
- name: test
|
- name: test
|
||||||
kind: dart-package
|
kind: dart-package
|
||||||
version: "1.25.8"
|
version: "1.30.0"
|
||||||
homepage: https://pub.dev/packages/test
|
homepage: https://pub.dev/packages/test
|
||||||
license: BSD-3-Clause
|
license: BSD-3-Clause
|
||||||
purpose: >-
|
purpose: >-
|
||||||
Dart test runner for the core package (Flutter-free; the app
|
Dart test runner for the Flutter-free PTY tests under
|
||||||
uses flutter_test from the Flutter SDK for widget + golden
|
`dart test --tags forkpty` (flutter_test is used elsewhere).
|
||||||
coverage).
|
|
||||||
|
|||||||
-436
@@ -1,436 +0,0 @@
|
|||||||
// clide — CLI + daemon entry point.
|
|
||||||
//
|
|
||||||
// One binary, two modes (per D-005):
|
|
||||||
// * `clide <subcommand>` — one-shot; connects to the daemon socket,
|
|
||||||
// sends a request, prints the response, exits with the D-006
|
|
||||||
// exit code.
|
|
||||||
// * `clide --daemon` — long-running; owns the socket, dispatches
|
|
||||||
// requests. Subsystems (pane, files, editor, …) register handlers
|
|
||||||
// at boot.
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:clide/clide.dart';
|
|
||||||
import 'package:clide/kernel/src/toolchain.dart';
|
|
||||||
import 'package:clide/src/git/client.dart';
|
|
||||||
// Daemon-only deep imports — these pull in dart:ffi (PTY) and
|
|
||||||
// daemon-subsystem wiring that the Flutter app doesn't need and
|
|
||||||
// can't compile for web. See lib/clide.dart for the barrel split.
|
|
||||||
import 'package:clide/src/daemon/editor_commands.dart';
|
|
||||||
import 'package:clide/src/daemon/files_commands.dart';
|
|
||||||
import 'package:clide/src/daemon/git_commands.dart';
|
|
||||||
import 'package:clide/src/daemon/pane_commands.dart';
|
|
||||||
import 'package:clide/src/daemon/pql_commands.dart';
|
|
||||||
import 'package:clide/src/editor/registry.dart' show EditorRegistry;
|
|
||||||
import 'package:clide/src/panes/registry.dart';
|
|
||||||
import 'package:clide/src/pql/client.dart';
|
|
||||||
|
|
||||||
Future<void> main(List<String> argv) async {
|
|
||||||
if (argv.isEmpty) {
|
|
||||||
_printHelp(stdout);
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (argv.first == '--daemon') {
|
|
||||||
await _runDaemon(argv.sublist(1));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tier-2 single-word shortcuts (per CLAUDE.md). Each maps a flat
|
|
||||||
// positional argv into the structured IPC shape of the canonical
|
|
||||||
// editor.* / pane.* verb. Keeps Claude's tool-use pattern short.
|
|
||||||
final rest = argv.sublist(1);
|
|
||||||
switch (argv.first) {
|
|
||||||
case '--version':
|
|
||||||
case 'version':
|
|
||||||
await _runCliArgs('version', const {}, exitOnOk: true);
|
|
||||||
case '--help':
|
|
||||||
case '-h':
|
|
||||||
case 'help':
|
|
||||||
_printHelp(stdout);
|
|
||||||
exit(0);
|
|
||||||
case 'ping':
|
|
||||||
await _runCliArgs('ping', const {}, exitOnOk: true);
|
|
||||||
case 'open':
|
|
||||||
if (rest.isEmpty) _die('usage: clide open <path>');
|
|
||||||
await _runCliArgs('editor.open', {'path': rest.first}, exitOnOk: true);
|
|
||||||
case 'active':
|
|
||||||
await _runCliArgs('editor.active', const {}, exitOnOk: true);
|
|
||||||
case 'insert':
|
|
||||||
final text = await _readTextArg(rest);
|
|
||||||
await _runCliArgs('editor.insert', {'text': text}, exitOnOk: true);
|
|
||||||
case 'replace-selection':
|
|
||||||
final text = await _readTextArg(rest);
|
|
||||||
await _runCliArgs(
|
|
||||||
'editor.replace-selection',
|
|
||||||
{'text': text},
|
|
||||||
exitOnOk: true,
|
|
||||||
);
|
|
||||||
case 'save':
|
|
||||||
await _runCliArgs('editor.save', const {}, exitOnOk: true);
|
|
||||||
case 'git':
|
|
||||||
await _runGit(rest);
|
|
||||||
case 'tail':
|
|
||||||
await _runTail(rest);
|
|
||||||
default:
|
|
||||||
// Unknown-to-the-CLI commands still go over IPC — the daemon is
|
|
||||||
// authoritative about what's registered. Lets extensions add
|
|
||||||
// subcommands without the CLI caring. Args forward as-is under
|
|
||||||
// {argv: [...]} so daemon-side can parse whatever shape it wants.
|
|
||||||
await _runCliArgs(
|
|
||||||
argv.first,
|
|
||||||
{'argv': rest},
|
|
||||||
exitOnOk: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _printHelp(IOSink sink) {
|
|
||||||
sink.writeln('''
|
|
||||||
clide $clideVersion — IDE for Claude Code CLI.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
clide --daemon Run the long-running daemon process.
|
|
||||||
clide <subcommand> Run a one-shot subcommand against the daemon.
|
|
||||||
|
|
||||||
Built-in subcommands:
|
|
||||||
ping Round-trip a ping to the daemon.
|
|
||||||
version Print the clide version.
|
|
||||||
help Print this help.
|
|
||||||
|
|
||||||
Editor (tier 2):
|
|
||||||
open <path> Open a file in the editor (editor.open).
|
|
||||||
active Print the active buffer (editor.active).
|
|
||||||
insert <text|-> Insert text at the cursor in the active buffer.
|
|
||||||
`-` reads text from stdin.
|
|
||||||
replace-selection <…> Replace the selected text in the active buffer.
|
|
||||||
`-` reads text from stdin.
|
|
||||||
save Save the active buffer (editor.save).
|
|
||||||
|
|
||||||
Git (tier 3):
|
|
||||||
git status Working tree status (staged/unstaged/conflicts).
|
|
||||||
git diff [--staged] [P] Diff for file(s) P, or all if omitted.
|
|
||||||
git stage <paths…> Stage files (git add).
|
|
||||||
git stage-all Stage everything (git add -A).
|
|
||||||
git unstage [paths…] Unstage files (git reset HEAD).
|
|
||||||
git discard <paths…> Discard unstaged changes in files.
|
|
||||||
git commit "<msg>" Commit staged changes.
|
|
||||||
git log [--count N] Recent commit log (default 20).
|
|
||||||
git stash [--message M] Stash working changes.
|
|
||||||
git stash-pop Pop the top stash entry.
|
|
||||||
git pull Pull from remote.
|
|
||||||
git push [remote] [br] Push to remote.
|
|
||||||
|
|
||||||
Event subscription:
|
|
||||||
tail --events [--filter SUBSYSTEM[:ID]]
|
|
||||||
Stream events as JSON lines. --filter keeps
|
|
||||||
only events from one subsystem, optionally
|
|
||||||
narrowed to a single id. Exits on SIGINT.
|
|
||||||
|
|
||||||
Any other subcommand is forwarded to the daemon; registered handlers
|
|
||||||
(e.g. `clide git status` once `builtin.git` lands) resolve there.
|
|
||||||
Matches D-006's exit-code contract:
|
|
||||||
0 success · 1 user-error · 2 tool-error · 3 not-found · 4 conflict
|
|
||||||
''');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _runDaemon(List<String> args) async {
|
|
||||||
final socketPath = defaultSocketPath();
|
|
||||||
final dispatcher = DaemonDispatcher();
|
|
||||||
late final DaemonServer server;
|
|
||||||
server = DaemonServer(
|
|
||||||
socketPath: socketPath,
|
|
||||||
dispatch: dispatcher.dispatch,
|
|
||||||
);
|
|
||||||
final toolchain = Toolchain();
|
|
||||||
toolchain.applyResolved(Toolchain.resolvePaths(workspaceRoot: Directory.current.path));
|
|
||||||
|
|
||||||
final events = _ServerEventSink(server);
|
|
||||||
final registry = PaneRegistry(events: events);
|
|
||||||
registerPaneCommands(dispatcher, registry);
|
|
||||||
|
|
||||||
final files = FilesService.atCwd(events: events);
|
|
||||||
registerFilesCommands(dispatcher, files);
|
|
||||||
|
|
||||||
final editor = EditorRegistry(events: events, workspaceRoot: files.root);
|
|
||||||
registerEditorCommands(dispatcher, editor);
|
|
||||||
final gitClient = GitClient(toolchain: toolchain, workDir: files.root);
|
|
||||||
registerGitCommands(dispatcher, gitClient, events);
|
|
||||||
|
|
||||||
final pql = PqlClient(workDir: files.root, toolchain: toolchain);
|
|
||||||
registerPqlCommands(dispatcher, pql);
|
|
||||||
|
|
||||||
final stopping = Completer<void>();
|
|
||||||
void shutdown(ProcessSignal sig) {
|
|
||||||
if (!stopping.isCompleted) {
|
|
||||||
stderr.writeln('clide daemon: received ${sig.toString()}, shutting down');
|
|
||||||
stopping.complete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ProcessSignal.sigint.watch().listen(shutdown);
|
|
||||||
ProcessSignal.sigterm.watch().listen(shutdown);
|
|
||||||
|
|
||||||
await server.start();
|
|
||||||
await stopping.future;
|
|
||||||
await registry.shutdown();
|
|
||||||
await editor.shutdown();
|
|
||||||
await files.shutdown();
|
|
||||||
await server.stop();
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Thin adapter: the server doesn't `implement DaemonEventSink` itself
|
|
||||||
/// (that would tie ipc/ to panes/); instead the daemon entrypoint wraps
|
|
||||||
/// it at the seam where both are known.
|
|
||||||
class _ServerEventSink implements DaemonEventSink {
|
|
||||||
_ServerEventSink(this._server);
|
|
||||||
final DaemonServer _server;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void emit(IpcEvent event) => _server.broadcast(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// CLI helpers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Read the "text" argument for insert / replace-selection. A lone
|
|
||||||
/// `-` means "slurp stdin"; anything else is concatenated into the
|
|
||||||
/// text body (so `clide insert hello world` emits "hello world").
|
|
||||||
Future<String> _readTextArg(List<String> rest) async {
|
|
||||||
if (rest.isEmpty) _die('usage: clide <verb> <text> (or `-` to read stdin)');
|
|
||||||
if (rest.length == 1 && rest.first == '-') {
|
|
||||||
final bytes = <int>[];
|
|
||||||
await for (final chunk in stdin) {
|
|
||||||
bytes.addAll(chunk);
|
|
||||||
}
|
|
||||||
return utf8.decode(bytes);
|
|
||||||
}
|
|
||||||
return rest.join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Socket> _connectSocket() async {
|
|
||||||
final socketPath = defaultSocketPath();
|
|
||||||
try {
|
|
||||||
return await Socket.connect(
|
|
||||||
InternetAddress(socketPath, type: InternetAddressType.unix),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
} catch (_) {
|
|
||||||
_emitError(
|
|
||||||
code: IpcExitCode.toolError,
|
|
||||||
kind: IpcErrorKind.toolError,
|
|
||||||
message: 'daemon not reachable at $socketPath',
|
|
||||||
hint: 'run `clide --daemon` in another terminal.',
|
|
||||||
);
|
|
||||||
exit(IpcExitCode.toolError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _runCliArgs(
|
|
||||||
String cmd,
|
|
||||||
Map<String, Object?> args, {
|
|
||||||
required bool exitOnOk,
|
|
||||||
}) async {
|
|
||||||
final socket = await _connectSocket();
|
|
||||||
final request = IpcRequest(id: '1', cmd: cmd, args: args);
|
|
||||||
socket.writeln(request.encode());
|
|
||||||
|
|
||||||
// Responses come back on the same socket. Events may be interleaved
|
|
||||||
// (the daemon broadcasts), so we skip events until we see the
|
|
||||||
// response whose id matches our request.
|
|
||||||
final lines = socket.cast<List<int>>().transform(utf8.decoder).transform(const LineSplitter());
|
|
||||||
|
|
||||||
try {
|
|
||||||
await for (final line in lines) {
|
|
||||||
if (line.isEmpty) continue;
|
|
||||||
final msg = IpcMessage.decode(line);
|
|
||||||
if (msg is! IpcResponse) continue;
|
|
||||||
if (msg.id != request.id) continue;
|
|
||||||
await socket.close();
|
|
||||||
if (msg.ok) {
|
|
||||||
stdout.writeln(jsonEncode(msg.data));
|
|
||||||
if (exitOnOk) exit(IpcExitCode.ok);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
final err = msg.error!;
|
|
||||||
_emitError(
|
|
||||||
code: err.code,
|
|
||||||
kind: err.kind,
|
|
||||||
message: err.message,
|
|
||||||
hint: err.hint,
|
|
||||||
);
|
|
||||||
exit(err.code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_emitError(
|
|
||||||
code: IpcExitCode.toolError,
|
|
||||||
kind: IpcErrorKind.toolError,
|
|
||||||
message: 'daemon closed socket before responding',
|
|
||||||
);
|
|
||||||
exit(IpcExitCode.toolError);
|
|
||||||
} on FormatException catch (e) {
|
|
||||||
_emitError(
|
|
||||||
code: IpcExitCode.toolError,
|
|
||||||
kind: IpcErrorKind.toolError,
|
|
||||||
message: 'bad response from daemon: $e',
|
|
||||||
);
|
|
||||||
exit(IpcExitCode.toolError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `clide git <verb> [args]` — maps to `git.*` IPC verbs.
|
|
||||||
Future<void> _runGit(List<String> args) async {
|
|
||||||
if (args.isEmpty) {
|
|
||||||
_die('usage: clide git <verb> [args…]');
|
|
||||||
}
|
|
||||||
switch (args.first) {
|
|
||||||
case 'status':
|
|
||||||
await _runCliArgs('git.status', const {}, exitOnOk: true);
|
|
||||||
case 'diff':
|
|
||||||
final staged = args.contains('--staged');
|
|
||||||
final paths = args.sublist(1).where((a) => !a.startsWith('--')).toList();
|
|
||||||
await _runCliArgs(
|
|
||||||
'git.diff',
|
|
||||||
{'staged': staged, if (paths.isNotEmpty) 'paths': paths},
|
|
||||||
exitOnOk: true,
|
|
||||||
);
|
|
||||||
case 'stage':
|
|
||||||
final paths = args.sublist(1);
|
|
||||||
if (paths.isEmpty) _die('usage: clide git stage <path…>');
|
|
||||||
await _runCliArgs('git.stage', {'paths': paths}, exitOnOk: true);
|
|
||||||
case 'stage-all':
|
|
||||||
await _runCliArgs('git.stage-all', const {}, exitOnOk: true);
|
|
||||||
case 'unstage':
|
|
||||||
final paths = args.sublist(1);
|
|
||||||
await _runCliArgs('git.unstage', {'paths': paths}, exitOnOk: true);
|
|
||||||
case 'discard':
|
|
||||||
final paths = args.sublist(1);
|
|
||||||
if (paths.isEmpty) _die('usage: clide git discard <path…>');
|
|
||||||
await _runCliArgs('git.discard', {'paths': paths}, exitOnOk: true);
|
|
||||||
case 'commit':
|
|
||||||
if (args.length < 2) _die('usage: clide git commit "<message>"');
|
|
||||||
final message = args.sublist(1).join(' ');
|
|
||||||
await _runCliArgs('git.commit', {'message': message}, exitOnOk: true);
|
|
||||||
case 'log':
|
|
||||||
var count = 20;
|
|
||||||
for (var i = 1; i < args.length; i++) {
|
|
||||||
if (args[i] == '--count' && i + 1 < args.length) {
|
|
||||||
count = int.tryParse(args[i + 1]) ?? 20;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await _runCliArgs('git.log', {'count': count}, exitOnOk: true);
|
|
||||||
case 'stash':
|
|
||||||
String? message;
|
|
||||||
for (var i = 1; i < args.length; i++) {
|
|
||||||
if (args[i] == '--message' && i + 1 < args.length) {
|
|
||||||
message = args[i + 1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await _runCliArgs(
|
|
||||||
'git.stash',
|
|
||||||
{if (message != null) 'message': message},
|
|
||||||
exitOnOk: true,
|
|
||||||
);
|
|
||||||
case 'stash-pop':
|
|
||||||
await _runCliArgs('git.stash-pop', const {}, exitOnOk: true);
|
|
||||||
case 'pull':
|
|
||||||
await _runCliArgs('git.pull', const {}, exitOnOk: true);
|
|
||||||
case 'push':
|
|
||||||
final rest = args.sublist(1);
|
|
||||||
final setUpstream = rest.contains('-u');
|
|
||||||
final positional = rest.where((a) => a != '-u').toList();
|
|
||||||
await _runCliArgs(
|
|
||||||
'git.push',
|
|
||||||
{
|
|
||||||
if (setUpstream) 'setUpstream': true,
|
|
||||||
if (positional.isNotEmpty) 'remote': positional[0],
|
|
||||||
if (positional.length > 1) 'branch': positional[1],
|
|
||||||
},
|
|
||||||
exitOnOk: true);
|
|
||||||
default:
|
|
||||||
_die('unknown git verb: ${args.first}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `clide tail --events [--filter SUBSYSTEM[:ID]]` — stream events.
|
|
||||||
Future<void> _runTail(List<String> args) async {
|
|
||||||
// Parse flags: --events (required today; keeps us honest when more
|
|
||||||
// modes like --history land), --filter SUBSYSTEM[:ID].
|
|
||||||
var wantEvents = false;
|
|
||||||
String? filterSubsystem;
|
|
||||||
String? filterId;
|
|
||||||
for (var i = 0; i < args.length; i++) {
|
|
||||||
final a = args[i];
|
|
||||||
if (a == '--events') {
|
|
||||||
wantEvents = true;
|
|
||||||
} else if (a == '--filter') {
|
|
||||||
if (i + 1 >= args.length) _die('--filter requires an argument');
|
|
||||||
final spec = args[++i];
|
|
||||||
final colon = spec.indexOf(':');
|
|
||||||
if (colon < 0) {
|
|
||||||
filterSubsystem = spec;
|
|
||||||
} else {
|
|
||||||
filterSubsystem = spec.substring(0, colon);
|
|
||||||
filterId = spec.substring(colon + 1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_die('unknown argument: $a');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!wantEvents) _die('clide tail: pass --events');
|
|
||||||
|
|
||||||
final socket = await _connectSocket();
|
|
||||||
|
|
||||||
// Shutdown on SIGINT / SIGTERM — close the socket so the stream
|
|
||||||
// drains and we exit cleanly.
|
|
||||||
void quit() {
|
|
||||||
unawaited(socket.close());
|
|
||||||
}
|
|
||||||
|
|
||||||
ProcessSignal.sigint.watch().listen((_) => quit());
|
|
||||||
ProcessSignal.sigterm.watch().listen((_) => quit());
|
|
||||||
|
|
||||||
final lines = socket.cast<List<int>>().transform(utf8.decoder).transform(const LineSplitter());
|
|
||||||
|
|
||||||
try {
|
|
||||||
await for (final line in lines) {
|
|
||||||
if (line.isEmpty) continue;
|
|
||||||
IpcMessage msg;
|
|
||||||
try {
|
|
||||||
msg = IpcMessage.decode(line);
|
|
||||||
} on FormatException {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (msg is! IpcEvent) continue;
|
|
||||||
if (filterSubsystem != null && msg.subsystem != filterSubsystem) continue;
|
|
||||||
if (filterId != null && msg.data['id'] != filterId) continue;
|
|
||||||
stdout.writeln(line);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
await socket.close();
|
|
||||||
}
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _emitError({
|
|
||||||
required int code,
|
|
||||||
required String kind,
|
|
||||||
required String message,
|
|
||||||
String? hint,
|
|
||||||
}) {
|
|
||||||
final err = IpcError(code: code, kind: kind, message: message, hint: hint);
|
|
||||||
stderr.writeln(jsonEncode(err.toJson()));
|
|
||||||
}
|
|
||||||
|
|
||||||
Never _die(String msg) {
|
|
||||||
_emitError(
|
|
||||||
code: IpcExitCode.userError,
|
|
||||||
kind: IpcErrorKind.userError,
|
|
||||||
message: msg,
|
|
||||||
);
|
|
||||||
exit(IpcExitCode.userError);
|
|
||||||
}
|
|
||||||
Executable
+73
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# CHANGELOG concision gate — enforces a single 60-word per-bullet hard
|
||||||
|
# cap on the `## [Unreleased]` section. Released sections are frozen
|
||||||
|
# and skipped (don't penalize historical entries pre-dating the rule).
|
||||||
|
#
|
||||||
|
# A "bullet" is a markdown list item beginning with `- `, including any
|
||||||
|
# indented continuation lines until the next bullet, blank line, or
|
||||||
|
# heading. Word count is whitespace-tokenized.
|
||||||
|
#
|
||||||
|
# A soft 40-word warning was tried earlier and dropped — warnings that
|
||||||
|
# never block a push just normalise drift, so the gate is now binary.
|
||||||
|
#
|
||||||
|
# Bypass: never. If the rule rejects something genuinely user-visible
|
||||||
|
# that needs more context, the context belongs in the commit body or a
|
||||||
|
# D-record — see .claude/skills/git-commit/SKILL.md.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
CHANGELOG=CHANGELOG.md
|
||||||
|
HARD_CAP=60
|
||||||
|
|
||||||
|
if [[ ! -f "$CHANGELOG" ]]; then
|
||||||
|
echo "==> changelog gate: $CHANGELOG missing" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
awk -v hard="$HARD_CAP" '
|
||||||
|
BEGIN { in_unreleased = 0; bullet = ""; bullet_start = 0; fail = 0 }
|
||||||
|
|
||||||
|
function check_bullet() {
|
||||||
|
if (bullet == "") return
|
||||||
|
# Trim the leading "- " marker, then tokenize on whitespace.
|
||||||
|
gsub(/^- +/, "", bullet)
|
||||||
|
n = split(bullet, _words, /[[:space:]]+/)
|
||||||
|
if (n > hard) {
|
||||||
|
printf "FAIL line %d: bullet is %d words (cap %d)\n", bullet_start, n, hard
|
||||||
|
printf " %s\n\n", substr(bullet, 1, 120) (length(bullet) > 120 ? "..." : "")
|
||||||
|
fail++
|
||||||
|
}
|
||||||
|
bullet = ""
|
||||||
|
bullet_start = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/^## \[Unreleased\]/ { in_unreleased = 1; next }
|
||||||
|
/^## \[/ && in_unreleased { check_bullet(); in_unreleased = 0; exit_loop = 1 }
|
||||||
|
!in_unreleased { next }
|
||||||
|
|
||||||
|
# Within Unreleased.
|
||||||
|
/^### / { check_bullet(); next } # subsection header
|
||||||
|
/^[[:space:]]*$/ { check_bullet(); next } # blank line ends bullet
|
||||||
|
/^- / { # new bullet
|
||||||
|
check_bullet()
|
||||||
|
bullet = $0
|
||||||
|
bullet_start = NR
|
||||||
|
next
|
||||||
|
}
|
||||||
|
/^[[:space:]]+/ && bullet != "" { # continuation of current bullet
|
||||||
|
bullet = bullet " " $0
|
||||||
|
next
|
||||||
|
}
|
||||||
|
|
||||||
|
END {
|
||||||
|
check_bullet()
|
||||||
|
if (fail > 0) {
|
||||||
|
printf "\n==> changelog gate FAIL: %d bullet(s) over %d words.\n", fail, hard
|
||||||
|
printf " Trim them. Rationale, probe results, behavior-change deep dives\n"
|
||||||
|
printf " belong in the commit body, a D-record, or the ticket — not here.\n"
|
||||||
|
printf " See .claude/skills/git-commit/SKILL.md \"Be concise\".\n"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
printf "==> changelog gate OK\n"
|
||||||
|
}
|
||||||
|
' "$CHANGELOG"
|
||||||
Executable
+50
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Coverage gate — fails if total line coverage drops below the
|
||||||
|
# `coverage_floor:` value in pubspec.yaml. Driven by D-66.
|
||||||
|
#
|
||||||
|
# Reads coverage/lcov.info (generated by `flutter test --coverage`,
|
||||||
|
# which `ci/test.sh` runs as part of the fast suite). Parses the
|
||||||
|
# total LF/LH counts and compares the integer percentage against
|
||||||
|
# the floor. The floor only ratchets up — bumping it requires an
|
||||||
|
# explicit edit to pubspec.yaml committed alongside the test
|
||||||
|
# additions that earned the bump.
|
||||||
|
#
|
||||||
|
# Self-contained parser (awk) — does not depend on `lcov` being
|
||||||
|
# installed on the dev machine.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
LCOV=coverage/lcov.info
|
||||||
|
|
||||||
|
if [[ ! -f "$LCOV" ]]; then
|
||||||
|
echo "==> coverage gate: $LCOV missing — run \`make test\` first (it writes lcov)" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
floor=$(awk -F: '/^coverage_floor:/ {gsub(/ /,"",$2); print $2; exit}' pubspec.yaml)
|
||||||
|
if [[ -z "$floor" ]]; then
|
||||||
|
echo "==> coverage gate: pubspec.yaml is missing coverage_floor: — see D-66" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
read measured measured_int < <(
|
||||||
|
awk -F: '
|
||||||
|
/^LF:/ { lf += $2 }
|
||||||
|
/^LH:/ { lh += $2 }
|
||||||
|
END {
|
||||||
|
pct = (lh / lf) * 100
|
||||||
|
printf "%.2f %d\n", pct, int(pct)
|
||||||
|
}
|
||||||
|
' "$LCOV"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (( measured_int < floor )); then
|
||||||
|
echo "==> coverage gate FAIL: ${measured}% < floor ${floor}%"
|
||||||
|
echo " Add tests, or — if the drop is intentional — explain in the commit and lower the floor explicitly."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( measured_int > floor )); then
|
||||||
|
echo "==> coverage gate OK: ${measured}% (floor ${floor}%) — ${measured_int}% available; consider bumping pubspec.yaml coverage_floor: to ${measured_int}"
|
||||||
|
else
|
||||||
|
echo "==> coverage gate OK: ${measured}% (floor ${floor}%)"
|
||||||
|
fi
|
||||||
+4
-4
@@ -6,13 +6,13 @@ set -euo pipefail
|
|||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
echo "==> flutter analyze"
|
echo "==> flutter analyze"
|
||||||
flutter analyze --no-fatal-infos
|
flutter analyze
|
||||||
|
|
||||||
echo "==> dart format (whole tree)"
|
echo "==> dart format (whole tree)"
|
||||||
dart format --set-exit-if-changed .
|
dart format --set-exit-if-changed .
|
||||||
|
|
||||||
echo "==> dart test (forkpty — incompatible with flutter test runner)"
|
echo "==> dart test (forkpty — incompatible with flutter test runner)"
|
||||||
dart test --tags forkpty test/pty/session_test.dart
|
dart test --tags forkpty test/pty/session_test.dart test/panes/registry_test.dart
|
||||||
|
|
||||||
echo "==> flutter test (unit + widget + golden)"
|
echo "==> flutter test --coverage (unit + widget + golden)"
|
||||||
flutter test --exclude-tags forkpty
|
flutter test --coverage --exclude-tags forkpty
|
||||||
|
|||||||
+1
-7
@@ -20,11 +20,6 @@ if ! command -v dart >/dev/null; then
|
|||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! command -v ptyc >/dev/null && [[ ! -x "ptyc/bin/ptyc" ]]; then
|
|
||||||
echo "test-core: building ptyc (required by PTY tests)"
|
|
||||||
make -C ptyc >/dev/null
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Hard timeout (seconds). The PTY tests should finish in <5s; IPC/daemon
|
# Hard timeout (seconds). The PTY tests should finish in <5s; IPC/daemon
|
||||||
# tests are faster still. 120s is generous for CI warmup, tiny for a
|
# tests are faster still. 120s is generous for CI warmup, tiny for a
|
||||||
# hang.
|
# hang.
|
||||||
@@ -33,7 +28,7 @@ TIMEOUT_SECONDS=${TIMEOUT_SECONDS:-120}
|
|||||||
# Run dart test in its own process group so we can kill descendants on
|
# Run dart test in its own process group so we can kill descendants on
|
||||||
# timeout. `setsid` starts a new session; `timeout --kill-after` SIGKILLs
|
# timeout. `setsid` starts a new session; `timeout --kill-after` SIGKILLs
|
||||||
# after SIGTERM if the test ignores it.
|
# after SIGTERM if the test ignores it.
|
||||||
CORE_DIRS="test/ipc test/pty test/daemon test/git test/panes test/files test/editor test/cli test/pql"
|
CORE_DIRS="test/ipc test/pty test/daemon test/git test/panes test/files test/editor test/pql"
|
||||||
|
|
||||||
echo "test-core: dart test ${CORE_DIRS} (timeout ${TIMEOUT_SECONDS}s)"
|
echo "test-core: dart test ${CORE_DIRS} (timeout ${TIMEOUT_SECONDS}s)"
|
||||||
if ! timeout --kill-after=5s "${TIMEOUT_SECONDS}s" \
|
if ! timeout --kill-after=5s "${TIMEOUT_SECONDS}s" \
|
||||||
@@ -42,7 +37,6 @@ if ! timeout --kill-after=5s "${TIMEOUT_SECONDS}s" \
|
|||||||
if [[ $rc -eq 124 ]]; then
|
if [[ $rc -eq 124 ]]; then
|
||||||
echo "test-core: TIMEOUT — killing descendants" >&2
|
echo "test-core: TIMEOUT — killing descendants" >&2
|
||||||
pkill -9 -f "dart test test/" 2>/dev/null || true
|
pkill -9 -f "dart test test/" 2>/dev/null || true
|
||||||
pkill -9 -f "ptyc" 2>/dev/null || true
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
exit $rc
|
exit $rc
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Generate + summarize lcov coverage. No thresholds yet (see plan's
|
|
||||||
# "Open questions deferred" — we let the suite run for a week of real
|
|
||||||
# commits before setting hard gates that would just need tuning).
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
|
|
||||||
echo "==> flutter test --coverage"
|
|
||||||
flutter test --coverage
|
|
||||||
|
|
||||||
if command -v lcov >/dev/null 2>&1; then
|
|
||||||
echo "==> lcov summary"
|
|
||||||
lcov --summary coverage/lcov.info
|
|
||||||
fi
|
|
||||||
+1
-9
@@ -1,16 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# End-to-end layer: daemon subprocess + web WASM Playwright smoke.
|
# End-to-end layer: web WASM Playwright smoke.
|
||||||
# Neither fits in `make test`; together they're the "everything still
|
|
||||||
# works across process/runtime boundaries" gate.
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
echo "==> build bin/clide (required by daemon subprocess test)"
|
|
||||||
make build
|
|
||||||
|
|
||||||
echo "==> daemon subprocess test"
|
|
||||||
dart test test/daemon/
|
|
||||||
|
|
||||||
echo "==> browser WASM smoke (Playwright)"
|
echo "==> browser WASM smoke (Playwright)"
|
||||||
./tools/ui/build.sh
|
./tools/ui/build.sh
|
||||||
./tools/ui/serve.sh
|
./tools/ui/serve.sh
|
||||||
|
|||||||
@@ -3,10 +3,16 @@
|
|||||||
# start" regression gate. Flutter integration tests prefer one file at
|
# start" regression gate. Flutter integration tests prefer one file at
|
||||||
# a time on desktop; we iterate to avoid the "Unable to start the app"
|
# a time on desktop; we iterate to avoid the "Unable to start the app"
|
||||||
# error that hits when they run as a batch.
|
# error that hits when they run as a batch.
|
||||||
|
#
|
||||||
|
# Skips: theme_picker_test.dart — pumpAndSettle hangs on theme.pick
|
||||||
|
# (T-116). Restore once that's fixed.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
for f in integration_test/*_test.dart; do
|
for f in integration_test/*_test.dart; do
|
||||||
|
case "$f" in
|
||||||
|
integration_test/theme_picker_test.dart) echo "==> integration_test: $f (SKIPPED — T-116)"; continue ;;
|
||||||
|
esac
|
||||||
echo "==> integration_test: $f"
|
echo "==> integration_test: $f"
|
||||||
flutter test "$f"
|
flutter test "$f"
|
||||||
done
|
done
|
||||||
|
|||||||
+776
@@ -0,0 +1,776 @@
|
|||||||
|
# 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, 1–5, 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.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Declares tags used by inline `test('...', tags: [...], ...)` calls so
|
||||||
|
# that `flutter test --exclude-tags <name>` and `dart test --tags <name>`
|
||||||
|
# both honor them. Undeclared tags are ignored by the runners, which
|
||||||
|
# silently breaks selective excludes.
|
||||||
|
tags:
|
||||||
|
# Tests that call `forkpty()` via Dart FFI. Must run under `dart test`,
|
||||||
|
# not `flutter test` — the latter's runner hosts a multi-threaded
|
||||||
|
# Flutter engine in which forkpty produces a master fd that never
|
||||||
|
# delivers output. See `test/pty/session_test.dart`.
|
||||||
|
forkpty:
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
# Decisions
|
|
||||||
|
|
||||||
Confirmed decisions, open questions, and rejected alternatives for clide.
|
|
||||||
|
|
||||||
Decisions are split by domain. When unsure where a record belongs: if
|
|
||||||
it constrains **how we build**, it's architecture. If it defines **what
|
|
||||||
ships to users**, it's extensions / accessibility. If it defines **how
|
|
||||||
we verify**, it's testing. If it defines **what the toolchain looks
|
|
||||||
like**, it's tooling. If it defines **how the team works**, it's
|
|
||||||
process.
|
|
||||||
|
|
||||||
Cross-domain records live in one file with `[D-NNN]`-shaped cross-
|
|
||||||
references in related files. Split threshold: when any file exceeds
|
|
||||||
~350 lines, review whether it should split (see settled-reach's
|
|
||||||
`questions-*.md` split pattern for precedent).
|
|
||||||
|
|
||||||
## Domain files
|
|
||||||
|
|
||||||
| File | Domain |
|
|
||||||
|------|--------|
|
|
||||||
| [architecture.md](architecture.md) | Core, rendering, IPC, kernel, panel manager |
|
|
||||||
| [extensions.md](extensions.md) | Extension contract, Lua runtime, grain, contribution points |
|
|
||||||
| [accessibility.md](accessibility.md) | A11y + i18n policy, WCAG gates |
|
|
||||||
| [testing.md](testing.md) | Test pyramid, drivers, client-side constraint |
|
|
||||||
| [tooling.md](tooling.md) | Toolchain, supply chain, CI, ignore strategy |
|
|
||||||
| [process.md](process.md) | Q&D system, kanban, commit conventions, changelog |
|
|
||||||
| [rejected.md](rejected.md) | Rejected alternatives across all domains |
|
|
||||||
| [questions.md](questions.md) | Master index of open questions |
|
|
||||||
| [questions-architecture.md](questions-architecture.md) | Architecture Qs |
|
|
||||||
| [questions-extensions.md](questions-extensions.md) | Extension Qs |
|
|
||||||
| [questions-accessibility.md](questions-accessibility.md) | A11y / i18n Qs |
|
|
||||||
| [questions-testing.md](questions-testing.md) | Testing Qs |
|
|
||||||
| [questions-process.md](questions-process.md) | Process + tooling Qs |
|
|
||||||
|
|
||||||
## Record shape
|
|
||||||
|
|
||||||
Confirmed decisions (`D-NNN`):
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
### D-NNN: Short title
|
|
||||||
- **Date:** YYYY-MM-DD
|
|
||||||
- **Decision:** one-sentence summary, then details.
|
|
||||||
- **Rationale:** why this over alternatives.
|
|
||||||
- **Cost:** known downsides / what we're accepting.
|
|
||||||
- **Raised by:** who proposed / endorsed.
|
|
||||||
```
|
|
||||||
|
|
||||||
Domain-specific fields (`Kill switch:`, `Evaluation reports:`,
|
|
||||||
`Amendment:`, `Cross-reference:`) are additive. Amendments are inline
|
|
||||||
and dated: `**Amendment (YYYY-MM-DD):** …`. Cross-references use
|
|
||||||
markdown anchor links with the full slug:
|
|
||||||
`[D-5](architecture.md#d-5-dart-core-ptyc-peer)`.
|
|
||||||
|
|
||||||
Open questions (`Q-NNN`):
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
### Q-NNN: Short question-form title
|
|
||||||
- **Status:** Open | Partially resolved → [D-NNN] | Resolved → [D-NNN]
|
|
||||||
- **Question:** ...
|
|
||||||
- **Context:** ...
|
|
||||||
- **Assigned to:** (optional)
|
|
||||||
- **Source:** (optional)
|
|
||||||
```
|
|
||||||
|
|
||||||
Rejected alternatives (`R-NNN`):
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
### R-NNN: Short rejected-option title
|
|
||||||
- **Rejected:** YYYY-MM-DD
|
|
||||||
- **Reason:** ...
|
|
||||||
- **Cross-reference:** [D-NNN] (what was picked instead)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Claiming an ID
|
|
||||||
|
|
||||||
Until the pql planning subcommands land ([`Q-21`](questions-process.md)),
|
|
||||||
claim IDs by inspecting the highest existing `D-NNN` / `Q-NNN` /
|
|
||||||
`R-NNN` in the target file and incrementing.
|
|
||||||
|
|
||||||
Once `pql decisions claim D <domain> "title"` exists, use that —
|
|
||||||
same semantics, no race on concurrent sessions.
|
|
||||||
|
|
||||||
## Querying
|
|
||||||
|
|
||||||
`pql decisions …` reads `decisions/*.md` and writes `.pql/pql.db`
|
|
||||||
(gitignored; markdown is the source of truth).
|
|
||||||
|
|
||||||
Common queries:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pql decisions list --type confirmed --domain architecture
|
|
||||||
pql decisions show D-5 --with-refs
|
|
||||||
pql decisions coverage # D-records without tickets
|
|
||||||
pql decisions validate # pre-push parser gate
|
|
||||||
pql ticket board # kanban view of tickets
|
|
||||||
```
|
|
||||||
|
|
||||||
## Adding a decision
|
|
||||||
|
|
||||||
1. Edit the appropriate domain file.
|
|
||||||
2. Follow the record shape above.
|
|
||||||
3. Run `pql decisions validate` (also runs in `make push-check`).
|
|
||||||
4. Commit. The SQLite index rebuilds from markdown on any
|
|
||||||
`pql decisions sync`.
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# Open Questions — Master Index
|
|
||||||
|
|
||||||
Open questions live in per-domain `questions-<domain>.md` files.
|
|
||||||
This index is a pointer and a place to record the most-load-bearing
|
|
||||||
open questions with one-line summaries.
|
|
||||||
|
|
||||||
## By domain
|
|
||||||
|
|
||||||
| File | Topics |
|
|
||||||
|------|--------|
|
|
||||||
| [questions-architecture.md](questions-architecture.md) | IPC, events, canvas, window chrome, macOS signing, pql absorption, ticket persistence, interaction model |
|
|
||||||
| [questions-extensions.md](questions-extensions.md) | Extension API shape, Lua runtime vendoring, manifest schema version |
|
|
||||||
| [questions-accessibility.md](questions-accessibility.md) | Web-mode a11y, i18n plurals/gender/dates |
|
|
||||||
| [questions-testing.md](questions-testing.md) | Coverage gates, screen-reader automation |
|
|
||||||
| [questions-process.md](questions-process.md) | Editor tab (LSP vs tree-sitter), icon set, theme hot-reload, kernel DB access, planning-tool location |
|
|
||||||
|
|
||||||
## Load-bearing questions (gate other work)
|
|
||||||
|
|
||||||
- **[Q-21](questions-process.md#q-21-pql-absorbs-planning-vs-keeps-separate)** — Pql absorbs planning features vs clide absorbs pql vs separate CLI. Blocks the stopgap sunset and shapes the pql-side planning session.
|
|
||||||
- **[Q-22](questions-process.md#q-22-ticket-persistence-strategy)** — Ticket persistence: per-dev only / milestone-committed / markdown-mirrored. Shapes multi-contributor story.
|
|
||||||
- **[Q-5](questions-architecture.md#q-5-ipc-wire-format-stability)** — IPC wire-format stability and `schema_version:` in `pubspec.yaml`. Decide when the first real subcommand lands.
|
|
||||||
- **[Q-15](questions-process.md#q-15-editor-tab-full-lsp-vs-tree-sitter-only)** — Editor tab: full LSP integration vs tree-sitter-only highlight. Decide during Tier 2.
|
|
||||||
|
|
||||||
---
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
# Rejected Alternatives
|
|
||||||
|
|
||||||
Alternatives considered and rejected, with rationale preserved for
|
|
||||||
future reference.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### R-2: Go sidecar
|
|
||||||
- **Rejected:** 2026-04-20 (was ADR 0002; superseded by [D-5](architecture.md#d-5-dart-core-ptyc-peer))
|
|
||||||
- **Reason:** The ADR picked Go on two premises — (a) the heavy work belongs in a language separate from the UI layer, and (b) pql is Go so muscle memory transfers. Both broke on reassessment. The sidecar stripped of PTY is I/O-bound glue that `dart:io` covers cleanly (unix sockets, JSON-lines framing, process tables, shell-outs). The real axis was *separate process vs shared language*, not Go vs Rust, and separate-process is what matters (session persistence needs the daemon to outlive the app), not language. PTY is the one place Dart is genuinely weak — Dart's multi-threaded VM can't safely `fork()` — and that single constraint forces a native helper regardless, independent of whether the rest of the core is Dart. Once a small native helper is accepted, the question "does *everything else* need to be in that same native language" answers itself: no. Go sidecar directory dissolved; `ptyc` (C, PTY-only, pql-peer) is the surviving native supporter tool.
|
|
||||||
- **Cross-reference:** [D-5](architecture.md#d-5-dart-core-ptyc-peer)
|
|
||||||
|
|
||||||
### R-3: `MaterialApp` root
|
|
||||||
- **Rejected:** 2026-04-21
|
|
||||||
- **Reason:** Dragged in Material theming, default icons, and platform chrome that fought the custom three-tier theme pipeline ([D-9](architecture.md#d-9-three-tier-theme-pipeline)). Every bundled theme had to override Material defaults to look like clide; the overrides were visible in widget tests as "why is this `ElevatedButton` colored this way."
|
|
||||||
- **Cross-reference:** [D-7](architecture.md#d-7-app-root-is-bare-widgetsapp)
|
|
||||||
|
|
||||||
### R-4: Flutter `intl` + ARB codegen for i18n
|
|
||||||
- **Rejected:** 2026-04-21
|
|
||||||
- **Reason:** ARB codegen is inflexible for plugin-contributed catalogs — every catalogue needs a codegen pass, every extension ships with pre-generated Dart, and runtime merging is fighting the tool. The fframe text-driven pattern reads JSON at runtime with no codegen, which fits extension-shipped catalogs cleanly.
|
|
||||||
- **Cross-reference:** [D-21](accessibility.md#d-21-i18n-is-a-tier-0-contract)
|
|
||||||
|
|
||||||
### R-5: Patrol test runner
|
|
||||||
- **Rejected:** 2026-04-21
|
|
||||||
- **Reason:** Adds a dependency (violates [D-31](tooling.md#d-31-prefer-zero-deps-exact-pin)) for a capability we get from Playwright + Flutter's own semantics tree. Patrol's value proposition (native-gesture emulation) is less relevant on Linux desktop than on mobile.
|
|
||||||
- **Cross-reference:** [D-26](testing.md#d-26-web-driver-raw-playwright-plus-flutter-semantics)
|
|
||||||
|
|
||||||
### R-6: Nerd-font glyph icons
|
|
||||||
- **Rejected:** 2026-04-21
|
|
||||||
- **Reason:** TUI hangover from the Python-era clide under `legacy/`. Not desktop-native; forces a font dependency; doesn't theme consistently. Clide uses custom icon primitives (Tier 6 revisits with proper icon-set design).
|
|
||||||
- **Cross-reference:** [Q-17](questions-process.md#q-17-icon-set-growth)
|
|
||||||
|
|
||||||
### R-7: `CupertinoApp` root
|
|
||||||
- **Rejected:** 2026-04-21
|
|
||||||
- **Reason:** iOS-opinionated; wrong shell for a Linux-primary desktop IDE. Same theming-collision problem as [R-3](#r-3-materialapp-root).
|
|
||||||
- **Cross-reference:** [D-7](architecture.md#d-7-app-root-is-bare-widgetsapp)
|
|
||||||
|
|
||||||
### R-8: Riverpod / Provider / BLoC for state
|
|
||||||
- **Rejected:** 2026-04-21
|
|
||||||
- **Reason:** Violates [D-31](tooling.md#d-31-prefer-zero-deps-exact-pin). `ChangeNotifier` + `ListenableBuilder` ship in the SDK, fake trivially, and cover the state model we need. The ergonomic wins of Riverpod / Provider don't clear the "new dependency" bar at clide's scale.
|
|
||||||
- **Cross-reference:** [D-10](architecture.md#d-10-state-management-changenotifier)
|
|
||||||
|
|
||||||
### R-9: Port planning tooling into clide
|
|
||||||
- **Rejected:** 2026-04-21
|
|
||||||
- **Reason:** Earlier in the planning session the assumption was "clide owns Dart subcommands for decisions + tickets." That breaks the day a contributor works in a terminal or in VS Code / JetBrains — they have no `clide` binary to run. Reversing: pql owns planning long-term (see [D-39](process.md#d-39-planning-tooling-lives-in-pql)); clide consumes via shell-out.
|
|
||||||
- **Cross-reference:** [D-39](process.md#d-39-planning-tooling-lives-in-pql)
|
|
||||||
|
|
||||||
### R-10: Python-script stopgap under `tooling/db/`
|
|
||||||
- **Rejected:** 2026-04-21
|
|
||||||
- **Reason:** Location, not language. Settled-reach puts scripts at `tooling/db/` — copying that path here creates a script-pollution problem: every project using the pattern commits its own copy. The accepted Python port ([D-40](process.md#d-40-python-stopgap-under-toolsscriptsplan)) lives at `tools/scripts/plan`, clearly signalled as dev-tooling and time-limited.
|
|
||||||
- **Cross-reference:** [D-40](process.md#d-40-python-stopgap-under-toolsscriptsplan)
|
|
||||||
|
|
||||||
### R-11: Permanent stopgap
|
|
||||||
- **Rejected:** 2026-04-21
|
|
||||||
- **Reason:** If the Python port under `tools/scripts/plan` outlasts pql's feature parity, delete it. The deletion commit should be one changeset: remove `tools/scripts/plan`, remove its Makefile target (`decisions-validate` rewires to `pql decisions validate`), add a `CHANGELOG.md` entry under Removed, and verify `.pql/pql.db` still opens under the new `pql` binary.
|
|
||||||
- **Cross-reference:** [D-40](process.md#d-40-python-stopgap-under-toolsscriptsplan)
|
|
||||||
|
|
||||||
### R-12: MaterialApp wrapper from design handoff
|
|
||||||
- **Rejected:** 2026-04-22
|
|
||||||
- **Reason:** The design handoff delivers theme files as `MaterialApp`/`ThemeData` Dart classes. This is the delivery format of claude.ai/design, not a design intent. Adopting Material's widget system would contradict [D-7](architecture.md#d-7-app-root-is-bare-widgetsapp) (bare WidgetsApp, no Material/Cupertino). We translate the palette tokens and syntax roles into our existing YAML + `SurfaceTokens` pipeline.
|
|
||||||
- **Cross-reference:** [D-43](architecture.md#d-43-design-handoff-adopt-token-palettes-reject-material-wrapper)
|
|
||||||
|
|
||||||
---
|
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
# clide — Architecture
|
||||||
|
|
||||||
|
Current as of 2026-05-17. Tracks the code on `main`. For the original
|
||||||
|
design plan (much of it now superseded), see
|
||||||
|
[`docs/initial-plan.md`](initial-plan.md). For decisions, see
|
||||||
|
[`governance/decisions/`](../governance/decisions/).
|
||||||
|
|
||||||
|
## Shape
|
||||||
|
|
||||||
|
clide is a **single Flutter package at the repo root**. There is no
|
||||||
|
sidecar process, no separate daemon, no Go binary. One `flutter run`
|
||||||
|
boots the whole IDE.
|
||||||
|
|
||||||
|
```
|
||||||
|
flutter desktop app (lib/main.dart)
|
||||||
|
├── kernel services (lib/kernel/) — theme, i18n, settings, panels,
|
||||||
|
│ commands, focus, scheduler, …
|
||||||
|
├── core subsystems (lib/src/) — ipc, daemon dispatch, panes,
|
||||||
|
│ │ pty, editor, files, git, pql
|
||||||
|
│ └── PTY via Dart FFI posix_openpt + posix_spawn (T-96)
|
||||||
|
├── widget primitives (lib/widgets/) — ClideButton, ClideText, … no
|
||||||
|
│ Material/Cupertino
|
||||||
|
├── built-in extensions (lib/builtin/) — claude, editor, files, git, terminal,
|
||||||
|
│ welcome, … each a ClideExtension
|
||||||
|
└── extension framework (lib/extension/) — contract + dependency-gated activation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Process model
|
||||||
|
|
||||||
|
One OS process. The Flutter app hosts:
|
||||||
|
|
||||||
|
- the IPC dispatcher (`DaemonDispatcher` in `lib/src/daemon/`),
|
||||||
|
- every subsystem handler (pane/files/editor/git/pql),
|
||||||
|
- the extension manager and all built-in extensions.
|
||||||
|
|
||||||
|
`tmux` is the only external long-lived process — it owns Claude
|
||||||
|
session persistence so panes survive app restarts (D-41). The app
|
||||||
|
re-attaches via `tmux new-session -A` on boot.
|
||||||
|
|
||||||
|
PTYs are spawned natively from Dart. `lib/src/pty/native_pty.dart`
|
||||||
|
calls `posix_openpt()` + `posix_spawn()` via FFI; the child inherits
|
||||||
|
the slave PTY as stdin/stdout/stderr. No C helper binary.
|
||||||
|
|
||||||
|
### Native dependencies
|
||||||
|
|
||||||
|
Vendored under [`native/`](../native/) with per-platform subdirectories
|
||||||
|
(`linux-x64/` today). Currently:
|
||||||
|
|
||||||
|
- `libtree-sitter.so` — wasmtime-embedded tree-sitter for syntax
|
||||||
|
highlighting. Loaded via `dart:ffi`. See
|
||||||
|
`lib/kernel/src/syntax/tree_sitter_ffi.dart`.
|
||||||
|
|
||||||
|
Each entry is listed in [`assets/licenses.yaml`](../assets/licenses.yaml)
|
||||||
|
with version + SHA expectation (D-42).
|
||||||
|
|
||||||
|
### External tools
|
||||||
|
|
||||||
|
clide shells out to two binaries at runtime:
|
||||||
|
|
||||||
|
- **`git`** — vendored as `dugite-native` if present at the install
|
||||||
|
directory; otherwise `PATH` `git`. Resolution happens at
|
||||||
|
`lib/kernel/src/toolchain_paths.dart`. **Never resolves against the
|
||||||
|
open workspace** (T-98).
|
||||||
|
- **`pql`** — the [pql](https://github.com/postmeridiem/pql) project
|
||||||
|
query language; supporter tool, wrapped in `lib/src/pql/`. Clide
|
||||||
|
never re-implements pql features (D-3).
|
||||||
|
|
||||||
|
## Surfaces
|
||||||
|
|
||||||
|
### Claude-facing — `clide` CLI
|
||||||
|
|
||||||
|
Per D-1 and D-6, Claude talks to clide exclusively via Bash. Every
|
||||||
|
state-changing command emits one or more events on a long-lived
|
||||||
|
event stream; every UI affordance has a matching CLI verb. See D-6
|
||||||
|
for the subsystem/verb/event contract.
|
||||||
|
|
||||||
|
> **Caveat (2026-05):** the Unix-socket server that exposes the
|
||||||
|
> dispatcher to a thin `clide` C client is currently unimplemented.
|
||||||
|
> Today's working path is in-process direct dispatch. See **T-99**
|
||||||
|
> (IPC server implementation) and **D-68** (dual integration surface
|
||||||
|
> — Bash CLI primary, MCP secondary).
|
||||||
|
|
||||||
|
### User-facing — Flutter desktop
|
||||||
|
|
||||||
|
Three-column layout (sidebar / workspace / context) with collapsible
|
||||||
|
panels, a custom title bar, and per-panel "hats" for branding +
|
||||||
|
window controls. The interaction model is documented in D-47 and
|
||||||
|
neighboring decisions.
|
||||||
|
|
||||||
|
## Subsystems at a glance
|
||||||
|
|
||||||
|
| Subsystem | Location | Owns |
|
||||||
|
|---|---|---|
|
||||||
|
| IPC envelope + dispatcher | `lib/src/ipc/`, `lib/src/daemon/` | request/response framing, command registration, broadcast events |
|
||||||
|
| Pane registry | `lib/src/panes/` | spawn/list/write/resize/close, event emission |
|
||||||
|
| PTY | `lib/src/pty/` | `posix_openpt` + `posix_spawn`, reader isolate, signal forwarding |
|
||||||
|
| Editor | `lib/src/editor/` | buffer registry, open/save/setContent |
|
||||||
|
| Files | `lib/src/files/` | ls / read / watch, path-safety containment check |
|
||||||
|
| Git | `lib/src/git/` | client (no shell), status/diff/operations parsing |
|
||||||
|
| Pql wrapper | `lib/src/pql/` | shell-out only; never re-implements pql |
|
||||||
|
| Kernel services | `lib/kernel/src/` | theme, i18n, settings, panels, commands, focus, syntax |
|
||||||
|
| Extensions | `lib/extension/`, `lib/builtin/` | dependency-gated activation, contribution points |
|
||||||
|
|
||||||
|
## Build + test
|
||||||
|
|
||||||
|
```
|
||||||
|
make hooks && flutter pub get # one-time setup
|
||||||
|
make run # launch app
|
||||||
|
make test # analyze + format + unit + widget + golden
|
||||||
|
make test-core # IPC / PTY / git / pane registry
|
||||||
|
make test-a11y # accessibility contract
|
||||||
|
make test-integration # real-app boot tests
|
||||||
|
make push-check # the full pre-push gate
|
||||||
|
```
|
||||||
|
|
||||||
|
The pre-push gate enforces a 95% line-coverage floor (D-66), a
|
||||||
|
40-word soft / 60-word hard CHANGELOG bullet cap, all unit/widget
|
||||||
|
tests, and the a11y contract.
|
||||||
|
|
||||||
|
## Governance
|
||||||
|
|
||||||
|
All architectural choices live in
|
||||||
|
[`governance/decisions/<domain>.md`](../governance/decisions/) as
|
||||||
|
`D-NNN` records, open questions as `Q-NNN`, rejected alternatives as
|
||||||
|
`R-NNN`. Claim new IDs via `pql decisions claim D <domain> "title"`.
|
||||||
|
The governance index lists everything: [`governance/README.md`](../governance/README.md).
|
||||||
|
|
||||||
|
The pql ticket backlog tracks in-flight work; `pql ticket board
|
||||||
|
--pretty` for the live view.
|
||||||
@@ -8,8 +8,8 @@
|
|||||||
> Update wireframes there, not here.
|
> Update wireframes there, not here.
|
||||||
>
|
>
|
||||||
> **Why kept:** the design tokens under `tokens/` and `themes/` still
|
> **Why kept:** the design tokens under `tokens/` and `themes/` still
|
||||||
> feed the runtime themes (per [D-43](../../decisions/architecture.md#d-43-design-handoff-adopt-token-palettes-reject-material-wrapper)
|
> feed the runtime themes (per [D-43](../../governance/decisions/architecture.md#d-43-design-handoff-adopt-token-palettes-reject-material-wrapper)
|
||||||
> / [D-44](../../decisions/architecture.md#d-44-four-bundled-themes-clide-midnight-paper-terminal)).
|
> / [D-44](../../governance/decisions/architecture.md#d-44-four-bundled-themes-clide-midnight-paper-terminal)).
|
||||||
> The HTMLs and PNGs are kept for historical context.
|
> The HTMLs and PNGs are kept for historical context.
|
||||||
>
|
>
|
||||||
> **What changed since:** welcome screen has logo-with-wordmark and a
|
> **What changed since:** welcome screen has logo-with-wordmark and a
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Date: 2026-05-06
|
|||||||
|
|
||||||
Some panes need to host multiple, dynamically-spawned views of the
|
Some panes need to host multiple, dynamically-spawned views of the
|
||||||
same kind. The first concrete case is the Claude pane: per
|
same kind. The first concrete case is the Claude pane: per
|
||||||
[D-41](../../decisions/architecture.md#d-41-claude-panes-one-primary-per-repo-tmux-backed),
|
[D-41](../../governance/decisions/architecture.md#d-41-claude-panes-one-primary-per-repo-tmux-backed),
|
||||||
each repo has exactly one **primary** Claude pane plus zero or more
|
each repo has exactly one **primary** Claude pane plus zero or more
|
||||||
**secondary** panes spawned at runtime. The user needs a way to:
|
**secondary** panes spawned at runtime. The user needs a way to:
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ diff/preview surfaces) can adopt it without reinventing tab strips.
|
|||||||
- Replacing `TabContribution`. Slot-host tabs are static-by-design;
|
- Replacing `TabContribution`. Slot-host tabs are static-by-design;
|
||||||
this widget is for inside-a-tab dynamism.
|
this widget is for inside-a-tab dynamism.
|
||||||
- Window-level tab management (browser-style "tear off into a window").
|
- Window-level tab management (browser-style "tear off into a window").
|
||||||
- Editor multi-buffer tabs. [D-48](../../decisions/architecture.md#d-48-chrome-budget-no-tabs-no-breadcrumbs-keyboard-first)
|
- Editor multi-buffer tabs. [D-48](../../governance/decisions/architecture.md#d-48-chrome-budget-no-tabs-no-breadcrumbs-keyboard-first)
|
||||||
rejected those; revisiting is a separate decision.
|
rejected those; revisiting is a separate decision.
|
||||||
|
|
||||||
## API sketch
|
## API sketch
|
||||||
|
|||||||
@@ -1,5 +1,24 @@
|
|||||||
# Clide — Initial Plan (Flutter rebuild)
|
# Clide — Initial Plan (Flutter rebuild)
|
||||||
|
|
||||||
|
> **⚠ HISTORICAL — preserved as a snapshot of the 2026-04 plan.**
|
||||||
|
>
|
||||||
|
> Several load-bearing choices in this document have since been
|
||||||
|
> superseded by formal decisions:
|
||||||
|
>
|
||||||
|
> - **No Go sidecar.** Dart is the sole core language (D-5).
|
||||||
|
> - **No separate daemon process.** The Flutter app hosts the IPC
|
||||||
|
> server in-process (D-56).
|
||||||
|
> - **No `ptyc/` C helper.** PTY spawning uses Dart FFI
|
||||||
|
> `posix_openpt()` + `posix_spawn()` (D-5 amendments, T-96).
|
||||||
|
> - **Single package at the repo root**, not `app/` + `lib/` + `bin/`.
|
||||||
|
> - **No `project.yaml`.** Project metadata lives in `pubspec.yaml`.
|
||||||
|
>
|
||||||
|
> For the current architecture, see [`docs/architecture.md`](architecture.md).
|
||||||
|
> For the decision trail, see [`governance/decisions/`](../governance/decisions/).
|
||||||
|
> The text below is left intact for anyone tracing the design history.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
**Working name:** clide (unchanged from the Python era). Repo root:
|
**Working name:** clide (unchanged from the Python era). Repo root:
|
||||||
`/var/mnt/data/projects/clide`. Flutter desktop app + Go sidecar/CLI.
|
`/var/mnt/data/projects/clide`. Flutter desktop app + Go sidecar/CLI.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# macOS PTY Problem — Diagnosis Complete
|
# macOS PTY Problem — Diagnosis Complete
|
||||||
|
|
||||||
|
> **⚠ HISTORICAL — `ptyc` is retired.** PTY spawning moved to
|
||||||
|
> Dart FFI `posix_openpt()` + `posix_spawn()` (D-5 amendments,
|
||||||
|
> T-96). The race described below was specific to the old C helper's
|
||||||
|
> SCM_RIGHTS hand-off and no longer applies.
|
||||||
|
|
||||||
|
|
||||||
## Status: Root cause found
|
## Status: Root cause found
|
||||||
|
|
||||||
The PTY master fd is valid (`isatty=1`), SCM_RIGHTS transfer is correct, all struct layouts are correct. The problem is **timing**: the reader isolate starts too late and the shell has already exited by the time `read()` is called on the master fd. macOS returns EOF (n=0) immediately when the slave side is closed — unlike Linux which buffers data.
|
The PTY master fd is valid (`isatty=1`), SCM_RIGHTS transfer is correct, all struct layouts are correct. The problem is **timing**: the reader isolate starts too late and the shell has already exited by the time `read()` is called on the master fd. macOS returns EOF (n=0) immediately when the slave side is closed — unlike Linux which buffers data.
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# PTY on macOS: A New Diagnostic and Resolution Plan
|
# PTY on macOS: A New Diagnostic and Resolution Plan
|
||||||
|
|
||||||
|
> **⚠ HISTORICAL — `ptyc` is retired.** PTY spawning moved to
|
||||||
|
> Dart FFI `posix_openpt()` + `posix_spawn()` (D-5 amendments,
|
||||||
|
> T-96). This document captures the forensic investigation of the
|
||||||
|
> SCM_RIGHTS control-message mismatch in the old C helper. Kept for
|
||||||
|
> the diagnostic technique; the code it discusses no longer exists.
|
||||||
|
|
||||||
|
|
||||||
## 1. Executive Summary
|
## 1. Executive Summary
|
||||||
|
|
||||||
Previous attempts to fix the PTY functionality on macOS have failed, even after correcting a deadlock in the Dart code. The core of the problem appears to be a fundamental mismatch in how the C helper (`ptyc`) constructs a control message and how the Dart FFI layer is trying to parse it.
|
Previous attempts to fix the PTY functionality on macOS have failed, even after correcting a deadlock in the Dart code. The core of the problem appears to be a fundamental mismatch in how the C helper (`ptyc`) constructs a control message and how the Dart FFI layer is trying to parse it.
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
# Decisions, Questions, Rejected
|
||||||
|
|
||||||
|
This directory holds structured planning records that pql parses
|
||||||
|
into pql.db. Each record is a `### [DQR]-N: Title` heading inside
|
||||||
|
a markdown file. Files live in three per-type subdirectories:
|
||||||
|
|
||||||
|
- `decisions/<domain>.md` — confirmed design decisions
|
||||||
|
- `questions/<domain>.md` — open questions that may resolve into
|
||||||
|
decisions or rejected proposals
|
||||||
|
- `rejected/<domain>.md` — rejected proposals (kept for the audit
|
||||||
|
trail)
|
||||||
|
|
||||||
|
The parser infers domain from the filename stem and record type
|
||||||
|
from the parent subdirectory.
|
||||||
|
|
||||||
|
D-records that propose implementation work link to `initiative`-type
|
||||||
|
tickets via `decision_ref`. Run `pql decisions show <id>
|
||||||
|
--with-tickets` to inspect implementation status.
|
||||||
|
|
||||||
|
## Recommended domains
|
||||||
|
|
||||||
|
Start with this canonical set; create files as records land in
|
||||||
|
each domain:
|
||||||
|
|
||||||
|
- **architecture** — structural commitments (storage, layering,
|
||||||
|
languages, libraries)
|
||||||
|
- **process** — team workflow (commits, branches, releases, reviews)
|
||||||
|
- **design** — user-facing surface (UX, UI, public APIs)
|
||||||
|
- **coding-conventions** — team-internal code shape (style, lint,
|
||||||
|
file layout)
|
||||||
|
- **testing** — quality strategy (coverage, layers, gates)
|
||||||
|
|
||||||
|
You might also want, project-permitting:
|
||||||
|
|
||||||
|
- `accessibility` — if you ship user-facing software
|
||||||
|
- `security` — if you handle user data or network surfaces
|
||||||
|
- `licensing` — if you release open-source or commercial
|
||||||
|
- `documentation` — if user-docs are non-trivial
|
||||||
|
- `deployment` — if shipping is non-trivial
|
||||||
|
- `performance` — if you have perf budgets / SLOs
|
||||||
|
|
||||||
|
<!-- pql:records (auto-generated; do not edit manually) -->
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- [D-1: CLI-first, not MCP](decisions/architecture.md#d-1-cli-first-not-mcp) — _architecture_
|
||||||
|
- [D-3: pql as supporter tool; clide wraps, never duplicates](decisions/architecture.md#d-3-pql-as-supporter-tool-clide-wraps-never-duplicates) — _architecture_
|
||||||
|
- [D-4: Ignore file strategy](decisions/architecture.md#d-4-ignore-file-strategy) — _architecture_
|
||||||
|
- [D-5: Dart core; sidecar dissolved; `ptyc` as pql-peer](decisions/architecture.md#d-5-dart-core-sidecar-dissolved-ptyc-as-pql-peer) — _architecture_
|
||||||
|
- [D-6: CLI and event surface contract](decisions/architecture.md#d-6-cli-and-event-surface-contract) — _architecture_
|
||||||
|
- [D-7: App root is bare `WidgetsApp`](decisions/architecture.md#d-7-app-root-is-bare-widgetsapp) — _architecture_
|
||||||
|
- [D-8: Feature-first folder layout](decisions/architecture.md#d-8-feature-first-folder-layout) — _architecture_
|
||||||
|
- [D-9: Three-tier theme pipeline](decisions/architecture.md#d-9-three-tier-theme-pipeline) — _architecture_
|
||||||
|
- [D-10: State management — `ChangeNotifier` + `ListenableBuilder`](decisions/architecture.md#d-10-state-management--changenotifier--listenablebuilder) — _architecture_
|
||||||
|
- [D-11: Panel manager is kernel; layout is data; three-column is a preset](decisions/architecture.md#d-11-panel-manager-is-kernel-layout-is-data-three-column-is-a-preset) — _architecture_
|
||||||
|
- [D-12: Kernel admission rule — mandatory shared singletons only](decisions/architecture.md#d-12-kernel-admission-rule--mandatory-shared-singletons-only) — _architecture_
|
||||||
|
- [D-13: Git hardcoded in kernel project-loader](decisions/architecture.md#d-13-git-hardcoded-in-kernel-project-loader) — _architecture_
|
||||||
|
- [D-14: Two-tier disable — kernel locked, everything else extension-shaped](decisions/architecture.md#d-14-two-tier-disable--kernel-locked-everything-else-extension-shaped) — _architecture_
|
||||||
|
- [D-15: Extension grain — container-level, multi-contribution](decisions/extensions.md#d-15-extension-grain--container-level-multi-contribution) — _extensions_
|
||||||
|
- [D-16: Built-ins in Dart, third-party in sandboxed Lua](decisions/extensions.md#d-16-built-ins-in-dart-third-party-in-sandboxed-lua) — _extensions_
|
||||||
|
- [D-17: Panels are extension-shaped from day one](decisions/extensions.md#d-17-panels-are-extension-shaped-from-day-one) — _extensions_
|
||||||
|
- [D-18: YAML for themes + manifests; JSON for i18n catalogs](decisions/extensions.md#d-18-yaml-for-themes--manifests-json-for-i18n-catalogs) — _extensions_
|
||||||
|
- [D-19: Lua runtime as `ptyc`-peer supporter tool](decisions/extensions.md#d-19-lua-runtime-as-ptyc-peer-supporter-tool) — _extensions_
|
||||||
|
- [D-20: A11y is a Tier-0 contract](decisions/accessibility.md#d-20-a11y-is-a-tier-0-contract) — _accessibility_
|
||||||
|
- [D-21: i18n is a Tier-0 contract (fframe pattern + locale-fallback chain)](decisions/accessibility.md#d-21-i18n-is-a-tier-0-contract-fframe-pattern--locale-fallback-chain) — _accessibility_
|
||||||
|
- [D-22: WCAG-AA contrast gate on bundled themes](decisions/accessibility.md#d-22-wcag-aa-contrast-gate-on-bundled-themes) — _accessibility_
|
||||||
|
- [D-23: Test pyramid — seven layers](decisions/testing.md#d-23-test-pyramid--seven-layers) — _testing_
|
||||||
|
- [D-24: Golden tests — primitives only, Alchemist + Ahem](decisions/testing.md#d-24-golden-tests--primitives-only-alchemist--ahem) — _testing_
|
||||||
|
- [D-25: Mocks — mocktail at IO, hand-rolled fakes for ChangeNotifiers](decisions/testing.md#d-25-mocks--mocktail-at-io-hand-rolled-fakes-for-changenotifiers) — _testing_
|
||||||
|
- [D-26: Web driver — raw Playwright + Flutter semantics](decisions/testing.md#d-26-web-driver--raw-playwright--flutter-semantics) — _testing_
|
||||||
|
- [D-27: Startup regression gate](decisions/testing.md#d-27-startup-regression-gate) — _testing_
|
||||||
|
- [D-28: Test organisation — mirror `lib/` in `test/`](decisions/testing.md#d-28-test-organisation--mirror-lib-in-test) — _testing_
|
||||||
|
- [D-29: Pre-push gate — fast layer only](decisions/testing.md#d-29-pre-push-gate--fast-layer-only) — _testing_
|
||||||
|
- [D-30: Tests are client-side only](decisions/testing.md#d-30-tests-are-client-side-only) — _testing_
|
||||||
|
- [D-31: Prefer-zero-deps, exact-pin](decisions/tooling.md#d-31-prefer-zero-deps-exact-pin) — _tooling_
|
||||||
|
- [D-32: CI — Gitea primary, Linux-only runners, not yet activated](decisions/tooling.md#d-32-ci--gitea-primary-linux-only-runners-not-yet-activated) — _tooling_
|
||||||
|
- [D-33: Golden-output ignore pattern — `coverage.*` excludes output, not scripts](decisions/tooling.md#d-33-golden-output-ignore-pattern--coverage-excludes-output-not-scripts) — _tooling_
|
||||||
|
- [D-34: Q&D record system](decisions/process.md#d-34-qd-record-system) — _process_
|
||||||
|
- [D-35: Kanban / waterfall, not Scrum](decisions/process.md#d-35-kanban--waterfall-not-scrum) — _process_
|
||||||
|
- [D-36: `.claude/` is committed project surface, managed through the IDE](decisions/process.md#d-36-claude-is-committed-project-surface-managed-through-the-ide) — _process_
|
||||||
|
- [D-37: Commit conventions per git-commit skill](decisions/process.md#d-37-commit-conventions-per-git-commit-skill) — _process_
|
||||||
|
- [D-38: Changelog discipline — Keep a Changelog 1.1.0](decisions/process.md#d-38-changelog-discipline--keep-a-changelog-110) — _process_
|
||||||
|
- [D-39: Planning tooling lives in pql, not clide](decisions/process.md#d-39-planning-tooling-lives-in-pql-not-clide) — _process_
|
||||||
|
- [D-40: [SUPERSEDED] Python stopgap under `tools/scripts/plan`](decisions/process.md#d-40-superseded-python-stopgap-under-toolsscriptsplan) — _process_
|
||||||
|
- [D-41: Claude panes — one primary per repo, tmux-backed](decisions/architecture.md#d-41-claude-panes--one-primary-per-repo-tmux-backed) — _architecture_
|
||||||
|
- [D-42: Dependencies documented in `licenses.yaml`](decisions/tooling.md#d-42-dependencies-documented-in-licensesyaml) — _tooling_
|
||||||
|
- [D-43: Design handoff — adopt token palettes, reject Material wrapper](decisions/architecture.md#d-43-design-handoff--adopt-token-palettes-reject-material-wrapper) — _architecture_
|
||||||
|
- [D-44: Four bundled themes — clide, midnight, paper, terminal](decisions/architecture.md#d-44-four-bundled-themes--clide-midnight-paper-terminal) — _architecture_
|
||||||
|
- [D-45: Syntax highlighting tokens in the theme pipeline](decisions/architecture.md#d-45-syntax-highlighting-tokens-in-the-theme-pipeline) — _architecture_
|
||||||
|
- [D-46: Core frame builtins vs shipped extensions boundary](decisions/extensions.md#d-46-core-frame-builtins-vs-shipped-extensions-boundary) — _extensions_
|
||||||
|
- [D-47: Interaction model — Claude-is-home layout](decisions/architecture.md#d-47-interaction-model--claude-is-home-layout) — _architecture_
|
||||||
|
- [D-48: Chrome budget — no tabs, no breadcrumbs, keyboard-first](decisions/architecture.md#d-48-chrome-budget--no-tabs-no-breadcrumbs-keyboard-first) — _architecture_
|
||||||
|
- [D-49: Editor mode — inline above Claude, viewer swap](decisions/architecture.md#d-49-editor-mode--inline-above-claude-viewer-swap) — _architecture_
|
||||||
|
- [D-50: Context auto-behavior — right panel reacts to Claude](decisions/architecture.md#d-50-context-auto-behavior--right-panel-reacts-to-claude) — _architecture_
|
||||||
|
- [D-51: Panel collapse — 12px spine with badge](decisions/architecture.md#d-51-panel-collapse--12px-spine-with-badge) — _architecture_
|
||||||
|
- [D-52: Focus mode — full-window takeover](decisions/architecture.md#d-52-focus-mode--full-window-takeover) — _architecture_
|
||||||
|
- [D-53: State persistence across sessions](decisions/architecture.md#d-53-state-persistence-across-sessions) — _architecture_
|
||||||
|
- [D-54: Keyboard map — canonical shortcuts](decisions/architecture.md#d-54-keyboard-map--canonical-shortcuts) — _architecture_
|
||||||
|
- [D-55: Claude pane internal tabs for multi-session](decisions/architecture.md#d-55-claude-pane-internal-tabs-for-multi-session) — _architecture_
|
||||||
|
- [D-56: Dissolve daemon process; Flutter app hosts IPC server](decisions/architecture.md#d-56-dissolve-daemon-process-flutter-app-hosts-ipc-server) — _architecture_
|
||||||
|
- [D-57: Frameless custom chrome with per-column 24px hats](decisions/architecture.md#d-57-frameless-custom-chrome-with-per-column-24px-hats) — _architecture_
|
||||||
|
- [D-58: Format engines are adoptable dependencies](decisions/tooling.md#d-58-format-engines-are-adoptable-dependencies) — _tooling_
|
||||||
|
- [D-59: Bundled git via dugite-native](decisions/tooling.md#d-59-bundled-git-via-dugite-native) — _tooling_
|
||||||
|
- [D-60: No network on default launch path](decisions/tooling.md#d-60-no-network-on-default-launch-path) — _tooling_
|
||||||
|
- [D-61: Dependency vetting checklist](decisions/tooling.md#d-61-dependency-vetting-checklist) — _tooling_
|
||||||
|
- [D-62: Dependency removal process](decisions/tooling.md#d-62-dependency-removal-process) — _tooling_
|
||||||
|
- [D-63: Vendored binary rebuild process](decisions/tooling.md#d-63-vendored-binary-rebuild-process) — _tooling_
|
||||||
|
- [D-64: No telemetry — architectural commitment](decisions/architecture.md#d-64-no-telemetry--architectural-commitment) — _architecture_
|
||||||
|
- [D-65: License compatibility matrix](decisions/tooling.md#d-65-license-compatibility-matrix) — _tooling_
|
||||||
|
- [D-66: Line coverage gate at 95%, ratcheted from current](decisions/testing.md#d-66-line-coverage-gate-at-95-ratcheted-from-current) — _testing_
|
||||||
|
- [D-67: Pql changelog files are committed alongside code](decisions/process.md#d-67-pql-changelog-files-are-committed-alongside-code) — _process_
|
||||||
|
- [D-68: Dual integration surface — Bash CLI primary, MCP secondary](decisions/architecture.md#d-68-dual-integration-surface--bash-cli-primary-mcp-secondary) — _architecture_
|
||||||
|
- [D-69: published themes are user contracts; ship -hc variants for a11y](decisions/accessibility.md#d-69-published-themes-are-user-contracts-ship--hc-variants-for-a11y) — _accessibility_
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
- [Q-1: Authorisation granularity on the IPC socket](questions/architecture.md#q-1-authorisation-granularity-on-the-ipc-socket) — _architecture_
|
||||||
|
- [Q-2: Back-pressure on event streams](questions/architecture.md#q-2-back-pressure-on-event-streams) — _architecture_
|
||||||
|
- [Q-3: Event persistence + audit/undo](questions/architecture.md#q-3-event-persistence--auditundo) — _architecture_
|
||||||
|
- [Q-4: `.canvas` schema compatibility with Obsidian](questions/architecture.md#q-4-canvas-schema-compatibility-with-obsidian) — _architecture_
|
||||||
|
- [Q-5: IPC wire-format stability + `schema_version:`](questions/architecture.md#q-5-ipc-wire-format-stability--schema-version) — _architecture_
|
||||||
|
- [Q-6: Window chrome — native frame vs frameless custom](questions/architecture.md#q-6-window-chrome--native-frame-vs-frameless-custom) — _architecture_
|
||||||
|
- [Q-7: macOS app bundle signing / notarisation](questions/architecture.md#q-7-macos-app-bundle-signing--notarisation) — _architecture_
|
||||||
|
- [Q-8: Extension API shape — widgets, subcommands, both?](questions/extensions.md#q-8-extension-api-shape--widgets-subcommands-both) — _extensions_
|
||||||
|
- [Q-9: Lua runtime vendoring](questions/extensions.md#q-9-lua-runtime-vendoring) — _extensions_
|
||||||
|
- [Q-10: Extension manifest `schema_version:`](questions/extensions.md#q-10-extension-manifest-schema-version) — _extensions_
|
||||||
|
- [Q-11: Coverage gates — hard thresholds vs soft reporting](questions/testing.md#q-11-coverage-gates--hard-thresholds-vs-soft-reporting) — _testing_
|
||||||
|
- [Q-12: Screen-reader automation (axe-core via Playwright)](questions/testing.md#q-12-screen-reader-automation-axe-core-via-playwright) — _testing_
|
||||||
|
- [Q-13: Web production-mode a11y](questions/accessibility.md#q-13-web-production-mode-a11y) — _accessibility_
|
||||||
|
- [Q-14: i18n plurals / gender / date-format tooling](questions/accessibility.md#q-14-i18n-plurals--gender--date-format-tooling) — _accessibility_
|
||||||
|
- [Q-15: Editor tab — full LSP vs tree-sitter-only highlight](questions/process.md#q-15-editor-tab--full-lsp-vs-tree-sitter-only-highlight) — _process_
|
||||||
|
- [Q-16: `tree-sitter-dart` grammar maintenance](questions/process.md#q-16-tree-sitter-dart-grammar-maintenance) — _process_
|
||||||
|
- [Q-17: Icon set growth](questions/process.md#q-17-icon-set-growth) — _process_
|
||||||
|
- [Q-18: Theme hot-reload in release builds](questions/process.md#q-18-theme-hot-reload-in-release-builds) — _process_
|
||||||
|
- [Q-19: (withdrawn)](questions/process.md#q-19-withdrawn) — _process_
|
||||||
|
- [Q-20: Kernel DB service — namespaced SQL access?](questions/process.md#q-20-kernel-db-service--namespaced-sql-access) — _process_
|
||||||
|
- [Q-21: Pql absorbs planning vs keeps separate](questions/architecture.md#q-21-pql-absorbs-planning-vs-keeps-separate) — _architecture_
|
||||||
|
- [Q-22: Ticket persistence strategy](questions/architecture.md#q-22-ticket-persistence-strategy) — _architecture_
|
||||||
|
- [Q-23: SSH-remote development — run clide against a remote workspace](questions/architecture.md#q-23-ssh-remote-development--run-clide-against-a-remote-workspace) — _architecture_
|
||||||
|
- [Q-25: Body text face — mono everywhere vs Josefin Sans UI + mono code](questions/architecture.md#q-25-body-text-face--mono-everywhere-vs-josefin-sans-ui--mono-code) — _architecture_
|
||||||
|
- [Q-26: Small screen layout (< 1000px)](questions/architecture.md#q-26-small-screen-layout--1000px) — _architecture_
|
||||||
|
- [Q-27: Two-editor split](questions/architecture.md#q-27-two-editor-split) — _architecture_
|
||||||
|
- [Q-28: Terminal strip scope — shell only or logs/errors/tests](questions/architecture.md#q-28-terminal-strip-scope--shell-only-or-logserrorstests) — _architecture_
|
||||||
|
- [Q-29: Branch picker location](questions/architecture.md#q-29-branch-picker-location) — _architecture_
|
||||||
|
- [Q-30: Focus behavior when editor is dirty and viewer is peeked](questions/architecture.md#q-30-focus-behavior-when-editor-is-dirty-and-viewer-is-peeked) — _architecture_
|
||||||
|
- [Q-31: XWayland fallback for frameless — proper Wayland protocol needed](questions/architecture.md#q-31-xwayland-fallback-for-frameless--proper-wayland-protocol-needed) — _architecture_
|
||||||
|
- [Q-32: MCP tool surface — minimum slash-ide or extended clide tools?](questions/architecture.md#q-32-mcp-tool-surface--minimum-slash-ide-or-extended-clide-tools) — _architecture_
|
||||||
|
- [Q-33: MCP transport — SSE, WebSocket, stdio, or all?](questions/architecture.md#q-33-mcp-transport--sse-websocket-stdio-or-all) — _architecture_
|
||||||
|
|
||||||
|
## Rejected
|
||||||
|
|
||||||
|
- [R-2: Go sidecar](rejected/architecture.md#r-2-go-sidecar) — _architecture_
|
||||||
|
- [R-3: `MaterialApp` root](rejected/architecture.md#r-3-materialapp-root) — _architecture_
|
||||||
|
- [R-4: Flutter `intl` + ARB codegen for i18n](rejected/accessibility.md#r-4-flutter-intl--arb-codegen-for-i18n) — _accessibility_
|
||||||
|
- [R-5: Patrol test runner](rejected/testing.md#r-5-patrol-test-runner) — _testing_
|
||||||
|
- [R-6: Nerd-font glyph icons](rejected/process.md#r-6-nerd-font-glyph-icons) — _process_
|
||||||
|
- [R-7: `CupertinoApp` root](rejected/architecture.md#r-7-cupertinoapp-root) — _architecture_
|
||||||
|
- [R-8: Riverpod / Provider / BLoC for state](rejected/architecture.md#r-8-riverpod--provider--bloc-for-state) — _architecture_
|
||||||
|
- [R-9: Port planning tooling into clide](rejected/process.md#r-9-port-planning-tooling-into-clide) — _process_
|
||||||
|
- [R-10: Python-script stopgap under `tooling/db/`](rejected/process.md#r-10-python-script-stopgap-under-toolingdb) — _process_
|
||||||
|
- [R-11: Permanent stopgap](rejected/process.md#r-11-permanent-stopgap) — _process_
|
||||||
|
- [R-12: MaterialApp wrapper from design handoff](rejected/architecture.md#r-12-materialapp-wrapper-from-design-handoff) — _architecture_
|
||||||
@@ -23,6 +23,13 @@ A11y + i18n are Tier-0 contracts, not Tier-6 polish.
|
|||||||
- **Decision:** Every bundled theme must pass a WCAG-AA contrast check on its canonical token pairs (text/background, link/background, focus-ring/background) at test time. `ci/test_a11y.sh` runs the gate; CI fails on regressions.
|
- **Decision:** Every bundled theme must pass a WCAG-AA contrast check on its canonical token pairs (text/background, link/background, focus-ring/background) at test time. `ci/test_a11y.sh` runs the gate; CI fails on regressions.
|
||||||
- **Rationale:** Themes drift under "looks nicer" tweaks; contrast regressions land silently. Running the gate on every PR is the cheapest insurance. Ran the gate on initial themes — caught one summer-night muted token at 2.81:1 (below AA), fixed before landing.
|
- **Rationale:** Themes drift under "looks nicer" tweaks; contrast regressions land silently. Running the gate on every PR is the cheapest insurance. Ran the gate on initial themes — caught one summer-night muted token at 2.81:1 (below AA), fixed before landing.
|
||||||
- **Cost:** Third-party themes (Tier 6) won't be gated until an extension-time test hook lands. Bundled themes are gated today.
|
- **Cost:** Third-party themes (Tier 6) won't be gated until an extension-time test hook lands. Bundled themes are gated today.
|
||||||
- **Raised by:** 2026-04-21 planning.
|
- **Raised by:** 2026-04-21 planning. Refined by [D-69](#d-69-published-themes-are-user-contracts-ship-hc-variants-for-a11y) — the gate's *strict* pair set only applies to high-contrast variants; named themes keep their published palettes.
|
||||||
|
|
||||||
|
### D-69: published themes are user contracts; ship -hc variants for a11y
|
||||||
|
- **Date:** 2026-05-17
|
||||||
|
- **Decision:** The four bundled themes that ship under a recognisable name — `clide`, `midnight`, `paper`, `terminal` — are user contracts. Their palette colours (including syntax tokens, status colours, and borderHi) MUST NOT be retuned to satisfy contrast gates. When a stricter contrast check would fail one of them, the fix is one of: (a) ship a sibling theme with `-hc` (high-contrast) or `-cb` (colour-blind) in the name and enforce the strict pair set only there, or (b) split `canonicalPairs` into a *baseline* set every theme must pass and an *extended* set that only the `-hc`/`-cb` variants must pass.
|
||||||
|
- **Rationale:** Users pick `midnight` because it looks like VS Code, `paper` because it reads as a drafting sheet, `terminal` because of the amber-on-near-black tmux feel. Quietly darkening `paper`'s success/warning/info or boosting `midnight`'s `borderHi` to pass a WCAG-AA check changes what they got and what they signed up for. A11y is a Tier-0 contract ([D-20](#d-20-a11y-is-a-tier-0-contract)), but it's served by *offering* an accessible variant, not by overwriting the aesthetic ones. VS Code itself ships `Default Dark+` and a separate `Default High Contrast` for exactly this reason.
|
||||||
|
- **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.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -73,6 +73,7 @@ Core, rendering, IPC, kernel, panel manager.
|
|||||||
|
|
||||||
### D-1: CLI-first, not MCP
|
### D-1: CLI-first, not MCP
|
||||||
- **Date:** 2026-04-20 (was ADR 0001; ported from the claudian lineage)
|
- **Date:** 2026-04-20 (was ADR 0001; ported from the claudian lineage)
|
||||||
|
- **Amendment (2026-05-15):** D-1's intent — the CLI is the *primary* agent-facing surface, with the same contract as pql — stands. An additional `/ide`-compatible MCP surface is added per [D-68](#d-68-dual-integration-surface-bash-cli-primary-mcp-secondary); both wrap the same in-process dispatcher. The escape-hatch line in this record's Cost ("nothing here precludes adding [MCP] later that shells out to the same CLI") is realised — the MCP server does not bypass the CLI's surface, it offers a second transport to it.
|
||||||
- **Decision:** Claude talks to clide exclusively via Bash (`clide …`). No MCP server. No protocol layer in Claude's face. The CLI uses the same exit-code + stderr-JSON contract as pql.
|
- **Decision:** Claude talks to clide exclusively via Bash (`clide …`). No MCP server. No protocol layer in Claude's face. The CLI uses the same exit-code + stderr-JSON contract as pql.
|
||||||
- **Context:** The two mainstream options for the agent-facing surface were an MCP server or a plain Bash CLI matching pql's contract.
|
- **Context:** The two mainstream options for the agent-facing surface were an MCP server or a plain Bash CLI matching pql's contract.
|
||||||
- **Rationale:** Same mental model as pql for the agent — one tool-use pattern covers both. No MCP runtime to host, authenticate, or keep in sync with client versions. User/Claude parity is easier to enforce: every CLI subcommand must have a UI affordance in the Flutter app and vice versa ([D-6](#d-6-cli-and-event-surface-contract)). Claude Code's `Bash(clide *)` allow rule is the only configuration clide needs on the agent side.
|
- **Rationale:** Same mental model as pql for the agent — one tool-use pattern covers both. No MCP runtime to host, authenticate, or keep in sync with client versions. User/Claude parity is easier to enforce: every CLI subcommand must have a UI affordance in the Flutter app and vice versa ([D-6](#d-6-cli-and-event-surface-contract)). Claude Code's `Bash(clide *)` allow rule is the only configuration clide needs on the agent side.
|
||||||
@@ -97,7 +98,9 @@ Core, rendering, IPC, kernel, panel manager.
|
|||||||
|
|
||||||
### D-5: Dart core; sidecar dissolved; `ptyc` as pql-peer
|
### D-5: Dart core; sidecar dissolved; `ptyc` as pql-peer
|
||||||
- **Date:** 2026-04-20 (was ADR 0005; supersedes [R-2](rejected.md#r-2-go-sidecar))
|
- **Date:** 2026-04-20 (was ADR 0005; supersedes [R-2](rejected.md#r-2-go-sidecar))
|
||||||
- **Amendment (2026-04-23):** The separate daemon process and two-package layout are dissolved per [D-56](#d-56-dissolve-daemon-process-flutter-app-hosts-ipc-server). Dart-core and ptyc-as-peer principles survive; the daemon binary does not.
|
- **Amendment (2026-04-23):** The separate daemon process and two-package layout are dissolved per [D-56](#d-56-dissolve-daemon-process-flutter-app-hosts-ipc-server). Dart-core principle survives; the daemon binary does not.
|
||||||
|
- **Amendment (2026-05-07):** `ptyc` retired. PTY spawning moved to Dart FFI `forkpty()` (`lib/src/pty/native_pty.dart`). The `ptyc/` source tree, `PtySession`, and `scm_rights.dart` are removed. `pql` remains the sole external supporter tool.
|
||||||
|
- **Amendment (2026-05-17):** `forkpty()` replaced with `posix_openpt()` + `posix_spawn()` (T-96). `forkpty` calls `fork()` underneath, which is unsafe in the multithreaded Dart VM: ~5% of spawns deadlocked in the child before `execve` due to libc locks held by ghost-threads at fork time. `posix_spawn` uses `vfork` under glibc/musl/macOS, keeping the parent suspended until `execve` completes — no Dart code runs in the child. Side benefit: dropped the `libutil.so.1` dynamic dependency; PTY now resolves entirely against libc via `DynamicLibrary.process()`.
|
||||||
- **Decision:** Three moves. **(1) Dart is the core language.** Everything that used to live under `sidecar/` — IPC server, CLI dispatch, process management, file watching, git shell-outs, pql wrapper — is written in Dart. Two execution modes of one Dart AOT binary: `clide <subcommand>` (one-shot, pql-style) and `clide --daemon` (long-running, owns PTYs and subprocesses, survives app restarts). The Flutter app imports the Dart core as a library *and* connects to the daemon over IPC. **(2) The sidecar directory dissolves.** Layout is `app/` (Flutter UI), `lib/` (Dart core), `bin/clide.dart` (AOT entry), `ptyc/` (C helper), no `sidecar/`, no Go module. **(3) `ptyc` is a pql-peer supporter tool.** Small C binary that does `posix_openpt` + `fork` + `exec` + fd-passing via `SCM_RIGHTS`; clide wraps it the same way it wraps pql. Shells out for every PTY (terminal pane, tmux session, Claude, LSP server, debug adapter — one code path). Consumers other than clide can use `ptyc` standalone.
|
- **Decision:** Three moves. **(1) Dart is the core language.** Everything that used to live under `sidecar/` — IPC server, CLI dispatch, process management, file watching, git shell-outs, pql wrapper — is written in Dart. Two execution modes of one Dart AOT binary: `clide <subcommand>` (one-shot, pql-style) and `clide --daemon` (long-running, owns PTYs and subprocesses, survives app restarts). The Flutter app imports the Dart core as a library *and* connects to the daemon over IPC. **(2) The sidecar directory dissolves.** Layout is `app/` (Flutter UI), `lib/` (Dart core), `bin/clide.dart` (AOT entry), `ptyc/` (C helper), no `sidecar/`, no Go module. **(3) `ptyc` is a pql-peer supporter tool.** Small C binary that does `posix_openpt` + `fork` + `exec` + fd-passing via `SCM_RIGHTS`; clide wraps it the same way it wraps pql. Shells out for every PTY (terminal pane, tmux session, Claude, LSP server, debug adapter — one code path). Consumers other than clide can use `ptyc` standalone.
|
||||||
- **Context:** [R-2](rejected.md#r-2-go-sidecar) picked Go for the sidecar/CLI on two premises: (a) the heavy work belongs in a language separate from the UI layer, and (b) pql is Go so the muscle memory transfers. On reassessment, both premises broke: the "heavy work" is I/O-bound glue that `dart:io` covers cleanly — the real choice was **separate process vs shared language**, and separate-process is what matters. PTY is the one place Dart is genuinely weak (multi-threaded VM can't safely `fork()`), and once you accept a small native helper, *nothing else* needs to be in the same language.
|
- **Context:** [R-2](rejected.md#r-2-go-sidecar) picked Go for the sidecar/CLI on two premises: (a) the heavy work belongs in a language separate from the UI layer, and (b) pql is Go so the muscle memory transfers. On reassessment, both premises broke: the "heavy work" is I/O-bound glue that `dart:io` covers cleanly — the real choice was **separate process vs shared language**, and separate-process is what matters. PTY is the one place Dart is genuinely weak (multi-threaded VM can't safely `fork()`), and once you accept a small native helper, *nothing else* needs to be in the same language.
|
||||||
- **Rationale:** One toolchain for the IDE proper (Flutter + Dart). C toolchain needed only to build `ptyc` — tiny, rarely-changing. Session persistence stays because PTY master fds live in the Dart daemon process, not the app. `ptyc` naming: **p** for *project* (parallel to pql's *project query language*), **ptyc** reads as both "PTY + child" (domain vocabulary) and "PTY + C" (implementation language). Usable from Dart, Python, Go, shell — anywhere a subprocess can be spawned and a fd received.
|
- **Rationale:** One toolchain for the IDE proper (Flutter + Dart). C toolchain needed only to build `ptyc` — tiny, rarely-changing. Session persistence stays because PTY master fds live in the Dart daemon process, not the app. `ptyc` naming: **p** for *project* (parallel to pql's *project query language*), **ptyc** reads as both "PTY + child" (domain vocabulary) and "PTY + C" (implementation language). Usable from Dart, Python, Go, shell — anywhere a subprocess can be spawned and a fd received.
|
||||||
@@ -243,4 +246,13 @@ Core, rendering, IPC, kernel, panel manager.
|
|||||||
- **Cross-reference:** [D-60](tooling.md#d-60-no-network-on-default-launch-path), `POLICY.md`.
|
- **Cross-reference:** [D-60](tooling.md#d-60-no-network-on-default-launch-path), `POLICY.md`.
|
||||||
- **Raised by:** 2026-05-03 policy-to-decision migration (T-28).
|
- **Raised by:** 2026-05-03 policy-to-decision migration (T-28).
|
||||||
|
|
||||||
|
### D-68: Dual integration surface — Bash CLI primary, MCP secondary
|
||||||
|
- **Date:** 2026-05-15
|
||||||
|
- **Decision:** clide exposes two integration surfaces over the same in-process `DaemonDispatcher`. **(1) Bash CLI over Unix socket — primary.** Per [D-1](#d-1-cli-first-not-mcp) and [D-56](#d-56-dissolve-daemon-process-flutter-app-hosts-ipc-server), a thin C client (`clide …`) connects to a per-user Unix socket served in-process by the Flutter app, exchanges JSON-lines, and exits. This is the surface Claude-Code-in-a-pane uses; it is also the surface for human shell use, scripts, and external editor integrations. Full action surface — `pane.*`, `files.*`, `editor.*`, `git.*`, `pql.*`, …. **(2) `/ide`-compatible MCP server — secondary.** clide additionally serves an MCP endpoint compatible with Claude Code's `/ide` integration (the same protocol VS Code and JetBrains plugins serve). Minimum tools: `mcp__ide__getDiagnostics`, `mcp__ide__executeCode`. Optional `mcp__clide__*` namespace exposing high-leverage clide tools is deferred to [Q-32](../questions/architecture.md#q-32-mcp-tool-surface-minimum-slash-ide-or-extended-clide-tools). Transport choice deferred to [Q-33](../questions/architecture.md#q-33-mcp-transport-sse-websocket-stdio-or-all). The MCP server wraps the *same* `DaemonDispatcher`; there is no second source of truth.
|
||||||
|
- **Context:** [D-1](#d-1-cli-first-not-mcp) chose CLI-first over MCP-only because MCP alone doesn't cover the action surface clide needs — Claude Code's `/ide` MCP exposes only two narrow tools (`getDiagnostics`, `executeCode`), enough for Claude to read diagnostics and run Jupyter cells but not enough to *drive* an IDE. The CLI surface gives full reach. But for users who run Claude Code *outside* clide and connect via `/ide`, MCP is the only path Claude Code knows; not serving it means clide is invisible to that workflow. The two surfaces are complementary, not alternatives. Reinforced by the [2026-05-14 consultant review](../../consultants.md): the architect flagged the absent socket server as the most critical drift; user confirmed the socket server (D-56 path a) plus an MCP companion.
|
||||||
|
- **Rationale:** Both surfaces wrap the same dispatcher, so neither becomes a second source of truth. CLI remains the contract user/Claude parity ([D-6](#d-6-cli-and-event-surface-contract)) is enforced against. MCP is added because the `/ide` ecosystem is real and growing — VS Code, JetBrains, Cursor, Windsurf all serve compatible MCP — and clide should be a peer there. The implementation cost is a protocol adapter + tool definitions, not duplicate business logic.
|
||||||
|
- **Cost:** Two transports to maintain. Mitigated by both wrapping the same dispatcher: the MCP adapter is the only thing that has to track `/ide` protocol evolution. If `mcp__clide__*` tools are added (pending Q-32), surface bloat is the obvious risk — every CLI verb invites an MCP twin; resist by default, justify on user need.
|
||||||
|
- **Cross-reference:** [D-1](#d-1-cli-first-not-mcp) (amended — see amendment line there), [D-6](#d-6-cli-and-event-surface-contract), [D-56](#d-56-dissolve-daemon-process-flutter-app-hosts-ipc-server), [Q-32](../questions/architecture.md#q-32-mcp-tool-surface-minimum-slash-ide-or-extended-clide-tools), [Q-33](../questions/architecture.md#q-33-mcp-transport-sse-websocket-stdio-or-all).
|
||||||
|
- **Raised by:** 2026-05-15 — consultant review (`consultants.md`) flagged the absent socket server (D-56 unimplemented) as the highest architectural drift; user chose option (a) "implement the server" and asked for MCP coverage alongside.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -53,6 +53,15 @@ Q&D record system itself, kanban, commit conventions, changelog.
|
|||||||
- **Rationale:** Planning tooling must work day one. Pql's Go implementation won't land for at least a cycle or two. Without a stopgap, the convention lives on paper; with one, tickets + decisions are queryable from today. Same schema means migration is call-site find-replace (`tools/scripts/plan ` → `pql `), no data migration.
|
- **Rationale:** Planning tooling must work day one. Pql's Go implementation won't land for at least a cycle or two. Without a stopgap, the convention lives on paper; with one, tickets + decisions are queryable from today. Same schema means migration is call-site find-replace (`tools/scripts/plan ` → `pql `), no data migration.
|
||||||
- **Cost:** Python dep on contributors' machines (already present on most Linux dists). One time-limited tool to maintain. See [R-10](rejected.md#r-10-python-script-stopgap-at-toolingdb) for why `tools/scripts/plan` and not `tooling/db/`.
|
- **Cost:** Python dep on contributors' machines (already present on most Linux dists). One time-limited tool to maintain. See [R-10](rejected.md#r-10-python-script-stopgap-at-toolingdb) for why `tools/scripts/plan` and not `tooling/db/`.
|
||||||
- **Raised by:** 2026-04-21 planning.
|
- **Raised by:** 2026-04-21 planning.
|
||||||
- **Amendment (2026-04-22):** Sunset condition met. pql 1.0.0 ships full feature parity. Stopgap deleted per [R-11](rejected.md#r-11-permanent-stopgap).
|
- **Amendment (2026-04-22):** Sunset condition met. pql 1.0.0 ships full feature parity. Stopgap deleted per [R-11](../rejected/process.md#r-11-permanent-stopgap).
|
||||||
|
|
||||||
|
### D-67: Pql changelog files are committed alongside code
|
||||||
|
- **Date:** 2026-05-11
|
||||||
|
- **Decision:** Clide commits `.pql/changelog/{tickets,ticket_history,ticket_deps,ticket_labels}/<YYYY-MM>.sql` files alongside source changes. `pql.db` itself stays gitignored — it's the local replay target, rebuildable from changelog + `governance/*.md` on any clone. Pre-commit hook auto-stages the changelog deltas; post-merge / post-checkout / post-rewrite hooks replay them into `pql.db`.
|
||||||
|
- **Rationale:** Resolves [Q-22](../questions/architecture.md#q-22-ticket-persistence-strategy). The single-file `pql-plan.json` snapshot model couldn't merge concurrent edits cleanly (every ticket flip rewrote the same JSON). Pql 1.4.x reshaped persistence into append-only per-month SQL files with inline LWW guards, which is option (3) of Q-22 (markdown/SQL mirror, git-legible, DB rebuildable) evolved into a form that merges by default. Clide migrated to it on 2026-05-09 (`01a99ed`, `d162ba2`).
|
||||||
|
- **Cost:** Each user-visible commit also carries the matching changelog diff. The auto-stage hook handles it. Changelog files grow monotonically across commits even on no-change exports — minor file-size cost, no replay-correctness impact (LWW dedupes on import).
|
||||||
|
- **Resolves:** [Q-22](../questions/architecture.md#q-22-ticket-persistence-strategy).
|
||||||
|
- **Cross-references:** [D-3](architecture.md#d-3-pql-as-supporter-tool-clide-wraps-never-duplicates), [D-39](#d-39-planning-tooling-lives-in-pql-not-clide).
|
||||||
|
- **Raised by:** 2026-05-11; cleanup after pql D-21 / governance/ migration.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -6,7 +6,7 @@ Test pyramid, drivers, client-side constraint.
|
|||||||
|
|
||||||
### D-23: Test pyramid — seven layers
|
### D-23: Test pyramid — seven layers
|
||||||
- **Date:** 2026-04-21
|
- **Date:** 2026-04-21
|
||||||
- **Decision:** The pyramid has seven layers: unit (pure Dart) → widget (pumped + find) → golden (visual primitives) → a11y (semantics coverage + keyboard + contrast + i18n) → integration (`flutter test integration_test/`) → E2E (Playwright driving the WASM build + `clide --daemon` subprocess) → startup-smoke (`ci/smoke_bundle.sh`: build Linux release, run under xvfb for 5 s).
|
- **Decision:** The pyramid has seven layers: unit (pure Dart) → widget (pumped + find) → golden (visual primitives) → a11y (semantics coverage + keyboard + contrast + i18n) → integration (`flutter test integration_test/`) → E2E (Playwright driving the WASM build) → startup-smoke (`ci/smoke_bundle.sh`: build Linux release, run under xvfb for 5 s).
|
||||||
- **Rationale:** Each layer catches a distinct regression class. Skipping any layer means that class ships unprotected. Pushed back when earlier rounds proposed "just widget + E2E"; widget can't catch paint regressions (that's golden), E2E can't catch a11y tree drift (that's semantics).
|
- **Rationale:** Each layer catches a distinct regression class. Skipping any layer means that class ships unprotected. Pushed back when earlier rounds proposed "just widget + E2E"; widget can't catch paint regressions (that's golden), E2E can't catch a11y tree drift (that's semantics).
|
||||||
- **Cost:** Seven CI jobs; total wall time budgeted at < 15 min. Pre-push runs layers 1-4 (< 90 s — see [D-29](#d-29-pre-push-gate-fast-layer-only)).
|
- **Cost:** Seven CI jobs; total wall time budgeted at < 15 min. Pre-push runs layers 1-4 (< 90 s — see [D-29](#d-29-pre-push-gate-fast-layer-only)).
|
||||||
- **Raised by:** 2026-04-21 planning.
|
- **Raised by:** 2026-04-21 planning.
|
||||||
@@ -60,4 +60,13 @@ Test pyramid, drivers, client-side constraint.
|
|||||||
- **Cost:** pql / daemon / extension tests stand up real subprocesses and real sockets locally — no mocked network convenience.
|
- **Cost:** pql / daemon / extension tests stand up real subprocesses and real sockets locally — no mocked network convenience.
|
||||||
- **Raised by:** 2026-04-21 planning.
|
- **Raised by:** 2026-04-21 planning.
|
||||||
|
|
||||||
|
### D-66: Line coverage gate at 95%, ratcheted from current
|
||||||
|
- **Date:** 2026-05-06
|
||||||
|
- **Amendment (2026-05-17):** Floor location consolidated — the committed floor lives at `coverage_floor:` in `pubspec.yaml` (single source of truth); `coverage/floor.txt` is no longer used. The 95% target was reached on 2026-05-17; floor is **95** as of that date (T-91 closed). A pre-push **CHANGELOG concision gate** (`ci/changelog_gate.sh`) runs alongside the coverage gate; both live under `make push-check`. A separate `make push-check-full` adds `test-integration` + `smoke-bundle` for pre-release checks (T-103).
|
||||||
|
- **Decision:** The pre-push gate runs `flutter test --coverage --exclude-tags forkpty`, parses `coverage/lcov.info`, and hard-fails if total line coverage drops below a committed floor at `coverage/floor.txt`. The floor starts at the actual current coverage (≈35%, dragged down by `lib/src/terminal/`'s 0.4%) and only ever ratchets up. The end target is 95%; getting there is tracked as a campaign of deliberate floor bumps under one epic ticket. **No carve-outs** — code under `lib/` is owned regardless of file-header attribution, including the terminal emulator port. Branch coverage is not gated (Dart's lcov output models it weakly). Lint suppressions to dodge the gate are never acceptable.
|
||||||
|
- **Rationale:** A flat 95% threshold today blocks every push; an informational coverage report rots into noise. The committed-floor ratchet makes "don't make it worse" the durable rule and turns the journey to 95% into explicit, reviewed bumps rather than a single overnight cliff. Excluding `forkpty`-tagged tests matches `ci/test.sh` (forkpty + flutter test runner are incompatible — see `test/pty/session_test.dart`).
|
||||||
|
- **Cost:** Pre-push wall time grows by `flutter test --coverage` (currently ≈11 s on this tree). Acceptable within D-29's < 90 s budget; reassess if it slips. Floor bumps require an explicit edit to `coverage/floor.txt` in the same commit that adds tests — so contributors can't silently raise it.
|
||||||
|
- **Cross-reference:** [D-29](#d-29-pre-push-gate-fast-layer-only).
|
||||||
|
- **Raised by:** 2026-05-06 — coverage triage during T-73 follow-up.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -9,18 +9,21 @@ ticket persistence.
|
|||||||
- **Status:** Open
|
- **Status:** Open
|
||||||
- **Question:** The daemon's token auth is coarse (allow all / deny all). Do we need per-subsystem grants later (e.g. restrict `git push`), and if so, what's the model — capability tokens? An explicit grant table per client? Time-limited grants?
|
- **Question:** The daemon's token auth is coarse (allow all / deny all). Do we need per-subsystem grants later (e.g. restrict `git push`), and if so, what's the model — capability tokens? An explicit grant table per client? Time-limited grants?
|
||||||
- **Context:** Surfaced in the old ADR 0006 open-questions footer; deferred until Tier 1 is in real use.
|
- **Context:** Surfaced in the old ADR 0006 open-questions footer; deferred until Tier 1 is in real use.
|
||||||
|
- **Triage (2026-05-17):** Still open. Tier 1 has shipped but the IPC socket server itself is unimplemented (T-99). Re-evaluate once the socket lands and external CLI clients exist.
|
||||||
- **Source:** ADR 0006 (migrated to [D-6](architecture.md)).
|
- **Source:** ADR 0006 (migrated to [D-6](architecture.md)).
|
||||||
|
|
||||||
### Q-2: Back-pressure on event streams
|
### Q-2: Back-pressure on event streams
|
||||||
- **Status:** Open
|
- **Status:** Open
|
||||||
- **Question:** A subscriber that falls behind on `pane.output` (a firehose) needs a policy: drop oldest, block producer, coalesce, or kill subscriber. Which?
|
- **Question:** A subscriber that falls behind on `pane.output` (a firehose) needs a policy: drop oldest, block producer, coalesce, or kill subscriber. Which?
|
||||||
- **Context:** The event bus is in-memory; back-pressure policy is undefined. Defer until Tier 1 is in real use and we have a real firehose to measure against.
|
- **Context:** The event bus is in-memory; back-pressure policy is undefined. Defer until Tier 1 is in real use and we have a real firehose to measure against.
|
||||||
|
- **Triage (2026-05-17):** Still open. PTY panes ship and produce real firehoses, but no subscriber has fallen behind in observed use. Re-evaluate when a multi-client IPC scenario (T-99) makes this measurable.
|
||||||
- **Source:** ADR 0006 (migrated to [D-6](architecture.md)).
|
- **Source:** ADR 0006 (migrated to [D-6](architecture.md)).
|
||||||
|
|
||||||
### Q-3: Event persistence + audit/undo
|
### Q-3: Event persistence + audit/undo
|
||||||
- **Status:** Open
|
- **Status:** Open
|
||||||
- **Question:** Events are in-memory only in v1. If a future need (audit log, undo history) wants persistence, is it a property of the bus or a subsystem that subscribes and writes?
|
- **Question:** Events are in-memory only in v1. If a future need (audit log, undo history) wants persistence, is it a property of the bus or a subsystem that subscribes and writes?
|
||||||
- **Context:** ADR 0006 leaned "subsystem that subscribes and writes" but didn't commit.
|
- **Context:** ADR 0006 leaned "subsystem that subscribes and writes" but didn't commit.
|
||||||
|
- **Triage (2026-05-17):** Still open; no concrete trigger yet. Revisit when the first persistence requirement lands (likely Tier-6 audit/undo).
|
||||||
- **Source:** ADR 0006 (migrated to [D-6](architecture.md)).
|
- **Source:** ADR 0006 (migrated to [D-6](architecture.md)).
|
||||||
|
|
||||||
### Q-4: `.canvas` schema compatibility with Obsidian
|
### Q-4: `.canvas` schema compatibility with Obsidian
|
||||||
@@ -48,9 +51,9 @@ ticket persistence.
|
|||||||
- **Source:** 2026-04-21 planning.
|
- **Source:** 2026-04-21 planning.
|
||||||
|
|
||||||
### Q-21: Pql absorbs planning vs keeps separate
|
### Q-21: Pql absorbs planning vs keeps separate
|
||||||
- **Status:** Open
|
- **Status:** Resolved → [D-3](../decisions/architecture.md#d-3-pql-as-supporter-tool-clide-wraps-never-duplicates) + [D-39](../decisions/process.md#d-39-planning-tooling-lives-in-pql-not-clide)
|
||||||
- **Question:** Three shapes for planning tooling's long-term home: (A) Pql absorbs planning — `pql decisions …` + `pql ticket …` subcommands; clide shells out. (B) Clide absorbs pql — reverse [D-3](architecture.md), one big Dart tool. (C) Separate new binary just for planning.
|
- **Question:** Three shapes for planning tooling's long-term home: (A) Pql absorbs planning — `pql decisions …` + `pql ticket …` subcommands; clide shells out. (B) Clide absorbs pql — reverse [D-3](../decisions/architecture.md#d-3-pql-as-supporter-tool-clide-wraps-never-duplicates), one big Dart tool. (C) Separate new binary just for planning.
|
||||||
- **Context:** User is leaning (A). This plan assumes (A) without committing. If (A) doesn't land, [D-40](process.md#d-40-python-stopgap-under-toolsscriptsplan)'s sunset condition changes. Gates all tooling work. Integration constraints that shape this question are captured in [D-39](process.md#d-39-planning-tooling-lives-in-pql) / [R-9](rejected.md#r-9-port-planning-tooling-into-clide).
|
- **Context:** Resolved 2026-05-11 in favour of (A). pql 1.4.30 ships full planning surface (`pql decisions …`, `pql ticket …`, `pql plan …`). Clide consumes via shell-out under `lib/src/pql/`. D-39 already encoded the intent; D-3 the wrap-don't-duplicate rule. The Python stopgap ([D-40](../decisions/process.md#d-40-superseded-python-stopgap-under-toolsscriptsplan)) was sunset on schedule.
|
||||||
- **Source:** 2026-04-21 planning.
|
- **Source:** 2026-04-21 planning.
|
||||||
|
|
||||||
### Q-23: SSH-remote development — run clide against a remote workspace
|
### Q-23: SSH-remote development — run clide against a remote workspace
|
||||||
@@ -60,15 +63,16 @@ ticket persistence.
|
|||||||
- **Source:** 2026-04-22 planning (user-raised).
|
- **Source:** 2026-04-22 planning (user-raised).
|
||||||
|
|
||||||
### Q-22: Ticket persistence strategy
|
### Q-22: Ticket persistence strategy
|
||||||
- **Status:** Open
|
- **Status:** Resolved → [D-67](../decisions/process.md#d-67-pql-changelog-files-are-committed-alongside-code)
|
||||||
- **Question:** Once [Q-21](#q-21-pql-absorbs-planning-vs-keeps-separate) resolves in favour of (A), how do tickets handle shared team state? (1) Never commit (per-dev, ephemeral — works for solo). (2) Commit on milestone (settled-reach's sprint-close pattern — kanban has no natural equivalent, `release` or `tier-cut` is the closest). (3) Markdown mirror — every mutation writes `tickets/T-NNN.md` alongside SQLite; git-legible authoritative record; DB is rebuildable. (3) is probably the eventual answer.
|
- **Question:** Once [Q-21](#q-21-pql-absorbs-planning-vs-keeps-separate) resolves in favour of (A), how do tickets handle shared team state? (1) Never commit (per-dev, ephemeral — works for solo). (2) Commit on milestone (settled-reach's sprint-close pattern — kanban has no natural equivalent, `release` or `tier-cut` is the closest). (3) Markdown mirror — every mutation writes `tickets/T-NNN.md` alongside SQLite; git-legible authoritative record; DB is rebuildable. (3) is probably the eventual answer.
|
||||||
- **Context:** Kanban's lack of a sync event breaks settled-reach's SQLite-authoritative approach the moment two devs collaborate.
|
- **Context:** Resolved 2026-05-11 → option (3), evolved. Pql 1.4.x reshaped ticket persistence into append-only per-month `.pql/changelog/<table>/<YYYY-MM>.sql` files with inline LWW guards. Committed alongside code; `pql.db` rebuildable from changelog + `governance/*.md`. Clide migrated on 2026-05-09.
|
||||||
- **Source:** 2026-04-21 planning.
|
- **Source:** 2026-04-21 planning.
|
||||||
|
|
||||||
### Q-25: Body text face — mono everywhere vs Josefin Sans UI + mono code
|
### Q-25: Body text face — mono everywhere vs Josefin Sans UI + mono code
|
||||||
- **Status:** Open
|
- **Status:** Open
|
||||||
- **Question:** The design handoff uses JetBrains Mono for all UI text (tab labels, file paths, status bar, sidebar labels), reserving Josefin Sans only for display/title text. Our current implementation uses Josefin Sans as the ambient UI face with JetBrains Mono only for code/terminal/diff surfaces. Which direction?
|
- **Question:** The design handoff uses JetBrains Mono for all UI text (tab labels, file paths, status bar, sidebar labels), reserving Josefin Sans only for display/title text. Our current implementation uses Josefin Sans as the ambient UI face with JetBrains Mono only for code/terminal/diff surfaces. Which direction?
|
||||||
- **Context:** The design's "mono everywhere" rationale: clide is an IDE for people who like grids. The current Josefin Sans rationale: visual distinction between chrome text and code text, warmer feel. Both are valid — this is a feel decision, not a technical one.
|
- **Context:** The design's "mono everywhere" rationale: clide is an IDE for people who like grids. The current Josefin Sans rationale: visual distinction between chrome text and code text, warmer feel. Both are valid — this is a feel decision, not a technical one.
|
||||||
|
- **Triage (2026-05-17):** Still open. The Josefin-Sans-as-UI-face implementation has shipped and is the current default; the design's "mono everywhere" direction remains unrealised. Convert to a D-record when the design call is made.
|
||||||
- **Source:** 2026-04-22 design handoff review.
|
- **Source:** 2026-04-22 design handoff review.
|
||||||
|
|
||||||
### Q-26: Small screen layout (< 1000px)
|
### Q-26: Small screen layout (< 1000px)
|
||||||
@@ -125,4 +129,16 @@ ticket persistence.
|
|||||||
|
|
||||||
- **Source:** 2026-04-23 D-57 implementation.
|
- **Source:** 2026-04-23 D-57 implementation.
|
||||||
|
|
||||||
|
### Q-32: MCP tool surface — minimum slash-ide or extended clide tools?
|
||||||
|
- **Status:** Open
|
||||||
|
- **Question:** [D-68](../decisions/architecture.md#d-68-dual-integration-surface-bash-cli-primary-mcp-secondary) commits clide to an `/ide`-compatible MCP server. The minimum surface is the two tools Claude Code's `/ide` integration currently expects: `mcp__ide__getDiagnostics` (lint/diagnostics for a file) and `mcp__ide__executeCode` (run code in a Jupyter kernel). Do we stop there, or also expose a `mcp__clide__*` namespace with higher-leverage tools (`open_file`, `goto_symbol`, `pql_query`, `pane_spawn`, `git_status`, …) so MCP clients other than Claude Code (Cursor, Windsurf, VS Code Copilot) can drive clide as a real backend?
|
||||||
|
- **Context:** The minimum surface keeps clide a good citizen in the `/ide` ecosystem and avoids duplicating the CLI in MCP form. The extended surface would let non-Claude-Code MCP clients integrate richly, but invites surface bloat (every CLI verb tempted to gain an MCP twin) and a maintenance second front. Note that for *Claude-Code-in-a-clide-pane*, the CLI surface already covers this — extended MCP tools serve external MCP clients only.
|
||||||
|
- **Source:** [D-68](../decisions/architecture.md#d-68-dual-integration-surface-bash-cli-primary-mcp-secondary).
|
||||||
|
|
||||||
|
### Q-33: MCP transport — SSE, WebSocket, stdio, or all?
|
||||||
|
- **Status:** Open
|
||||||
|
- **Question:** Claude Code's `/ide` integration connects via SSE-IDE or WS-IDE (URL passed at startup). MCP also supports stdio for process-spawn clients. Which transport(s) should clide's MCP server expose — SSE only (the most common `/ide` server pattern), SSE + WS (broader compatibility), or all three including stdio?
|
||||||
|
- **Context:** Transport choice affects discovery and lifecycle. SSE/WS need a port and a published URL, which collides with the `XDG_RUNTIME_DIR` Unix-socket model used for the CLI; we'd likely publish the URL alongside the socket path (env var or `XDG_RUNTIME_DIR` discovery file). stdio is process-per-client and works for clients that prefer process-spawn over network. Decision interacts with [Q-32](#q-32-mcp-tool-surface-minimum-slash-ide-or-extended-clide-tools) — if the surface stays at the `/ide` minimum, SSE alone is sufficient.
|
||||||
|
- **Source:** [D-68](../decisions/architecture.md#d-68-dual-integration-surface-bash-cli-primary-mcp-secondary).
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Rejected — Accessibility
|
||||||
|
|
||||||
|
Alternatives considered and rejected, with rationale preserved for
|
||||||
|
future reference.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### R-4: Flutter `intl` + ARB codegen for i18n
|
||||||
|
- **Rejected:** 2026-04-21
|
||||||
|
- **Reason:** ARB codegen is inflexible for plugin-contributed catalogs — every catalogue needs a codegen pass, every extension ships with pre-generated Dart, and runtime merging is fighting the tool. The fframe text-driven pattern reads JSON at runtime with no codegen, which fits extension-shipped catalogs cleanly.
|
||||||
|
- **Cross-reference:** [D-21](../decisions/accessibility.md#d-21-i18n-is-a-tier-0-contract)
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Rejected — Architecture
|
||||||
|
|
||||||
|
Alternatives considered and rejected, with rationale preserved for
|
||||||
|
future reference.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### R-2: Go sidecar
|
||||||
|
- **Rejected:** 2026-04-20 (was ADR 0002; superseded by [D-5](../decisions/architecture.md#d-5-dart-core-ptyc-peer))
|
||||||
|
- **Reason:** The ADR picked Go on two premises — (a) the heavy work belongs in a language separate from the UI layer, and (b) pql is Go so muscle memory transfers. Both broke on reassessment. The sidecar stripped of PTY is I/O-bound glue that `dart:io` covers cleanly (unix sockets, JSON-lines framing, process tables, shell-outs). The real axis was *separate process vs shared language*, not Go vs Rust, and separate-process is what matters (session persistence needs the daemon to outlive the app), not language. PTY is the one place Dart is genuinely weak — Dart's multi-threaded VM can't safely `fork()` — and that single constraint forces a native helper regardless, independent of whether the rest of the core is Dart. Once a small native helper is accepted, the question "does *everything else* need to be in that same native language" answers itself: no. Go sidecar directory dissolved; `ptyc` (C, PTY-only, pql-peer) is the surviving native supporter tool.
|
||||||
|
- **Cross-reference:** [D-5](../decisions/architecture.md#d-5-dart-core-ptyc-peer)
|
||||||
|
|
||||||
|
### R-3: `MaterialApp` root
|
||||||
|
- **Rejected:** 2026-04-21
|
||||||
|
- **Reason:** Dragged in Material theming, default icons, and platform chrome that fought the custom three-tier theme pipeline ([D-9](../decisions/architecture.md#d-9-three-tier-theme-pipeline)). Every bundled theme had to override Material defaults to look like clide; the overrides were visible in widget tests as "why is this `ElevatedButton` colored this way."
|
||||||
|
- **Cross-reference:** [D-7](../decisions/architecture.md#d-7-app-root-is-bare-widgetsapp)
|
||||||
|
|
||||||
|
### R-7: `CupertinoApp` root
|
||||||
|
- **Rejected:** 2026-04-21
|
||||||
|
- **Reason:** iOS-opinionated; wrong shell for a Linux-primary desktop IDE. Same theming-collision problem as [R-3](#r-3-materialapp-root).
|
||||||
|
- **Cross-reference:** [D-7](../decisions/architecture.md#d-7-app-root-is-bare-widgetsapp)
|
||||||
|
|
||||||
|
### R-8: Riverpod / Provider / BLoC for state
|
||||||
|
- **Rejected:** 2026-04-21
|
||||||
|
- **Reason:** Violates [D-31](../decisions/tooling.md#d-31-prefer-zero-deps-exact-pin). `ChangeNotifier` + `ListenableBuilder` ship in the SDK, fake trivially, and cover the state model we need. The ergonomic wins of Riverpod / Provider don't clear the "new dependency" bar at clide's scale.
|
||||||
|
- **Cross-reference:** [D-10](../decisions/architecture.md#d-10-state-management-changenotifier)
|
||||||
|
|
||||||
|
### R-12: MaterialApp wrapper from design handoff
|
||||||
|
- **Rejected:** 2026-04-22
|
||||||
|
- **Reason:** The design handoff delivers theme files as `MaterialApp`/`ThemeData` Dart classes. This is the delivery format of claude.ai/design, not a design intent. Adopting Material's widget system would contradict [D-7](../decisions/architecture.md#d-7-app-root-is-bare-widgetsapp) (bare WidgetsApp, no Material/Cupertino). We translate the palette tokens and syntax roles into our existing YAML + `SurfaceTokens` pipeline.
|
||||||
|
- **Cross-reference:** [D-43](../decisions/architecture.md#d-43-design-handoff-adopt-token-palettes-reject-material-wrapper)
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Rejected — Process
|
||||||
|
|
||||||
|
Alternatives considered and rejected, with rationale preserved for
|
||||||
|
future reference.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### R-6: Nerd-font glyph icons
|
||||||
|
- **Rejected:** 2026-04-21
|
||||||
|
- **Reason:** TUI hangover from the Python-era clide under `legacy/`. Not desktop-native; forces a font dependency; doesn't theme consistently. Clide uses custom icon primitives (Tier 6 revisits with proper icon-set design).
|
||||||
|
- **Cross-reference:** [Q-17](../questions/process.md#q-17-icon-set-growth)
|
||||||
|
|
||||||
|
### R-9: Port planning tooling into clide
|
||||||
|
- **Rejected:** 2026-04-21
|
||||||
|
- **Reason:** Earlier in the planning session the assumption was "clide owns Dart subcommands for decisions + tickets." That breaks the day a contributor works in a terminal or in VS Code / JetBrains — they have no `clide` binary to run. Reversing: pql owns planning long-term (see [D-39](../decisions/process.md#d-39-planning-tooling-lives-in-pql)); clide consumes via shell-out.
|
||||||
|
- **Cross-reference:** [D-39](../decisions/process.md#d-39-planning-tooling-lives-in-pql)
|
||||||
|
|
||||||
|
### R-10: Python-script stopgap under `tooling/db/`
|
||||||
|
- **Rejected:** 2026-04-21
|
||||||
|
- **Reason:** Location, not language. Settled-reach puts scripts at `tooling/db/` — copying that path here creates a script-pollution problem: every project using the pattern commits its own copy. The accepted Python port ([D-40](../decisions/process.md#d-40-python-stopgap-under-toolsscriptsplan)) lives at `tools/scripts/plan`, clearly signalled as dev-tooling and time-limited.
|
||||||
|
- **Cross-reference:** [D-40](../decisions/process.md#d-40-python-stopgap-under-toolsscriptsplan)
|
||||||
|
|
||||||
|
### R-11: Permanent stopgap
|
||||||
|
- **Rejected:** 2026-04-21
|
||||||
|
- **Reason:** If the Python port under `tools/scripts/plan` outlasts pql's feature parity, delete it. The deletion commit should be one changeset: remove `tools/scripts/plan`, remove its Makefile target (`decisions-validate` rewires to `pql decisions validate`), add a `CHANGELOG.md` entry under Removed, and verify `.pql/pql.db` still opens under the new `pql` binary.
|
||||||
|
- **Cross-reference:** [D-40](../decisions/process.md#d-40-python-stopgap-under-toolsscriptsplan)
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Rejected — Testing
|
||||||
|
|
||||||
|
Alternatives considered and rejected, with rationale preserved for
|
||||||
|
future reference.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### R-5: Patrol test runner
|
||||||
|
- **Rejected:** 2026-04-21
|
||||||
|
- **Reason:** Adds a dependency (violates [D-31](../decisions/tooling.md#d-31-prefer-zero-deps-exact-pin)) for a capability we get from Playwright + Flutter's own semantics tree. Patrol's value proposition (native-gesture emulation) is less relevant on Linux desktop than on mobile.
|
||||||
|
- **Cross-reference:** [D-26](../decisions/testing.md#d-26-web-driver-raw-playwright-plus-flutter-semantics)
|
||||||
|
|
||||||
|
---
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:clide/app.dart';
|
import 'package:clide/app.dart';
|
||||||
import 'package:clide/builtin/default_layout/default_layout.dart';
|
import 'package:clide/builtin/default_layout/default_layout.dart';
|
||||||
@@ -22,6 +23,16 @@ void main() {
|
|||||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
testWidgets('clide app boots with classic 3-column layout + welcome + statusbar', (tester) async {
|
testWidgets('clide app boots with classic 3-column layout + welcome + statusbar', (tester) async {
|
||||||
|
// The welcome view's TIPS card overflows the default headless test
|
||||||
|
// viewport (~800px); give it a desktop-sized window so layout is
|
||||||
|
// representative of a real launch.
|
||||||
|
tester.view.physicalSize = const Size(1600, 1000);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(() {
|
||||||
|
tester.view.resetPhysicalSize();
|
||||||
|
tester.view.resetDevicePixelRatio();
|
||||||
|
});
|
||||||
|
|
||||||
final themes = [
|
final themes = [
|
||||||
await const ThemeLoader().fromAsset(
|
await const ThemeLoader().fromAsset(
|
||||||
rootBundle,
|
rootBundle,
|
||||||
@@ -58,10 +69,15 @@ void main() {
|
|||||||
|
|
||||||
// Welcome tab is mounted in the workspace.
|
// Welcome tab is mounted in the workspace.
|
||||||
expect(find.text('clide'), findsWidgets);
|
expect(find.text('clide'), findsWidgets);
|
||||||
expect(find.text('Open project'), findsOneWidget);
|
// The visible START row exposes "Open folder…"; the "Open project"
|
||||||
|
// dialog title only appears after the user clicks through, so we
|
||||||
|
// assert the on-boot label here.
|
||||||
|
expect(find.text('Open folder…'), findsWidgets);
|
||||||
|
|
||||||
// IPC status indicator reports disconnected (fake client never connects).
|
// Welcome view's toolchain status shows "checking…" while the
|
||||||
expect(find.text('disconnected'), findsOneWidget);
|
// backend hasn't reported resolution (FakeDaemonClient never does
|
||||||
|
// — autoStartDaemonClient is false).
|
||||||
|
expect(find.text('checking…'), findsWidgets);
|
||||||
|
|
||||||
await services.dispose();
|
await services.dispose();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:clide/app.dart';
|
import 'package:clide/app.dart';
|
||||||
import 'package:clide/builtin/default_layout/default_layout.dart';
|
import 'package:clide/builtin/default_layout/default_layout.dart';
|
||||||
@@ -15,6 +16,15 @@ void main() {
|
|||||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
testWidgets('disable + re-enable an extension mounts/unmounts its UI', (tester) async {
|
testWidgets('disable + re-enable an extension mounts/unmounts its UI', (tester) async {
|
||||||
|
// Welcome view overflows the default headless viewport — give it a
|
||||||
|
// desktop-sized window so layout is representative.
|
||||||
|
tester.view.physicalSize = const Size(1600, 1000);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(() {
|
||||||
|
tester.view.resetPhysicalSize();
|
||||||
|
tester.view.resetDevicePixelRatio();
|
||||||
|
});
|
||||||
|
|
||||||
final themes = [
|
final themes = [
|
||||||
await const ThemeLoader().fromAsset(
|
await const ThemeLoader().fromAsset(
|
||||||
rootBundle,
|
rootBundle,
|
||||||
@@ -41,17 +51,21 @@ void main() {
|
|||||||
|
|
||||||
await tester.pumpWidget(ClideApp(services: services));
|
await tester.pumpWidget(ClideApp(services: services));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('disconnected'), findsOneWidget);
|
// The ipc-status extension contributes a ToolStatusItem to the
|
||||||
|
// statusbar; we assert its presence by widget type so the test
|
||||||
|
// doesn't depend on whichever status string (`application ok` /
|
||||||
|
// `checking…` / `<tool> not found`) the toolchain happens to be in.
|
||||||
|
expect(find.byType(ToolStatusItem), findsOneWidget);
|
||||||
|
|
||||||
// Disable ipc-status; status item should disappear.
|
// Disable ipc-status; status item should disappear.
|
||||||
await services.extensions.setEnabled('builtin.ipc-status', false);
|
await services.extensions.setEnabled('builtin.ipc-status', false);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('disconnected'), findsNothing);
|
expect(find.byType(ToolStatusItem), findsNothing);
|
||||||
|
|
||||||
// Re-enable; status item reappears.
|
// Re-enable; status item reappears.
|
||||||
await services.extensions.setEnabled('builtin.ipc-status', true);
|
await services.extensions.setEnabled('builtin.ipc-status', true);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('disconnected'), findsOneWidget);
|
expect(find.byType(ToolStatusItem), findsOneWidget);
|
||||||
|
|
||||||
await services.dispose();
|
await services.dispose();
|
||||||
});
|
});
|
||||||
|
|||||||
+253
-122
@@ -55,24 +55,23 @@ class _RootShell extends StatefulWidget {
|
|||||||
|
|
||||||
class _RootShellState extends State<_RootShell> {
|
class _RootShellState extends State<_RootShell> {
|
||||||
late final FocusNode _keyFocus;
|
late final FocusNode _keyFocus;
|
||||||
double _textScale = 1.0;
|
|
||||||
|
|
||||||
static const double _scaleStep = 0.05;
|
|
||||||
static const double _scaleMin = 0.6;
|
|
||||||
static const double _scaleMax = 2.0;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_keyFocus = FocusNode()..requestFocus();
|
_keyFocus = FocusNode()..requestFocus();
|
||||||
|
widget.services.textZoom.addListener(_onZoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
widget.services.textZoom.removeListener(_onZoom);
|
||||||
_keyFocus.dispose();
|
_keyFocus.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onZoom() => setState(() {});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final tokens = ClideTheme.of(context).surface;
|
final tokens = ClideTheme.of(context).surface;
|
||||||
@@ -86,31 +85,77 @@ class _RootShellState extends State<_RootShell> {
|
|||||||
fontFamilyFallback: clideUiFamilyFallback,
|
fontFamilyFallback: clideUiFamilyFallback,
|
||||||
),
|
),
|
||||||
child: MediaQuery(
|
child: MediaQuery(
|
||||||
data: MediaQuery.of(context).copyWith(textScaler: TextScaler.linear(_textScale)),
|
data: MediaQuery.of(context).copyWith(textScaler: TextScaler.linear(widget.services.textZoom.scale)),
|
||||||
child: KeyboardListener(
|
child: Actions(
|
||||||
focusNode: _keyFocus,
|
actions: <Type, Action<Intent>>{
|
||||||
autofocus: true,
|
TextScaleIncreaseIntent: CallbackAction<TextScaleIncreaseIntent>(
|
||||||
onKeyEvent: _onKey,
|
onInvoke: (_) {
|
||||||
child: ColoredBox(
|
widget.services.textZoom.increase();
|
||||||
color: tokens.globalBackground,
|
return null;
|
||||||
child: ClideResizeBorder(
|
},
|
||||||
windowControls: widget.services.window,
|
),
|
||||||
child: Column(
|
TextScaleDecreaseIntent: CallbackAction<TextScaleDecreaseIntent>(
|
||||||
children: [
|
onInvoke: (_) {
|
||||||
_HatBar(kernel: widget.services),
|
widget.services.textZoom.decrease();
|
||||||
Expanded(
|
return null;
|
||||||
child: DialogHost(
|
},
|
||||||
router: widget.services.dialog,
|
),
|
||||||
child: Stack(
|
TextScaleResetIntent: CallbackAction<TextScaleResetIntent>(
|
||||||
children: [
|
onInvoke: (_) {
|
||||||
const Positioned.fill(child: RootLayout()),
|
widget.services.textZoom.reset();
|
||||||
const ClidePalette(),
|
return null;
|
||||||
const Positioned.fill(child: _WelcomeOverlay()),
|
},
|
||||||
],
|
),
|
||||||
|
InvokeCommandIntent: CallbackAction<InvokeCommandIntent>(
|
||||||
|
onInvoke: (intent) {
|
||||||
|
widget.services.commands.execute(intent.commandId);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
PaletteOpenIntent: CallbackAction<PaletteOpenIntent>(
|
||||||
|
onInvoke: (_) {
|
||||||
|
widget.services.palette.open();
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
FocusNextPanelIntent: CallbackAction<FocusNextPanelIntent>(
|
||||||
|
onInvoke: (_) {
|
||||||
|
widget.services.focus.focusNextSlot();
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
FocusPreviousPanelIntent: CallbackAction<FocusPreviousPanelIntent>(
|
||||||
|
onInvoke: (_) {
|
||||||
|
widget.services.focus.focusPreviousSlot();
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
child: KeyboardListener(
|
||||||
|
focusNode: _keyFocus,
|
||||||
|
autofocus: true,
|
||||||
|
onKeyEvent: _onKey,
|
||||||
|
child: ColoredBox(
|
||||||
|
color: tokens.globalBackground,
|
||||||
|
child: ClideResizeBorder(
|
||||||
|
windowControls: widget.services.window,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_HatBar(kernel: widget.services),
|
||||||
|
Expanded(
|
||||||
|
child: DialogHost(
|
||||||
|
router: widget.services.dialog,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
const Positioned.fill(child: RootLayout()),
|
||||||
|
const ClidePalette(),
|
||||||
|
const Positioned.fill(child: _WelcomeOverlay()),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -120,31 +165,14 @@ class _RootShellState extends State<_RootShell> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onKey(KeyEvent event) {
|
void _onKey(KeyEvent event) {
|
||||||
if (event is KeyDownEvent || event is KeyRepeatEvent) {
|
final intent = widget.services.keymap.resolveEvent(event, HardwareKeyboard.instance);
|
||||||
final ctrl = HardwareKeyboard.instance.isControlPressed;
|
if (intent == null) return;
|
||||||
if (ctrl) {
|
// Dispatch the intent. Try the focused context first so feature
|
||||||
if (event.logicalKey == LogicalKeyboardKey.equal || event.logicalKey == LogicalKeyboardKey.add) {
|
// widgets (palette, editor, …) get a chance to handle their own
|
||||||
setState(() => _textScale = (_textScale + _scaleStep).clamp(_scaleMin, _scaleMax));
|
// intents; fall back to the app root's Actions for global ones
|
||||||
return;
|
// (text scale, generic command bridge).
|
||||||
}
|
final ctx = FocusManager.instance.primaryFocus?.context ?? context;
|
||||||
if (event.logicalKey == LogicalKeyboardKey.minus) {
|
Actions.maybeInvoke(ctx, intent);
|
||||||
setState(() => _textScale = (_textScale - _scaleStep).clamp(_scaleMin, _scaleMax));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.logicalKey == LogicalKeyboardKey.digit0) {
|
|
||||||
setState(() => _textScale = 1.0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final binding = KeybindingResolver.fromKeyEvent(
|
|
||||||
event,
|
|
||||||
HardwareKeyboard.instance,
|
|
||||||
);
|
|
||||||
if (binding == null) return;
|
|
||||||
final commandId = widget.services.keybindings.commandFor(binding);
|
|
||||||
if (commandId == null) return;
|
|
||||||
widget.services.commands.execute(commandId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,7 +345,7 @@ class _WinBtn extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final hoverBg = isClose ? const Color(0xFFE81123) : tokens.listItemHoverBackground;
|
final hoverBg = isClose ? tokens.windowControlCloseHoverBackground : tokens.listItemHoverBackground;
|
||||||
return ClideTappable(
|
return ClideTappable(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
builder: (context, hovered, _) => Container(
|
builder: (context, hovered, _) => Container(
|
||||||
@@ -325,7 +353,11 @@ class _WinBtn extends StatelessWidget {
|
|||||||
height: hatHeight,
|
height: hatHeight,
|
||||||
color: hovered ? hoverBg : null,
|
color: hovered ? hoverBg : null,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: ClideIcon(icon, size: 14, color: hovered && isClose ? const Color(0xFFFFFFFF) : tokens.chromeForeground),
|
child: ClideIcon(
|
||||||
|
icon,
|
||||||
|
size: 14,
|
||||||
|
color: hovered && isClose ? tokens.windowControlCloseHoverForeground : tokens.chromeForeground,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -719,66 +751,122 @@ class _NotARepoDialog extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SlotHost extends StatelessWidget {
|
class SlotHost extends StatefulWidget {
|
||||||
const SlotHost({super.key, required this.slot});
|
const SlotHost({super.key, required this.slot});
|
||||||
final SlotId slot;
|
final SlotId slot;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SlotHost> createState() => _SlotHostState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SlotHostState extends State<SlotHost> {
|
||||||
|
late final FocusScopeNode _scope = FocusScopeNode(debugLabel: 'SlotScope:${widget.slot.value}');
|
||||||
|
FocusTracker? _tracker;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
final kernel = ClideKernel.of(context);
|
||||||
|
if (!identical(_tracker, kernel.focus)) {
|
||||||
|
_tracker?.unregisterSlotScope(widget.slot, _scope);
|
||||||
|
_tracker = kernel.focus;
|
||||||
|
_tracker!.registerSlotScope(widget.slot, _scope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_tracker?.unregisterSlotScope(widget.slot, _scope);
|
||||||
|
_scope.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onFocusChange(bool hasFocus) {
|
||||||
|
if (!hasFocus || _tracker == null) return;
|
||||||
|
final kernel = ClideKernel.of(context);
|
||||||
|
final activeId = kernel.panels.activeTabIn(widget.slot);
|
||||||
|
if (activeId != null) {
|
||||||
|
_tracker!.setActive(slot: widget.slot, contributionId: activeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final kernel = ClideKernel.of(context);
|
final kernel = ClideKernel.of(context);
|
||||||
final tokens = ClideTheme.of(context).surface;
|
final tokens = ClideTheme.of(context).surface;
|
||||||
return ListenableBuilder(
|
return FocusScope(
|
||||||
listenable: Listenable.merge([kernel.panels, kernel.i18n]),
|
node: _scope,
|
||||||
builder: (ctx, _) {
|
onFocusChange: _onFocusChange,
|
||||||
final tabs = kernel.panels.tabsFor(slot);
|
child: FocusTraversalGroup(
|
||||||
if (tabs.isEmpty) {
|
child: ListenableBuilder(
|
||||||
return Container(color: tokens.panelBackground);
|
listenable: Listenable.merge([kernel.panels, kernel.i18n]),
|
||||||
}
|
builder: (ctx, _) {
|
||||||
final activeId = kernel.panels.activeTabIn(slot) ?? tabs.first.id;
|
final tabs = kernel.panels.tabsFor(widget.slot);
|
||||||
final active = tabs.firstWhere(
|
if (tabs.isEmpty) {
|
||||||
(t) => t.id == activeId,
|
return Container(color: tokens.panelBackground);
|
||||||
orElse: () => tabs.first,
|
}
|
||||||
);
|
final activeId = kernel.panels.activeTabIn(widget.slot) ?? tabs.first.id;
|
||||||
|
final active = tabs.firstWhere(
|
||||||
|
(t) => t.id == activeId,
|
||||||
|
orElse: () => tabs.first,
|
||||||
|
);
|
||||||
|
return _SlotBody(slot: widget.slot, tabs: tabs, active: active, activeId: activeId);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (slot == Slots.sidebar) {
|
class _SlotBody extends StatelessWidget {
|
||||||
return _SidebarSlot(
|
const _SlotBody({required this.slot, required this.tabs, required this.active, required this.activeId});
|
||||||
tabs: tabs,
|
final SlotId slot;
|
||||||
active: active,
|
final List<TabContribution> tabs;
|
||||||
activeId: activeId,
|
final TabContribution active;
|
||||||
onSelect: (id) => kernel.panels.activateTab(slot, id),
|
final String activeId;
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (slot == Slots.contextPanel) {
|
@override
|
||||||
return _ContextSlot(
|
Widget build(BuildContext context) {
|
||||||
tabs: tabs,
|
final kernel = ClideKernel.of(context);
|
||||||
active: active,
|
final tokens = ClideTheme.of(context).surface;
|
||||||
activeId: activeId,
|
|
||||||
onSelect: (id) => kernel.panels.activateTab(slot, id),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (slot == Slots.workspace) {
|
if (slot == Slots.sidebar) {
|
||||||
return _WorkspaceSlot(tabs: tabs, active: active);
|
return _SidebarSlot(
|
||||||
}
|
tabs: tabs,
|
||||||
|
active: active,
|
||||||
|
activeId: activeId,
|
||||||
|
onSelect: (id) => kernel.panels.activateTab(slot, id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Container(
|
if (slot == Slots.contextPanel) {
|
||||||
color: tokens.panelBackground,
|
return _ContextSlot(
|
||||||
child: Column(
|
tabs: tabs,
|
||||||
children: [
|
active: active,
|
||||||
ClideTabBar(
|
activeId: activeId,
|
||||||
items: [
|
onSelect: (id) => kernel.panels.activateTab(slot, id),
|
||||||
for (final t in tabs) ClideTabItem(id: t.id, title: _resolveTitle(ctx, t)),
|
);
|
||||||
],
|
}
|
||||||
activeId: active.id,
|
|
||||||
onSelect: (id) => kernel.panels.activateTab(slot, id),
|
if (slot == Slots.workspace) {
|
||||||
),
|
return _WorkspaceSlot(tabs: tabs, active: active);
|
||||||
ClideDivider(),
|
}
|
||||||
Expanded(child: active.build(ctx)),
|
|
||||||
|
return Container(
|
||||||
|
color: tokens.panelBackground,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ClideTabBar(
|
||||||
|
items: [
|
||||||
|
for (final t in tabs) ClideTabItem(id: t.id, title: _resolveTitle(context, t)),
|
||||||
],
|
],
|
||||||
|
activeId: active.id,
|
||||||
|
onSelect: (id) => kernel.panels.activateTab(slot, id),
|
||||||
),
|
),
|
||||||
);
|
ClideDivider(),
|
||||||
},
|
Expanded(child: active.build(context)),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -879,36 +967,79 @@ class _EditorDragHandle extends StatefulWidget {
|
|||||||
|
|
||||||
class _EditorDragHandleState extends State<_EditorDragHandle> {
|
class _EditorDragHandleState extends State<_EditorDragHandle> {
|
||||||
bool _hovered = false;
|
bool _hovered = false;
|
||||||
|
bool _focused = false;
|
||||||
double? _dragStartRatio;
|
double? _dragStartRatio;
|
||||||
double? _dragStartY;
|
double? _dragStartY;
|
||||||
|
|
||||||
|
// Editor split is a 0..1 fraction; the kernel clamps to 0.15..0.70.
|
||||||
|
// 2% per fine step, 10% per Shift step keeps keyboard feel close to
|
||||||
|
// the pixel-based DragResizeHandle.
|
||||||
|
static const double _stepFine = 0.02;
|
||||||
|
static const double _stepCoarse = 0.10;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final tokens = ClideTheme.of(context).surface;
|
final tokens = ClideTheme.of(context).surface;
|
||||||
return MouseRegion(
|
final lineColor = (_hovered || _focused) ? tokens.panelActiveBorder : tokens.panelBorder;
|
||||||
cursor: SystemMouseCursors.resizeRow,
|
|
||||||
onEnter: (_) => setState(() => _hovered = true),
|
return Semantics(
|
||||||
onExit: (_) => setState(() => _hovered = false),
|
container: true,
|
||||||
child: Listener(
|
slider: true,
|
||||||
onPointerDown: (e) {
|
label: 'Editor split',
|
||||||
_dragStartRatio = widget.arrangement.editorRatio;
|
value: '${(widget.arrangement.editorRatio * 100).round()}%',
|
||||||
_dragStartY = e.position.dy;
|
onIncrease: () => _bump(_stepFine),
|
||||||
|
onDecrease: () => _bump(-_stepFine),
|
||||||
|
child: FocusableActionDetector(
|
||||||
|
onShowFocusHighlight: (v) => setState(() => _focused = v),
|
||||||
|
shortcuts: const <ShortcutActivator, Intent>{
|
||||||
|
SingleActivator(LogicalKeyboardKey.arrowUp): _EditorBumpIntent(-_stepFine),
|
||||||
|
SingleActivator(LogicalKeyboardKey.arrowDown): _EditorBumpIntent(_stepFine),
|
||||||
|
SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): _EditorBumpIntent(-_stepCoarse),
|
||||||
|
SingleActivator(LogicalKeyboardKey.arrowDown, shift: true): _EditorBumpIntent(_stepCoarse),
|
||||||
},
|
},
|
||||||
onPointerMove: (e) {
|
actions: <Type, Action<Intent>>{
|
||||||
final startR = _dragStartRatio;
|
_EditorBumpIntent: CallbackAction<_EditorBumpIntent>(
|
||||||
final startY = _dragStartY;
|
onInvoke: (intent) {
|
||||||
if (startR == null || startY == null || widget.totalHeight <= 0) return;
|
_bump(intent.delta);
|
||||||
final deltaRatio = (e.position.dy - startY) / widget.totalHeight;
|
return null;
|
||||||
widget.arrangement.setEditorRatio(startR + deltaRatio);
|
},
|
||||||
|
),
|
||||||
},
|
},
|
||||||
onPointerUp: (_) {
|
child: MouseRegion(
|
||||||
_dragStartRatio = null;
|
cursor: SystemMouseCursors.resizeRow,
|
||||||
_dragStartY = null;
|
onEnter: (_) => setState(() => _hovered = true),
|
||||||
},
|
onExit: (_) => setState(() => _hovered = false),
|
||||||
child: Container(height: 4, color: _hovered ? tokens.panelActiveBorder : tokens.panelBorder),
|
child: Listener(
|
||||||
|
onPointerDown: (e) {
|
||||||
|
_dragStartRatio = widget.arrangement.editorRatio;
|
||||||
|
_dragStartY = e.position.dy;
|
||||||
|
},
|
||||||
|
onPointerMove: (e) {
|
||||||
|
final startR = _dragStartRatio;
|
||||||
|
final startY = _dragStartY;
|
||||||
|
if (startR == null || startY == null || widget.totalHeight <= 0) return;
|
||||||
|
final deltaRatio = (e.position.dy - startY) / widget.totalHeight;
|
||||||
|
widget.arrangement.setEditorRatio(startR + deltaRatio);
|
||||||
|
},
|
||||||
|
onPointerUp: (_) {
|
||||||
|
_dragStartRatio = null;
|
||||||
|
_dragStartY = null;
|
||||||
|
},
|
||||||
|
child: Container(height: 4, color: lineColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _bump(double delta) {
|
||||||
|
widget.arrangement.setEditorRatio(widget.arrangement.editorRatio + delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EditorBumpIntent extends Intent {
|
||||||
|
const _EditorBumpIntent(this.delta);
|
||||||
|
final double delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ContextSlot extends StatelessWidget {
|
class _ContextSlot extends StatelessWidget {
|
||||||
@@ -958,7 +1089,7 @@ class _BottomRail extends StatelessWidget {
|
|||||||
ClideIconRailItem(
|
ClideIconRailItem(
|
||||||
id: t.id,
|
id: t.id,
|
||||||
icon: _iconFor(slot, t),
|
icon: _iconFor(slot, t),
|
||||||
tooltip: SlotHost._resolveTitle(ctx, t),
|
tooltip: _SlotBody._resolveTitle(ctx, t),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
activeId: activeId,
|
activeId: activeId,
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class _ClaudePaneState extends State<ClaudePane> {
|
|||||||
// Primary panes leave the tmux session alive so the next launch
|
// Primary panes leave the tmux session alive so the next launch
|
||||||
// re-attaches via `tmux new-session -A` (D-41).
|
// re-attaches via `tmux new-session -A` (D-41).
|
||||||
//
|
//
|
||||||
// pane.close kills the ptyc-spawned tmux *client*; the tmux server
|
// pane.close kills the PTY-spawned tmux *client*; the tmux server
|
||||||
// keeps the session alive. We need an explicit kill-session for
|
// keeps the session alive. We need an explicit kill-session for
|
||||||
// secondaries to actually disappear (D-41 close semantics).
|
// secondaries to actually disappear (D-41 close semantics).
|
||||||
if (id != null && !widget.isPrimary) {
|
if (id != null && !widget.isPrimary) {
|
||||||
@@ -129,9 +129,7 @@ class _ClaudePaneState extends State<ClaudePane> {
|
|||||||
repoRoot = (rootResp.data['path'] as String?) ?? repoRoot;
|
repoRoot = (rootResp.data['path'] as String?) ?? repoRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
_sessionName = widget.isPrimary
|
_sessionName = widget.isPrimary ? primarySessionName(repoRoot) : secondarySessionName(repoRoot, widget.secondaryIndex!);
|
||||||
? primarySessionName(repoRoot)
|
|
||||||
: secondarySessionName(repoRoot, widget.secondaryIndex!);
|
|
||||||
|
|
||||||
final tmuxConf = await _ensureTmuxConf();
|
final tmuxConf = await _ensureTmuxConf();
|
||||||
final cols = _terminal.viewWidth;
|
final cols = _terminal.viewWidth;
|
||||||
@@ -139,7 +137,8 @@ class _ClaudePaneState extends State<ClaudePane> {
|
|||||||
|
|
||||||
var argv = <String>[
|
var argv = <String>[
|
||||||
'tmux',
|
'tmux',
|
||||||
'-L', 'clide',
|
'-L',
|
||||||
|
'clide',
|
||||||
if (tmuxConf != null) ...['-f', tmuxConf],
|
if (tmuxConf != null) ...['-f', tmuxConf],
|
||||||
'new-session',
|
'new-session',
|
||||||
'-A',
|
'-A',
|
||||||
@@ -215,14 +214,10 @@ class _ClaudePaneState extends State<ClaudePane> {
|
|||||||
final b64 = e.data['bytes_b64'];
|
final b64 = e.data['bytes_b64'];
|
||||||
if (b64 is String) {
|
if (b64 is String) {
|
||||||
_outputBuf.write(utf8.decode(base64Decode(b64), allowMalformed: true));
|
_outputBuf.write(utf8.decode(base64Decode(b64), allowMalformed: true));
|
||||||
if (_flushTimer == null) {
|
_flushTimer ??= Timer(Duration.zero, _flushOutput);
|
||||||
_flushTimer = Timer(Duration.zero, _flushOutput);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case 'pane.exit':
|
case 'pane.exit':
|
||||||
setState(() => _statusLine = widget.isPrimary
|
setState(() => _statusLine = widget.isPrimary ? 'session exited — restart clide to retry' : 'session exited');
|
||||||
? 'session exited — restart clide to retry'
|
|
||||||
: 'session exited');
|
|
||||||
case 'pane.closed':
|
case 'pane.closed':
|
||||||
_paneId = null;
|
_paneId = null;
|
||||||
}
|
}
|
||||||
@@ -252,10 +247,15 @@ class _ClaudePaneState extends State<ClaudePane> {
|
|||||||
_ipc()?.request('pane.resize', args: {'id': id, 'cols': cols, 'rows': rows});
|
_ipc()?.request('pane.resize', args: {'id': id, 'cols': cols, 'rows': rows});
|
||||||
if (_sessionName != null) {
|
if (_sessionName != null) {
|
||||||
Process.run('tmux', [
|
Process.run('tmux', [
|
||||||
'-L', 'clide', 'resize-window',
|
'-L',
|
||||||
'-t', _sessionName!,
|
'clide',
|
||||||
'-x', '$cols',
|
'resize-window',
|
||||||
'-y', '$rows',
|
'-t',
|
||||||
|
_sessionName!,
|
||||||
|
'-x',
|
||||||
|
'$cols',
|
||||||
|
'-y',
|
||||||
|
'$rows',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -277,9 +277,7 @@ class _ClaudePaneState extends State<ClaudePane> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final title = widget.isPrimary
|
final title = widget.isPrimary ? 'claude — primary' : 'claude — secondary ${widget.secondaryIndex}';
|
||||||
? 'claude — primary'
|
|
||||||
: 'claude — secondary ${widget.secondaryIndex}';
|
|
||||||
|
|
||||||
final body = _error != null
|
final body = _error != null
|
||||||
? Padding(
|
? Padding(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/// tmux server interactions for Claude panes (D-41 lifecycle).
|
/// tmux server interactions for Claude panes (D-41 lifecycle).
|
||||||
///
|
///
|
||||||
/// `pane.close` only kills the ptyc-spawned tmux *client*; tmux is
|
/// `pane.close` only kills the PTY-spawned tmux *client*; tmux is
|
||||||
/// client/server, so the server-side session keeps running after the
|
/// client/server, so the server-side session keeps running after the
|
||||||
/// client disconnects. To honour D-41 ("closing a secondary kills that
|
/// client disconnects. To honour D-41 ("closing a secondary kills that
|
||||||
/// tmux session" + "secondary numbering resets between clide runs"),
|
/// tmux session" + "secondary numbering resets between clide runs"),
|
||||||
@@ -14,8 +14,7 @@ typedef TmuxRunner = Future<ProcessResult> Function(List<String> args);
|
|||||||
|
|
||||||
TmuxRunner tmuxRunner = _defaultRunner;
|
TmuxRunner tmuxRunner = _defaultRunner;
|
||||||
|
|
||||||
Future<ProcessResult> _defaultRunner(List<String> args) =>
|
Future<ProcessResult> _defaultRunner(List<String> args) => Process.run('tmux', args);
|
||||||
Process.run('tmux', args);
|
|
||||||
|
|
||||||
const _socket = ['-L', 'clide'];
|
const _socket = ['-L', 'clide'];
|
||||||
|
|
||||||
@@ -30,11 +29,7 @@ Future<void> killSession(String name) async {
|
|||||||
Future<List<String>> listClideSessions() async {
|
Future<List<String>> listClideSessions() async {
|
||||||
final r = await tmuxRunner([..._socket, 'list-sessions', '-F', '#{session_name}']);
|
final r = await tmuxRunner([..._socket, 'list-sessions', '-F', '#{session_name}']);
|
||||||
if (r.exitCode != 0) return const [];
|
if (r.exitCode != 0) return const [];
|
||||||
return (r.stdout as String)
|
return (r.stdout as String).split('\n').map((s) => s.trim()).where((s) => s.isNotEmpty).toList();
|
||||||
.split('\n')
|
|
||||||
.map((s) => s.trim())
|
|
||||||
.where((s) => s.isNotEmpty)
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Kill every secondary clide-claude session whose name begins with
|
/// Kill every secondary clide-claude session whose name begins with
|
||||||
|
|||||||
@@ -123,8 +123,9 @@ class _DecisionsViewState extends State<DecisionsView> {
|
|||||||
final tokens = ClideTheme.of(context).surface;
|
final tokens = ClideTheme.of(context).surface;
|
||||||
if (_loading) return const Center(child: ClideText('Loading decisions...', muted: true));
|
if (_loading) return const Center(child: ClideText('Loading decisions...', muted: true));
|
||||||
if (_error != null) return Padding(padding: const EdgeInsets.all(12), child: ClideText(_error!, muted: true));
|
if (_error != null) return Padding(padding: const EdgeInsets.all(12), child: ClideText(_error!, muted: true));
|
||||||
if (_decisions.isEmpty)
|
if (_decisions.isEmpty) {
|
||||||
return const Padding(padding: EdgeInsets.all(12), child: ClideText('No decisions found.\nRun `pql decisions sync` to index.', muted: true));
|
return const Padding(padding: EdgeInsets.all(12), child: ClideText('No decisions found.\nRun `pql decisions sync` to index.', muted: true));
|
||||||
|
}
|
||||||
|
|
||||||
final lf = _filter.toLowerCase();
|
final lf = _filter.toLowerCase();
|
||||||
final hasFilter = lf.isNotEmpty;
|
final hasFilter = lf.isNotEmpty;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import 'package:clide/builtin/decisions/src/decision_detail_view.dart';
|
|||||||
import 'package:clide/builtin/decisions/src/decisions_view.dart';
|
import 'package:clide/builtin/decisions/src/decisions_view.dart';
|
||||||
import 'package:clide/extension/extension.dart';
|
import 'package:clide/extension/extension.dart';
|
||||||
import 'package:clide/kernel/kernel.dart';
|
import 'package:clide/kernel/kernel.dart';
|
||||||
import 'package:clide/kernel/src/events/message_bus.dart';
|
|
||||||
import 'package:clide/widgets/widgets.dart';
|
import 'package:clide/widgets/widgets.dart';
|
||||||
|
|
||||||
class DecisionsExtension extends ClideExtension {
|
class DecisionsExtension extends ClideExtension {
|
||||||
|
|||||||
@@ -17,13 +17,11 @@ import 'package:clide/kernel/kernel.dart';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
class EditorController extends ChangeNotifier {
|
class EditorController extends ChangeNotifier {
|
||||||
EditorController({required this.ipc, required DaemonBus events}) : _events = events {
|
EditorController({required this.ipc, required DaemonBus events}) {
|
||||||
_eventSub = events.on<DaemonEvent>().listen(_onEvent);
|
_eventSub = events.on<DaemonEvent>().listen(_onEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
final DaemonClient ipc;
|
final DaemonClient ipc;
|
||||||
// ignore: unused_field — kept for future subscription changes
|
|
||||||
final DaemonBus _events;
|
|
||||||
|
|
||||||
StreamSubscription<DaemonEvent>? _eventSub;
|
StreamSubscription<DaemonEvent>? _eventSub;
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:clide/clide.dart' show FileEntry;
|
||||||
import 'package:clide/kernel/kernel.dart';
|
import 'package:clide/kernel/kernel.dart';
|
||||||
import 'package:clide/widgets/widgets.dart';
|
import 'package:clide/widgets/widgets.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'package:clide/src/files/listing.dart' show FileEntry;
|
|
||||||
|
|
||||||
import 'file_tree_controller.dart';
|
import 'file_tree_controller.dart';
|
||||||
|
|
||||||
/// Sidebar panel rendering the workspace file tree.
|
/// Sidebar panel rendering the workspace file tree.
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ class ProblemsController extends ChangeNotifier {
|
|||||||
if (broken > 0) {
|
if (broken > 0) {
|
||||||
found.add(Problem(
|
found.add(Problem(
|
||||||
source: 'decisions',
|
source: 'decisions',
|
||||||
message: '$broken broken cross-reference(s) in decisions/',
|
message: '$broken broken cross-reference(s) in governance/',
|
||||||
hint: 'Run: pql decisions validate',
|
hint: 'Run: pql decisions validate',
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class _TerminalPaneState extends State<TerminalPane> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final ipc = _kernelIpc();
|
final ipc = _kernelIpc();
|
||||||
if (ipc == null || !ipc.isConnected) {
|
if (ipc == null || !ipc.isConnected) {
|
||||||
setState(() => _error = 'Daemon not connected. Start `clide --daemon`.');
|
setState(() => _error = 'Backend not connected.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import 'package:clide/builtin/tickets/src/ticket_detail_view.dart';
|
|||||||
import 'package:clide/builtin/tickets/src/tickets_view.dart';
|
import 'package:clide/builtin/tickets/src/tickets_view.dart';
|
||||||
import 'package:clide/extension/extension.dart';
|
import 'package:clide/extension/extension.dart';
|
||||||
import 'package:clide/kernel/kernel.dart';
|
import 'package:clide/kernel/kernel.dart';
|
||||||
import 'package:clide/kernel/src/events/message_bus.dart';
|
|
||||||
import 'package:clide/widgets/widgets.dart';
|
import 'package:clide/widgets/widgets.dart';
|
||||||
|
|
||||||
class TicketsExtension extends ClideExtension {
|
class TicketsExtension extends ClideExtension {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:clide/kernel/kernel.dart';
|
import 'package:clide/kernel/kernel.dart';
|
||||||
import 'package:clide/kernel/src/events/message_bus.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
class TicketDetail {
|
class TicketDetail {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:clide/builtin/tickets/src/ticket_colors.dart';
|
import 'package:clide/builtin/tickets/src/ticket_colors.dart';
|
||||||
import 'package:clide/kernel/kernel.dart';
|
import 'package:clide/kernel/kernel.dart';
|
||||||
import 'package:clide/kernel/src/events/message_bus.dart';
|
|
||||||
import 'package:clide/widgets/widgets.dart';
|
import 'package:clide/widgets/widgets.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import 'package:clide/clide.dart';
|
||||||
|
import 'package:clide/extension/extension.dart';
|
||||||
|
import 'package:clide/kernel/kernel.dart';
|
||||||
|
|
||||||
|
/// Surfaces view-level commands (currently the three text-zoom verbs)
|
||||||
|
/// in the command palette so they're discoverable. The keybindings
|
||||||
|
/// themselves are owned by the default keymap; commands here exist so
|
||||||
|
/// users browsing `Ctrl+Shift+P` see the same actions.
|
||||||
|
class ViewExtension extends ClideExtension {
|
||||||
|
ViewExtension({required this.textZoom});
|
||||||
|
|
||||||
|
final TextZoom textZoom;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get id => 'builtin.view';
|
||||||
|
@override
|
||||||
|
String get title => 'View';
|
||||||
|
@override
|
||||||
|
String get version => '0.1.0';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> activate(ClideExtensionContext ctx) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<ContributionPoint> get contributions => [
|
||||||
|
// Keybindings live in `assets/keymaps/default.yaml` against the
|
||||||
|
// text.scale* intents — registering a `defaultBinding` here too
|
||||||
|
// would shadow them. Palette discovery is the only goal.
|
||||||
|
CommandContribution(
|
||||||
|
id: 'view.zoomIn',
|
||||||
|
command: 'view.zoomIn',
|
||||||
|
title: 'View: Zoom In',
|
||||||
|
run: (_) async {
|
||||||
|
textZoom.increase();
|
||||||
|
return IpcResponse.ok(id: '', data: {'scale': textZoom.scale});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
CommandContribution(
|
||||||
|
id: 'view.zoomOut',
|
||||||
|
command: 'view.zoomOut',
|
||||||
|
title: 'View: Zoom Out',
|
||||||
|
run: (_) async {
|
||||||
|
textZoom.decrease();
|
||||||
|
return IpcResponse.ok(id: '', data: {'scale': textZoom.scale});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
CommandContribution(
|
||||||
|
id: 'view.zoomReset',
|
||||||
|
command: 'view.zoomReset',
|
||||||
|
title: 'View: Reset Zoom',
|
||||||
|
run: (_) async {
|
||||||
|
textZoom.reset();
|
||||||
|
return IpcResponse.ok(id: '', data: {'scale': textZoom.scale});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export 'src/extension.dart';
|
||||||
+7
-12
@@ -1,20 +1,16 @@
|
|||||||
/// clide — Dart core library.
|
/// clide — Dart core library.
|
||||||
///
|
///
|
||||||
/// Shared by `bin/clide.dart` (CLI + daemon) and by the Flutter app
|
|
||||||
/// under `app/` (which depends on this package via `path: ../`).
|
|
||||||
///
|
|
||||||
/// See:
|
/// See:
|
||||||
/// * `decisions/architecture.md` `D-005` — layout + language rationale.
|
/// * `governance/decisions/architecture.md` `D-005` — layout + language rationale.
|
||||||
/// * `decisions/architecture.md` `D-006` — CLI + event contract.
|
/// * `governance/decisions/architecture.md` `D-006` — CLI + event contract.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
// Flutter-app-visible surface. Deliberately **does not** export the
|
// Flutter-app-visible surface. Deliberately **does not** export the
|
||||||
// `pty/` or `panes/registry.dart` modules — those import `dart:ffi`
|
// `pty/` or `panes/registry.dart` modules — those import `dart:ffi`
|
||||||
// and pull in the PTY machinery that only runs on desktop. The
|
// and pull in the PTY machinery that only runs on desktop.
|
||||||
// daemon entrypoint (`bin/clide.dart`) imports them via deep paths.
|
|
||||||
//
|
//
|
||||||
// `Pane` + `PaneKind` + the event-sink interfaces travel here because
|
// `Pane` + `PaneKind` + the event-sink interfaces travel here because
|
||||||
// they're pure data types that both the app and the daemon reference.
|
// they're pure data types referenced throughout the app.
|
||||||
|
|
||||||
export 'src/daemon/dispatcher.dart';
|
export 'src/daemon/dispatcher.dart';
|
||||||
export 'src/editor/buffer.dart';
|
export 'src/editor/buffer.dart';
|
||||||
@@ -28,15 +24,14 @@ export 'src/pql/client.dart' show PqlClient, PqlException;
|
|||||||
export 'src/ipc/envelope.dart';
|
export 'src/ipc/envelope.dart';
|
||||||
export 'src/ipc/paths.dart';
|
export 'src/ipc/paths.dart';
|
||||||
export 'src/ipc/schema_v1.dart';
|
export 'src/ipc/schema_v1.dart';
|
||||||
export 'src/ipc/server.dart';
|
|
||||||
export 'src/panes/event_sink.dart';
|
export 'src/panes/event_sink.dart';
|
||||||
export 'src/panes/pane.dart' show Pane, PaneKind;
|
export 'src/panes/pane.dart' show Pane, PaneKind;
|
||||||
|
|
||||||
/// Build-time-stamped version string.
|
/// Build-time-stamped version string.
|
||||||
///
|
///
|
||||||
/// The Makefile's `build` target passes `--define=clideVersion=…` when
|
/// The Makefile's `build` target passes `--define=clideVersion=…`,
|
||||||
/// invoking `dart compile exe`, stamping `project.yaml`'s `version:`
|
/// stamping `pubspec.yaml`'s `version:` plus the git short SHA and
|
||||||
/// plus the git short SHA and dirty marker.
|
/// dirty marker.
|
||||||
const clideVersion = String.fromEnvironment(
|
const clideVersion = String.fromEnvironment(
|
||||||
'clideVersion',
|
'clideVersion',
|
||||||
defaultValue: '2.0.0-dev',
|
defaultValue: '2.0.0-dev',
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ export 'src/clipboard.dart';
|
|||||||
export 'src/commands/keybindings.dart';
|
export 'src/commands/keybindings.dart';
|
||||||
export 'src/commands/palette.dart';
|
export 'src/commands/palette.dart';
|
||||||
export 'src/commands/registry.dart';
|
export 'src/commands/registry.dart';
|
||||||
|
export 'src/keymap/intents.dart';
|
||||||
|
export 'src/keymap/key_chord.dart';
|
||||||
|
export 'src/keymap/keymap.dart';
|
||||||
|
export 'src/keymap/keymap_service.dart';
|
||||||
|
export 'src/keymap/when_clause.dart';
|
||||||
export 'src/dialog.dart';
|
export 'src/dialog.dart';
|
||||||
export 'src/extensions_manager.dart';
|
export 'src/extensions_manager.dart';
|
||||||
export 'src/files.dart';
|
export 'src/files.dart';
|
||||||
@@ -35,6 +40,7 @@ export 'src/os.dart';
|
|||||||
export 'src/panels/arrangement.dart';
|
export 'src/panels/arrangement.dart';
|
||||||
export 'src/project.dart';
|
export 'src/project.dart';
|
||||||
export 'src/scheduler.dart';
|
export 'src/scheduler.dart';
|
||||||
|
export 'src/text_zoom.dart';
|
||||||
export 'src/secrets.dart';
|
export 'src/secrets.dart';
|
||||||
export 'src/tray.dart';
|
export 'src/tray.dart';
|
||||||
export 'src/panels/drag_resize.dart';
|
export 'src/panels/drag_resize.dart';
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ class Backend {
|
|||||||
git: tcData['git'] as String?,
|
git: tcData['git'] as String?,
|
||||||
pql: tcData['pql'] as String?,
|
pql: tcData['pql'] as String?,
|
||||||
tmux: tcData['tmux'] as String?,
|
tmux: tcData['tmux'] as String?,
|
||||||
ptyc: tcData['ptyc'] as String?,
|
|
||||||
shell: tcData['shell'] as String?,
|
shell: tcData['shell'] as String?,
|
||||||
gitEnv: (tcData['gitEnv'] as Map?)?.cast<String, String>(),
|
gitEnv: (tcData['gitEnv'] as Map?)?.cast<String, String>(),
|
||||||
));
|
));
|
||||||
@@ -86,7 +85,6 @@ class Backend {
|
|||||||
git: tcData['git'] as String?,
|
git: tcData['git'] as String?,
|
||||||
pql: tcData['pql'] as String?,
|
pql: tcData['pql'] as String?,
|
||||||
tmux: tcData['tmux'] as String?,
|
tmux: tcData['tmux'] as String?,
|
||||||
ptyc: tcData['ptyc'] as String?,
|
|
||||||
shell: tcData['shell'] as String?,
|
shell: tcData['shell'] as String?,
|
||||||
gitEnv: (tcData['gitEnv'] as Map?)?.cast<String, String>(),
|
gitEnv: (tcData['gitEnv'] as Map?)?.cast<String, String>(),
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -45,11 +45,10 @@ void backendEntry(BackendBootMessage boot) {
|
|||||||
late Toolchain toolchain;
|
late Toolchain toolchain;
|
||||||
|
|
||||||
// Phase 1: resolve toolchain — just find binaries, don't init services.
|
// Phase 1: resolve toolchain — just find binaries, don't init services.
|
||||||
// We need a project root for ptyc/dugite paths. Use a sensible
|
// Dugite is resolved against the install dir; per T-98 the project
|
||||||
// default; the real project comes from project.open.
|
// root is never inspected during toolchain resolution.
|
||||||
final resolveRoot = boot.hintRoot ?? Platform.environment['HOME'] ?? '/tmp';
|
|
||||||
toolchain = Toolchain();
|
toolchain = Toolchain();
|
||||||
toolchain.applyResolved(resolveToolchainPaths(resolveRoot));
|
toolchain.applyResolved(resolveToolchainPaths());
|
||||||
|
|
||||||
// Listen for messages from the frontend.
|
// Listen for messages from the frontend.
|
||||||
requestPort.listen((message) async {
|
requestPort.listen((message) async {
|
||||||
@@ -77,10 +76,10 @@ void backendEntry(BackendBootMessage boot) {
|
|||||||
final projectPath = message['path'] as String;
|
final projectPath = message['path'] as String;
|
||||||
final workDir = Directory(projectPath);
|
final workDir = Directory(projectPath);
|
||||||
|
|
||||||
// Re-resolve toolchain with the actual project root (finds
|
// Re-resolve toolchain. Project path is not inspected (T-98);
|
||||||
// dugite in native/dugite/, ptyc in ptyc/bin/, etc.)
|
// dugite still comes from the install dir + env override.
|
||||||
toolchain = Toolchain();
|
toolchain = Toolchain();
|
||||||
toolchain.applyResolved(resolveToolchainPaths(projectPath));
|
toolchain.applyResolved(resolveToolchainPaths());
|
||||||
|
|
||||||
// Clear existing handlers and re-register with new project.
|
// Clear existing handlers and re-register with new project.
|
||||||
dispatcher.clear();
|
dispatcher.clear();
|
||||||
@@ -138,7 +137,6 @@ Map<String, Object?> _serializeToolchain(Toolchain tc) => {
|
|||||||
'git': tc.git,
|
'git': tc.git,
|
||||||
'pql': tc.pql,
|
'pql': tc.pql,
|
||||||
'tmux': tc.tmux,
|
'tmux': tc.tmux,
|
||||||
'ptyc': tc.ptyc,
|
|
||||||
'shell': tc.shell,
|
'shell': tc.shell,
|
||||||
'gitEnv': tc.gitEnv,
|
'gitEnv': tc.gitEnv,
|
||||||
'missing': tc.missing,
|
'missing': tc.missing,
|
||||||
|
|||||||
@@ -9,13 +9,26 @@ class PaletteController extends ChangeNotifier {
|
|||||||
|
|
||||||
bool _open = false;
|
bool _open = false;
|
||||||
String _filter = '';
|
String _filter = '';
|
||||||
|
int _selectedIndex = 0;
|
||||||
|
|
||||||
bool get isOpen => _open;
|
bool get isOpen => _open;
|
||||||
String get filter => _filter;
|
String get filter => _filter;
|
||||||
|
|
||||||
|
/// Index of the highlighted entry inside the currently-filtered
|
||||||
|
/// list. Clamped to `[0, filtered().length - 1]` on read. Returns 0
|
||||||
|
/// when the filter excludes everything.
|
||||||
|
int get selectedIndex {
|
||||||
|
final n = filtered().length;
|
||||||
|
if (n == 0) return 0;
|
||||||
|
if (_selectedIndex < 0) return 0;
|
||||||
|
if (_selectedIndex >= n) return n - 1;
|
||||||
|
return _selectedIndex;
|
||||||
|
}
|
||||||
|
|
||||||
void open() {
|
void open() {
|
||||||
if (_open) return;
|
if (_open) return;
|
||||||
_open = true;
|
_open = true;
|
||||||
|
_selectedIndex = 0;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,6 +36,7 @@ class PaletteController extends ChangeNotifier {
|
|||||||
if (!_open) return;
|
if (!_open) return;
|
||||||
_open = false;
|
_open = false;
|
||||||
_filter = '';
|
_filter = '';
|
||||||
|
_selectedIndex = 0;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,9 +45,35 @@ class PaletteController extends ChangeNotifier {
|
|||||||
void setFilter(String f) {
|
void setFilter(String f) {
|
||||||
if (_filter == f) return;
|
if (_filter == f) return;
|
||||||
_filter = f;
|
_filter = f;
|
||||||
|
_selectedIndex = 0;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Highlight the next entry, wrapping at the end. No-op when the
|
||||||
|
/// filtered list has fewer than 2 entries.
|
||||||
|
void selectNext() {
|
||||||
|
final n = filtered().length;
|
||||||
|
if (n < 2) return;
|
||||||
|
_selectedIndex = (selectedIndex + 1) % n;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Highlight the previous entry, wrapping at the start.
|
||||||
|
void selectPrevious() {
|
||||||
|
final n = filtered().length;
|
||||||
|
if (n < 2) return;
|
||||||
|
_selectedIndex = (selectedIndex - 1 + n) % n;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invoke whatever's currently highlighted; no-op when the filter
|
||||||
|
/// excludes everything.
|
||||||
|
Future<void> acceptSelected() async {
|
||||||
|
final list = filtered();
|
||||||
|
if (list.isEmpty) return;
|
||||||
|
await invoke(list[selectedIndex].command);
|
||||||
|
}
|
||||||
|
|
||||||
List<CommandContribution> filtered() {
|
List<CommandContribution> filtered() {
|
||||||
if (_filter.isEmpty) return _registry.all.toList();
|
if (_filter.isEmpty) return _registry.all.toList();
|
||||||
final q = _filter.toLowerCase();
|
final q = _filter.toLowerCase();
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import 'package:clide/kernel/src/files.dart';
|
|||||||
import 'package:clide/kernel/src/focus.dart';
|
import 'package:clide/kernel/src/focus.dart';
|
||||||
import 'package:clide/kernel/src/i18n/i18n.dart';
|
import 'package:clide/kernel/src/i18n/i18n.dart';
|
||||||
import 'package:clide/kernel/src/ipc/client.dart';
|
import 'package:clide/kernel/src/ipc/client.dart';
|
||||||
|
import 'package:clide/kernel/src/keymap/keymap_service.dart';
|
||||||
import 'package:clide/kernel/src/log.dart';
|
import 'package:clide/kernel/src/log.dart';
|
||||||
import 'package:clide/kernel/src/net.dart';
|
import 'package:clide/kernel/src/net.dart';
|
||||||
import 'package:clide/kernel/src/notify.dart';
|
import 'package:clide/kernel/src/notify.dart';
|
||||||
@@ -40,6 +41,7 @@ class ExtensionManager extends ChangeNotifier {
|
|||||||
required this.commands,
|
required this.commands,
|
||||||
required this.palette,
|
required this.palette,
|
||||||
required this.keybindings,
|
required this.keybindings,
|
||||||
|
required this.keymap,
|
||||||
required this.clipboard,
|
required this.clipboard,
|
||||||
required this.files,
|
required this.files,
|
||||||
required this.notify,
|
required this.notify,
|
||||||
@@ -64,6 +66,7 @@ class ExtensionManager extends ChangeNotifier {
|
|||||||
final CommandRegistry commands;
|
final CommandRegistry commands;
|
||||||
final PaletteController palette;
|
final PaletteController palette;
|
||||||
final KeybindingResolver keybindings;
|
final KeybindingResolver keybindings;
|
||||||
|
final KeymapService keymap;
|
||||||
final ClideClipboard clipboard;
|
final ClideClipboard clipboard;
|
||||||
final FileServices files;
|
final FileServices files;
|
||||||
final Notifications notify;
|
final Notifications notify;
|
||||||
@@ -78,6 +81,9 @@ class ExtensionManager extends ChangeNotifier {
|
|||||||
|
|
||||||
final Map<String, ClideExtension> _known = {};
|
final Map<String, ClideExtension> _known = {};
|
||||||
final Set<String> _activated = {};
|
final Set<String> _activated = {};
|
||||||
|
// Extensions whose activate() / deactivate() threw — exposed so the
|
||||||
|
// UI can show a "degraded" status next to them.
|
||||||
|
final Map<String, Object> _failed = {};
|
||||||
|
|
||||||
void register(ClideExtension ext) {
|
void register(ClideExtension ext) {
|
||||||
if (_known.containsKey(ext.id)) {
|
if (_known.containsKey(ext.id)) {
|
||||||
@@ -91,6 +97,12 @@ class ExtensionManager extends ChangeNotifier {
|
|||||||
Iterable<ClideExtension> get all => _known.values;
|
Iterable<ClideExtension> get all => _known.values;
|
||||||
bool isActivated(String id) => _activated.contains(id);
|
bool isActivated(String id) => _activated.contains(id);
|
||||||
|
|
||||||
|
/// Map of extension id → most recent activate/deactivate error.
|
||||||
|
/// Cleared for an extension when it activates cleanly. UI surfaces
|
||||||
|
/// read this for the "degraded" indicator.
|
||||||
|
Map<String, Object> get failedExtensions => Map.unmodifiable(_failed);
|
||||||
|
bool didFail(String id) => _failed.containsKey(id);
|
||||||
|
|
||||||
bool isEnabled(String id) {
|
bool isEnabled(String id) {
|
||||||
final v = settings.get<bool>('app.extensions.$id.enabled');
|
final v = settings.get<bool>('app.extensions.$id.enabled');
|
||||||
return v ?? true;
|
return v ?? true;
|
||||||
@@ -135,11 +147,14 @@ class ExtensionManager extends ChangeNotifier {
|
|||||||
_applyContribution(c);
|
_applyContribution(c);
|
||||||
}
|
}
|
||||||
_activated.add(id);
|
_activated.add(id);
|
||||||
|
_failed.remove(id);
|
||||||
events.emit(ExtensionActivated(id: id));
|
events.emit(ExtensionActivated(id: id));
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
log.info('extensions', 'activated $id');
|
log.info('extensions', 'activated $id');
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
|
_failed[id] = e;
|
||||||
log.error('extensions', 'activate failed for $id', error: e, stackTrace: st);
|
log.error('extensions', 'activate failed for $id', error: e, stackTrace: st);
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +172,9 @@ class ExtensionManager extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
log.info('extensions', 'deactivated $id');
|
log.info('extensions', 'deactivated $id');
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
|
_failed[id] = e;
|
||||||
log.error('extensions', 'deactivate failed for $id', error: e, stackTrace: st);
|
log.error('extensions', 'deactivate failed for $id', error: e, stackTrace: st);
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +188,11 @@ class ExtensionManager extends ChangeNotifier {
|
|||||||
commands.register(cmd);
|
commands.register(cmd);
|
||||||
final binding = cmd.defaultBinding;
|
final binding = cmd.defaultBinding;
|
||||||
if (binding != null) {
|
if (binding != null) {
|
||||||
|
// Legacy KeybindingResolver still wired for back-compat
|
||||||
|
// until all callers migrate; the keymap layer is the
|
||||||
|
// canonical home for chord → command bindings (T-117).
|
||||||
keybindings.bind(Keybinding.parse(binding), cmd.command);
|
keybindings.bind(Keybinding.parse(binding), cmd.command);
|
||||||
|
keymap.registerCommandBinding(binding, cmd.command);
|
||||||
}
|
}
|
||||||
case TrayItemContribution t:
|
case TrayItemContribution t:
|
||||||
tray.add(t);
|
tray.add(t);
|
||||||
@@ -194,6 +215,7 @@ class ExtensionManager extends ChangeNotifier {
|
|||||||
if (binding != null) {
|
if (binding != null) {
|
||||||
keybindings.unbind(Keybinding.parse(binding));
|
keybindings.unbind(Keybinding.parse(binding));
|
||||||
}
|
}
|
||||||
|
keymap.unregisterCommandBindings(cmd.command);
|
||||||
case TrayItemContribution t:
|
case TrayItemContribution t:
|
||||||
tray.remove(t.id);
|
tray.remove(t.id);
|
||||||
case LayoutPresetContribution _:
|
case LayoutPresetContribution _:
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import 'dart:io';
|
|||||||
import 'package:clide/clide.dart';
|
import 'package:clide/clide.dart';
|
||||||
import 'package:clide/kernel/src/clipboard.dart';
|
import 'package:clide/kernel/src/clipboard.dart';
|
||||||
import 'package:clide/kernel/src/commands/keybindings.dart';
|
import 'package:clide/kernel/src/commands/keybindings.dart';
|
||||||
|
import 'package:clide/kernel/src/keymap/keymap_service.dart';
|
||||||
|
import 'package:clide/kernel/src/text_zoom.dart';
|
||||||
import 'package:clide/kernel/src/commands/palette.dart';
|
import 'package:clide/kernel/src/commands/palette.dart';
|
||||||
import 'package:clide/kernel/src/commands/registry.dart';
|
import 'package:clide/kernel/src/commands/registry.dart';
|
||||||
import 'package:clide/kernel/src/dialog.dart';
|
import 'package:clide/kernel/src/dialog.dart';
|
||||||
@@ -63,6 +65,8 @@ class KernelServices {
|
|||||||
required this.window,
|
required this.window,
|
||||||
required this.toolchain,
|
required this.toolchain,
|
||||||
required this.scheduler,
|
required this.scheduler,
|
||||||
|
required this.keymap,
|
||||||
|
required this.textZoom,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Logger log;
|
final Logger log;
|
||||||
@@ -91,6 +95,8 @@ class KernelServices {
|
|||||||
final WindowControls window;
|
final WindowControls window;
|
||||||
final Toolchain toolchain;
|
final Toolchain toolchain;
|
||||||
final SchedulerService scheduler;
|
final SchedulerService scheduler;
|
||||||
|
final KeymapService keymap;
|
||||||
|
final TextZoom textZoom;
|
||||||
|
|
||||||
static Future<KernelServices> boot({
|
static Future<KernelServices> boot({
|
||||||
required Directory appDir,
|
required Directory appDir,
|
||||||
@@ -132,6 +138,8 @@ class KernelServices {
|
|||||||
final arrangement = LayoutArrangement();
|
final arrangement = LayoutArrangement();
|
||||||
final commands = CommandRegistry();
|
final commands = CommandRegistry();
|
||||||
final keybindings = KeybindingResolver();
|
final keybindings = KeybindingResolver();
|
||||||
|
final keymap = KeymapService(settings: settings, appDir: appDir);
|
||||||
|
await keymap.load();
|
||||||
final palette = PaletteController(commands);
|
final palette = PaletteController(commands);
|
||||||
final clipboard = ClideClipboard();
|
final clipboard = ClideClipboard();
|
||||||
final files = FileServices(events);
|
final files = FileServices(events);
|
||||||
@@ -146,6 +154,7 @@ class KernelServices {
|
|||||||
final tc = toolchain ?? Toolchain();
|
final tc = toolchain ?? Toolchain();
|
||||||
final scheduler = SchedulerService(events);
|
final scheduler = SchedulerService(events);
|
||||||
scheduler.start();
|
scheduler.start();
|
||||||
|
final textZoom = TextZoom();
|
||||||
final project = ProjectManager(
|
final project = ProjectManager(
|
||||||
log: log,
|
log: log,
|
||||||
events: events,
|
events: events,
|
||||||
@@ -174,6 +183,7 @@ class KernelServices {
|
|||||||
commands: commands,
|
commands: commands,
|
||||||
palette: palette,
|
palette: palette,
|
||||||
keybindings: keybindings,
|
keybindings: keybindings,
|
||||||
|
keymap: keymap,
|
||||||
clipboard: clipboard,
|
clipboard: clipboard,
|
||||||
files: files,
|
files: files,
|
||||||
notify: notify,
|
notify: notify,
|
||||||
@@ -218,6 +228,8 @@ class KernelServices {
|
|||||||
window: window,
|
window: window,
|
||||||
toolchain: tc,
|
toolchain: tc,
|
||||||
scheduler: scheduler,
|
scheduler: scheduler,
|
||||||
|
keymap: keymap,
|
||||||
|
textZoom: textZoom,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +250,9 @@ class KernelServices {
|
|||||||
focus.dispose();
|
focus.dispose();
|
||||||
project.dispose();
|
project.dispose();
|
||||||
extensions.dispose();
|
extensions.dispose();
|
||||||
scheduler.dispose();
|
await scheduler.dispose();
|
||||||
|
keymap.dispose();
|
||||||
|
textZoom.dispose();
|
||||||
await log.dispose();
|
await log.dispose();
|
||||||
messages.dispose();
|
messages.dispose();
|
||||||
await events.dispose();
|
await events.dispose();
|
||||||
|
|||||||
@@ -1,13 +1,27 @@
|
|||||||
import 'package:clide/kernel/src/panels/slot_id.dart';
|
import 'package:clide/kernel/src/panels/slot_id.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
/// Tracks the currently focused contribution (tab id + slot). Backs
|
/// Tracks the currently focused contribution (tab id + slot) and
|
||||||
/// `clide active`; extensions that need "which tab does the user care
|
/// hosts a registry of per-slot [FocusScopeNode]s for panel-to-panel
|
||||||
/// about right now?" read from here instead of poking Flutter's
|
/// focus traversal.
|
||||||
/// FocusScope directly.
|
///
|
||||||
|
/// Backs `clide active` (extensions read activeSlot / activeContributionId)
|
||||||
|
/// and the `FocusNextPanelIntent` / `FocusPreviousPanelIntent` actions
|
||||||
|
/// the keymap dispatches on F6 / Shift+F6.
|
||||||
class FocusTracker extends ChangeNotifier {
|
class FocusTracker extends ChangeNotifier {
|
||||||
SlotId? _slot;
|
SlotId? _slot;
|
||||||
String? _contributionId;
|
String? _contributionId;
|
||||||
|
final Map<SlotId, FocusScopeNode> _scopes = {};
|
||||||
|
|
||||||
|
/// Static order panels cycle through. Matches the visual left-to-right
|
||||||
|
/// of the three-column layout (sidebar → workspace → context); the
|
||||||
|
/// statusbar / toolbar aren't included because they don't host
|
||||||
|
/// keyboard-active content.
|
||||||
|
static const List<SlotId> traversalOrder = [
|
||||||
|
Slots.sidebar,
|
||||||
|
Slots.workspace,
|
||||||
|
Slots.contextPanel,
|
||||||
|
];
|
||||||
|
|
||||||
SlotId? get activeSlot => _slot;
|
SlotId? get activeSlot => _slot;
|
||||||
String? get activeContributionId => _contributionId;
|
String? get activeContributionId => _contributionId;
|
||||||
@@ -25,4 +39,44 @@ class FocusTracker extends ChangeNotifier {
|
|||||||
_contributionId = null;
|
_contributionId = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// SlotHost calls this in initState. Replaces any prior registration
|
||||||
|
/// for the same slot (handles hot-reload + slot rebuild).
|
||||||
|
void registerSlotScope(SlotId slot, FocusScopeNode scope) {
|
||||||
|
_scopes[slot] = scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SlotHost calls this in dispose. No-op if a newer scope already
|
||||||
|
/// took the slot.
|
||||||
|
void unregisterSlotScope(SlotId slot, FocusScopeNode scope) {
|
||||||
|
if (identical(_scopes[slot], scope)) {
|
||||||
|
_scopes.remove(slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request focus on [slot]'s registered scope. No-op when the slot
|
||||||
|
/// has no registered scope (panel not mounted, layout doesn't
|
||||||
|
/// include it, etc.).
|
||||||
|
void focusSlot(SlotId slot) {
|
||||||
|
final scope = _scopes[slot];
|
||||||
|
if (scope == null) return;
|
||||||
|
scope.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move focus to the next slot in [traversalOrder], wrapping at the
|
||||||
|
/// end. No-op when fewer than two slots are registered.
|
||||||
|
void focusNextSlot() => _cycleSlot(1);
|
||||||
|
|
||||||
|
/// Move focus to the previous slot, wrapping at the start.
|
||||||
|
void focusPreviousSlot() => _cycleSlot(-1);
|
||||||
|
|
||||||
|
void _cycleSlot(int delta) {
|
||||||
|
final registered = traversalOrder.where(_scopes.containsKey).toList();
|
||||||
|
if (registered.length < 2) return;
|
||||||
|
final from = registered.indexOf(_slot ?? registered.first);
|
||||||
|
final start = from < 0 ? 0 : from;
|
||||||
|
final next = (start + delta) % registered.length;
|
||||||
|
final wrapped = next < 0 ? next + registered.length : next;
|
||||||
|
focusSlot(registered[wrapped]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"connected": { "translation": "connected" },
|
"connected": { "translation": "connected" },
|
||||||
"connected.hint": { "translation": "clide daemon is reachable over the local socket" },
|
"connected.hint": { "translation": "backend isolate is reachable" },
|
||||||
"disconnected": { "translation": "disconnected" },
|
"disconnected": { "translation": "disconnected" },
|
||||||
"disconnected.hint": { "translation": "clide daemon is not running — start it with `clide --daemon`" }
|
"disconnected.hint": { "translation": "backend isolate is not running" }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,5 @@
|
|||||||
"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": "Daemon not connected. Start `clide --daemon`." }
|
"error.daemon": { "translation": "Backend not connected." }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ class DaemonClient extends ChangeNotifier {
|
|||||||
code: IpcExitCode.toolError,
|
code: IpcExitCode.toolError,
|
||||||
kind: IpcErrorKind.toolError,
|
kind: IpcErrorKind.toolError,
|
||||||
message: 'daemon not connected',
|
message: 'daemon not connected',
|
||||||
hint: 'is `clide --daemon` running?',
|
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -150,6 +149,10 @@ class DaemonClient extends ChangeNotifier {
|
|||||||
void _setConnected(bool v) {
|
void _setConnected(bool v) {
|
||||||
if (_connected == v) return;
|
if (_connected == v) return;
|
||||||
_connected = v;
|
_connected = v;
|
||||||
|
// Skip side-effects (event emit + notifyListeners) after dispose —
|
||||||
|
// the socket stream's onDone can fire post-dispose and would
|
||||||
|
// otherwise hit ChangeNotifier's "used after disposed" assert.
|
||||||
|
if (_disposed) return;
|
||||||
_events.emit(DaemonConnectionChanged(connected: v));
|
_events.emit(DaemonConnectionChanged(connected: v));
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
import 'package:clide/clide.dart';
|
import 'package:clide/clide.dart';
|
||||||
import 'package:clide/kernel/src/ipc/client.dart';
|
import 'package:clide/kernel/src/ipc/client.dart';
|
||||||
import 'package:clide/kernel/src/events/bus.dart';
|
|
||||||
import 'package:clide/kernel/src/log.dart';
|
|
||||||
import 'package:clide/src/daemon/dispatcher.dart';
|
|
||||||
|
|
||||||
class InProcessClient extends DaemonClient {
|
class InProcessClient extends DaemonClient {
|
||||||
InProcessClient({
|
InProcessClient({
|
||||||
required Logger log,
|
required super.log,
|
||||||
required DaemonBus events,
|
required super.events,
|
||||||
required DaemonDispatcher dispatcher,
|
required this.dispatcher,
|
||||||
}) : _dispatcher = dispatcher,
|
}) : super(socketPath: '');
|
||||||
super(socketPath: '', log: log, events: events);
|
|
||||||
|
|
||||||
DaemonDispatcher _dispatcher;
|
DaemonDispatcher dispatcher;
|
||||||
|
|
||||||
DaemonDispatcher get dispatcher => _dispatcher;
|
|
||||||
set dispatcher(DaemonDispatcher d) => _dispatcher = d;
|
|
||||||
int _nextReqId = 0;
|
int _nextReqId = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -31,6 +24,6 @@ class InProcessClient extends DaemonClient {
|
|||||||
Future<IpcResponse> request(String cmd, {Map<String, Object?> args = const {}}) {
|
Future<IpcResponse> request(String cmd, {Map<String, Object?> args = const {}}) {
|
||||||
final id = '${_nextReqId++}';
|
final id = '${_nextReqId++}';
|
||||||
final req = IpcRequest(id: id, cmd: cmd, args: args);
|
final req = IpcRequest(id: id, cmd: cmd, args: args);
|
||||||
return _dispatcher.dispatch(req);
|
return dispatcher.dispatch(req);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,16 +13,15 @@ import 'package:clide/clide.dart';
|
|||||||
import 'package:clide/kernel/src/events/bus.dart';
|
import 'package:clide/kernel/src/events/bus.dart';
|
||||||
import 'package:clide/kernel/src/events/types.dart';
|
import 'package:clide/kernel/src/events/types.dart';
|
||||||
import 'package:clide/kernel/src/ipc/client.dart';
|
import 'package:clide/kernel/src/ipc/client.dart';
|
||||||
import 'package:clide/kernel/src/log.dart';
|
|
||||||
|
|
||||||
class IsolateClient extends DaemonClient {
|
class IsolateClient extends DaemonClient {
|
||||||
IsolateClient({
|
IsolateClient({
|
||||||
required Logger log,
|
required super.log,
|
||||||
required DaemonBus events,
|
required super.events,
|
||||||
required SendPort backendPort,
|
required SendPort backendPort,
|
||||||
}) : _backendPort = backendPort,
|
}) : _backendPort = backendPort,
|
||||||
_events = events,
|
_events = events,
|
||||||
super(socketPath: '', log: log, events: events);
|
super(socketPath: '');
|
||||||
|
|
||||||
final SendPort _backendPort;
|
final SendPort _backendPort;
|
||||||
final DaemonBus _events;
|
final DaemonBus _events;
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/// Typed [Intent]s the keymap dispatches.
|
||||||
|
///
|
||||||
|
/// Widgets bind Actions to Intent types via `Actions.handler`. Preset
|
||||||
|
/// YAML files reference Intents by their string id (`activate`,
|
||||||
|
/// `palette.selectNext`, …). The id stays stable across SDK reshapes;
|
||||||
|
/// the Dart class name can move without invalidating user keymaps.
|
||||||
|
///
|
||||||
|
/// Two flavors of intents live here:
|
||||||
|
/// 1. **Flutter-provided** — `ActivateIntent` and `DismissIntent`
|
||||||
|
/// are first-class Flutter intents; we reuse them so the keymap
|
||||||
|
/// integrates with anything else in the Flutter ecosystem that
|
||||||
|
/// already dispatches those (focus traversal, modal scrims, …).
|
||||||
|
/// They're mapped by id in [builtinIntents] but not declared
|
||||||
|
/// here.
|
||||||
|
/// 2. **Clide-specific** — palette navigation, text scale, the
|
||||||
|
/// `InvokeCommandIntent` bridge. Each subclass extends [Intent]
|
||||||
|
/// directly.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
// -- Panel-to-panel focus traversal -----------------------------------------
|
||||||
|
|
||||||
|
/// Move focus to the next panel in `FocusTracker.traversalOrder`
|
||||||
|
/// (sidebar → workspace → context). Bound to F6 by default.
|
||||||
|
class FocusNextPanelIntent extends Intent {
|
||||||
|
const FocusNextPanelIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move focus to the previous panel. Bound to Shift+F6 by default.
|
||||||
|
class FocusPreviousPanelIntent extends Intent {
|
||||||
|
const FocusPreviousPanelIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Command palette --------------------------------------------------------
|
||||||
|
|
||||||
|
/// Open the command palette.
|
||||||
|
class PaletteOpenIntent extends Intent {
|
||||||
|
const PaletteOpenIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Highlight the next palette result.
|
||||||
|
class PaletteSelectNextIntent extends Intent {
|
||||||
|
const PaletteSelectNextIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Highlight the previous palette result.
|
||||||
|
class PaletteSelectPreviousIntent extends Intent {
|
||||||
|
const PaletteSelectPreviousIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invoke the highlighted palette result.
|
||||||
|
class PaletteAcceptIntent extends Intent {
|
||||||
|
const PaletteAcceptIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Text scale -------------------------------------------------------------
|
||||||
|
|
||||||
|
class TextScaleIncreaseIntent extends Intent {
|
||||||
|
const TextScaleIncreaseIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class TextScaleDecreaseIntent extends Intent {
|
||||||
|
const TextScaleDecreaseIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class TextScaleResetIntent extends Intent {
|
||||||
|
const TextScaleResetIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Command bridge ---------------------------------------------------------
|
||||||
|
|
||||||
|
/// Generic "invoke this CommandRegistry command id" intent. Used for
|
||||||
|
/// bindings that target a contributed command rather than a typed
|
||||||
|
/// intent. The keymap creates one per binding; the Actions handler
|
||||||
|
/// dispatches to the [CommandRegistry].
|
||||||
|
class InvokeCommandIntent extends Intent {
|
||||||
|
const InvokeCommandIntent(this.commandId);
|
||||||
|
final String commandId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Lookup -----------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Map from YAML id → factory. Preset files reference intents by id;
|
||||||
|
/// the keymap loader uses this to instantiate them. Intents with a
|
||||||
|
/// configurable payload (only `InvokeCommandIntent` today) are not in
|
||||||
|
/// the map — the loader recognises the `command:` prefix and
|
||||||
|
/// instantiates them inline.
|
||||||
|
final Map<String, Intent Function()> builtinIntents = {
|
||||||
|
'activate': () => const ActivateIntent(),
|
||||||
|
'dismiss': () => const DismissIntent(),
|
||||||
|
'focus.nextPanel': () => const FocusNextPanelIntent(),
|
||||||
|
'focus.previousPanel': () => const FocusPreviousPanelIntent(),
|
||||||
|
'palette.open': () => const PaletteOpenIntent(),
|
||||||
|
'palette.selectNext': () => const PaletteSelectNextIntent(),
|
||||||
|
'palette.selectPrevious': () => const PaletteSelectPreviousIntent(),
|
||||||
|
'palette.accept': () => const PaletteAcceptIntent(),
|
||||||
|
'text.scaleIncrease': () => const TextScaleIncreaseIntent(),
|
||||||
|
'text.scaleDecrease': () => const TextScaleDecreaseIntent(),
|
||||||
|
'text.scaleReset': () => const TextScaleResetIntent(),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Parse an intent id into an [Intent]. Returns null if the id is
|
||||||
|
/// unknown. Recognises:
|
||||||
|
/// - any builtin intent by its stable id
|
||||||
|
/// - `command:<command-id>` → [InvokeCommandIntent]
|
||||||
|
Intent? parseIntentId(String id) {
|
||||||
|
final builtin = builtinIntents[id];
|
||||||
|
if (builtin != null) return builtin();
|
||||||
|
if (id.startsWith('command:')) {
|
||||||
|
return InvokeCommandIntent(id.substring('command:'.length));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
/// Layout-independent representation of a single keystroke.
|
||||||
|
///
|
||||||
|
/// The consultant's note (T-110) flagged the old `KeybindingResolver`
|
||||||
|
/// for keying off `LogicalKeyboardKey.keyLabel`, which is locale-aware
|
||||||
|
/// (US-QWERTY `Ctrl+/` differs from AZERTY `Ctrl+:`). We key off
|
||||||
|
/// `LogicalKeyboardKey.keyId` instead — a stable u32 that survives
|
||||||
|
/// layout changes.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
/// One of the four POSIX-style modifier keys. Order is the canonical
|
||||||
|
/// presentation order in YAML and toString output.
|
||||||
|
enum KeyModifier {
|
||||||
|
ctrl,
|
||||||
|
alt,
|
||||||
|
shift,
|
||||||
|
meta;
|
||||||
|
|
||||||
|
/// Lowercase short form used in YAML (`ctrl`, `alt`, `shift`, `meta`).
|
||||||
|
String get yaml => name;
|
||||||
|
|
||||||
|
/// Display string used in palette + tooltip hints.
|
||||||
|
String get display => switch (this) {
|
||||||
|
KeyModifier.ctrl => 'Ctrl',
|
||||||
|
KeyModifier.alt => 'Alt',
|
||||||
|
KeyModifier.shift => 'Shift',
|
||||||
|
KeyModifier.meta => 'Cmd',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A modifier-set + a single key, identified by layout-independent
|
||||||
|
/// `LogicalKeyboardKey.keyId`. Canonicalised on construction
|
||||||
|
/// (modifiers sorted by enum order) so equality + hashing work for
|
||||||
|
/// lookup-keying.
|
||||||
|
@immutable
|
||||||
|
class KeyChord {
|
||||||
|
factory KeyChord({Set<KeyModifier> modifiers = const {}, required LogicalKeyboardKey key}) {
|
||||||
|
final sorted = modifiers.toList()..sort((a, b) => a.index.compareTo(b.index));
|
||||||
|
return KeyChord._(List.unmodifiable(sorted), key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const KeyChord._(this.modifiers, this.key);
|
||||||
|
|
||||||
|
final List<KeyModifier> modifiers;
|
||||||
|
final LogicalKeyboardKey key;
|
||||||
|
|
||||||
|
/// Build from a Flutter [KeyEvent]. Returns null for non-down events
|
||||||
|
/// or events whose logical key has no meaningful id (e.g. a bare
|
||||||
|
/// modifier press in isolation).
|
||||||
|
static KeyChord? fromKeyEvent(KeyEvent event, HardwareKeyboard kb) {
|
||||||
|
if (event is! KeyDownEvent && event is! KeyRepeatEvent) return null;
|
||||||
|
final logical = event.logicalKey;
|
||||||
|
// Bare modifier presses don't form a chord on their own.
|
||||||
|
if (_isBareModifier(logical)) return null;
|
||||||
|
final mods = <KeyModifier>{
|
||||||
|
if (kb.isControlPressed) KeyModifier.ctrl,
|
||||||
|
if (kb.isAltPressed) KeyModifier.alt,
|
||||||
|
if (kb.isShiftPressed) KeyModifier.shift,
|
||||||
|
if (kb.isMetaPressed) KeyModifier.meta,
|
||||||
|
};
|
||||||
|
return KeyChord(modifiers: mods, key: logical);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a YAML chord spec like `ctrl+shift+p`, `cmd+enter`, `escape`.
|
||||||
|
/// Whitespace tolerated. Throws [FormatException] on unknown tokens
|
||||||
|
/// or empty input.
|
||||||
|
static KeyChord parse(String spec) {
|
||||||
|
final trimmed = spec.trim();
|
||||||
|
if (trimmed.isEmpty) throw const FormatException('empty key chord');
|
||||||
|
final parts = trimmed.split('+').map((s) => s.trim()).toList();
|
||||||
|
final keyName = parts.removeLast();
|
||||||
|
if (keyName.isEmpty) throw FormatException('missing key in chord: "$spec"');
|
||||||
|
final mods = <KeyModifier>{};
|
||||||
|
for (final m in parts) {
|
||||||
|
final mod = _modByName(m);
|
||||||
|
if (mod == null) throw FormatException('unknown modifier "$m" in chord: "$spec"');
|
||||||
|
mods.add(mod);
|
||||||
|
}
|
||||||
|
final key = _keyByName(keyName);
|
||||||
|
if (key == null) throw FormatException('unknown key "$keyName" in chord: "$spec"');
|
||||||
|
return KeyChord(modifiers: mods, key: key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Canonical YAML form: `ctrl+shift+p`.
|
||||||
|
String get canonical {
|
||||||
|
final modPart = modifiers.map((m) => m.yaml).join('+');
|
||||||
|
final keyPart = _keyName(key);
|
||||||
|
return modPart.isEmpty ? keyPart : '$modPart+$keyPart';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Display form for UI hints: `Ctrl+Shift+P`.
|
||||||
|
String get display {
|
||||||
|
final modPart = modifiers.map((m) => m.display).join('+');
|
||||||
|
final keyPart = _keyName(key).toUpperCase();
|
||||||
|
return modPart.isEmpty ? keyPart : '$modPart+$keyPart';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => other is KeyChord && other.key == key && listEquals(other.modifiers, modifiers);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(key, Object.hashAll(modifiers));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'KeyChord($canonical)';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isBareModifier(LogicalKeyboardKey k) =>
|
||||||
|
k == LogicalKeyboardKey.control ||
|
||||||
|
k == LogicalKeyboardKey.controlLeft ||
|
||||||
|
k == LogicalKeyboardKey.controlRight ||
|
||||||
|
k == LogicalKeyboardKey.alt ||
|
||||||
|
k == LogicalKeyboardKey.altLeft ||
|
||||||
|
k == LogicalKeyboardKey.altRight ||
|
||||||
|
k == LogicalKeyboardKey.shift ||
|
||||||
|
k == LogicalKeyboardKey.shiftLeft ||
|
||||||
|
k == LogicalKeyboardKey.shiftRight ||
|
||||||
|
k == LogicalKeyboardKey.meta ||
|
||||||
|
k == LogicalKeyboardKey.metaLeft ||
|
||||||
|
k == LogicalKeyboardKey.metaRight ||
|
||||||
|
k == LogicalKeyboardKey.fn;
|
||||||
|
|
||||||
|
KeyModifier? _modByName(String name) {
|
||||||
|
switch (name.toLowerCase()) {
|
||||||
|
case 'ctrl':
|
||||||
|
case 'control':
|
||||||
|
return KeyModifier.ctrl;
|
||||||
|
case 'alt':
|
||||||
|
case 'option':
|
||||||
|
return KeyModifier.alt;
|
||||||
|
case 'shift':
|
||||||
|
return KeyModifier.shift;
|
||||||
|
case 'meta':
|
||||||
|
case 'cmd':
|
||||||
|
case 'command':
|
||||||
|
case 'super':
|
||||||
|
case 'win':
|
||||||
|
return KeyModifier.meta;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Key name <-> LogicalKeyboardKey ----------------------------------------
|
||||||
|
//
|
||||||
|
// We map YAML names to LogicalKeyboardKey instances. The map covers
|
||||||
|
// every key a binding can plausibly want; unknown names throw on parse.
|
||||||
|
|
||||||
|
const Map<String, LogicalKeyboardKey> _byName = {
|
||||||
|
// Letters
|
||||||
|
'a': LogicalKeyboardKey.keyA, 'b': LogicalKeyboardKey.keyB, 'c': LogicalKeyboardKey.keyC,
|
||||||
|
'd': LogicalKeyboardKey.keyD, 'e': LogicalKeyboardKey.keyE, 'f': LogicalKeyboardKey.keyF,
|
||||||
|
'g': LogicalKeyboardKey.keyG, 'h': LogicalKeyboardKey.keyH, 'i': LogicalKeyboardKey.keyI,
|
||||||
|
'j': LogicalKeyboardKey.keyJ, 'k': LogicalKeyboardKey.keyK, 'l': LogicalKeyboardKey.keyL,
|
||||||
|
'm': LogicalKeyboardKey.keyM, 'n': LogicalKeyboardKey.keyN, 'o': LogicalKeyboardKey.keyO,
|
||||||
|
'p': LogicalKeyboardKey.keyP, 'q': LogicalKeyboardKey.keyQ, 'r': LogicalKeyboardKey.keyR,
|
||||||
|
's': LogicalKeyboardKey.keyS, 't': LogicalKeyboardKey.keyT, 'u': LogicalKeyboardKey.keyU,
|
||||||
|
'v': LogicalKeyboardKey.keyV, 'w': LogicalKeyboardKey.keyW, 'x': LogicalKeyboardKey.keyX,
|
||||||
|
'y': LogicalKeyboardKey.keyY, 'z': LogicalKeyboardKey.keyZ,
|
||||||
|
// Digits
|
||||||
|
'0': LogicalKeyboardKey.digit0, '1': LogicalKeyboardKey.digit1, '2': LogicalKeyboardKey.digit2,
|
||||||
|
'3': LogicalKeyboardKey.digit3, '4': LogicalKeyboardKey.digit4, '5': LogicalKeyboardKey.digit5,
|
||||||
|
'6': LogicalKeyboardKey.digit6, '7': LogicalKeyboardKey.digit7, '8': LogicalKeyboardKey.digit8,
|
||||||
|
'9': LogicalKeyboardKey.digit9,
|
||||||
|
// Function keys
|
||||||
|
'f1': LogicalKeyboardKey.f1, 'f2': LogicalKeyboardKey.f2, 'f3': LogicalKeyboardKey.f3,
|
||||||
|
'f4': LogicalKeyboardKey.f4, 'f5': LogicalKeyboardKey.f5, 'f6': LogicalKeyboardKey.f6,
|
||||||
|
'f7': LogicalKeyboardKey.f7, 'f8': LogicalKeyboardKey.f8, 'f9': LogicalKeyboardKey.f9,
|
||||||
|
'f10': LogicalKeyboardKey.f10, 'f11': LogicalKeyboardKey.f11, 'f12': LogicalKeyboardKey.f12,
|
||||||
|
// Arrows
|
||||||
|
'left': LogicalKeyboardKey.arrowLeft,
|
||||||
|
'right': LogicalKeyboardKey.arrowRight,
|
||||||
|
'up': LogicalKeyboardKey.arrowUp,
|
||||||
|
'down': LogicalKeyboardKey.arrowDown,
|
||||||
|
// Common control keys
|
||||||
|
'enter': LogicalKeyboardKey.enter,
|
||||||
|
'return': LogicalKeyboardKey.enter,
|
||||||
|
'escape': LogicalKeyboardKey.escape,
|
||||||
|
'esc': LogicalKeyboardKey.escape,
|
||||||
|
'tab': LogicalKeyboardKey.tab,
|
||||||
|
'space': LogicalKeyboardKey.space,
|
||||||
|
'backspace': LogicalKeyboardKey.backspace,
|
||||||
|
'delete': LogicalKeyboardKey.delete,
|
||||||
|
'home': LogicalKeyboardKey.home,
|
||||||
|
'end': LogicalKeyboardKey.end,
|
||||||
|
'pageup': LogicalKeyboardKey.pageUp,
|
||||||
|
'pagedown': LogicalKeyboardKey.pageDown,
|
||||||
|
'insert': LogicalKeyboardKey.insert,
|
||||||
|
// Punctuation (US-QWERTY positions; preset authors can rely on these names).
|
||||||
|
'minus': LogicalKeyboardKey.minus, '-': LogicalKeyboardKey.minus,
|
||||||
|
'equal': LogicalKeyboardKey.equal, '=': LogicalKeyboardKey.equal,
|
||||||
|
'comma': LogicalKeyboardKey.comma, ',': LogicalKeyboardKey.comma,
|
||||||
|
'period': LogicalKeyboardKey.period, '.': LogicalKeyboardKey.period,
|
||||||
|
'slash': LogicalKeyboardKey.slash, '/': LogicalKeyboardKey.slash,
|
||||||
|
'backslash': LogicalKeyboardKey.backslash, r'\\': LogicalKeyboardKey.backslash,
|
||||||
|
'semicolon': LogicalKeyboardKey.semicolon, ';': LogicalKeyboardKey.semicolon,
|
||||||
|
'quote': LogicalKeyboardKey.quote, "'": LogicalKeyboardKey.quote,
|
||||||
|
'bracketLeft': LogicalKeyboardKey.bracketLeft, '[': LogicalKeyboardKey.bracketLeft,
|
||||||
|
'bracketRight': LogicalKeyboardKey.bracketRight, ']': LogicalKeyboardKey.bracketRight,
|
||||||
|
'backquote': LogicalKeyboardKey.backquote, '`': LogicalKeyboardKey.backquote,
|
||||||
|
};
|
||||||
|
|
||||||
|
LogicalKeyboardKey? _keyByName(String name) => _byName[name.toLowerCase()];
|
||||||
|
|
||||||
|
String _keyName(LogicalKeyboardKey key) {
|
||||||
|
// Reverse lookup; prefer the canonical (first) name for each key.
|
||||||
|
for (final entry in _byName.entries) {
|
||||||
|
if (entry.value == key) return entry.key;
|
||||||
|
}
|
||||||
|
// Fallback: use the debugName-like representation.
|
||||||
|
return key.keyLabel.isNotEmpty ? key.keyLabel.toLowerCase() : 'key(0x${key.keyId.toRadixString(16)})';
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
/// In-memory representation of a layered keymap.
|
||||||
|
///
|
||||||
|
/// A [Keymap] is built from one or more [KeymapLayer]s (preset →
|
||||||
|
/// user-file overlay → settings overlay). Each layer contributes
|
||||||
|
/// [KeymapBinding]s; later layers replace earlier bindings with the
|
||||||
|
/// same (chord, when-clause) tuple.
|
||||||
|
///
|
||||||
|
/// At resolve time, the [Keymap] walks the layered list once per
|
||||||
|
/// (chord, scope) and returns the [Intent] bound by the highest-
|
||||||
|
/// precedence matching layer.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/widgets.dart' show Intent;
|
||||||
|
import 'package:yaml/yaml.dart';
|
||||||
|
|
||||||
|
import 'intents.dart';
|
||||||
|
import 'key_chord.dart';
|
||||||
|
import 'when_clause.dart';
|
||||||
|
|
||||||
|
/// One row in a layer: a chord, an optional when-clause, and the
|
||||||
|
/// intent to fire when the chord matches and the when-clause is true.
|
||||||
|
@immutable
|
||||||
|
class KeymapBinding {
|
||||||
|
const KeymapBinding({
|
||||||
|
required this.chord,
|
||||||
|
required this.intent,
|
||||||
|
this.when,
|
||||||
|
});
|
||||||
|
|
||||||
|
final KeyChord chord;
|
||||||
|
final Intent intent;
|
||||||
|
final WhenExpr? when;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'Binding($chord → ${intent.runtimeType}${when == null ? '' : ' when $when'})';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One source of bindings. Layers are merged in order — later layers
|
||||||
|
/// take precedence on (chord, when) collisions.
|
||||||
|
@immutable
|
||||||
|
class KeymapLayer {
|
||||||
|
const KeymapLayer({required this.name, required this.bindings});
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final List<KeymapBinding> bindings;
|
||||||
|
|
||||||
|
/// Parse a YAML document into a layer. Expected shape:
|
||||||
|
///
|
||||||
|
/// ```yaml
|
||||||
|
/// name: default
|
||||||
|
/// bindings:
|
||||||
|
/// - intent: activate
|
||||||
|
/// keys: [enter, space]
|
||||||
|
/// when: focused
|
||||||
|
/// - intent: palette.selectNext
|
||||||
|
/// keys: [down]
|
||||||
|
/// when: palette.open
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// `keys:` may be a single chord string or a list. `when:` is
|
||||||
|
/// optional. Unknown intent ids cause a [FormatException].
|
||||||
|
factory KeymapLayer.fromYaml(String source, {String? nameOverride}) {
|
||||||
|
final doc = loadYaml(source);
|
||||||
|
if (doc is! YamlMap) {
|
||||||
|
throw const FormatException('keymap YAML must be a map at top level');
|
||||||
|
}
|
||||||
|
final name = nameOverride ?? (doc['name'] as String? ?? 'unnamed');
|
||||||
|
final raw = doc['bindings'];
|
||||||
|
if (raw is! YamlList) {
|
||||||
|
throw const FormatException('keymap YAML must define `bindings:` as a list');
|
||||||
|
}
|
||||||
|
final out = <KeymapBinding>[];
|
||||||
|
for (final entry in raw) {
|
||||||
|
if (entry is! YamlMap) {
|
||||||
|
throw FormatException('binding entries must be maps; got $entry');
|
||||||
|
}
|
||||||
|
final intentId = entry['intent'] as String?;
|
||||||
|
if (intentId == null) {
|
||||||
|
throw FormatException('binding missing `intent:` — $entry');
|
||||||
|
}
|
||||||
|
final intent = parseIntentId(intentId);
|
||||||
|
if (intent == null) {
|
||||||
|
throw FormatException('unknown intent id "$intentId" — $entry');
|
||||||
|
}
|
||||||
|
final keysRaw = entry['keys'];
|
||||||
|
final keySpecs = <String>[];
|
||||||
|
if (keysRaw is String) {
|
||||||
|
keySpecs.add(keysRaw);
|
||||||
|
} else if (keysRaw is YamlList) {
|
||||||
|
for (final k in keysRaw) {
|
||||||
|
if (k is! String) throw FormatException('keys must be strings; got $k in $entry');
|
||||||
|
keySpecs.add(k);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw FormatException('binding missing `keys:` (string or list of strings) — $entry');
|
||||||
|
}
|
||||||
|
final when = WhenExpr.tryParse(entry['when'] as String?);
|
||||||
|
for (final spec in keySpecs) {
|
||||||
|
out.add(KeymapBinding(chord: KeyChord.parse(spec), intent: intent, when: when));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return KeymapLayer(name: name, bindings: out);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'KeymapLayer($name, ${bindings.length} binding${bindings.length == 1 ? '' : 's'})';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A flattened keymap, ready for resolution.
|
||||||
|
@immutable
|
||||||
|
class Keymap {
|
||||||
|
Keymap(this.layers) : _effective = _flatten(layers);
|
||||||
|
|
||||||
|
final List<KeymapLayer> layers;
|
||||||
|
final List<KeymapBinding> _effective;
|
||||||
|
|
||||||
|
/// Resolve a [chord] against the current [context]. Returns the
|
||||||
|
/// highest-precedence binding whose chord matches and whose when-
|
||||||
|
/// clause (if any) evaluates true. Returns null if no match.
|
||||||
|
Intent? resolve(KeyChord chord, Map<String, bool> context) {
|
||||||
|
// Effective list is highest-precedence-first; first match wins.
|
||||||
|
for (final b in _effective) {
|
||||||
|
if (b.chord != chord) continue;
|
||||||
|
if (b.when != null && !b.when!.evaluate(context)) continue;
|
||||||
|
return b.intent;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All resolved bindings in effective-precedence order. Exposed for
|
||||||
|
/// debug surfaces (keybindings UI, palette hints).
|
||||||
|
List<KeymapBinding> get effectiveBindings => List.unmodifiable(_effective);
|
||||||
|
|
||||||
|
/// Concatenate layers in REVERSE order (last layer first). Later
|
||||||
|
/// layers fully shadow earlier (chord, when) collisions: when we walk
|
||||||
|
/// the list, the first matching entry wins, so highest-precedence
|
||||||
|
/// must come first. We don't dedupe — a no-op match in a later layer
|
||||||
|
/// just earns the first slot.
|
||||||
|
static List<KeymapBinding> _flatten(List<KeymapLayer> layers) {
|
||||||
|
return [
|
||||||
|
for (final l in layers.reversed) ...l.bindings,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'Keymap(${layers.map((l) => l.name).join(' < ')})';
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
/// Kernel service that owns the active [Keymap], scope context, and
|
||||||
|
/// resolution surface.
|
||||||
|
///
|
||||||
|
/// Layering (lowest precedence → highest):
|
||||||
|
/// 1. The active preset (asset under `assets/keymaps/<preset>.yaml`).
|
||||||
|
/// Selected by the `app.keymap.preset` setting; defaults to
|
||||||
|
/// `default`.
|
||||||
|
/// 2. A user keymap file at `<appDir>/keybindings.yaml` (per-user
|
||||||
|
/// power-user overrides).
|
||||||
|
/// 3. A settings-stored JSON overlay at `app.keymap.overrides` —
|
||||||
|
/// list of `{intent, keys, when?}` maps in the same shape as
|
||||||
|
/// preset YAML.
|
||||||
|
///
|
||||||
|
/// Scope context is a `Map<String, bool>` keyed by named flags (e.g.
|
||||||
|
/// `palette.open`, `editor.focused`). Producing services call
|
||||||
|
/// [setScopeFlag] when their state changes; consumers reference the
|
||||||
|
/// flag name in when-clauses.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart' show AssetBundle, KeyEvent, HardwareKeyboard, rootBundle;
|
||||||
|
import 'package:flutter/widgets.dart' show Intent;
|
||||||
|
|
||||||
|
import '../settings.dart';
|
||||||
|
import 'intents.dart';
|
||||||
|
import 'key_chord.dart';
|
||||||
|
import 'keymap.dart';
|
||||||
|
import 'when_clause.dart';
|
||||||
|
|
||||||
|
/// Setting key for the active preset name.
|
||||||
|
const String kKeymapPresetSetting = 'app.keymap.preset';
|
||||||
|
|
||||||
|
/// Setting key for the JSON overlay list.
|
||||||
|
const String kKeymapOverridesSetting = 'app.keymap.overrides';
|
||||||
|
|
||||||
|
/// Filename for the user keymap file under the app dir.
|
||||||
|
const String kKeymapUserFile = 'keybindings.yaml';
|
||||||
|
|
||||||
|
class KeymapService extends ChangeNotifier {
|
||||||
|
KeymapService({
|
||||||
|
required SettingsStore settings,
|
||||||
|
required Directory appDir,
|
||||||
|
AssetBundle? bundle,
|
||||||
|
}) : _settings = settings,
|
||||||
|
_appDir = appDir,
|
||||||
|
_bundle = bundle ?? rootBundle;
|
||||||
|
|
||||||
|
final SettingsStore _settings;
|
||||||
|
final Directory _appDir;
|
||||||
|
final AssetBundle _bundle;
|
||||||
|
|
||||||
|
Keymap? _active;
|
||||||
|
final Map<String, bool> _scope = {};
|
||||||
|
|
||||||
|
// The four layer slots, lowest to highest precedence. Held
|
||||||
|
// separately so [registerCommandBinding] can refresh the
|
||||||
|
// contributions layer without re-reading the preset / file /
|
||||||
|
// settings.
|
||||||
|
KeymapLayer? _preset;
|
||||||
|
final List<KeymapBinding> _contributions = [];
|
||||||
|
KeymapLayer? _userFile;
|
||||||
|
KeymapLayer? _settingsOverlay;
|
||||||
|
|
||||||
|
/// The currently effective layered keymap. Null before [load] runs.
|
||||||
|
Keymap? get keymap => _active;
|
||||||
|
|
||||||
|
/// Live read-only view of the scope context.
|
||||||
|
Map<String, bool> get scope => Map.unmodifiable(_scope);
|
||||||
|
|
||||||
|
/// Read the preset from settings (default `default`), load all
|
||||||
|
/// non-contribution layers, and rebuild the active keymap. Safe to
|
||||||
|
/// call repeatedly. Contributions registered via
|
||||||
|
/// [registerCommandBinding] are preserved across reloads.
|
||||||
|
Future<void> load() async {
|
||||||
|
final presetName = _settings.get<String>(kKeymapPresetSetting) ?? 'default';
|
||||||
|
|
||||||
|
// Preset (asset).
|
||||||
|
try {
|
||||||
|
final src = await _bundle.loadString('assets/keymaps/$presetName.yaml');
|
||||||
|
_preset = KeymapLayer.fromYaml(src, nameOverride: presetName);
|
||||||
|
} catch (_) {
|
||||||
|
// A missing preset means we ship without a default. Tests can
|
||||||
|
// inject a custom bundle. We don't surface this beyond an empty
|
||||||
|
// active map.
|
||||||
|
_preset = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// User file overlay.
|
||||||
|
final userFile = File('${_appDir.path}/$kKeymapUserFile');
|
||||||
|
if (await userFile.exists()) {
|
||||||
|
try {
|
||||||
|
_userFile = KeymapLayer.fromYaml(await userFile.readAsString(), nameOverride: 'user-file');
|
||||||
|
} on FormatException {
|
||||||
|
_userFile = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_userFile = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings overlay.
|
||||||
|
final overlay = _settings.get<List<Object?>>(kKeymapOverridesSetting);
|
||||||
|
if (overlay != null && overlay.isNotEmpty) {
|
||||||
|
final asYaml = StringBuffer('name: settings-overlay\nbindings:\n');
|
||||||
|
for (final entry in overlay) {
|
||||||
|
if (entry is! Map) continue;
|
||||||
|
asYaml.writeln(' - ${jsonEncode(entry)}');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
_settingsOverlay = KeymapLayer.fromYaml(asYaml.toString(), nameOverride: 'settings-overlay');
|
||||||
|
} on FormatException {
|
||||||
|
_settingsOverlay = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_settingsOverlay = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_rebuildActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register a programmatic chord → command-id binding (typically
|
||||||
|
/// from an extension's `defaultBinding`). Contributions form a
|
||||||
|
/// layer between preset and user-file: extensions establish their
|
||||||
|
/// defaults, the user can override either via the user file or
|
||||||
|
/// settings overlay.
|
||||||
|
void registerCommandBinding(String chordSpec, String commandId, {String? when}) {
|
||||||
|
_contributions.add(KeymapBinding(
|
||||||
|
chord: KeyChord.parse(chordSpec),
|
||||||
|
intent: InvokeCommandIntent(commandId),
|
||||||
|
when: WhenExpr.tryParse(when),
|
||||||
|
));
|
||||||
|
_rebuildActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove all extension-contributed bindings for [commandId]. Used
|
||||||
|
/// when an extension is disabled or unregistered.
|
||||||
|
void unregisterCommandBindings(String commandId) {
|
||||||
|
final before = _contributions.length;
|
||||||
|
_contributions.removeWhere((b) {
|
||||||
|
final i = b.intent;
|
||||||
|
return i is InvokeCommandIntent && i.commandId == commandId;
|
||||||
|
});
|
||||||
|
if (_contributions.length != before) {
|
||||||
|
_rebuildActive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _rebuildActive() {
|
||||||
|
final layers = <KeymapLayer>[
|
||||||
|
if (_preset != null) _preset!,
|
||||||
|
KeymapLayer(name: 'contributions', bindings: List.unmodifiable(_contributions)),
|
||||||
|
if (_userFile != null) _userFile!,
|
||||||
|
if (_settingsOverlay != null) _settingsOverlay!,
|
||||||
|
];
|
||||||
|
_active = Keymap(layers);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a [KeyEvent] against the active keymap and current scope.
|
||||||
|
/// Returns null when nothing matches.
|
||||||
|
Intent? resolveEvent(KeyEvent event, HardwareKeyboard kb) {
|
||||||
|
final km = _active;
|
||||||
|
if (km == null) return null;
|
||||||
|
final chord = KeyChord.fromKeyEvent(event, kb);
|
||||||
|
if (chord == null) return null;
|
||||||
|
return km.resolve(chord, _scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a named scope flag. Producers should call this when their
|
||||||
|
/// state changes so when-clauses re-evaluate correctly. Notifies
|
||||||
|
/// listeners when the value actually changes.
|
||||||
|
void setScopeFlag(String name, bool value) {
|
||||||
|
if (_scope[name] == value) return;
|
||||||
|
_scope[name] = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear a named scope flag.
|
||||||
|
void clearScopeFlag(String name) {
|
||||||
|
if (!_scope.containsKey(name)) return;
|
||||||
|
_scope.remove(name);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Switch presets. Persists the new preset name to settings and
|
||||||
|
/// re-loads the layered keymap.
|
||||||
|
Future<void> setPreset(String name) async {
|
||||||
|
await _settings.set<String>(kKeymapPresetSetting, name);
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All effective bindings, highest-precedence first. Useful for
|
||||||
|
/// debug surfaces and keybinding hints in the UI.
|
||||||
|
List<KeymapBinding> get effectiveBindings => _active?.effectiveBindings ?? const [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
/// Boolean "when:" expressions over a named context bag, VS-Code style.
|
||||||
|
///
|
||||||
|
/// Grammar:
|
||||||
|
/// expr := or
|
||||||
|
/// or := and ('||' and)*
|
||||||
|
/// and := unary ('&&' unary)*
|
||||||
|
/// unary := '!' unary | atom
|
||||||
|
/// atom := IDENT | '(' expr ')'
|
||||||
|
/// IDENT := [a-zA-Z_][a-zA-Z0-9._-]*
|
||||||
|
///
|
||||||
|
/// Identifiers resolve against a `Map<String, bool>` context. A missing
|
||||||
|
/// identifier evaluates to `false` — bindings can assume any required
|
||||||
|
/// scope flag is published by the producing service.
|
||||||
|
///
|
||||||
|
/// The grammar is intentionally small: no equality, no arithmetic, no
|
||||||
|
/// string literals. If a binding needs more, the producing service
|
||||||
|
/// should publish a richer named flag (e.g. `editor.dirty`).
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
sealed class WhenExpr {
|
||||||
|
const WhenExpr();
|
||||||
|
|
||||||
|
/// Evaluate against [context]. Missing identifiers are `false`.
|
||||||
|
bool evaluate(Map<String, bool> context);
|
||||||
|
|
||||||
|
/// Parse [source]. Throws [FormatException] on syntax errors.
|
||||||
|
static WhenExpr parse(String source) => _Parser(source).parseAll();
|
||||||
|
|
||||||
|
/// Convenience: null on empty input, otherwise [parse].
|
||||||
|
static WhenExpr? tryParse(String? source) {
|
||||||
|
if (source == null || source.trim().isEmpty) return null;
|
||||||
|
return parse(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WhenIdent extends WhenExpr {
|
||||||
|
const WhenIdent(this.name);
|
||||||
|
final String name;
|
||||||
|
@override
|
||||||
|
bool evaluate(Map<String, bool> context) => context[name] ?? false;
|
||||||
|
@override
|
||||||
|
String toString() => name;
|
||||||
|
}
|
||||||
|
|
||||||
|
class WhenNot extends WhenExpr {
|
||||||
|
const WhenNot(this.child);
|
||||||
|
final WhenExpr child;
|
||||||
|
@override
|
||||||
|
bool evaluate(Map<String, bool> context) => !child.evaluate(context);
|
||||||
|
@override
|
||||||
|
String toString() => '!$child';
|
||||||
|
}
|
||||||
|
|
||||||
|
class WhenAnd extends WhenExpr {
|
||||||
|
const WhenAnd(this.left, this.right);
|
||||||
|
final WhenExpr left;
|
||||||
|
final WhenExpr right;
|
||||||
|
@override
|
||||||
|
bool evaluate(Map<String, bool> context) => left.evaluate(context) && right.evaluate(context);
|
||||||
|
@override
|
||||||
|
String toString() => '($left && $right)';
|
||||||
|
}
|
||||||
|
|
||||||
|
class WhenOr extends WhenExpr {
|
||||||
|
const WhenOr(this.left, this.right);
|
||||||
|
final WhenExpr left;
|
||||||
|
final WhenExpr right;
|
||||||
|
@override
|
||||||
|
bool evaluate(Map<String, bool> context) => left.evaluate(context) || right.evaluate(context);
|
||||||
|
@override
|
||||||
|
String toString() => '($left || $right)';
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Parser -----------------------------------------------------------------
|
||||||
|
|
||||||
|
class _Parser {
|
||||||
|
_Parser(this._src);
|
||||||
|
|
||||||
|
final String _src;
|
||||||
|
int _pos = 0;
|
||||||
|
|
||||||
|
WhenExpr parseAll() {
|
||||||
|
_skip();
|
||||||
|
final e = _or();
|
||||||
|
_skip();
|
||||||
|
if (_pos != _src.length) {
|
||||||
|
throw FormatException('unexpected "${_src[_pos]}" at column ${_pos + 1} in when-clause: "$_src"');
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
WhenExpr _or() {
|
||||||
|
var left = _and();
|
||||||
|
while (_consume('||')) {
|
||||||
|
final right = _and();
|
||||||
|
left = WhenOr(left, right);
|
||||||
|
}
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
|
||||||
|
WhenExpr _and() {
|
||||||
|
var left = _unary();
|
||||||
|
while (_consume('&&')) {
|
||||||
|
final right = _unary();
|
||||||
|
left = WhenAnd(left, right);
|
||||||
|
}
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
|
||||||
|
WhenExpr _unary() {
|
||||||
|
_skip();
|
||||||
|
if (_consume('!')) {
|
||||||
|
return WhenNot(_unary());
|
||||||
|
}
|
||||||
|
return _atom();
|
||||||
|
}
|
||||||
|
|
||||||
|
WhenExpr _atom() {
|
||||||
|
_skip();
|
||||||
|
if (_consume('(')) {
|
||||||
|
final inner = _or();
|
||||||
|
_skip();
|
||||||
|
if (!_consume(')')) {
|
||||||
|
throw FormatException('expected ")" at column ${_pos + 1} in when-clause: "$_src"');
|
||||||
|
}
|
||||||
|
return inner;
|
||||||
|
}
|
||||||
|
final ident = _ident();
|
||||||
|
if (ident == null) {
|
||||||
|
final at = _pos < _src.length ? '"${_src[_pos]}"' : 'end of input';
|
||||||
|
throw FormatException('expected identifier at column ${_pos + 1} in when-clause: "$_src" (got $at)');
|
||||||
|
}
|
||||||
|
return WhenIdent(ident);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _ident() {
|
||||||
|
_skip();
|
||||||
|
final start = _pos;
|
||||||
|
if (_pos >= _src.length) return null;
|
||||||
|
final first = _src.codeUnitAt(_pos);
|
||||||
|
if (!_isIdentStart(first)) return null;
|
||||||
|
_pos++;
|
||||||
|
while (_pos < _src.length && _isIdentCont(_src.codeUnitAt(_pos))) {
|
||||||
|
_pos++;
|
||||||
|
}
|
||||||
|
return _src.substring(start, _pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _consume(String token) {
|
||||||
|
_skip();
|
||||||
|
if (_src.startsWith(token, _pos)) {
|
||||||
|
_pos += token.length;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _skip() {
|
||||||
|
while (_pos < _src.length && _isSpace(_src.codeUnitAt(_pos))) {
|
||||||
|
_pos++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isSpace(int c) => c == 0x20 || c == 0x09 || c == 0x0A || c == 0x0D;
|
||||||
|
|
||||||
|
bool _isIdentStart(int c) {
|
||||||
|
// a-z | A-Z | _
|
||||||
|
return (c >= 0x61 && c <= 0x7A) || (c >= 0x41 && c <= 0x5A) || c == 0x5F;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isIdentCont(int c) {
|
||||||
|
// a-z | A-Z | 0-9 | _ . -
|
||||||
|
return _isIdentStart(c) || (c >= 0x30 && c <= 0x39) || c == 0x2E || c == 0x2D;
|
||||||
|
}
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
import 'package:clide/kernel/src/panels/arrangement.dart';
|
import 'package:clide/kernel/src/panels/arrangement.dart';
|
||||||
import 'package:clide/kernel/src/panels/slot_id.dart';
|
import 'package:clide/kernel/src/panels/slot_id.dart';
|
||||||
import 'package:clide/kernel/src/theme/controller.dart';
|
import 'package:clide/kernel/src/theme/controller.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
/// A 4-px draggable splitter that adjusts the size of [slot] in the
|
/// A 4-px splitter that adjusts the size of [slot] in [arrangement].
|
||||||
/// given [arrangement]. Slot hosts wrap this around their edges to make
|
/// Slot hosts wrap this around their edges to make the three-column
|
||||||
/// the three-column layout resizable.
|
/// layout resizable.
|
||||||
|
///
|
||||||
|
/// Drag with the mouse, or tab to it and use the arrow keys (Shift =
|
||||||
|
/// coarse step). Exposes a `slider` Semantics node so screen readers
|
||||||
|
/// announce the current width.
|
||||||
class DragResizeHandle extends StatefulWidget {
|
class DragResizeHandle extends StatefulWidget {
|
||||||
const DragResizeHandle({
|
const DragResizeHandle({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -21,6 +26,8 @@ class DragResizeHandle extends StatefulWidget {
|
|||||||
final double thickness;
|
final double thickness;
|
||||||
|
|
||||||
static const defaultThickness = 8.0;
|
static const defaultThickness = 8.0;
|
||||||
|
static const double stepFine = 10.0;
|
||||||
|
static const double stepCoarse = 50.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<DragResizeHandle> createState() => _DragResizeHandleState();
|
State<DragResizeHandle> createState() => _DragResizeHandleState();
|
||||||
@@ -28,32 +35,57 @@ class DragResizeHandle extends StatefulWidget {
|
|||||||
|
|
||||||
class _DragResizeHandleState extends State<DragResizeHandle> {
|
class _DragResizeHandleState extends State<DragResizeHandle> {
|
||||||
bool _hovered = false;
|
bool _hovered = false;
|
||||||
|
bool _focused = false;
|
||||||
double? _dragStartSize;
|
double? _dragStartSize;
|
||||||
Offset? _dragStartPointer;
|
Offset? _dragStartPointer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final tokens = ClideTheme.of(context).surface;
|
final tokens = ClideTheme.of(context).surface;
|
||||||
final lineColor = _hovered ? tokens.panelActiveBorder : tokens.dividerColor;
|
final lineColor = (_hovered || _focused) ? tokens.panelActiveBorder : tokens.dividerColor;
|
||||||
|
|
||||||
return MouseRegion(
|
final size = widget.arrangement.sizeOf(widget.slot);
|
||||||
cursor: widget.axis == Axis.horizontal ? SystemMouseCursors.resizeColumn : SystemMouseCursors.resizeRow,
|
|
||||||
onEnter: (_) => setState(() => _hovered = true),
|
return Semantics(
|
||||||
onExit: (_) => setState(() => _hovered = false),
|
container: true,
|
||||||
child: Listener(
|
slider: true,
|
||||||
onPointerDown: _onDown,
|
label: _semanticLabel(),
|
||||||
onPointerMove: _onMove,
|
value: size == null ? null : '${size.round()} pixels',
|
||||||
onPointerUp: _onUp,
|
increasedValue: size == null ? null : '${(size + DragResizeHandle.stepFine).round()} pixels',
|
||||||
child: Container(
|
decreasedValue: size == null ? null : '${(size - DragResizeHandle.stepFine).round()} pixels',
|
||||||
width: widget.axis == Axis.horizontal ? widget.thickness : null,
|
onIncrease: () => _bump(DragResizeHandle.stepFine),
|
||||||
height: widget.axis == Axis.vertical ? widget.thickness : null,
|
onDecrease: () => _bump(-DragResizeHandle.stepFine),
|
||||||
color: tokens.chromeBackground,
|
child: FocusableActionDetector(
|
||||||
child: Align(
|
onShowFocusHighlight: (v) => setState(() => _focused = v),
|
||||||
alignment: widget.slot == Slots.sidebar ? Alignment.centerRight : Alignment.centerLeft,
|
shortcuts: _shortcuts(),
|
||||||
|
actions: <Type, Action<Intent>>{
|
||||||
|
_BumpIntent: CallbackAction<_BumpIntent>(
|
||||||
|
onInvoke: (intent) {
|
||||||
|
_bump(intent.delta);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
child: MouseRegion(
|
||||||
|
cursor: widget.axis == Axis.horizontal ? SystemMouseCursors.resizeColumn : SystemMouseCursors.resizeRow,
|
||||||
|
onEnter: (_) => setState(() => _hovered = true),
|
||||||
|
onExit: (_) => setState(() => _hovered = false),
|
||||||
|
child: Listener(
|
||||||
|
onPointerDown: _onDown,
|
||||||
|
onPointerMove: _onMove,
|
||||||
|
onPointerUp: _onUp,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: widget.axis == Axis.horizontal ? 1 : null,
|
width: widget.axis == Axis.horizontal ? widget.thickness : null,
|
||||||
height: widget.axis == Axis.vertical ? 1 : null,
|
height: widget.axis == Axis.vertical ? widget.thickness : null,
|
||||||
color: lineColor,
|
color: tokens.chromeBackground,
|
||||||
|
child: Align(
|
||||||
|
alignment: widget.slot == Slots.sidebar ? Alignment.centerRight : Alignment.centerLeft,
|
||||||
|
child: Container(
|
||||||
|
width: widget.axis == Axis.horizontal ? (_focused ? 2 : 1) : null,
|
||||||
|
height: widget.axis == Axis.vertical ? (_focused ? 2 : 1) : null,
|
||||||
|
color: lineColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -61,6 +93,38 @@ class _DragResizeHandleState extends State<DragResizeHandle> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _semanticLabel() {
|
||||||
|
final axis = widget.axis == Axis.horizontal ? 'width' : 'height';
|
||||||
|
if (widget.slot == Slots.sidebar) return 'Sidebar $axis';
|
||||||
|
if (widget.slot == Slots.contextPanel) return 'Context panel $axis';
|
||||||
|
return '${widget.slot.value} $axis';
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<ShortcutActivator, Intent> _shortcuts() {
|
||||||
|
final horiz = widget.axis == Axis.horizontal;
|
||||||
|
final fine = DragResizeHandle.stepFine;
|
||||||
|
final coarse = DragResizeHandle.stepCoarse;
|
||||||
|
return <ShortcutActivator, Intent>{
|
||||||
|
if (horiz) ...{
|
||||||
|
const SingleActivator(LogicalKeyboardKey.arrowLeft): _BumpIntent(-fine),
|
||||||
|
const SingleActivator(LogicalKeyboardKey.arrowRight): _BumpIntent(fine),
|
||||||
|
const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true): _BumpIntent(-coarse),
|
||||||
|
const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true): _BumpIntent(coarse),
|
||||||
|
} else ...{
|
||||||
|
const SingleActivator(LogicalKeyboardKey.arrowUp): _BumpIntent(-fine),
|
||||||
|
const SingleActivator(LogicalKeyboardKey.arrowDown): _BumpIntent(fine),
|
||||||
|
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): _BumpIntent(-coarse),
|
||||||
|
const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true): _BumpIntent(coarse),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void _bump(double rawDelta) {
|
||||||
|
final current = widget.arrangement.sizeOf(widget.slot);
|
||||||
|
if (current == null) return;
|
||||||
|
widget.arrangement.setSize(widget.slot, bumpedSlotSize(slot: widget.slot, current: current, rawDelta: rawDelta));
|
||||||
|
}
|
||||||
|
|
||||||
void _onDown(PointerDownEvent e) {
|
void _onDown(PointerDownEvent e) {
|
||||||
_dragStartSize = widget.arrangement.sizeOf(widget.slot);
|
_dragStartSize = widget.arrangement.sizeOf(widget.slot);
|
||||||
_dragStartPointer = e.position;
|
_dragStartPointer = e.position;
|
||||||
@@ -80,3 +144,22 @@ class _DragResizeHandleState extends State<DragResizeHandle> {
|
|||||||
_dragStartPointer = null;
|
_dragStartPointer = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _BumpIntent extends Intent {
|
||||||
|
const _BumpIntent(this.delta);
|
||||||
|
final double delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply a raw delta in the natural axis direction. Drag and arrow
|
||||||
|
/// keys both call this so the keyboard mirrors the drag: positive
|
||||||
|
/// delta = right/down. Context-panel sits on the right edge of the
|
||||||
|
/// app, so we flip the sign there — right-arrow should *shrink* it,
|
||||||
|
/// matching how dragging the left-edge handle rightward works.
|
||||||
|
double bumpedSlotSize({
|
||||||
|
required SlotId slot,
|
||||||
|
required double current,
|
||||||
|
required double rawDelta,
|
||||||
|
}) {
|
||||||
|
final delta = slot == Slots.contextPanel ? -rawDelta : rawDelta;
|
||||||
|
return current + delta;
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,6 +38,13 @@ class SchedulerService {
|
|||||||
StreamSubscription<dynamic>? _sub;
|
StreamSubscription<dynamic>? _sub;
|
||||||
StreamSubscription<dynamic>? _projectSub;
|
StreamSubscription<dynamic>? _projectSub;
|
||||||
|
|
||||||
|
/// Tracks the spawn future so [_stopTicker] can await it before
|
||||||
|
/// killing — otherwise a stop racing a still-spawning isolate
|
||||||
|
/// leaves `_isolate` null at kill time and the just-spawned isolate
|
||||||
|
/// (with its `Timer.periodic`) leaks forever. Same race shape we
|
||||||
|
/// fixed in `NativePty` via `_readerReady` (T-96).
|
||||||
|
Future<Isolate?>? _isolateReady;
|
||||||
|
|
||||||
/// Listen for project lifecycle events. The periodic ticker only runs
|
/// Listen for project lifecycle events. The periodic ticker only runs
|
||||||
/// while a project is open — no wasted cycles on the welcome screen.
|
/// while a project is open — no wasted cycles on the welcome screen.
|
||||||
void start() {
|
void start() {
|
||||||
@@ -47,8 +54,8 @@ class SchedulerService {
|
|||||||
|
|
||||||
/// Start the periodic ticker and fire an immediate first cycle so
|
/// Start the periodic ticker and fire an immediate first cycle so
|
||||||
/// all panels refresh without waiting for the first interval.
|
/// all panels refresh without waiting for the first interval.
|
||||||
void _startTicker() {
|
Future<void> _startTicker() async {
|
||||||
_stopTicker();
|
await _stopTicker();
|
||||||
|
|
||||||
// Stagger the initial ticks to avoid a rebuild storm on project open.
|
// Stagger the initial ticks to avoid a rebuild storm on project open.
|
||||||
var delay = 0;
|
var delay = 0;
|
||||||
@@ -68,14 +75,26 @@ class SchedulerService {
|
|||||||
_events.emit(SchedulerTick(tier: tier));
|
_events.emit(SchedulerTick(tier: tier));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Isolate.spawn(_isolateEntry, _port!.sendPort).then((iso) => _isolate = iso);
|
_isolateReady = Isolate.spawn(_isolateEntry, _port!.sendPort);
|
||||||
|
_isolateReady!.then((iso) => _isolate = iso).catchError((_) => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _stopTicker() {
|
Future<void> _stopTicker() async {
|
||||||
|
// Await any in-flight spawn so we never miss killing an isolate
|
||||||
|
// that's mid-creation — see _isolateReady.
|
||||||
|
if (_isolateReady != null) {
|
||||||
|
try {
|
||||||
|
final pending = await _isolateReady;
|
||||||
|
_isolate ??= pending;
|
||||||
|
} catch (_) {
|
||||||
|
// Spawn failed; nothing to kill.
|
||||||
|
}
|
||||||
|
}
|
||||||
_sub?.cancel();
|
_sub?.cancel();
|
||||||
_port?.close();
|
_port?.close();
|
||||||
_isolate?.kill(priority: Isolate.immediate);
|
_isolate?.kill(priority: Isolate.immediate);
|
||||||
_isolate = null;
|
_isolate = null;
|
||||||
|
_isolateReady = null;
|
||||||
_port = null;
|
_port = null;
|
||||||
_sub = null;
|
_sub = null;
|
||||||
}
|
}
|
||||||
@@ -97,8 +116,8 @@ class SchedulerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
Future<void> dispose() async {
|
||||||
_projectSub?.cancel();
|
_projectSub?.cancel();
|
||||||
_stopTicker();
|
await _stopTicker();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'dart:ffi';
|
|||||||
import 'dart:io' show File, Platform;
|
import 'dart:io' show File, Platform;
|
||||||
|
|
||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
// -- Opaque handles ----------------------------------------------------------
|
// -- Opaque handles ----------------------------------------------------------
|
||||||
|
|
||||||
@@ -146,6 +147,60 @@ class TreeSitterLib {
|
|||||||
wasmEngineNew = lib.lookupFunction<_WasmEngineNew, DWasmEngineNew>('wasm_engine_new'),
|
wasmEngineNew = lib.lookupFunction<_WasmEngineNew, DWasmEngineNew>('wasm_engine_new'),
|
||||||
wasmEngineDelete = lib.lookupFunction<_WasmEngineDelete, DWasmEngineDelete>('wasm_engine_delete');
|
wasmEngineDelete = lib.lookupFunction<_WasmEngineDelete, DWasmEngineDelete>('wasm_engine_delete');
|
||||||
|
|
||||||
|
/// Constructs a [TreeSitterLib] from caller-supplied Dart closures. Used by
|
||||||
|
/// tests to substitute the FFI surface without dlopen'ing the real library;
|
||||||
|
/// each unspecified function defaults to a safe no-op (pointers return
|
||||||
|
/// `nullptr`, ints return `0`, bools return `false`). Tests override the
|
||||||
|
/// few entries they exercise.
|
||||||
|
@visibleForTesting
|
||||||
|
TreeSitterLib.testing({
|
||||||
|
DTsParserNew? parserNew,
|
||||||
|
DTsParserDelete? parserDelete,
|
||||||
|
DTsParserSetLanguage? parserSetLanguage,
|
||||||
|
DTsParserSetWasmStore? parserSetWasmStore,
|
||||||
|
DTsParserParseString? parserParseString,
|
||||||
|
DTsTreeDelete? treeDelete,
|
||||||
|
DTsTreeRootNode? treeRootNode,
|
||||||
|
DTsNodeStartByte? nodeStartByte,
|
||||||
|
DTsNodeEndByte? nodeEndByte,
|
||||||
|
DTsQueryNew? queryNew,
|
||||||
|
DTsQueryDelete? queryDelete,
|
||||||
|
DTsQueryCaptureCount? queryCaptureCount,
|
||||||
|
DTsQueryCaptureNameForId? queryCaptureNameForId,
|
||||||
|
DTsQueryCursorNew? queryCursorNew,
|
||||||
|
DTsQueryCursorDelete? queryCursorDelete,
|
||||||
|
DTsQueryCursorExec? queryCursorExec,
|
||||||
|
DTsQueryCursorNextMatch? queryCursorNextMatch,
|
||||||
|
DTsWasmStoreNew? wasmStoreNew,
|
||||||
|
DTsWasmStoreDelete? wasmStoreDelete,
|
||||||
|
DTsWasmStoreLoadLanguage? wasmStoreLoadLanguage,
|
||||||
|
DWasmEngineNew? wasmEngineNew,
|
||||||
|
DWasmEngineDelete? wasmEngineDelete,
|
||||||
|
}) : parserNew = parserNew ?? (() => nullptr),
|
||||||
|
parserDelete = parserDelete ?? ((_) {}),
|
||||||
|
parserSetLanguage = parserSetLanguage ?? ((_, __) => false),
|
||||||
|
parserSetWasmStore = parserSetWasmStore ?? ((_, __) {}),
|
||||||
|
parserParseString = parserParseString ?? ((_, __, ___, ____) => nullptr),
|
||||||
|
treeDelete = treeDelete ?? ((_) {}),
|
||||||
|
// Leaks a zeroed TSNode allocation — only hit when the test supplies
|
||||||
|
// a non-null parserParseString without also supplying treeRootNode.
|
||||||
|
treeRootNode = treeRootNode ?? ((_) => calloc<TSNode>().ref),
|
||||||
|
nodeStartByte = nodeStartByte ?? ((_) => 0),
|
||||||
|
nodeEndByte = nodeEndByte ?? ((_) => 0),
|
||||||
|
queryNew = queryNew ?? ((_, __, ___, ____, _____) => nullptr),
|
||||||
|
queryDelete = queryDelete ?? ((_) {}),
|
||||||
|
queryCaptureCount = queryCaptureCount ?? ((_) => 0),
|
||||||
|
queryCaptureNameForId = queryCaptureNameForId ?? ((_, __, ___) => nullptr),
|
||||||
|
queryCursorNew = queryCursorNew ?? (() => nullptr),
|
||||||
|
queryCursorDelete = queryCursorDelete ?? ((_) {}),
|
||||||
|
queryCursorExec = queryCursorExec ?? ((_, __, ___) {}),
|
||||||
|
queryCursorNextMatch = queryCursorNextMatch ?? ((_, __) => false),
|
||||||
|
wasmStoreNew = wasmStoreNew ?? ((_, __) => nullptr),
|
||||||
|
wasmStoreDelete = wasmStoreDelete ?? ((_) {}),
|
||||||
|
wasmStoreLoadLanguage = wasmStoreLoadLanguage ?? ((_, __, ___, ____, _____) => nullptr),
|
||||||
|
wasmEngineNew = wasmEngineNew ?? (() => nullptr),
|
||||||
|
wasmEngineDelete = wasmEngineDelete ?? ((_) {});
|
||||||
|
|
||||||
final DTsParserNew parserNew;
|
final DTsParserNew parserNew;
|
||||||
final DTsParserDelete parserDelete;
|
final DTsParserDelete parserDelete;
|
||||||
final DTsParserSetLanguage parserSetLanguage;
|
final DTsParserSetLanguage parserSetLanguage;
|
||||||
@@ -181,6 +236,22 @@ class TreeSitterLib {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Constructs a [TreeSitterLib] from an already-loaded [DynamicLibrary].
|
||||||
|
/// Used by the smoke test to dlopen the vendored `libtree-sitter.so`
|
||||||
|
/// directly without going through the global `init()` / `_instance`
|
||||||
|
/// dance, so the test stays isolated from the singleton.
|
||||||
|
@visibleForTesting
|
||||||
|
static TreeSitterLib fromDynamicLibrary(DynamicLibrary lib) => TreeSitterLib._(lib);
|
||||||
|
|
||||||
|
/// Most recent error raised while trying to dlopen the tree-sitter
|
||||||
|
/// library, paired with the path that was attempted. Cleared on a
|
||||||
|
/// successful open. Null means "no attempt failed yet."
|
||||||
|
///
|
||||||
|
/// Callers that observe a null `instance` after `init()` should read
|
||||||
|
/// this for the diagnostic instead of guessing.
|
||||||
|
static Object? lastOpenError;
|
||||||
|
static String? lastOpenErrorPath;
|
||||||
|
|
||||||
static DynamicLibrary? _openLibrary() {
|
static DynamicLibrary? _openLibrary() {
|
||||||
final libName = Platform.isLinux
|
final libName = Platform.isLinux
|
||||||
? 'libtree-sitter.so'
|
? 'libtree-sitter.so'
|
||||||
@@ -189,12 +260,22 @@ class TreeSitterLib {
|
|||||||
: Platform.isWindows
|
: Platform.isWindows
|
||||||
? 'tree-sitter.dll'
|
? 'tree-sitter.dll'
|
||||||
: null;
|
: null;
|
||||||
if (libName == null) return null;
|
if (libName == null) {
|
||||||
|
lastOpenError = 'unsupported platform ${Platform.operatingSystem}';
|
||||||
|
lastOpenErrorPath = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Try standard dlopen path first (works when lib is in bundle/lib/).
|
// Try standard dlopen path first (works when lib is in bundle/lib/).
|
||||||
try {
|
try {
|
||||||
return DynamicLibrary.open(libName);
|
final lib = DynamicLibrary.open(libName);
|
||||||
} catch (_) {}
|
lastOpenError = null;
|
||||||
|
lastOpenErrorPath = null;
|
||||||
|
return lib;
|
||||||
|
} catch (e) {
|
||||||
|
lastOpenError = e;
|
||||||
|
lastOpenErrorPath = libName;
|
||||||
|
}
|
||||||
|
|
||||||
// Try next to executable.
|
// Try next to executable.
|
||||||
final exe = File(Platform.resolvedExecutable).parent.path;
|
final exe = File(Platform.resolvedExecutable).parent.path;
|
||||||
@@ -202,8 +283,14 @@ class TreeSitterLib {
|
|||||||
final path = '$dir/$libName';
|
final path = '$dir/$libName';
|
||||||
if (File(path).existsSync()) {
|
if (File(path).existsSync()) {
|
||||||
try {
|
try {
|
||||||
return DynamicLibrary.open(path);
|
final lib = DynamicLibrary.open(path);
|
||||||
} catch (_) {}
|
lastOpenError = null;
|
||||||
|
lastOpenErrorPath = null;
|
||||||
|
return lib;
|
||||||
|
} catch (e) {
|
||||||
|
lastOpenError = e;
|
||||||
|
lastOpenErrorPath = path;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -8,8 +8,17 @@ import 'package:clide/kernel/src/syntax/language_map.dart';
|
|||||||
import 'package:clide/kernel/src/syntax/tree_sitter_ffi.dart';
|
import 'package:clide/kernel/src/syntax/tree_sitter_ffi.dart';
|
||||||
import 'package:clide/kernel/src/theme/tokens.dart';
|
import 'package:clide/kernel/src/theme/tokens.dart';
|
||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart' show rootBundle;
|
import 'package:flutter/services.dart' show rootBundle;
|
||||||
|
|
||||||
|
/// Loads grammar WASM bytes for [language] (e.g. "dart" → `dart.wasm`).
|
||||||
|
/// Throws on missing or unreadable assets.
|
||||||
|
typedef GrammarBytesLoader = Future<Uint8List> Function(String language);
|
||||||
|
|
||||||
|
/// Loads the highlight query (`.scm` source) for [language], or returns
|
||||||
|
/// null if no query is bundled for it.
|
||||||
|
typedef GrammarQueryLoader = Future<String?> Function(String language);
|
||||||
|
|
||||||
class SyntaxSpan {
|
class SyntaxSpan {
|
||||||
const SyntaxSpan({
|
const SyntaxSpan({
|
||||||
required this.start,
|
required this.start,
|
||||||
@@ -42,8 +51,37 @@ class _LoadedGrammar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class TreeSitterService {
|
class TreeSitterService {
|
||||||
static final TreeSitterService shared = TreeSitterService._();
|
static final TreeSitterService shared = TreeSitterService();
|
||||||
TreeSitterService._();
|
|
||||||
|
/// Production constructor: uses the dlopen'd [TreeSitterLib.instance] and
|
||||||
|
/// the Flutter [rootBundle]. Tests pass [lib] / [grammarBytes] /
|
||||||
|
/// [grammarQuery] to substitute a fake FFI surface and in-memory assets.
|
||||||
|
TreeSitterService({
|
||||||
|
TreeSitterLib? lib,
|
||||||
|
GrammarBytesLoader? grammarBytes,
|
||||||
|
GrammarQueryLoader? grammarQuery,
|
||||||
|
}) : _injectedLib = lib,
|
||||||
|
_grammarBytes = grammarBytes ?? _defaultGrammarBytes,
|
||||||
|
_grammarQuery = grammarQuery ?? _defaultGrammarQuery;
|
||||||
|
|
||||||
|
final TreeSitterLib? _injectedLib;
|
||||||
|
final GrammarBytesLoader _grammarBytes;
|
||||||
|
final GrammarQueryLoader _grammarQuery;
|
||||||
|
|
||||||
|
TreeSitterLib? get _lib => _injectedLib ?? TreeSitterLib.instance;
|
||||||
|
|
||||||
|
static Future<Uint8List> _defaultGrammarBytes(String language) async {
|
||||||
|
final data = await rootBundle.load('assets/grammars/$language.wasm');
|
||||||
|
return data.buffer.asUint8List();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<String?> _defaultGrammarQuery(String language) async {
|
||||||
|
try {
|
||||||
|
return await rootBundle.loadString('assets/queries/$language.scm');
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final Map<String, _LoadedGrammar> _grammars = {};
|
final Map<String, _LoadedGrammar> _grammars = {};
|
||||||
final Set<String> _unavailable = {};
|
final Set<String> _unavailable = {};
|
||||||
@@ -58,7 +96,7 @@ class TreeSitterService {
|
|||||||
if (_initDone) return _parser != null;
|
if (_initDone) return _parser != null;
|
||||||
_initDone = true;
|
_initDone = true;
|
||||||
|
|
||||||
final lib = TreeSitterLib.instance;
|
final lib = _lib;
|
||||||
if (lib == null) return false;
|
if (lib == null) return false;
|
||||||
|
|
||||||
final engine = lib.wasmEngineNew();
|
final engine = lib.wasmEngineNew();
|
||||||
@@ -92,12 +130,11 @@ class TreeSitterService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final lib = TreeSitterLib.instance!;
|
final lib = _lib!;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Load grammar WASM bytes.
|
// Load grammar WASM bytes.
|
||||||
final wasmData = await rootBundle.load('assets/grammars/$language.wasm');
|
final wasmBytes = await _grammarBytes(language);
|
||||||
final wasmBytes = wasmData.buffer.asUint8List();
|
|
||||||
|
|
||||||
// Load into WASM store.
|
// Load into WASM store.
|
||||||
final nameNative = language.toNativeUtf8();
|
final nameNative = language.toNativeUtf8();
|
||||||
@@ -126,10 +163,7 @@ class TreeSitterService {
|
|||||||
calloc.free(error);
|
calloc.free(error);
|
||||||
|
|
||||||
// Load highlight query.
|
// Load highlight query.
|
||||||
String? querySource;
|
final querySource = await _grammarQuery(language);
|
||||||
try {
|
|
||||||
querySource = await rootBundle.loadString('assets/queries/$language.scm');
|
|
||||||
} catch (_) {}
|
|
||||||
|
|
||||||
Pointer<TSQuery> query = nullptr;
|
Pointer<TSQuery> query = nullptr;
|
||||||
List<String> captureNames = [];
|
List<String> captureNames = [];
|
||||||
@@ -200,7 +234,7 @@ class TreeSitterService {
|
|||||||
return SyntaxResult.empty;
|
return SyntaxResult.empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
final lib = TreeSitterLib.instance!;
|
final lib = _lib!;
|
||||||
final parser = _parser!;
|
final parser = _parser!;
|
||||||
final cursor = _cursor!;
|
final cursor = _cursor!;
|
||||||
|
|
||||||
@@ -253,7 +287,7 @@ class TreeSitterService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
final lib = TreeSitterLib.instance;
|
final lib = _lib;
|
||||||
if (lib == null) return;
|
if (lib == null) return;
|
||||||
|
|
||||||
for (final grammar in _grammars.values) {
|
for (final grammar in _grammars.values) {
|
||||||
@@ -273,6 +307,15 @@ class TreeSitterService {
|
|||||||
_unavailable.clear();
|
_unavailable.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resets the service to a pre-init state. Tests use this to re-exercise
|
||||||
|
/// `_init()` without constructing a new singleton; production code never
|
||||||
|
/// needs it.
|
||||||
|
@visibleForTesting
|
||||||
|
void resetForTests() {
|
||||||
|
dispose();
|
||||||
|
_initDone = false;
|
||||||
|
}
|
||||||
|
|
||||||
static Color colorForRole(String role, SurfaceTokens tokens) {
|
static Color colorForRole(String role, SurfaceTokens tokens) {
|
||||||
return switch (role) {
|
return switch (role) {
|
||||||
'keyword' || 'repeat' || 'conditional' || 'include' || 'exception' || 'operator' => tokens.syntaxKeyword,
|
'keyword' || 'repeat' || 'conditional' || 'include' || 'exception' || 'operator' => tokens.syntaxKeyword,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user