Compare commits
@@ -59,7 +59,10 @@
|
|||||||
"Bash(git checkout -- *)",
|
"Bash(git checkout -- *)",
|
||||||
"Bash(git restore .*)",
|
"Bash(git restore .*)",
|
||||||
"Bash(chmod -R 777 *)",
|
"Bash(chmod -R 777 *)",
|
||||||
"Bash(chmod 777 *)"
|
"Bash(chmod 777 *)",
|
||||||
|
"Bash(git add -A*)",
|
||||||
|
"Bash(git add --all*)",
|
||||||
|
"Bash(git add .)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-10
@@ -5,19 +5,22 @@ 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
|
helper scripts. This index is for humans skimming what's available; Claude
|
||||||
discovers skills automatically from the directory structure.
|
discovers skills automatically from the directory structure.
|
||||||
|
|
||||||
|
Only clide-specific skills live in this repo. Repo-agnostic skills
|
||||||
|
(`d2-diagram`, `frame0-wireframe`, `skill-create`) live at user scope
|
||||||
|
(`~/.claude/skills/`), as do the pql-distributed ones (`pql`, `clean-house`,
|
||||||
|
installed via `pql init`).
|
||||||
|
|
||||||
| Skill | Purpose |
|
| Skill | Purpose |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [`d2-diagram`](d2-diagram/SKILL.md) | Generate technical diagrams from `.d2` source with the d2 CLI; renders to PNG. |
|
| [`clide`](clide/SKILL.md) | Observe/drive the live clide UI through the `clide` CLI; discover the surface with `clide capabilities`. |
|
||||||
| [`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 (60-word cap), attribution trailer, safety rules. |
|
||||||
| [`git-commit`](git-commit/SKILL.md) | Commit conventions for this repo — message style, CHANGELOG discipline (40/60 word cap), attribution trailer, safety rules. |
|
| [`testmode`](testmode/SKILL.md) | Run and interpret the `ClideTestApp` platform integration harness; smoke-test after toolchain / IPC / PTY / theme / native changes. |
|
||||||
| [`pql`](pql/SKILL.md) | Query and plan against the markdown vault via the `pql` CLI (decisions, tickets, structural queries). |
|
| [`ui-design`](ui-design/SKILL.md) | Visual design guide — surface tokens, control geometry, Phosphor icons (`byName` + generated glyph table). |
|
||||||
| [`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. |
|
| [`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
|
## Adding a skill
|
||||||
|
|
||||||
Use the `skill-create` skill (or follow its SKILL.md by hand). Add a row to
|
Use the `skill-create` skill (user scope; or follow its SKILL.md by hand).
|
||||||
the table above so the inventory stays accurate; the index is otherwise just
|
Clide-specific skills go here; repo-agnostic ones go to `~/.claude/skills/`.
|
||||||
a directory listing.
|
Add a row to the table above so the inventory stays accurate; the index is
|
||||||
|
otherwise just a directory listing.
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "1.5.0",
|
|
||||||
"hash": "sha256:8090b99552c828ce019aa3740d8a0acb4768a443704b4e935efa23b32e91128d",
|
|
||||||
"installed_at": "2026-06-02T08:46:25Z"
|
|
||||||
}
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
---
|
|
||||||
name: clean-house
|
|
||||||
description: >
|
|
||||||
Run a documentation-discipline pass over a DQR (Decisions / Questions /
|
|
||||||
Rejected) markdown system. Audits decisions/, surfaces drift (broken
|
|
||||||
anchor links, missing Q→D backlinks, sunset-shaped phrases without
|
|
||||||
linked tickets, files over the split threshold, dead cross-references),
|
|
||||||
batches findings by category, and uses AskUserQuestion to apply fixes
|
|
||||||
interactively. Trigger this skill whenever the user asks to clean,
|
|
||||||
tidy, audit, sweep, garden, or review the state of decisions/, the
|
|
||||||
DQR system, the questions index, or the records under decisions/.
|
|
||||||
Also trigger before a release, after a session that touched many
|
|
||||||
records, or whenever the user mentions documentation drift, stale
|
|
||||||
plans, or "house" / "clean-house" / "house cleaning". Imperative —
|
|
||||||
apply fixes, not just report.
|
|
||||||
---
|
|
||||||
|
|
||||||
# clean-house
|
|
||||||
|
|
||||||
A periodic gardening pass over `decisions/`. The DQR system is
|
|
||||||
markdown-as-source-of-truth; `.pql/pql.db` is a derived index. This skill
|
|
||||||
operates on the markdown files, batches drift by category, and uses
|
|
||||||
`AskUserQuestion` to decide what to apply.
|
|
||||||
|
|
||||||
The skill is **imperative**. It really cleans. The verb-noun match is
|
|
||||||
load-bearing — a `clean-house` that only produced a report would be
|
|
||||||
misnamed. Mechanical fixes get a single batched approval, judgment-call
|
|
||||||
findings get individual prompts, and a "stop asking" escape hatch is
|
|
||||||
always present.
|
|
||||||
|
|
||||||
You — Claude — are the runtime. Read this file, read
|
|
||||||
`references/rules.md`, follow the procedure below, call `pql` and `Edit`
|
|
||||||
to do the work, call `AskUserQuestion` for the prompts.
|
|
||||||
|
|
||||||
## When to run
|
|
||||||
|
|
||||||
- Before a release.
|
|
||||||
- After a session that touched many D / Q / R records.
|
|
||||||
- When something feels stale and a sweep is wanted.
|
|
||||||
- When `pql decisions validate` is green but `decisions/` still feels off.
|
|
||||||
|
|
||||||
This skill is **not** a replacement for `pql decisions validate`.
|
|
||||||
Validate is fast, hot, and gates pre-push. clean-house is slow, cold,
|
|
||||||
on-demand. Escalation path: rules that fire often here are candidates
|
|
||||||
for promotion to `pql decisions validate`; rules that almost never fire
|
|
||||||
stay here where false positives don't slow anyone down.
|
|
||||||
|
|
||||||
## Procedure
|
|
||||||
|
|
||||||
1. **Gate on validate.** Run `pql decisions validate`. If it exits
|
|
||||||
non-zero, stop and tell the user to fix validator findings first.
|
|
||||||
Do not proceed.
|
|
||||||
|
|
||||||
2. **Load the rule catalog.** Read `references/rules.md`. Each rule
|
|
||||||
names its detection, fix, and finding-id format.
|
|
||||||
|
|
||||||
3. **Probe conventions.** Read `decisions/.clean-house.yaml` if it
|
|
||||||
exists; otherwise infer from the project. Conventions to resolve:
|
|
||||||
- `heading_level`: 2 or 3 — the depth records use (`## D-N` vs
|
|
||||||
`### D-N`). Probe by sampling the first 3 records' file
|
|
||||||
locations and looking for the first matching ATX heading.
|
|
||||||
- `backlink_phrasing`: the project's Q→D backlink phrase (default
|
|
||||||
`Resolved → D-N`; some projects use `Resolved as D-N` or
|
|
||||||
`→ D-N`).
|
|
||||||
- `file_threshold`: default 350 (RULE-FILE-OVER-THRESHOLD).
|
|
||||||
- `stale_open_q_days`: default 60 (RULE-STALE-OPEN-Q).
|
|
||||||
|
|
||||||
Pass the resolved conventions into each rule's detection. Cache
|
|
||||||
the probe result for the run.
|
|
||||||
|
|
||||||
`decisions/.clean-house.yaml` shape (all keys optional):
|
|
||||||
```yaml
|
|
||||||
heading_level: 3
|
|
||||||
backlink_phrasing: "Resolved → "
|
|
||||||
file_threshold: 500
|
|
||||||
stale_open_q_days: 90
|
|
||||||
exclude_paths: ["legacy/", "drafts/"]
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Sync the index.** Run `pql decisions sync` so the DB reflects
|
|
||||||
the markdown.
|
|
||||||
|
|
||||||
5. **Walk decisions/.** Use `pql decisions list` to enumerate
|
|
||||||
records, plus `pql decisions read <id>` per record when a rule
|
|
||||||
needs the body. For file-level rules (size, sort), read the
|
|
||||||
markdown files directly. **Filter out** any record whose
|
|
||||||
`file_path` begins with `legacy/` (or any prefix in
|
|
||||||
`exclude_paths` from the probe).
|
|
||||||
|
|
||||||
6. **Run each rule's detection.** Collect findings. Tag each with
|
|
||||||
its rule ID, finding ID (per the rule's format), category
|
|
||||||
(`mechanical` | `judgment`), and the record(s)/file(s) involved.
|
|
||||||
|
|
||||||
7. **Group findings by rule** and prompt. Honor any "skipped 3+
|
|
||||||
times" findings from the skip ledger by escalating their prompt
|
|
||||||
("skipped 3x — promote to manual decision, downgrade to
|
|
||||||
summary-only, or keep asking?"):
|
|
||||||
|
|
||||||
- **Mechanical rules** — one prompt per rule, batched. Show the
|
|
||||||
count and offer: apply all / show diff first / skip this rule /
|
|
||||||
**stop asking** (global — see below).
|
|
||||||
- **Judgment rules** — one prompt per finding. Options match the
|
|
||||||
rule's `Fix` section, plus the same global **stop asking**.
|
|
||||||
|
|
||||||
**"Stop asking" semantics (global).** When the user picks "stop
|
|
||||||
asking" on any prompt, immediately halt all further prompts in
|
|
||||||
the run — mechanical batches not yet asked AND remaining
|
|
||||||
judgment findings. Move all unprompted findings to the skip
|
|
||||||
ledger marked `deferred-stop-asking`. Skip to step 9.
|
|
||||||
|
|
||||||
**"Show diff first" loop.** When the user picks "show diff
|
|
||||||
first" on a mechanical batch, emit a unified diff of the staged
|
|
||||||
edits (one block per file), then re-prompt the same question
|
|
||||||
with the same options minus "show diff first" — preventing
|
|
||||||
infinite loops while still letting the user inspect before
|
|
||||||
committing.
|
|
||||||
|
|
||||||
8. **Apply approvals.** For mechanical rules use `Edit` to mutate
|
|
||||||
the markdown directly. For judgment rules, follow the option the
|
|
||||||
user picked (file a ticket via `pql ticket new`, mark superseded
|
|
||||||
by editing the record, etc.).
|
|
||||||
|
|
||||||
9. **Update the skip ledger and history.** Both files live at
|
|
||||||
`decisions/.clean-house-state.md`, gitignored. The file uses
|
|
||||||
per-run section headers so it doubles as run history:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# clean-house run history
|
|
||||||
|
|
||||||
## 2026-05-06T13:42Z
|
|
||||||
fired: RULE-ANCHOR-DRIFT(1) RULE-SUNSET-WITHOUT-TICKET(1)
|
|
||||||
applied: RULE-ANCHOR-DRIFT(1) RULE-SUNSET-WITHOUT-TICKET(1)
|
|
||||||
skipped:
|
|
||||||
- RULE-ANCHOR-DRIFT D-8:#q-1-markdown-mirror-for-tickets
|
|
||||||
|
|
||||||
## 2026-05-04T11:10Z
|
|
||||||
fired: ...
|
|
||||||
```
|
|
||||||
|
|
||||||
Each section starts with a UTC ISO-8601 timestamp. `fired`
|
|
||||||
counts findings detected; `applied` counts findings the user
|
|
||||||
acted on; `skipped` lists `<rule-id> <finding-id>` pairs.
|
|
||||||
|
|
||||||
**Promotion-candidate computation.** Scan the most recent N=7
|
|
||||||
sections (or all sections if fewer). A rule is a promotion
|
|
||||||
candidate if it `fired` in ≥ 5 of those sections. Surface the
|
|
||||||
candidates in the summary; do not auto-promote.
|
|
||||||
|
|
||||||
10. **Emit the summary** (see Output below).
|
|
||||||
|
|
||||||
## Question phrasing
|
|
||||||
|
|
||||||
Always include the rule ID in the prompt so the user learns what's
|
|
||||||
being checked.
|
|
||||||
|
|
||||||
**Mechanical (batched):**
|
|
||||||
> RULE-ANCHOR-DRIFT: 12 cross-reference anchors point to headings that
|
|
||||||
> no longer exist (renamed or deleted). Apply all 12 fixes? Options:
|
|
||||||
> apply all / show diff first / skip this rule / stop asking, summarize
|
|
||||||
> the rest.
|
|
||||||
|
|
||||||
**Judgment (individual):**
|
|
||||||
> RULE-SUNSET-WITHOUT-TICKET: D-59 mentions "must track dugite-native
|
|
||||||
> releases for security updates" but has no linked T. Options: file T
|
|
||||||
> now / draft a T description first / mark as already covered (add note
|
|
||||||
> to D) / skip / stop asking.
|
|
||||||
|
|
||||||
Phrasing rules:
|
|
||||||
|
|
||||||
- **Always include "stop asking, summarize the rest."** Sessions
|
|
||||||
without bandwidth for full review need an escape. Forcing answers
|
|
||||||
to every question kills the tool.
|
|
||||||
- **Always include "skip."** Skipped findings go to the ledger; three
|
|
||||||
consecutive skips of the same finding promote the next prompt.
|
|
||||||
- **Mechanical batches; judgment doesn't.** "Apply all 12 fixes?" is
|
|
||||||
one decision against a clear delta. Twelve individual prompts defeat
|
|
||||||
the point. Conversely, judgment findings are each separate work —
|
|
||||||
presenting them as a batch hides cost.
|
|
||||||
|
|
||||||
## What this skill does NOT do
|
|
||||||
|
|
||||||
- Does not file tickets without asking.
|
|
||||||
- Does not rewrite D-record body prose, only metadata fields and
|
|
||||||
cross-reference links.
|
|
||||||
- Does not touch records under `legacy/`.
|
|
||||||
- Does not run `pql decisions validate` *as a fix* — only as a
|
|
||||||
precondition gate.
|
|
||||||
- Does not promote rules to `pql decisions validate` automatically.
|
|
||||||
Promotion is a human decision; this skill only flags candidates
|
|
||||||
("RULE-X has fired in 5 of the last 7 runs — consider promoting").
|
|
||||||
|
|
||||||
## Non-interactive context
|
|
||||||
|
|
||||||
If invoked without an interactive `AskUserQuestion` surface (a CI run,
|
|
||||||
a batch script), refuse to apply judgment fixes. Apply mechanical
|
|
||||||
fixes only if the user explicitly says so via the trigger phrase
|
|
||||||
("auto-apply mechanical, skip judgment"); otherwise emit the summary
|
|
||||||
and exit without mutating files.
|
|
||||||
|
|
||||||
## Output
|
|
||||||
|
|
||||||
At the end, emit a summary block:
|
|
||||||
|
|
||||||
```
|
|
||||||
clean-house — sweep complete
|
|
||||||
────────────────────────────
|
|
||||||
Files scanned: 11
|
|
||||||
Records parsed: 62 D, 23 Q, 11 R
|
|
||||||
Conventions: heading_level=3 backlink="Resolved → "
|
|
||||||
Mechanical fixes: 14 applied, 0 deferred
|
|
||||||
Judgment findings: 3 acted on, 2 deferred (in state file)
|
|
||||||
Promotion candidates: RULE-ANCHOR-DRIFT (5/7 runs)
|
|
||||||
Touched files: decisions/architecture.md, decisions/questions.md
|
|
||||||
```
|
|
||||||
|
|
||||||
If this is the first run (no prior history) or there are fewer
|
|
||||||
than 5 prior runs, `Promotion candidates:` reports
|
|
||||||
`(none — N/7 runs of history)` instead of decorative empty content.
|
|
||||||
|
|
||||||
The summary names the touched files so the next commit message can be
|
|
||||||
honest about what changed. The skill does not commit on its own.
|
|
||||||
|
|
||||||
## Versioning
|
|
||||||
|
|
||||||
clean-house ships embedded in the pql binary; its version is the
|
|
||||||
pql version (`pql --version`). To see what changed and when, read
|
|
||||||
`CHANGELOG.md` or `git log internal/skill/clean-house/` in the pql
|
|
||||||
repo. Keep this file and `references/rules.md` consistent with
|
|
||||||
each other when iterating — they're the contract.
|
|
||||||
@@ -1,386 +0,0 @@
|
|||||||
# clean-house rule catalog
|
|
||||||
|
|
||||||
The rules clean-house runs and the reasoning behind each. New rules
|
|
||||||
land here as their own entry. Catalog churn — additions, refinements,
|
|
||||||
retirements — is tracked in pql's `CHANGELOG.md` and `git log
|
|
||||||
internal/skill/clean-house/`. When a rule is retired, leave a
|
|
||||||
**Retired** stub (ID + one-line reason + retirement commit) so the
|
|
||||||
trail of "we used to check X" is recoverable from this file alone.
|
|
||||||
|
|
||||||
## Reading a rule
|
|
||||||
|
|
||||||
Each entry below has:
|
|
||||||
|
|
||||||
- **ID** — Stable identifier (e.g. `RULE-ANCHOR-DRIFT`). Used in
|
|
||||||
AskUserQuestion prompts and skip-ledger entries so the user learns
|
|
||||||
what's being checked.
|
|
||||||
- **Category** — `mechanical` or `judgment`. Drives whether findings
|
|
||||||
batch into one prompt or each get their own.
|
|
||||||
- **Finding ID** — Format string for the per-finding stable
|
|
||||||
identifier. Skip-ledger entries are written as `<rule-id>
|
|
||||||
<finding-id> <ISO-date>`; subsequent runs use the same format to
|
|
||||||
recognize "same finding skipped 3 runs in a row" and escalate.
|
|
||||||
Use only stable inputs (record IDs, file paths, slug strings, body
|
|
||||||
hashes) — never line numbers or timestamps.
|
|
||||||
- **Detection** — Concrete steps to find violations. Names the
|
|
||||||
pql command or file primitive used.
|
|
||||||
- **Fix** — Mechanical: deterministic action. Judgment: the prompt
|
|
||||||
option list and what each option does.
|
|
||||||
- **Why** — One paragraph on the failure mode this rule guards
|
|
||||||
against. Future-you reading the rule benefits from the reasoning,
|
|
||||||
not just the check.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## RULE-ANCHOR-DRIFT
|
|
||||||
|
|
||||||
**Category:** mechanical
|
|
||||||
|
|
||||||
**Finding ID:** `<source-record>:<link-target>` (e.g.
|
|
||||||
`D-8:#q-1-markdown-mirror-for-tickets` or
|
|
||||||
`D-8:questions.md#q-1`).
|
|
||||||
|
|
||||||
**Detection:**
|
|
||||||
|
|
||||||
Anchor-only markdown links (`[text](#slug)`) resolve against the
|
|
||||||
**source file's full heading set**, not the body of a single record
|
|
||||||
— record-level headings (`### D-N: …`) live as siblings in the
|
|
||||||
file and are valid link targets. The previous detection (which used
|
|
||||||
`pql decisions read`'s body-only `headings` array) missed these
|
|
||||||
and produced false positives.
|
|
||||||
|
|
||||||
Per source body:
|
|
||||||
|
|
||||||
1. Open the source body's file (`<vault>/<file_path>` from
|
|
||||||
`pql decisions list`) and extract every ATX heading. Build a
|
|
||||||
slug index for the file using the GFM convention (lowercase,
|
|
||||||
hyphenate spaces, drop punctuation, disambiguate duplicates with
|
|
||||||
`-1`/`-2`). Cache per file — every record in that file uses the
|
|
||||||
same index.
|
|
||||||
2. For each `[text](target)` link in the body:
|
|
||||||
- Anchor-only (`#slug`): check `slug` against the **source file's**
|
|
||||||
index. Missing → flag.
|
|
||||||
- Cross-file (`path.md#slug`): resolve `path.md` relative to the
|
|
||||||
source file's directory; check `slug` against that file's
|
|
||||||
index. Missing file or missing slug → flag.
|
|
||||||
|
|
||||||
**Fix:**
|
|
||||||
|
|
||||||
If the slug exists in a sibling file's index (same directory) and
|
|
||||||
the link is anchor-only, rewrite to `path.md#slug` (the canonical
|
|
||||||
cross-file form). If the slug exists in the source file's index
|
|
||||||
with edit-distance ≤ 2 from the link target, rewrite to the
|
|
||||||
matched slug. Otherwise downgrade to judgment ("no auto-fix; the
|
|
||||||
heading was removed, not renamed") with options: drop the link /
|
|
||||||
point elsewhere / mark as intentionally dangling.
|
|
||||||
|
|
||||||
**Fix:**
|
|
||||||
|
|
||||||
If exactly one heading with a closely-matching slug exists (slug
|
|
||||||
edit-distance ≤ 2 or substring match), rewrite the link to point at
|
|
||||||
that heading. If no plausible match exists, downgrade the finding to
|
|
||||||
judgment ("no auto-fix; the heading was removed, not renamed") and
|
|
||||||
prompt the user with options: drop the link / point elsewhere / mark
|
|
||||||
as intentionally dangling.
|
|
||||||
|
|
||||||
**Why:**
|
|
||||||
|
|
||||||
When a heading is renamed for clarity, every cross-reference pointing
|
|
||||||
at the old slug silently breaks. The rendered docs still look fine —
|
|
||||||
the link just goes nowhere. Without periodic sweeps these decay
|
|
||||||
indefinitely; the cost of the sweep is small compared to the cost of
|
|
||||||
a reader following a dead link and losing trust in the index.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## RULE-MISSING-Q-BACKLINK
|
|
||||||
|
|
||||||
**Category:** mechanical
|
|
||||||
|
|
||||||
**Finding ID:** `<d-id>:<q-id>` (e.g. `D-7:Q-2`). Order is always
|
|
||||||
D-first regardless of which side is missing the backlink.
|
|
||||||
|
|
||||||
**Detection:**
|
|
||||||
|
|
||||||
For each D record body, find lines of the shape `Resolves: Q-N`
|
|
||||||
(plain text, typically in the metadata block at the top of the
|
|
||||||
record). For each Q-N referenced, fetch that Q record's body via
|
|
||||||
`pql decisions read Q-N` and check whether it contains a line of the
|
|
||||||
shape `Resolved → D-N` (or equivalent backlink phrasing — match the
|
|
||||||
project's own convention; default pattern is `Resolved → D-N`).
|
|
||||||
|
|
||||||
Also check the reverse direction: any Q record claiming
|
|
||||||
`Resolved → D-N` whose D-N body lacks `Resolves: Q-N`.
|
|
||||||
|
|
||||||
**Fix:**
|
|
||||||
|
|
||||||
Insert the missing backlink in-place using `Edit`:
|
|
||||||
|
|
||||||
- Missing on the D side: add `Resolves: Q-N` to the D's metadata
|
|
||||||
block (typically right after `Domain:` / `Status:` lines).
|
|
||||||
- Missing on the Q side: append `Resolved → D-N` to the Q's status
|
|
||||||
line (or in the conventional position for that project).
|
|
||||||
|
|
||||||
**Why:**
|
|
||||||
|
|
||||||
Bidirectional Q↔D links are the navigation backbone of the DQR
|
|
||||||
system. When one side drifts, search-by-decision finds nothing for
|
|
||||||
that question and search-by-question doesn't surface its resolution.
|
|
||||||
The asymmetric state usually arises from a hand-edit on one record
|
|
||||||
that forgot to update the other; the fix is purely mechanical because
|
|
||||||
the correct content is already determined by the existing pointer
|
|
||||||
in the other direction.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## RULE-RECORD-SORT
|
|
||||||
|
|
||||||
**Category:** mechanical
|
|
||||||
|
|
||||||
**Finding ID:** `<file-path>` (e.g. `decisions/architecture.md`).
|
|
||||||
The whole file is one finding — sort applies to the file as a
|
|
||||||
unit.
|
|
||||||
|
|
||||||
**Detection:**
|
|
||||||
|
|
||||||
For each `decisions/*.md` file, read it directly (no pql) and find
|
|
||||||
all top-level `## D-N` / `## Q-N` / `## R-N` headings in order. Strip
|
|
||||||
the prefix, parse the numeric suffix, and check whether the sequence
|
|
||||||
is monotonically ascending within each ID family (D, Q, R kept
|
|
||||||
separate — files commonly mix families).
|
|
||||||
|
|
||||||
Only flag a file if it is **otherwise tidy** — currently no out-of-
|
|
||||||
order amendments interleaved with new records. The heuristic: if the
|
|
||||||
sort would touch fewer than three records out of position, apply it;
|
|
||||||
if more, downgrade to judgment ("this file looks deliberately
|
|
||||||
arranged — confirm before reordering"). Three is a soft threshold; if
|
|
||||||
the file's last commit message contains `WIP` or `do-not-sort`, skip.
|
|
||||||
|
|
||||||
**Fix:**
|
|
||||||
|
|
||||||
Reorder the records within the file so each ID family is ascending.
|
|
||||||
Preserve everything else (headings between record blocks, any prose
|
|
||||||
prelude/postlude). Use `Edit` with full block replacement, not in-
|
|
||||||
place line shuffling — easier to verify the diff.
|
|
||||||
|
|
||||||
**Why:**
|
|
||||||
|
|
||||||
Records added at the bottom of a file are easy to write but hard to
|
|
||||||
find. Sorted IDs let a reader find D-37 by jumping to the
|
|
||||||
two-thirds mark of the file rather than scanning. The cost of the
|
|
||||||
sort is one reordering pass; the benefit is paid back on every
|
|
||||||
subsequent read.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## RULE-EOF-NORMALIZATION
|
|
||||||
|
|
||||||
**Category:** mechanical
|
|
||||||
|
|
||||||
**Finding ID:** `<file-path>` (one finding per file).
|
|
||||||
|
|
||||||
**Detection:**
|
|
||||||
|
|
||||||
For each `decisions/*.md` file (and any other markdown the skill
|
|
||||||
touched during this run), read directly. Flag if:
|
|
||||||
|
|
||||||
- File does not end with exactly one `\n`.
|
|
||||||
- Any line contains trailing whitespace before its `\n`.
|
|
||||||
|
|
||||||
**Fix:**
|
|
||||||
|
|
||||||
Trim trailing whitespace from each line. Ensure exactly one trailing
|
|
||||||
newline at end of file. Use `Edit` only if a violation was found —
|
|
||||||
this rule must not produce a no-op diff.
|
|
||||||
|
|
||||||
**Why:**
|
|
||||||
|
|
||||||
Editor and git config drift causes whitespace creep that's invisible
|
|
||||||
in rendering but pollutes diffs (every record edit ends up touching
|
|
||||||
unrelated lines). A periodic normalization keeps future diffs clean
|
|
||||||
without forcing per-editor enforcement on every contributor.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## RULE-SUNSET-WITHOUT-TICKET
|
|
||||||
|
|
||||||
**Category:** judgment
|
|
||||||
|
|
||||||
**Finding ID:** `<record-id>:<phrase-hash-8>` where phrase-hash-8 is
|
|
||||||
the first 8 hex chars of `sha256(<matched-phrase>)`. Lets the
|
|
||||||
ledger distinguish two sunset phrases in the same record.
|
|
||||||
|
|
||||||
**Detection:**
|
|
||||||
|
|
||||||
For each D record body (via `pql decisions read <id>`), grep for
|
|
||||||
sunset-shaped phrases. Default regex set:
|
|
||||||
|
|
||||||
```
|
|
||||||
(?i)\b(delete|remove|sunset|kill[ -]?switch|tear[ -]?down) when\b
|
|
||||||
(?i)\bmust (track|monitor|watch|follow)\b
|
|
||||||
(?i)\brevisit (when|after|once)\b
|
|
||||||
(?i)\b(deprecate|retire) (when|after|once)\b
|
|
||||||
```
|
|
||||||
|
|
||||||
For each match, run `pql ticket list --decision <id>`. If no tickets
|
|
||||||
exist, the D has work-shaped intent without a tracked T — flag.
|
|
||||||
|
|
||||||
**Fix (prompt options):**
|
|
||||||
|
|
||||||
- **File T now** — Run `pql ticket new task "<phrase>" --decision <id>`,
|
|
||||||
then prompt the user for a description (or call `pql ticket refine
|
|
||||||
write <T> --description ...` after creation).
|
|
||||||
- **Draft a T description first** — Open an `AskUserQuestion` for the
|
|
||||||
description, then file as above.
|
|
||||||
- **Mark as already covered** — Add a note to the D body
|
|
||||||
("Tracked under T-N") via `Edit`. Skill does not assert which T;
|
|
||||||
user provides the ID.
|
|
||||||
- **Skip** — Adds to the ledger.
|
|
||||||
- **Stop asking** — Skip remaining sunset findings, summarize.
|
|
||||||
|
|
||||||
**Why:**
|
|
||||||
|
|
||||||
Sunset-shaped intent ("we'll handle X when Y happens") is the most
|
|
||||||
common source of accumulated debt in a DQR system: the trigger
|
|
||||||
condition arrives, nobody remembers the D, the work doesn't happen.
|
|
||||||
Linking each sunset to a T is the simplest defense — a T is a
|
|
||||||
backlog item that surfaces in `plan whatsnext` / `plan board`. The
|
|
||||||
fix is judgment because the right action depends on whether the D's
|
|
||||||
condition is still relevant, whether it's already covered, and what
|
|
||||||
the right scope is for the resulting T.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## RULE-FILE-OVER-THRESHOLD
|
|
||||||
|
|
||||||
**Category:** judgment
|
|
||||||
|
|
||||||
**Finding ID:** `<file-path>` (one finding per file; threshold
|
|
||||||
choice is judgment, not a per-line problem).
|
|
||||||
|
|
||||||
**Detection:**
|
|
||||||
|
|
||||||
For each `decisions/*.md` file, count lines (via `wc -l` or
|
|
||||||
equivalent). Default threshold: **350 lines**. Configurable per
|
|
||||||
project — read `decisions/.clean-house.yaml` if present, key
|
|
||||||
`file_threshold`. Fall back to 350.
|
|
||||||
|
|
||||||
**Fix (prompt options):**
|
|
||||||
|
|
||||||
- **Split now (which axis?)** — Prompt for the split axis: by ID
|
|
||||||
family (D/Q/R), by domain, by date range, custom. Then perform the
|
|
||||||
split: create new file(s), move records, update any anchor links
|
|
||||||
pointing into the moved records (run RULE-ANCHOR-DRIFT in apply
|
|
||||||
mode against the affected files after the split).
|
|
||||||
- **Accept and raise the threshold** — Update
|
|
||||||
`decisions/.clean-house.yaml`'s `file_threshold` to the next
|
|
||||||
reasonable round number above the current line count.
|
|
||||||
- **Defer** — Skip until next run.
|
|
||||||
|
|
||||||
**Why:**
|
|
||||||
|
|
||||||
A single decisions file growing past ~350 lines is the point at which
|
|
||||||
linear scanning starts to lose to grep, and where the cost of
|
|
||||||
splitting (renaming anchors) is still small. Beyond ~600 lines the
|
|
||||||
split cost compounds. The threshold is judgment because some projects
|
|
||||||
deliberately keep one file per domain and accept the size; others
|
|
||||||
split aggressively. The skill surfaces the question; it doesn't
|
|
||||||
decide.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## RULE-DEAD-FILE-REFERENCE
|
|
||||||
|
|
||||||
**Category:** judgment
|
|
||||||
|
|
||||||
**Finding ID:** `<record-id>:<path-token>` (path token as written
|
|
||||||
in the body, not resolved).
|
|
||||||
|
|
||||||
**Detection:**
|
|
||||||
|
|
||||||
For each D record body (via `pql decisions read <id>`), grep for
|
|
||||||
path-shaped tokens. Default regex:
|
|
||||||
|
|
||||||
```
|
|
||||||
\b([\w./-]+\.(md|go|py|sql|yaml|yml|toml))\b
|
|
||||||
```
|
|
||||||
|
|
||||||
The first detection pass produced 6/6 false positives in
|
|
||||||
real-world use; the regex is necessary but not sufficient. For
|
|
||||||
each match, apply the filters below in order — if any matches,
|
|
||||||
**skip without flagging**:
|
|
||||||
|
|
||||||
1. **Placeholder filter.** Token contains `T-NNN`, `D-NNN`,
|
|
||||||
`Q-NNN`, `R-NNN`, `...`, `<`, `>`, or `*` — it's a pattern,
|
|
||||||
not a real path.
|
|
||||||
2. **Source-relative resolution.** Resolve the token against the
|
|
||||||
source file's directory (`<vault>/<file_path>`'s dir). If
|
|
||||||
`[ -e <resolved> ]`, the reference is live.
|
|
||||||
3. **Repo-relative resolution.** If `[ -e <token> ]` from the
|
|
||||||
repo root, the reference is live.
|
|
||||||
4. **Basename-fallback.** Run `find <repo-root> -name <basename>`
|
|
||||||
(where basename is the last path component). If exactly one
|
|
||||||
match exists, treat the reference as live and emit a low-
|
|
||||||
priority "consider rewriting to the absolute path" note (not a
|
|
||||||
judgment finding). If multiple matches exist, do not flag —
|
|
||||||
the token is too ambiguous.
|
|
||||||
|
|
||||||
Only after all four filters miss does the reference qualify as
|
|
||||||
truly dead and warrant a judgment prompt.
|
|
||||||
|
|
||||||
**Fix (prompt options):**
|
|
||||||
|
|
||||||
- **Update reference** — Prompt for the new path; rewrite the
|
|
||||||
reference via `Edit`. If the user types a path, validate it exists
|
|
||||||
before applying.
|
|
||||||
- **Mark superseded** — Add a note to the D ("This decision
|
|
||||||
references files no longer present; the underlying constraint
|
|
||||||
was retired by D-N") and prompt for the superseding D ID.
|
|
||||||
- **Defer** — Skip until next run.
|
|
||||||
|
|
||||||
**Why:**
|
|
||||||
|
|
||||||
Decisions reference code paths to ground their reasoning in the
|
|
||||||
codebase that motivated them. When the code moves or gets deleted,
|
|
||||||
the D's reasoning becomes harder to verify. Dead refs are a signal
|
|
||||||
that either the decision should be updated or the decision itself is
|
|
||||||
no longer load-bearing — both are judgment calls the user needs to
|
|
||||||
make.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## RULE-STALE-OPEN-Q
|
|
||||||
|
|
||||||
**Category:** judgment
|
|
||||||
|
|
||||||
**Finding ID:** `<q-id>` (one finding per Q-record; staleness
|
|
||||||
threshold is global per run, so a Q is either stale or not).
|
|
||||||
|
|
||||||
**Detection:**
|
|
||||||
|
|
||||||
Run `pql decisions list --type question --status open`. For each
|
|
||||||
returned record, check the `date` field. Default staleness threshold:
|
|
||||||
**60 days** (configurable via `decisions/.clean-house.yaml` key
|
|
||||||
`stale_open_q_days`). Flag any Q with `date` older than the
|
|
||||||
threshold.
|
|
||||||
|
|
||||||
**Fix (prompt options):**
|
|
||||||
|
|
||||||
- **Still relevant** — Touch the Q's `date` field to today, optionally
|
|
||||||
add a one-line "still open as of YYYY-MM-DD: <reason>" to the body.
|
|
||||||
- **Nudge to load-bearing index** — If the project keeps a
|
|
||||||
load-bearing Qs list (a curated index of unresolved questions
|
|
||||||
affecting current work), prompt for inclusion and add the Q there.
|
|
||||||
- **Mark withdrawn** — Change the Q's status to `withdrawn` (the DQR
|
|
||||||
system's terminal state for "no longer worth answering"); the
|
|
||||||
user supplies a one-line reason.
|
|
||||||
- **Defer** — Skip until next run.
|
|
||||||
|
|
||||||
**Why:**
|
|
||||||
|
|
||||||
Open questions are useful when they're current; stale ones become
|
|
||||||
ambient noise that drowns the signal of new questions. Most
|
|
||||||
projects don't enforce explicit Q-lifecycle, so without a periodic
|
|
||||||
prompt the open list grows forever. The right action depends on
|
|
||||||
whether the question is still open in fact — not just in metadata —
|
|
||||||
which only the user can confirm.
|
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
---
|
---
|
||||||
name: clide
|
name: clide
|
||||||
description: >
|
description: >
|
||||||
Use when you are running inside the clide IDE and want to observe or drive
|
clide is the IDE hosting this session; it puts `clide` on your PATH and a
|
||||||
its live UI — panes, editor, files, git, readers, toasts, layout — through
|
per-workspace socket in `CLIDE_SOCK`. Start with `clide capabilities` to
|
||||||
the `clide` CLI, or to find out what commands clide exposes. clide is the
|
enumerate the live tool surface, then observe or drive the UI — panes,
|
||||||
IDE hosting this session; it puts `clide` on your PATH and a per-workspace
|
editor, files, git, readers, toasts, layout — via `clide <subsystem> <verb>`.
|
||||||
socket in `CLIDE_SOCK`. Start with `clide capabilities` to enumerate the
|
Triggers: "what can clide do", "drive the clide UI", "open this in clide",
|
||||||
live tool surface. Triggers: "what can clide do", "drive the clide UI",
|
"show the user", "toast", or invoking /clide.
|
||||||
"open this in clide", "show the user", "toast", or invoking /clide.
|
|
||||||
user-invocable: true
|
user-invocable: true
|
||||||
allowed-tools: Bash
|
allowed-tools: Bash
|
||||||
---
|
---
|
||||||
@@ -15,9 +14,10 @@ allowed-tools: Bash
|
|||||||
# Driving clide from the CLI
|
# Driving clide from the CLI
|
||||||
|
|
||||||
You are (often) running **inside clide** — a Flutter IDE that hosts this
|
You are (often) running **inside clide** — a Flutter IDE that hosts this
|
||||||
Claude session. It exposes its whole UI surface as a `clide <subsystem> <verb>`
|
Claude session. It puts `clide` on your PATH and exposes its whole UI surface
|
||||||
CLI on your PATH, talking to the running app over a per-workspace socket
|
as a `clide <subsystem> <verb>` CLI, talking to the running app over a
|
||||||
(`CLIDE_SOCK`). Every UI action the user can take has a CLI verb, and every
|
per-workspace socket (`CLIDE_SOCK`). Every UI action the user can take has a
|
||||||
|
CLI verb, and every
|
||||||
verb's effect is observable — that is the parity contract (D-6). So you can
|
verb's effect is observable — that is the parity contract (D-6). So you can
|
||||||
*see what the user sees* and *show the user what you mean*.
|
*see what the user sees* and *show the user what you mean*.
|
||||||
|
|
||||||
@@ -53,9 +53,11 @@ one). `clide <subsystem>` with no verb, or an unknown command, prints usage.
|
|||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
- **Slots:** the layout has three content slots — `sidebar` (left), `workspace`
|
- **Slots:** the layout has four content slots — `sidebar` (left), `workspace`
|
||||||
(center, where Claude lives), `context` (right) — plus the bottom `statusbar`.
|
(center, where Claude lives), `context` (right), and `dock` (bottom —
|
||||||
Many verbs take a slot.
|
Output/Problems panes, hidden by default; D-87) — plus the bottom
|
||||||
|
`statusbar`. Many verbs take a slot. The live list is whatever
|
||||||
|
`clide capabilities` reports.
|
||||||
- **Honest failures:** a drive verb with no live GUI returns a `toolError`
|
- **Honest failures:** a drive verb with no live GUI returns a `toolError`
|
||||||
("no live UI to drive"), not a hang. JSON on stdout; exit code conveys
|
("no live UI to drive"), not a hang. JSON on stdout; exit code conveys
|
||||||
ok/usage/tool error.
|
ok/usage/tool error.
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
---
|
|
||||||
name: commit
|
|
||||||
description: Create a well-formatted git commit with staged changes
|
|
||||||
---
|
|
||||||
|
|
||||||
# Git Commit
|
|
||||||
|
|
||||||
Create a well-formatted commit with staged changes following best practices.
|
|
||||||
|
|
||||||
## Steps
|
|
||||||
|
|
||||||
1. Run `git status --porcelain` to check for changes
|
|
||||||
2. If no staged changes, show unstaged files and ask what to stage
|
|
||||||
3. Run `git diff --cached` to review staged changes
|
|
||||||
4. Generate a commit message following Conventional Commits format:
|
|
||||||
- `feat:` new feature
|
|
||||||
- `fix:` bug fix
|
|
||||||
- `docs:` documentation
|
|
||||||
- `refactor:` code restructuring
|
|
||||||
- `test:` adding tests
|
|
||||||
- `chore:` maintenance
|
|
||||||
5. Create commit with the message, adding Co-Authored-By trailer
|
|
||||||
|
|
||||||
## Commit Message Format
|
|
||||||
|
|
||||||
```
|
|
||||||
<type>(<scope>): <short description>
|
|
||||||
|
|
||||||
<body - what and why, not how>
|
|
||||||
|
|
||||||
Co-Authored-By: Claude <noreply@anthropic.com>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
- Warn about large commits (>500 lines changed)
|
|
||||||
- Suggest splitting large changes into smaller commits
|
|
||||||
- Never skip pre-commit hooks unless explicitly requested
|
|
||||||
|
|
||||||
## Changelog
|
|
||||||
|
|
||||||
After committing, update `CHANGELOG.md` following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format:
|
|
||||||
|
|
||||||
1. Add entry under `[Unreleased]` section (create if not exists)
|
|
||||||
2. Categorize changes:
|
|
||||||
- `Added` - new features
|
|
||||||
- `Changed` - changes to existing functionality
|
|
||||||
- `Deprecated` - features marked for removal
|
|
||||||
- `Removed` - removed features
|
|
||||||
- `Fixed` - bug fixes
|
|
||||||
- `Security` - security-related changes
|
|
||||||
3. Write entries in imperative mood: "Add feature" not "Added feature"
|
|
||||||
4. Reference issue numbers where applicable
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
---
|
|
||||||
name: d2-diagram
|
|
||||||
description: >
|
|
||||||
Generate technical diagrams using d2 (text-to-diagram CLI). Use when the
|
|
||||||
user says "create a diagram", "draw architecture", "make a flowchart",
|
|
||||||
"diagram this", "render d2", "d2", "data flow diagram", "entity relationship",
|
|
||||||
"state machine", "sequence diagram", "UI flow", or invokes /d2-diagram.
|
|
||||||
Produces .d2 source files and renders them to PNG. Also use when asked
|
|
||||||
to update, re-render, or batch render existing diagrams.
|
|
||||||
---
|
|
||||||
|
|
||||||
# d2 Diagram Generation
|
|
||||||
|
|
||||||
Generate technical diagrams from text using d2 (v0.7.1). Pure CLI, no
|
|
||||||
external dependencies beyond the d2 binary.
|
|
||||||
|
|
||||||
**Binary:** `/home/linuxbrew/.linuxbrew/bin/d2`
|
|
||||||
|
|
||||||
## Project Defaults
|
|
||||||
|
|
||||||
| Setting | Value | Override |
|
|
||||||
|---------|-------|----------|
|
|
||||||
| Theme | 200 (Dark Mauve) | `--theme N` |
|
|
||||||
| Layout | dagre | `--layout elk` |
|
|
||||||
| Padding | 100px | — |
|
|
||||||
| Format | PNG | `--svg` |
|
|
||||||
|
|
||||||
## Output Convention
|
|
||||||
|
|
||||||
```
|
|
||||||
docs/diagrams/
|
|
||||||
architecture/ # System architecture, IPC, component layout
|
|
||||||
data-flow/ # Sequence diagrams, data pipelines
|
|
||||||
entity/ # ER diagrams, ECS component schemas
|
|
||||||
state/ # State machines, behavior trees
|
|
||||||
ui/ # UI navigation flow, screen transitions
|
|
||||||
```
|
|
||||||
|
|
||||||
Both `.d2` source and `.png` output are tracked in git.
|
|
||||||
|
|
||||||
## Single Diagram Workflow
|
|
||||||
|
|
||||||
1. **Determine category** — architecture, data-flow, entity, state, or ui
|
|
||||||
2. **Read template** — `references/diagram-templates.md` for the matching category
|
|
||||||
3. **Read syntax** — `references/d2-syntax-guide.md` if unfamiliar with d2 syntax
|
|
||||||
4. **Write .d2 source** — to `docs/diagrams/{category}/{name}.d2`
|
|
||||||
5. **Validate** — `.claude/skills/d2-diagram/scripts/d2-render.sh validate {file}`
|
|
||||||
6. **Render** — `.claude/skills/d2-diagram/scripts/d2-render.sh {file}`
|
|
||||||
7. **Read SVG** — verify the output, present to user
|
|
||||||
|
|
||||||
### Script Usage
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Render with project defaults
|
|
||||||
.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/architecture/ipc-bridge.d2
|
|
||||||
|
|
||||||
# Validate syntax only
|
|
||||||
.claude/skills/d2-diagram/scripts/d2-render.sh validate docs/diagrams/architecture/ipc-bridge.d2
|
|
||||||
|
|
||||||
# Auto-format source
|
|
||||||
.claude/skills/d2-diagram/scripts/d2-render.sh fmt docs/diagrams/architecture/ipc-bridge.d2
|
|
||||||
|
|
||||||
# Sketch mode (hand-drawn look for drafts)
|
|
||||||
.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/ui/flow.d2 --sketch
|
|
||||||
|
|
||||||
# Light theme (for printable docs)
|
|
||||||
.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/entity/schema.d2 --theme 0
|
|
||||||
|
|
||||||
# SVG output (if specifically needed)
|
|
||||||
.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/architecture/overview.d2 --svg
|
|
||||||
```
|
|
||||||
|
|
||||||
## Batch Render
|
|
||||||
|
|
||||||
Re-render all diagrams after theme or style changes:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# All diagrams
|
|
||||||
.claude/skills/d2-diagram/scripts/d2-batch.sh
|
|
||||||
|
|
||||||
# One category
|
|
||||||
.claude/skills/d2-diagram/scripts/d2-batch.sh docs/diagrams/architecture/
|
|
||||||
|
|
||||||
# Preview what would render
|
|
||||||
.claude/skills/d2-diagram/scripts/d2-batch.sh --dry-run
|
|
||||||
|
|
||||||
# Force re-render everything
|
|
||||||
.claude/skills/d2-diagram/scripts/d2-batch.sh --force
|
|
||||||
```
|
|
||||||
|
|
||||||
Batch skips files whose PNG is newer than the `.d2` source unless `--force`.
|
|
||||||
|
|
||||||
## Advanced Patterns
|
|
||||||
|
|
||||||
### Variables for consistent styling
|
|
||||||
|
|
||||||
```d2
|
|
||||||
vars: {
|
|
||||||
color-bg: "#2a3040"
|
|
||||||
color-stroke: "#333340"
|
|
||||||
color-text: "#c8d0e0"
|
|
||||||
color-accent: "#c8d8f0"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Multi-board (layers)
|
|
||||||
|
|
||||||
```d2
|
|
||||||
# Base diagram here
|
|
||||||
|
|
||||||
layers: {
|
|
||||||
detailed: {
|
|
||||||
# More detailed view
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Sequence diagrams
|
|
||||||
|
|
||||||
```d2
|
|
||||||
shape: sequence_diagram
|
|
||||||
client: Godot Client
|
|
||||||
server: Rust Server
|
|
||||||
|
|
||||||
client -> server: TickRequest(delta)
|
|
||||||
server -> client: WorldState(entities)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Imports
|
|
||||||
|
|
||||||
Split shared definitions into a separate file and import:
|
|
||||||
|
|
||||||
```d2
|
|
||||||
...@shared-defs.d2
|
|
||||||
```
|
|
||||||
|
|
||||||
## Agent Guidance
|
|
||||||
|
|
||||||
- **Qatux** — Architecture decision records, system overview diagrams, data
|
|
||||||
schemas. Prefer architecture and entity templates.
|
|
||||||
- **Tyre** — IPC bridge, ECS system flow, chunk loading pipeline, perception
|
|
||||||
system data flow. Prefer architecture and data-flow templates.
|
|
||||||
- **Araminta** — UI navigation flow, screen transitions, component hierarchy.
|
|
||||||
Prefer UI flow template.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- `references/d2-syntax-guide.md` — Language quick reference (shapes, edges,
|
|
||||||
containers, styling, variables). Read when unfamiliar with d2 syntax.
|
|
||||||
- `references/diagram-templates.md` — Five category templates with complete
|
|
||||||
d2 source examples. Read when starting a new diagram.
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
# D2 Language Quick Reference
|
|
||||||
|
|
||||||
## Nodes
|
|
||||||
|
|
||||||
```d2
|
|
||||||
server # Implicit label from key
|
|
||||||
server: Simulation Server # Explicit label
|
|
||||||
server: Simulation Server { # With properties
|
|
||||||
shape: hexagon
|
|
||||||
style.fill: "#2d3436"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Edges
|
|
||||||
|
|
||||||
```d2
|
|
||||||
a -> b # Directed
|
|
||||||
a <- b # Reverse directed
|
|
||||||
a <-> b # Bidirectional
|
|
||||||
a -- b # Undirected
|
|
||||||
a -> b: "label" # Labeled edge
|
|
||||||
a -> b -> c # Chained
|
|
||||||
```
|
|
||||||
|
|
||||||
## Containers (nesting)
|
|
||||||
|
|
||||||
```d2
|
|
||||||
infrastructure: {
|
|
||||||
server: Simulation Server
|
|
||||||
database: State Store {
|
|
||||||
shape: cylinder
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Shapes
|
|
||||||
|
|
||||||
| Shape | Use for |
|
|
||||||
|-------|---------|
|
|
||||||
| `rectangle` | Default. Components, modules, generic. |
|
|
||||||
| `hexagon` | Systems, services, major components. |
|
|
||||||
| `cylinder` | Databases, storage, persistent state. |
|
|
||||||
| `diamond` | Decisions, conditions, branch points. |
|
|
||||||
| `oval` / `circle` | Start/end states, events. |
|
|
||||||
| `cloud` | External systems, networks. |
|
|
||||||
| `person` | Actors, users, NPCs. |
|
|
||||||
| `queue` | Message queues, buffers. |
|
|
||||||
| `page` | Documents, files. |
|
|
||||||
| `package` | Packages, modules, crates. |
|
|
||||||
| `sql_table` | Database tables, ECS component schemas. |
|
|
||||||
| `class` | Class diagrams, ECS system definitions. |
|
|
||||||
| `code` | Code blocks (set `language` property). |
|
|
||||||
| `markdown` | Rich text blocks. |
|
|
||||||
|
|
||||||
## SQL Tables
|
|
||||||
|
|
||||||
```d2
|
|
||||||
entity: {
|
|
||||||
shape: sql_table
|
|
||||||
id: u64 {constraint: primary_key}
|
|
||||||
name: String
|
|
||||||
position: Vec2
|
|
||||||
faction_id: u64 {constraint: foreign_key}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Class Diagrams
|
|
||||||
|
|
||||||
```d2
|
|
||||||
perception_system: {
|
|
||||||
shape: class
|
|
||||||
+run(world: &mut World)
|
|
||||||
-calculate_los(entity: Entity): HashSet<Entity>
|
|
||||||
#update_knowledge(entity: Entity, seen: HashSet<Entity>)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Sequence Diagrams
|
|
||||||
|
|
||||||
```d2
|
|
||||||
shape: sequence_diagram
|
|
||||||
client: Godot Client
|
|
||||||
server: Rust Server
|
|
||||||
|
|
||||||
client -> server: TickRequest(delta)
|
|
||||||
server -> server: run ECS systems
|
|
||||||
server -> client: WorldState(entities)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Styling
|
|
||||||
|
|
||||||
```d2
|
|
||||||
node: Label {
|
|
||||||
style: {
|
|
||||||
fill: "#2d3436"
|
|
||||||
stroke: "#333340"
|
|
||||||
stroke-width: 2
|
|
||||||
stroke-dash: 5 # Dashed line
|
|
||||||
opacity: 0.8
|
|
||||||
font-size: 14
|
|
||||||
font-color: "#c8d0e0"
|
|
||||||
bold: true
|
|
||||||
italic: false
|
|
||||||
border-radius: 4
|
|
||||||
shadow: true
|
|
||||||
3d: true # Rectangles only
|
|
||||||
multiple: true # Stacked appearance
|
|
||||||
double-border: true # Rectangles/ovals only
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Edge styling
|
|
||||||
|
|
||||||
```d2
|
|
||||||
a -> b: {
|
|
||||||
style: {
|
|
||||||
stroke: "#c8d8f0"
|
|
||||||
stroke-width: 2
|
|
||||||
stroke-dash: 5
|
|
||||||
opacity: 0.8
|
|
||||||
animated: true # Animated flow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Variables
|
|
||||||
|
|
||||||
```d2
|
|
||||||
vars: {
|
|
||||||
color-bg: "#1a1e24"
|
|
||||||
color-stroke: "#333340"
|
|
||||||
color-text: "#c8d0e0"
|
|
||||||
color-accent: "#c8d8f0"
|
|
||||||
}
|
|
||||||
|
|
||||||
node: {
|
|
||||||
style.fill: ${color-bg}
|
|
||||||
style.stroke: ${color-stroke}
|
|
||||||
style.font-color: ${color-text}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Direction
|
|
||||||
|
|
||||||
```d2
|
|
||||||
direction: right # left-to-right (default for dagre)
|
|
||||||
direction: down # top-to-bottom
|
|
||||||
direction: up
|
|
||||||
direction: left
|
|
||||||
```
|
|
||||||
|
|
||||||
## Imports
|
|
||||||
|
|
||||||
```d2
|
|
||||||
...@shared-defs.d2 # Spread import (inline all definitions)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Icons
|
|
||||||
|
|
||||||
```d2
|
|
||||||
node: Label {
|
|
||||||
icon: https://icons.terrastruct.com/essentials/time.svg
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Layers (multi-board)
|
|
||||||
|
|
||||||
```d2
|
|
||||||
# Base diagram content here
|
|
||||||
|
|
||||||
layers: {
|
|
||||||
detailed: {
|
|
||||||
# More detailed view
|
|
||||||
}
|
|
||||||
simplified: {
|
|
||||||
# Simplified overview
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Scenarios (animated transitions)
|
|
||||||
|
|
||||||
```d2
|
|
||||||
# Base state
|
|
||||||
|
|
||||||
scenarios: {
|
|
||||||
alert: {
|
|
||||||
# Changes from base for alert state
|
|
||||||
}
|
|
||||||
combat: {
|
|
||||||
# Changes from base for combat state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Comments
|
|
||||||
|
|
||||||
```d2
|
|
||||||
# This is a comment
|
|
||||||
node: Label # Inline comment
|
|
||||||
```
|
|
||||||
|
|
||||||
## Project Colors (from visual-grammar-v01.md)
|
|
||||||
|
|
||||||
| Constant | Hex | Usage |
|
|
||||||
|----------|-----|-------|
|
|
||||||
| Zone 1 floor | `#1a1e24` | Dark backgrounds |
|
|
||||||
| Zone 1 wall | `#2a3040` | Component fill |
|
|
||||||
| Outline standard | `#333340` | Borders, strokes |
|
|
||||||
| Insert chrome | `#c8d0e0` | Text, labels |
|
|
||||||
| Zone 1 fixture | `#c8d8f0` | Accents, highlights |
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
# Diagram Templates
|
|
||||||
|
|
||||||
Copy, adapt, and render. Each template uses project colors from visual-grammar-v01.md.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Architecture Diagram
|
|
||||||
|
|
||||||
System components, relationships, communication channels.
|
|
||||||
|
|
||||||
**When to use:** IPC bridge, perception pipeline, chunk loading, ECS system layout, client-server architecture.
|
|
||||||
|
|
||||||
**Agents:** Tyre (system architecture), Qatux (architecture decision records).
|
|
||||||
|
|
||||||
```d2
|
|
||||||
vars: {
|
|
||||||
color-bg: "#2a3040"
|
|
||||||
color-stroke: "#333340"
|
|
||||||
color-text: "#c8d0e0"
|
|
||||||
color-accent: "#c8d8f0"
|
|
||||||
}
|
|
||||||
|
|
||||||
direction: right
|
|
||||||
|
|
||||||
client: Godot Client {
|
|
||||||
shape: hexagon
|
|
||||||
style.fill: ${color-bg}
|
|
||||||
style.font-color: ${color-text}
|
|
||||||
|
|
||||||
rendering: Rendering {
|
|
||||||
style.fill: ${color-bg}
|
|
||||||
style.font-color: ${color-text}
|
|
||||||
}
|
|
||||||
ui: UI Layer {
|
|
||||||
style.fill: ${color-bg}
|
|
||||||
style.font-color: ${color-text}
|
|
||||||
}
|
|
||||||
bridge: IPC Bridge {
|
|
||||||
style.fill: ${color-bg}
|
|
||||||
style.font-color: ${color-text}
|
|
||||||
style.stroke: ${color-accent}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
server: Rust Server {
|
|
||||||
shape: hexagon
|
|
||||||
style.fill: ${color-bg}
|
|
||||||
style.font-color: ${color-text}
|
|
||||||
|
|
||||||
ecs: bevy_ecs {
|
|
||||||
style.fill: ${color-bg}
|
|
||||||
style.font-color: ${color-text}
|
|
||||||
}
|
|
||||||
perception: Perception {
|
|
||||||
style.fill: ${color-bg}
|
|
||||||
style.font-color: ${color-text}
|
|
||||||
}
|
|
||||||
bridge: IPC Bridge {
|
|
||||||
style.fill: ${color-bg}
|
|
||||||
style.font-color: ${color-text}
|
|
||||||
style.stroke: ${color-accent}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
client.bridge -> server.bridge: "stdin/stdout" {
|
|
||||||
style.stroke: ${color-accent}
|
|
||||||
style.stroke-dash: 5
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Entity Relationship
|
|
||||||
|
|
||||||
Data schemas, ECS components, knowledge graph structure.
|
|
||||||
|
|
||||||
**When to use:** Database tables, component definitions, entity relationships, knowledge store schema.
|
|
||||||
|
|
||||||
**Agents:** Tyre (ECS component design), Qatux (schema documentation).
|
|
||||||
|
|
||||||
```d2
|
|
||||||
entity: Entity {
|
|
||||||
shape: sql_table
|
|
||||||
id: u64 {constraint: primary_key}
|
|
||||||
name: String
|
|
||||||
faction_id: u64 {constraint: foreign_key}
|
|
||||||
}
|
|
||||||
|
|
||||||
position: Position {
|
|
||||||
shape: sql_table
|
|
||||||
entity_id: u64 {constraint: foreign_key}
|
|
||||||
x: f32
|
|
||||||
y: f32
|
|
||||||
chunk_id: u32
|
|
||||||
}
|
|
||||||
|
|
||||||
knowledge: KnowledgeEntry {
|
|
||||||
shape: sql_table
|
|
||||||
observer_id: u64 {constraint: foreign_key}
|
|
||||||
subject_id: u64 {constraint: foreign_key}
|
|
||||||
fact_type: FactType
|
|
||||||
confidence: f32
|
|
||||||
last_seen_tick: u64
|
|
||||||
}
|
|
||||||
|
|
||||||
entity.id -> position.entity_id
|
|
||||||
entity.id -> knowledge.observer_id
|
|
||||||
entity.id -> knowledge.subject_id
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Sequence / Data Flow
|
|
||||||
|
|
||||||
Ordered interactions between systems over time.
|
|
||||||
|
|
||||||
**When to use:** IPC message flow, tick processing, perception update cycle, dialogue system exchanges.
|
|
||||||
|
|
||||||
**Agents:** Tyre (system interaction design), Qatux (protocol documentation).
|
|
||||||
|
|
||||||
```d2
|
|
||||||
shape: sequence_diagram
|
|
||||||
|
|
||||||
client: Godot Client
|
|
||||||
bridge: IPC Bridge
|
|
||||||
server: Rust Server
|
|
||||||
ecs: ECS Systems
|
|
||||||
|
|
||||||
client -> bridge: TickRequest(delta, input)
|
|
||||||
bridge -> server: deserialize + dispatch
|
|
||||||
server -> ecs: run_systems(delta)
|
|
||||||
ecs -> ecs: perception, AI, physics
|
|
||||||
ecs -> server: collect WorldState
|
|
||||||
server -> bridge: serialize WorldState
|
|
||||||
bridge -> client: WorldState(entities, events)
|
|
||||||
client -> client: update rendering
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. State Machine
|
|
||||||
|
|
||||||
Entity states, transitions, conditions.
|
|
||||||
|
|
||||||
**When to use:** NPC behavior states, game mode transitions, dialogue state, investigation phases.
|
|
||||||
|
|
||||||
**Agents:** Tyre (behavior system design), Qatux (state documentation).
|
|
||||||
|
|
||||||
```d2
|
|
||||||
vars: {
|
|
||||||
color-state: "#2a3040"
|
|
||||||
color-text: "#c8d0e0"
|
|
||||||
color-edge: "#c8d8f0"
|
|
||||||
color-decision: "#333340"
|
|
||||||
}
|
|
||||||
|
|
||||||
idle: Idle {
|
|
||||||
style.fill: ${color-state}
|
|
||||||
style.font-color: ${color-text}
|
|
||||||
}
|
|
||||||
|
|
||||||
alert: Alert {
|
|
||||||
style.fill: ${color-state}
|
|
||||||
style.font-color: ${color-text}
|
|
||||||
}
|
|
||||||
|
|
||||||
investigate: Investigate {
|
|
||||||
style.fill: ${color-state}
|
|
||||||
style.font-color: ${color-text}
|
|
||||||
}
|
|
||||||
|
|
||||||
combat: Combat {
|
|
||||||
style.fill: ${color-state}
|
|
||||||
style.font-color: ${color-text}
|
|
||||||
style.stroke: "#f0b840"
|
|
||||||
}
|
|
||||||
|
|
||||||
flee: Flee {
|
|
||||||
style.fill: ${color-state}
|
|
||||||
style.font-color: ${color-text}
|
|
||||||
}
|
|
||||||
|
|
||||||
idle -> alert: "stimulus detected" { style.stroke: ${color-edge} }
|
|
||||||
alert -> investigate: "stimulus confirmed" { style.stroke: ${color-edge} }
|
|
||||||
alert -> idle: "timeout / stimulus lost" { style.stroke: ${color-edge}; style.stroke-dash: 5 }
|
|
||||||
investigate -> combat: "threat confirmed" { style.stroke: "#f0b840" }
|
|
||||||
investigate -> idle: "nothing found" { style.stroke: ${color-edge}; style.stroke-dash: 5 }
|
|
||||||
combat -> flee: "health < threshold" { style.stroke: "#f0b840" }
|
|
||||||
combat -> idle: "threat eliminated" { style.stroke: ${color-edge}; style.stroke-dash: 5 }
|
|
||||||
flee -> idle: "safe distance reached" { style.stroke: ${color-edge}; style.stroke-dash: 5 }
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. UI Flow
|
|
||||||
|
|
||||||
Screen navigation, component hierarchy, interaction paths.
|
|
||||||
|
|
||||||
**When to use:** HUD layout relationships, menu navigation, dialogue flow, insert mode transitions.
|
|
||||||
|
|
||||||
**Agents:** Araminta (UI/visual design), Tyre (interface architecture), Qatux (UI documentation).
|
|
||||||
|
|
||||||
```d2
|
|
||||||
vars: {
|
|
||||||
color-screen: "#1a1e24"
|
|
||||||
color-panel: "#2a3040"
|
|
||||||
color-text: "#c8d0e0"
|
|
||||||
color-nav: "#c8d8f0"
|
|
||||||
}
|
|
||||||
|
|
||||||
gameplay: Gameplay {
|
|
||||||
style.fill: ${color-screen}
|
|
||||||
style.font-color: ${color-text}
|
|
||||||
|
|
||||||
hud: HUD {
|
|
||||||
style.fill: ${color-panel}
|
|
||||||
style.font-color: ${color-text}
|
|
||||||
|
|
||||||
minimap: Minimap
|
|
||||||
monologue: Monologue Panel
|
|
||||||
insert_display: Insert Display
|
|
||||||
}
|
|
||||||
|
|
||||||
world: World View {
|
|
||||||
style.fill: ${color-panel}
|
|
||||||
style.font-color: ${color-text}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pause: Pause Menu {
|
|
||||||
style.fill: ${color-screen}
|
|
||||||
style.font-color: ${color-text}
|
|
||||||
|
|
||||||
inventory: Inventory
|
|
||||||
journal: Journal
|
|
||||||
settings: Settings
|
|
||||||
}
|
|
||||||
|
|
||||||
dialogue: Dialogue Mode {
|
|
||||||
style.fill: ${color-screen}
|
|
||||||
style.font-color: ${color-text}
|
|
||||||
|
|
||||||
speaker: Speaker Panel
|
|
||||||
responses: Response List
|
|
||||||
}
|
|
||||||
|
|
||||||
gameplay -> pause: "ESC" { style.stroke: ${color-nav} }
|
|
||||||
pause -> gameplay: "ESC / Resume" { style.stroke: ${color-nav}; style.stroke-dash: 5 }
|
|
||||||
gameplay -> dialogue: "interact with NPC" { style.stroke: ${color-nav} }
|
|
||||||
dialogue -> gameplay: "end conversation" { style.stroke: ${color-nav}; style.stroke-dash: 5 }
|
|
||||||
```
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
REPO_ROOT="$(cd "$SCRIPT_DIR" && git rev-parse --show-toplevel)"
|
|
||||||
RENDER="$SCRIPT_DIR/d2-render.sh"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<EOF
|
|
||||||
Usage: $(basename "$0") [directory] [options]
|
|
||||||
|
|
||||||
Batch render all .d2 files in a directory. Skips files whose PNG is
|
|
||||||
newer than the source unless --force is used.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--dry-run List files that would be rendered
|
|
||||||
--force Re-render even if SVG is up to date
|
|
||||||
--theme N Override theme for all files
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
$(basename "$0") # All in docs/diagrams/
|
|
||||||
$(basename "$0") docs/diagrams/architecture/ # One category
|
|
||||||
$(basename "$0") --dry-run # Preview
|
|
||||||
$(basename "$0") --force # Re-render everything
|
|
||||||
EOF
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
DIR="$REPO_ROOT/docs/diagrams"
|
|
||||||
DRY_RUN=false
|
|
||||||
FORCE=false
|
|
||||||
EXTRA_ARGS=()
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--dry-run) DRY_RUN=true; shift ;;
|
|
||||||
--force) FORCE=true; shift ;;
|
|
||||||
--theme) EXTRA_ARGS+=(--theme "$2"); shift 2 ;;
|
|
||||||
--help|-h) usage ;;
|
|
||||||
*)
|
|
||||||
if [[ -d "$1" ]] || [[ -d "$REPO_ROOT/$1" ]]; then
|
|
||||||
DIR="$1"
|
|
||||||
[[ "$DIR" != /* ]] && DIR="$REPO_ROOT/$DIR"
|
|
||||||
else
|
|
||||||
echo "Unknown option or directory: $1" >&2; exit 1
|
|
||||||
fi
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
[[ ! -d "$DIR" ]] && { echo "ERROR: Directory not found: $DIR" >&2; exit 1; }
|
|
||||||
|
|
||||||
RENDERED=0
|
|
||||||
SKIPPED=0
|
|
||||||
FAILED=0
|
|
||||||
|
|
||||||
while IFS= read -r -d '' d2_file; do
|
|
||||||
png_file="${d2_file%.d2}.png"
|
|
||||||
|
|
||||||
# Skip if PNG is newer than source (unless --force)
|
|
||||||
if [[ "$FORCE" != true ]] && [[ -f "$png_file" ]] && [[ "$png_file" -nt "$d2_file" ]]; then
|
|
||||||
SKIPPED=$((SKIPPED + 1))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
rel_path="${d2_file#"$REPO_ROOT/"}"
|
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == true ]]; then
|
|
||||||
echo "Would render: $rel_path"
|
|
||||||
RENDERED=$((RENDERED + 1))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
if "$RENDER" "$d2_file" "${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"}"; then
|
|
||||||
RENDERED=$((RENDERED + 1))
|
|
||||||
else
|
|
||||||
echo "FAILED: $rel_path" >&2
|
|
||||||
FAILED=$((FAILED + 1))
|
|
||||||
fi
|
|
||||||
done < <(find "$DIR" -name '*.d2' -print0 | sort -z)
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Batch complete: $RENDERED rendered, $SKIPPED skipped (up to date), $FAILED failed"
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
D2="/home/linuxbrew/.linuxbrew/bin/d2"
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
REPO_ROOT="$(cd "$SCRIPT_DIR" && git rev-parse --show-toplevel)"
|
|
||||||
|
|
||||||
DEFAULT_THEME=200
|
|
||||||
DEFAULT_LAYOUT="dagre"
|
|
||||||
DEFAULT_PAD=100
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<EOF
|
|
||||||
Usage: $(basename "$0") [validate|fmt] <file.d2> [options]
|
|
||||||
|
|
||||||
Render a .d2 file to PNG with project defaults (theme $DEFAULT_THEME, $DEFAULT_LAYOUT layout).
|
|
||||||
|
|
||||||
Commands:
|
|
||||||
validate <file> Check syntax without rendering
|
|
||||||
fmt <file> Auto-format in place
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--theme N Override theme (default: $DEFAULT_THEME)
|
|
||||||
--layout NAME Override layout engine (default: $DEFAULT_LAYOUT)
|
|
||||||
--sketch Enable hand-drawn sketch mode
|
|
||||||
--output PATH Override output path (default: input with .png extension)
|
|
||||||
--svg Render to SVG instead of PNG
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
$(basename "$0") docs/diagrams/architecture/ipc-bridge.d2
|
|
||||||
$(basename "$0") validate docs/diagrams/architecture/ipc-bridge.d2
|
|
||||||
$(basename "$0") docs/diagrams/architecture/ipc-bridge.d2 --sketch --theme 0
|
|
||||||
EOF
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
[[ $# -lt 1 ]] && usage
|
|
||||||
|
|
||||||
# Parse subcommand
|
|
||||||
SUBCMD=""
|
|
||||||
case "$1" in
|
|
||||||
validate|fmt)
|
|
||||||
SUBCMD="$1"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
[[ $# -lt 1 ]] && usage
|
|
||||||
|
|
||||||
INPUT="$1"
|
|
||||||
shift
|
|
||||||
|
|
||||||
# Resolve to absolute path
|
|
||||||
[[ "$INPUT" != /* ]] && INPUT="$REPO_ROOT/$INPUT"
|
|
||||||
|
|
||||||
[[ ! -f "$INPUT" ]] && { echo "ERROR: File not found: $INPUT" >&2; exit 1; }
|
|
||||||
|
|
||||||
# Handle subcommands
|
|
||||||
if [[ -n "$SUBCMD" ]]; then
|
|
||||||
"$D2" "$SUBCMD" "$INPUT"
|
|
||||||
echo "OK: $SUBCMD $INPUT"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Parse render options
|
|
||||||
THEME="$DEFAULT_THEME"
|
|
||||||
LAYOUT="$DEFAULT_LAYOUT"
|
|
||||||
SKETCH=""
|
|
||||||
OUTPUT=""
|
|
||||||
FORMAT="png"
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--theme) THEME="$2"; shift 2 ;;
|
|
||||||
--layout) LAYOUT="$2"; shift 2 ;;
|
|
||||||
--sketch) SKETCH="-s"; shift ;;
|
|
||||||
--output) OUTPUT="$2"; shift 2 ;;
|
|
||||||
--svg) FORMAT="svg"; shift ;;
|
|
||||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# Derive output path
|
|
||||||
if [[ -z "$OUTPUT" ]]; then
|
|
||||||
OUTPUT="${INPUT%.d2}.$FORMAT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Render
|
|
||||||
"$D2" -t "$THEME" -l "$LAYOUT" --pad "$DEFAULT_PAD" $SKETCH "$INPUT" "$OUTPUT"
|
|
||||||
|
|
||||||
SIZE=$(stat --printf="%s" "$OUTPUT" 2>/dev/null || stat -f%z "$OUTPUT" 2>/dev/null)
|
|
||||||
echo "Rendered: $OUTPUT ($(( SIZE / 1024 ))KB)"
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
---
|
|
||||||
name: frame0-wireframe
|
|
||||||
description: >
|
|
||||||
Create and export UI wireframes using Frame0 (local desktop wireframing
|
|
||||||
app with HTTP API). Use when the user says "create wireframe", "wireframe
|
|
||||||
this", "mock up the UI", "draw a screen", "UI layout", "wireframe the HUD",
|
|
||||||
"Frame0", "frame0", "export wireframe", or invokes /frame0-wireframe.
|
|
||||||
Wireframes are authored as local JSON files (source of truth) and synced
|
|
||||||
to Frame0 for rendering and export. Requires Frame0 to be running locally.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Frame0 Wireframe Generation
|
|
||||||
|
|
||||||
Create UI wireframes as JSON files, sync them to Frame0 for rendering, and
|
|
||||||
export as PNG. Local JSON is the source of truth — Frame0 is the renderer.
|
|
||||||
|
|
||||||
**Frame0 is a renderer, not a workspace.** Treat it as disposable output.
|
|
||||||
Push freely, delete test pages, keep it clean. Never pull from Frame0 unless
|
|
||||||
the user explicitly says they have made edits in Frame0 and want to import
|
|
||||||
them. The pull workflow exists for that case only — do not use it proactively.
|
|
||||||
|
|
||||||
**Prerequisite:** Frame0 desktop app must be running. If not available,
|
|
||||||
stop and inform the user. Point to `references/setup-guide.md`.
|
|
||||||
|
|
||||||
## Health Check
|
|
||||||
|
|
||||||
Always check first:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh health
|
|
||||||
```
|
|
||||||
|
|
||||||
## Core Workflow
|
|
||||||
|
|
||||||
Wireframing is a **design conversation, not a batch job.** Produce **one screen
|
|
||||||
at a time**, show it, and **stop for the user's reaction** before authoring the
|
|
||||||
next. Each shown screen is a checkpoint — never fan out a whole set of
|
|
||||||
wireframes unprompted, even if the user described several screens up front.
|
|
||||||
|
|
||||||
1. **Health check** — verify Frame0 is running
|
|
||||||
2. **Write wireframe JSON** — to `docs/design/wireframes/{category}/{name}.json`.
|
|
||||||
Author **one** screen.
|
|
||||||
3. **Push to Frame0** — `frame0-sync.py push <file.json>`
|
|
||||||
4. **Export PNG** — `frame0-sync.py export <file.json> <output.png>`
|
|
||||||
5. **Show it inline** — surface the exported PNG in the conversation so the
|
|
||||||
user sees the result without opening Frame0. Always do this after an export,
|
|
||||||
and again whenever you re-export or the user asks to see it.
|
|
||||||
- **Inside clide** (the IDE hosting this session — `CLIDE_SOCK` is set):
|
|
||||||
render it as a native image card in the live Claude pane with
|
|
||||||
`clide image show <path> [--caption "…"]` (T-249). This is the *proper*
|
|
||||||
way to put an image in the clide conversation — a bare `Read` only shows
|
|
||||||
it in the transcript, not in the UI the user is looking at.
|
|
||||||
- **Otherwise**: `Read` the PNG so it renders inline in the transcript.
|
|
||||||
6. **STOP and wait for approval** — after showing a screen, pause and ask the
|
|
||||||
user for feedback. Do **not** author, push, or export the next screen until
|
|
||||||
they give an explicit go-ahead. Iterate on the current screen until they're
|
|
||||||
happy, then proceed to the next one — looping back to step 2 for each.
|
|
||||||
7. **Clean up** — delete test/scratch pages from Frame0 when done
|
|
||||||
|
|
||||||
> **Multi-screen requests are still one-at-a-time.** If the user asks for
|
|
||||||
> several screens, treat it as a queue: build the first, show it, get approval,
|
|
||||||
> then move on. The only exception is an explicit, unambiguous instruction to
|
|
||||||
> generate a batch without stopping (and even then prefer the batch-export
|
|
||||||
> dry-run + approval flow below).
|
|
||||||
|
|
||||||
### Scripts
|
|
||||||
|
|
||||||
| Script | Purpose |
|
|
||||||
|--------|---------|
|
|
||||||
| `scripts/frame0-sync.py` | **Primary.** Push/pull/export wireframes between JSON and Frame0 |
|
|
||||||
| `scripts/frame0-cmd.sh` | Low-level API wrapper for ad-hoc commands |
|
|
||||||
|
|
||||||
## Wireframe JSON Format
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "Dialogue Box",
|
|
||||||
"shapes": {
|
|
||||||
"panel": {
|
|
||||||
"type": "Rectangle",
|
|
||||||
"left": 170, "top": 500, "width": 800, "height": 260,
|
|
||||||
"fillColor": "#1a1e24",
|
|
||||||
"strokeColor": "#333340",
|
|
||||||
"corners": [8, 8, 8, 8]
|
|
||||||
},
|
|
||||||
"speaker": {
|
|
||||||
"type": "Text",
|
|
||||||
"parent": "panel",
|
|
||||||
"left": 190, "top": 520,
|
|
||||||
"text": "LERA KONSTANTIN",
|
|
||||||
"fontColor": "#c8d0e0",
|
|
||||||
"fontSize": 16
|
|
||||||
},
|
|
||||||
"btn-ask": {
|
|
||||||
"type": "Rectangle",
|
|
||||||
"parent": "panel",
|
|
||||||
"left": 190, "top": 670, "width": 370, "height": 30,
|
|
||||||
"fillColor": "#2a3040",
|
|
||||||
"strokeColor": "#c8d8f0",
|
|
||||||
"corners": [4, 4, 4, 4]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"connectors": {
|
|
||||||
"flow-1": {
|
|
||||||
"tailId": "panel",
|
|
||||||
"headId": "btn-ask",
|
|
||||||
"strokeColor": "#c8d8f0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key rules
|
|
||||||
|
|
||||||
- **Shape IDs are stable local IDs** you control (e.g. `"panel"`, `"btn-ask"`)
|
|
||||||
- **`parent`** references another local shape ID for nesting
|
|
||||||
- **`type`** uses create-API names: `Rectangle`, `Ellipse`, `Text`, `Line`
|
|
||||||
- **Colors** can be hex (`#2a3040`) or Frame0 theme tokens (`$slate6`)
|
|
||||||
- After a pull, Frame0 returns its native vocabulary (`Box` for Rectangle,
|
|
||||||
theme tokens for colors). The sync script handles the mapping transparently.
|
|
||||||
- The `.idmap.json` mapping file (gitignored) tracks local ID ↔ Frame0 ID
|
|
||||||
|
|
||||||
### Sync commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
SYNC=".claude/skills/frame0-wireframe/scripts/frame0-sync.py"
|
|
||||||
|
|
||||||
# Push local JSON to Frame0 (clears page, recreates all shapes)
|
|
||||||
$SYNC push docs/design/wireframes/dialogue/dialogue-box.json
|
|
||||||
|
|
||||||
# Pull Frame0 page back to local JSON (preserves local IDs via mapping)
|
|
||||||
$SYNC pull "Dialogue Box" docs/design/wireframes/dialogue/dialogue-box.json
|
|
||||||
|
|
||||||
# Push + export as PNG in one step
|
|
||||||
$SYNC export docs/design/wireframes/dialogue/dialogue-box.json \
|
|
||||||
docs/design/wireframes/dialogue/dialogue-box.png
|
|
||||||
```
|
|
||||||
|
|
||||||
### Batch export
|
|
||||||
|
|
||||||
Use this when exporting multiple wireframes. It runs as a single Bash call,
|
|
||||||
avoiding repeated permission prompts.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
BATCH=".claude/skills/frame0-wireframe/scripts/frame0-export-batch.sh"
|
|
||||||
|
|
||||||
# Dry run first — shows full manifest, no Frame0 calls
|
|
||||||
$BATCH --dry-run
|
|
||||||
|
|
||||||
# Export everything (skips PNGs already newer than their JSON)
|
|
||||||
$BATCH
|
|
||||||
|
|
||||||
# Export one category only
|
|
||||||
$BATCH --category dialogue
|
|
||||||
|
|
||||||
# Force re-export of everything
|
|
||||||
$BATCH --force
|
|
||||||
```
|
|
||||||
|
|
||||||
**Always dry-run first, then get approval before running the live export.**
|
|
||||||
|
|
||||||
## Low-Level Commands
|
|
||||||
|
|
||||||
For ad-hoc operations or exec calls not covered by sync:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
CMD=".claude/skills/frame0-wireframe/scripts/frame0-cmd.sh"
|
|
||||||
$CMD health
|
|
||||||
$CMD list-pages
|
|
||||||
$CMD current-page
|
|
||||||
$CMD get-page <page-id>
|
|
||||||
$CMD create-shape Rectangle '{"name":"btn","left":100,"top":100,"width":120,"height":36}'
|
|
||||||
$CMD create-connector <tail-id> <head-id>
|
|
||||||
$CMD move <shape-id> <dx> <dy>
|
|
||||||
$CMD export --format image/png
|
|
||||||
$CMD exec "view:fit-to-screen" '{}'
|
|
||||||
```
|
|
||||||
|
|
||||||
If you find yourself using `exec` for the same command repeatedly, flag it as
|
|
||||||
a candidate for a proper wrapper in `frame0-cmd.sh`.
|
|
||||||
|
|
||||||
## Project Styling Defaults
|
|
||||||
|
|
||||||
Colors from `docs/design/visual-grammar-v01.md`:
|
|
||||||
|
|
||||||
| Role | Hex | Frame0 token |
|
|
||||||
|------|-----|-------------|
|
|
||||||
| Background | `#1a1e24` | `$sage3` |
|
|
||||||
| Stroke | `#333340` | `$slate6` |
|
|
||||||
| Fill | `#2a3040` | `$slate5` |
|
|
||||||
| Text | `#c8d0e0` | `$mint12` |
|
|
||||||
| Accent | `#c8d8f0` | `$blue12` |
|
|
||||||
|
|
||||||
Use hex when authoring new wireframes. Frame0 maps them to theme tokens on push.
|
|
||||||
|
|
||||||
## Output Convention
|
|
||||||
|
|
||||||
```
|
|
||||||
docs/design/wireframes/
|
|
||||||
hud/ # HUD layout wireframes
|
|
||||||
menus/ # Menu screen wireframes
|
|
||||||
dialogue/ # Dialogue box wireframes
|
|
||||||
insert/ # Neural insert wireframes
|
|
||||||
```
|
|
||||||
|
|
||||||
Both `.json` source and `.png` exports are tracked in git.
|
|
||||||
`.idmap.json` mapping files are gitignored.
|
|
||||||
|
|
||||||
## Agent Guidance
|
|
||||||
|
|
||||||
- **Araminta** — Primary user. Full wireframe creation, layout iteration,
|
|
||||||
visual consistency. Uses all component library patterns.
|
|
||||||
- **Tyre** — Interface architecture wireframes. System boundary diagrams.
|
|
||||||
- **Qatux** — Export wireframes for UI decision records and documentation.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- `references/component-library.md` — Pre-built JSON wireframe templates
|
|
||||||
(HUD, dialogue, menus, modals, lists, inventory). Copy and adapt.
|
|
||||||
- `references/api-reference.md` — Frame0 HTTP API command reference and
|
|
||||||
type mappings. Read for low-level control.
|
|
||||||
- `references/setup-guide.md` — Frame0 installation and startup for Fedora.
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
# Frame0 HTTP API Reference
|
|
||||||
|
|
||||||
Frame0 exposes a local HTTP API when the desktop app is running.
|
|
||||||
|
|
||||||
## Endpoint
|
|
||||||
|
|
||||||
```
|
|
||||||
POST http://localhost:{port}/execute_command
|
|
||||||
Content-Type: application/json
|
|
||||||
```
|
|
||||||
|
|
||||||
Default port: **58320** (override via `FRAME0_PORT` env var or `--port` flag).
|
|
||||||
|
|
||||||
## Request / Response
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"command": "namespace:action", "args": { ... }}
|
|
||||||
```
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"success": true, "data": { ... }}
|
|
||||||
{"success": false, "error": "description"}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Type Mapping
|
|
||||||
|
|
||||||
Frame0 uses different type names for create vs get:
|
|
||||||
|
|
||||||
| Create API (`type`) | Get API (internal) | Description |
|
|
||||||
|--------------------|--------------------|-------------|
|
|
||||||
| `Rectangle` | `Box` | Rectangle with optional corners |
|
|
||||||
| `Ellipse` | `Ellipse` | Circle/ellipse |
|
|
||||||
| `Text` | `Text` | Text label |
|
|
||||||
| `Line` | `Line` | Line/polyline |
|
|
||||||
| `Frame` | `Frame` | Container from library |
|
|
||||||
| `Freehand` | `Freehand` | Freehand drawing |
|
|
||||||
| `Highlighter` | `Highlighter` | Highlighter stroke |
|
|
||||||
|
|
||||||
The sync script handles this mapping transparently.
|
|
||||||
|
|
||||||
## Color Tokens
|
|
||||||
|
|
||||||
Frame0 maps hex colors to theme tokens on creation (`convertColors: true`):
|
|
||||||
|
|
||||||
| Hex | Token | Role |
|
|
||||||
|-----|-------|------|
|
|
||||||
| `#1a1e24` | `$sage3` | Background |
|
|
||||||
| `#2a3040` | `$slate5` | Fill |
|
|
||||||
| `#333340` | `$slate6` | Stroke |
|
|
||||||
| `#c8d0e0` | `$mint12` | Text |
|
|
||||||
| `#c8d8f0` | `$blue12` | Accent |
|
|
||||||
|
|
||||||
Both hex and token strings work in the API. Tokens are preserved on round-trip.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
### shape:create-shape
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"command": "shape:create-shape",
|
|
||||||
"args": {
|
|
||||||
"type": "Rectangle",
|
|
||||||
"shapeProps": {
|
|
||||||
"name": "my-button",
|
|
||||||
"left": 100, "top": 200, "width": 120, "height": 36,
|
|
||||||
"fillColor": "#2a3040",
|
|
||||||
"strokeColor": "#c8d8f0",
|
|
||||||
"corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"parentId": "optional-parent-shape-id",
|
|
||||||
"convertColors": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Returns: shape ID (string).
|
|
||||||
|
|
||||||
### shape:get-shape
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"command": "shape:get-shape", "args": {"shapeId": "id"}}
|
|
||||||
```
|
|
||||||
|
|
||||||
### shape:update-shape
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"command": "shape:update-shape",
|
|
||||||
"args": {
|
|
||||||
"shapeId": "id",
|
|
||||||
"shapeProps": {"fillColor": "#1a1e24", "text": "Updated"},
|
|
||||||
"convertColors": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### shape:move
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"command": "shape:move", "args": {"shapeId": "id", "dx": 50, "dy": -20}}
|
|
||||||
```
|
|
||||||
|
|
||||||
### shape:create-connector
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"command": "shape:create-connector",
|
|
||||||
"args": {
|
|
||||||
"tailId": "source-id",
|
|
||||||
"headId": "target-id",
|
|
||||||
"shapeProps": {"strokeColor": "#c8d8f0"},
|
|
||||||
"convertColors": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### shape:create-icon
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"command": "shape:create-icon",
|
|
||||||
"args": {
|
|
||||||
"iconName": "search",
|
|
||||||
"shapeProps": {"left": 100, "top": 100, "width": 24, "height": 24}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### shape:get-available-icons
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"command": "shape:get-available-icons", "args": {}}
|
|
||||||
```
|
|
||||||
|
|
||||||
### shape:group / shape:ungroup
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"command": "shape:group", "args": {"shapeIdArray": ["id1", "id2"]}}
|
|
||||||
{"command": "shape:ungroup", "args": {"shapeIdArray": ["group-id"]}}
|
|
||||||
```
|
|
||||||
|
|
||||||
### edit:delete / edit:duplicate
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"command": "edit:delete", "args": {"shapeIdArray": ["id1", "id2"]}}
|
|
||||||
{"command": "edit:duplicate", "args": {"shapeIdArray": ["id"], "dx": 20, "dy": 0}}
|
|
||||||
```
|
|
||||||
|
|
||||||
### page:add
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"command": "page:add", "args": {"pageProps": {"name": "Page Name"}}}
|
|
||||||
```
|
|
||||||
|
|
||||||
Returns: `{id, type, name}`.
|
|
||||||
|
|
||||||
### page:get
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"command": "page:get", "args": {"pageId": "id", "exportShapes": true}}
|
|
||||||
```
|
|
||||||
|
|
||||||
### page:get-current-page
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"command": "page:get-current-page", "args": {}}
|
|
||||||
```
|
|
||||||
|
|
||||||
Returns: page ID string.
|
|
||||||
|
|
||||||
### page:set-current-page
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"command": "page:set-current-page", "args": {"pageId": "id"}}
|
|
||||||
```
|
|
||||||
|
|
||||||
### doc:get (list all pages)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"command": "doc:get", "args": {"exportPages": true, "exportShapes": false}}
|
|
||||||
```
|
|
||||||
|
|
||||||
### page:delete
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"command": "page:delete", "args": {"pageId": "id"}}
|
|
||||||
```
|
|
||||||
|
|
||||||
### file:export-image
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"command": "file:export-image",
|
|
||||||
"args": {
|
|
||||||
"pageId": "optional-page-id",
|
|
||||||
"format": "image/png",
|
|
||||||
"fillBackground": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Formats: `image/png`, `image/jpeg`, `image/webp`, `image/svg+xml`.
|
|
||||||
Returns: base64-encoded image data.
|
|
||||||
|
|
||||||
### view:fit-to-screen
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"command": "view:fit-to-screen", "args": {}}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Shape Properties
|
|
||||||
|
|
||||||
| Property | Type | Notes |
|
|
||||||
|----------|------|-------|
|
|
||||||
| `name` | string | Shape identifier/label |
|
|
||||||
| `left` | number | X position (origin: top-left) |
|
|
||||||
| `top` | number | Y position |
|
|
||||||
| `width` | number | Width in pixels |
|
|
||||||
| `height` | number | Height in pixels |
|
|
||||||
| `fillColor` | string | Hex or `$token` |
|
|
||||||
| `strokeColor` | string | Hex or `$token` |
|
|
||||||
| `strokeWidth` | number | Border width |
|
|
||||||
| `fontColor` | string | Text color (hex or `$token`) |
|
|
||||||
| `fontSize` | number | Font size in pixels |
|
|
||||||
| `fontFamily` | string | Font name (Frame0 default: `Loranthus`) |
|
|
||||||
| `text` | string | Text content |
|
|
||||||
| `wordWrap` | boolean | Enable word wrapping |
|
|
||||||
| `corners` | number[4] | Border radius [TL, TR, BR, BL] |
|
|
||||||
| `roughness` | number | Sketch roughness (Frame0 default: 1) |
|
|
||||||
| `constraints` | array | Auto-sizing constraints |
|
|
||||||
| `horzAlign` | string | Horizontal text alignment |
|
|
||||||
| `vertAlign` | string | Vertical text alignment |
|
|
||||||
| `fillStyle` | string | Fill style (`none` for transparent) |
|
|
||||||
| `path` | array | Coordinate pairs for lines |
|
|
||||||
@@ -1,467 +0,0 @@
|
|||||||
# Component Library
|
|
||||||
|
|
||||||
Pre-built wireframe JSON templates for The Settled Reach UI. Copy the JSON,
|
|
||||||
adapt positions/sizes, save to `docs/design/wireframes/{category}/`, and push.
|
|
||||||
|
|
||||||
**Viewport:** 1140x780 (Godot project settings)
|
|
||||||
**Grid unit:** 8px
|
|
||||||
**Min touch target:** 36px height
|
|
||||||
**Font sizes:** 12 (label), 14 (body), 16 (subtitle), 18 (heading), 24 (title)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. HUD Layout
|
|
||||||
|
|
||||||
Main gameplay overlay. Minimap top-right, monologue bottom-center,
|
|
||||||
insert display bottom-left, action hints bottom-right.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "HUD Layout",
|
|
||||||
"shapes": {
|
|
||||||
"minimap": {
|
|
||||||
"type": "Rectangle",
|
|
||||||
"left": 880, "top": 20, "width": 240, "height": 240,
|
|
||||||
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
|
||||||
"corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"minimap-label": {
|
|
||||||
"type": "Text",
|
|
||||||
"parent": "minimap",
|
|
||||||
"left": 890, "top": 30,
|
|
||||||
"text": "Minimap",
|
|
||||||
"fontColor": "#c8d0e0", "fontSize": 14
|
|
||||||
},
|
|
||||||
"monologue": {
|
|
||||||
"type": "Rectangle",
|
|
||||||
"left": 300, "top": 680, "width": 520, "height": 80,
|
|
||||||
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
|
||||||
"corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"monologue-text": {
|
|
||||||
"type": "Text",
|
|
||||||
"parent": "monologue",
|
|
||||||
"left": 310, "top": 700, "width": 500,
|
|
||||||
"text": "Internal monologue text appears here...",
|
|
||||||
"fontColor": "#c8d0e0", "fontSize": 13, "wordWrap": true
|
|
||||||
},
|
|
||||||
"insert": {
|
|
||||||
"type": "Rectangle",
|
|
||||||
"left": 20, "top": 600, "width": 260, "height": 160,
|
|
||||||
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
|
||||||
"corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"insert-label": {
|
|
||||||
"type": "Text",
|
|
||||||
"parent": "insert",
|
|
||||||
"left": 30, "top": 620,
|
|
||||||
"text": "Neural Insert Data",
|
|
||||||
"fontColor": "#c8d0e0", "fontSize": 14
|
|
||||||
},
|
|
||||||
"hints": {
|
|
||||||
"type": "Rectangle",
|
|
||||||
"left": 880, "top": 700, "width": 240, "height": 60,
|
|
||||||
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
|
||||||
"corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"hints-label": {
|
|
||||||
"type": "Text",
|
|
||||||
"parent": "hints",
|
|
||||||
"left": 890, "top": 720,
|
|
||||||
"text": "[E] Interact [TAB] Insert",
|
|
||||||
"fontColor": "#c8d0e0", "fontSize": 12
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Dialogue Box
|
|
||||||
|
|
||||||
Speaker panel with response options. Anchored bottom-center during dialogue mode.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "Dialogue Box",
|
|
||||||
"shapes": {
|
|
||||||
"panel": {
|
|
||||||
"type": "Rectangle",
|
|
||||||
"left": 170, "top": 500, "width": 800, "height": 260,
|
|
||||||
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
|
||||||
"corners": [8, 8, 8, 8]
|
|
||||||
},
|
|
||||||
"speaker": {
|
|
||||||
"type": "Text",
|
|
||||||
"parent": "panel",
|
|
||||||
"left": 190, "top": 520,
|
|
||||||
"text": "LERA KONSTANTIN",
|
|
||||||
"fontColor": "#c8d0e0", "fontSize": 16
|
|
||||||
},
|
|
||||||
"text-area": {
|
|
||||||
"type": "Rectangle",
|
|
||||||
"parent": "panel",
|
|
||||||
"left": 190, "top": 550, "width": 760, "height": 100,
|
|
||||||
"fillColor": "#2a3040", "strokeColor": "#333340",
|
|
||||||
"corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"dialogue-text": {
|
|
||||||
"type": "Text",
|
|
||||||
"parent": "text-area",
|
|
||||||
"left": 200, "top": 560, "width": 740,
|
|
||||||
"text": "You look like you could use a drink. First time on the station?",
|
|
||||||
"fontColor": "#c8d0e0", "fontSize": 14, "wordWrap": true
|
|
||||||
},
|
|
||||||
"btn-option1": {
|
|
||||||
"type": "Rectangle",
|
|
||||||
"parent": "panel",
|
|
||||||
"left": 190, "top": 670, "width": 370, "height": 30,
|
|
||||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0",
|
|
||||||
"corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"btn-option1-label": {
|
|
||||||
"type": "Text",
|
|
||||||
"parent": "btn-option1",
|
|
||||||
"left": 200, "top": 674,
|
|
||||||
"text": "[1] Ask about the station",
|
|
||||||
"fontColor": "#c8d8f0", "fontSize": 12
|
|
||||||
},
|
|
||||||
"btn-option2": {
|
|
||||||
"type": "Rectangle",
|
|
||||||
"parent": "panel",
|
|
||||||
"left": 190, "top": 710, "width": 370, "height": 30,
|
|
||||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0",
|
|
||||||
"corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"btn-option2-label": {
|
|
||||||
"type": "Text",
|
|
||||||
"parent": "btn-option2",
|
|
||||||
"left": 200, "top": 714,
|
|
||||||
"text": "[2] Ask about recent events",
|
|
||||||
"fontColor": "#c8d8f0", "fontSize": 12
|
|
||||||
},
|
|
||||||
"btn-leave": {
|
|
||||||
"type": "Rectangle",
|
|
||||||
"parent": "panel",
|
|
||||||
"left": 580, "top": 670, "width": 180, "height": 30,
|
|
||||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0",
|
|
||||||
"corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"btn-leave-label": {
|
|
||||||
"type": "Text",
|
|
||||||
"parent": "btn-leave",
|
|
||||||
"left": 590, "top": 674,
|
|
||||||
"text": "[3] Leave",
|
|
||||||
"fontColor": "#c8d8f0", "fontSize": 12
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Menu Screen
|
|
||||||
|
|
||||||
Full-screen menu with sidebar navigation and content area.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "Pause Menu",
|
|
||||||
"shapes": {
|
|
||||||
"bg": {
|
|
||||||
"type": "Rectangle",
|
|
||||||
"left": 0, "top": 0, "width": 1140, "height": 780,
|
|
||||||
"fillColor": "#1a1e24"
|
|
||||||
},
|
|
||||||
"nav": {
|
|
||||||
"type": "Rectangle",
|
|
||||||
"parent": "bg",
|
|
||||||
"left": 20, "top": 20, "width": 200, "height": 740,
|
|
||||||
"fillColor": "#2a3040", "strokeColor": "#333340",
|
|
||||||
"corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"btn-inventory": {
|
|
||||||
"type": "Rectangle", "parent": "nav",
|
|
||||||
"left": 30, "top": 40, "width": 180, "height": 36,
|
|
||||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"btn-inventory-label": {
|
|
||||||
"type": "Text", "parent": "btn-inventory",
|
|
||||||
"left": 40, "top": 48, "text": "Inventory",
|
|
||||||
"fontColor": "#c8d8f0", "fontSize": 14
|
|
||||||
},
|
|
||||||
"btn-journal": {
|
|
||||||
"type": "Rectangle", "parent": "nav",
|
|
||||||
"left": 30, "top": 86, "width": 180, "height": 36,
|
|
||||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"btn-journal-label": {
|
|
||||||
"type": "Text", "parent": "btn-journal",
|
|
||||||
"left": 40, "top": 94, "text": "Journal",
|
|
||||||
"fontColor": "#c8d8f0", "fontSize": 14
|
|
||||||
},
|
|
||||||
"btn-map": {
|
|
||||||
"type": "Rectangle", "parent": "nav",
|
|
||||||
"left": 30, "top": 132, "width": 180, "height": 36,
|
|
||||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"btn-map-label": {
|
|
||||||
"type": "Text", "parent": "btn-map",
|
|
||||||
"left": 40, "top": 140, "text": "Map",
|
|
||||||
"fontColor": "#c8d8f0", "fontSize": 14
|
|
||||||
},
|
|
||||||
"btn-settings": {
|
|
||||||
"type": "Rectangle", "parent": "nav",
|
|
||||||
"left": 30, "top": 178, "width": 180, "height": 36,
|
|
||||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"btn-settings-label": {
|
|
||||||
"type": "Text", "parent": "btn-settings",
|
|
||||||
"left": 40, "top": 186, "text": "Settings",
|
|
||||||
"fontColor": "#c8d8f0", "fontSize": 14
|
|
||||||
},
|
|
||||||
"btn-resume": {
|
|
||||||
"type": "Rectangle", "parent": "nav",
|
|
||||||
"left": 30, "top": 720, "width": 180, "height": 36,
|
|
||||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"btn-resume-label": {
|
|
||||||
"type": "Text", "parent": "btn-resume",
|
|
||||||
"left": 40, "top": 728, "text": "Resume",
|
|
||||||
"fontColor": "#c8d8f0", "fontSize": 14
|
|
||||||
},
|
|
||||||
"content": {
|
|
||||||
"type": "Rectangle",
|
|
||||||
"parent": "bg",
|
|
||||||
"left": 240, "top": 20, "width": 880, "height": 740,
|
|
||||||
"fillColor": "#2a3040", "strokeColor": "#333340",
|
|
||||||
"corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"content-label": {
|
|
||||||
"type": "Text", "parent": "content",
|
|
||||||
"left": 260, "top": 40,
|
|
||||||
"text": "Content area",
|
|
||||||
"fontColor": "#c8d0e0", "fontSize": 14
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Modal Dialog
|
|
||||||
|
|
||||||
Centered overlay for confirmations, alerts, choices.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "Modal Dialog",
|
|
||||||
"shapes": {
|
|
||||||
"overlay": {
|
|
||||||
"type": "Rectangle",
|
|
||||||
"left": 0, "top": 0, "width": 1140, "height": 780,
|
|
||||||
"fillColor": "#0a0c10"
|
|
||||||
},
|
|
||||||
"modal": {
|
|
||||||
"type": "Rectangle",
|
|
||||||
"parent": "overlay",
|
|
||||||
"left": 320, "top": 240, "width": 500, "height": 300,
|
|
||||||
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
|
||||||
"corners": [8, 8, 8, 8]
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"type": "Text", "parent": "modal",
|
|
||||||
"left": 340, "top": 260,
|
|
||||||
"text": "Confirm Action",
|
|
||||||
"fontColor": "#c8d0e0", "fontSize": 18
|
|
||||||
},
|
|
||||||
"divider": {
|
|
||||||
"type": "Line", "parent": "modal",
|
|
||||||
"left": 340, "top": 290, "width": 460, "height": 0,
|
|
||||||
"strokeColor": "#333340"
|
|
||||||
},
|
|
||||||
"body-1": {
|
|
||||||
"type": "Text", "parent": "modal",
|
|
||||||
"left": 340, "top": 310,
|
|
||||||
"text": "Are you sure you want to proceed?",
|
|
||||||
"fontColor": "#c8d0e0", "fontSize": 14
|
|
||||||
},
|
|
||||||
"body-2": {
|
|
||||||
"type": "Text", "parent": "modal",
|
|
||||||
"left": 340, "top": 340,
|
|
||||||
"text": "This action cannot be undone.",
|
|
||||||
"fontColor": "#c8d0e0", "fontSize": 14
|
|
||||||
},
|
|
||||||
"btn-cancel": {
|
|
||||||
"type": "Rectangle", "parent": "modal",
|
|
||||||
"left": 480, "top": 480, "width": 120, "height": 36,
|
|
||||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"btn-cancel-label": {
|
|
||||||
"type": "Text", "parent": "btn-cancel",
|
|
||||||
"left": 510, "top": 488,
|
|
||||||
"text": "Cancel",
|
|
||||||
"fontColor": "#c8d8f0", "fontSize": 14
|
|
||||||
},
|
|
||||||
"btn-confirm": {
|
|
||||||
"type": "Rectangle", "parent": "modal",
|
|
||||||
"left": 620, "top": 480, "width": 120, "height": 36,
|
|
||||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"btn-confirm-label": {
|
|
||||||
"type": "Text", "parent": "btn-confirm",
|
|
||||||
"left": 645, "top": 488,
|
|
||||||
"text": "Confirm",
|
|
||||||
"fontColor": "#c8d8f0", "fontSize": 14
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. List View
|
|
||||||
|
|
||||||
Scrollable list with item selection and detail panel.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "List View",
|
|
||||||
"shapes": {
|
|
||||||
"list-panel": {
|
|
||||||
"type": "Rectangle",
|
|
||||||
"left": 20, "top": 20, "width": 400, "height": 740,
|
|
||||||
"fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"item-1": {
|
|
||||||
"type": "Rectangle", "parent": "list-panel",
|
|
||||||
"left": 30, "top": 30, "width": 380, "height": 40,
|
|
||||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"item-1-label": {
|
|
||||||
"type": "Text", "parent": "item-1",
|
|
||||||
"left": 40, "top": 38, "text": "Item Alpha",
|
|
||||||
"fontColor": "#c8d8f0", "fontSize": 14
|
|
||||||
},
|
|
||||||
"item-2": {
|
|
||||||
"type": "Rectangle", "parent": "list-panel",
|
|
||||||
"left": 30, "top": 80, "width": 380, "height": 40,
|
|
||||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"item-2-label": {
|
|
||||||
"type": "Text", "parent": "item-2",
|
|
||||||
"left": 40, "top": 88, "text": "Item Beta",
|
|
||||||
"fontColor": "#c8d0e0", "fontSize": 14
|
|
||||||
},
|
|
||||||
"item-3": {
|
|
||||||
"type": "Rectangle", "parent": "list-panel",
|
|
||||||
"left": 30, "top": 130, "width": 380, "height": 40,
|
|
||||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"item-3-label": {
|
|
||||||
"type": "Text", "parent": "item-3",
|
|
||||||
"left": 40, "top": 138, "text": "Item Gamma",
|
|
||||||
"fontColor": "#c8d0e0", "fontSize": 14
|
|
||||||
},
|
|
||||||
"detail-panel": {
|
|
||||||
"type": "Rectangle",
|
|
||||||
"left": 440, "top": 20, "width": 680, "height": 740,
|
|
||||||
"fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"detail-title": {
|
|
||||||
"type": "Text", "parent": "detail-panel",
|
|
||||||
"left": 460, "top": 40,
|
|
||||||
"text": "Item Alpha",
|
|
||||||
"fontColor": "#c8d0e0", "fontSize": 18
|
|
||||||
},
|
|
||||||
"detail-body": {
|
|
||||||
"type": "Text", "parent": "detail-panel",
|
|
||||||
"left": 460, "top": 80, "width": 640,
|
|
||||||
"text": "Description and properties appear here.",
|
|
||||||
"fontColor": "#c8d0e0", "fontSize": 14, "wordWrap": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Inventory Grid
|
|
||||||
|
|
||||||
Grid of cells for item management.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "Inventory Grid",
|
|
||||||
"shapes": {
|
|
||||||
"panel": {
|
|
||||||
"type": "Rectangle",
|
|
||||||
"left": 240, "top": 100, "width": 660, "height": 580,
|
|
||||||
"fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [8, 8, 8, 8]
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"type": "Text", "parent": "panel",
|
|
||||||
"left": 260, "top": 120,
|
|
||||||
"text": "INVENTORY",
|
|
||||||
"fontColor": "#c8d0e0", "fontSize": 18
|
|
||||||
},
|
|
||||||
"cell-1-1": {
|
|
||||||
"type": "Rectangle", "parent": "panel",
|
|
||||||
"left": 260, "top": 160, "width": 64, "height": 64,
|
|
||||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"cell-1-2": {
|
|
||||||
"type": "Rectangle", "parent": "panel",
|
|
||||||
"left": 332, "top": 160, "width": 64, "height": 64,
|
|
||||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"cell-1-3": {
|
|
||||||
"type": "Rectangle", "parent": "panel",
|
|
||||||
"left": 404, "top": 160, "width": 64, "height": 64,
|
|
||||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"cell-1-4": {
|
|
||||||
"type": "Rectangle", "parent": "panel",
|
|
||||||
"left": 476, "top": 160, "width": 64, "height": 64,
|
|
||||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"cell-2-1": {
|
|
||||||
"type": "Rectangle", "parent": "panel",
|
|
||||||
"left": 260, "top": 232, "width": 64, "height": 64,
|
|
||||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"cell-2-2": {
|
|
||||||
"type": "Rectangle", "parent": "panel",
|
|
||||||
"left": 332, "top": 232, "width": 64, "height": 64,
|
|
||||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"cell-2-3": {
|
|
||||||
"type": "Rectangle", "parent": "panel",
|
|
||||||
"left": 404, "top": 232, "width": 64, "height": 64,
|
|
||||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"cell-2-4": {
|
|
||||||
"type": "Rectangle", "parent": "panel",
|
|
||||||
"left": 476, "top": 232, "width": 64, "height": 64,
|
|
||||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"detail": {
|
|
||||||
"type": "Rectangle", "parent": "panel",
|
|
||||||
"left": 580, "top": 160, "width": 300, "height": 400,
|
|
||||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
|
||||||
},
|
|
||||||
"detail-title": {
|
|
||||||
"type": "Text", "parent": "detail",
|
|
||||||
"left": 600, "top": 180,
|
|
||||||
"text": "Selected Item Name",
|
|
||||||
"fontColor": "#c8d0e0", "fontSize": 16
|
|
||||||
},
|
|
||||||
"detail-body": {
|
|
||||||
"type": "Text", "parent": "detail",
|
|
||||||
"left": 600, "top": 210, "width": 260,
|
|
||||||
"text": "Item description and stats",
|
|
||||||
"fontColor": "#c8d0e0", "fontSize": 14, "wordWrap": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# Frame0 Setup Guide
|
|
||||||
|
|
||||||
## Installation (Fedora)
|
|
||||||
|
|
||||||
Download from https://frame0.app/download and install the RPM:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo dnf install ./frame0-*.x86_64.rpm
|
|
||||||
```
|
|
||||||
|
|
||||||
Requires: Fedora 40 or later (x86_64).
|
|
||||||
|
|
||||||
## Starting Frame0
|
|
||||||
|
|
||||||
Launch the desktop application:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
frame0 &
|
|
||||||
```
|
|
||||||
|
|
||||||
Frame0 exposes an HTTP API at `localhost:58320` when running.
|
|
||||||
|
|
||||||
## Verify API Access
|
|
||||||
|
|
||||||
```bash
|
|
||||||
.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh health
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected output: `Frame0 is running on port 58320`
|
|
||||||
|
|
||||||
## Port Configuration
|
|
||||||
|
|
||||||
Default port: **58320**
|
|
||||||
|
|
||||||
To use a different port, set the environment variable:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export FRAME0_PORT=58321
|
|
||||||
```
|
|
||||||
|
|
||||||
Or pass `--port` to any script:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh --port 58321 health
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
| Symptom | Cause | Fix |
|
|
||||||
|---------|-------|-----|
|
|
||||||
| "Connection refused" | Frame0 not running | Start the desktop app |
|
|
||||||
| "Port in use" | Another instance running | Close duplicate or use different port |
|
|
||||||
| Script hangs | API unresponsive | Restart Frame0 |
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
PORT="${FRAME0_PORT:-58320}"
|
|
||||||
ENDPOINT="http://localhost:${PORT}/execute_command"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<EOF
|
|
||||||
Usage: $(basename "$0") <command> [args...] [--port N]
|
|
||||||
|
|
||||||
Low-level Frame0 HTTP API wrapper. Replaces the MCP server with direct
|
|
||||||
curl calls. Requires Frame0 desktop app to be running.
|
|
||||||
|
|
||||||
Commands:
|
|
||||||
health Check if Frame0 is running
|
|
||||||
exec <namespace:action> <json> Execute a raw API command
|
|
||||||
create-shape <type> <json-props> Create a shape (Rectangle, Ellipse, Text, Line)
|
|
||||||
get-shape <id> Get shape details
|
|
||||||
update-shape <id> <json-props> Update shape properties
|
|
||||||
delete <id> [id...] Delete shapes by ID
|
|
||||||
move <id> <dx> <dy> Move a shape by pixel offset
|
|
||||||
duplicate <id> Duplicate a shape
|
|
||||||
group <id> [id...] Group shapes
|
|
||||||
ungroup <group-id> Ungroup a group
|
|
||||||
create-connector <tail-id> <head-id> [json-props] Connect two shapes
|
|
||||||
create-icon <name> <json-props> Create an icon shape
|
|
||||||
add-page <name> Add a new page (becomes current)
|
|
||||||
get-page [page-id] Get current or specific page data
|
|
||||||
list-pages [--shapes] List all pages (--shapes for shape data)
|
|
||||||
current-page Get current page ID
|
|
||||||
set-page <page-id> Set current page
|
|
||||||
export [page-id] [--format mime] Export page as image (default: image/png)
|
|
||||||
fit Fit view to screen
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--port N Frame0 API port (default: $PORT, env: FRAME0_PORT)
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
$(basename "$0") health
|
|
||||||
$(basename "$0") add-page "HUD Layout"
|
|
||||||
$(basename "$0") create-shape Rectangle '{"name":"btn","left":100,"top":100,"width":120,"height":36}'
|
|
||||||
$(basename "$0") list-pages
|
|
||||||
$(basename "$0") export --format image/png
|
|
||||||
EOF
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Parse --port from anywhere in args
|
|
||||||
ARGS=()
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--port) PORT="$2"; ENDPOINT="http://localhost:${PORT}/execute_command"; shift 2 ;;
|
|
||||||
*) ARGS+=("$1"); shift ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
set -- "${ARGS[@]+"${ARGS[@]}"}"
|
|
||||||
|
|
||||||
[[ $# -lt 1 ]] && usage
|
|
||||||
|
|
||||||
# Execute a Frame0 API command, return data or error
|
|
||||||
frame0_exec() {
|
|
||||||
local command="$1"
|
|
||||||
local args
|
|
||||||
args="${2:-"{}"}"
|
|
||||||
|
|
||||||
local response
|
|
||||||
response=$(curl -s -w "\n%{http_code}" -X POST "$ENDPOINT" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{\"command\": \"$command\", \"args\": $args}" 2>&1) || {
|
|
||||||
echo "ERROR: Cannot connect to Frame0 at localhost:$PORT" >&2
|
|
||||||
echo "Is Frame0 running? See: .claude/skills/frame0-wireframe/references/setup-guide.md" >&2
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
local http_code body
|
|
||||||
http_code=$(echo "$response" | tail -1)
|
|
||||||
body=$(echo "$response" | sed '$d')
|
|
||||||
|
|
||||||
if [[ "$http_code" != 2* ]]; then
|
|
||||||
echo "ERROR: HTTP $http_code from Frame0" >&2
|
|
||||||
echo "$body" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Parse success/error from response
|
|
||||||
python3 -c "
|
|
||||||
import sys, json
|
|
||||||
try:
|
|
||||||
r = json.loads(sys.stdin.read())
|
|
||||||
if r.get('success'):
|
|
||||||
d = r.get('data')
|
|
||||||
if d is not None:
|
|
||||||
print(json.dumps(d, indent=2))
|
|
||||||
else:
|
|
||||||
print('ERROR: ' + str(r.get('error', 'Unknown error')), file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
print(f'ERROR: Invalid JSON response: {e}', file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
" <<< "$body"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Build JSON array from remaining args
|
|
||||||
ids_to_json_array() {
|
|
||||||
local arr="["
|
|
||||||
local first=true
|
|
||||||
for id in "$@"; do
|
|
||||||
[[ "$first" == true ]] && first=false || arr+=","
|
|
||||||
arr+="\"$id\""
|
|
||||||
done
|
|
||||||
arr+="]"
|
|
||||||
echo "$arr"
|
|
||||||
}
|
|
||||||
|
|
||||||
CMD="${1:-}"
|
|
||||||
shift || true
|
|
||||||
|
|
||||||
case "$CMD" in
|
|
||||||
health)
|
|
||||||
if curl -s -o /dev/null -w "%{http_code}" "http://localhost:${PORT}/" 2>/dev/null | grep -q "^[23]"; then
|
|
||||||
echo "Frame0 is running on port $PORT"
|
|
||||||
else
|
|
||||||
echo "Frame0 is NOT running on port $PORT" >&2
|
|
||||||
echo "Start Frame0 desktop app, then retry." >&2
|
|
||||||
echo "See: .claude/skills/frame0-wireframe/references/setup-guide.md" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
|
|
||||||
exec)
|
|
||||||
[[ $# -lt 2 ]] && { echo "Usage: exec <command> <json-args>" >&2; exit 1; }
|
|
||||||
frame0_exec "$1" "$2"
|
|
||||||
;;
|
|
||||||
|
|
||||||
create-shape)
|
|
||||||
[[ $# -lt 2 ]] && { echo "Usage: create-shape <Type> <json-props>" >&2; exit 1; }
|
|
||||||
local_type="$1"
|
|
||||||
local_props="$2"
|
|
||||||
local_parent="${3:-}"
|
|
||||||
local_parent_arg=""
|
|
||||||
[[ -n "$local_parent" ]] && local_parent_arg=", \"parentId\": \"$local_parent\""
|
|
||||||
frame0_exec "shape:create-shape" "{\"type\": \"$local_type\", \"shapeProps\": $local_props$local_parent_arg, \"convertColors\": true}"
|
|
||||||
;;
|
|
||||||
|
|
||||||
get-shape)
|
|
||||||
[[ $# -lt 1 ]] && { echo "Usage: get-shape <id>" >&2; exit 1; }
|
|
||||||
frame0_exec "shape:get-shape" "{\"shapeId\": \"$1\"}"
|
|
||||||
;;
|
|
||||||
|
|
||||||
update-shape)
|
|
||||||
[[ $# -lt 2 ]] && { echo "Usage: update-shape <id> <json-props>" >&2; exit 1; }
|
|
||||||
frame0_exec "shape:update-shape" "{\"shapeId\": \"$1\", \"shapeProps\": $2, \"convertColors\": true}"
|
|
||||||
;;
|
|
||||||
|
|
||||||
delete)
|
|
||||||
[[ $# -lt 1 ]] && { echo "Usage: delete <id> [id...]" >&2; exit 1; }
|
|
||||||
local_arr=$(ids_to_json_array "$@")
|
|
||||||
frame0_exec "edit:delete" "{\"shapeIdArray\": $local_arr}"
|
|
||||||
;;
|
|
||||||
|
|
||||||
move)
|
|
||||||
[[ $# -lt 3 ]] && { echo "Usage: move <id> <dx> <dy>" >&2; exit 1; }
|
|
||||||
frame0_exec "shape:move" "{\"shapeId\": \"$1\", \"dx\": $2, \"dy\": $3}"
|
|
||||||
;;
|
|
||||||
|
|
||||||
duplicate)
|
|
||||||
[[ $# -lt 1 ]] && { echo "Usage: duplicate <id> [dx] [dy]" >&2; exit 1; }
|
|
||||||
local_dx="${2:-0}"
|
|
||||||
local_dy="${3:-0}"
|
|
||||||
frame0_exec "edit:duplicate" "{\"shapeIdArray\": [\"$1\"], \"dx\": $local_dx, \"dy\": $local_dy}"
|
|
||||||
;;
|
|
||||||
|
|
||||||
group)
|
|
||||||
[[ $# -lt 2 ]] && { echo "Usage: group <id> <id> [id...]" >&2; exit 1; }
|
|
||||||
local_arr=$(ids_to_json_array "$@")
|
|
||||||
frame0_exec "shape:group" "{\"shapeIdArray\": $local_arr}"
|
|
||||||
;;
|
|
||||||
|
|
||||||
ungroup)
|
|
||||||
[[ $# -lt 1 ]] && { echo "Usage: ungroup <group-id>" >&2; exit 1; }
|
|
||||||
frame0_exec "shape:ungroup" "{\"shapeIdArray\": [\"$1\"]}"
|
|
||||||
;;
|
|
||||||
|
|
||||||
create-connector)
|
|
||||||
[[ $# -lt 2 ]] && { echo "Usage: create-connector <tail-id> <head-id> [json-props]" >&2; exit 1; }
|
|
||||||
local_props="${3:-{}}"
|
|
||||||
frame0_exec "shape:create-connector" "{\"tailId\": \"$1\", \"headId\": \"$2\", \"shapeProps\": $local_props, \"convertColors\": true}"
|
|
||||||
;;
|
|
||||||
|
|
||||||
create-icon)
|
|
||||||
[[ $# -lt 2 ]] && { echo "Usage: create-icon <name> <json-props>" >&2; exit 1; }
|
|
||||||
frame0_exec "shape:create-icon" "{\"iconName\": \"$1\", \"shapeProps\": $2, \"convertColors\": true}"
|
|
||||||
;;
|
|
||||||
|
|
||||||
add-page)
|
|
||||||
[[ $# -lt 1 ]] && { echo "Usage: add-page <name>" >&2; exit 1; }
|
|
||||||
frame0_exec "page:add" "{\"pageProps\": {\"name\": \"$1\"}}"
|
|
||||||
;;
|
|
||||||
|
|
||||||
get-page)
|
|
||||||
if [[ $# -ge 1 ]]; then
|
|
||||||
frame0_exec "page:get" "{\"pageId\": \"$1\", \"exportShapes\": true}"
|
|
||||||
else
|
|
||||||
local_id
|
|
||||||
local_id=$(frame0_exec "page:get-current-page")
|
|
||||||
# Strip quotes from returned ID
|
|
||||||
local_id=$(echo "$local_id" | tr -d '"')
|
|
||||||
frame0_exec "page:get" "{\"pageId\": \"$local_id\", \"exportShapes\": true}"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
|
|
||||||
list-pages)
|
|
||||||
local_shapes="false"
|
|
||||||
[[ "${1:-}" == "--shapes" ]] && local_shapes="true"
|
|
||||||
frame0_exec "doc:get" "{\"exportPages\": true, \"exportShapes\": $local_shapes}"
|
|
||||||
;;
|
|
||||||
|
|
||||||
current-page)
|
|
||||||
frame0_exec "page:get-current-page"
|
|
||||||
;;
|
|
||||||
|
|
||||||
set-page)
|
|
||||||
[[ $# -lt 1 ]] && { echo "Usage: set-page <page-id>" >&2; exit 1; }
|
|
||||||
frame0_exec "page:set-current-page" "{\"pageId\": \"$1\"}"
|
|
||||||
;;
|
|
||||||
|
|
||||||
export)
|
|
||||||
local_page_id=""
|
|
||||||
local_format="image/png"
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--format) local_format="$2"; shift 2 ;;
|
|
||||||
*) local_page_id="$1"; shift ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
local_page_arg=""
|
|
||||||
[[ -n "$local_page_id" ]] && local_page_arg="\"pageId\": \"$local_page_id\", "
|
|
||||||
frame0_exec "file:export-image" "{${local_page_arg}\"format\": \"$local_format\", \"fillBackground\": true}"
|
|
||||||
;;
|
|
||||||
|
|
||||||
fit)
|
|
||||||
frame0_exec "view:fit-to-screen"
|
|
||||||
;;
|
|
||||||
|
|
||||||
--help|-h|help)
|
|
||||||
usage
|
|
||||||
;;
|
|
||||||
|
|
||||||
*)
|
|
||||||
echo "Unknown command: $CMD" >&2
|
|
||||||
usage
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Batch export wireframes from JSON to PNG via Frame0.
|
|
||||||
#
|
|
||||||
# Finds all .json wireframe files under docs/design/wireframes/ and exports
|
|
||||||
# each to a matching .png. Skips files whose PNG is already newer than the
|
|
||||||
# JSON, unless --force is passed.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# frame0-export-batch.sh [--dry-run] [--force] [--category CAT] [--root DIR]
|
|
||||||
#
|
|
||||||
# Options:
|
|
||||||
# --dry-run Print manifest only, don't touch Frame0.
|
|
||||||
# --force Re-export even if PNG already exists and is up to date.
|
|
||||||
# --category CAT Limit to one subdirectory (e.g. --category dialogue)
|
|
||||||
# --root DIR Wireframes root dir (default: docs/design/wireframes)
|
|
||||||
#
|
|
||||||
# Exit codes:
|
|
||||||
# 0 All exports succeeded (or nothing to do)
|
|
||||||
# 1 One or more exports failed
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
|
||||||
SYNC="$SCRIPT_DIR/frame0-sync.py"
|
|
||||||
DEFAULT_ROOT="$REPO_ROOT/docs/design/wireframes"
|
|
||||||
|
|
||||||
DRY_RUN=false
|
|
||||||
FORCE=false
|
|
||||||
CATEGORY=""
|
|
||||||
WF_ROOT="$DEFAULT_ROOT"
|
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--dry-run) DRY_RUN=true; shift ;;
|
|
||||||
--force) FORCE=true; shift ;;
|
|
||||||
--category) CATEGORY="$2"; shift 2 ;;
|
|
||||||
--root) WF_ROOT="$2"; shift 2 ;;
|
|
||||||
-h|--help)
|
|
||||||
sed -n '/^# /p' "$0" | sed 's/^# //'
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ ! -d "$WF_ROOT" ]]; then
|
|
||||||
echo "ERROR: Wireframes directory not found: $WF_ROOT" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Collect JSON files, optionally filtered by category subdirectory
|
|
||||||
mapfile -t JSON_FILES < <(
|
|
||||||
if [[ -n "$CATEGORY" ]]; then
|
|
||||||
find "$WF_ROOT/$CATEGORY" -name "*.json" ! -name ".*" | sort
|
|
||||||
else
|
|
||||||
find "$WF_ROOT" -name "*.json" ! -name ".*" | sort
|
|
||||||
fi
|
|
||||||
)
|
|
||||||
|
|
||||||
if [[ ${#JSON_FILES[@]} -eq 0 ]]; then
|
|
||||||
echo "No wireframe JSON files found."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Classify files into to-export and to-skip
|
|
||||||
TO_EXPORT=()
|
|
||||||
TO_SKIP=()
|
|
||||||
|
|
||||||
for json in "${JSON_FILES[@]}"; do
|
|
||||||
png="${json%.json}.png"
|
|
||||||
if $FORCE || [[ ! -f "$png" ]] || [[ "$json" -nt "$png" ]]; then
|
|
||||||
TO_EXPORT+=("$json")
|
|
||||||
else
|
|
||||||
TO_SKIP+=("$json")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Print manifest
|
|
||||||
if [[ ${#TO_EXPORT[@]} -gt 0 ]]; then
|
|
||||||
echo ""
|
|
||||||
echo "Will export (${#TO_EXPORT[@]} files):"
|
|
||||||
for json in "${TO_EXPORT[@]}"; do
|
|
||||||
png="${json%.json}.png"
|
|
||||||
rel="${json#$REPO_ROOT/}"
|
|
||||||
if [[ ! -f "$png" ]]; then
|
|
||||||
status="new"
|
|
||||||
else
|
|
||||||
status="updated"
|
|
||||||
fi
|
|
||||||
printf " [%-7s] %s\n" "$status" "$rel"
|
|
||||||
done
|
|
||||||
else
|
|
||||||
echo ""
|
|
||||||
echo "Nothing to export (all PNGs up to date)."
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ${#TO_SKIP[@]} -gt 0 ]]; then
|
|
||||||
echo ""
|
|
||||||
echo "Will skip (${#TO_SKIP[@]} files already up to date):"
|
|
||||||
for json in "${TO_SKIP[@]}"; do
|
|
||||||
rel="${json#$REPO_ROOT/}"
|
|
||||||
printf " [skip ] %s\n" "$rel"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
if $DRY_RUN; then
|
|
||||||
echo ""
|
|
||||||
echo "Dry run — no exports performed."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ${#TO_EXPORT[@]} -eq 0 ]]; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
PASSED=0
|
|
||||||
FAILED=0
|
|
||||||
FAILED_FILES=()
|
|
||||||
|
|
||||||
TOTAL=${#TO_EXPORT[@]}
|
|
||||||
IDX=0
|
|
||||||
|
|
||||||
for json in "${TO_EXPORT[@]}"; do
|
|
||||||
IDX=$((IDX + 1))
|
|
||||||
png="${json%.json}.png"
|
|
||||||
rel="${json#$REPO_ROOT/}"
|
|
||||||
|
|
||||||
printf "[%d/%d] %s ... " "$IDX" "$TOTAL" "$rel"
|
|
||||||
|
|
||||||
output=$(python3 "$SYNC" export "$json" "$png" 2>/tmp/frame0-batch-err.txt)
|
|
||||||
rc=$?
|
|
||||||
if [[ $rc -eq 0 ]]; then
|
|
||||||
size=$(echo "$output" | tail -1 | grep -oP '\(\K[^)]+' || true)
|
|
||||||
echo "ok $size"
|
|
||||||
PASSED=$((PASSED + 1))
|
|
||||||
else
|
|
||||||
echo "FAILED"
|
|
||||||
cat /tmp/frame0-batch-err.txt >&2
|
|
||||||
FAILED=$((FAILED + 1))
|
|
||||||
FAILED_FILES+=("$rel")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "$PASSED exported, $FAILED failed."
|
|
||||||
|
|
||||||
if [[ $FAILED -gt 0 ]]; then
|
|
||||||
echo ""
|
|
||||||
echo "Failed:" >&2
|
|
||||||
for f in "${FAILED_FILES[@]}"; do
|
|
||||||
echo " $f" >&2
|
|
||||||
done
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
@@ -1,414 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Frame0 sync: push local JSON wireframes to Frame0, pull pages back.
|
|
||||||
|
|
||||||
Local JSON is source of truth. Frame0 is a renderer.
|
|
||||||
A mapping file tracks local_id <-> frame0_id across push/pull cycles.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
frame0-sync.py push <wireframe.json> [--port PORT]
|
|
||||||
frame0-sync.py pull <page-id|page-name> <output.json> [--port PORT]
|
|
||||||
frame0-sync.py export <wireframe.json> <output.png> [--port PORT] [--format MIME]
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import urllib.request
|
|
||||||
import urllib.error
|
|
||||||
|
|
||||||
DEFAULT_PORT = 58320
|
|
||||||
|
|
||||||
|
|
||||||
def api(port, command, args=None):
|
|
||||||
"""Execute a Frame0 API command. Returns the data field on success."""
|
|
||||||
url = f"http://localhost:{port}/execute_command"
|
|
||||||
payload = json.dumps({"command": command, "args": args or {}}).encode()
|
|
||||||
req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"})
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(req) as resp:
|
|
||||||
result = json.loads(resp.read())
|
|
||||||
except urllib.error.HTTPError as e:
|
|
||||||
body = e.read().decode()
|
|
||||||
print(f"ERROR: {command}: HTTP {e.code}: {body[:500]}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
except urllib.error.URLError as e:
|
|
||||||
print(f"ERROR: Cannot connect to Frame0 on port {port}: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if not result.get("success"):
|
|
||||||
print(f"ERROR: {command}: {result.get('error', 'unknown')}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
return result.get("data")
|
|
||||||
|
|
||||||
|
|
||||||
# -- Mapping file management --------------------------------------------------
|
|
||||||
|
|
||||||
def mapping_path(wireframe_path):
|
|
||||||
"""Derive the idmap path from the wireframe JSON path."""
|
|
||||||
d = os.path.dirname(wireframe_path)
|
|
||||||
base = os.path.splitext(os.path.basename(wireframe_path))[0]
|
|
||||||
return os.path.join(d, f".{base}.idmap.json")
|
|
||||||
|
|
||||||
|
|
||||||
def load_mapping(wireframe_path):
|
|
||||||
p = mapping_path(wireframe_path)
|
|
||||||
if os.path.exists(p):
|
|
||||||
with open(p) as f:
|
|
||||||
return json.load(f)
|
|
||||||
return {"page_id": None, "shapes": {}, "connectors": {}}
|
|
||||||
|
|
||||||
|
|
||||||
def save_mapping(wireframe_path, mapping):
|
|
||||||
p = mapping_path(wireframe_path)
|
|
||||||
os.makedirs(os.path.dirname(p) or ".", exist_ok=True)
|
|
||||||
with open(p, "w") as f:
|
|
||||||
json.dump(mapping, f, indent=2)
|
|
||||||
f.write("\n")
|
|
||||||
|
|
||||||
|
|
||||||
# -- Push: local JSON -> Frame0 -----------------------------------------------
|
|
||||||
|
|
||||||
# Frame0 returns different type names from get vs what create accepts.
|
|
||||||
# Map internal types back to create-API types.
|
|
||||||
TYPE_TO_CREATE = {
|
|
||||||
"Box": "Rectangle",
|
|
||||||
"box": "Rectangle",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def topo_sort_shapes(shapes):
|
|
||||||
"""Sort shape IDs so parents come before children."""
|
|
||||||
order = []
|
|
||||||
visited = set()
|
|
||||||
|
|
||||||
def visit(sid):
|
|
||||||
if sid in visited:
|
|
||||||
return
|
|
||||||
visited.add(sid)
|
|
||||||
shape = shapes[sid]
|
|
||||||
parent = shape.get("parent")
|
|
||||||
if parent and parent in shapes:
|
|
||||||
visit(parent)
|
|
||||||
order.append(sid)
|
|
||||||
|
|
||||||
for sid in shapes:
|
|
||||||
visit(sid)
|
|
||||||
return order
|
|
||||||
|
|
||||||
|
|
||||||
def find_or_create_page(port, name, mapping):
|
|
||||||
"""Find existing page by mapping or name, or create a new one."""
|
|
||||||
# Try mapped page_id first
|
|
||||||
if mapping.get("page_id"):
|
|
||||||
try:
|
|
||||||
page = api(port, "page:get", {"pageId": mapping["page_id"]})
|
|
||||||
if page:
|
|
||||||
return mapping["page_id"]
|
|
||||||
except SystemExit:
|
|
||||||
pass # Page no longer exists, fall through
|
|
||||||
|
|
||||||
# Search by name in existing pages
|
|
||||||
doc = api(port, "doc:get", {"exportPages": True, "exportShapes": False})
|
|
||||||
if doc and "children" in doc:
|
|
||||||
for page in doc["children"]:
|
|
||||||
if page.get("name") == name:
|
|
||||||
return page["id"]
|
|
||||||
|
|
||||||
# Create new page
|
|
||||||
page = api(port, "page:add", {"pageProps": {"name": name}})
|
|
||||||
return page["id"]
|
|
||||||
|
|
||||||
|
|
||||||
def clear_page(port, page_id):
|
|
||||||
"""Delete all shapes on a page."""
|
|
||||||
page = api(port, "page:get", {"pageId": page_id, "exportShapes": True})
|
|
||||||
if not page or "children" not in page:
|
|
||||||
return
|
|
||||||
shape_ids = [s["id"] for s in page["children"]]
|
|
||||||
if shape_ids:
|
|
||||||
api(port, "edit:delete", {"shapeIdArray": shape_ids})
|
|
||||||
|
|
||||||
|
|
||||||
def push(wireframe_path, port):
|
|
||||||
"""Push local wireframe JSON to Frame0."""
|
|
||||||
with open(wireframe_path) as f:
|
|
||||||
wireframe = json.load(f)
|
|
||||||
|
|
||||||
name = wireframe.get("name", os.path.splitext(os.path.basename(wireframe_path))[0])
|
|
||||||
shapes = wireframe.get("shapes", {})
|
|
||||||
connectors = wireframe.get("connectors", {})
|
|
||||||
|
|
||||||
mapping = load_mapping(wireframe_path)
|
|
||||||
|
|
||||||
# Find or create page
|
|
||||||
page_id = find_or_create_page(port, name, mapping)
|
|
||||||
mapping["page_id"] = page_id
|
|
||||||
|
|
||||||
# Switch to page and clear it
|
|
||||||
api(port, "page:set-current-page", {"pageId": page_id})
|
|
||||||
clear_page(port, page_id)
|
|
||||||
|
|
||||||
# Reset ID mappings (shapes are recreated)
|
|
||||||
mapping["shapes"] = {}
|
|
||||||
mapping["connectors"] = {}
|
|
||||||
|
|
||||||
# Create shapes in dependency order
|
|
||||||
order = topo_sort_shapes(shapes)
|
|
||||||
for local_id in order:
|
|
||||||
shape = shapes[local_id]
|
|
||||||
raw_type = shape.get("type", "Rectangle")
|
|
||||||
shape_type = TYPE_TO_CREATE.get(raw_type, raw_type)
|
|
||||||
parent_local = shape.get("parent")
|
|
||||||
|
|
||||||
# Build shapeProps from everything except meta fields
|
|
||||||
meta_keys = {"type", "parent"}
|
|
||||||
props = {k: v for k, v in shape.items() if k not in meta_keys}
|
|
||||||
|
|
||||||
# Set name to local_id if not explicitly set
|
|
||||||
if "name" not in props:
|
|
||||||
props["name"] = local_id
|
|
||||||
|
|
||||||
create_args = {
|
|
||||||
"type": shape_type,
|
|
||||||
"shapeProps": props,
|
|
||||||
"convertColors": True,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Resolve parent ID
|
|
||||||
if parent_local and parent_local in mapping["shapes"]:
|
|
||||||
create_args["parentId"] = mapping["shapes"][parent_local]
|
|
||||||
|
|
||||||
f0_id = api(port, "shape:create-shape", create_args)
|
|
||||||
mapping["shapes"][local_id] = f0_id
|
|
||||||
|
|
||||||
# Create connectors
|
|
||||||
for local_id, conn in connectors.items():
|
|
||||||
tail_local = conn.get("tailId")
|
|
||||||
head_local = conn.get("headId")
|
|
||||||
|
|
||||||
if tail_local not in mapping["shapes"] or head_local not in mapping["shapes"]:
|
|
||||||
print(f"WARNING: connector '{local_id}' references unknown shape, skipping", file=sys.stderr)
|
|
||||||
continue
|
|
||||||
|
|
||||||
meta_keys = {"tailId", "headId"}
|
|
||||||
props = {k: v for k, v in conn.items() if k not in meta_keys}
|
|
||||||
if "name" not in props:
|
|
||||||
props["name"] = local_id
|
|
||||||
|
|
||||||
f0_id = api(port, "shape:create-connector", {
|
|
||||||
"tailId": mapping["shapes"][tail_local],
|
|
||||||
"headId": mapping["shapes"][head_local],
|
|
||||||
"shapeProps": props,
|
|
||||||
"convertColors": True,
|
|
||||||
})
|
|
||||||
mapping["connectors"][local_id] = f0_id
|
|
||||||
|
|
||||||
# Fit to screen
|
|
||||||
api(port, "view:fit-to-screen")
|
|
||||||
|
|
||||||
save_mapping(wireframe_path, mapping)
|
|
||||||
total = len(mapping["shapes"]) + len(mapping["connectors"])
|
|
||||||
print(f"Pushed '{name}' to Frame0: {len(mapping['shapes'])} shapes, {len(mapping['connectors'])} connectors")
|
|
||||||
|
|
||||||
|
|
||||||
# -- Pull: Frame0 -> local JSON -----------------------------------------------
|
|
||||||
|
|
||||||
def pull(page_ref, output_path, port):
|
|
||||||
"""Pull a Frame0 page into local wireframe JSON."""
|
|
||||||
# Resolve page_ref: could be an ID or a name
|
|
||||||
page_id = None
|
|
||||||
doc = api(port, "doc:get", {"exportPages": True, "exportShapes": False})
|
|
||||||
if doc and "children" in doc:
|
|
||||||
for page in doc["children"]:
|
|
||||||
if page["id"] == page_ref or page.get("name") == page_ref:
|
|
||||||
page_id = page["id"]
|
|
||||||
page_name = page.get("name", page_ref)
|
|
||||||
break
|
|
||||||
|
|
||||||
if not page_id:
|
|
||||||
print(f"ERROR: Page not found: {page_ref}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Load existing mapping for reverse lookup
|
|
||||||
mapping = load_mapping(output_path)
|
|
||||||
reverse_map = {v: k for k, v in mapping.get("shapes", {}).items()}
|
|
||||||
reverse_conn = {v: k for k, v in mapping.get("connectors", {}).items()}
|
|
||||||
|
|
||||||
# Get full page with shapes
|
|
||||||
page = api(port, "page:get", {"pageId": page_id, "exportShapes": True})
|
|
||||||
|
|
||||||
shapes = {}
|
|
||||||
connectors = {}
|
|
||||||
new_mapping = {"page_id": page_id, "shapes": {}, "connectors": {}}
|
|
||||||
auto_id_counter = [0]
|
|
||||||
|
|
||||||
def auto_id(f0_shape):
|
|
||||||
"""Generate a stable local ID from shape name or auto-number."""
|
|
||||||
# Prefer existing mapping
|
|
||||||
f0_id = f0_shape["id"]
|
|
||||||
if f0_id in reverse_map:
|
|
||||||
return reverse_map[f0_id]
|
|
||||||
# Use sanitized name
|
|
||||||
name = f0_shape.get("name", "")
|
|
||||||
if name:
|
|
||||||
sanitized = name.lower().replace(" ", "-").replace("_", "-")
|
|
||||||
if sanitized not in shapes:
|
|
||||||
return sanitized
|
|
||||||
# Fallback: auto-number
|
|
||||||
auto_id_counter[0] += 1
|
|
||||||
return f"s{auto_id_counter[0]:03d}"
|
|
||||||
|
|
||||||
def process_shape(f0_shape, parent_local_id=None):
|
|
||||||
f0_id = f0_shape["id"]
|
|
||||||
local_id = auto_id(f0_shape)
|
|
||||||
new_mapping["shapes"][local_id] = f0_id
|
|
||||||
|
|
||||||
# Extract shape properties — only strip structural keys that our
|
|
||||||
# ID mapping replaces. Everything else passes through as-is so the
|
|
||||||
# local JSON speaks Frame0's native vocabulary.
|
|
||||||
shape_type = f0_shape.get("type", "Box")
|
|
||||||
skip_keys = {"id", "type", "children", "pageId", "parentId"}
|
|
||||||
props = {k: v for k, v in f0_shape.items() if k not in skip_keys and v is not None}
|
|
||||||
|
|
||||||
entry = {"type": shape_type}
|
|
||||||
if parent_local_id:
|
|
||||||
entry["parent"] = parent_local_id
|
|
||||||
entry.update(props)
|
|
||||||
|
|
||||||
# Remove name if it matches local_id (redundant)
|
|
||||||
if entry.get("name") == local_id:
|
|
||||||
del entry["name"]
|
|
||||||
|
|
||||||
shapes[local_id] = entry
|
|
||||||
|
|
||||||
# Process children recursively
|
|
||||||
for child in f0_shape.get("children", []):
|
|
||||||
child_type = child.get("type", "")
|
|
||||||
if child_type == "Connector":
|
|
||||||
process_connector(child)
|
|
||||||
else:
|
|
||||||
process_shape(child, local_id)
|
|
||||||
|
|
||||||
def process_connector(f0_conn):
|
|
||||||
f0_id = f0_conn["id"]
|
|
||||||
local_id = reverse_conn.get(f0_id)
|
|
||||||
if not local_id:
|
|
||||||
auto_id_counter[0] += 1
|
|
||||||
local_id = f"c{auto_id_counter[0]:03d}"
|
|
||||||
|
|
||||||
new_mapping["connectors"][local_id] = f0_id
|
|
||||||
|
|
||||||
tail_f0 = f0_conn.get("tail", {}).get("id")
|
|
||||||
head_f0 = f0_conn.get("head", {}).get("id")
|
|
||||||
|
|
||||||
entry = {}
|
|
||||||
if tail_f0:
|
|
||||||
# Will be resolved after all shapes are processed
|
|
||||||
entry["_tailF0"] = tail_f0
|
|
||||||
if head_f0:
|
|
||||||
entry["_headF0"] = head_f0
|
|
||||||
|
|
||||||
skip_keys = {"id", "type", "children", "pageId", "tail", "head"}
|
|
||||||
props = {k: v for k, v in f0_conn.items() if k not in skip_keys and v is not None}
|
|
||||||
entry.update(props)
|
|
||||||
|
|
||||||
connectors[local_id] = entry
|
|
||||||
|
|
||||||
# Process all top-level shapes
|
|
||||||
for child in page.get("children", []):
|
|
||||||
child_type = child.get("type", "")
|
|
||||||
if child_type == "Connector":
|
|
||||||
process_connector(child)
|
|
||||||
else:
|
|
||||||
process_shape(child)
|
|
||||||
|
|
||||||
# Resolve connector references to local IDs
|
|
||||||
f0_to_local = {v: k for k, v in new_mapping["shapes"].items()}
|
|
||||||
for conn in connectors.values():
|
|
||||||
tail_f0 = conn.pop("_tailF0", None)
|
|
||||||
head_f0 = conn.pop("_headF0", None)
|
|
||||||
if tail_f0 and tail_f0 in f0_to_local:
|
|
||||||
conn["tailId"] = f0_to_local[tail_f0]
|
|
||||||
if head_f0 and head_f0 in f0_to_local:
|
|
||||||
conn["headId"] = f0_to_local[head_f0]
|
|
||||||
|
|
||||||
wireframe = {"name": page_name}
|
|
||||||
if shapes:
|
|
||||||
wireframe["shapes"] = shapes
|
|
||||||
if connectors:
|
|
||||||
wireframe["connectors"] = connectors
|
|
||||||
|
|
||||||
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
|
|
||||||
with open(output_path, "w") as f:
|
|
||||||
json.dump(wireframe, f, indent=2)
|
|
||||||
f.write("\n")
|
|
||||||
|
|
||||||
save_mapping(output_path, new_mapping)
|
|
||||||
print(f"Pulled '{page_name}' -> {output_path}: {len(shapes)} shapes, {len(connectors)} connectors")
|
|
||||||
|
|
||||||
|
|
||||||
# -- Export: push then export as image -----------------------------------------
|
|
||||||
|
|
||||||
def export_image(wireframe_path, output_path, port, fmt="image/png"):
|
|
||||||
"""Push wireframe to Frame0 and export the page as an image."""
|
|
||||||
import base64
|
|
||||||
|
|
||||||
# Push first to ensure Frame0 is up to date
|
|
||||||
push(wireframe_path, port)
|
|
||||||
|
|
||||||
mapping = load_mapping(wireframe_path)
|
|
||||||
page_id = mapping.get("page_id")
|
|
||||||
if not page_id:
|
|
||||||
print("ERROR: No page_id in mapping after push", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
image_b64 = api(port, "file:export-image", {
|
|
||||||
"pageId": page_id,
|
|
||||||
"format": fmt,
|
|
||||||
"fillBackground": True,
|
|
||||||
})
|
|
||||||
|
|
||||||
image_bytes = base64.b64decode(image_b64)
|
|
||||||
with open(output_path, "wb") as f:
|
|
||||||
f.write(image_bytes)
|
|
||||||
|
|
||||||
print(f"Exported: {output_path} ({len(image_bytes) // 1024}KB)")
|
|
||||||
|
|
||||||
|
|
||||||
# -- CLI -----------------------------------------------------------------------
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(description="Sync wireframe JSON with Frame0")
|
|
||||||
parser.add_argument("--port", type=int, default=int(os.environ.get("FRAME0_PORT", DEFAULT_PORT)))
|
|
||||||
sub = parser.add_subparsers(dest="command")
|
|
||||||
|
|
||||||
p_push = sub.add_parser("push", help="Push local JSON to Frame0")
|
|
||||||
p_push.add_argument("wireframe", help="Path to wireframe .json file")
|
|
||||||
|
|
||||||
p_pull = sub.add_parser("pull", help="Pull Frame0 page to local JSON")
|
|
||||||
p_pull.add_argument("page", help="Page ID or page name")
|
|
||||||
p_pull.add_argument("output", help="Output .json path")
|
|
||||||
|
|
||||||
p_export = sub.add_parser("export", help="Push and export as image")
|
|
||||||
p_export.add_argument("wireframe", help="Path to wireframe .json file")
|
|
||||||
p_export.add_argument("output", help="Output image path (e.g. wireframe.png)")
|
|
||||||
p_export.add_argument("--format", default="image/png",
|
|
||||||
help="Export MIME type (default: image/png)")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.command == "push":
|
|
||||||
push(args.wireframe, args.port)
|
|
||||||
elif args.command == "pull":
|
|
||||||
pull(args.page, args.output, args.port)
|
|
||||||
elif args.command == "export":
|
|
||||||
export_image(args.wireframe, args.output, args.port, args.format)
|
|
||||||
else:
|
|
||||||
parser.print_help()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -14,29 +14,40 @@ Follow these conventions whenever you create a commit in this repository. These
|
|||||||
|
|
||||||
## Message style
|
## Message style
|
||||||
|
|
||||||
- **First line:** imperative mood, ≤ 70 characters. Examples: `add sidecar PTY scaffold`, `fix IPC reconnect after app reload`, `update CLI exit-code contract`.
|
This repo uses [Conventional Commits 1.0](https://www.conventionalcommits.org/en/v1.0.0/) (per [D-37](../../../governance/decisions/process.md#d-37)).
|
||||||
|
|
||||||
|
- **First line:** `type(scope): imperative subject`, ≤ 72 characters **including** the prefix. Examples: `feat(settings): add Appearance font picker (T-460)`, `fix(ipc): reconnect after app reload`, `docs(readme): drop brittle version line`.
|
||||||
|
- **Type:** one of `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `chore`. Use `feat`/`fix` for user-visible behavior; `chore` for bookkeeping (`chore(plan)` is the convention for pql ticket housekeeping). Append `!` after the scope for a breaking change (`feat(ipc)!: …`).
|
||||||
|
- **Scope (optional but preferred):** the subsystem the change lives in — `settings`, `vim`, `pty`, `git`, `plan`, etc. Lower-case, no spaces.
|
||||||
|
- **Ticket ref:** keep a trailing `(T-NNN)` on the subject when the work has a ticket — `feat(settings): category rail + navigation (T-447)`.
|
||||||
- **Body (optional):** wrap at ~72 chars. Explain the *why* — the reason this change exists. The diff already shows the *what*; don't restate it in prose.
|
- **Body (optional):** wrap at ~72 chars. Explain the *why* — the reason this change exists. The diff already shows the *what*; don't restate it in prose.
|
||||||
- **No emojis.** Anywhere.
|
- **No emojis.** Anywhere.
|
||||||
- **Don't prefix with types** like `feat:` or `fix:` — this repo isn't Conventional Commits. (The Python-era clide under `legacy/` used Conventional Commits; the Flutter rebuild at the repo root does not.)
|
|
||||||
- **Don't reference the current task or flow** (`for the v2.0 milestone`, `used by the canvas panel`) — that context belongs in the PR description and rots as the repo evolves.
|
- **Don't reference the current task or flow** (`for the v2.0 milestone`, `used by the canvas panel`) — that context belongs in the PR description and rots as the repo evolves.
|
||||||
- **Naming:** the project is `clide`. The Flutter desktop app lives at the repo root; the Go sidecar/CLI binary is `clide`. The supporter project is `pql` (referenced, not part of this repo). The archived Python implementation lives under `legacy/`.
|
- **Naming:** the project is `clide`. The Flutter desktop app lives at the repo root; the `clide` CLI is a thin C client (`native/clide-cli/`). The supporter project is `pql` (referenced, not part of this repo). The archived Python implementation lives under `legacy/`.
|
||||||
|
|
||||||
## Logically-separated commits
|
## Logically-separated commits
|
||||||
|
|
||||||
Default to **one logical change per commit**, even when a lot of work lands in the tree at once. When there's a pile of uncommitted or untracked files:
|
Default to **one logical change per commit**, even when a lot of work lands in the tree at once. When there's a pile of uncommitted or untracked files:
|
||||||
|
|
||||||
1. **Read `git status` + `git diff` first** — never stage the whole tree blind.
|
1. **Read `git status` + `git diff` first** — never stage the whole tree blind.
|
||||||
2. **Group by concern**, not by file location. Typical concerns to separate:
|
2. **Triage: session vs pre-existing.** Separate changes you made in this
|
||||||
|
session from changes that were already dirty (or that a concurrent session
|
||||||
|
is producing — this worktree can host several Claude sessions at once).
|
||||||
|
Check the conversation history — if you didn't touch a file, it's not yours
|
||||||
|
to stage. After grouping your own commits, surface any remaining modified
|
||||||
|
or untracked files to the user and ask whether they belong in a commit.
|
||||||
|
Never silently skip or silently include changes that aren't yours.
|
||||||
|
3. **Group by concern**, not by file location. Typical concerns to separate:
|
||||||
- **Bookkeeping** — `.gitignore`, editor/IDE config, lockfiles, `go.sum` / `pubspec.lock` updates.
|
- **Bookkeeping** — `.gitignore`, editor/IDE config, lockfiles, `go.sum` / `pubspec.lock` updates.
|
||||||
- **Documentation** — `README.md`, `CLAUDE.md`, ADRs under `docs/ADRs/`, design notes under `docs/`.
|
- **Documentation** — `README.md`, `CLAUDE.md`, D-records under `governance/`, design notes under `docs/`.
|
||||||
- **General-purpose tooling / skills** — things that aren't project-specific (reusable skills, shared scripts).
|
- **General-purpose tooling / skills** — things that aren't project-specific (reusable skills, shared scripts).
|
||||||
- **Project-specific conventions** — this repo's own rules.
|
- **Project-specific conventions** — this repo's own rules.
|
||||||
- **Feature or subsystem** — one cohesive change per commit; a sidecar change and an app change for the same feature can land together, but two unrelated features should split.
|
- **Feature or subsystem** — one cohesive change per commit; a CLI change and an app change for the same feature can land together, but two unrelated features should split.
|
||||||
- **Layer changes** — Flutter app, sidecar CLI, sidecar daemon, IPC server, pql wrapper, canvas driver, git panel — separate concerns; prefer separate commits when the changes are independent.
|
- **Layer changes** — Flutter app, `clide` CLI (`native/clide-cli/`), IPC server, pql wrapper, canvas driver, git panel — separate concerns; prefer separate commits when the changes are independent.
|
||||||
3. **Sequence the commits** so each one is cleanly scoped, but don't obsess about whether each intermediate commit "works" — for scaffolding PRs it's fine if the full picture only snaps together at the end.
|
4. **Sequence the commits** so each one is cleanly scoped, but don't obsess about whether each intermediate commit "works" — for scaffolding PRs it's fine if the full picture only snaps together at the end.
|
||||||
4. **Prefer many small focused commits over one large mixed one** — a reviewer can read, revert, or cherry-pick a focused commit; they can't do any of those to a blob.
|
5. **Prefer many small focused commits over one large mixed one** — a reviewer can read, revert, or cherry-pick a focused commit; they can't do any of those to a blob.
|
||||||
5. **Use `git add <specific paths>`** — never `git add -A` or `git add .` when splitting, or you'll sweep in the next commit's work by accident.
|
6. **Use `git add <specific paths>`** — never `git add -A` or `git add .` when splitting, or you'll sweep in the next commit's work by accident.
|
||||||
6. **Verify between commits** with `git status` and `git log -1` to confirm the split landed as intended.
|
7. **Verify between commits** with `git status` and `git log -1` to confirm the split landed as intended.
|
||||||
|
|
||||||
Corollary: if a commit's subject line needs the word "and" to be accurate, it probably should have been two commits.
|
Corollary: if a commit's subject line needs the word "and" to be accurate, it probably should have been two commits.
|
||||||
|
|
||||||
@@ -95,6 +106,7 @@ Cutting a release is its own commit. In a single commit:
|
|||||||
3. Bump `pubspec.yaml` `version:` to `X.Y.Z` (drop the `-dev` suffix for the tag; re-add it on the next development commit if desired).
|
3. Bump `pubspec.yaml` `version:` to `X.Y.Z` (drop the `-dev` suffix for the tag; re-add it on the next development commit if desired).
|
||||||
4. Run `make gen-build-info` so `assets/licenses.yaml` `self.version:` re-syncs from pubspec (it's auto-rewritten by every build but commit the fresh state). Stage the resulting diff alongside step 3.
|
4. Run `make gen-build-info` so `assets/licenses.yaml` `self.version:` re-syncs from pubspec (it's auto-rewritten by every build but commit the fresh state). Stage the resulting diff alongside step 3.
|
||||||
5. Commit subject: `release vX.Y.Z`.
|
5. Commit subject: `release vX.Y.Z`.
|
||||||
|
6. **Tag it.** After the commit lands, run `make release` — it verifies the version/changelog/clean-tree invariants, runs the full gate, and creates the annotated `vX.Y.Z` tag. Then `git push origin main --follow-tags` to publish the commit and the tag together. **Every released version must have a matching `git tag`** — a CHANGELOG heading without a tag is an incomplete release (T-393: tagging had silently lapsed from v2.2.0 through v2.8.0). Don't hand-roll the tag; `make release` keeps the tag, gate, and version in lockstep. If you tag by hand, use an annotated tag (`git tag -a vX.Y.Z -m "clide vX.Y.Z"`); never push a release commit without its tag.
|
||||||
|
|
||||||
`pubspec.yaml` is the single source of truth for the version. Every `make` build/run/test target regenerates `lib/src/build_info.g.dart` (gitignored) and rewrites `assets/licenses.yaml` `self.version:` from it — so the Flutter app sees the current version everywhere without manual sync. Bumping `pubspec.yaml` and the changelog out of sync is the mistake this rule prevents.
|
`pubspec.yaml` is the single source of truth for the version. Every `make` build/run/test target regenerates `lib/src/build_info.g.dart` (gitignored) and rewrites `assets/licenses.yaml` `self.version:` from it — so the Flutter app sees the current version everywhere without manual sync. Bumping `pubspec.yaml` and the changelog out of sync is the mistake this rule prevents.
|
||||||
|
|
||||||
@@ -108,29 +120,32 @@ Co-Authored-By: Claude <noreply@anthropic.com>
|
|||||||
|
|
||||||
The model-identifier variant produced by the Claude Code harness (e.g. `Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>`) is also accepted — don't rewrite it if the harness emits that form.
|
The model-identifier variant produced by the Claude Code harness (e.g. `Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>`) is also accepted — don't rewrite it if the harness emits that form.
|
||||||
|
|
||||||
## HEREDOC discipline
|
## Message-file discipline
|
||||||
|
|
||||||
Pass commit messages via HEREDOC so multi-line formatting survives:
|
Pass commit messages via a message file so multi-line formatting survives
|
||||||
|
and the command stays inside the permission allowlist (command substitution
|
||||||
|
defeats the `git commit` prefix match and triggers permission prompts):
|
||||||
|
|
||||||
|
1. Write the full message to a file in `/tmp` (never inside `.git/` — that
|
||||||
|
directory is git's own state) using the Write tool.
|
||||||
|
2. Commit with it:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git commit -m "$(cat <<'EOF'
|
git commit -F /tmp/commit-msg.txt
|
||||||
short imperative summary
|
|
||||||
|
|
||||||
Optional longer body explaining why this change was needed,
|
|
||||||
wrapped at about 72 characters.
|
|
||||||
|
|
||||||
Co-Authored-By: Claude <noreply@anthropic.com>
|
|
||||||
EOF
|
|
||||||
)"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Never pass multi-line messages via `-m "line1\nline2"` or multiple `-m` flags — Git's behavior differs between shells and quoting regimes and the trailer can end up in the wrong place.
|
Fallback only: the HEREDOC-in-substitution form
|
||||||
|
(`git commit -m "$(cat <<'EOF' … EOF)"`) works but prompts for permission —
|
||||||
|
use it only when writing a file is impossible. Never pass multi-line messages
|
||||||
|
via `-m "line1\nline2"` or multiple `-m` flags — Git's behavior differs
|
||||||
|
between shells and quoting regimes and the trailer can end up in the wrong
|
||||||
|
place.
|
||||||
|
|
||||||
## What not to commit
|
## What not to commit
|
||||||
|
|
||||||
- `.env` and any `*.env.local` — see `.gitignore`.
|
- `.env` and any `*.env.local` — see `.gitignore`.
|
||||||
- `.claude/settings.local.json` — user-specific Claude Code settings, ignored.
|
- `.claude/settings.local.json` — user-specific Claude Code settings, ignored.
|
||||||
- Build artefacts: `sidecar/bin/`, `sidecar/dist/`, `build/` (Flutter output), `app/.dart_tool/` — gitignored.
|
- Build artefacts: `/build/` (Flutter output), `/.dart_tool/` — gitignored.
|
||||||
- SQLite index files (`*.sqlite`, `*.sqlite-wal`, `*.sqlite-shm`, `*.db`) — caches generated against local repos; must never land here. Gitignored defensively.
|
- SQLite index files (`*.sqlite`, `*.sqlite-wal`, `*.sqlite-shm`, `*.db`) — caches generated against local repos; must never land here. Gitignored defensively.
|
||||||
- Coverage / test output (`*.out`, `coverage.*`, `*.test`) — gitignored.
|
- Coverage / test output (`*.out`, `coverage.*`, `*.test`) — gitignored.
|
||||||
|
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
---
|
|
||||||
name: penpot-login
|
|
||||||
description: Connect to Penpot MCP - starts the MCP server, logs into penpot.schweitz.net, and installs/connects the plugin
|
|
||||||
allowed-tools: Read, Bash, mcp__chrome-devtools__new_page, mcp__chrome-devtools__navigate_page, mcp__chrome-devtools__take_snapshot, mcp__chrome-devtools__take_screenshot, mcp__chrome-devtools__click, mcp__chrome-devtools__fill, mcp__chrome-devtools__press_key, mcp__chrome-devtools__list_pages
|
|
||||||
---
|
|
||||||
|
|
||||||
# Penpot MCP Connection
|
|
||||||
|
|
||||||
This skill connects Claude to Penpot by:
|
|
||||||
1. Starting the Penpot MCP server (if not running)
|
|
||||||
2. Logging into penpot.schweitz.net via Authentik
|
|
||||||
3. Installing and connecting the Penpot MCP plugin
|
|
||||||
|
|
||||||
## Step 1: Start MCP Server
|
|
||||||
|
|
||||||
Check if the MCP server is running, start it if not:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check if server is running
|
|
||||||
if ! curl -s http://localhost:9880/mcp > /dev/null 2>&1; then
|
|
||||||
# Start the server in background
|
|
||||||
cd ~/Projects/penpot-mcp && ./start-server.sh &
|
|
||||||
sleep 3 # Wait for server to start
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
|
|
||||||
Server endpoints when running:
|
|
||||||
- MCP Server: http://localhost:9880/mcp
|
|
||||||
- Plugin Server: http://localhost:9879/
|
|
||||||
- WebSocket: ws://localhost:4402
|
|
||||||
|
|
||||||
## Step 2: Login Credentials
|
|
||||||
|
|
||||||
Read credentials from the project root file `claude-authentik-credentials.md`:
|
|
||||||
!`cat claude-authentik-credentials.md`
|
|
||||||
|
|
||||||
## Step 3: Login Flow
|
|
||||||
|
|
||||||
1. **Navigate to Penpot**
|
|
||||||
- Use `mcp__chrome-devtools__new_page` to go to `https://penpot.schweitz.net`
|
|
||||||
|
|
||||||
2. **Check if already logged in**
|
|
||||||
- Take a snapshot
|
|
||||||
- If you see "Projects" heading, you're logged in - skip to Step 4
|
|
||||||
- If you see login page, continue with authentication
|
|
||||||
|
|
||||||
3. **Authenticate via Authentik** (if not logged in)
|
|
||||||
- Click the OpenID button to redirect to Authentik
|
|
||||||
- On auth.schweitz.net, fill username textbox
|
|
||||||
- Click "Log in" button
|
|
||||||
- Fill password textbox
|
|
||||||
- Click "Continue" button
|
|
||||||
- If redirected to Authentik user page instead of Penpot, navigate back to `https://penpot.schweitz.net`
|
|
||||||
|
|
||||||
## Step 4: Open Design File
|
|
||||||
|
|
||||||
1. **Navigate to the Clide project**
|
|
||||||
- Double-click on the TUI file to open the workspace
|
|
||||||
|
|
||||||
## Step 5: Install/Connect Plugin
|
|
||||||
|
|
||||||
1. **Open Plugins menu**
|
|
||||||
- Click the Plugins button (puzzle icon, keyboard shortcut: Cmd+Alt+P)
|
|
||||||
|
|
||||||
2. **Check if plugin is installed**
|
|
||||||
- If "Penpot MCP Plugin" appears under "INSTALLED PLUGINS", click OPEN
|
|
||||||
- Otherwise, install it first:
|
|
||||||
|
|
||||||
3. **Install plugin** (if not installed)
|
|
||||||
- Fill the plugin URL textbox with: `http://localhost:9879/manifest.json`
|
|
||||||
- Click INSTALL
|
|
||||||
- Click ALLOW on the permissions dialog
|
|
||||||
|
|
||||||
4. **Connect to MCP server**
|
|
||||||
- In the plugin UI, click "CONNECT TO MCP SERVER"
|
|
||||||
- Verify it shows "Connected to MCP server"
|
|
||||||
|
|
||||||
## Step 6: Configure Claude Code MCP
|
|
||||||
|
|
||||||
Add the Penpot MCP server to Claude Code (if not already configured):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
claude mcp add penpot -t http http://localhost:9880/mcp
|
|
||||||
```
|
|
||||||
|
|
||||||
**Important**: Use HTTP transport (`-t http`) with the `/mcp` endpoint. Do NOT use SSE transport - it causes "Server not initialized" errors.
|
|
||||||
|
|
||||||
## Step 7: Verify Connection
|
|
||||||
|
|
||||||
After connecting, restart the MCP connection in Claude Code:
|
|
||||||
- Use `/mcp` command to reconnect the penpot server
|
|
||||||
- The Penpot MCP tools (execute_code, export_shape, etc.) will then be available
|
|
||||||
|
|
||||||
Test with:
|
|
||||||
```javascript
|
|
||||||
mcp__penpot__execute_code(code="return penpot.currentPage.name;")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Important Notes
|
|
||||||
|
|
||||||
- Keep the Penpot plugin UI window open while using MCP tools
|
|
||||||
- The MCP server must be running for the plugin to connect
|
|
||||||
- If connection fails, check that the server is running on port 9880
|
|
||||||
- Use HTTP transport (`-t http`), NOT SSE transport
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
---
|
|
||||||
name: skill-create
|
|
||||||
description: >
|
|
||||||
Guidance for creating effective Claude Code skills (.skill packages).
|
|
||||||
Use when the user wants to create, build, design, or iterate on a skill —
|
|
||||||
including writing SKILL.md files, bundling scripts/references/assets,
|
|
||||||
initializing new skills, packaging skills, or improving existing ones.
|
|
||||||
Triggers on requests like "create a skill", "make a new skill",
|
|
||||||
"build a skill for X", "package this skill", or "improve my skill".
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
# Skill Creator
|
|
||||||
|
|
||||||
## About Skills
|
|
||||||
|
|
||||||
Skills are modular, self-contained packages that extend Claude's capabilities
|
|
||||||
by providing specialized knowledge, workflows, and tools. They transform Claude
|
|
||||||
from a general-purpose agent into a specialized agent equipped with procedural
|
|
||||||
knowledge that no model can fully possess.
|
|
||||||
|
|
||||||
### What Skills Provide
|
|
||||||
|
|
||||||
- **Specialized workflows** — Multi-step procedures for specific domains
|
|
||||||
- **Tool integrations** — Instructions for working with specific file formats or APIs
|
|
||||||
- **Domain expertise** — Company-specific knowledge, schemas, business logic
|
|
||||||
- **Bundled resources** — Scripts, references, and assets for complex and repetitive tasks
|
|
||||||
|
|
||||||
## Core Principles
|
|
||||||
|
|
||||||
### Concise is Key
|
|
||||||
|
|
||||||
The context window is a public good. Skills share it with everything else Claude
|
|
||||||
needs: system prompt, conversation history, other skills' metadata, and the
|
|
||||||
actual user request.
|
|
||||||
|
|
||||||
Default assumption: Claude is already very smart. Only add context Claude doesn't
|
|
||||||
already have. Challenge each piece of information: "Does Claude really need this
|
|
||||||
explanation?" and "Does this paragraph justify its token cost?"
|
|
||||||
|
|
||||||
Prefer concise examples over verbose explanations.
|
|
||||||
|
|
||||||
### Set Appropriate Degrees of Freedom
|
|
||||||
|
|
||||||
Match specificity to the task's fragility and variability:
|
|
||||||
|
|
||||||
- **High freedom** (text-based instructions): Multiple approaches valid, decisions
|
|
||||||
depend on context, heuristics guide the approach.
|
|
||||||
- **Medium freedom** (pseudocode or scripts with parameters): Preferred pattern
|
|
||||||
exists, some variation acceptable, configuration affects behavior.
|
|
||||||
- **Low freedom** (specific scripts, few parameters): Operations are fragile and
|
|
||||||
error-prone, consistency is critical, specific sequence must be followed.
|
|
||||||
|
|
||||||
Think of Claude as exploring a path: a narrow bridge with cliffs needs specific
|
|
||||||
guardrails (low freedom), while an open field allows many routes (high freedom).
|
|
||||||
|
|
||||||
## Anatomy of a Skill
|
|
||||||
|
|
||||||
```
|
|
||||||
skill-name/
|
|
||||||
├── SKILL.md (required)
|
|
||||||
│ ├── YAML frontmatter metadata (required)
|
|
||||||
│ │ ├── name: (required)
|
|
||||||
│ │ ├── description: (required)
|
|
||||||
│ │ └── compatibility: (optional, rarely needed)
|
|
||||||
│ └── Markdown instructions (required)
|
|
||||||
└── Bundled Resources (optional)
|
|
||||||
├── scripts/ - Executable code (Python/Bash/etc.)
|
|
||||||
├── references/ - Documentation loaded into context as needed
|
|
||||||
└── assets/ - Files used in output (templates, icons, fonts, etc.)
|
|
||||||
```
|
|
||||||
|
|
||||||
### SKILL.md (required)
|
|
||||||
|
|
||||||
- **Frontmatter (YAML)**: `name` and `description` fields (required). Only these
|
|
||||||
are read by Claude to determine when the skill triggers — be clear and
|
|
||||||
comprehensive. The `compatibility` field is for environment requirements but
|
|
||||||
most skills don't need it.
|
|
||||||
- **Body (Markdown)**: Instructions and guidance. Only loaded AFTER the skill
|
|
||||||
triggers.
|
|
||||||
|
|
||||||
### Bundled Resources (optional)
|
|
||||||
|
|
||||||
**Scripts (`scripts/`)** — Executable code for tasks requiring deterministic
|
|
||||||
reliability or that are repeatedly rewritten.
|
|
||||||
|
|
||||||
**References (`references/`)** — Documentation loaded as needed into context.
|
|
||||||
Keep SKILL.md lean; move detailed reference material, schemas, and examples here.
|
|
||||||
If files are large (>10k words), include grep search patterns in SKILL.md.
|
|
||||||
|
|
||||||
**Assets (`assets/`)** — Files used in output, not loaded into context (templates,
|
|
||||||
images, icons, boilerplate code, fonts).
|
|
||||||
|
|
||||||
### What to NOT Include
|
|
||||||
|
|
||||||
Do NOT create extraneous files like README.md, INSTALLATION_GUIDE.md,
|
|
||||||
QUICK_REFERENCE.md, CHANGELOG.md, etc. The skill should only contain information
|
|
||||||
needed for an AI agent to do the job.
|
|
||||||
|
|
||||||
## Progressive Disclosure
|
|
||||||
|
|
||||||
Skills use a three-level loading system:
|
|
||||||
|
|
||||||
1. **Metadata** (name + description) — Always in context (~100 words)
|
|
||||||
2. **SKILL.md body** — When skill triggers (<5k words)
|
|
||||||
3. **Bundled resources** — As needed (unlimited; scripts can run without reading)
|
|
||||||
|
|
||||||
Keep SKILL.md body under 500 lines. Split content into separate files when
|
|
||||||
approaching this limit. Reference split files from SKILL.md with clear
|
|
||||||
descriptions of when to read them.
|
|
||||||
|
|
||||||
### Disclosure Patterns
|
|
||||||
|
|
||||||
**Pattern 1: High-level guide with references** — Keep overview in SKILL.md,
|
|
||||||
link to detail files loaded only when needed.
|
|
||||||
|
|
||||||
**Pattern 2: Domain-specific organization** — Organize content by domain
|
|
||||||
(e.g., `references/finance.md`, `references/sales.md`) so only relevant content
|
|
||||||
is loaded.
|
|
||||||
|
|
||||||
**Pattern 3: Conditional details** — Show basic content, link to advanced
|
|
||||||
content loaded only when the user needs those features.
|
|
||||||
|
|
||||||
Guidelines:
|
|
||||||
- Avoid deeply nested references — keep one level deep from SKILL.md
|
|
||||||
- Structure longer reference files with a table of contents at the top
|
|
||||||
|
|
||||||
## Skill Creation Process
|
|
||||||
|
|
||||||
Follow these steps in order, skipping only with clear reason:
|
|
||||||
|
|
||||||
### Step 1: Understand the Skill with Concrete Examples
|
|
||||||
|
|
||||||
Skip only when usage patterns are already clearly understood.
|
|
||||||
|
|
||||||
Ask the user for concrete examples of how the skill will be used:
|
|
||||||
- "What functionality should the skill support?"
|
|
||||||
- "Can you give some examples of how this skill would be used?"
|
|
||||||
- "What would a user say that should trigger this skill?"
|
|
||||||
|
|
||||||
Avoid overwhelming users — start with the most important questions.
|
|
||||||
|
|
||||||
### Step 2: Plan the Reusable Skill Contents
|
|
||||||
|
|
||||||
Analyze each example by considering how to execute from scratch and identifying
|
|
||||||
what scripts, references, and assets would help with repeated execution.
|
|
||||||
|
|
||||||
Establish a list of reusable resources: scripts, references, and assets.
|
|
||||||
|
|
||||||
### Step 3: Initialize the Skill
|
|
||||||
|
|
||||||
Create the skill directory manually:
|
|
||||||
|
|
||||||
```
|
|
||||||
mkdir -p <output-directory>/<skill-name>
|
|
||||||
```
|
|
||||||
|
|
||||||
Then create `SKILL.md` with frontmatter and body. Add `scripts/`, `references/`,
|
|
||||||
and `assets/` subdirectories only as needed.
|
|
||||||
|
|
||||||
Skip if iterating on an existing skill.
|
|
||||||
|
|
||||||
### Step 4: Edit the Skill
|
|
||||||
|
|
||||||
Remember the skill is for another Claude instance to use. Include beneficial,
|
|
||||||
non-obvious information.
|
|
||||||
|
|
||||||
For design patterns, consult:
|
|
||||||
- `references/workflows.md` — Sequential workflows and conditional logic
|
|
||||||
- `references/output-patterns.md` — Template and example patterns
|
|
||||||
|
|
||||||
**Implementation order:**
|
|
||||||
1. Start with reusable resources (`scripts/`, `references/`, `assets/`)
|
|
||||||
2. Test added scripts by running them
|
|
||||||
3. Delete unused example files from initialization
|
|
||||||
4. Update SKILL.md
|
|
||||||
|
|
||||||
**Writing guidelines:** Always use imperative/infinitive form.
|
|
||||||
|
|
||||||
**Frontmatter:**
|
|
||||||
- `name`: The skill name — use **domain-action** naming: `{domain}-{action}`.
|
|
||||||
The domain is the system/area the skill operates on, the action is what it does.
|
|
||||||
Examples: `pr-review`, `sprint-plan`, `docs-search`, `git-commit`, `debt-scan`.
|
|
||||||
Multi-action wrappers (like `ticket`) can use the domain name alone.
|
|
||||||
The directory name must match the `name` field.
|
|
||||||
- `description`: Primary triggering mechanism. Include what the skill does AND
|
|
||||||
specific triggers/contexts. All "when to use" info goes here (not in body).
|
|
||||||
|
|
||||||
**Body:** Instructions for using the skill and its bundled resources.
|
|
||||||
|
|
||||||
### Step 5: Validate the Skill
|
|
||||||
|
|
||||||
Check the skill manually:
|
|
||||||
- Frontmatter has `name` and `description`
|
|
||||||
- SKILL.md body is under 500 lines
|
|
||||||
- No extraneous files (README.md, CHANGELOG.md, etc.)
|
|
||||||
- Scripts are executable and tested
|
|
||||||
- References are referenced from SKILL.md
|
|
||||||
|
|
||||||
### Step 6: Iterate
|
|
||||||
|
|
||||||
After real usage, notice struggles or inefficiencies, identify needed updates,
|
|
||||||
implement changes, and test again.
|
|
||||||
@@ -6,9 +6,10 @@ description: >
|
|||||||
lifecycle, theme loading, or native platform code. Also use when
|
lifecycle, theme loading, or native platform code. Also use when
|
||||||
the user says "run testmode", "test the app", "smoke test", or
|
the user says "run testmode", "test the app", "smoke test", or
|
||||||
"verify the build". Triggers on changes to: lib/kernel/src/toolchain.dart,
|
"verify the build". Triggers on changes to: lib/kernel/src/toolchain.dart,
|
||||||
lib/src/daemon/dispatcher.dart, lib/src/ipc/, lib/extension/,
|
lib/src/daemon/dispatcher.dart, lib/src/ipc/, lib/src/pty/,
|
||||||
lib/kernel/src/theme/, lib/builtin/*/src/extension.dart,
|
lib/builtin/terminal/, lib/extension/, lib/kernel/src/theme/,
|
||||||
linux/CMakeLists.txt, macos/, Makefile (run targets), lib/main.dart.
|
lib/builtin/*/src/extension.dart, linux/CMakeLists.txt, macos/,
|
||||||
|
Makefile (run targets), lib/main.dart.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Testmode harness
|
# Testmode harness
|
||||||
@@ -26,6 +27,7 @@ make run-testmode # all categories, 60s timeout
|
|||||||
make run-testmode TESTMODE_CATEGORY=toolchain
|
make run-testmode TESTMODE_CATEGORY=toolchain
|
||||||
make run-testmode TESTMODE_CATEGORY=ipc
|
make run-testmode TESTMODE_CATEGORY=ipc
|
||||||
make run-testmode TESTMODE_CATEGORY=extensions
|
make run-testmode TESTMODE_CATEGORY=extensions
|
||||||
|
make run-testmode TESTMODE_CATEGORY=terminal
|
||||||
make run-testmode TESTMODE_TIMEOUT=120 # longer timeout for slow builds
|
make run-testmode TESTMODE_TIMEOUT=120 # longer timeout for slow builds
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -47,6 +49,7 @@ Exit code is non-zero when any test fails. The Makefile verifies
|
|||||||
| IPC envelope, dispatcher, schema | `ipc` | Round-trip + error contract |
|
| IPC envelope, dispatcher, schema | `ipc` | Round-trip + error contract |
|
||||||
| Extension manifest, activate, contributions | `extensions` | Register + activate lifecycle |
|
| Extension manifest, activate, contributions | `extensions` | Register + activate lifecycle |
|
||||||
| Theme YAML, loader, palette | `extensions` | Theme parse is in this category |
|
| Theme YAML, loader, palette | `extensions` | Theme parse is in this category |
|
||||||
|
| PTY spawning, terminal pane, xterm wiring | `terminal` | Real PTY spawn + I/O round-trip |
|
||||||
| Platform config (CMakeLists, pbxproj, Makefile) | `all` | Full rebuild validates everything |
|
| Platform config (CMakeLists, pbxproj, Makefile) | `all` | Full rebuild validates everything |
|
||||||
| Any doubt | `all` | ~30s, cheap insurance |
|
| Any doubt | `all` | ~30s, cheap insurance |
|
||||||
|
|
||||||
|
|||||||
@@ -36,8 +36,41 @@ These apply across every reference and every surface:
|
|||||||
- Never use `Material*` or `Cupertino*` widgets or color constants — clide
|
- Never use `Material*` or `Cupertino*` widgets or color constants — clide
|
||||||
is `WidgetsApp` only (D-7).
|
is `WidgetsApp` only (D-7).
|
||||||
- Use `ClideText` for themed text; never bare `Text` in production widgets.
|
- Use `ClideText` for themed text; never bare `Text` in production widgets.
|
||||||
- Typography: `clideFontMono` for code/paths/IDs, `clideFontCaption` for
|
- Typography sizes: `clideFontMono` for code/paths/IDs, `clideFontCaption` for
|
||||||
status/section headers, body inherits from `DefaultTextStyle`.
|
status/section headers, body inherits from `DefaultTextStyle`.
|
||||||
|
- Font *family* comes from the user-selectable facade, not a const: a
|
||||||
|
monospace surface uses `fontFamily: ClideSettings.fonts.monoOf(context)`
|
||||||
|
(and `fontFamilyFallback: clideMonoFamilyFallback`); the UI face is inherited
|
||||||
|
via the root `DefaultTextStyle`, or `ClideSettings.fonts.uiOf(context)` when a
|
||||||
|
widget must set it explicitly. `clideMonoFamily` / `clideUiFamily` are the
|
||||||
|
facade's defaults — don't read them directly in new widgets (D-101). Same
|
||||||
|
facade exposes `ClideSettings.theme.of(context)` and `.i18n.of(context)`.
|
||||||
|
- User-facing strings resolve through the catalog, never a hardcoded literal
|
||||||
|
(D-21/D-102): `ClideSettings.i18n.string(context, 'dotted.key', namespace:
|
||||||
|
<ext id or 'core'>, placeholder: '<English>')` (or `.interpolated` for
|
||||||
|
templated). Add the key→English to `assets/i18n/en_us/<namespace>.json`. The
|
||||||
|
`placeholder` is the English fallback; the extension's own id is its
|
||||||
|
namespace (framework chrome uses `core`). Contribution manifests carry
|
||||||
|
`titleKey`/`labelKey` for the same reason.
|
||||||
|
|
||||||
|
## Localization & string length (D-21/D-102)
|
||||||
|
|
||||||
|
- **Config / layout.** Catalogs are bundled assets at
|
||||||
|
`assets/i18n/<locale>/<namespace>.json` — the locale is a *directory*
|
||||||
|
(`en_us`, `nl_nl`, `nl_be`, `en_eu`, …); a new language is a new folder of the
|
||||||
|
same namespace files. The active language is `app.locale` (Settings →
|
||||||
|
Appearance → Language), applied live by `root_shell` via `i18n.setLocale`;
|
||||||
|
add the `Locale` to `availableLocales` in `main.dart` and a folder under
|
||||||
|
`assets/i18n/`. `en_US` is default; `nl_NL` ships.
|
||||||
|
- **Design for length variation.** Translations are not the same width — Dutch
|
||||||
|
runs ~20% longer than English, German more. So **never hard-size a surface to
|
||||||
|
its English label.** Tight surfaces (status-bar items, chips, buttons, tab
|
||||||
|
titles, menu items) must tolerate ~30% growth: let them wrap, ellipsis, or
|
||||||
|
`Flexible`/`Expanded`, not a fixed width tuned to English. When you add or
|
||||||
|
translate a label, sanity-check the length delta on those tight surfaces (an
|
||||||
|
`*.semantics` label is screen-reader-only, so its length never deforms
|
||||||
|
layout). A quick audit: compare `len(nl)/len(en)` per key and eyeball the
|
||||||
|
short-but-grew cases on real (non-semantics) surfaces.
|
||||||
|
|
||||||
## Conversation-panel cards (T-305)
|
## Conversation-panel cards (T-305)
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -112,9 +112,37 @@ tooltip → tooltipBackground / tooltipForeground / tooltipBorder
|
|||||||
dropdown → dropdownBackground / dropdownForeground / dropdownBorder
|
dropdown → dropdownBackground / dropdownForeground / dropdownBorder
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Settings & grouped lists — sectioned cards
|
||||||
|
|
||||||
|
Settings surfaces and any long grouped list (e.g. the Claude config lists)
|
||||||
|
read as **sectioned cards**, not bare rows floating on the panel. Each logical
|
||||||
|
group gets its own card; the small-caps section label (+ optional count) sits
|
||||||
|
just **above** the card.
|
||||||
|
|
||||||
|
```
|
||||||
|
panel bg → panelBackground (#20202C)
|
||||||
|
card surface → surface (#242838) fill + dividerColor/border (1px), ~6px corners
|
||||||
|
section head → sidebarSectionHeader (small-caps), with the count muted to its right
|
||||||
|
control inset → inputs INSIDE a card recede to panelBackground, so they still
|
||||||
|
read as fields against the elevated card
|
||||||
|
```
|
||||||
|
|
||||||
|
- **One card per group** — a settings table, each config list. The card's
|
||||||
|
elevated fill + border do the visual separation; don't rely on spacing alone.
|
||||||
|
- **Field row inside a card:** label (`globalForeground`) + help
|
||||||
|
(`globalTextMuted`) + the control right-aligned, with the per-field scope tag
|
||||||
|
in the far-right column.
|
||||||
|
- **Scroll, don't cram:** when stacked cards exceed the modal/pane viewport, the
|
||||||
|
pane scrolls vertically (sticky header, scrolling body) — prefer that over
|
||||||
|
shrinking content to fit one screen.
|
||||||
|
- Pattern reference: the settings wireframes under
|
||||||
|
`docs/design/wireframes/settings/` (T-302).
|
||||||
|
|
||||||
## Anti-patterns
|
## Anti-patterns
|
||||||
|
|
||||||
- `globalBackground` for panel fill → use `panelBackground`
|
- `globalBackground` for panel fill → use `panelBackground`
|
||||||
|
- Bare settings rows on the panel where a group of them should be one card →
|
||||||
|
see "Settings & grouped lists".
|
||||||
- `listItemHoverBackground` in sidebar → use `sidebarItemHover`
|
- `listItemHoverBackground` in sidebar → use `sidebarItemHover`
|
||||||
- Tab active bg = `panelBackground` → use `panelHeader` (elevated chrome)
|
- Tab active bg = `panelBackground` → use `panelHeader` (elevated chrome)
|
||||||
- Tab active border = `globalFocus` → use `panelActiveBorder`
|
- Tab active border = `globalFocus` → use `panelActiveBorder`
|
||||||
|
|||||||
Regular → Executable
+17
-23
@@ -3,27 +3,22 @@
|
|||||||
|
|
||||||
Mirrors `assets/fonts/phosphor/codepoints.csv` (the full bundled glyph set)
|
Mirrors `assets/fonts/phosphor/codepoints.csv` (the full bundled glyph set)
|
||||||
into a readable, greppable markdown table at
|
into a readable, greppable markdown table at
|
||||||
`.claude/skills/ui-design/references/phosphor-glyphs.md`, flagging which
|
`.claude/skills/ui-design/references/phosphor-glyphs.md`.
|
||||||
glyphs clide already defines in `lib/widgets/src/icons/phosphor.dart`.
|
|
||||||
|
That table is also the source of truth for the `kPhosphorGlyphs` map — after
|
||||||
|
regenerating it, re-run `dart run tool/gen_phosphor_glyphs.dart` so
|
||||||
|
`lib/widgets/src/icons/phosphor_glyphs.g.dart` stays in sync.
|
||||||
|
|
||||||
Run from the repo root: python3 .claude/skills/ui-design/scripts/gen-phosphor-glyphs.py
|
Run from the repo root: python3 .claude/skills/ui-design/scripts/gen-phosphor-glyphs.py
|
||||||
"""
|
"""
|
||||||
import csv
|
import csv
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
|
||||||
|
|
||||||
ROOT = pathlib.Path(__file__).resolve().parents[4]
|
ROOT = pathlib.Path(__file__).resolve().parents[4]
|
||||||
CSV = ROOT / "assets/fonts/phosphor/codepoints.csv"
|
CSV = ROOT / "assets/fonts/phosphor/codepoints.csv"
|
||||||
DART = ROOT / "lib/widgets/src/icons/phosphor.dart"
|
|
||||||
OUT = ROOT / ".claude/skills/ui-design/references/phosphor-glyphs.md"
|
OUT = ROOT / ".claude/skills/ui-design/references/phosphor-glyphs.md"
|
||||||
|
|
||||||
# Codepoint -> camelCase accessor already defined in PhosphorIcons.
|
|
||||||
defined = {}
|
|
||||||
for m in re.finditer(r"static const (\w+) = PhosphorIconPainter\((0x[0-9a-fA-F]+)\)", DART.read_text()):
|
|
||||||
defined[int(m.group(2), 16)] = m.group(1)
|
|
||||||
|
|
||||||
rows = list(csv.DictReader(CSV.open()))
|
rows = list(csv.DictReader(CSV.open()))
|
||||||
n_def = sum(1 for r in rows if int(r["codepoint"], 16) in defined)
|
|
||||||
|
|
||||||
lines = [
|
lines = [
|
||||||
"---",
|
"---",
|
||||||
@@ -37,22 +32,21 @@ lines = [
|
|||||||
f"All **{len(rows)}** glyphs bundled in clide's Phosphor font "
|
f"All **{len(rows)}** glyphs bundled in clide's Phosphor font "
|
||||||
"(`assets/fonts/phosphor/`, MIT). Generated from "
|
"(`assets/fonts/phosphor/`, MIT). Generated from "
|
||||||
"`assets/fonts/phosphor/codepoints.csv` — **do not hand-edit**; regenerate with "
|
"`assets/fonts/phosphor/codepoints.csv` — **do not hand-edit**; regenerate with "
|
||||||
"`python3 .claude/skills/ui-design/scripts/gen-phosphor-glyphs.py`.",
|
"`python3 .claude/skills/ui-design/scripts/gen-phosphor-glyphs.py`, then re-run "
|
||||||
|
"`dart run tool/gen_phosphor_glyphs.dart` (this table is the source for the "
|
||||||
|
"generated `kPhosphorGlyphs` map).",
|
||||||
"",
|
"",
|
||||||
f"The **In clide** column flags the **{n_def}** glyphs already wired into "
|
"Every glyph is available in code via `PhosphorIcons.byName('<kebab-name>')` "
|
||||||
"`PhosphorIcons` (`lib/widgets/src/icons/phosphor.dart`) — reach for those first. "
|
"(T-314) — e.g. `ClideIcon(PhosphorIcons.byName('folder'), size: 13)`. Raw "
|
||||||
"To use any other glyph, add a one-line `static const` to that class with the "
|
"codepoints never appear in feature code; they live only in the generated map. "
|
||||||
"codepoint below, then `ClideIcon(PhosphorIcons.<name>, size: 13)`. Keep additions "
|
"An unknown name renders the `placeholder` glyph, and `phosphor_glyphs_test` "
|
||||||
"to icons we actually use — don't bulk-import.",
|
"asserts every `byName('…')` literal in `lib/` resolves.",
|
||||||
"",
|
"",
|
||||||
"| Codepoint | Name (kebab) | Pascal | In clide |",
|
"| Codepoint | Name (kebab) | Pascal |",
|
||||||
"|---|---|---|---|",
|
"|---|---|---|",
|
||||||
]
|
]
|
||||||
for r in rows:
|
for r in rows:
|
||||||
cp = r["codepoint"]
|
lines.append(f"| `{r['codepoint']}` | {r['name']} | {r['pascal_name']} |")
|
||||||
accessor = defined.get(int(cp, 16))
|
|
||||||
in_clide = f"`PhosphorIcons.{accessor}`" if accessor else ""
|
|
||||||
lines.append(f"| `{cp}` | {r['name']} | {r['pascal_name']} | {in_clide} |")
|
|
||||||
|
|
||||||
OUT.write_text("\n".join(lines) + "\n")
|
OUT.write_text("\n".join(lines) + "\n")
|
||||||
print(f"wrote {OUT.relative_to(ROOT)} — {len(rows)} glyphs, {n_def} already defined")
|
print(f"wrote {OUT.relative_to(ROOT)} — {len(rows)} glyphs")
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ description: >
|
|||||||
Surface the best batch of tickets to pick up next from pql. Walks the
|
Surface the best batch of tickets to pick up next from pql. Walks the
|
||||||
initiative/epic tree, filters to unblocked work, refines context via
|
initiative/epic tree, filters to unblocked work, refines context via
|
||||||
parallel agents (or `pql ticket refine` for empty descriptions), and
|
parallel agents (or `pql ticket refine` for empty descriptions), and
|
||||||
optionally activates the batch on a fresh branch. Use when the user
|
optionally activates the batch (directly on main — no topic branch by
|
||||||
|
default). Use when the user
|
||||||
says "what's next", "next batch", "pick up work", or invokes
|
says "what's next", "next batch", "pick up work", or invokes
|
||||||
/whats-next. NOT triggered by "what should we work on" in a design
|
/whats-next. NOT triggered by "what should we work on" in a design
|
||||||
context — that's a discussion, not a batch selection.
|
context — that's a discussion, not a batch selection.
|
||||||
@@ -18,7 +19,8 @@ Dependency-driven batch selection against pql. Three steps:
|
|||||||
batch selection → refinement review → batch activation.
|
batch selection → refinement review → batch activation.
|
||||||
|
|
||||||
Pql is the single source of truth for tickets and decisions in this repo
|
Pql is the single source of truth for tickets and decisions in this repo
|
||||||
(see [pql skill](../pql/SKILL.md) and [`decisions/README.md`](../../../decisions/README.md)). Always run from the repo root.
|
(see the pql skill — user scope, installed via `pql init` — and
|
||||||
|
[`governance/README.md`](../../../governance/README.md)). Always run from the repo root.
|
||||||
|
|
||||||
## Step 0: Sync state
|
## Step 0: Sync state
|
||||||
|
|
||||||
@@ -158,8 +160,8 @@ Agent({
|
|||||||
|
|
||||||
Your job:
|
Your job:
|
||||||
1. Run `pql decisions show <decision_ref> --with-refs --pretty` and
|
1. Run `pql decisions show <decision_ref> --with-refs --pretty` and
|
||||||
read the linked D/Q-record in decisions/<domain>.md.
|
read the linked D/Q-record in governance/decisions/<domain>.md.
|
||||||
2. Grep decisions/questions-*.md for related Q-records.
|
2. Grep governance/questions/<domain>.md for related Q-records.
|
||||||
3. Verify referenced files, classes, and APIs actually exist in the
|
3. Verify referenced files, classes, and APIs actually exist in the
|
||||||
current tree (Read/Grep). Flag dangling references.
|
current tree (Read/Grep). Flag dangling references.
|
||||||
4. Cross-check against CLAUDE.md guardrails (single process, CLI-first,
|
4. Cross-check against CLAUDE.md guardrails (single process, CLI-first,
|
||||||
@@ -217,24 +219,22 @@ Batch transition (comma-separated IDs):
|
|||||||
pql ticket status T-1,T-2,T-3 in_progress
|
pql ticket status T-1,T-2,T-3 in_progress
|
||||||
```
|
```
|
||||||
|
|
||||||
**Then persist it.** Ticket mutations (status here, and any `ticket new` in
|
**Then make sure it persists.** Ticket mutations (status here, and any
|
||||||
Step 2) land only in the gitignored `.pql/pql.db`. The post-checkout/post-merge
|
`ticket new` in Step 2) land only in the gitignored `.pql/pql.db`. The
|
||||||
hooks rebuild that DB from the committed changelog on every branch switch — so
|
pre-commit hook runs `pql plan export --stage` automatically — the changelog
|
||||||
un-exported changes vanish silently the next time anyone switches branches. After
|
is exported and staged on every commit, so never hand-run the export or
|
||||||
creating or transitioning tickets, always:
|
`git add .pql/changelog`. The rule is simpler: the turn must land at least
|
||||||
|
one commit (through the git-commit skill). A ticket-only turn with no commit
|
||||||
```bash
|
leaves the mutations in `pql.db` only, and the post-checkout/post-merge hooks
|
||||||
pql plan export # regenerates .pql/changelog/*.sql
|
rebuild that DB from the committed changelog on the next branch switch —
|
||||||
git add .pql/changelog && git commit # durable; survives rebuild
|
silently dropping them. See the pql skill's "Versioning planning state"
|
||||||
```
|
section for the full mechanics.
|
||||||
|
|
||||||
See the [pql skill](../pql/SKILL.md#versioning-planning-state--data-loss-footgun-read-this).
|
|
||||||
|
|
||||||
### 3b. Branch? Default no.
|
### 3b. Branch? Default no.
|
||||||
|
|
||||||
Solo-dev flow on this repo — work lands directly on `main` (see recent
|
Solo-dev flow on this repo — work lands directly on `main` (see recent
|
||||||
`git log`). Don't create a topic branch unless the user explicitly asks.
|
`git log`). Don't create a topic branch unless the user explicitly asks.
|
||||||
If they do, plain `git checkout -b` is fine; there is no `gh` CLI.
|
If they do, plain `git checkout -b` is fine.
|
||||||
|
|
||||||
### 3c. Spawn implementation agents (optional)
|
### 3c. Spawn implementation agents (optional)
|
||||||
|
|
||||||
@@ -263,13 +263,13 @@ End with a tight summary:
|
|||||||
## Anti-patterns
|
## Anti-patterns
|
||||||
|
|
||||||
- Don't skip Step 0 — stale `pql.db` makes the rest of the skill lie.
|
- Don't skip Step 0 — stale `pql.db` makes the rest of the skill lie.
|
||||||
- Don't leave ticket changes un-exported — `pql.db` is gitignored and the
|
- Don't hand-export or hand-stage `.pql/changelog/` — the pre-commit hook does
|
||||||
post-checkout/post-merge hooks rebuild it from the committed changelog, so a
|
both on every commit. The real footgun is a turn that mutates tickets but
|
||||||
branch switch silently drops un-exported tickets. Always `pql plan export` +
|
never commits: `pql.db` is gitignored and gets rebuilt from the committed
|
||||||
commit `.pql/changelog/` after mutating tickets (Step 3a).
|
changelog on branch switch, silently dropping un-committed mutations. Land
|
||||||
|
at least one commit per ticket-mutating turn (Step 3a).
|
||||||
- Don't activate a batch the user hasn't confirmed.
|
- Don't activate a batch the user hasn't confirmed.
|
||||||
- Don't spawn refinement agents for tickets that have no description — use
|
- Don't spawn refinement agents for tickets that have no description — use
|
||||||
`pql ticket refine` instead; it's cheaper and writes back through the
|
`pql ticket refine` instead; it's cheaper and writes back through the
|
||||||
proper channel.
|
proper channel.
|
||||||
- Don't reach for `gh` — this system doesn't have it. Plain `git` only.
|
|
||||||
- Don't `cd` into subdirectories — run everything from the repo root.
|
- Don't `cd` into subdirectories — run everything from the repo root.
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
# Gitea Actions workflow for clide.
|
|
||||||
#
|
|
||||||
# NOT YET ACTIVATED. Gitea Actions must be enabled in the instance
|
|
||||||
# settings before this runs; until then the file is just a ready-made
|
|
||||||
# pipeline Claude + the user can review.
|
|
||||||
#
|
|
||||||
# When the repo eventually lands on GitHub, copy this file verbatim to
|
|
||||||
# `.github/workflows/test.yml` — Gitea Actions consumes GitHub-Actions
|
|
||||||
# syntax, so no rewrite is needed.
|
|
||||||
#
|
|
||||||
# Steps go through the make targets (the repo's tooling-discipline rule:
|
|
||||||
# the make layer sets up the environment — gen-build-info etc. — and
|
|
||||||
# stays correct if a wrapped script moves). T-384 fixed three latent
|
|
||||||
# breaks here: a `cd app` into the flattened-away app/ directory, a
|
|
||||||
# coverage gate with no coverage run before it, and raw ci/ script
|
|
||||||
# invocations that skipped build-info generation.
|
|
||||||
|
|
||||||
name: test
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
unit:
|
|
||||||
name: unit + widget + golden + a11y + coverage gate
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: subosito/flutter-action@v2
|
|
||||||
with: { channel: stable, cache: true }
|
|
||||||
- run: flutter pub get
|
|
||||||
# test-coverage runs the full fast suite WITH coverage (it includes
|
|
||||||
# the a11y suite — see the push-check note in the Makefile), which
|
|
||||||
# is what coverage-gate consumes.
|
|
||||||
- run: make test-coverage
|
|
||||||
- run: make coverage-gate
|
|
||||||
|
|
||||||
integration:
|
|
||||||
name: integration_test (xvfb)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: unit
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: subosito/flutter-action@v2
|
|
||||||
with: { channel: stable, cache: true }
|
|
||||||
- run: sudo apt-get update && sudo apt-get install -y xvfb ninja-build libgtk-3-dev
|
|
||||||
- run: flutter pub get
|
|
||||||
- uses: coactions/setup-xvfb@v1
|
|
||||||
with: { run: make test-integration }
|
|
||||||
|
|
||||||
startup-bundle:
|
|
||||||
name: bundle smoke (xvfb 5s)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: unit
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: subosito/flutter-action@v2
|
|
||||||
with: { channel: stable, cache: true }
|
|
||||||
- run: sudo apt-get update && sudo apt-get install -y xvfb ninja-build libgtk-3-dev
|
|
||||||
- run: flutter pub get
|
|
||||||
- run: make smoke-bundle
|
|
||||||
|
|
||||||
# The web-WASM Playwright job is withheld: `flutter build web --wasm`
|
|
||||||
# cannot compile the tree since the tree-sitter/PTY dart:ffi pivot
|
|
||||||
# (dart:ffi is unavailable on the wasm target). Whether the web target
|
|
||||||
# gets conditional-import fences or is dropped is an open question —
|
|
||||||
# see Q-50 in governance/questions/architecture.md. Re-add the job
|
|
||||||
# (steps: setup-node, npm install + playwright install in tools/ui,
|
|
||||||
# `make test-e2e`) when Q-50 resolves toward keeping it.
|
|
||||||
|
|
||||||
docs:
|
|
||||||
name: dart doc (lib API)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: subosito/flutter-action@v2
|
|
||||||
with: { channel: stable, cache: true }
|
|
||||||
- run: flutter pub get
|
|
||||||
- name: dart doc --validate-links (fail on warning)
|
|
||||||
run: |
|
|
||||||
set -o pipefail
|
|
||||||
dart doc --validate-links 2>&1 | tee dartdoc.log
|
|
||||||
if grep -q "^ warning:" dartdoc.log; then
|
|
||||||
echo "::error::dartdoc emitted warnings — see log above"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: dart-api-docs
|
|
||||||
path: doc/api/
|
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# pql: source .pql/hooks/post-checkout (rebuild pql.db on branch checkout)
|
# pql: source .pql/hooks/post-checkout (rebuild pql.db on branch checkout).
|
||||||
. "$(git rev-parse --show-toplevel)/.pql/hooks/post-checkout"
|
# The pql hook is untracked (a local `pql init` install), so a fresh
|
||||||
|
# `git worktree add` has no .pql/hooks — source it only when present, and
|
||||||
|
# always exit 0: post-checkout is best-effort and must never abort the
|
||||||
|
# checkout / worktree creation.
|
||||||
|
hook="$(git rev-parse --show-toplevel)/.pql/hooks/post-checkout"
|
||||||
|
if [ -f "$hook" ]; then . "$hook"; fi
|
||||||
|
exit 0
|
||||||
|
|||||||
+20
-12
@@ -5,14 +5,16 @@
|
|||||||
# Bypass: never. If this runs slowly, fix the slow test; don't reach
|
# Bypass: never. If this runs slowly, fix the slow test; don't reach
|
||||||
# for --no-verify (git-commit skill forbids it).
|
# for --no-verify (git-commit skill forbids it).
|
||||||
#
|
#
|
||||||
# Fast path (T-348): run the full ~2min test suite only when the push touches
|
# Fast path (T-348, widened T-393): run the full ~2min test suite when the push
|
||||||
# lib/ (app + runtime Dart source) or pubspec.* (deps / version). test/,
|
# touches lib/ (app + runtime Dart source), pubspec.* (deps / version), or the
|
||||||
# assets/, docs, and tooling changes ride along with a lib change in practice,
|
# things that can themselves break the suite or this gate — test/, ci/, and
|
||||||
# and an otherwise-skipped push is covered by the next one that does touch lib.
|
# .githooks/. (The old regex matched only lib/ and pubspec.*, so a push that
|
||||||
# The full suite is always available via `make push-check`, and the release CI
|
# ONLY changed a test, a ci/ gate script, or this hook skipped the whole suite.)
|
||||||
# runs it forced on a tagged version. So a lib/pubspec-free push runs just the
|
# Pure assets/docs changes still ride along with the next lib-touching push.
|
||||||
# instant decisions + changelog gates. A state we can't classify (unfetched
|
# There is no release CI — the full suite is only ever run here or via
|
||||||
# remote, new branch) runs the full gate.
|
# `make push-check`. So a push touching none of the above runs just the instant
|
||||||
|
# decisions + changelog gates. A state we can't classify (unfetched remote, new
|
||||||
|
# branch) runs the full gate.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
cd "$(git rev-parse --show-toplevel)"
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
@@ -40,16 +42,22 @@ while read -r _local_ref local_sha _remote_ref remote_sha; do
|
|||||||
changed+=$'\n'"$(git diff --name-only "$base" "$local_sha")"
|
changed+=$'\n'"$(git diff --name-only "$base" "$local_sha")"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Run the full gate when lib/ (app + runtime source) or pubspec.* (deps /
|
# Paths that force the full gate: source (lib/), deps/version (pubspec.*), and
|
||||||
# version) is touched, or when we couldn't classify above.
|
# the dirs that can themselves break the suite or this gate (test/, ci/,
|
||||||
|
# .githooks/). Single source of truth — test/tooling/pre_push_hook_test.dart
|
||||||
|
# reads this exact pattern, so narrowing it fails that test (the T-393 guard).
|
||||||
|
trigger_re='^(lib/|test/|ci/|\.githooks/|pubspec\.)'
|
||||||
|
|
||||||
|
# Run the full gate when a trigger path is touched, or when we couldn't classify
|
||||||
|
# above.
|
||||||
needs_gate=1
|
needs_gate=1
|
||||||
if [[ "$force_full" -eq 0 ]]; then
|
if [[ "$force_full" -eq 0 ]]; then
|
||||||
trigger_files="$(printf '%s\n' "$changed" | grep -E '^(lib/|pubspec\.)' || true)"
|
trigger_files="$(printf '%s\n' "$changed" | grep -E "$trigger_re" || true)"
|
||||||
[[ -z "$trigger_files" ]] && needs_gate=0
|
[[ -z "$trigger_files" ]] && needs_gate=0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$needs_gate" -eq 0 ]]; then
|
if [[ "$needs_gate" -eq 0 ]]; then
|
||||||
echo "==> pre-push: no lib/ or pubspec change — decisions + changelog gates, skipping tests"
|
echo "==> pre-push: no source/test/ci/hook/pubspec change — decisions + changelog gates, skipping tests"
|
||||||
make decisions-validate changelog-gate
|
make decisions-validate changelog-gate
|
||||||
else
|
else
|
||||||
echo "==> pre-push: make push-check"
|
echo "==> pre-push: make push-check"
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
name: release
|
||||||
|
|
||||||
|
# Build + publish versioned Windows and Linux release bundles when the version
|
||||||
|
# in pubspec.yaml changes on main. The `version` job only proceeds when the
|
||||||
|
# v<version> tag doesn't already exist, so an unrelated pubspec edit is a no-op.
|
||||||
|
#
|
||||||
|
# FIRST CUT — neither build has run in CI yet (Windows has never been built at
|
||||||
|
# all), so expect to iterate on these from the first run's logs. The repo's own
|
||||||
|
# `make` targets are the build contract (gen-build-info + clide-cli + flutter
|
||||||
|
# build, all wired in `make build`).
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths: ['pubspec.yaml']
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write # create the tag + the release
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
version:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.v.outputs.version }}
|
||||||
|
fresh: ${{ steps.v.outputs.fresh }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with: { fetch-depth: 0 } # tags, to tell new vs. already-released
|
||||||
|
- id: v
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
version=$(awk -F': *' '/^version:/ {gsub(/[" ]/,"",$2); print $2; exit}' pubspec.yaml)
|
||||||
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
if git rev-parse "v$version" >/dev/null 2>&1; then
|
||||||
|
echo "fresh=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "v$version already tagged — nothing to release."
|
||||||
|
else
|
||||||
|
echo "fresh=true" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "v$version is new — building."
|
||||||
|
fi
|
||||||
|
|
||||||
|
build-linux:
|
||||||
|
needs: version
|
||||||
|
if: needs.version.outputs.fresh == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with: { channel: stable }
|
||||||
|
- run: sudo apt-get update && sudo apt-get install -y ninja-build libgtk-3-dev
|
||||||
|
- run: make dugite-fetch
|
||||||
|
- run: make build # gen-build-info + clide-cli + flutter build linux
|
||||||
|
- name: package
|
||||||
|
run: tar -C build/linux/x64/release/bundle -czf clide-linux-x64-${{ needs.version.outputs.version }}.tar.gz .
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with: { name: linux, path: clide-linux-x64-*.tar.gz }
|
||||||
|
|
||||||
|
build-windows:
|
||||||
|
needs: version
|
||||||
|
if: needs.version.outputs.fresh == 'true'
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with: { channel: stable }
|
||||||
|
- run: choco install -y make
|
||||||
|
- name: build
|
||||||
|
shell: bash
|
||||||
|
run: make dugite-fetch && make build # MSVC + bash already on windows-latest
|
||||||
|
- name: package
|
||||||
|
shell: pwsh
|
||||||
|
run: Compress-Archive -Path build/windows/x64/runner/Release/* -DestinationPath clide-windows-x64-${{ needs.version.outputs.version }}.zip
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with: { name: windows, path: clide-windows-x64-*.zip }
|
||||||
|
|
||||||
|
publish:
|
||||||
|
needs: [version, build-linux, build-windows]
|
||||||
|
if: needs.version.outputs.fresh == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: changelog notes for this version
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
ver="${{ needs.version.outputs.version }}"
|
||||||
|
# Pull the entries under `## [<version>]` — the changelog cut that the
|
||||||
|
# version-bump commit lands per the changelog discipline — as the
|
||||||
|
# release body; fall back to a one-liner if the section is absent.
|
||||||
|
awk -v ver="$ver" '
|
||||||
|
$0 ~ "^## \\[" ver "\\]" {grab=1; next}
|
||||||
|
grab && /^## \[/ {exit}
|
||||||
|
grab {print}
|
||||||
|
' CHANGELOG.md > release-notes.md
|
||||||
|
[ -s release-notes.md ] || echo "Release v$ver." > release-notes.md
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with: { path: dist }
|
||||||
|
- uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: v${{ needs.version.outputs.version }}
|
||||||
|
name: clide v${{ needs.version.outputs.version }}
|
||||||
|
body_path: release-notes.md # the version's CHANGELOG section
|
||||||
|
generate_release_notes: true # + auto commit list appended
|
||||||
|
files: dist/**/* # the built versioned bundles
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
name: test
|
||||||
|
|
||||||
|
# Linux CI for clide (GitHub Actions). Moved here from .gitea/workflows/ when CI
|
||||||
|
# consolidated onto GitHub (the primary remote); the Gitea secondary has Actions
|
||||||
|
# disabled. Pairs with windows.yml (ConPTY suite on windows-latest) and
|
||||||
|
# release.yml (versioned release bundles).
|
||||||
|
#
|
||||||
|
# Steps go through the make targets (the repo's tooling-discipline rule: the
|
||||||
|
# make layer sets up the environment — gen-build-info etc. — and stays correct
|
||||||
|
# if a wrapped script moves).
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
unit:
|
||||||
|
name: unit + widget + golden + a11y + coverage gate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with: { channel: stable, cache: true }
|
||||||
|
- run: flutter pub get
|
||||||
|
- name: install pql
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
gh release download --repo postmeridiem/pql --pattern 'pql_*_Linux_x86_64.tar.gz' --output /tmp/pql.tgz
|
||||||
|
tar -xzf /tmp/pql.tgz -C /tmp
|
||||||
|
sudo install -m 0755 /tmp/pql /usr/local/bin/pql
|
||||||
|
pql --version
|
||||||
|
# The pql tests query the repo's vault, but .pql/pql.db is gitignored
|
||||||
|
# (the post-checkout hook rebuilds it from the committed changelog).
|
||||||
|
# A fresh CI checkout has the changelog but no db — materialize it,
|
||||||
|
# and sync decision records from the governance/ DQR markdown tree
|
||||||
|
# (tickets come from the changelog; decisions from `decisions sync`).
|
||||||
|
pql plan import
|
||||||
|
pql decisions sync
|
||||||
|
# test-coverage runs the full fast suite WITH coverage (it includes the
|
||||||
|
# a11y suite — see the push-check note in the Makefile), which is what
|
||||||
|
# coverage-gate consumes.
|
||||||
|
- run: make test-coverage
|
||||||
|
- run: make coverage-gate
|
||||||
|
|
||||||
|
integration:
|
||||||
|
name: integration_test (xvfb)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: unit
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with: { channel: stable, cache: true }
|
||||||
|
- run: sudo apt-get update && sudo apt-get install -y xvfb ninja-build libgtk-3-dev
|
||||||
|
- run: flutter pub get
|
||||||
|
- name: install pql
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
gh release download --repo postmeridiem/pql --pattern 'pql_*_Linux_x86_64.tar.gz' --output /tmp/pql.tgz
|
||||||
|
tar -xzf /tmp/pql.tgz -C /tmp
|
||||||
|
sudo install -m 0755 /tmp/pql /usr/local/bin/pql
|
||||||
|
pql --version
|
||||||
|
# The pql tests query the repo's vault, but .pql/pql.db is gitignored
|
||||||
|
# (the post-checkout hook rebuilds it from the committed changelog).
|
||||||
|
# A fresh CI checkout has the changelog but no db — materialize it,
|
||||||
|
# and sync decision records from the governance/ DQR markdown tree
|
||||||
|
# (tickets come from the changelog; decisions from `decisions sync`).
|
||||||
|
pql plan import
|
||||||
|
pql decisions sync
|
||||||
|
- uses: coactions/setup-xvfb@v1
|
||||||
|
with: { run: make test-integration }
|
||||||
|
|
||||||
|
startup-bundle:
|
||||||
|
name: bundle smoke (xvfb 5s)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: unit
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with: { channel: stable, cache: true }
|
||||||
|
- run: sudo apt-get update && sudo apt-get install -y xvfb ninja-build libgtk-3-dev
|
||||||
|
- run: flutter pub get
|
||||||
|
- name: install pql
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
gh release download --repo postmeridiem/pql --pattern 'pql_*_Linux_x86_64.tar.gz' --output /tmp/pql.tgz
|
||||||
|
tar -xzf /tmp/pql.tgz -C /tmp
|
||||||
|
sudo install -m 0755 /tmp/pql /usr/local/bin/pql
|
||||||
|
pql --version
|
||||||
|
# The pql tests query the repo's vault, but .pql/pql.db is gitignored
|
||||||
|
# (the post-checkout hook rebuilds it from the committed changelog).
|
||||||
|
# A fresh CI checkout has the changelog but no db — materialize it,
|
||||||
|
# and sync decision records from the governance/ DQR markdown tree
|
||||||
|
# (tickets come from the changelog; decisions from `decisions sync`).
|
||||||
|
pql plan import
|
||||||
|
pql decisions sync
|
||||||
|
# Point the real release app's crash logs at an uploadable workspace dir
|
||||||
|
# (T-436): if the bundle wedges on boot, the watchdog heartbeat/sample +
|
||||||
|
# FileLogSink land here and get uploaded below. CLIDE_LOG=debug so the
|
||||||
|
# file sink captures info/debug, not just the release-default warn.
|
||||||
|
- name: bundle smoke (logs → artifact)
|
||||||
|
env:
|
||||||
|
CLIDE_LOG: debug
|
||||||
|
CLIDE_LOG_DIR: ${{ github.workspace }}/clide-logs
|
||||||
|
run: make smoke-bundle
|
||||||
|
- name: Upload crash logs
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: bundle-crash-logs
|
||||||
|
path: ${{ github.workspace }}/clide-logs
|
||||||
|
if-no-files-found: ignore
|
||||||
|
|
||||||
|
web-wasm:
|
||||||
|
name: web build (wasm compile gate)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with: { channel: stable, cache: true }
|
||||||
|
- run: flutter pub get
|
||||||
|
# Compile gate for the dart:ffi web fence (D-100 / T-438, resolving Q-50):
|
||||||
|
# every native binding lives behind a `dart.library.ffi` conditional import
|
||||||
|
# with a web stub. If a new one lands without its stub, this fails — the
|
||||||
|
# fence can't silently rot. Build-only for now; the full web-WASM Playwright
|
||||||
|
# e2e (`make test-e2e`: setup-node + playwright install in tools/ui +
|
||||||
|
# serve) is the follow-on once the harness is wired back up.
|
||||||
|
- run: flutter build web --wasm
|
||||||
|
|
||||||
|
docs:
|
||||||
|
name: dart doc (lib API)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with: { channel: stable, cache: true }
|
||||||
|
- run: flutter pub get
|
||||||
|
- name: dart doc --validate-links (fail on warning)
|
||||||
|
run: |
|
||||||
|
set -o pipefail
|
||||||
|
dart doc --validate-links 2>&1 | tee dartdoc.log
|
||||||
|
if grep -q "^ warning:" dartdoc.log; then
|
||||||
|
echo "::error::dartdoc emitted warnings — see log above"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: dart-api-docs
|
||||||
|
path: doc/api/
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
name: windows-soak
|
||||||
|
|
||||||
|
# ConPTY orphan-leak soak on a GitHub-hosted Windows runner — the cheap
|
||||||
|
# alternative to a dedicated Windows VM. The freeze hypothesis (T-424) is that
|
||||||
|
# each WindowsPty.start() leaks its conhost/OpenConsole host because the child
|
||||||
|
# is not in a kill-on-close Job Object; across many runs those hosts pile up
|
||||||
|
# until the box starves. tools/windows-verify/soak-conpty.ps1 reproduces that
|
||||||
|
# WITHOUT crashing: it runs the ConPTY suite many times IN ONE job and counts
|
||||||
|
# the hosts that survive each dart.exe exit. A throwaway runner is fine — we
|
||||||
|
# watch the accumulation (the leading indicator), not the reboot. The repeated
|
||||||
|
# runs happen inside this single job, so the leak can build up here even though
|
||||||
|
# the runner is discarded afterwards (cf. the note in windows.yml, which only
|
||||||
|
# runs the suite once).
|
||||||
|
#
|
||||||
|
# Diagnostic, never a gate: it always exits 0 and just publishes the verdict +
|
||||||
|
# CSV. Runs on demand (workflow_dispatch) and when the soak kit itself changes.
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
iterations:
|
||||||
|
description: How many times to run the ConPTY suite (clean-path soak)
|
||||||
|
default: "25"
|
||||||
|
kill_iterations:
|
||||||
|
description: Spawn+force-kill cycles (abrupt-death orphan probe)
|
||||||
|
default: "15"
|
||||||
|
ptys_per_iter:
|
||||||
|
description: WindowsPty sessions spawned per kill cycle
|
||||||
|
default: "2"
|
||||||
|
push:
|
||||||
|
branches: [windows-support]
|
||||||
|
paths:
|
||||||
|
- tools/windows-verify/**
|
||||||
|
- .github/workflows/windows-soak.yml
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
conpty-soak:
|
||||||
|
runs-on: windows-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: stable
|
||||||
|
- run: flutter --version
|
||||||
|
- run: flutter pub get
|
||||||
|
- name: ConPTY orphan-leak soak
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$iters = "${{ github.event.inputs.iterations }}"
|
||||||
|
if (-not $iters) { $iters = "25" }
|
||||||
|
tools/windows-verify/soak-conpty.ps1 -Iterations ([int]$iters) -OutDir "$env:GITHUB_WORKSPACE/soak-out"
|
||||||
|
- name: Publish verdict to job summary
|
||||||
|
if: always()
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$s = Get-ChildItem "$env:GITHUB_WORKSPACE/soak-out/*.summary.txt" -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||||
|
if ($s) { Get-Content $s.FullName | Add-Content $env:GITHUB_STEP_SUMMARY }
|
||||||
|
- name: Upload soak CSV + summary
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: conpty-soak
|
||||||
|
path: soak-out
|
||||||
|
if-no-files-found: warn
|
||||||
|
|
||||||
|
conpty-kill-probe:
|
||||||
|
# Abrupt-death half: force-kill the parent dart.exe mid-life (no close(),
|
||||||
|
# no Job Object) and count the ConPTY hosts that survive. This is the path
|
||||||
|
# the freeze hypothesis (T-424) actually implicates — the clean-path soak
|
||||||
|
# above never exercises it. Diagnostic only; always succeeds.
|
||||||
|
runs-on: windows-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: stable
|
||||||
|
- run: flutter --version
|
||||||
|
- run: flutter pub get
|
||||||
|
- name: ConPTY abrupt-death orphan probe
|
||||||
|
shell: pwsh
|
||||||
|
# CLIDE_LOG_DIR makes the probe emit FFI breadcrumbs (T-436): when a
|
||||||
|
# parent is force-killed mid-life, its reader/waiter isolates' last
|
||||||
|
# crumb ("ReadFile enter" / "WaitForSingleObject enter") is fsynced to
|
||||||
|
# clide-pty.crumbs.log and uploaded below — naming what the wedged
|
||||||
|
# isolate was doing at the instant of death.
|
||||||
|
env:
|
||||||
|
CLIDE_LOG_DIR: ${{ github.workspace }}/kill-crumbs
|
||||||
|
run: |
|
||||||
|
$iters = "${{ github.event.inputs.kill_iterations }}"
|
||||||
|
if (-not $iters) { $iters = "15" }
|
||||||
|
$ptys = "${{ github.event.inputs.ptys_per_iter }}"
|
||||||
|
if (-not $ptys) { $ptys = "2" }
|
||||||
|
tools/windows-verify/soak-conpty-kill.ps1 -Iterations ([int]$iters) -PtysPerIter ([int]$ptys) -OutDir "$env:GITHUB_WORKSPACE/kill-out"
|
||||||
|
- name: Publish verdict to job summary
|
||||||
|
if: always()
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$s = Get-ChildItem "$env:GITHUB_WORKSPACE/kill-out/*.summary.txt" -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||||
|
if ($s) { Get-Content $s.FullName | Add-Content $env:GITHUB_STEP_SUMMARY }
|
||||||
|
- name: Upload kill-probe CSV + summary
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: conpty-kill-probe
|
||||||
|
path: kill-out
|
||||||
|
if-no-files-found: warn
|
||||||
|
- name: Upload FFI breadcrumbs (last act of each killed reader/waiter)
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: conpty-kill-crumbs
|
||||||
|
path: ${{ github.workspace }}/kill-crumbs
|
||||||
|
if-no-files-found: ignore
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
name: windows
|
||||||
|
|
||||||
|
# Windows CI on GitHub-hosted runners — the only hosted Windows available, and
|
||||||
|
# GitHub is clide's primary remote (the Gitea secondary is self-hosted Linux and
|
||||||
|
# keeps running the Linux suite). This is the first real execution of the ConPTY
|
||||||
|
# backend (lib/src/pty/windows_pty.dart), so expect genuine failures until the
|
||||||
|
# Windows fixes land (T-424). Keep this OUT of required status checks until it's
|
||||||
|
# reliably green — it reports + uploads artifacts without blocking merges. Each
|
||||||
|
# run is a fresh, discarded runner, so the accumulation freeze (which needs
|
||||||
|
# repeated runs on one machine) can't build up here.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, windows-support]
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
windows-pty:
|
||||||
|
runs-on: windows-latest
|
||||||
|
timeout-minutes: 25
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: stable
|
||||||
|
- run: flutter --version
|
||||||
|
- run: flutter pub get
|
||||||
|
# No `flutter analyze` here: it's platform-agnostic — the Linux job already
|
||||||
|
# analyzes windows_pty.dart and everything else statically, and the
|
||||||
|
# `flutter build windows` release job catches Windows-specific compile
|
||||||
|
# errors. Skipping it also avoids needing `make gen-build-info`, since the
|
||||||
|
# pty tests import the pty libraries directly, not the build_info-bearing
|
||||||
|
# barrel (lib/clide.dart). This job's unique value is running real ConPTY.
|
||||||
|
- name: ConPTY + Windows-arg unit tests
|
||||||
|
# windows_pty_test.dart drives real ConPTY (it self-skips off-Windows);
|
||||||
|
# the args/size suites are the pure-logic coverage. --timeout 60s so a
|
||||||
|
# wedged reader fails fast instead of hanging the runner.
|
||||||
|
run: dart test --concurrency=1 --timeout 60s test/pty/windows_pty_test.dart test/pty/windows_pty_args_test.dart test/pty/pty_size_test.dart
|
||||||
|
- name: Upload test output / logs
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: windows-test-output
|
||||||
|
# T-425 (crash-survivable FileLogSink) writes under %LOCALAPPDATA%\clide\logs;
|
||||||
|
# add that path here once it lands so a freeze leaves a downloadable log.
|
||||||
|
path: test/.test-output
|
||||||
|
if-no-files-found: ignore
|
||||||
@@ -58,6 +58,8 @@ tools/ui/.serve.pid
|
|||||||
/native/linux-x64/clide
|
/native/linux-x64/clide
|
||||||
/native/macos-arm64/clide
|
/native/macos-arm64/clide
|
||||||
/native/macos-x64/clide
|
/native/macos-x64/clide
|
||||||
|
/native/windows-x64/clide.exe
|
||||||
|
/native/windows-x64/clide.obj
|
||||||
|
|
||||||
# -- Test, coverage, profile output ------------------------------------
|
# -- Test, coverage, profile output ------------------------------------
|
||||||
*.test
|
*.test
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# This file should be version controlled and should not be manually edited.
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
version:
|
version:
|
||||||
revision: "cc0734ac716fbb8b90f3f9db8020958b1553afa7"
|
revision: "c9a6c484230f8b5e408ec57be1ef71dee1e77020"
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
|
|
||||||
project_type: app
|
project_type: app
|
||||||
@@ -13,11 +13,11 @@ project_type: app
|
|||||||
migration:
|
migration:
|
||||||
platforms:
|
platforms:
|
||||||
- platform: root
|
- platform: root
|
||||||
create_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7
|
create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
|
||||||
base_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7
|
base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
|
||||||
- platform: web
|
- platform: windows
|
||||||
create_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7
|
create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
|
||||||
base_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7
|
base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
|
||||||
|
|
||||||
# User provided section
|
# User provided section
|
||||||
|
|
||||||
|
|||||||
@@ -35,3 +35,24 @@ INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updat
|
|||||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKMV7Y13PAYKZC0WB4FQXKC', '06FBKN09R21H3AWR2Q2ZTSGNSW', '2026-06-12 03:16:40', '2026-06-12 03:16:40', NULL, '7e00348c10432c65b03f9dce36520e48', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKMV7Y13PAYKZC0WB4FQXKC', '06FBKN09R21H3AWR2Q2ZTSGNSW', '2026-06-12 03:16:40', '2026-06-12 03:16:40', NULL, '7e00348c10432c65b03f9dce36520e48', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKMXSVCE98K1H76N00TYCQR', '06FBKN2MP35NPPK1BRDYY2M428', '2026-06-12 03:16:44', '2026-06-12 03:16:44', NULL, 'a6b1c20279ac30f692a30c802cf8f3e5', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKMXSVCE98K1H76N00TYCQR', '06FBKN2MP35NPPK1BRDYY2M428', '2026-06-12 03:16:44', '2026-06-12 03:16:44', NULL, 'a6b1c20279ac30f692a30c802cf8f3e5', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKMV7Y13PAYKZC0WB4FQXKC', '06FBKN4QVFVE51MY2N0CWCVXHM', '2026-06-12 03:16:49', '2026-06-12 03:16:49', NULL, '9063328f18ba32c0737997ac9af0911a', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKMV7Y13PAYKZC0WB4FQXKC', '06FBKN4QVFVE51MY2N0CWCVXHM', '2026-06-12 03:16:49', '2026-06-12 03:16:49', NULL, '9063328f18ba32c0737997ac9af0911a', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||||
|
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBDSM0PRGYR61R0NWYAT9VDC', '06FFW49VYMPF18PYQXD9PCMHN8', '2026-06-25 10:24:38', '2026-06-25 10:24:38', NULL, 'a5146a3cfb2f6a3fd904224150836336', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||||
|
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49VYMPF18PYQXD9PCMHN8', '06FFW49W3V175EM4F8ZHC6JFM4', '2026-06-25 10:24:38', '2026-06-25 10:24:38', NULL, 'd57042b235b73ed2d25252d5253e6e4a', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||||
|
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49VYMPF18PYQXD9PCMHN8', '06FFW49W6GP535GR8XD1XEHYWG', '2026-06-25 10:24:38', '2026-06-25 10:24:38', NULL, '07fe1945cb3e60c43cce0989366af8fb', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||||
|
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49VYMPF18PYQXD9PCMHN8', '06FFW49W95HJK08BCRQSWFZBF4', '2026-06-25 10:24:38', '2026-06-25 10:24:38', NULL, 'e4484230e2202044baaa5ce7a2438bbb', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||||
|
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W3V175EM4F8ZHC6JFM4', '06FFW49W95HJK08BCRQSWFZBF4', '2026-06-25 10:24:38', '2026-06-25 10:24:38', NULL, '24cc5602737e75d30377522f38b3e7b5', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||||
|
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W95HJK08BCRQSWFZBF4', '06FFW49WBTT3ESK6QG0XZ1VN2G', '2026-06-25 10:24:38', '2026-06-25 10:24:38', NULL, '51f4759153b30d2a62ed6c69ab62b84d', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||||
|
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49VYMPF18PYQXD9PCMHN8', '06FFW49WEE5N4PESF5G1G5JRHR', '2026-06-25 10:24:38', '2026-06-25 10:24:38', NULL, 'b348cfc357b73fb28366459795752d8b', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||||
|
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W95HJK08BCRQSWFZBF4', '06FFW49WEE5N4PESF5G1G5JRHR', '2026-06-25 10:24:38', '2026-06-25 10:24:38', NULL, 'e114eff56fef8c92fe3b19df2b8b6e3c', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||||
|
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSQ789MDB00EQAXTXBVJDXM', '06FGSQ8B7WDKZ174GJDGWW0GS0', '2026-06-28 06:14:17.763', '2026-06-28 06:14:17.763', NULL, '973169c6f426d9b4bcfd925ce0f36f32', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||||
|
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSQ8B7WDKZ174GJDGWW0GS0', '06FGSQ8YSZA5YNP1JE8SJHXWBG', '2026-06-28 06:14:17.799', '2026-06-28 06:14:17.799', NULL, '60769ef20f951c01c5d20f512913b946', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||||
|
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2AD3HPR3HXSVASVZEX8PK0', '06FB2ACSDBDZARV3NNGYD9NYYR', '2026-06-28 13:28:39.768', '2026-06-28 13:28:39.768', NULL, 'd5a7084fa717846475fad88d8f30febd', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||||
|
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB234WP4Y6Q16A0HFW8BSXMG', '06FB2ACSDBDZARV3NNGYD9NYYR', '2026-06-28 13:28:39.808', '2026-06-28 13:28:39.808', NULL, '79027c30d19f6f7d43c3068fd80ba8ed', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||||
|
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ETJQP0CT6X7W3CWZ6NS9G', '06FGX26B0NAC9WRJMVB0QE6CV8', '2026-06-28 14:01:13.773', '2026-06-28 14:01:13.773', NULL, '68cfce9178bbb310b87c76a908781c76', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||||
|
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', '06FB2ETJQP0CT6X7W3CWZ6NS9G', '2026-06-10 11:12:20', '2026-06-28 15:29:40.913', '2026-06-28 15:29:40.913', '16d8a936aa3083cd16ca25a57a2e1b86', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||||
|
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ETJQP0CT6X7W3CWZ6NS9G', '06FB2ERREMEEF26KKHGNZBWW64', '2026-06-28 15:29:44.904', '2026-06-28 15:29:44.904', NULL, '545d2ce4f863fd5bb2eee240b696be0c', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||||
|
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', '06FB234WP4Y6Q16A0HFW8BSXMG', '2026-06-10 11:12:11', '2026-06-28 15:29:57.491', NULL, '6e43179e6b211be30766ca56f5ed5885', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||||
|
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', '06FB2AD3HPR3HXSVASVZEX8PK0', '2026-06-28 15:30:00.399', '2026-06-28 15:30:00.399', NULL, 'e0961be9550747530f530a7fd300583f', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||||
|
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', '06FGX26B0NAC9WRJMVB0QE6CV8', '2026-06-28 15:30:03.803', '2026-06-28 15:30:03.803', NULL, 'fa47a4a4aff706215efa3a449681d18f', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||||
|
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2G2KHKT5CJYR0TK1WQGMD0', '06FB2EV29HSK6EJ5VF50R87VC4', '2026-06-28 15:30:10.567', '2026-06-28 15:30:10.567', NULL, '52cd02f3a176cd078db479ef850cbcf4', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||||
|
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGYPJ6FTEPP4JK3D7D01ZSMM', '06FGX26B0NAC9WRJMVB0QE6CV8', '2026-06-28 17:50:13.767', '2026-06-28 17:50:13.767', NULL, '3e0ac1d814ac13a12b68714c7320075f', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||||
|
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2G2KHKT5CJYR0TK1WQGMD0', '06FB2EV29HSK6EJ5VF50R87VC4', '2026-06-28 15:30:10.567', '2026-06-29 21:58:13.907', '2026-06-29 21:58:13.907', '3b7ec3277f971c3646a7587b06606335', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,111 @@
|
|||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2G2KHKT5CJYR0TK1WQGMD0', 'description', 'Tier-5 graph VIEW, absorbed from the former T-7 into the unified canvas epic (T-317). Force-directed layout of the vault (or a filtered subset): nodes are notes, edges are wikilinks. Hover highlights the connected subgraph; click opens the note in the editor. Filter pane: tag include/exclude, file glob, depth-from-active. pql provides the link data (pql backlinks / pql outlinks); rendering is owned in-app (CustomPaint, own-the-rendering-stack). Lives in its own slot per D-47 (context panel), uses MultitabPane (T-83). DISTINCT from the in-card graph TEMPLATE (T-321): T-321 renders a graph passed into the conversation drawing card; this is the full interactive graph pane over the whole vault. Relates to the core renderer (T-318) but has its own force-directed layout.
|
||||||
|
|
||||||
|
PROGRESS (2026-06-29): force-directed layout SOLVER built + tested (lib/src/graph/force_layout.dart, 6 dart-test cases, commit bcef3a6f) — deterministic Fruchterman-Reingold, Flutter-free. REMAINING (the bulk, multi-session): graph data model from pql backlinks/outlinks over the vault; CustomPaint rendering (nodes+edges); pan/zoom; hover-highlight-subgraph; click-opens-note; filter pane (tag/glob/depth); MultitabPane + context-panel slot (D-47) wiring; widget/golden tests. This is a full interactive Tier-5 pane, a fresh-session undertaking — only its solver core is done.', 'Tier-5 graph VIEW, absorbed from the former T-7 into the unified canvas epic (T-317). Force-directed layout of the vault (or a filtered subset): nodes are notes, edges are wikilinks. Hover highlights the connected subgraph; click opens the note in the editor. Filter pane: tag include/exclude, file glob, depth-from-active. pql provides the link data (pql backlinks / pql outlinks); rendering is owned in-app (CustomPaint, own-the-rendering-stack). Lives in its own slot per D-47 (context panel), uses MultitabPane (T-83). DISTINCT from the in-card graph TEMPLATE (T-321): T-321 renders a graph passed into the conversation drawing card; this is the full interactive graph pane over the whole vault. Relates to the core renderer (T-318) but has its own force-directed layout.
|
||||||
|
|
||||||
|
PROGRESS (2026-06-29): force-directed layout SOLVER built + tested (lib/src/graph/force_layout.dart, 6 dart-test cases, commit bcef3a6f) — deterministic Fruchterman-Reingold, Flutter-free. REMAINING (the bulk, multi-session): graph data model from pql backlinks/outlinks over the vault; CustomPaint rendering (nodes+edges); pan/zoom; hover-highlight-subgraph; click-opens-note; filter pane (tag/glob/depth); MultitabPane + context-panel slot (D-47) wiring; widget/golden tests. This is a full interactive Tier-5 pane, a fresh-session undertaking — only its solver core is done.
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02): the graph rendering + interaction foundation is BUILT + tested (4 slices, all committed). (1) force-directed solver — force_layout.dart (bcef3a6f). (2) link-graph model — vault_graph.dart: VaultGraph.fromOutlinks, neighborhood, edgePairs (084bf842). (3) CustomPaint renderer — graph_painter.dart: edges/nodes/labels, hover-dim, fit transform (6bea1723). (4) interactive GraphView — graph_view.dart: hover-highlight + click-to-open, shared GraphViewport hit-testing (c529703b). The graph is a fully working WIDGET (renders + hover + click). REMAINING = the app-integration phase: pql link-data wiring (pql outlinks over the vault -> the file->outlinks map -> VaultGraph), the pane shell (async load + filter: tag/glob/depth + pan/zoom), and MultitabPane + context-panel slot (D-47) + GraphExtension registration.', NULL, '2026-07-02 06:35:03', '2026-07-02 06:35:03.555', '2026-07-02 06:35:03.555', NULL, '33a6fd1c81f2e1142d15ce5791ce3b24', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2G2KHKT5CJYR0TK1WQGMD0', 'description', 'Tier-5 graph VIEW, absorbed from the former T-7 into the unified canvas epic (T-317). Force-directed layout of the vault (or a filtered subset): nodes are notes, edges are wikilinks. Hover highlights the connected subgraph; click opens the note in the editor. Filter pane: tag include/exclude, file glob, depth-from-active. pql provides the link data (pql backlinks / pql outlinks); rendering is owned in-app (CustomPaint, own-the-rendering-stack). Lives in its own slot per D-47 (context panel), uses MultitabPane (T-83). DISTINCT from the in-card graph TEMPLATE (T-321): T-321 renders a graph passed into the conversation drawing card; this is the full interactive graph pane over the whole vault. Relates to the core renderer (T-318) but has its own force-directed layout.
|
||||||
|
|
||||||
|
PROGRESS (2026-06-29): force-directed layout SOLVER built + tested (lib/src/graph/force_layout.dart, 6 dart-test cases, commit bcef3a6f) — deterministic Fruchterman-Reingold, Flutter-free. REMAINING (the bulk, multi-session): graph data model from pql backlinks/outlinks over the vault; CustomPaint rendering (nodes+edges); pan/zoom; hover-highlight-subgraph; click-opens-note; filter pane (tag/glob/depth); MultitabPane + context-panel slot (D-47) wiring; widget/golden tests. This is a full interactive Tier-5 pane, a fresh-session undertaking — only its solver core is done.
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02): the graph rendering + interaction foundation is BUILT + tested (4 slices, all committed). (1) force-directed solver — force_layout.dart (bcef3a6f). (2) link-graph model — vault_graph.dart: VaultGraph.fromOutlinks, neighborhood, edgePairs (084bf842). (3) CustomPaint renderer — graph_painter.dart: edges/nodes/labels, hover-dim, fit transform (6bea1723). (4) interactive GraphView — graph_view.dart: hover-highlight + click-to-open, shared GraphViewport hit-testing (c529703b). The graph is a fully working WIDGET (renders + hover + click). REMAINING = the app-integration phase: pql link-data wiring (pql outlinks over the vault -> the file->outlinks map -> VaultGraph), the pane shell (async load + filter: tag/glob/depth + pan/zoom), and MultitabPane + context-panel slot (D-47) + GraphExtension registration.', 'Tier-5 graph VIEW, absorbed from the former T-7 into the unified canvas epic (T-317). Force-directed layout of the vault (or a filtered subset): nodes are notes, edges are wikilinks. Hover highlights the connected subgraph; click opens the note in the editor. Filter pane: tag include/exclude, file glob, depth-from-active. pql provides the link data (pql backlinks / pql outlinks); rendering is owned in-app (CustomPaint, own-the-rendering-stack). Lives in its own slot per D-47 (context panel), uses MultitabPane (T-83). DISTINCT from the in-card graph TEMPLATE (T-321): T-321 renders a graph passed into the conversation drawing card; this is the full interactive graph pane over the whole vault. Relates to the core renderer (T-318) but has its own force-directed layout.
|
||||||
|
|
||||||
|
PROGRESS (2026-06-29): force-directed layout SOLVER built + tested (lib/src/graph/force_layout.dart, 6 dart-test cases, commit bcef3a6f) — deterministic Fruchterman-Reingold, Flutter-free. REMAINING (the bulk, multi-session): graph data model from pql backlinks/outlinks over the vault; CustomPaint rendering (nodes+edges); pan/zoom; hover-highlight-subgraph; click-opens-note; filter pane (tag/glob/depth); MultitabPane + context-panel slot (D-47) wiring; widget/golden tests. This is a full interactive Tier-5 pane, a fresh-session undertaking — only its solver core is done.
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02): the graph rendering + interaction foundation is BUILT + tested (4 slices, all committed). (1) force-directed solver — force_layout.dart (bcef3a6f). (2) link-graph model — vault_graph.dart: VaultGraph.fromOutlinks, neighborhood, edgePairs (084bf842). (3) CustomPaint renderer — graph_painter.dart: edges/nodes/labels, hover-dim, fit transform (6bea1723). (4) interactive GraphView — graph_view.dart: hover-highlight + click-to-open, shared GraphViewport hit-testing (c529703b). The graph is a fully working WIDGET (renders + hover + click). REMAINING = the app-integration phase: pql link-data wiring (pql outlinks over the vault -> the file->outlinks map -> VaultGraph), the pane shell (async load + filter: tag/glob/depth + pan/zoom), and MultitabPane + context-panel slot (D-47) + GraphExtension registration.
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02, session 2): app-integration phase — 3 slices committed. (1) GraphController (graph_controller.dart, 3b67725b) — pql.files + per-file pql.outlinks over the vault -> VaultGraph; loading/error state; debounced files.changed refresh; 6 tests. (2) GraphPanel shell (graph_panel.dart, 6fae1bc4) — loading/empty/error states + GraphView + click-to-open via editor.open; 5 widget tests. (3) GraphExtension registration (extension.dart + builtin.graph i18n, a5acb6ac) — graph.view context-panel tab (D-47), activates after builtin.pql dep; 4 tests; CHANGELOG entry. The graph is now REACHABLE end-to-end (21 graph tests green). REMAINING (T-323 enrichment): pan/zoom on the view; filter controls (tag include/exclude, glob, depth-from-active).', NULL, '2026-07-02 08:33:27', '2026-07-02 08:33:27.519', '2026-07-02 08:33:27.519', NULL, '65090e3a71c9f5f49b8070a112216210', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FJ4D0GAXM1FN42PJKG7FE34G', 'status', 'backlog', 'done', NULL, '2026-07-02 09:55:52', '2026-07-02 09:55:52.114', '2026-07-02 09:55:52.114', NULL, 'a417eec1fa067a31e46b21578a6228fb', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2G2KHKT5CJYR0TK1WQGMD0', 'description', 'Tier-5 graph VIEW, absorbed from the former T-7 into the unified canvas epic (T-317). Force-directed layout of the vault (or a filtered subset): nodes are notes, edges are wikilinks. Hover highlights the connected subgraph; click opens the note in the editor. Filter pane: tag include/exclude, file glob, depth-from-active. pql provides the link data (pql backlinks / pql outlinks); rendering is owned in-app (CustomPaint, own-the-rendering-stack). Lives in its own slot per D-47 (context panel), uses MultitabPane (T-83). DISTINCT from the in-card graph TEMPLATE (T-321): T-321 renders a graph passed into the conversation drawing card; this is the full interactive graph pane over the whole vault. Relates to the core renderer (T-318) but has its own force-directed layout.
|
||||||
|
|
||||||
|
PROGRESS (2026-06-29): force-directed layout SOLVER built + tested (lib/src/graph/force_layout.dart, 6 dart-test cases, commit bcef3a6f) — deterministic Fruchterman-Reingold, Flutter-free. REMAINING (the bulk, multi-session): graph data model from pql backlinks/outlinks over the vault; CustomPaint rendering (nodes+edges); pan/zoom; hover-highlight-subgraph; click-opens-note; filter pane (tag/glob/depth); MultitabPane + context-panel slot (D-47) wiring; widget/golden tests. This is a full interactive Tier-5 pane, a fresh-session undertaking — only its solver core is done.
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02): the graph rendering + interaction foundation is BUILT + tested (4 slices, all committed). (1) force-directed solver — force_layout.dart (bcef3a6f). (2) link-graph model — vault_graph.dart: VaultGraph.fromOutlinks, neighborhood, edgePairs (084bf842). (3) CustomPaint renderer — graph_painter.dart: edges/nodes/labels, hover-dim, fit transform (6bea1723). (4) interactive GraphView — graph_view.dart: hover-highlight + click-to-open, shared GraphViewport hit-testing (c529703b). The graph is a fully working WIDGET (renders + hover + click). REMAINING = the app-integration phase: pql link-data wiring (pql outlinks over the vault -> the file->outlinks map -> VaultGraph), the pane shell (async load + filter: tag/glob/depth + pan/zoom), and MultitabPane + context-panel slot (D-47) + GraphExtension registration.
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02, session 2): app-integration phase — 3 slices committed. (1) GraphController (graph_controller.dart, 3b67725b) — pql.files + per-file pql.outlinks over the vault -> VaultGraph; loading/error state; debounced files.changed refresh; 6 tests. (2) GraphPanel shell (graph_panel.dart, 6fae1bc4) — loading/empty/error states + GraphView + click-to-open via editor.open; 5 widget tests. (3) GraphExtension registration (extension.dart + builtin.graph i18n, a5acb6ac) — graph.view context-panel tab (D-47), activates after builtin.pql dep; 4 tests; CHANGELOG entry. The graph is now REACHABLE end-to-end (21 graph tests green). REMAINING (T-323 enrichment): pan/zoom on the view; filter controls (tag include/exclude, glob, depth-from-active).', 'Tier-5 graph VIEW, absorbed from the former T-7 into the unified canvas epic (T-317). Force-directed layout of the vault (or a filtered subset): nodes are notes, edges are wikilinks. Hover highlights the connected subgraph; click opens the note in the editor. Filter pane: tag include/exclude, file glob, depth-from-active. pql provides the link data (pql backlinks / pql outlinks); rendering is owned in-app (CustomPaint, own-the-rendering-stack). Lives in its own slot per D-47 (context panel), uses MultitabPane (T-83). DISTINCT from the in-card graph TEMPLATE (T-321): T-321 renders a graph passed into the conversation drawing card; this is the full interactive graph pane over the whole vault. Relates to the core renderer (T-318) but has its own force-directed layout.
|
||||||
|
|
||||||
|
PROGRESS (2026-06-29): force-directed layout SOLVER built + tested (lib/src/graph/force_layout.dart, 6 dart-test cases, commit bcef3a6f) — deterministic Fruchterman-Reingold, Flutter-free. REMAINING (the bulk, multi-session): graph data model from pql backlinks/outlinks over the vault; CustomPaint rendering (nodes+edges); pan/zoom; hover-highlight-subgraph; click-opens-note; filter pane (tag/glob/depth); MultitabPane + context-panel slot (D-47) wiring; widget/golden tests. This is a full interactive Tier-5 pane, a fresh-session undertaking — only its solver core is done.
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02): the graph rendering + interaction foundation is BUILT + tested (4 slices, all committed). (1) force-directed solver — force_layout.dart (bcef3a6f). (2) link-graph model — vault_graph.dart: VaultGraph.fromOutlinks, neighborhood, edgePairs (084bf842). (3) CustomPaint renderer — graph_painter.dart: edges/nodes/labels, hover-dim, fit transform (6bea1723). (4) interactive GraphView — graph_view.dart: hover-highlight + click-to-open, shared GraphViewport hit-testing (c529703b). The graph is a fully working WIDGET (renders + hover + click). REMAINING = the app-integration phase: pql link-data wiring (pql outlinks over the vault -> the file->outlinks map -> VaultGraph), the pane shell (async load + filter: tag/glob/depth + pan/zoom), and MultitabPane + context-panel slot (D-47) + GraphExtension registration.
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02, session 2): app-integration phase — 3 slices committed. (1) GraphController (graph_controller.dart, 3b67725b) — pql.files + per-file pql.outlinks over the vault -> VaultGraph; loading/error state; debounced files.changed refresh; 6 tests. (2) GraphPanel shell (graph_panel.dart, 6fae1bc4) — loading/empty/error states + GraphView + click-to-open via editor.open; 5 widget tests. (3) GraphExtension registration (extension.dart + builtin.graph i18n, a5acb6ac) — graph.view context-panel tab (D-47), activates after builtin.pql dep; 4 tests; CHANGELOG entry. The graph is now REACHABLE end-to-end (21 graph tests green). REMAINING (T-323 enrichment): pan/zoom on the view; filter controls (tag include/exclude, glob, depth-from-active).
|
||||||
|
|
||||||
|
COMPLETE (2026-07-02, session 2). All scope delivered + tested (56 graph tests, make test green, real-boot verified via testmode). Slices this session: GraphController (3b67725b), pane shell (6fae1bc4), extension+i18n registration (a5acb6ac), pan+zoom (e6556401), testmode coverage (46d9b403), filter model (f859173b), filter bar (aabec87d). Final feature: force-directed vault graph in the context panel (graph.view, D-47) — hover-highlight, click-to-open, scroll-zoom, drag-pan, and filters (path glob re-queries pql; depth-from-active local graph; tri-state tag include/exclude). Controller uses pql.meta (outlinks+tags in one call) and strips #heading fragments so heading links connect. NOTE: pql data load is 1 files + N meta calls (sequential); batching is a future optimisation if large vaults feel slow.', NULL, '2026-07-02 19:47:37', '2026-07-02 19:47:37.694', '2026-07-02 19:47:37.694', NULL, 'a9f6b8b38858b687f106d1bda236821d', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2G2KHKT5CJYR0TK1WQGMD0', 'status', 'in_progress', 'done', NULL, '2026-07-02 19:47:44', '2026-07-02 19:47:44.357', '2026-07-02 19:47:44.357', NULL, 'c8bf520d62290050e6d0feb4a9a6a68d', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2G1WD1839Z90AQ5C0BHNV4', 'description', 'Tier-5 canvas PANE, absorbed from the former T-7 into the unified canvas epic (T-317, decision D-91). A full workspace pane that renders .canvas files by CONVERTING them into the drawing-card JSON (per D-91: .canvas is an import format, not a native schema) and painting via the shared canvas renderer (T-318). Scope: nodes (note, text, group, image) + edges + layout state; pan/zoom; node selection, drag, resize; edit affordances (add note from file picker, add text node, draw edge between nodes); persist layout back to the .canvas file on disk. Uses MultitabPane (T-83) for tabs and its own slot per D-47; panels are extension-shaped (D-17). Depends on the core renderer (T-318). NOTE: unlike the conversation drawing card (display-only, D-78), this pane is interactive/editable — it is a pane, not a conversation widget.
|
||||||
|
|
||||||
|
SCOPE NOTE (2026-06-29): currently a 17-line stub. This is a from-scratch interactive Tier-5 pane (parse .canvas -> drawing-card JSON, CustomPaint render, pan/zoom, node select/drag/resize, edit affordances, persist .canvas) — a multi-session feature, NOT template-class work. The drawing-card TEMPLATE half of epic T-317 is now complete (svg/d2/image/icon/compare/graph + stdin).', 'Tier-5 canvas PANE, absorbed from the former T-7 into the unified canvas epic (T-317, decision D-91). A full workspace pane that renders .canvas files by CONVERTING them into the drawing-card JSON (per D-91: .canvas is an import format, not a native schema) and painting via the shared canvas renderer (T-318). Scope: nodes (note, text, group, image) + edges + layout state; pan/zoom; node selection, drag, resize; edit affordances (add note from file picker, add text node, draw edge between nodes); persist layout back to the .canvas file on disk. Uses MultitabPane (T-83) for tabs and its own slot per D-47; panels are extension-shaped (D-17). Depends on the core renderer (T-318). NOTE: unlike the conversation drawing card (display-only, D-78), this pane is interactive/editable — it is a pane, not a conversation widget.
|
||||||
|
|
||||||
|
SCOPE NOTE (2026-06-29): currently a 17-line stub. This is a from-scratch interactive Tier-5 pane (parse .canvas -> drawing-card JSON, CustomPaint render, pan/zoom, node select/drag/resize, edit affordances, persist .canvas) — a multi-session feature, NOT template-class work. The drawing-card TEMPLATE half of epic T-317 is now complete (svg/d2/image/icon/compare/graph + stdin).
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02): two foundation slices built + tested (from the 17-line stub). (1) JSONCanvas parser/model — lib/src/canvas/json_canvas.dart (2eb5d0f1): typed CanvasDoc (text/file/link/group nodes + edges with sides/ends/color/label), parse + toJson round-trip, skips unknown/malformed; pure Dart, 8 dart-test cases. (2) CustomPaint renderer — lib/builtin/canvas/src/canvas_painter.dart (035a37a1): CanvasBounds + pan/zoom CanvasViewport, group frames + edges (arrowheads) + node cards; node colours from the file (Obsidian presets/hex, NOT clide tokens per arbitrary-content rule), chrome themed; 7 tests (pixel-probe + viewport + colour). DECISION NOTE: the interactive pane paints the CanvasDoc model directly via CustomPaint (hit-testable, cheap re-paint) rather than re-lowering to SVG each frame — the ticket''s ''convert to drawing-card JSON'' path (D-91 SVG substrate) is for the display-only viewer; an editable pane needs direct model painting. Mirrors the graph pane. REMAINING (multi-session): interactive CanvasView (hit-test nodes + pan/zoom + click-select), pane shell (load .canvas from disk, workspace slot D-47, MultitabPane), CanvasExtension registration + open-.canvas routing, then edit affordances (add node/edge, drag, resize) + persist back to disk.', NULL, '2026-07-02 20:27:03', '2026-07-02 20:27:03.493', '2026-07-02 20:27:03.493', NULL, '7a661d8cc999f526e26cb1a14b7839c4', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FJ91RYFXBJH99HJ18QTHY2B0', 'description', NULL, 'REPORTED (2026-07-02, Jeroen): can''t trust that the model switch and the effort toggle actually took effect when used — the UI feedback + positioning leave it uncertain whether the change applied. INVESTIGATE: what confirmation (if any) fires today when the model or effort is changed, and where it surfaces relative to the control. Likely-related surfaces: the Config tab controls (T-414), /effort ownership (T-412), account/model settings control. IMPROVE: give an unmistakable, well-positioned confirmation that the change landed — e.g. the control reflects the new value immediately (selected state), and/or a brief toast/inline acknowledgement near the control, not somewhere the eye isn''t. Goal: after a switch, the user is certain which model/effort is now active.', NULL, '2026-07-02 20:31:02', '2026-07-02 20:31:02.807', '2026-07-02 20:31:02.807', NULL, '5030849d297f6312073802a0641a2594', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2G1WD1839Z90AQ5C0BHNV4', 'status', 'backlog', 'in_progress', NULL, '2026-07-02 20:35:12', '2026-07-02 20:35:12.739', '2026-07-02 20:35:12.739', NULL, 'd971e23e05c7547672b6989ca0a98434', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FJ91RYFXBJH99HJ18QTHY2B0', 'description', 'REPORTED (2026-07-02, Jeroen): can''t trust that the model switch and the effort toggle actually took effect when used — the UI feedback + positioning leave it uncertain whether the change applied. INVESTIGATE: what confirmation (if any) fires today when the model or effort is changed, and where it surfaces relative to the control. Likely-related surfaces: the Config tab controls (T-414), /effort ownership (T-412), account/model settings control. IMPROVE: give an unmistakable, well-positioned confirmation that the change landed — e.g. the control reflects the new value immediately (selected state), and/or a brief toast/inline acknowledgement near the control, not somewhere the eye isn''t. Goal: after a switch, the user is certain which model/effort is now active.', 'REPORTED (2026-07-02, Jeroen): can''t trust that the model switch and the effort toggle actually took effect when used — the UI feedback + positioning leave it uncertain whether the change applied. INVESTIGATE: what confirmation (if any) fires today when the model or effort is changed, and where it surfaces relative to the control. Likely-related surfaces: the Config tab controls (T-414), /effort ownership (T-412), account/model settings control. IMPROVE: give an unmistakable, well-positioned confirmation that the change landed — e.g. the control reflects the new value immediately (selected state), and/or a brief toast/inline acknowledgement near the control, not somewhere the eye isn''t. Goal: after a switch, the user is certain which model/effort is now active.
|
||||||
|
|
||||||
|
EVIDENCE + ROOT CAUSE (2026-07-02, from a /model screenshot). The confirmation renders as a raw, unstyled line: literally ''<local-command-stdout>Set model to claude-fable-5[1m] (claude-fable-5)</local-command-stdout>'', shown under the ''you'' (user) speaker stripe. Three distinct defects in that one line: (a) the CLI''s <local-command-stdout> wrapper tag is displayed verbatim instead of being unwrapped; (b) an ANSI SGR bold code leaks through as literal ''[1m]'' (a \x1b[1m) — slash-command stdout is not ANSI-stripped; (c) it is attributed to the USER speaker, so a system/command acknowledgement masquerades as something the user typed. ROOT CAUSE: clide has NO handling for local-command-stdout at all (grep in lib/ = zero hits) — the slash-command result falls through and is rendered as plain user prose. ENTRY POINTS for the fix: inbound message parse / role attribution in lib/builtin/claude/src/transcript_reader.dart; existing ANSI-strip refs in transcript_reader.dart + claude_composer.dart. IMPROVE: detect local-command-stdout content, unwrap the tag + strip ANSI, and render it as a distinct, clearly-positioned system/command-acknowledgement (NOT the user stripe) so a /model or /effort change is an unmistakable confirmation. Affects all slash-command stdout (model, effort, etc.), so fix at the render/parse path, not per-command.', NULL, '2026-07-02 20:39:06', '2026-07-02 20:39:06.684', '2026-07-02 20:39:06.684', NULL, '02ea6e2dbb60929aed687275f5329a4f', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FJ91RYFXBJH99HJ18QTHY2B0', 'description', 'REPORTED (2026-07-02, Jeroen): can''t trust that the model switch and the effort toggle actually took effect when used — the UI feedback + positioning leave it uncertain whether the change applied. INVESTIGATE: what confirmation (if any) fires today when the model or effort is changed, and where it surfaces relative to the control. Likely-related surfaces: the Config tab controls (T-414), /effort ownership (T-412), account/model settings control. IMPROVE: give an unmistakable, well-positioned confirmation that the change landed — e.g. the control reflects the new value immediately (selected state), and/or a brief toast/inline acknowledgement near the control, not somewhere the eye isn''t. Goal: after a switch, the user is certain which model/effort is now active.
|
||||||
|
|
||||||
|
EVIDENCE + ROOT CAUSE (2026-07-02, from a /model screenshot). The confirmation renders as a raw, unstyled line: literally ''<local-command-stdout>Set model to claude-fable-5[1m] (claude-fable-5)</local-command-stdout>'', shown under the ''you'' (user) speaker stripe. Three distinct defects in that one line: (a) the CLI''s <local-command-stdout> wrapper tag is displayed verbatim instead of being unwrapped; (b) an ANSI SGR bold code leaks through as literal ''[1m]'' (a \x1b[1m) — slash-command stdout is not ANSI-stripped; (c) it is attributed to the USER speaker, so a system/command acknowledgement masquerades as something the user typed. ROOT CAUSE: clide has NO handling for local-command-stdout at all (grep in lib/ = zero hits) — the slash-command result falls through and is rendered as plain user prose. ENTRY POINTS for the fix: inbound message parse / role attribution in lib/builtin/claude/src/transcript_reader.dart; existing ANSI-strip refs in transcript_reader.dart + claude_composer.dart. IMPROVE: detect local-command-stdout content, unwrap the tag + strip ANSI, and render it as a distinct, clearly-positioned system/command-acknowledgement (NOT the user stripe) so a /model or /effort change is an unmistakable confirmation. Affects all slash-command stdout (model, effort, etc.), so fix at the render/parse path, not per-command.', 'REPORTED (2026-07-02, Jeroen): can''t trust that the model switch and the effort toggle actually took effect when used — the UI feedback + positioning leave it uncertain whether the change applied. INVESTIGATE: what confirmation (if any) fires today when the model or effort is changed, and where it surfaces relative to the control. Likely-related surfaces: the Config tab controls (T-414), /effort ownership (T-412), account/model settings control. IMPROVE: give an unmistakable, well-positioned confirmation that the change landed — e.g. the control reflects the new value immediately (selected state), and/or a brief toast/inline acknowledgement near the control, not somewhere the eye isn''t. Goal: after a switch, the user is certain which model/effort is now active.
|
||||||
|
|
||||||
|
EVIDENCE + ROOT CAUSE (2026-07-02, from a /model screenshot). The confirmation renders as a raw, unstyled line: literally ''<local-command-stdout>Set model to claude-fable-5[1m] (claude-fable-5)</local-command-stdout>'', shown under the ''you'' (user) speaker stripe. Three distinct defects in that one line: (a) the CLI''s <local-command-stdout> wrapper tag is displayed verbatim instead of being unwrapped; (b) an ANSI SGR bold code leaks through as literal ''[1m]'' (a \x1b[1m) — slash-command stdout is not ANSI-stripped; (c) it is attributed to the USER speaker, so a system/command acknowledgement masquerades as something the user typed. ROOT CAUSE: clide has NO handling for local-command-stdout at all (grep in lib/ = zero hits) — the slash-command result falls through and is rendered as plain user prose. ENTRY POINTS for the fix: inbound message parse / role attribution in lib/builtin/claude/src/transcript_reader.dart; existing ANSI-strip refs in transcript_reader.dart + claude_composer.dart. IMPROVE: detect local-command-stdout content, unwrap the tag + strip ANSI, and render it as a distinct, clearly-positioned system/command-acknowledgement (NOT the user stripe) so a /model or /effort change is an unmistakable confirmation. Affects all slash-command stdout (model, effort, etc.), so fix at the render/parse path, not per-command.
|
||||||
|
|
||||||
|
SCOPE (do not narrow): the local-command-stdout rendering defect above is ONE symptom, not the whole ticket. The core problem is that after switching model or effort the user has no reliable, well-placed signal that it took effect — so this stays a broad investigate-and-improve of the switch/toggle trust UX, with the render bug as just the first concrete instance. Other symptoms/questions to investigate (non-exhaustive): does the control itself (Config tab picker / effort control / any menu) visibly update to the NEW active value after a switch, or does it look unchanged? is there ANY acknowledgement at the point of interaction, or only (mangled) output buried in the transcript far from where the user clicked? do model vs effort behave consistently, or differently? can clide''s shown ''active'' value drift from what actually took effect (e.g. a switch that silently no-ops)? is the confirmation positioned where the eye already is? The deliverable is: across all these controls, a consistent and unmistakable ''this is now active'' — fixing the render path is necessary but not sufficient. Enumerate the full symptom set during investigation before designing the fix.', NULL, '2026-07-02 20:41:13', '2026-07-02 20:41:13.402', '2026-07-02 20:41:13.402', NULL, '1c7d99c842cc857d5978d91e5b23ab8b', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2G1WD1839Z90AQ5C0BHNV4', 'description', 'Tier-5 canvas PANE, absorbed from the former T-7 into the unified canvas epic (T-317, decision D-91). A full workspace pane that renders .canvas files by CONVERTING them into the drawing-card JSON (per D-91: .canvas is an import format, not a native schema) and painting via the shared canvas renderer (T-318). Scope: nodes (note, text, group, image) + edges + layout state; pan/zoom; node selection, drag, resize; edit affordances (add note from file picker, add text node, draw edge between nodes); persist layout back to the .canvas file on disk. Uses MultitabPane (T-83) for tabs and its own slot per D-47; panels are extension-shaped (D-17). Depends on the core renderer (T-318). NOTE: unlike the conversation drawing card (display-only, D-78), this pane is interactive/editable — it is a pane, not a conversation widget.
|
||||||
|
|
||||||
|
SCOPE NOTE (2026-06-29): currently a 17-line stub. This is a from-scratch interactive Tier-5 pane (parse .canvas -> drawing-card JSON, CustomPaint render, pan/zoom, node select/drag/resize, edit affordances, persist .canvas) — a multi-session feature, NOT template-class work. The drawing-card TEMPLATE half of epic T-317 is now complete (svg/d2/image/icon/compare/graph + stdin).
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02): two foundation slices built + tested (from the 17-line stub). (1) JSONCanvas parser/model — lib/src/canvas/json_canvas.dart (2eb5d0f1): typed CanvasDoc (text/file/link/group nodes + edges with sides/ends/color/label), parse + toJson round-trip, skips unknown/malformed; pure Dart, 8 dart-test cases. (2) CustomPaint renderer — lib/builtin/canvas/src/canvas_painter.dart (035a37a1): CanvasBounds + pan/zoom CanvasViewport, group frames + edges (arrowheads) + node cards; node colours from the file (Obsidian presets/hex, NOT clide tokens per arbitrary-content rule), chrome themed; 7 tests (pixel-probe + viewport + colour). DECISION NOTE: the interactive pane paints the CanvasDoc model directly via CustomPaint (hit-testable, cheap re-paint) rather than re-lowering to SVG each frame — the ticket''s ''convert to drawing-card JSON'' path (D-91 SVG substrate) is for the display-only viewer; an editable pane needs direct model painting. Mirrors the graph pane. REMAINING (multi-session): interactive CanvasView (hit-test nodes + pan/zoom + click-select), pane shell (load .canvas from disk, workspace slot D-47, MultitabPane), CanvasExtension registration + open-.canvas routing, then edit affordances (add node/edge, drag, resize) + persist back to disk.', 'Tier-5 canvas PANE, absorbed from the former T-7 into the unified canvas epic (T-317, decision D-91). A full workspace pane that renders .canvas files by CONVERTING them into the drawing-card JSON (per D-91: .canvas is an import format, not a native schema) and painting via the shared canvas renderer (T-318). Scope: nodes (note, text, group, image) + edges + layout state; pan/zoom; node selection, drag, resize; edit affordances (add note from file picker, add text node, draw edge between nodes); persist layout back to the .canvas file on disk. Uses MultitabPane (T-83) for tabs and its own slot per D-47; panels are extension-shaped (D-17). Depends on the core renderer (T-318). NOTE: unlike the conversation drawing card (display-only, D-78), this pane is interactive/editable — it is a pane, not a conversation widget.
|
||||||
|
|
||||||
|
SCOPE NOTE (2026-06-29): currently a 17-line stub. This is a from-scratch interactive Tier-5 pane (parse .canvas -> drawing-card JSON, CustomPaint render, pan/zoom, node select/drag/resize, edit affordances, persist .canvas) — a multi-session feature, NOT template-class work. The drawing-card TEMPLATE half of epic T-317 is now complete (svg/d2/image/icon/compare/graph + stdin).
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02): two foundation slices built + tested (from the 17-line stub). (1) JSONCanvas parser/model — lib/src/canvas/json_canvas.dart (2eb5d0f1): typed CanvasDoc (text/file/link/group nodes + edges with sides/ends/color/label), parse + toJson round-trip, skips unknown/malformed; pure Dart, 8 dart-test cases. (2) CustomPaint renderer — lib/builtin/canvas/src/canvas_painter.dart (035a37a1): CanvasBounds + pan/zoom CanvasViewport, group frames + edges (arrowheads) + node cards; node colours from the file (Obsidian presets/hex, NOT clide tokens per arbitrary-content rule), chrome themed; 7 tests (pixel-probe + viewport + colour). DECISION NOTE: the interactive pane paints the CanvasDoc model directly via CustomPaint (hit-testable, cheap re-paint) rather than re-lowering to SVG each frame — the ticket''s ''convert to drawing-card JSON'' path (D-91 SVG substrate) is for the display-only viewer; an editable pane needs direct model painting. Mirrors the graph pane. REMAINING (multi-session): interactive CanvasView (hit-test nodes + pan/zoom + click-select), pane shell (load .canvas from disk, workspace slot D-47, MultitabPane), CanvasExtension registration + open-.canvas routing, then edit affordances (add node/edge, drag, resize) + persist back to disk.
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02, cont.): third foundation slice landed — interactive CanvasView (lib/builtin/canvas/src/canvas_view.dart, 7700fc7a): hitTestCanvasNode (topmost node under a point; cards beat the group frames behind, via the painter''s own viewport) + scroll-zoom + drag-pan + click-select with a focus ring; 6 tests. The canvas rendering + interaction FOUNDATION is now complete + tested (parser 2eb5d0f1, renderer 035a37a1, view 7700fc7a — 21 canvas tests), mirroring where T-323''s foundation was before its integration phase. NOT yet reachable. REACHABLE-INTEGRATION PHASE (next, fresh session): (a) pane shell — a StatefulWidget that loads a .canvas from disk (files.read/editor.open path), holds the CanvasDoc, embeds CanvasView; (b) CanvasExtension registration as a workspace pane (own slot D-47, MultitabPane T-83); (c) FILE-OPEN ROUTING — how opening a .canvas lands in this pane instead of the text editor: NOTE the TabContribution.fileGlobs field is DECLARED but has ZERO consumers in lib/, so this routing must be built (wire fileGlobs, or have the editor delegate by extension, or a command + workspace tab); (d) i18n + tests. THEN the edit phase: add node/text/edge affordances, node drag + resize, and persist the CanvasDoc back to the .canvas file (encode() already exists).', NULL, '2026-07-02 21:22:59', '2026-07-02 21:22:59.395', '2026-07-02 21:22:59.395', NULL, '6b17245b0d3d39a0d61bc90a2a0a034b', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2G1WD1839Z90AQ5C0BHNV4', 'description', 'Tier-5 canvas PANE, absorbed from the former T-7 into the unified canvas epic (T-317, decision D-91). A full workspace pane that renders .canvas files by CONVERTING them into the drawing-card JSON (per D-91: .canvas is an import format, not a native schema) and painting via the shared canvas renderer (T-318). Scope: nodes (note, text, group, image) + edges + layout state; pan/zoom; node selection, drag, resize; edit affordances (add note from file picker, add text node, draw edge between nodes); persist layout back to the .canvas file on disk. Uses MultitabPane (T-83) for tabs and its own slot per D-47; panels are extension-shaped (D-17). Depends on the core renderer (T-318). NOTE: unlike the conversation drawing card (display-only, D-78), this pane is interactive/editable — it is a pane, not a conversation widget.
|
||||||
|
|
||||||
|
SCOPE NOTE (2026-06-29): currently a 17-line stub. This is a from-scratch interactive Tier-5 pane (parse .canvas -> drawing-card JSON, CustomPaint render, pan/zoom, node select/drag/resize, edit affordances, persist .canvas) — a multi-session feature, NOT template-class work. The drawing-card TEMPLATE half of epic T-317 is now complete (svg/d2/image/icon/compare/graph + stdin).
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02): two foundation slices built + tested (from the 17-line stub). (1) JSONCanvas parser/model — lib/src/canvas/json_canvas.dart (2eb5d0f1): typed CanvasDoc (text/file/link/group nodes + edges with sides/ends/color/label), parse + toJson round-trip, skips unknown/malformed; pure Dart, 8 dart-test cases. (2) CustomPaint renderer — lib/builtin/canvas/src/canvas_painter.dart (035a37a1): CanvasBounds + pan/zoom CanvasViewport, group frames + edges (arrowheads) + node cards; node colours from the file (Obsidian presets/hex, NOT clide tokens per arbitrary-content rule), chrome themed; 7 tests (pixel-probe + viewport + colour). DECISION NOTE: the interactive pane paints the CanvasDoc model directly via CustomPaint (hit-testable, cheap re-paint) rather than re-lowering to SVG each frame — the ticket''s ''convert to drawing-card JSON'' path (D-91 SVG substrate) is for the display-only viewer; an editable pane needs direct model painting. Mirrors the graph pane. REMAINING (multi-session): interactive CanvasView (hit-test nodes + pan/zoom + click-select), pane shell (load .canvas from disk, workspace slot D-47, MultitabPane), CanvasExtension registration + open-.canvas routing, then edit affordances (add node/edge, drag, resize) + persist back to disk.
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02, cont.): third foundation slice landed — interactive CanvasView (lib/builtin/canvas/src/canvas_view.dart, 7700fc7a): hitTestCanvasNode (topmost node under a point; cards beat the group frames behind, via the painter''s own viewport) + scroll-zoom + drag-pan + click-select with a focus ring; 6 tests. The canvas rendering + interaction FOUNDATION is now complete + tested (parser 2eb5d0f1, renderer 035a37a1, view 7700fc7a — 21 canvas tests), mirroring where T-323''s foundation was before its integration phase. NOT yet reachable. REACHABLE-INTEGRATION PHASE (next, fresh session): (a) pane shell — a StatefulWidget that loads a .canvas from disk (files.read/editor.open path), holds the CanvasDoc, embeds CanvasView; (b) CanvasExtension registration as a workspace pane (own slot D-47, MultitabPane T-83); (c) FILE-OPEN ROUTING — how opening a .canvas lands in this pane instead of the text editor: NOTE the TabContribution.fileGlobs field is DECLARED but has ZERO consumers in lib/, so this routing must be built (wire fileGlobs, or have the editor delegate by extension, or a command + workspace tab); (d) i18n + tests. THEN the edit phase: add node/text/edge affordances, node drag + resize, and persist the CanvasDoc back to the .canvas file (encode() already exists).', 'Tier-5 canvas PANE, absorbed from the former T-7 into the unified canvas epic (T-317, decision D-91). A full workspace pane that renders .canvas files by CONVERTING them into the drawing-card JSON (per D-91: .canvas is an import format, not a native schema) and painting via the shared canvas renderer (T-318). Scope: nodes (note, text, group, image) + edges + layout state; pan/zoom; node selection, drag, resize; edit affordances (add note from file picker, add text node, draw edge between nodes); persist layout back to the .canvas file on disk. Uses MultitabPane (T-83) for tabs and its own slot per D-47; panels are extension-shaped (D-17). Depends on the core renderer (T-318). NOTE: unlike the conversation drawing card (display-only, D-78), this pane is interactive/editable — it is a pane, not a conversation widget.
|
||||||
|
|
||||||
|
SCOPE NOTE (2026-06-29): currently a 17-line stub. This is a from-scratch interactive Tier-5 pane (parse .canvas -> drawing-card JSON, CustomPaint render, pan/zoom, node select/drag/resize, edit affordances, persist .canvas) — a multi-session feature, NOT template-class work. The drawing-card TEMPLATE half of epic T-317 is now complete (svg/d2/image/icon/compare/graph + stdin).
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02): two foundation slices built + tested (from the 17-line stub). (1) JSONCanvas parser/model — lib/src/canvas/json_canvas.dart (2eb5d0f1): typed CanvasDoc (text/file/link/group nodes + edges with sides/ends/color/label), parse + toJson round-trip, skips unknown/malformed; pure Dart, 8 dart-test cases. (2) CustomPaint renderer — lib/builtin/canvas/src/canvas_painter.dart (035a37a1): CanvasBounds + pan/zoom CanvasViewport, group frames + edges (arrowheads) + node cards; node colours from the file (Obsidian presets/hex, NOT clide tokens per arbitrary-content rule), chrome themed; 7 tests (pixel-probe + viewport + colour). DECISION NOTE: the interactive pane paints the CanvasDoc model directly via CustomPaint (hit-testable, cheap re-paint) rather than re-lowering to SVG each frame — the ticket''s ''convert to drawing-card JSON'' path (D-91 SVG substrate) is for the display-only viewer; an editable pane needs direct model painting. Mirrors the graph pane. REMAINING (multi-session): interactive CanvasView (hit-test nodes + pan/zoom + click-select), pane shell (load .canvas from disk, workspace slot D-47, MultitabPane), CanvasExtension registration + open-.canvas routing, then edit affordances (add node/edge, drag, resize) + persist back to disk.
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02, cont.): third foundation slice landed — interactive CanvasView (lib/builtin/canvas/src/canvas_view.dart, 7700fc7a): hitTestCanvasNode (topmost node under a point; cards beat the group frames behind, via the painter''s own viewport) + scroll-zoom + drag-pan + click-select with a focus ring; 6 tests. The canvas rendering + interaction FOUNDATION is now complete + tested (parser 2eb5d0f1, renderer 035a37a1, view 7700fc7a — 21 canvas tests), mirroring where T-323''s foundation was before its integration phase. NOT yet reachable. REACHABLE-INTEGRATION PHASE (next, fresh session): (a) pane shell — a StatefulWidget that loads a .canvas from disk (files.read/editor.open path), holds the CanvasDoc, embeds CanvasView; (b) CanvasExtension registration as a workspace pane (own slot D-47, MultitabPane T-83); (c) FILE-OPEN ROUTING — how opening a .canvas lands in this pane instead of the text editor: NOTE the TabContribution.fileGlobs field is DECLARED but has ZERO consumers in lib/, so this routing must be built (wire fileGlobs, or have the editor delegate by extension, or a command + workspace tab); (d) i18n + tests. THEN the edit phase: add node/text/edge affordances, node drag + resize, and persist the CanvasDoc back to the .canvas file (encode() already exists).
|
||||||
|
|
||||||
|
Refinement (2026-07-03, user-confirmed): (1) SLOT: Slots.workspace — diff/editor recipe, center column; coexists with Claude the way diff does (D-47). (2) TABS: real MultitabPane<CanvasDoc> — N .canvas files open as tabs (user explicitly chose over the single-TabContribution graph/diff precedent; needs MultitabController plumbing like Claude sessions). (3) FILE-OPEN ROUTING resolved by inspection: TabContribution.fileGlobs is dead (zero consumers) — do NOT wire it. Add a .canvas branch in lib/kernel/src/file_open.dart openWorkspaceFile() publishing to ''builtin.canvas''/''selection'' (mirror of the .md branch), and D-6 CLI parity via a ''canvas'' entry in the _readers map at lib/src/daemon/ui_command.dart:34 → clide ui open canvas <path> (diff precedent). (4) Fix stale doc header lib/src/canvas/json_canvas.dart:5-7 (claims SVG-lowering; actual architecture paints CanvasDoc directly per the ticket''s DECISION NOTE) in the same pass. (5) Edit phase later needs a mutation path (CanvasView has no onChanged; CanvasDoc is immutable-construction only) — deferred, flagged now.', NULL, '2026-07-03 20:20:00', '2026-07-03 20:20:00.827', '2026-07-03 20:20:00.827', NULL, '541312e6b1f559f60934ad1a1b784d76', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2G1WD1839Z90AQ5C0BHNV4', 'description', 'Tier-5 canvas PANE, absorbed from the former T-7 into the unified canvas epic (T-317, decision D-91). A full workspace pane that renders .canvas files by CONVERTING them into the drawing-card JSON (per D-91: .canvas is an import format, not a native schema) and painting via the shared canvas renderer (T-318). Scope: nodes (note, text, group, image) + edges + layout state; pan/zoom; node selection, drag, resize; edit affordances (add note from file picker, add text node, draw edge between nodes); persist layout back to the .canvas file on disk. Uses MultitabPane (T-83) for tabs and its own slot per D-47; panels are extension-shaped (D-17). Depends on the core renderer (T-318). NOTE: unlike the conversation drawing card (display-only, D-78), this pane is interactive/editable — it is a pane, not a conversation widget.
|
||||||
|
|
||||||
|
SCOPE NOTE (2026-06-29): currently a 17-line stub. This is a from-scratch interactive Tier-5 pane (parse .canvas -> drawing-card JSON, CustomPaint render, pan/zoom, node select/drag/resize, edit affordances, persist .canvas) — a multi-session feature, NOT template-class work. The drawing-card TEMPLATE half of epic T-317 is now complete (svg/d2/image/icon/compare/graph + stdin).
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02): two foundation slices built + tested (from the 17-line stub). (1) JSONCanvas parser/model — lib/src/canvas/json_canvas.dart (2eb5d0f1): typed CanvasDoc (text/file/link/group nodes + edges with sides/ends/color/label), parse + toJson round-trip, skips unknown/malformed; pure Dart, 8 dart-test cases. (2) CustomPaint renderer — lib/builtin/canvas/src/canvas_painter.dart (035a37a1): CanvasBounds + pan/zoom CanvasViewport, group frames + edges (arrowheads) + node cards; node colours from the file (Obsidian presets/hex, NOT clide tokens per arbitrary-content rule), chrome themed; 7 tests (pixel-probe + viewport + colour). DECISION NOTE: the interactive pane paints the CanvasDoc model directly via CustomPaint (hit-testable, cheap re-paint) rather than re-lowering to SVG each frame — the ticket''s ''convert to drawing-card JSON'' path (D-91 SVG substrate) is for the display-only viewer; an editable pane needs direct model painting. Mirrors the graph pane. REMAINING (multi-session): interactive CanvasView (hit-test nodes + pan/zoom + click-select), pane shell (load .canvas from disk, workspace slot D-47, MultitabPane), CanvasExtension registration + open-.canvas routing, then edit affordances (add node/edge, drag, resize) + persist back to disk.
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02, cont.): third foundation slice landed — interactive CanvasView (lib/builtin/canvas/src/canvas_view.dart, 7700fc7a): hitTestCanvasNode (topmost node under a point; cards beat the group frames behind, via the painter''s own viewport) + scroll-zoom + drag-pan + click-select with a focus ring; 6 tests. The canvas rendering + interaction FOUNDATION is now complete + tested (parser 2eb5d0f1, renderer 035a37a1, view 7700fc7a — 21 canvas tests), mirroring where T-323''s foundation was before its integration phase. NOT yet reachable. REACHABLE-INTEGRATION PHASE (next, fresh session): (a) pane shell — a StatefulWidget that loads a .canvas from disk (files.read/editor.open path), holds the CanvasDoc, embeds CanvasView; (b) CanvasExtension registration as a workspace pane (own slot D-47, MultitabPane T-83); (c) FILE-OPEN ROUTING — how opening a .canvas lands in this pane instead of the text editor: NOTE the TabContribution.fileGlobs field is DECLARED but has ZERO consumers in lib/, so this routing must be built (wire fileGlobs, or have the editor delegate by extension, or a command + workspace tab); (d) i18n + tests. THEN the edit phase: add node/text/edge affordances, node drag + resize, and persist the CanvasDoc back to the .canvas file (encode() already exists).
|
||||||
|
|
||||||
|
Refinement (2026-07-03, user-confirmed): (1) SLOT: Slots.workspace — diff/editor recipe, center column; coexists with Claude the way diff does (D-47). (2) TABS: real MultitabPane<CanvasDoc> — N .canvas files open as tabs (user explicitly chose over the single-TabContribution graph/diff precedent; needs MultitabController plumbing like Claude sessions). (3) FILE-OPEN ROUTING resolved by inspection: TabContribution.fileGlobs is dead (zero consumers) — do NOT wire it. Add a .canvas branch in lib/kernel/src/file_open.dart openWorkspaceFile() publishing to ''builtin.canvas''/''selection'' (mirror of the .md branch), and D-6 CLI parity via a ''canvas'' entry in the _readers map at lib/src/daemon/ui_command.dart:34 → clide ui open canvas <path> (diff precedent). (4) Fix stale doc header lib/src/canvas/json_canvas.dart:5-7 (claims SVG-lowering; actual architecture paints CanvasDoc directly per the ticket''s DECISION NOTE) in the same pass. (5) Edit phase later needs a mutation path (CanvasView has no onChanged; CanvasDoc is immutable-construction only) — deferred, flagged now.', 'Tier-5 canvas PANE, absorbed from the former T-7 into the unified canvas epic (T-317, decision D-91). A full workspace pane that renders .canvas files by CONVERTING them into the drawing-card JSON (per D-91: .canvas is an import format, not a native schema) and painting via the shared canvas renderer (T-318). Scope: nodes (note, text, group, image) + edges + layout state; pan/zoom; node selection, drag, resize; edit affordances (add note from file picker, add text node, draw edge between nodes); persist layout back to the .canvas file on disk. Uses MultitabPane (T-83) for tabs and its own slot per D-47; panels are extension-shaped (D-17). Depends on the core renderer (T-318). NOTE: unlike the conversation drawing card (display-only, D-78), this pane is interactive/editable — it is a pane, not a conversation widget.
|
||||||
|
|
||||||
|
SCOPE NOTE (2026-06-29): currently a 17-line stub. This is a from-scratch interactive Tier-5 pane (parse .canvas -> drawing-card JSON, CustomPaint render, pan/zoom, node select/drag/resize, edit affordances, persist .canvas) — a multi-session feature, NOT template-class work. The drawing-card TEMPLATE half of epic T-317 is now complete (svg/d2/image/icon/compare/graph + stdin).
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02): two foundation slices built + tested (from the 17-line stub). (1) JSONCanvas parser/model — lib/src/canvas/json_canvas.dart (2eb5d0f1): typed CanvasDoc (text/file/link/group nodes + edges with sides/ends/color/label), parse + toJson round-trip, skips unknown/malformed; pure Dart, 8 dart-test cases. (2) CustomPaint renderer — lib/builtin/canvas/src/canvas_painter.dart (035a37a1): CanvasBounds + pan/zoom CanvasViewport, group frames + edges (arrowheads) + node cards; node colours from the file (Obsidian presets/hex, NOT clide tokens per arbitrary-content rule), chrome themed; 7 tests (pixel-probe + viewport + colour). DECISION NOTE: the interactive pane paints the CanvasDoc model directly via CustomPaint (hit-testable, cheap re-paint) rather than re-lowering to SVG each frame — the ticket''s ''convert to drawing-card JSON'' path (D-91 SVG substrate) is for the display-only viewer; an editable pane needs direct model painting. Mirrors the graph pane. REMAINING (multi-session): interactive CanvasView (hit-test nodes + pan/zoom + click-select), pane shell (load .canvas from disk, workspace slot D-47, MultitabPane), CanvasExtension registration + open-.canvas routing, then edit affordances (add node/edge, drag, resize) + persist back to disk.
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02, cont.): third foundation slice landed — interactive CanvasView (lib/builtin/canvas/src/canvas_view.dart, 7700fc7a): hitTestCanvasNode (topmost node under a point; cards beat the group frames behind, via the painter''s own viewport) + scroll-zoom + drag-pan + click-select with a focus ring; 6 tests. The canvas rendering + interaction FOUNDATION is now complete + tested (parser 2eb5d0f1, renderer 035a37a1, view 7700fc7a — 21 canvas tests), mirroring where T-323''s foundation was before its integration phase. NOT yet reachable. REACHABLE-INTEGRATION PHASE (next, fresh session): (a) pane shell — a StatefulWidget that loads a .canvas from disk (files.read/editor.open path), holds the CanvasDoc, embeds CanvasView; (b) CanvasExtension registration as a workspace pane (own slot D-47, MultitabPane T-83); (c) FILE-OPEN ROUTING — how opening a .canvas lands in this pane instead of the text editor: NOTE the TabContribution.fileGlobs field is DECLARED but has ZERO consumers in lib/, so this routing must be built (wire fileGlobs, or have the editor delegate by extension, or a command + workspace tab); (d) i18n + tests. THEN the edit phase: add node/text/edge affordances, node drag + resize, and persist the CanvasDoc back to the .canvas file (encode() already exists).
|
||||||
|
|
||||||
|
Refinement (2026-07-03, user-confirmed): (1) SLOT: Slots.workspace — diff/editor recipe, center column; coexists with Claude the way diff does (D-47). (2) TABS: real MultitabPane<CanvasDoc> — N .canvas files open as tabs (user explicitly chose over the single-TabContribution graph/diff precedent; needs MultitabController plumbing like Claude sessions). (3) FILE-OPEN ROUTING resolved by inspection: TabContribution.fileGlobs is dead (zero consumers) — do NOT wire it. Add a .canvas branch in lib/kernel/src/file_open.dart openWorkspaceFile() publishing to ''builtin.canvas''/''selection'' (mirror of the .md branch), and D-6 CLI parity via a ''canvas'' entry in the _readers map at lib/src/daemon/ui_command.dart:34 → clide ui open canvas <path> (diff precedent). (4) Fix stale doc header lib/src/canvas/json_canvas.dart:5-7 (claims SVG-lowering; actual architecture paints CanvasDoc directly per the ticket''s DECISION NOTE) in the same pass. (5) Edit phase later needs a mutation path (CanvasView has no onChanged; CanvasDoc is immutable-construction only) — deferred, flagged now.
|
||||||
|
|
||||||
|
PROGRESS (2026-07-05): REACHABLE-INTEGRATION PHASE COMPLETE (e2e305de). CanvasExtension is a real workspace pane: app-scoped MultitabController on the extension (diff/T-233 pattern) with real per-document sub-tabs (MultitabPane keepAlive, per refinement decision), CanvasPaneHost + CanvasDocumentTab (files.read -> CanvasDoc.parse -> CanvasView; loading/error states), routing wired (openWorkspaceFile .canvas branch; ui.open ''canvas'' reader so clide ui open canvas <path> works; fileGlobs left dead as decided), en_us+nl_nl catalogs, json_canvas doc header corrected. Tests: extension lifecycle (7), pane-host widget (6), file_open routing (5, new file), ui_command canvas case; testmode extension roster now includes builtin.canvas (26/26 real-boot pass). REMAINING (edit phase, fresh session): node drag + resize, add note/text/edge affordances, persist CanvasDoc back to disk — encode() exists; CanvasView needs an onChanged/mutation path (refinement note item 5).', NULL, '2026-07-05 07:29:15', '2026-07-05 07:29:15.452', '2026-07-05 07:29:15.452', NULL, '0d0ff6c05b550d4a85296eb38adc830f', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FK3CDZQD2JM95ZQY5MH53PWC', 'status', 'backlog', 'in_progress', NULL, '2026-07-05 09:52:54', '2026-07-05 09:52:54.013', '2026-07-05 09:52:54.013', NULL, '59efe4bd0a098bfea3f146af21b0caf3', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FK3CDZQD2JM95ZQY5MH53PWC', 'status', 'in_progress', 'done', NULL, '2026-07-05 10:54:23', '2026-07-05 10:54:23.532', '2026-07-05 10:54:23.532', NULL, '941d08bde0cc4479860e015aded3d664', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FNR3DP0D48AZZ3NRA8JF3Q10', 'status', 'backlog', 'in_progress', NULL, '2026-07-13 15:17:17', '2026-07-13 15:17:17.537', '2026-07-13 15:17:17.537', NULL, 'fb951233b5fc242889cae1156dc1d0d8', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FNR3DP0D48AZZ3NRA8JF3Q10', 'description', NULL, 'Implements D-106 (FR-1 + worktree addendum). Storage: user-scope key app.env.pathPrepend.<fnv1a64Hex(canonicalWorkspaceKey(presetRoot))> in ~/.config/clide/settings.yaml — never committed; presetRootFor() resolves a linked-worktree workspace (gitdir: pointer file, e.g. .worktrees/<name>) to the main repo root so all worktrees share the repo''s preset.
|
||||||
|
|
||||||
|
Injection (prepend, dedup, next-spawn semantics): PTY panes via PaneRegistry.pathForSpawn hook (main.dart wires the preset-aware closure); hosted Claude via agentEnvDelta(prependDirs:) — now exports PATH whenever a preset exists even if clide is already on PATH (previous gap) — threaded orchestrator.pathPresetFor ← claude extension ← ctx.settings. Toolchain/supporter-binary resolution deliberately preset-blind (T-98/D-92 fence).
|
||||||
|
|
||||||
|
CLI (D-6): one cmd id env.path → clide env path list|set|add|remove|clear|capture; Flutter-free handler lib/src/daemon/env_path_commands.dart over an injected PathPresetStore port (adapter in main.dart); mutations publish on envPathChannel; capture diffs loginShellPathOrNull() (new shell_env getter) vs process PATH. UI: Settings → Tools → Workspace PATH section, custom control (ordered list, add/remove/reorder, missing-dir warning, worktree note, capture suggestions) in builtin.tools-settings; en+nl catalogs.
|
||||||
|
|
||||||
|
.worktrees/ added to IgnoreSet.builtin() and the project-init gitignore scaffold. Tests: path_preset_test (pure fns + real worktree layout), env_path_commands_test (all verbs), agent_bootstrap prependDirs cases, PaneRegistry pathForSpawn PTY test, widget tests for the control incl. worktree key sharing + capture. make test (4048) + make test-a11y green.', NULL, '2026-07-13 15:39:31', '2026-07-13 15:39:31.309', '2026-07-13 15:39:31.309', NULL, '44007d87d28b52050716cd913009d341', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FNR3DP0D48AZZ3NRA8JF3Q10', 'status', 'in_progress', 'done', NULL, '2026-07-13 16:48:56', '2026-07-13 16:48:56.833', '2026-07-13 16:48:56.833', NULL, 'd6c02324e60b8d412840b1ffd600a13f', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FNTF7KE5Y497PQC7SCFGR1KC', 'status', 'backlog', 'done', NULL, '2026-07-13 20:49:17', '2026-07-13 20:49:17.282', '2026-07-13 20:49:17.282', NULL, 'd2d3b437c1063f835e5b34c0cd996f9d', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
@@ -232,3 +232,110 @@ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_
|
|||||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKPAZR4XEV8YW3PVR2XJBFC', 'T-405', '2026-06-12 03:21:31', '2026-06-12 03:21:31', NULL, 'e4e1695f838b8fbf02aae49a6f2df4fe', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKPAZR4XEV8YW3PVR2XJBFC', 'T-405', '2026-06-12 03:21:31', '2026-06-12 03:21:31', NULL, 'e4e1695f838b8fbf02aae49a6f2df4fe', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKPD85PFBPJJTQ0WS3PWWXR', 'T-406', '2026-06-12 03:21:49', '2026-06-12 03:21:49', NULL, '689352238d2050a3769b2a9613f0a793', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKPD85PFBPJJTQ0WS3PWWXR', 'T-406', '2026-06-12 03:21:49', '2026-06-12 03:21:49', NULL, '689352238d2050a3769b2a9613f0a793', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKPFTC7H5NY0XHBTEGF8XQ4', 'T-407', '2026-06-12 03:22:10', '2026-06-12 03:22:10', NULL, '129d2b3c31022d53025a3e28169a060e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKPFTC7H5NY0XHBTEGF8XQ4', 'T-407', '2026-06-12 03:22:10', '2026-06-12 03:22:10', NULL, '129d2b3c31022d53025a3e28169a060e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBN3VTK2MYQQ173MSJN6E1DM', 'T-408', '2026-06-12 06:40:25', '2026-06-12 06:40:25', NULL, '0353aaab57a40900b00883fb12e135f7', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBN3VYR84023Z5XFEX9DS0S0', 'T-409', '2026-06-12 06:40:26', '2026-06-12 06:40:26', NULL, '69c280319335a8d0ef646998f4531e96', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBP3EZC7AJANXZVF3D91QYWM', 'T-410', '2026-06-12 08:58:29', '2026-06-12 08:58:29', NULL, '4f726d1a66d38d14018a62c8d24ffe62', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBP3GM6V0RZBY2PXE9ZQFR88', 'T-411', '2026-06-12 08:58:42', '2026-06-12 08:58:42', NULL, 'bdd3f8b13caf25677ac661ec29599488', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBP3J7TXMG0F9E2WQDENPVJG', 'T-412', '2026-06-12 08:58:55', '2026-06-12 08:58:55', NULL, 'e56d50bc6fc04f6d646f22c104e63183', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBP3KRWM65MD3DS251NN9YX0', 'T-413', '2026-06-12 08:59:08', '2026-06-12 08:59:08', NULL, 'b5247ea05c106500682be978a47e61ee', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBP3P8YERJ5R7ENSD675BX00', 'T-414', '2026-06-12 08:59:28', '2026-06-12 08:59:28', NULL, '7d973f16c99441dbba0f8df89a665b32', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBP3P91QQQDT5J50F52FPCKM', 'T-415', '2026-06-12 08:59:28', '2026-06-12 08:59:28', NULL, 'a2dea9268d44b9e8a1fd746bbb947c92', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBPQ8QNGJFFK7G24CBWQAR2C', 'T-416', '2026-06-12 10:25:00', '2026-06-12 10:25:00', NULL, 'f8c2a125e661607d5dd0c73cd2c3f2ab', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBQ4BYD4STCKCY8JNKF23Q4W', 'T-417', '2026-06-12 11:22:15', '2026-06-12 11:22:15', NULL, '15aa9b25417162126cbcde174d3537da', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBQ595H08JFTRFSR90GSZQ0G', 'T-418', '2026-06-12 11:26:14', '2026-06-12 11:26:14', NULL, '000e07ae64b08273a2d2d9f8a77d193f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBTTMGKSYMTF8M1KQWTG774W', 'T-419', '2026-06-12 19:58:58', '2026-06-12 19:58:58', NULL, 'd2b01a2c3d1ce24cc863ac6d9d814d3d', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FC2XY1T85A65YY9SG25VVEY4', 'T-420', '2026-06-13 14:51:51', '2026-06-13 14:51:51', NULL, '16ea4c9353a56798b894ab3d85fb7b56', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCDG3T4A7CYPTG535KVAVH6C', 'T-421', '2026-06-14 15:29:23', '2026-06-14 15:29:23', NULL, 'a28eed4b57104034c5216344a326195c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCDKX4CVHWVGDAJC6X09602M', 'T-422', '2026-06-14 15:45:57', '2026-06-14 15:45:57', NULL, '5701c634f5737a2ba1612deab8df7049', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCDM61KAA3GV3CVTE8PAZ8N0', 'T-423', '2026-06-14 15:47:10', '2026-06-14 15:47:10', NULL, 'ce7dfb8b9bd088b4c2e8ddfacc8d2124', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCENXW1VF7VXQX64982X171R', 'T-424', '2026-06-14 18:14:36', '2026-06-14 18:14:36', NULL, 'ecbed75dcd34c39bcbd86e60d6f4a2c2', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCENXXZFBZ0HVD1VCW4ZASCC', 'T-425', '2026-06-14 18:14:36', '2026-06-14 18:14:36', NULL, 'b09fc55465f7de02cf98f69c99e0e6e3', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCEP60AS6AF654SWA189A5ZR', 'T-426', '2026-06-14 18:15:42', '2026-06-14 18:15:42', NULL, '91f50c6e38332047f8619db428d4b376', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCEP642C8ZZ1T20RXQQ3143M', 'T-427', '2026-06-14 18:15:43', '2026-06-14 18:15:43', NULL, '55a937def177025ef6b61a222d88b142', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCEP67ZHMBFW0GRH9JKDMQ7R', 'T-428', '2026-06-14 18:15:44', '2026-06-14 18:15:44', NULL, '185e7d3ce8529fbe1f4543f4c635f200', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCEP6BDBHGMK9VCRV6JQ00TW', 'T-429', '2026-06-14 18:15:45', '2026-06-14 18:15:45', NULL, 'ef268614a716384a7597b3e304a6c176', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCEP6EVN9S35T02MHA2AS7YW', 'T-430', '2026-06-14 18:15:46', '2026-06-14 18:15:46', NULL, 'd6333df4da5b7ab8958149a9f0d17974', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCGJ30V24BJB001GZCR5QKTC', 'T-431', '2026-06-14 22:37:27', '2026-06-14 22:37:27', NULL, 'b886820e87f5abd329126bf5e9f1a3da', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCM9ER04JVFW8CN3JW1AWYA8', 'T-432', '2026-06-15 07:18:58', '2026-06-15 07:18:58', NULL, '71f3e4c95f66abc5e7e5820548201e41', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCM9F446MZFXVHH65Q6CKTPM', 'T-433', '2026-06-15 07:19:01', '2026-06-15 07:19:01', NULL, 'b9a361f2c29b286bc808dbea57a05a7e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCM9FHC8VX50759X35VNER1R', 'T-434', '2026-06-15 07:19:04', '2026-06-15 07:19:04', NULL, '403c4c8aa5659bb379cb8add9dff800b', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCM9FYDEXCM15FXTER032K84', 'T-435', '2026-06-15 07:19:08', '2026-06-15 07:19:08', NULL, '7c2ed604aecea99b742b341166cf2257', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCM9GAQ2G0KCVMZS67SK3324', 'T-436', '2026-06-15 07:19:11', '2026-06-15 07:19:11', NULL, '3114ab57de9b03aa1e745af01001eee1', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCNYXXH5AAHZR7WV0550J3RC', 'T-437', '2026-06-15 11:12:36', '2026-06-15 11:12:36', NULL, '74dd08c8c42f556959746ee1a47561e6', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCQ8HB61N3TWVJ8YSMHH2TJ4', 'T-438', '2026-06-15 14:14:23', '2026-06-15 14:14:23', NULL, 'fa072ae8dd819784440bb44b5fe689d8', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCQHWQ40AY6SNVRJ86YWA0J8', 'T-439', '2026-06-15 14:55:15', '2026-06-15 14:55:15', NULL, '2427484ebb324d731cb8e099a2ad04ab', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCQZ47MAN835B215GSSMRV8W', 'T-440', '2026-06-15 15:53:05', '2026-06-15 15:53:05', NULL, 'f219bb1a70eb1f49e30975ce8c526051', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCZDVPBWGM5NHJ9BNQBVKCD0', 'T-441', '2026-06-16 09:16:07', '2026-06-16 09:16:07', NULL, 'fbabc4fc344b34476b38a186c2f44a16', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCZGG38FF8T9ADF945Z0XPMG', 'T-442', '2026-06-16 09:27:39', '2026-06-16 09:27:39', NULL, 'c85d8fd95cd73aec1e83b477612e1131', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCRERT7X5WMZSGKQCA6T0VB4', 'T-440', '2026-06-15 17:01:25', '2026-06-15 17:01:25', NULL, 'b8e152bf87ec8d81d1bddeb023f37883', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCQZ47MAN835B215GSSMRV8W', 'T-443', '2026-06-15 15:53:05', '2026-06-16 10:17:57', NULL, 'a4d4390596d24d7dc28d80861e36d66b', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD07ECS3GK2V7Z2WYYPJHJYC', 'T-444', '2026-06-16 11:07:54', '2026-06-16 11:07:54', NULL, 'ec7a0d8d01c984d931855a6e5c4e86ec', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD09C4A0HPZSP8HP44F7A894', 'T-445', '2026-06-16 11:16:20', '2026-06-16 11:16:20', NULL, '418d561d50d37c3dd9b6e3abe5fc1b1f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD09DH88J1659FDDYJ83JH1M', 'T-446', '2026-06-16 11:16:31', '2026-06-16 11:16:31', NULL, '7c0bc42be1a170b5bb2dd8bd7b027d69', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD09SHDMGT66C4D1DRZQZ7RR', 'T-447', '2026-06-16 11:18:10', '2026-06-16 11:18:10', NULL, '203dfb0348a94392f2d087b79aee655f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD09TVT3E3ZSV09QRNTSQ8J4', 'T-448', '2026-06-16 11:18:20', '2026-06-16 11:18:20', NULL, '765a91fb5b51889861a41f7eea308ea6', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD09WB2XSRP0AZ204FDP681M', 'T-449', '2026-06-16 11:18:32', '2026-06-16 11:18:32', NULL, '1dbd17ff345551d3f091a226bb0f7164', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD09XWEXRJ4JS8MFPQGJEY5M', 'T-450', '2026-06-16 11:18:45', '2026-06-16 11:18:45', NULL, '56f97ccefa83681c387ee9cc4ca6a477', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD0A0ZTS9JZCSQ4W8ZVEH2VR', 'T-451', '2026-06-16 11:19:11', '2026-06-16 11:19:11', NULL, 'c329f6f1a5d205b7bdf5ab682812a6d1', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD0A4MY8DF6NB0KNV96RQADR', 'T-452', '2026-06-16 11:19:41', '2026-06-16 11:19:41', NULL, '71a7729a8cf7ebc4e915a9e2736717bd', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD0A6K9B60ADTFBZA6NBVJFM', 'T-453', '2026-06-16 11:19:57', '2026-06-16 11:19:57', NULL, 'd408d563785166ee1b40e7494c7f3bbc', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD0ABC7QNEC3XCTV73YPSGR4', 'T-454', '2026-06-16 11:20:36', '2026-06-16 11:20:36', NULL, '27e2ae5b0897eb0f589f4617061101a9', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD15S1A0WRTY9WWFG0R4AAZ4', 'T-455', '2026-06-16 13:20:25', '2026-06-16 13:20:25', NULL, '370e913c62eab739cf39bda08089e033', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD15TY946689VZK3MNABSMDW', 'T-456', '2026-06-16 13:20:41', '2026-06-16 13:20:41', NULL, '6041202d66dd93ee3d22206ece3a7db7', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD15W7SBVAD3QR5NVE67PKWR', 'T-457', '2026-06-16 13:20:52', '2026-06-16 13:20:52', NULL, 'c9bef415fb7d9d03444fde53ecac3bb1', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD1HK7YKJTEK1WV4VHK0RT8R', 'T-458', '2026-06-16 14:12:04.212', '2026-06-16 14:12:04.212', NULL, '070f1def635ee013df639cb9fe81e357', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD1JBRABJHX804CPMZJDC444', 'T-459', '2026-06-16 14:15:25.011', '2026-06-16 14:15:25.011', NULL, '44d5db70ef13ead00c4dcc437c387cec', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD91A7VEW3VCY7QX1END2Z3G', 'T-460', '2026-06-17 07:39:25.019', '2026-06-17 07:39:25.019', NULL, 'e7e749d35dc413a560d3347be5f4a9ee', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD91RWDWZPHFPJ72FHHHWS08', 'T-461', '2026-06-17 07:41:24.975', '2026-06-17 07:41:24.975', NULL, '4e3fba7aaad762b7b7cd854459cf664a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0TPYSWEM10RP0Q76XAP58', 'T-462', '2026-06-17 09:57:06.422', '2026-06-17 09:57:06.422', NULL, '4be5f7eb00ca81eb07015af05ed144d3', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0VPB99NGS4J6Z5B2RJM2M', 'T-463', '2026-06-17 09:57:14.458', '2026-06-17 09:57:14.458', NULL, 'd5d02453175411c13d21361b084494bd', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0WCPG458GF5FXTXM9VY98', 'T-464', '2026-06-17 09:57:20.180', '2026-06-17 09:57:20.180', NULL, 'c787ef61befc6698286ebdaf613e2a41', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0X5WFME83GS0DKYG3WVJ8', 'T-465', '2026-06-17 09:57:26.627', '2026-06-17 09:57:26.627', NULL, '116b1a377950dfcef4e5b6db90141faa', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0XMA9BQV4RHJ55BFN4JR4', 'T-466', '2026-06-17 09:57:30.322', '2026-06-17 09:57:30.322', NULL, 'd25bc26eb29445fc35cf53f3d54049e5', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0YCY2WGSG8W46RHQ5Z9MW', 'T-467', '2026-06-17 09:57:36.624', '2026-06-17 09:57:36.624', NULL, 'a47c675ca67b86ca3c8d4c1a8acc5b80', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0YYCJ0NK8C4HMDYKJHRDG', 'T-468', '2026-06-17 09:57:41.092', '2026-06-17 09:57:41.092', NULL, 'b988d9578b1ca079c040f07265a008b4', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA10DRQE3SED10JAW2CZDCR', 'T-469', '2026-06-17 09:57:53.221', '2026-06-17 09:57:53.221', NULL, '2a72e3e647afc1b5b235ab1a97d298f8', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDB1VFADV4QK1YG29T658T8C', 'T-470', '2026-06-17 12:21:23.411', '2026-06-17 12:21:23.411', NULL, '0f95e5a1faa7a7e902c765f449cceb15', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDDJ17C3GZWNE98NRVXP189C', 'T-471', '2026-06-17 18:11:42.049', '2026-06-17 18:11:42.049', NULL, '10a9a957d647a63ce69dc4b6b564f6aa', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDDTCGG5Z0KNQKF89W1VZSQ8', 'T-472', '2026-06-17 18:48:11.649', '2026-06-17 18:48:11.649', NULL, '097120cff851cb2dac9f54937f4c7b17', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDDX5GVH3FTCVDEC1QAFACY4', 'T-473', '2026-06-17 19:00:20.828', '2026-06-17 19:00:20.828', NULL, '647782edc3fbb4e7285e4f82a1fb84be', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDDX5KEAJ8KRWVV7ZSG6H7A0', 'T-474', '2026-06-17 19:00:21.490', '2026-06-17 19:00:21.490', NULL, '79576f18553ed885d3a2b745becdf2f5', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDEMY3DQWDNY535PMMSB0CSW', 'T-475', '2026-06-17 20:44:11.501', '2026-06-17 20:44:11.501', NULL, 'bb2b56ea9a76d967123238f588c70aba', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'T-476', '2026-06-19 07:42:08.735', '2026-06-19 07:42:08.735', NULL, 'f75a0a8c00b0ec40c110c1169e42fe2c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FEWB9JA5CMTXB5D7G1NSHAV4', 'T-477', '2026-06-22 07:13:19.958', '2026-06-22 07:13:19.958', NULL, '0314d96a690bdadc122ce0a1e9ed0f5c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFKFYBZFKBX0WV574QJEH2BC', 'T-478', '2026-06-24 13:09:16.923', '2026-06-24 13:09:16.923', NULL, 'f1ea48c2cfa08915841d99eea06a9974', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49VYMPF18PYQXD9PCMHN8', 'T-477', '2026-06-25 09:16:42', '2026-06-25 09:16:42', NULL, '64605163f24e3846463f3afb4ed62fd1', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W3V175EM4F8ZHC6JFM4', 'T-478', '2026-06-25 09:16:42', '2026-06-25 09:16:42', NULL, '1c2e59e6220c0b288103c4a71a8282ca', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W6GP535GR8XD1XEHYWG', 'T-479', '2026-06-25 09:16:42', '2026-06-25 09:16:42', NULL, '2a6162f26c4c0b6e7bceb1f1a2ffe167', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W95HJK08BCRQSWFZBF4', 'T-480', '2026-06-25 09:16:42', '2026-06-25 09:16:42', NULL, '8a42c8c1fc2036d6ea73521266e87235', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49WBTT3ESK6QG0XZ1VN2G', 'T-481', '2026-06-25 09:16:42', '2026-06-25 09:16:42', NULL, 'd1d616d020e6ea8661c1f5553e3001eb', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49WEE5N4PESF5G1G5JRHR', 'T-482', '2026-06-25 09:16:42', '2026-06-25 09:16:42', NULL, '557f92392db9b5c1e6b6840fe1293b7d', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49VYMPF18PYQXD9PCMHN8', 'T-483', '2026-06-25 09:16:42', '2026-06-26 21:35:04.076', NULL, '2841b381bbb03e6a9cdb970280cd2207', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W3V175EM4F8ZHC6JFM4', 'T-484', '2026-06-25 09:16:42', '2026-06-26 21:35:07.645', NULL, 'd90df1d5e2da44e81f60edb8c5fb2823', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGN13H42TP2MP3T27CTSCDRW', 'T-485', '2026-06-27 19:17:59.713', '2026-06-27 19:17:59.713', NULL, 'b345ba60d55cfdbc7c2e9bb9187ee171', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGPG4WNNH3BWDRVZXYQTW7Z0', 'T-486', '2026-06-27 22:43:31.629', '2026-06-27 22:43:31.629', NULL, 'cd344c35e0eca5673be26790aa8d292c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSQ789MDB00EQAXTXBVJDXM', 'T-487', '2026-06-28 06:13:51.821', '2026-06-28 06:13:51.821', NULL, '184f63035c0a014dc4fb863e512f8779', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSQ8B7WDKZ174GJDGWW0GS0', 'T-488', '2026-06-28 06:14:00.768', '2026-06-28 06:14:00.768', NULL, '69e3283ee3c10bcd2ca5b5cbad6a8b1f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSQ8YSZA5YNP1JE8SJHXWBG', 'T-489', '2026-06-28 06:14:05.776', '2026-06-28 06:14:05.776', NULL, '0b37f24529eb3bb4f2b295dda3bb04ea', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSQP1YPG70RPQJ4XZDH2T1M', 'T-490', '2026-06-28 06:15:53.077', '2026-06-28 06:15:53.077', NULL, '586b7d8495ad019b436fddbf973c9e63', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGVCY4M0ZK72HZT2FSZF968W', 'T-491', '2026-06-28 10:08:32.933', '2026-06-28 10:08:32.933', NULL, '3895f296d80984ccd613ca9853a05eda', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGVDBD0F14FV5CDJQB063YSR', 'T-492', '2026-06-28 10:10:21.572', '2026-06-28 10:10:21.572', NULL, '341150811f14eb3f4486067ffb1197c4', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGVNCZS0JZP5Y7596GBV4RFC', 'T-493', '2026-06-28 10:45:31.721', '2026-06-28 10:45:31.721', NULL, 'eec40f4714242cf343035481684f2aba', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGX26B0NAC9WRJMVB0QE6CV8', 'T-494', '2026-06-28 14:01:13.733', '2026-06-28 14:01:13.733', NULL, '37bf4baec69cb3d06072793cd3d60a46', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGYPJ6FTEPP4JK3D7D01ZSMM', 'T-495', '2026-06-28 17:50:02.366', '2026-06-28 17:50:02.366', NULL, '40dbd25d323a8192e8f65a6e1c2dd33d', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGYS06SDJGZ4W3Z9V9HDEATW', 'T-496', '2026-06-28 18:00:41.420', '2026-06-28 18:00:41.420', NULL, 'd2e472745c710eb00555b2b824d4e214', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZK6PWBRBK9J2XPJ55Y3630', 'T-497', '2026-06-28 19:55:10.439', '2026-06-28 19:55:10.439', NULL, '56f8bf5d0e9a52d9ce3c7a293b5f8972', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZKEHPT1TMFMR0KYFGR087W', 'T-498', '2026-06-28 19:56:14.646', '2026-06-28 19:56:14.646', NULL, 'c62ee89f4aa203262fab74da8604777a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZKEHPSSHEG6ZBM0B4DMM6C', 'T-498', '2026-06-28 19:56:14.655', '2026-06-28 19:56:14.655', NULL, '1a47f2a97f13a7565080893758424fec', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZKEHPW9GMXNACA38MK56HR', 'T-498', '2026-06-28 19:56:14.655', '2026-06-28 19:56:14.655', NULL, '2c606da2b11071cee3bfcee4855d4e66', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZKJKW1721NKWY6XZG3KYB8', 'T-499', '2026-06-28 19:56:47.968', '2026-06-28 19:56:47.968', NULL, '135e7af3ddf32a676fcfadd150061dd6', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZKJKW6EDH8TR45M86EWB5M', 'T-499', '2026-06-28 19:56:47.977', '2026-06-28 19:56:47.977', NULL, 'fc370ccee9cd94d0de9988f08963b8f1', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZKJKW9P697RTQ68R3NKBR8', 'T-499', '2026-06-28 19:56:47.978', '2026-06-28 19:56:47.978', NULL, '99eb5554507cf5b29a158838f156d544', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZKEHPW9GMXNACA38MK56HR', 'T-500', '2026-06-28 19:56:14.655', '2026-06-28 19:57:02.538', NULL, '6b529f95abd005b73e20b293dcba765c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZKEHPT1TMFMR0KYFGR087W', 'T-501', '2026-06-28 19:56:14.646', '2026-06-28 19:57:25.895', NULL, '4e1059be29e69c094168bb55e06704a4', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZKJKW1721NKWY6XZG3KYB8', 'T-502', '2026-06-28 19:56:47.968', '2026-06-28 19:57:25.904', NULL, '2c08cf1dfc5c7ba16f756db7c80bc300', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZKJKW6EDH8TR45M86EWB5M', 'T-503', '2026-06-28 19:56:47.977', '2026-06-28 19:57:25.905', NULL, '5177977a9215f17efa101e50b03ad8eb', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZKJKW9P697RTQ68R3NKBR8', 'T-504', '2026-06-28 19:56:47.978', '2026-06-28 19:57:25.906', NULL, 'cb7f70a279326ff6e29a8c7bdeb78c0f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGZKJKW9P697RTQ68R3NKBR8', 'T-504', '2026-06-28 19:56:47.978', '2026-06-28 19:57:25.906', NULL, 'cb7f70a279326ff6e29a8c7bdeb78c0f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FH621XBYJBGE1XPSFFK4YZGM', 'T-505', '2026-06-29 10:58:54.943', '2026-06-29 10:58:54.943', NULL, 'b402cc72b921d446a6e4f6c759d72d54', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FHAX7FV5KWGZQ31617R63W94', 'T-506', '2026-06-29 22:16:52.953', '2026-06-29 22:16:52.953', NULL, 'fdeccc75d6a70fae57de814d11695473', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FJ4D0GAXM1FN42PJKG7FE34G', 'T-507', '2026-07-02 09:41:05.241', '2026-07-02 09:41:05.241', NULL, 'd9dec9b9ed45db65467318f53eb05157', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FJ4D1M6ESSSQ1ZERTH4V9YN4', 'T-508', '2026-07-02 09:41:14.425', '2026-07-02 09:41:14.425', NULL, '5389a964251dc22408c41ceac6954f4d', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FJ91RYFXBJH99HJ18QTHY2B0', 'T-509', '2026-07-02 20:31:02.783', '2026-07-02 20:31:02.783', NULL, 'ea835aca1960e1bee3d0710be19b8bba', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FK3CDZQD2JM95ZQY5MH53PWC', 'T-510', '2026-07-05 09:52:40.384', '2026-07-05 09:52:40.384', NULL, '6acffc225f49858f782c8c5e801e1c24', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FNR3DP0D48AZZ3NRA8JF3Q10', 'T-511', '2026-07-13 15:17:10.276', '2026-07-13 15:17:10.276', NULL, 'ffc35cbc7ffe3577e9f103ce6b411857', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FNTF7KE5Y497PQC7SCFGR1KC', 'T-512', '2026-07-13 20:48:23.414', '2026-07-13 20:48:23.414', NULL, '90218f8881c60ea87ee29cdfc4e4bdb3', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
INSERT INTO ticket_labels (ticket_record_id, label, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCDG3T4A7CYPTG535KVAVH6C', 'multi-window', '2026-06-14 15:29:27', '2026-06-14 15:29:27', NULL, 'ce03f8eb534bc45e2c1c12e9b30a2c30', 2) ON CONFLICT(ticket_record_id, label) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_labels.updated_at OR (excluded.updated_at = ticket_labels.updated_at AND excluded.hash > ticket_labels.hash);
|
||||||
|
INSERT INTO ticket_labels (ticket_record_id, label, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCDG3T4A7CYPTG535KVAVH6C', 'ipc', '2026-06-14 15:29:28', '2026-06-14 15:29:28', NULL, 'e6789b66db5c1de85daf6fc8d3474449', 2) ON CONFLICT(ticket_record_id, label) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_labels.updated_at OR (excluded.updated_at = ticket_labels.updated_at AND excluded.hash > ticket_labels.hash);
|
||||||
|
INSERT INTO ticket_labels (ticket_record_id, label, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCNYXXH5AAHZR7WV0550J3RC', 'regression', '2026-06-15 11:12:41', '2026-06-15 11:12:41', NULL, 'd1aee45df369a9afed1ecc7bc6043255', 2) ON CONFLICT(ticket_record_id, label) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_labels.updated_at OR (excluded.updated_at = ticket_labels.updated_at AND excluded.hash > ticket_labels.hash);
|
||||||
|
INSERT INTO ticket_labels (ticket_record_id, label, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCNYXXH5AAHZR7WV0550J3RC', 'claude-cli', '2026-06-15 11:12:41', '2026-06-15 11:12:41', NULL, '2d486f878edbd7a898ece18900e0d9d6', 2) ON CONFLICT(ticket_record_id, label) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_labels.updated_at OR (excluded.updated_at = ticket_labels.updated_at AND excluded.hash > ticket_labels.hash);
|
||||||
|
INSERT INTO ticket_labels (ticket_record_id, label, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCQ8HB61N3TWVJ8YSMHH2TJ4', 'web', '2026-06-15 14:22:06', '2026-06-15 14:22:06', NULL, '6867c9766259a7f09e81ff515973914e', 2) ON CONFLICT(ticket_record_id, label) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_labels.updated_at OR (excluded.updated_at = ticket_labels.updated_at AND excluded.hash > ticket_labels.hash);
|
||||||
|
INSERT INTO ticket_labels (ticket_record_id, label, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCQHWQ40AY6SNVRJ86YWA0J8', 'path', '2026-06-15 15:24:11', '2026-06-15 15:24:11', NULL, 'e73b3b85732a2c21be23353a0f7f1493', 2) ON CONFLICT(ticket_record_id, label) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_labels.updated_at OR (excluded.updated_at = ticket_labels.updated_at AND excluded.hash > ticket_labels.hash);
|
||||||
|
INSERT INTO ticket_labels (ticket_record_id, label, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCQHWQ40AY6SNVRJ86YWA0J8', 'web', '2026-06-15 15:24:11', '2026-06-15 15:24:11', NULL, '8c93337891e74d67d2465ba2c3b901d5', 2) ON CONFLICT(ticket_record_id, label) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_labels.updated_at OR (excluded.updated_at = ticket_labels.updated_at AND excluded.hash > ticket_labels.hash);
|
||||||
|
INSERT INTO ticket_labels (ticket_record_id, label, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCQZ47MAN835B215GSSMRV8W', 'web', '2026-06-15 15:53:35', '2026-06-15 15:53:35', NULL, '4cbf5007b12087b88b8acb36f3616bf8', 2) ON CONFLICT(ticket_record_id, label) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_labels.updated_at OR (excluded.updated_at = ticket_labels.updated_at AND excluded.hash > ticket_labels.hash);
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,101 @@
|
|||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2G2KHKT5CJYR0TK1WQGMD0', 'story', '06FHAX7FV5KWGZQ31617R63W94', 'Tier-5 graph view (builtin.graph)', 'Tier-5 graph VIEW, absorbed from the former T-7 into the unified canvas epic (T-317). Force-directed layout of the vault (or a filtered subset): nodes are notes, edges are wikilinks. Hover highlights the connected subgraph; click opens the note in the editor. Filter pane: tag include/exclude, file glob, depth-from-active. pql provides the link data (pql backlinks / pql outlinks); rendering is owned in-app (CustomPaint, own-the-rendering-stack). Lives in its own slot per D-47 (context panel), uses MultitabPane (T-83). DISTINCT from the in-card graph TEMPLATE (T-321): T-321 renders a graph passed into the conversation drawing card; this is the full interactive graph pane over the whole vault. Relates to the core renderer (T-318) but has its own force-directed layout.
|
||||||
|
|
||||||
|
PROGRESS (2026-06-29): force-directed layout SOLVER built + tested (lib/src/graph/force_layout.dart, 6 dart-test cases, commit bcef3a6f) — deterministic Fruchterman-Reingold, Flutter-free. REMAINING (the bulk, multi-session): graph data model from pql backlinks/outlinks over the vault; CustomPaint rendering (nodes+edges); pan/zoom; hover-highlight-subgraph; click-opens-note; filter pane (tag/glob/depth); MultitabPane + context-panel slot (D-47) wiring; widget/golden tests. This is a full interactive Tier-5 pane, a fresh-session undertaking — only its solver core is done.
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02): the graph rendering + interaction foundation is BUILT + tested (4 slices, all committed). (1) force-directed solver — force_layout.dart (bcef3a6f). (2) link-graph model — vault_graph.dart: VaultGraph.fromOutlinks, neighborhood, edgePairs (084bf842). (3) CustomPaint renderer — graph_painter.dart: edges/nodes/labels, hover-dim, fit transform (6bea1723). (4) interactive GraphView — graph_view.dart: hover-highlight + click-to-open, shared GraphViewport hit-testing (c529703b). The graph is a fully working WIDGET (renders + hover + click). REMAINING = the app-integration phase: pql link-data wiring (pql outlinks over the vault -> the file->outlinks map -> VaultGraph), the pane shell (async load + filter: tag/glob/depth + pan/zoom), and MultitabPane + context-panel slot (D-47) + GraphExtension registration.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 11:17:23', '2026-07-02 06:35:03.555', NULL, 'a46ad64b03bc1b4126e9882ed7b81a3a', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2G2KHKT5CJYR0TK1WQGMD0', 'story', '06FHAX7FV5KWGZQ31617R63W94', 'Tier-5 graph view (builtin.graph)', 'Tier-5 graph VIEW, absorbed from the former T-7 into the unified canvas epic (T-317). Force-directed layout of the vault (or a filtered subset): nodes are notes, edges are wikilinks. Hover highlights the connected subgraph; click opens the note in the editor. Filter pane: tag include/exclude, file glob, depth-from-active. pql provides the link data (pql backlinks / pql outlinks); rendering is owned in-app (CustomPaint, own-the-rendering-stack). Lives in its own slot per D-47 (context panel), uses MultitabPane (T-83). DISTINCT from the in-card graph TEMPLATE (T-321): T-321 renders a graph passed into the conversation drawing card; this is the full interactive graph pane over the whole vault. Relates to the core renderer (T-318) but has its own force-directed layout.
|
||||||
|
|
||||||
|
PROGRESS (2026-06-29): force-directed layout SOLVER built + tested (lib/src/graph/force_layout.dart, 6 dart-test cases, commit bcef3a6f) — deterministic Fruchterman-Reingold, Flutter-free. REMAINING (the bulk, multi-session): graph data model from pql backlinks/outlinks over the vault; CustomPaint rendering (nodes+edges); pan/zoom; hover-highlight-subgraph; click-opens-note; filter pane (tag/glob/depth); MultitabPane + context-panel slot (D-47) wiring; widget/golden tests. This is a full interactive Tier-5 pane, a fresh-session undertaking — only its solver core is done.
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02): the graph rendering + interaction foundation is BUILT + tested (4 slices, all committed). (1) force-directed solver — force_layout.dart (bcef3a6f). (2) link-graph model — vault_graph.dart: VaultGraph.fromOutlinks, neighborhood, edgePairs (084bf842). (3) CustomPaint renderer — graph_painter.dart: edges/nodes/labels, hover-dim, fit transform (6bea1723). (4) interactive GraphView — graph_view.dart: hover-highlight + click-to-open, shared GraphViewport hit-testing (c529703b). The graph is a fully working WIDGET (renders + hover + click). REMAINING = the app-integration phase: pql link-data wiring (pql outlinks over the vault -> the file->outlinks map -> VaultGraph), the pane shell (async load + filter: tag/glob/depth + pan/zoom), and MultitabPane + context-panel slot (D-47) + GraphExtension registration.
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02, session 2): app-integration phase — 3 slices committed. (1) GraphController (graph_controller.dart, 3b67725b) — pql.files + per-file pql.outlinks over the vault -> VaultGraph; loading/error state; debounced files.changed refresh; 6 tests. (2) GraphPanel shell (graph_panel.dart, 6fae1bc4) — loading/empty/error states + GraphView + click-to-open via editor.open; 5 widget tests. (3) GraphExtension registration (extension.dart + builtin.graph i18n, a5acb6ac) — graph.view context-panel tab (D-47), activates after builtin.pql dep; 4 tests; CHANGELOG entry. The graph is now REACHABLE end-to-end (21 graph tests green). REMAINING (T-323 enrichment): pan/zoom on the view; filter controls (tag include/exclude, glob, depth-from-active).', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 11:17:23', '2026-07-02 08:33:27.518', NULL, 'dc254ed9b53362c45d11ac23f600b57d', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FJ4D0GAXM1FN42PJKG7FE34G', 'task', NULL, 'Skill cleanup: apply 2026-07-02 estate-review fixes to clide repo + user scope', 'Apply verified findings from the estate-wide skill review: delete commit/penpot-login/clean-house(project copy) skills; centralize d2-diagram/frame0-wireframe/skill-create to user scope de-contaminated; repoint whats-next (governance/, gh, dead links, step 3a); fix git-commit heredoc guidance + stale sidecar/ADR refs; fix clide skill dock slot + description trim; testmode terminal category; ui-design phosphor glyph reference post-T-314; rewrite skills README index.', 'backlog', 'medium', NULL, NULL, NULL, '2026-07-02 09:41:05.239', '2026-07-02 09:41:05.239', NULL, 'c5ddebbb4ffe8611329732087dd1cebb', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FJ4D1M6ESSSQ1ZERTH4V9YN4', 'task', NULL, 'Distribute the clide skill per-workspace with an install marker', 'Estate review 2026-07-02: the clide skill is repo-agnostic and hand-copied into other repos (settled-reach), drifting. Long-term story: clide installs the skill into each workspace''s .claude/skills/ with a .pql-install.json-style marker (version + sha256) so staleness is detectable, following pql''s clean-house precedent. Until then the canonical copy lives in the clide repo.', 'backlog', 'medium', NULL, NULL, NULL, '2026-07-02 09:41:14.420', '2026-07-02 09:41:14.420', NULL, '698b529e4ea8e4417ddb3b9846b30c13', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FJ4D0GAXM1FN42PJKG7FE34G', 'task', NULL, 'Skill cleanup: apply 2026-07-02 estate-review fixes to clide repo + user scope', 'Apply verified findings from the estate-wide skill review: delete commit/penpot-login/clean-house(project copy) skills; centralize d2-diagram/frame0-wireframe/skill-create to user scope de-contaminated; repoint whats-next (governance/, gh, dead links, step 3a); fix git-commit heredoc guidance + stale sidecar/ADR refs; fix clide skill dock slot + description trim; testmode terminal category; ui-design phosphor glyph reference post-T-314; rewrite skills README index.', 'done', 'medium', NULL, NULL, NULL, '2026-07-02 09:41:05.239', '2026-07-02 09:55:52.109', NULL, '7470547f02b1269981faefe053aa4ae3', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2G2KHKT5CJYR0TK1WQGMD0', 'story', '06FHAX7FV5KWGZQ31617R63W94', 'Tier-5 graph view (builtin.graph)', 'Tier-5 graph VIEW, absorbed from the former T-7 into the unified canvas epic (T-317). Force-directed layout of the vault (or a filtered subset): nodes are notes, edges are wikilinks. Hover highlights the connected subgraph; click opens the note in the editor. Filter pane: tag include/exclude, file glob, depth-from-active. pql provides the link data (pql backlinks / pql outlinks); rendering is owned in-app (CustomPaint, own-the-rendering-stack). Lives in its own slot per D-47 (context panel), uses MultitabPane (T-83). DISTINCT from the in-card graph TEMPLATE (T-321): T-321 renders a graph passed into the conversation drawing card; this is the full interactive graph pane over the whole vault. Relates to the core renderer (T-318) but has its own force-directed layout.
|
||||||
|
|
||||||
|
PROGRESS (2026-06-29): force-directed layout SOLVER built + tested (lib/src/graph/force_layout.dart, 6 dart-test cases, commit bcef3a6f) — deterministic Fruchterman-Reingold, Flutter-free. REMAINING (the bulk, multi-session): graph data model from pql backlinks/outlinks over the vault; CustomPaint rendering (nodes+edges); pan/zoom; hover-highlight-subgraph; click-opens-note; filter pane (tag/glob/depth); MultitabPane + context-panel slot (D-47) wiring; widget/golden tests. This is a full interactive Tier-5 pane, a fresh-session undertaking — only its solver core is done.
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02): the graph rendering + interaction foundation is BUILT + tested (4 slices, all committed). (1) force-directed solver — force_layout.dart (bcef3a6f). (2) link-graph model — vault_graph.dart: VaultGraph.fromOutlinks, neighborhood, edgePairs (084bf842). (3) CustomPaint renderer — graph_painter.dart: edges/nodes/labels, hover-dim, fit transform (6bea1723). (4) interactive GraphView — graph_view.dart: hover-highlight + click-to-open, shared GraphViewport hit-testing (c529703b). The graph is a fully working WIDGET (renders + hover + click). REMAINING = the app-integration phase: pql link-data wiring (pql outlinks over the vault -> the file->outlinks map -> VaultGraph), the pane shell (async load + filter: tag/glob/depth + pan/zoom), and MultitabPane + context-panel slot (D-47) + GraphExtension registration.
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02, session 2): app-integration phase — 3 slices committed. (1) GraphController (graph_controller.dart, 3b67725b) — pql.files + per-file pql.outlinks over the vault -> VaultGraph; loading/error state; debounced files.changed refresh; 6 tests. (2) GraphPanel shell (graph_panel.dart, 6fae1bc4) — loading/empty/error states + GraphView + click-to-open via editor.open; 5 widget tests. (3) GraphExtension registration (extension.dart + builtin.graph i18n, a5acb6ac) — graph.view context-panel tab (D-47), activates after builtin.pql dep; 4 tests; CHANGELOG entry. The graph is now REACHABLE end-to-end (21 graph tests green). REMAINING (T-323 enrichment): pan/zoom on the view; filter controls (tag include/exclude, glob, depth-from-active).
|
||||||
|
|
||||||
|
COMPLETE (2026-07-02, session 2). All scope delivered + tested (56 graph tests, make test green, real-boot verified via testmode). Slices this session: GraphController (3b67725b), pane shell (6fae1bc4), extension+i18n registration (a5acb6ac), pan+zoom (e6556401), testmode coverage (46d9b403), filter model (f859173b), filter bar (aabec87d). Final feature: force-directed vault graph in the context panel (graph.view, D-47) — hover-highlight, click-to-open, scroll-zoom, drag-pan, and filters (path glob re-queries pql; depth-from-active local graph; tri-state tag include/exclude). Controller uses pql.meta (outlinks+tags in one call) and strips #heading fragments so heading links connect. NOTE: pql data load is 1 files + N meta calls (sequential); batching is a future optimisation if large vaults feel slow.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 11:17:23', '2026-07-02 19:47:37.694', NULL, '6ef9144fd880038d5db1d318925a9afd', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2G2KHKT5CJYR0TK1WQGMD0', 'story', '06FHAX7FV5KWGZQ31617R63W94', 'Tier-5 graph view (builtin.graph)', 'Tier-5 graph VIEW, absorbed from the former T-7 into the unified canvas epic (T-317). Force-directed layout of the vault (or a filtered subset): nodes are notes, edges are wikilinks. Hover highlights the connected subgraph; click opens the note in the editor. Filter pane: tag include/exclude, file glob, depth-from-active. pql provides the link data (pql backlinks / pql outlinks); rendering is owned in-app (CustomPaint, own-the-rendering-stack). Lives in its own slot per D-47 (context panel), uses MultitabPane (T-83). DISTINCT from the in-card graph TEMPLATE (T-321): T-321 renders a graph passed into the conversation drawing card; this is the full interactive graph pane over the whole vault. Relates to the core renderer (T-318) but has its own force-directed layout.
|
||||||
|
|
||||||
|
PROGRESS (2026-06-29): force-directed layout SOLVER built + tested (lib/src/graph/force_layout.dart, 6 dart-test cases, commit bcef3a6f) — deterministic Fruchterman-Reingold, Flutter-free. REMAINING (the bulk, multi-session): graph data model from pql backlinks/outlinks over the vault; CustomPaint rendering (nodes+edges); pan/zoom; hover-highlight-subgraph; click-opens-note; filter pane (tag/glob/depth); MultitabPane + context-panel slot (D-47) wiring; widget/golden tests. This is a full interactive Tier-5 pane, a fresh-session undertaking — only its solver core is done.
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02): the graph rendering + interaction foundation is BUILT + tested (4 slices, all committed). (1) force-directed solver — force_layout.dart (bcef3a6f). (2) link-graph model — vault_graph.dart: VaultGraph.fromOutlinks, neighborhood, edgePairs (084bf842). (3) CustomPaint renderer — graph_painter.dart: edges/nodes/labels, hover-dim, fit transform (6bea1723). (4) interactive GraphView — graph_view.dart: hover-highlight + click-to-open, shared GraphViewport hit-testing (c529703b). The graph is a fully working WIDGET (renders + hover + click). REMAINING = the app-integration phase: pql link-data wiring (pql outlinks over the vault -> the file->outlinks map -> VaultGraph), the pane shell (async load + filter: tag/glob/depth + pan/zoom), and MultitabPane + context-panel slot (D-47) + GraphExtension registration.
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02, session 2): app-integration phase — 3 slices committed. (1) GraphController (graph_controller.dart, 3b67725b) — pql.files + per-file pql.outlinks over the vault -> VaultGraph; loading/error state; debounced files.changed refresh; 6 tests. (2) GraphPanel shell (graph_panel.dart, 6fae1bc4) — loading/empty/error states + GraphView + click-to-open via editor.open; 5 widget tests. (3) GraphExtension registration (extension.dart + builtin.graph i18n, a5acb6ac) — graph.view context-panel tab (D-47), activates after builtin.pql dep; 4 tests; CHANGELOG entry. The graph is now REACHABLE end-to-end (21 graph tests green). REMAINING (T-323 enrichment): pan/zoom on the view; filter controls (tag include/exclude, glob, depth-from-active).
|
||||||
|
|
||||||
|
COMPLETE (2026-07-02, session 2). All scope delivered + tested (56 graph tests, make test green, real-boot verified via testmode). Slices this session: GraphController (3b67725b), pane shell (6fae1bc4), extension+i18n registration (a5acb6ac), pan+zoom (e6556401), testmode coverage (46d9b403), filter model (f859173b), filter bar (aabec87d). Final feature: force-directed vault graph in the context panel (graph.view, D-47) — hover-highlight, click-to-open, scroll-zoom, drag-pan, and filters (path glob re-queries pql; depth-from-active local graph; tri-state tag include/exclude). Controller uses pql.meta (outlinks+tags in one call) and strips #heading fragments so heading links connect. NOTE: pql data load is 1 files + N meta calls (sequential); batching is a future optimisation if large vaults feel slow.', 'done', 'medium', NULL, NULL, NULL, '2026-06-10 11:17:23', '2026-07-02 19:47:44.357', NULL, '4d534c5ebbc8245ac9cdf1c41ddfca06', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2G1WD1839Z90AQ5C0BHNV4', 'story', '06FHAX7FV5KWGZQ31617R63W94', 'Tier-5 canvas pane (builtin.canvas)', 'Tier-5 canvas PANE, absorbed from the former T-7 into the unified canvas epic (T-317, decision D-91). A full workspace pane that renders .canvas files by CONVERTING them into the drawing-card JSON (per D-91: .canvas is an import format, not a native schema) and painting via the shared canvas renderer (T-318). Scope: nodes (note, text, group, image) + edges + layout state; pan/zoom; node selection, drag, resize; edit affordances (add note from file picker, add text node, draw edge between nodes); persist layout back to the .canvas file on disk. Uses MultitabPane (T-83) for tabs and its own slot per D-47; panels are extension-shaped (D-17). Depends on the core renderer (T-318). NOTE: unlike the conversation drawing card (display-only, D-78), this pane is interactive/editable — it is a pane, not a conversation widget.
|
||||||
|
|
||||||
|
SCOPE NOTE (2026-06-29): currently a 17-line stub. This is a from-scratch interactive Tier-5 pane (parse .canvas -> drawing-card JSON, CustomPaint render, pan/zoom, node select/drag/resize, edit affordances, persist .canvas) — a multi-session feature, NOT template-class work. The drawing-card TEMPLATE half of epic T-317 is now complete (svg/d2/image/icon/compare/graph + stdin).
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02): two foundation slices built + tested (from the 17-line stub). (1) JSONCanvas parser/model — lib/src/canvas/json_canvas.dart (2eb5d0f1): typed CanvasDoc (text/file/link/group nodes + edges with sides/ends/color/label), parse + toJson round-trip, skips unknown/malformed; pure Dart, 8 dart-test cases. (2) CustomPaint renderer — lib/builtin/canvas/src/canvas_painter.dart (035a37a1): CanvasBounds + pan/zoom CanvasViewport, group frames + edges (arrowheads) + node cards; node colours from the file (Obsidian presets/hex, NOT clide tokens per arbitrary-content rule), chrome themed; 7 tests (pixel-probe + viewport + colour). DECISION NOTE: the interactive pane paints the CanvasDoc model directly via CustomPaint (hit-testable, cheap re-paint) rather than re-lowering to SVG each frame — the ticket''s ''convert to drawing-card JSON'' path (D-91 SVG substrate) is for the display-only viewer; an editable pane needs direct model painting. Mirrors the graph pane. REMAINING (multi-session): interactive CanvasView (hit-test nodes + pan/zoom + click-select), pane shell (load .canvas from disk, workspace slot D-47, MultitabPane), CanvasExtension registration + open-.canvas routing, then edit affordances (add node/edge, drag, resize) + persist back to disk.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 11:17:17', '2026-07-02 20:27:03.493', NULL, '163772c5b568c9fb0b69715648555c30', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FJ91RYFXBJH99HJ18QTHY2B0', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'Model switch + effort toggle: unclear UI feedback', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-07-02 20:31:02.783', '2026-07-02 20:31:02.783', NULL, '931821336de8bd9ca5e77cd62f3ca1d3', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FJ91RYFXBJH99HJ18QTHY2B0', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'Model switch + effort toggle: unclear UI feedback', 'REPORTED (2026-07-02, Jeroen): can''t trust that the model switch and the effort toggle actually took effect when used — the UI feedback + positioning leave it uncertain whether the change applied. INVESTIGATE: what confirmation (if any) fires today when the model or effort is changed, and where it surfaces relative to the control. Likely-related surfaces: the Config tab controls (T-414), /effort ownership (T-412), account/model settings control. IMPROVE: give an unmistakable, well-positioned confirmation that the change landed — e.g. the control reflects the new value immediately (selected state), and/or a brief toast/inline acknowledgement near the control, not somewhere the eye isn''t. Goal: after a switch, the user is certain which model/effort is now active.', 'backlog', 'medium', NULL, NULL, NULL, '2026-07-02 20:31:02.783', '2026-07-02 20:31:02.806', NULL, 'b4e300bee467ba284a30772cb7a186bb', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2G1WD1839Z90AQ5C0BHNV4', 'story', '06FHAX7FV5KWGZQ31617R63W94', 'Tier-5 canvas pane (builtin.canvas)', 'Tier-5 canvas PANE, absorbed from the former T-7 into the unified canvas epic (T-317, decision D-91). A full workspace pane that renders .canvas files by CONVERTING them into the drawing-card JSON (per D-91: .canvas is an import format, not a native schema) and painting via the shared canvas renderer (T-318). Scope: nodes (note, text, group, image) + edges + layout state; pan/zoom; node selection, drag, resize; edit affordances (add note from file picker, add text node, draw edge between nodes); persist layout back to the .canvas file on disk. Uses MultitabPane (T-83) for tabs and its own slot per D-47; panels are extension-shaped (D-17). Depends on the core renderer (T-318). NOTE: unlike the conversation drawing card (display-only, D-78), this pane is interactive/editable — it is a pane, not a conversation widget.
|
||||||
|
|
||||||
|
SCOPE NOTE (2026-06-29): currently a 17-line stub. This is a from-scratch interactive Tier-5 pane (parse .canvas -> drawing-card JSON, CustomPaint render, pan/zoom, node select/drag/resize, edit affordances, persist .canvas) — a multi-session feature, NOT template-class work. The drawing-card TEMPLATE half of epic T-317 is now complete (svg/d2/image/icon/compare/graph + stdin).
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02): two foundation slices built + tested (from the 17-line stub). (1) JSONCanvas parser/model — lib/src/canvas/json_canvas.dart (2eb5d0f1): typed CanvasDoc (text/file/link/group nodes + edges with sides/ends/color/label), parse + toJson round-trip, skips unknown/malformed; pure Dart, 8 dart-test cases. (2) CustomPaint renderer — lib/builtin/canvas/src/canvas_painter.dart (035a37a1): CanvasBounds + pan/zoom CanvasViewport, group frames + edges (arrowheads) + node cards; node colours from the file (Obsidian presets/hex, NOT clide tokens per arbitrary-content rule), chrome themed; 7 tests (pixel-probe + viewport + colour). DECISION NOTE: the interactive pane paints the CanvasDoc model directly via CustomPaint (hit-testable, cheap re-paint) rather than re-lowering to SVG each frame — the ticket''s ''convert to drawing-card JSON'' path (D-91 SVG substrate) is for the display-only viewer; an editable pane needs direct model painting. Mirrors the graph pane. REMAINING (multi-session): interactive CanvasView (hit-test nodes + pan/zoom + click-select), pane shell (load .canvas from disk, workspace slot D-47, MultitabPane), CanvasExtension registration + open-.canvas routing, then edit affordances (add node/edge, drag, resize) + persist back to disk.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 11:17:17', '2026-07-02 20:35:12.739', NULL, '3e2c84564d3b6cf59f8c8f0e8af1b0b5', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FJ91RYFXBJH99HJ18QTHY2B0', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'Model switch + effort toggle: unclear UI feedback', 'REPORTED (2026-07-02, Jeroen): can''t trust that the model switch and the effort toggle actually took effect when used — the UI feedback + positioning leave it uncertain whether the change applied. INVESTIGATE: what confirmation (if any) fires today when the model or effort is changed, and where it surfaces relative to the control. Likely-related surfaces: the Config tab controls (T-414), /effort ownership (T-412), account/model settings control. IMPROVE: give an unmistakable, well-positioned confirmation that the change landed — e.g. the control reflects the new value immediately (selected state), and/or a brief toast/inline acknowledgement near the control, not somewhere the eye isn''t. Goal: after a switch, the user is certain which model/effort is now active.
|
||||||
|
|
||||||
|
EVIDENCE + ROOT CAUSE (2026-07-02, from a /model screenshot). The confirmation renders as a raw, unstyled line: literally ''<local-command-stdout>Set model to claude-fable-5[1m] (claude-fable-5)</local-command-stdout>'', shown under the ''you'' (user) speaker stripe. Three distinct defects in that one line: (a) the CLI''s <local-command-stdout> wrapper tag is displayed verbatim instead of being unwrapped; (b) an ANSI SGR bold code leaks through as literal ''[1m]'' (a \x1b[1m) — slash-command stdout is not ANSI-stripped; (c) it is attributed to the USER speaker, so a system/command acknowledgement masquerades as something the user typed. ROOT CAUSE: clide has NO handling for local-command-stdout at all (grep in lib/ = zero hits) — the slash-command result falls through and is rendered as plain user prose. ENTRY POINTS for the fix: inbound message parse / role attribution in lib/builtin/claude/src/transcript_reader.dart; existing ANSI-strip refs in transcript_reader.dart + claude_composer.dart. IMPROVE: detect local-command-stdout content, unwrap the tag + strip ANSI, and render it as a distinct, clearly-positioned system/command-acknowledgement (NOT the user stripe) so a /model or /effort change is an unmistakable confirmation. Affects all slash-command stdout (model, effort, etc.), so fix at the render/parse path, not per-command.', 'backlog', 'medium', NULL, NULL, NULL, '2026-07-02 20:31:02.783', '2026-07-02 20:39:06.684', NULL, '8678946b438ae5cbdff677bb73949374', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FJ91RYFXBJH99HJ18QTHY2B0', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'Model switch + effort toggle: unclear UI feedback', 'REPORTED (2026-07-02, Jeroen): can''t trust that the model switch and the effort toggle actually took effect when used — the UI feedback + positioning leave it uncertain whether the change applied. INVESTIGATE: what confirmation (if any) fires today when the model or effort is changed, and where it surfaces relative to the control. Likely-related surfaces: the Config tab controls (T-414), /effort ownership (T-412), account/model settings control. IMPROVE: give an unmistakable, well-positioned confirmation that the change landed — e.g. the control reflects the new value immediately (selected state), and/or a brief toast/inline acknowledgement near the control, not somewhere the eye isn''t. Goal: after a switch, the user is certain which model/effort is now active.
|
||||||
|
|
||||||
|
EVIDENCE + ROOT CAUSE (2026-07-02, from a /model screenshot). The confirmation renders as a raw, unstyled line: literally ''<local-command-stdout>Set model to claude-fable-5[1m] (claude-fable-5)</local-command-stdout>'', shown under the ''you'' (user) speaker stripe. Three distinct defects in that one line: (a) the CLI''s <local-command-stdout> wrapper tag is displayed verbatim instead of being unwrapped; (b) an ANSI SGR bold code leaks through as literal ''[1m]'' (a \x1b[1m) — slash-command stdout is not ANSI-stripped; (c) it is attributed to the USER speaker, so a system/command acknowledgement masquerades as something the user typed. ROOT CAUSE: clide has NO handling for local-command-stdout at all (grep in lib/ = zero hits) — the slash-command result falls through and is rendered as plain user prose. ENTRY POINTS for the fix: inbound message parse / role attribution in lib/builtin/claude/src/transcript_reader.dart; existing ANSI-strip refs in transcript_reader.dart + claude_composer.dart. IMPROVE: detect local-command-stdout content, unwrap the tag + strip ANSI, and render it as a distinct, clearly-positioned system/command-acknowledgement (NOT the user stripe) so a /model or /effort change is an unmistakable confirmation. Affects all slash-command stdout (model, effort, etc.), so fix at the render/parse path, not per-command.
|
||||||
|
|
||||||
|
SCOPE (do not narrow): the local-command-stdout rendering defect above is ONE symptom, not the whole ticket. The core problem is that after switching model or effort the user has no reliable, well-placed signal that it took effect — so this stays a broad investigate-and-improve of the switch/toggle trust UX, with the render bug as just the first concrete instance. Other symptoms/questions to investigate (non-exhaustive): does the control itself (Config tab picker / effort control / any menu) visibly update to the NEW active value after a switch, or does it look unchanged? is there ANY acknowledgement at the point of interaction, or only (mangled) output buried in the transcript far from where the user clicked? do model vs effort behave consistently, or differently? can clide''s shown ''active'' value drift from what actually took effect (e.g. a switch that silently no-ops)? is the confirmation positioned where the eye already is? The deliverable is: across all these controls, a consistent and unmistakable ''this is now active'' — fixing the render path is necessary but not sufficient. Enumerate the full symptom set during investigation before designing the fix.', 'backlog', 'medium', NULL, NULL, NULL, '2026-07-02 20:31:02.783', '2026-07-02 20:41:13.402', NULL, '9c7b3632bd0846533a1b65cc12b8145a', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2G1WD1839Z90AQ5C0BHNV4', 'story', '06FHAX7FV5KWGZQ31617R63W94', 'Tier-5 canvas pane (builtin.canvas)', 'Tier-5 canvas PANE, absorbed from the former T-7 into the unified canvas epic (T-317, decision D-91). A full workspace pane that renders .canvas files by CONVERTING them into the drawing-card JSON (per D-91: .canvas is an import format, not a native schema) and painting via the shared canvas renderer (T-318). Scope: nodes (note, text, group, image) + edges + layout state; pan/zoom; node selection, drag, resize; edit affordances (add note from file picker, add text node, draw edge between nodes); persist layout back to the .canvas file on disk. Uses MultitabPane (T-83) for tabs and its own slot per D-47; panels are extension-shaped (D-17). Depends on the core renderer (T-318). NOTE: unlike the conversation drawing card (display-only, D-78), this pane is interactive/editable — it is a pane, not a conversation widget.
|
||||||
|
|
||||||
|
SCOPE NOTE (2026-06-29): currently a 17-line stub. This is a from-scratch interactive Tier-5 pane (parse .canvas -> drawing-card JSON, CustomPaint render, pan/zoom, node select/drag/resize, edit affordances, persist .canvas) — a multi-session feature, NOT template-class work. The drawing-card TEMPLATE half of epic T-317 is now complete (svg/d2/image/icon/compare/graph + stdin).
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02): two foundation slices built + tested (from the 17-line stub). (1) JSONCanvas parser/model — lib/src/canvas/json_canvas.dart (2eb5d0f1): typed CanvasDoc (text/file/link/group nodes + edges with sides/ends/color/label), parse + toJson round-trip, skips unknown/malformed; pure Dart, 8 dart-test cases. (2) CustomPaint renderer — lib/builtin/canvas/src/canvas_painter.dart (035a37a1): CanvasBounds + pan/zoom CanvasViewport, group frames + edges (arrowheads) + node cards; node colours from the file (Obsidian presets/hex, NOT clide tokens per arbitrary-content rule), chrome themed; 7 tests (pixel-probe + viewport + colour). DECISION NOTE: the interactive pane paints the CanvasDoc model directly via CustomPaint (hit-testable, cheap re-paint) rather than re-lowering to SVG each frame — the ticket''s ''convert to drawing-card JSON'' path (D-91 SVG substrate) is for the display-only viewer; an editable pane needs direct model painting. Mirrors the graph pane. REMAINING (multi-session): interactive CanvasView (hit-test nodes + pan/zoom + click-select), pane shell (load .canvas from disk, workspace slot D-47, MultitabPane), CanvasExtension registration + open-.canvas routing, then edit affordances (add node/edge, drag, resize) + persist back to disk.
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02, cont.): third foundation slice landed — interactive CanvasView (lib/builtin/canvas/src/canvas_view.dart, 7700fc7a): hitTestCanvasNode (topmost node under a point; cards beat the group frames behind, via the painter''s own viewport) + scroll-zoom + drag-pan + click-select with a focus ring; 6 tests. The canvas rendering + interaction FOUNDATION is now complete + tested (parser 2eb5d0f1, renderer 035a37a1, view 7700fc7a — 21 canvas tests), mirroring where T-323''s foundation was before its integration phase. NOT yet reachable. REACHABLE-INTEGRATION PHASE (next, fresh session): (a) pane shell — a StatefulWidget that loads a .canvas from disk (files.read/editor.open path), holds the CanvasDoc, embeds CanvasView; (b) CanvasExtension registration as a workspace pane (own slot D-47, MultitabPane T-83); (c) FILE-OPEN ROUTING — how opening a .canvas lands in this pane instead of the text editor: NOTE the TabContribution.fileGlobs field is DECLARED but has ZERO consumers in lib/, so this routing must be built (wire fileGlobs, or have the editor delegate by extension, or a command + workspace tab); (d) i18n + tests. THEN the edit phase: add node/text/edge affordances, node drag + resize, and persist the CanvasDoc back to the .canvas file (encode() already exists).', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 11:17:17', '2026-07-02 21:22:59.395', NULL, '6ba2a2c1892dbf56c582f83bd9753314', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2G1WD1839Z90AQ5C0BHNV4', 'story', '06FHAX7FV5KWGZQ31617R63W94', 'Tier-5 canvas pane (builtin.canvas)', 'Tier-5 canvas PANE, absorbed from the former T-7 into the unified canvas epic (T-317, decision D-91). A full workspace pane that renders .canvas files by CONVERTING them into the drawing-card JSON (per D-91: .canvas is an import format, not a native schema) and painting via the shared canvas renderer (T-318). Scope: nodes (note, text, group, image) + edges + layout state; pan/zoom; node selection, drag, resize; edit affordances (add note from file picker, add text node, draw edge between nodes); persist layout back to the .canvas file on disk. Uses MultitabPane (T-83) for tabs and its own slot per D-47; panels are extension-shaped (D-17). Depends on the core renderer (T-318). NOTE: unlike the conversation drawing card (display-only, D-78), this pane is interactive/editable — it is a pane, not a conversation widget.
|
||||||
|
|
||||||
|
SCOPE NOTE (2026-06-29): currently a 17-line stub. This is a from-scratch interactive Tier-5 pane (parse .canvas -> drawing-card JSON, CustomPaint render, pan/zoom, node select/drag/resize, edit affordances, persist .canvas) — a multi-session feature, NOT template-class work. The drawing-card TEMPLATE half of epic T-317 is now complete (svg/d2/image/icon/compare/graph + stdin).
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02): two foundation slices built + tested (from the 17-line stub). (1) JSONCanvas parser/model — lib/src/canvas/json_canvas.dart (2eb5d0f1): typed CanvasDoc (text/file/link/group nodes + edges with sides/ends/color/label), parse + toJson round-trip, skips unknown/malformed; pure Dart, 8 dart-test cases. (2) CustomPaint renderer — lib/builtin/canvas/src/canvas_painter.dart (035a37a1): CanvasBounds + pan/zoom CanvasViewport, group frames + edges (arrowheads) + node cards; node colours from the file (Obsidian presets/hex, NOT clide tokens per arbitrary-content rule), chrome themed; 7 tests (pixel-probe + viewport + colour). DECISION NOTE: the interactive pane paints the CanvasDoc model directly via CustomPaint (hit-testable, cheap re-paint) rather than re-lowering to SVG each frame — the ticket''s ''convert to drawing-card JSON'' path (D-91 SVG substrate) is for the display-only viewer; an editable pane needs direct model painting. Mirrors the graph pane. REMAINING (multi-session): interactive CanvasView (hit-test nodes + pan/zoom + click-select), pane shell (load .canvas from disk, workspace slot D-47, MultitabPane), CanvasExtension registration + open-.canvas routing, then edit affordances (add node/edge, drag, resize) + persist back to disk.
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02, cont.): third foundation slice landed — interactive CanvasView (lib/builtin/canvas/src/canvas_view.dart, 7700fc7a): hitTestCanvasNode (topmost node under a point; cards beat the group frames behind, via the painter''s own viewport) + scroll-zoom + drag-pan + click-select with a focus ring; 6 tests. The canvas rendering + interaction FOUNDATION is now complete + tested (parser 2eb5d0f1, renderer 035a37a1, view 7700fc7a — 21 canvas tests), mirroring where T-323''s foundation was before its integration phase. NOT yet reachable. REACHABLE-INTEGRATION PHASE (next, fresh session): (a) pane shell — a StatefulWidget that loads a .canvas from disk (files.read/editor.open path), holds the CanvasDoc, embeds CanvasView; (b) CanvasExtension registration as a workspace pane (own slot D-47, MultitabPane T-83); (c) FILE-OPEN ROUTING — how opening a .canvas lands in this pane instead of the text editor: NOTE the TabContribution.fileGlobs field is DECLARED but has ZERO consumers in lib/, so this routing must be built (wire fileGlobs, or have the editor delegate by extension, or a command + workspace tab); (d) i18n + tests. THEN the edit phase: add node/text/edge affordances, node drag + resize, and persist the CanvasDoc back to the .canvas file (encode() already exists).
|
||||||
|
|
||||||
|
Refinement (2026-07-03, user-confirmed): (1) SLOT: Slots.workspace — diff/editor recipe, center column; coexists with Claude the way diff does (D-47). (2) TABS: real MultitabPane<CanvasDoc> — N .canvas files open as tabs (user explicitly chose over the single-TabContribution graph/diff precedent; needs MultitabController plumbing like Claude sessions). (3) FILE-OPEN ROUTING resolved by inspection: TabContribution.fileGlobs is dead (zero consumers) — do NOT wire it. Add a .canvas branch in lib/kernel/src/file_open.dart openWorkspaceFile() publishing to ''builtin.canvas''/''selection'' (mirror of the .md branch), and D-6 CLI parity via a ''canvas'' entry in the _readers map at lib/src/daemon/ui_command.dart:34 → clide ui open canvas <path> (diff precedent). (4) Fix stale doc header lib/src/canvas/json_canvas.dart:5-7 (claims SVG-lowering; actual architecture paints CanvasDoc directly per the ticket''s DECISION NOTE) in the same pass. (5) Edit phase later needs a mutation path (CanvasView has no onChanged; CanvasDoc is immutable-construction only) — deferred, flagged now.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 11:17:17', '2026-07-03 20:20:00.827', NULL, '34eeab12879a55e77e6c9841a442cf27', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2G1WD1839Z90AQ5C0BHNV4', 'story', '06FHAX7FV5KWGZQ31617R63W94', 'Tier-5 canvas pane (builtin.canvas)', 'Tier-5 canvas PANE, absorbed from the former T-7 into the unified canvas epic (T-317, decision D-91). A full workspace pane that renders .canvas files by CONVERTING them into the drawing-card JSON (per D-91: .canvas is an import format, not a native schema) and painting via the shared canvas renderer (T-318). Scope: nodes (note, text, group, image) + edges + layout state; pan/zoom; node selection, drag, resize; edit affordances (add note from file picker, add text node, draw edge between nodes); persist layout back to the .canvas file on disk. Uses MultitabPane (T-83) for tabs and its own slot per D-47; panels are extension-shaped (D-17). Depends on the core renderer (T-318). NOTE: unlike the conversation drawing card (display-only, D-78), this pane is interactive/editable — it is a pane, not a conversation widget.
|
||||||
|
|
||||||
|
SCOPE NOTE (2026-06-29): currently a 17-line stub. This is a from-scratch interactive Tier-5 pane (parse .canvas -> drawing-card JSON, CustomPaint render, pan/zoom, node select/drag/resize, edit affordances, persist .canvas) — a multi-session feature, NOT template-class work. The drawing-card TEMPLATE half of epic T-317 is now complete (svg/d2/image/icon/compare/graph + stdin).
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02): two foundation slices built + tested (from the 17-line stub). (1) JSONCanvas parser/model — lib/src/canvas/json_canvas.dart (2eb5d0f1): typed CanvasDoc (text/file/link/group nodes + edges with sides/ends/color/label), parse + toJson round-trip, skips unknown/malformed; pure Dart, 8 dart-test cases. (2) CustomPaint renderer — lib/builtin/canvas/src/canvas_painter.dart (035a37a1): CanvasBounds + pan/zoom CanvasViewport, group frames + edges (arrowheads) + node cards; node colours from the file (Obsidian presets/hex, NOT clide tokens per arbitrary-content rule), chrome themed; 7 tests (pixel-probe + viewport + colour). DECISION NOTE: the interactive pane paints the CanvasDoc model directly via CustomPaint (hit-testable, cheap re-paint) rather than re-lowering to SVG each frame — the ticket''s ''convert to drawing-card JSON'' path (D-91 SVG substrate) is for the display-only viewer; an editable pane needs direct model painting. Mirrors the graph pane. REMAINING (multi-session): interactive CanvasView (hit-test nodes + pan/zoom + click-select), pane shell (load .canvas from disk, workspace slot D-47, MultitabPane), CanvasExtension registration + open-.canvas routing, then edit affordances (add node/edge, drag, resize) + persist back to disk.
|
||||||
|
|
||||||
|
PROGRESS (2026-07-02, cont.): third foundation slice landed — interactive CanvasView (lib/builtin/canvas/src/canvas_view.dart, 7700fc7a): hitTestCanvasNode (topmost node under a point; cards beat the group frames behind, via the painter''s own viewport) + scroll-zoom + drag-pan + click-select with a focus ring; 6 tests. The canvas rendering + interaction FOUNDATION is now complete + tested (parser 2eb5d0f1, renderer 035a37a1, view 7700fc7a — 21 canvas tests), mirroring where T-323''s foundation was before its integration phase. NOT yet reachable. REACHABLE-INTEGRATION PHASE (next, fresh session): (a) pane shell — a StatefulWidget that loads a .canvas from disk (files.read/editor.open path), holds the CanvasDoc, embeds CanvasView; (b) CanvasExtension registration as a workspace pane (own slot D-47, MultitabPane T-83); (c) FILE-OPEN ROUTING — how opening a .canvas lands in this pane instead of the text editor: NOTE the TabContribution.fileGlobs field is DECLARED but has ZERO consumers in lib/, so this routing must be built (wire fileGlobs, or have the editor delegate by extension, or a command + workspace tab); (d) i18n + tests. THEN the edit phase: add node/text/edge affordances, node drag + resize, and persist the CanvasDoc back to the .canvas file (encode() already exists).
|
||||||
|
|
||||||
|
Refinement (2026-07-03, user-confirmed): (1) SLOT: Slots.workspace — diff/editor recipe, center column; coexists with Claude the way diff does (D-47). (2) TABS: real MultitabPane<CanvasDoc> — N .canvas files open as tabs (user explicitly chose over the single-TabContribution graph/diff precedent; needs MultitabController plumbing like Claude sessions). (3) FILE-OPEN ROUTING resolved by inspection: TabContribution.fileGlobs is dead (zero consumers) — do NOT wire it. Add a .canvas branch in lib/kernel/src/file_open.dart openWorkspaceFile() publishing to ''builtin.canvas''/''selection'' (mirror of the .md branch), and D-6 CLI parity via a ''canvas'' entry in the _readers map at lib/src/daemon/ui_command.dart:34 → clide ui open canvas <path> (diff precedent). (4) Fix stale doc header lib/src/canvas/json_canvas.dart:5-7 (claims SVG-lowering; actual architecture paints CanvasDoc directly per the ticket''s DECISION NOTE) in the same pass. (5) Edit phase later needs a mutation path (CanvasView has no onChanged; CanvasDoc is immutable-construction only) — deferred, flagged now.
|
||||||
|
|
||||||
|
PROGRESS (2026-07-05): REACHABLE-INTEGRATION PHASE COMPLETE (e2e305de). CanvasExtension is a real workspace pane: app-scoped MultitabController on the extension (diff/T-233 pattern) with real per-document sub-tabs (MultitabPane keepAlive, per refinement decision), CanvasPaneHost + CanvasDocumentTab (files.read -> CanvasDoc.parse -> CanvasView; loading/error states), routing wired (openWorkspaceFile .canvas branch; ui.open ''canvas'' reader so clide ui open canvas <path> works; fileGlobs left dead as decided), en_us+nl_nl catalogs, json_canvas doc header corrected. Tests: extension lifecycle (7), pane-host widget (6), file_open routing (5, new file), ui_command canvas case; testmode extension roster now includes builtin.canvas (26/26 real-boot pass). REMAINING (edit phase, fresh session): node drag + resize, add note/text/edge affordances, persist CanvasDoc back to disk — encode() exists; CanvasView needs an onChanged/mutation path (refinement note item 5).', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 11:17:17', '2026-07-05 07:29:15.447', NULL, '1be84ba4d4357171ef329f39b601f4e1', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FK3CDZQD2JM95ZQY5MH53PWC', 'task', '06FB0TNQM5TWC00GW0P3X02HZW', 'Composer bypass reachability: Ctrl/Cmd+Shift+M full cycle + shift-click bypass row', 'User decision (2026-07-05), amending T-226''s refinement wording: the original intent was Ctrl/Cmd+Shift+M cycling the FULL permission-mode list including bypassPermissions, with plain Ctrl/Cmd+M keeping the safe trio (default/acceptEdits/plan). Holding shift IS the explicit opt-in gesture — no separate confirm needed. Likewise the composer mode popup (T-275): the bypass row is currently permanently disabled, pointing at ''the cockpit''s confirmed path'' (T-181 roster shift-click) — but that roster is ghost-fed (T-396, TeamMemberJoined never emitted in production), so bypass is unreachable from the primary session''s UI entirely; a visible-but-unreachable option is worse than none. Scope: (1) claude_status: kFullPermissionCycle + nextPermissionMode(); (2) composer key handler: mod+shift+M -> full cycle, mod+M (no shift) -> safe trio; (3) PermissionModeControl: bypass row enabled behind a shift-click gate (plain click no-ops, menu stays open; shift-click selects + closes), trailing ''shift-click'' hint, semantics + tooltip updated; (4) i18n en+nl; (5) tests. The /permissions picker and roster badge paths are unchanged. Refs T-226, T-275, T-181, T-396.', 'backlog', 'medium', NULL, NULL, NULL, '2026-07-05 09:52:40.379', '2026-07-05 09:52:40.379', NULL, 'ec1eed07c9a69b4edbf3f4ffe671f696', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FK3CDZQD2JM95ZQY5MH53PWC', 'task', '06FB0TNQM5TWC00GW0P3X02HZW', 'Composer bypass reachability: Ctrl/Cmd+Shift+M full cycle + shift-click bypass row', 'User decision (2026-07-05), amending T-226''s refinement wording: the original intent was Ctrl/Cmd+Shift+M cycling the FULL permission-mode list including bypassPermissions, with plain Ctrl/Cmd+M keeping the safe trio (default/acceptEdits/plan). Holding shift IS the explicit opt-in gesture — no separate confirm needed. Likewise the composer mode popup (T-275): the bypass row is currently permanently disabled, pointing at ''the cockpit''s confirmed path'' (T-181 roster shift-click) — but that roster is ghost-fed (T-396, TeamMemberJoined never emitted in production), so bypass is unreachable from the primary session''s UI entirely; a visible-but-unreachable option is worse than none. Scope: (1) claude_status: kFullPermissionCycle + nextPermissionMode(); (2) composer key handler: mod+shift+M -> full cycle, mod+M (no shift) -> safe trio; (3) PermissionModeControl: bypass row enabled behind a shift-click gate (plain click no-ops, menu stays open; shift-click selects + closes), trailing ''shift-click'' hint, semantics + tooltip updated; (4) i18n en+nl; (5) tests. The /permissions picker and roster badge paths are unchanged. Refs T-226, T-275, T-181, T-396.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-07-05 09:52:40.379', '2026-07-05 09:52:54.013', NULL, '56204043cd686ca0fcae06db735b72ef', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FK3CDZQD2JM95ZQY5MH53PWC', 'task', '06FB0TNQM5TWC00GW0P3X02HZW', 'Composer bypass reachability: Ctrl/Cmd+Shift+M full cycle + shift-click bypass row', 'User decision (2026-07-05), amending T-226''s refinement wording: the original intent was Ctrl/Cmd+Shift+M cycling the FULL permission-mode list including bypassPermissions, with plain Ctrl/Cmd+M keeping the safe trio (default/acceptEdits/plan). Holding shift IS the explicit opt-in gesture — no separate confirm needed. Likewise the composer mode popup (T-275): the bypass row is currently permanently disabled, pointing at ''the cockpit''s confirmed path'' (T-181 roster shift-click) — but that roster is ghost-fed (T-396, TeamMemberJoined never emitted in production), so bypass is unreachable from the primary session''s UI entirely; a visible-but-unreachable option is worse than none. Scope: (1) claude_status: kFullPermissionCycle + nextPermissionMode(); (2) composer key handler: mod+shift+M -> full cycle, mod+M (no shift) -> safe trio; (3) PermissionModeControl: bypass row enabled behind a shift-click gate (plain click no-ops, menu stays open; shift-click selects + closes), trailing ''shift-click'' hint, semantics + tooltip updated; (4) i18n en+nl; (5) tests. The /permissions picker and roster badge paths are unchanged. Refs T-226, T-275, T-181, T-396.', 'done', 'medium', NULL, NULL, NULL, '2026-07-05 09:52:40.379', '2026-07-05 10:54:23.532', NULL, '143695c5eefa42dab2c05edcb06a4427', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FNR3DP0D48AZZ3NRA8JF3Q10', 'task', NULL, 'Per-workspace PATH preset: settings + CLI (env.path) + spawn injection, worktree-aware (FR-1)', NULL, 'backlog', 'high', NULL, NULL, 'D-106', '2026-07-13 15:17:10.275', '2026-07-13 15:17:10.275', NULL, 'eb6abfb644234fe0c73b4785efcdad16', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FNR3DP0D48AZZ3NRA8JF3Q10', 'task', NULL, 'Per-workspace PATH preset: settings + CLI (env.path) + spawn injection, worktree-aware (FR-1)', NULL, 'in_progress', 'high', NULL, NULL, 'D-106', '2026-07-13 15:17:10.275', '2026-07-13 15:17:17.532', NULL, '6e4b7347d0e854ea4c5fe4b394c61412', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FNR3DP0D48AZZ3NRA8JF3Q10', 'task', NULL, 'Per-workspace PATH preset: settings + CLI (env.path) + spawn injection, worktree-aware (FR-1)', 'Implements D-106 (FR-1 + worktree addendum). Storage: user-scope key app.env.pathPrepend.<fnv1a64Hex(canonicalWorkspaceKey(presetRoot))> in ~/.config/clide/settings.yaml — never committed; presetRootFor() resolves a linked-worktree workspace (gitdir: pointer file, e.g. .worktrees/<name>) to the main repo root so all worktrees share the repo''s preset.
|
||||||
|
|
||||||
|
Injection (prepend, dedup, next-spawn semantics): PTY panes via PaneRegistry.pathForSpawn hook (main.dart wires the preset-aware closure); hosted Claude via agentEnvDelta(prependDirs:) — now exports PATH whenever a preset exists even if clide is already on PATH (previous gap) — threaded orchestrator.pathPresetFor ← claude extension ← ctx.settings. Toolchain/supporter-binary resolution deliberately preset-blind (T-98/D-92 fence).
|
||||||
|
|
||||||
|
CLI (D-6): one cmd id env.path → clide env path list|set|add|remove|clear|capture; Flutter-free handler lib/src/daemon/env_path_commands.dart over an injected PathPresetStore port (adapter in main.dart); mutations publish on envPathChannel; capture diffs loginShellPathOrNull() (new shell_env getter) vs process PATH. UI: Settings → Tools → Workspace PATH section, custom control (ordered list, add/remove/reorder, missing-dir warning, worktree note, capture suggestions) in builtin.tools-settings; en+nl catalogs.
|
||||||
|
|
||||||
|
.worktrees/ added to IgnoreSet.builtin() and the project-init gitignore scaffold. Tests: path_preset_test (pure fns + real worktree layout), env_path_commands_test (all verbs), agent_bootstrap prependDirs cases, PaneRegistry pathForSpawn PTY test, widget tests for the control incl. worktree key sharing + capture. make test (4048) + make test-a11y green.', 'in_progress', 'high', NULL, NULL, 'D-106', '2026-07-13 15:17:10.275', '2026-07-13 15:39:31.309', NULL, 'c5a978f32c7497e478c471069ed20b7c', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FNR3DP0D48AZZ3NRA8JF3Q10', 'task', NULL, 'Per-workspace PATH preset: settings + CLI (env.path) + spawn injection, worktree-aware (FR-1)', 'Implements D-106 (FR-1 + worktree addendum). Storage: user-scope key app.env.pathPrepend.<fnv1a64Hex(canonicalWorkspaceKey(presetRoot))> in ~/.config/clide/settings.yaml — never committed; presetRootFor() resolves a linked-worktree workspace (gitdir: pointer file, e.g. .worktrees/<name>) to the main repo root so all worktrees share the repo''s preset.
|
||||||
|
|
||||||
|
Injection (prepend, dedup, next-spawn semantics): PTY panes via PaneRegistry.pathForSpawn hook (main.dart wires the preset-aware closure); hosted Claude via agentEnvDelta(prependDirs:) — now exports PATH whenever a preset exists even if clide is already on PATH (previous gap) — threaded orchestrator.pathPresetFor ← claude extension ← ctx.settings. Toolchain/supporter-binary resolution deliberately preset-blind (T-98/D-92 fence).
|
||||||
|
|
||||||
|
CLI (D-6): one cmd id env.path → clide env path list|set|add|remove|clear|capture; Flutter-free handler lib/src/daemon/env_path_commands.dart over an injected PathPresetStore port (adapter in main.dart); mutations publish on envPathChannel; capture diffs loginShellPathOrNull() (new shell_env getter) vs process PATH. UI: Settings → Tools → Workspace PATH section, custom control (ordered list, add/remove/reorder, missing-dir warning, worktree note, capture suggestions) in builtin.tools-settings; en+nl catalogs.
|
||||||
|
|
||||||
|
.worktrees/ added to IgnoreSet.builtin() and the project-init gitignore scaffold. Tests: path_preset_test (pure fns + real worktree layout), env_path_commands_test (all verbs), agent_bootstrap prependDirs cases, PaneRegistry pathForSpawn PTY test, widget tests for the control incl. worktree key sharing + capture. make test (4048) + make test-a11y green.', 'done', 'high', NULL, NULL, 'D-106', '2026-07-13 15:17:10.275', '2026-07-13 16:48:56.833', NULL, '09c6b262b3c0ecfd2f621d27f2cc5eb2', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FNTF7KE5Y497PQC7SCFGR1KC', 'task', NULL, 'Lead the clide skill description with clide capabilities + env mechanics (FR-2)', NULL, 'backlog', 'low', NULL, NULL, NULL, '2026-07-13 20:48:23.409', '2026-07-13 20:48:23.409', NULL, '28edb12dfa1f2bf75d108f6cf9465ea5', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FNTF7KE5Y497PQC7SCFGR1KC', 'task', NULL, 'Lead the clide skill description with clide capabilities + env mechanics (FR-2)', NULL, 'done', 'low', NULL, NULL, NULL, '2026-07-13 20:48:23.409', '2026-07-13 20:49:17.282', NULL, 'b7eaec1d037d1c36cf54fac07b3b2dd4', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
+582
@@ -0,0 +1,582 @@
|
|||||||
|
# AGENTS.md for clide — Mistral Vibe operating as Claude Code peer
|
||||||
|
|
||||||
|
This file configures Mistral Vibe to operate in this repo with the same
|
||||||
|
effectiveness as Claude Code. It distills the CLAUDE.md guardrails and all
|
||||||
|
`.claude/skills/` into Vibe's instruction hierarchy, preserving "Claude punch"
|
||||||
|
in a hybrid workflow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Identity
|
||||||
|
|
||||||
|
You are operating in **clide** — a Flutter/Dart IDE for Claude Code CLI.
|
||||||
|
Your role: **peer to Claude Code**, not replacement. Maintain Claude's
|
||||||
|
behavioral standards, tool discipline, and architectural rigor.
|
||||||
|
|
||||||
|
**Primary directive:** Never lose "Claude punch" — the combination of
|
||||||
|
strict guardrails, CLI-first interaction, and parity between UI and CLI
|
||||||
|
that defines effective operation in this repo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Non-Negotiable Guardrails (from CLAUDE.md)
|
||||||
|
|
||||||
|
These are load-bearing. Violating any means the design is wrong, not the rule.
|
||||||
|
|
||||||
|
- **Flutter desktop is the host. No Electron, ever.**
|
||||||
|
- **Single process.** The Flutter app hosts everything in-process: IPC server,
|
||||||
|
subsystem handlers (pane, files, editor, git, pql), extensions.
|
||||||
|
- **CLI-first, not MCP.** Drive via `clide <subsystem> <verb>` Bash commands.
|
||||||
|
- **Dart is the core; pql fills the query gap.** PTY spawning is native Dart FFI
|
||||||
|
(`posix_openpt` + `posix_spawn`). `pql` (Go) handles vault queries.
|
||||||
|
- **Own the rendering stack.** PTY, markdown, graph, canvas — all clide-owned.
|
||||||
|
- **User/Claude parity (D-6).** Every CLI subcommand has a UI affordance,
|
||||||
|
and every UI action has a CLI equivalent.
|
||||||
|
- **pql: wrap, don't duplicate.** Clide wraps pql; never re-implements it.
|
||||||
|
- **Repo-is-the-workspace.** Git repo root is the workspace.
|
||||||
|
- **Decision discipline.** All architectural choices live in
|
||||||
|
`governance/decisions/<domain>.md` as `D-NNN` records. Open questions as
|
||||||
|
`Q-NNN` under `governance/questions/<domain>.md`. Rejected as `R-NNN`.
|
||||||
|
- **No pre-existing excuse.** Solo-dev repo — if `make test` is red, fix it
|
||||||
|
first, then your work. Surface blockers; don't push on top of broken state.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tool Discipline (from CLAUDE.md)
|
||||||
|
|
||||||
|
### Make targets are the entry points
|
||||||
|
|
||||||
|
| Purpose | Command | Never call directly |
|
||||||
|
|---------|---------|---------------------|
|
||||||
|
| Launch app | `make run` | `flutter run` |
|
||||||
|
| Static analysis | `make analyze` | `flutter analyze` |
|
||||||
|
| Format | `make format` | `dart format` |
|
||||||
|
| Fast test suite | `make test` | `flutter test` |
|
||||||
|
| Core subsystem tests | `make test-core` | underlying scripts |
|
||||||
|
| Accessibility tests | `make test-a11y` | underlying scripts |
|
||||||
|
| Integration tests | `make test-integration` | underlying scripts |
|
||||||
|
| Pre-push gate | `make push-check` | `ci/*` scripts |
|
||||||
|
| Setup hooks | `make hooks` | `cp .githooks/* .git/hooks/` |
|
||||||
|
| Clean | `make clean` | `rm -rf build/` |
|
||||||
|
|
||||||
|
### Shell hygiene
|
||||||
|
|
||||||
|
- **Working directory is repo root** — never `cd /path/to/clide` or `git -C`
|
||||||
|
- **One command per invocation** — no `&&`/`;` chaining
|
||||||
|
- Exception: `git commit -F` HEREDOC for multi-line messages
|
||||||
|
- Prefer Read/Edit/Grep tools over `cat`/`sed`/`grep` for file inspection
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Git Workflow (from git-commit skill)
|
||||||
|
|
||||||
|
### Commit message format: Conventional Commits 1.0
|
||||||
|
|
||||||
|
```
|
||||||
|
<type>(<scope>): <imperative subject> (<T-NNN>)
|
||||||
|
|
||||||
|
Optional body: explain WHY, not WHAT. Wrap at ~72 chars.
|
||||||
|
|
||||||
|
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Type:** `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `chore`
|
||||||
|
- Use `feat`/`fix` for user-visible behavior
|
||||||
|
- Use `chore` for bookkeeping (`chore(plan)` for pql ticket housekeeping)
|
||||||
|
- Append `!` after scope for breaking changes: `feat(ipc)!: ...`
|
||||||
|
|
||||||
|
**Scope:** Optional but preferred — subsystem: `settings`, `vim`, `pty`, `git`, `plan`
|
||||||
|
- Lower-case, no spaces
|
||||||
|
|
||||||
|
**Subject:** ≤ 72 characters INCLUDING prefix. No emojis. No "and".
|
||||||
|
|
||||||
|
**Ticket ref:** Trailing `(T-NNN)` when work has a ticket.
|
||||||
|
|
||||||
|
**Body:** Explain the *why*. The diff shows the *what*. Don't restate it.
|
||||||
|
- Hard cap: **60 words per bullet** for CHANGELOG entries
|
||||||
|
- No multi-paragraph bullets
|
||||||
|
- No sub-headers inside bullets
|
||||||
|
- No probe numbers, latency stats, or %-coverage deltas
|
||||||
|
|
||||||
|
### Logically-separated commits
|
||||||
|
|
||||||
|
1. **Read `git status` + `git diff` first** — never stage blind
|
||||||
|
2. **Group by concern, not location:**
|
||||||
|
- Bookkeeping: `.gitignore`, lockfiles, config
|
||||||
|
- Documentation: `README.md`, ADRs, design notes
|
||||||
|
- Tooling/skills: reusable, non-project-specific
|
||||||
|
- Project conventions: repo's own rules
|
||||||
|
- Feature/subsystem: one cohesive change
|
||||||
|
- Layer changes: app, sidecar CLI, daemon, IPC, pql wrapper, canvas, git panel
|
||||||
|
3. **Prefer many small focused commits over one large mixed one**
|
||||||
|
4. **Use `git add <specific paths>`** — NEVER `git add -A`, `git add .`, `git add -u`
|
||||||
|
5. **Verify between commits** with `git status`, `git diff --staged`, `git log -1`
|
||||||
|
|
||||||
|
### Changelog discipline
|
||||||
|
|
||||||
|
- **Every user-visible commit must touch CHANGELOG.md** under `## [Unreleased]`
|
||||||
|
- Use Keep a Changelog 1.1.0 format with sections: Added, Changed, Deprecated, Removed, Fixed, Security
|
||||||
|
- Entries: short imperative phrases describing user-facing impact
|
||||||
|
- **60 words hard cap per bullet** — verify with `make changelog-gate`
|
||||||
|
- What skips changelog: pure bookkeeping with no user-visible effect
|
||||||
|
|
||||||
|
### Safety rules (reinforced)
|
||||||
|
|
||||||
|
- **Never** `--no-verify`
|
||||||
|
- **Never** `--amend` unless user explicitly asks
|
||||||
|
- **Never** force-push to `main` or `master`
|
||||||
|
- **Always** check `git status` before staging, `git diff --staged` before committing
|
||||||
|
- Commit message via HEREDOC:
|
||||||
|
```bash
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
<message>
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
### What NOT to commit
|
||||||
|
|
||||||
|
- `.env`, `*.env.local`
|
||||||
|
- `.claude/settings.local.json`
|
||||||
|
- Build artefacts: `sidecar/bin/`, `sidecar/dist/`, `build/`, `app/.dart_tool/`
|
||||||
|
- SQLite index files: `*.sqlite`, `*.sqlite-wal`, `*.sqlite-shm`, `*.db`
|
||||||
|
- Coverage/test output: `*.out`, `coverage.*`, `*.test`
|
||||||
|
- `.pql/changelog/` — auto-staged by pre-commit hook from pql DB
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## pql — Vault Queries + Project Planning
|
||||||
|
|
||||||
|
`pql` indexes a vault into SQLite and exposes structural queries plus a
|
||||||
|
planning layer for decision records and tickets. One binary, two surfaces.
|
||||||
|
|
||||||
|
### Precondition
|
||||||
|
|
||||||
|
```bash
|
||||||
|
command -v pql
|
||||||
|
```
|
||||||
|
|
||||||
|
If absent, tell the user to install from
|
||||||
|
https://github.com/postmeridiem/pql/releases/latest. Don't install it
|
||||||
|
yourself. Don't fall back to grep unless the user explicitly asks.
|
||||||
|
|
||||||
|
### First touch: learn the vault
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pql schema
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns one row per frontmatter key with observed types and file counts.
|
||||||
|
Run once per session before writing queries.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Surface 1: Vault queries
|
||||||
|
|
||||||
|
#### Subcommands
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `pql files [glob]` | List indexed files; optional glob filter |
|
||||||
|
| `pql tags [--sort count]` | Distinct tags with counts |
|
||||||
|
| `pql backlinks <path>` | Files linking TO a path |
|
||||||
|
| `pql outlinks <path>` | Links FROM a file |
|
||||||
|
| `pql meta <path>` | Frontmatter + tags + outlinks + headings for one file |
|
||||||
|
| `pql schema` | Typed frontmatter schema |
|
||||||
|
| `pql base <name>` | Execute an Obsidian .base file |
|
||||||
|
| `pql shell` | Interactive REPL (indexes once, then query per line) |
|
||||||
|
| `pql query "<DSL>"` | SQL-derived DSL for complex queries |
|
||||||
|
| `pql doctor` | Resolved vault/config/DB/index state |
|
||||||
|
|
||||||
|
#### DSL examples
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT name, fm.date WHERE fm.type = 'meeting' ORDER BY fm.date DESC LIMIT 10
|
||||||
|
SELECT path WHERE 'project' IN tags ORDER BY path
|
||||||
|
SELECT name, fm.prior_job WHERE fm.type = 'council-member' ORDER BY name
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `--file q.pql` or `--stdin` for long queries. Don't interpolate vault
|
||||||
|
content into the command line.
|
||||||
|
|
||||||
|
#### Query cookbook
|
||||||
|
|
||||||
|
- **Files in folder** → `pql files 'sessions/*'`
|
||||||
|
- **Top tags** → `pql tags --sort count --limit 20`
|
||||||
|
- **What links to X?** → `pql backlinks members/vaasa/persona.md`
|
||||||
|
- **Date range** → `pql query "SELECT name, fm.date WHERE fm.date BETWEEN '2024-01-01' AND '2024-12-31'"`
|
||||||
|
- **Run a Base** → `pql base council-sessions`
|
||||||
|
- **Inspect one file** → `pql meta members/vaasa/persona.md --pretty`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Surface 2: Planning (decisions + tickets)
|
||||||
|
|
||||||
|
Planning state lives in `<vault>/.pql/pql.db` (user-authored state, not a
|
||||||
|
cache). Decision records come from the DQR tree — `governance/{decisions,
|
||||||
|
questions,rejected}/<domain>.md` by default (D-21), configurable via
|
||||||
|
`dqr_dir` in `.pql/config.yaml` or the `PQL_DQR_DIR` env var (env > file >
|
||||||
|
default); a legacy flat `decisions/` is auto-detected as a fallback.
|
||||||
|
Tickets are SQLite-native.
|
||||||
|
|
||||||
|
#### Decision subcommands
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `pql decisions sync [--no-style]` | Parse the DQR tree → upsert into pql.db; surfaces style warnings (filename, subdir-type, domain pairing/conflicts) unless `--no-style` |
|
||||||
|
| `pql decisions validate [--no-style]` | Dry-run parse; structural errors exit non-zero, style issues warn (suppress with `--no-style`) |
|
||||||
|
| `pql decisions claim <D\|Q\|R> <domain> "title"` | Print next available ID |
|
||||||
|
| `pql decisions list [--type X] [--domain X] [--status X]` | List decisions |
|
||||||
|
| `pql decisions show <id> [--with-refs] [--with-tickets]` | Show with joins |
|
||||||
|
| `pql decisions coverage` | Confirmed decisions without tickets |
|
||||||
|
| `pql decisions refs <id>` | Cross-references involving a decision |
|
||||||
|
|
||||||
|
Always `pql decisions sync` before querying if decisions/*.md may have changed.
|
||||||
|
|
||||||
|
#### Ticket subcommands
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `pql ticket new <type> "title" [--parent T-NNN] [--decision D-NNN] [--priority P] [--id-only]` | Create (emits T-NNN; `--parent` files it under an epic/story in one step; `--id-only` prints the bare id for tree-creation scripts) |
|
||||||
|
| `pql ticket list [--status S] [--team T] [--assigned A] [--label L] [--under T-NNN] [--leaf] [--unblocked]` | List with filters. `--under` = recursive descendants of a ticket; `--leaf` = no children; `--unblocked` = blockers all reached a terminal status |
|
||||||
|
| `pql ticket show <id[,id,...]> [--with-context] [--with-blockers] [--with-children] [--tree] [--depth N]` | Show one or more (comma-batch → array of show-trees). `--with-children` = direct children; `--tree` = nested descendant subtree + direct parent (cap with `--depth N`) |
|
||||||
|
| `pql ticket status <id> <new-status> [--force]` | Change status. Closing (terminal status) is blocked while the ticket has open children; `--force` cascades that status to all not-yet-closed descendants and lists them |
|
||||||
|
| `pql ticket statuslist` | List the configured status vocabulary (name, label, class, order, is_default, is_terminal) — what a UI reads to render columns |
|
||||||
|
| `pql ticket relabel <id\|record_id> [--new-label T-NNN] [--fix-prose]` | Reassign a ticket's friendly T-NNN label (reconcile a duplicate-label collision). Identity (record_id) and the structural graph are untouched; only the label moves. `--fix-prose` rewrites stale T-NNN mentions in DQR markdown |
|
||||||
|
| `pql ticket assign <id> <agent>` | Set assignee |
|
||||||
|
| `pql ticket setparent <id[,id,...]> <parent-id \| none>` | Set (or clear with `none`) a ticket's **parent** — the hierarchy link (epic→story→task). Positional, not a flag. This is the parent/child relationship, distinct from blockers |
|
||||||
|
| `pql ticket append <id> <text\|--file\|--stdin>` | Append to the description (blank-line separated); never round-trips existing text |
|
||||||
|
| `pql ticket block <id> --by <other>` | Add a **blocker** (a dependency: <id> can't start until <other> is done) — NOT a parent/child link; use `setparent` or `new --parent` for hierarchy |
|
||||||
|
| `pql ticket unblock <id> --from <other>` | Remove blocker |
|
||||||
|
| `pql ticket team <id> <team>` | Set team |
|
||||||
|
| `pql ticket label <id> add\|rm <label>` | Manage labels |
|
||||||
|
| `pql ticket board [--team T]` | Kanban board view |
|
||||||
|
| `pql ticket refine list` | Tickets with empty descriptions, status-priority-sorted |
|
||||||
|
| `pql ticket refine next [--skip N]` | Head of the unrefined queue with full show-tree + remaining count |
|
||||||
|
| `pql ticket refine write <id> <json\|--file\|--stdin>` | Patch writable fields (title, description, priority, type) |
|
||||||
|
|
||||||
|
Ticket types: initiative, epic, story, task, bug.
|
||||||
|
The `id` you type and see (T-NNN) is a friendly label backed by a stable
|
||||||
|
underwater `record_id` (also in output); two clones never collide on identity,
|
||||||
|
and a duplicate label is fixed with `pql ticket relabel` (D-26).
|
||||||
|
Statuses are a per-vault vocabulary (`ticket_statuses` in `.pql/config.yaml`),
|
||||||
|
defaulting to: backlog, ready, in_progress, review, done, cancelled. Each status
|
||||||
|
has a class — initial, active, review, terminal — that the engine reasons about.
|
||||||
|
Run `pql ticket statuslist` to discover the live set. Any status can transition
|
||||||
|
to any other — pql does not enforce a state machine — except that a ticket
|
||||||
|
cannot reach a terminal status while it has open children (use `--force` to
|
||||||
|
cascade the close down the subtree).
|
||||||
|
|
||||||
|
#### Plan subcommands
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `pql plan status` | Dashboard: decision counts, open Qs, ticket summary, coverage gaps |
|
||||||
|
| `pql plan whatsnext` | Next ticket to work on (active work, then the "ready" lane) with full context bundle |
|
||||||
|
| `pql plan review` | Next ticket awaiting review with full context bundle |
|
||||||
|
| `pql plan export [--stage]` | Append changed planning rows to `.pql/changelog/<table>/<YYYY-MM>.sql` (the git-tracked log of record); `--stage` also `git add`s them. Normally a no-op — mutations already write through |
|
||||||
|
| `pql plan import [--legacy FILE]` | Replay `.pql/changelog/` into `pql.db` (or one-time `--legacy pql-plan.json` migration from the pre-D-15 snapshot) |
|
||||||
|
| `pql plan rebuild` | Drop replicated tables and replay `.pql/changelog/` from scratch. Warns on stderr (`changelog.ticket_id_collision`) + lists `collisions` in the result if one ticket id was filed twice across clones |
|
||||||
|
|
||||||
|
#### Versioning planning state
|
||||||
|
|
||||||
|
`pql.db` is gitignored — the durable, git-tracked artifact is
|
||||||
|
`.pql/changelog/` (D-15/D-16). Ticket mutations **write through** to the
|
||||||
|
changelog synchronously, so it is always current; you never have to
|
||||||
|
remember to "export". The hooks installed by `pql init` do the rest:
|
||||||
|
|
||||||
|
- `pre-commit` stages `.pql/changelog/` so it lands in the same commit as
|
||||||
|
the change that produced it.
|
||||||
|
- `post-merge` replays incoming changelog edits (`pql plan import`) and
|
||||||
|
re-syncs decisions from their markdown.
|
||||||
|
- `post-checkout` / `post-rewrite` rebuild `pql.db` from the changelog.
|
||||||
|
|
||||||
|
On a fresh clone, `pql plan import` (run automatically on first open)
|
||||||
|
replays the changelog into a new `pql.db`. There is **no** `pql-plan.json`
|
||||||
|
snapshot — that artifact is retired; `pql plan export` is now only a
|
||||||
|
manual catch-up/reconcile.
|
||||||
|
|
||||||
|
A ticket mutation (create / status transition / any write) leaves
|
||||||
|
`.pql/changelog/` dirty by design — the `pre-commit` hook stages it onto
|
||||||
|
the next `git commit`. This is expected, not a problem to flag. Don't
|
||||||
|
narrate "the ticket won't persist until committed" on every edit; either
|
||||||
|
fold the bookkeeping into a commit or trust the normal commit flow.
|
||||||
|
|
||||||
|
#### Planning cookbook
|
||||||
|
|
||||||
|
- **Sync and list confirmed** → `pql decisions sync && pql decisions list --type confirmed`
|
||||||
|
- **Show with refs** → `pql decisions show D-5 --with-refs --pretty`
|
||||||
|
- **Read full body** → `pql decisions read D-5`
|
||||||
|
- **Create ticket** → `pql ticket new task "implement X" --decision D-5`
|
||||||
|
- **Create ticket, capture id for a script** → `id=$(pql ticket new task "implement X" --id-only)` — prints just `T-NNN`
|
||||||
|
- **File a ticket under an epic** → `pql ticket new bug "fix X" --parent T-276` (one step), or reparent an existing one → `pql ticket setparent T-9 T-276` (clear with `none`). Parent = hierarchy; use `block` only for blocking dependencies
|
||||||
|
- **Batch close** → `pql ticket status T-1,T-2,T-3 done`
|
||||||
|
- **Full context** → `pql ticket show T-5 --with-context --pretty`
|
||||||
|
- **Batch show** → `pql ticket show T-1,T-2,T-3 --pretty`
|
||||||
|
- **Refine next ticket** → `pql ticket refine next --pretty`, then `pql ticket refine write T-N '{"description":"..."}'`
|
||||||
|
- **Append a note** → `pql ticket append T-5 "benchmarked; TTL now 5m"` — blank-line separated, never overwrites; use `--file note.md` or `--stdin` for longer content
|
||||||
|
- **Subtree of an epic** → `pql ticket show T-2 --tree --pretty` — nested `subtree` + direct parent in `ancestors`; add `--depth N` to cap levels
|
||||||
|
- **Ready leaf work under an epic** → `pql ticket list --under T-2 --leaf --unblocked` — leaf tickets beneath T-2 whose blockers have all reached a terminal status; the batch complement to `plan whatsnext`
|
||||||
|
- **What's next?** → `pql plan whatsnext --pretty`
|
||||||
|
- **Review queue** → `pql plan review --pretty`
|
||||||
|
- **Coverage gaps** → `pql decisions coverage`
|
||||||
|
- **Dashboard** → `pql plan status --pretty`
|
||||||
|
- **Force a changelog catch-up** → `pql plan export` (normally a no-op; mutations already write through to `.pql/changelog/`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Output contract (both surfaces)
|
||||||
|
|
||||||
|
- **stdout:** JSON array (default); `--jsonl` for one object/line; `--pretty`; `--limit N`.
|
||||||
|
- **stderr:** JSON diagnostics `{"level":"...","code":"pql.<phase>.<kind>","msg":"..."}`.
|
||||||
|
- **Exit codes:**
|
||||||
|
- `0` — success, including zero matches (empty `[]` — say "no matches", not "failed")
|
||||||
|
- `64` — bad flag
|
||||||
|
- `65` — parse/compile error (pass stderr back)
|
||||||
|
- `66` — vault/config not found
|
||||||
|
- `69` — unavailable
|
||||||
|
- `70` — internal error
|
||||||
|
|
||||||
|
### Anti-patterns
|
||||||
|
|
||||||
|
- Don't pipe to `jq` for simple projections — use `--limit`, `--pretty`, `--jsonl`.
|
||||||
|
- Don't chain `pql files` + `pql meta` — one `pql query` with WHERE.
|
||||||
|
- Don't parse errors — pass stderr diagnostics back directly.
|
||||||
|
- Don't forget `pql decisions sync` before querying decisions.
|
||||||
|
- Don't try to install or upgrade pql — instruct the user if missing.
|
||||||
|
|
||||||
|
### When NOT to use
|
||||||
|
|
||||||
|
- **Body text search** → `grep`/`rg`.
|
||||||
|
- **Reading file contents** → `Read` tool.
|
||||||
|
- **Code structure** → tree-sitter / LSP.
|
||||||
|
- **Modifying vault files** → `Write`/`Edit`. pql doesn't write to vault content.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Driving clide UI via CLI (from clide skill)
|
||||||
|
|
||||||
|
**Core principle: Every UI action has a CLI equivalent (D-6).**
|
||||||
|
Discover the live surface; don't hard-code it.
|
||||||
|
|
||||||
|
### Discover capabilities
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clide capabilities
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns JSON: every registered command, split into `subsystem` + `verb`,
|
||||||
|
with argument schema. **This is authoritative** — re-run it, don't trust
|
||||||
|
remembered lists.
|
||||||
|
|
||||||
|
### Slots (layout areas)
|
||||||
|
|
||||||
|
- `sidebar` — left
|
||||||
|
- `workspace` — center (where Claude lives)
|
||||||
|
- `context` — right
|
||||||
|
- `statusbar` — bottom
|
||||||
|
|
||||||
|
### Observe state
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# One-shot orientation
|
||||||
|
clide status
|
||||||
|
|
||||||
|
# Narrower snapshots
|
||||||
|
clide pane list
|
||||||
|
clide editor active
|
||||||
|
clide git status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Drive UI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Open a doc in a GUI reader
|
||||||
|
clide ui open <reader> <ref>
|
||||||
|
# readers: tickets, decisions, markdown, diff
|
||||||
|
# examples:
|
||||||
|
clide ui open tickets T-123
|
||||||
|
clide ui open decisions D-456
|
||||||
|
clide ui open diff lib/src/foo.dart
|
||||||
|
clide ui open markdown docs/bar.md
|
||||||
|
|
||||||
|
# Show diff and scroll to file
|
||||||
|
clide ui diff lib/src/foo.dart
|
||||||
|
|
||||||
|
# Raise a toast
|
||||||
|
clide ui toast "message" --severity success|warning|error|info
|
||||||
|
|
||||||
|
# General: clide <subsystem> <verb> [args]
|
||||||
|
clide files list
|
||||||
|
clide editor open <path>
|
||||||
|
clide pane focus <pane-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Convention:** If a drive verb has no live GUI, return toolError
|
||||||
|
("no live UI to drive"), not hang. Exit code conveys ok/usage/tool error.
|
||||||
|
JSON on stdout.
|
||||||
|
|
||||||
|
### You only see what flows through clide
|
||||||
|
|
||||||
|
Your own non-`clide` shell work (plain file reads, `make test`, `git`) is
|
||||||
|
outside clide's view by design (D-83). Run it **through** `clide ...` if
|
||||||
|
you want clide to observe it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testmode (from testmode skill)
|
||||||
|
|
||||||
|
`ClideTestApp` is a lightweight Flutter app for integration testing.
|
||||||
|
Catches regressions unit tests cannot: missing binaries, broken subprocess
|
||||||
|
wiring, IPC dispatch failures, extension activation order, theme parse errors.
|
||||||
|
|
||||||
|
### Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make run-testmode # all categories, 60s timeout
|
||||||
|
make run-testmode TESTMODE_CATEGORY=toolchain
|
||||||
|
make run-testmode TESTMODE_CATEGORY=ipc
|
||||||
|
make run-testmode TESTMODE_CATEGORY=extensions
|
||||||
|
make run-testmode TESTMODE_TIMEOUT=120
|
||||||
|
```
|
||||||
|
|
||||||
|
Results: stdout + `/tmp/clide-testmode.log`
|
||||||
|
Last line is machine-readable JSON: `{"passed":N,"failed":M,"total":N+M,"failures":[...]}`
|
||||||
|
|
||||||
|
### When to run
|
||||||
|
|
||||||
|
| Changed area | Category | Why |
|
||||||
|
|-------------|----------|-----|
|
||||||
|
| Toolchain, PATH, ptyc | `toolchain` | Binary resolution + exec |
|
||||||
|
| IPC envelope, dispatcher | `ipc` | Round-trip + error contract |
|
||||||
|
| Extension manifest, activate | `extensions` | Register + activate lifecycle |
|
||||||
|
| Theme YAML, loader | `extensions` | Theme parse is in this category |
|
||||||
|
| Platform config | `all` | Full rebuild validates everything |
|
||||||
|
| Any doubt | `all` | ~30s, cheap insurance |
|
||||||
|
|
||||||
|
### Interpreting output
|
||||||
|
|
||||||
|
- `[testmode] exec | ... | OK` — subprocess ran, exit 0 or 1
|
||||||
|
- `[testmode] PASS | ...` — assertion passed
|
||||||
|
- `[testmode] FAIL | ...` — assertion failed
|
||||||
|
- `[testmode] exec | ... | EXCEPTION` — binary not found or not executable
|
||||||
|
- `[testmode] exec | ... | TIMEOUT` — subprocess hung
|
||||||
|
|
||||||
|
If no `[testmode]` lines appear, the testmode gate didn't fire — verify
|
||||||
|
`CLIDE_TESTMODE` is set to a non-empty string.
|
||||||
|
|
||||||
|
### Adding tests
|
||||||
|
|
||||||
|
All test logic in `lib/test_app.dart`. Pattern:
|
||||||
|
```dart
|
||||||
|
await _testExec('label', binary, ['args'], workDir);
|
||||||
|
// or
|
||||||
|
_addResult('label', boolCondition, 'detail string');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Repo Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
lib/
|
||||||
|
main.dart # Flutter entry point
|
||||||
|
app.dart # Root layout, workspace, panels
|
||||||
|
clide.dart # Barrel: shared types
|
||||||
|
src/ # Core subsystems (IPC, PTY, git, files, pql, panes)
|
||||||
|
kernel/ # Kernel services (theme, i18n, settings, panels)
|
||||||
|
builtin/ # Built-in extensions
|
||||||
|
widgets/ # Custom widget primitives
|
||||||
|
extension/ # Extension contract and registration
|
||||||
|
lua/ # Lua runtime support
|
||||||
|
|
||||||
|
test/ # All tests (core + widgets + goldens + a11y)
|
||||||
|
assets/ # Fonts, themes, grammars, licenses, logo
|
||||||
|
linux/, macos/, web/ # Flutter platform directories
|
||||||
|
native/ # Vendored native libs (tree-sitter, dugite)
|
||||||
|
governance/ # D/Q/R records
|
||||||
|
docs/ # Design docs, wireframes
|
||||||
|
legacy/ # Python Textual clide v1.2 (frozen)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependency & Supply Chain Discipline
|
||||||
|
|
||||||
|
- **Prefer-zero-deps.** Flutter-SDK widgets first; third-party needs justification
|
||||||
|
- **Exact-pinned in pubspec.yaml** — no caret ranges
|
||||||
|
- **Advisories reviewed** before every bump
|
||||||
|
- **pubspec.lock committed**
|
||||||
|
- **Document every bundled dependency** in `assets/licenses.yaml`:
|
||||||
|
- name, kind, version, homepage, license, purpose
|
||||||
|
- Adding a dep: two-step commit — artefact AND `licenses.yaml` entry
|
||||||
|
- Native deps (dugite, libtree-sitter): vendored in `native/`, pinned by SHA
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pre-push Gate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make push-check
|
||||||
|
```
|
||||||
|
|
||||||
|
Runs: decisions validation + core tests + fast suite + a11y + coverage + changelog
|
||||||
|
|
||||||
|
**Never bypass.** If it fails, fix the underlying issue.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Session Setup
|
||||||
|
|
||||||
|
One-time setup on fresh clone:
|
||||||
|
```bash
|
||||||
|
make hooks && flutter pub get
|
||||||
|
pql init # wires up pql skill + perms
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mistral-Specific Notes
|
||||||
|
|
||||||
|
### What to preserve from Claude
|
||||||
|
|
||||||
|
- CLI-first interaction model
|
||||||
|
- Strict guardrails enforcement
|
||||||
|
- Logically-separated commits
|
||||||
|
- Changelog discipline (60-word cap)
|
||||||
|
- pql as single source of truth for decisions/tickets
|
||||||
|
- UI/CLI parity
|
||||||
|
- Testmode for integration validation
|
||||||
|
|
||||||
|
### What to adapt
|
||||||
|
|
||||||
|
- **Tool names:** Claude's `Bash(...)` → my `bash` tool
|
||||||
|
- **Agent spawning:** Claude's `Agent{}` → my `task` tool for subagents
|
||||||
|
- **Permissions:** `.claude/settings.json` allow/deny → my system prompt + your instructions
|
||||||
|
|
||||||
|
### When in doubt
|
||||||
|
|
||||||
|
Ask: "What would Claude do?" Then do that. The AGENTS.md is the bridge,
|
||||||
|
not a replacement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference Card
|
||||||
|
|
||||||
|
| Task | Command |
|
||||||
|
|------|---------|
|
||||||
|
| Discover clide commands | `clide capabilities` |
|
||||||
|
| See current UI state | `clide status` |
|
||||||
|
| Open ticket in UI | `clide ui open tickets T-NNN` |
|
||||||
|
| Toast notification | `clide ui toast "msg" --severity info` |
|
||||||
|
| List actionable tickets | `pql ticket list --under <id> --leaf --unblocked --status backlog --pretty` |
|
||||||
|
| Sync decisions | `pql decisions sync` |
|
||||||
|
| Run integration tests | `make run-testmode` |
|
||||||
|
| Fast test suite | `make test` |
|
||||||
|
| Full pre-push | `make push-check` |
|
||||||
|
| Commit with message | `git commit -m "$(cat <<'EOF'\n<message>\nEOF\n)"` |
|
||||||
|
| Verify changelog | `make changelog-gate` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Generated for Mistral Vibe. Preserves Claude Code punch for clide repo.*
|
||||||
+395
@@ -16,6 +16,401 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
## [2.10.0] — 2026-07-13
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Canvas pane.** `.canvas` (Obsidian JSONCanvas) files now open in an
|
||||||
|
interactive workspace pane — each document a sub-tab, with pan, zoom, and
|
||||||
|
node selection. Routed from the file tree, quick-open, and
|
||||||
|
`clide ui open canvas <path>`. (T-322)
|
||||||
|
- **Vault graph view.** A force-directed link graph of the whole vault in the
|
||||||
|
context panel — notes are nodes, wikilinks edges. Hover highlights a note's
|
||||||
|
neighbourhood; click opens it. Scroll to zoom, drag to pan; filter by path
|
||||||
|
glob, tag include/exclude, or depth from the active note. (T-323)
|
||||||
|
- **Per-workspace PATH preset.** Directories prepended to the PATH of every
|
||||||
|
shell clide spawns for a repo — Claude sessions and terminal panes. Settings
|
||||||
|
→ Tools → Workspace PATH, or `clide env path …` with capture-from-login-shell
|
||||||
|
suggestions. Worktrees share their repo's preset; stored machine-local, never
|
||||||
|
committed. (D-106, T-511)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Permission mode: Ctrl/Cmd+Shift+M cycles the full list including
|
||||||
|
bypassPermissions, and the composer menu's bypass row unlocks via
|
||||||
|
shift-click — plain Ctrl/Cmd+M and plain clicks stay safe-trio only (T-510).
|
||||||
|
- Bundled Claude skills pruned to clide-specific ones (T-507): broken/duplicate
|
||||||
|
skills removed, repo-agnostic ones moved to user scope, and stale guidance in
|
||||||
|
the remaining five (paths, commands, icon API) brought back in line with the
|
||||||
|
repo.
|
||||||
|
- The `clide` skill description now leads with the hosting mechanics (`clide`
|
||||||
|
on PATH, `CLIDE_SOCK`) and `clide capabilities` as the entry action, so an
|
||||||
|
agent deciding whether to invoke it sees the first move up front (T-512).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Hash-keyed settings (per-workspace PATH presets, Claude account bindings)
|
||||||
|
with a number-shaped hash suffix were silently lost on restart — YAML map
|
||||||
|
keys are now quoted on write.
|
||||||
|
|
||||||
|
## [2.9.0] — 2026-06-30
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Graph cards.** `clide draw --file graph.json` (template `graph`) renders a
|
||||||
|
nodes/edges graph in the conversation as a circular layout — labelled nodes,
|
||||||
|
lines between them. Honest error on a duplicate id or an edge to an unknown
|
||||||
|
node. (T-321)
|
||||||
|
- **Piped `--stdin` payloads.** `cat icons.json | clide icon show --stdin` (and
|
||||||
|
`image show`) accept a JSON payload on stdin — the ergonomic peer of `--file`
|
||||||
|
for structured commands. (T-315)
|
||||||
|
- **Before/after compare cards.** `clide draw --file compare.json` (template
|
||||||
|
`compare`) renders two or more images side by side, each with its own
|
||||||
|
label/description and tap-to-zoom; images aspect-fit so differing shapes
|
||||||
|
don't distort. (T-319)
|
||||||
|
- **Phosphor glyph cards.** `clide icon show gear folder` (or `--file` entries
|
||||||
|
with label/description/color) renders glyphs in the conversation at a hero
|
||||||
|
size plus a real-UI-size strip (10–48), for previewing and comparing icons.
|
||||||
|
Resolves by name or 0xNNNN codepoint; honest error on an unknown glyph. (T-313)
|
||||||
|
- **Annotated image cards.** `clide image show --file meta.json` attaches a
|
||||||
|
title/label and a longer description to an image card (alongside the existing
|
||||||
|
one-line caption); the bare `image show <path> --caption` form is unchanged.
|
||||||
|
Visual marker overlays remain a follow-up. (T-316)
|
||||||
|
- **D2 diagram cards.** `clide draw --file diagram.d2` compiles a d2 diagram to
|
||||||
|
SVG (via the `d2` binary) and renders it in the conversation; `template:"d2"`
|
||||||
|
with inline source works too, and a `.svg` file renders directly. Honest error
|
||||||
|
if d2 isn't installed or the source doesn't compile. (T-494, D-103)
|
||||||
|
- **Tool path resolution + settings.** clide resolves supporter binaries (claude,
|
||||||
|
d2) via an explicit per-tool path, else PATH and the common install dirs — now
|
||||||
|
including Homebrew-on-Linux. A Tools settings category edits the paths and
|
||||||
|
re-detects; a broken path shows in Problems. (T-495, D-104)
|
||||||
|
- **Check for updates (About box).** Help → About has a manual "Check for
|
||||||
|
updates" button that compares your version to the latest GitHub release and
|
||||||
|
links to the notes — explicit and user-initiated, no background polling (the
|
||||||
|
first and only outbound call clide makes, on your action). (T-47)
|
||||||
|
- **Live-sync markdown read-mirror.** Editing a `.md` in the editor auto-opens a
|
||||||
|
read-only preview in the context panel that mirrors the buffer and re-renders
|
||||||
|
as you type; non-renderable files get no auto-viewer. (T-36, D-50)
|
||||||
|
- **New project flow.** A "New project…" action in the welcome view creates +
|
||||||
|
opens a project, then prompts for the Claude account to bind it to — the
|
||||||
|
per-repo account roadblock fires only for freshly-created projects. (T-488,
|
||||||
|
story T-486)
|
||||||
|
- **Initialize a non-repo folder.** Opening a folder that isn't a git repo now
|
||||||
|
offers to initialize it as a clide project (`git init` + scaffold + the
|
||||||
|
account roadblock) instead of dead-ending. Also `clide project init`. (T-489)
|
||||||
|
- **`clide project new <name> [--dir <parent>]`.** Create a new clide project —
|
||||||
|
a fresh dir, `git init`, and a minimal scaffold. `--dir` defaults to the
|
||||||
|
current workspace's parent. (T-487, story T-486)
|
||||||
|
- **Claude account login pane.** `account login` (and the UI add/re-login
|
||||||
|
affordances) open a modal terminal running `CLAUDE_CONFIG_DIR=<dir> claude
|
||||||
|
login`; the CLI owns the OAuth flow, credentials land in that account's dir.
|
||||||
|
(T-485, epic T-476)
|
||||||
|
- **Claude pane account badge.** The pane header shows which account this repo
|
||||||
|
is bound to (colour-tinted per account); tap to switch. Hidden when no
|
||||||
|
accounts are registered. (T-481, epic T-476)
|
||||||
|
- **Settings → Claude → Accounts.** A registry list (sign-in status, dir,
|
||||||
|
re-login / remove, add) plus a per-workspace picker that binds this repo to an
|
||||||
|
account (or Default); switching respawns the pane onto it. (T-482, epic T-476)
|
||||||
|
- **Per-repo Claude accounts — `clide claude account` verbs.** Manage named
|
||||||
|
Claude config dirs and bind one per workspace: `add`/`list`/`set`/`unset`/
|
||||||
|
`remove [--purge]`. `set`/`unset` respawn the workspace's Claude pane(s) onto
|
||||||
|
the bound account (resuming the conversation under its `CLAUDE_CONFIG_DIR`).
|
||||||
|
(T-480, epic T-476)
|
||||||
|
- **`clide instances` / `clide instance` CLI verbs.** `instances` lists every
|
||||||
|
live clide on the machine with its version, pid, workspace, and socket path
|
||||||
|
(probing the runtime dir); `instance` reports the one you're connected to — so
|
||||||
|
you can find and target a specific instance via `CLIDE_SOCK`. (T-247)
|
||||||
|
- **Summer Night theme + high-contrast sibling.** The legacy v1.2 palette is
|
||||||
|
fleshed out to full token + syntax coverage and ships alongside a
|
||||||
|
contrast-hardened `summer-night-hc`, both selectable in Settings → Appearance.
|
||||||
|
(T-478)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Fresh Claude sessions are nudged to use the bundled skills.** New tabs and
|
||||||
|
post-`/clear` respawns get a one-line prompt to load the `pql` + `clide`
|
||||||
|
skills from the first turn; resumed/forked sessions are left alone. (T-490)
|
||||||
|
- **Claude meta sidebar facelift.** The Activity, Team, and Config tabs render
|
||||||
|
their sections as elevated cards with small-caps headers, matching the
|
||||||
|
settings overlay's card design. (T-158)
|
||||||
|
- **Bundled themes and Tier-0 i18n namespaces resolve from one canonical list
|
||||||
|
each.** The app, testmode harness, and contrast/i18n gates iterate the shared
|
||||||
|
lists instead of drifting copies; a new theme or catalog is validated
|
||||||
|
automatically, and the i18n gate now checks every catalog for en/nl parity.
|
||||||
|
(T-371)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **No more i18n "missing key" log spam for tool names.** Proper-name tools
|
||||||
|
(Bash, ScheduleWakeup, MCP tools, …) intentionally have no catalog entry and
|
||||||
|
fall back to the raw name; the conversation pane no longer logs a warning for
|
||||||
|
each. (T-493)
|
||||||
|
- **`clide` CLI honors `CLIDE_SOCK`.** The shell client now connects to the
|
||||||
|
socket named by `CLIDE_SOCK` when set — an explicit target (e.g. a spawned
|
||||||
|
agent pinning its parent instance) that beats workspace auto-discovery — and
|
||||||
|
fails loudly if that socket is dead instead of silently driving a different
|
||||||
|
instance. (T-247)
|
||||||
|
- **Orphaned IPC sockets are swept on startup.** The app probes the runtime
|
||||||
|
socket dir on launch and unlinks dead `*.sock` nodes left by crashed
|
||||||
|
instances (live instances are left untouched), so the dir no longer
|
||||||
|
accumulates stale sockets. (T-247)
|
||||||
|
- **Default window opens larger (1600×900) on Linux.** 720p was short enough
|
||||||
|
that the welcome screen's version/theme footer overlapped the tips card;
|
||||||
|
the taller default clears it, matching the macOS default. (T-477)
|
||||||
|
- **Editor split collapses when the last buffer closes.** Closing the final
|
||||||
|
editor buffer left the top split orphaned over the Claude pane; the daemon
|
||||||
|
now emits the buffer-cleared event it was suppressing, so the split drops
|
||||||
|
out and the primary pane fills the column. (T-459)
|
||||||
|
|
||||||
|
## [2.8.0] — 2026-06-19
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Language selector + Dutch (nl-NL).** Settings → Appearance → Language
|
||||||
|
switches the UI language live (persisted as `app.locale`); a full Dutch
|
||||||
|
translation ships. English (en-US) stays the default. (T-462)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **App is fully localizable (i18n everywhere).** Every user-facing label —
|
||||||
|
panels, dialogs, command palette, menus, and settings — now resolves through
|
||||||
|
the i18n catalog instead of a hardcoded string; catalogs are bundled per
|
||||||
|
locale under `assets/i18n/<locale>/`. A new language is a drop-in folder;
|
||||||
|
en_US behaviour is unchanged. (T-462)
|
||||||
|
|
||||||
|
## [2.8.1] — 2026-06-22
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Localized the remaining Claude-pane strings.** Running-indicator verbs, the
|
||||||
|
primary/secondary session titles and banner role, tool-card titles, the
|
||||||
|
step/edit/agent counters, and the folded-activity ticker now resolve through
|
||||||
|
the catalog — completing i18n coverage of the conversation surface. (T-462)
|
||||||
|
|
||||||
|
## [2.7.1] — 2026-06-18
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Josefin Sans is the default UI font again.** Reverts the Inter default from
|
||||||
|
2.7.0; Inter stays bundled and selectable in Settings → Appearance. JetBrains
|
||||||
|
Mono remains the default monospace face (Fira Mono selectable). (T-460)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Markdown prose honours the UI-font setting.** Claude's conversation prose
|
||||||
|
and inline links render through the markdown engine, which pinned the bundled
|
||||||
|
UI face and ignored the Appearance UI-font pick; it now follows the setting
|
||||||
|
live, like the rest of the app. (T-475)
|
||||||
|
|
||||||
|
## [2.7.0] — 2026-06-17
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Settings panel shell.** A new `settings.open` command (⌘`,`, plus a File
|
||||||
|
menu and command-palette entry) opens a centered Settings modal over the
|
||||||
|
dimmed app — the foundation of the schema-driven settings UI. It frames the
|
||||||
|
category rail and scrolling carded panel that later work fills in; closes on
|
||||||
|
✕, Esc, or a barrier tap. (T-445)
|
||||||
|
|
||||||
|
- **Schema-driven settings engine.** Subsystems register a `SettingsCategory`
|
||||||
|
(via `SettingsCategoryContribution` → the kernel `SettingsRegistry`); the
|
||||||
|
panel renders it as carded sections of toggle/select/text/number/file rows,
|
||||||
|
each bound to a `SettingsStore` key with help and reset-to-default.
|
||||||
|
Registering a category surfaces a new tab. (T-448)
|
||||||
|
|
||||||
|
- **Settings category rail.** The modal's left rail lists the registered
|
||||||
|
categories (icon + title, data-driven from the registry) with an accent
|
||||||
|
left-stripe + surfaceHi selection; picking one swaps the panel. (T-447)
|
||||||
|
|
||||||
|
- **Per-field scope tags.** Each field shows where its value lives — folder =
|
||||||
|
Project (`.clide`), globe = Always (`~/.clide`), circle-dashed =
|
||||||
|
Default/unset — with a tooltip and a menu to move the value between scopes or
|
||||||
|
reset it. Backed by scope-explicit `SettingsStore` access. (T-449)
|
||||||
|
|
||||||
|
- **Cross-category settings search.** A search box atop the rail filters fields
|
||||||
|
across every category; the panel shows the matches grouped under category
|
||||||
|
subheaders (editable inline), and each rail row shows its match count with
|
||||||
|
zero-match categories dimmed. (T-450)
|
||||||
|
|
||||||
|
- **Settings → Activity category.** The first real settings tab: the
|
||||||
|
conversation fold level (none / tools / thinking / everything) as a schema
|
||||||
|
field; picking a level applies live to the activity stream. (T-453)
|
||||||
|
|
||||||
|
- **Settings → Keymap category.** A preset select (Default / Vim / VS Code /
|
||||||
|
JetBrains); picking one switches the active keymap live via the preset
|
||||||
|
command. (T-451)
|
||||||
|
|
||||||
|
- **Settings → Appearance category.** A theme picker in the panel — base-theme
|
||||||
|
chips + a high-contrast toggle, applied live. Adds the engine's custom-control
|
||||||
|
escape hatch (`SettingsControlContribution` / `SettingsControlRegistry`) for
|
||||||
|
one-off controls the generic field kinds can't express. (T-452)
|
||||||
|
|
||||||
|
- **Settings → Extensions tab.** A "watch this space" notice — installing and
|
||||||
|
toggling extensions arrives with third-party (Lua) support; built-ins stay
|
||||||
|
always-on for now. (T-456)
|
||||||
|
|
||||||
|
- **Settings → Claude category.** New-session defaults — model, effort, and
|
||||||
|
permission mode — seed fresh sessions (effort via `--effort` at spawn;
|
||||||
|
model and permission applied right after start). (T-457)
|
||||||
|
|
||||||
|
- **Inter is the default UI font + a UI-font picker.** Bundled Inter (variable)
|
||||||
|
as the default interface typeface — Josefin Sans stays selectable — and
|
||||||
|
Settings → Appearance gains a UI-font picker that applies live. (T-460)
|
||||||
|
|
||||||
|
- **Monospace font picker.** Settings → Appearance adds a monospace font select
|
||||||
|
(JetBrains Mono / Fira Mono) that applies live to the terminal, diffs, code,
|
||||||
|
and IDs. Bundles Fira Mono. (T-471)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Theme picker relabelled "Theme…".** The ⌘K theme picker's command title
|
||||||
|
changed from "Settings…" to "Theme…" so it no longer collides with the new
|
||||||
|
Settings panel in the palette; behaviour is unchanged. (T-445)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Monospace setting now applies everywhere.** Eleven context-free render
|
||||||
|
helpers (Claude tool bodies + results, inline markdown code/refs, search
|
||||||
|
previews, welcome tips) hard-coded JetBrains Mono and ignored the Monospace
|
||||||
|
font setting; they now honour it live. (T-472)
|
||||||
|
|
||||||
|
## [2.6.0] — 2026-06-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Vim `gt` / `gT` tab motions.** Under the Vim preset, `gt`/`gT` cycle the
|
||||||
|
workspace tab strip (the same `workspace.tab.*` commands as `ctrl+pagedown`/
|
||||||
|
`ctrl+pageup`), resolved by the focused editor or pane and sharing the `g`
|
||||||
|
prefix with `gg`. Completes the T-403 cross-pane vim layer. (T-405)
|
||||||
|
|
||||||
|
- **Vim ex command-line (`:`).** Under the Vim preset, `:` opens a transient
|
||||||
|
one-line overlay running a fixed table — `:w` save, `:q` close the active tab
|
||||||
|
(the split self-collapses on the last one), `:wq`/`:x` and `ZZ` save+close,
|
||||||
|
`:e <path>` jump to quick-open seeded with the path, `:<n>` goto-line. Unknown
|
||||||
|
commands flash and stay open; with no active buffer it no-ops. Opens from the
|
||||||
|
editor or a focused pane; Esc dismisses. Adds the `editor.goto-line` CLI/IPC
|
||||||
|
verb. (T-407)
|
||||||
|
|
||||||
|
- **Crash-survivable logging.** clide writes a durable JSON-lines log to a
|
||||||
|
persistent per-platform dir (Windows `%LOCALAPPDATA%`, macOS `~/Library/Logs`,
|
||||||
|
Linux `$XDG_STATE_HOME`), fsyncing warn/error + pty/ffi records immediately so
|
||||||
|
a freeze leaves on-disk evidence. `CLIDE_LOG` (dart-define / env) or the
|
||||||
|
`app.log.level` setting sets verbosity (warn in release, info in debug);
|
||||||
|
`CLIDE_LOG_DIR` redirects where the logs land. (T-432, T-436)
|
||||||
|
- **PTY FFI breadcrumbs.** Each PTY backend drops a breadcrumb before/after
|
||||||
|
every risky syscall (`CreatePseudoConsole`/`CreateProcessW`/`ReadFile`,
|
||||||
|
`posix_spawn`/`read`); the reader/waiter isolates fsync their OWN file handle
|
||||||
|
so a wedged isolate's last crumb survives a freeze that also froze the main
|
||||||
|
isolate — naming the wedge after the fact. Per-syscall crumbs at debug level.
|
||||||
|
(T-434)
|
||||||
|
- **Crash-diagnostic watchdog.** A dedicated isolate fsyncs a heartbeat every
|
||||||
|
~500ms (bounding a freeze to ~500ms) and every ~2s samples this process's
|
||||||
|
thread / handle / child-host / RSS counts to `clide-watchdog.log` — a climbing
|
||||||
|
child or thread count is the leak signature. Survives a frozen main isolate;
|
||||||
|
spawn failure is non-fatal. (T-435)
|
||||||
|
- **Live log-verbosity toggle.** The output dock's Level chip now sets the
|
||||||
|
running logger's level and persists `app.log.level` (not just a view filter),
|
||||||
|
and `clide log level [<level>]` does the same from the CLI — D-6 parity. The
|
||||||
|
choice survives restart. (T-433)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **`/clear` no longer kills the Claude pane.** Clearing the primary pane tore
|
||||||
|
the session down and respawned on the same deterministic `--session-id`
|
||||||
|
*before the old `claude` process had actually exited*, so claude 2.1.177
|
||||||
|
rejected the id as "already in use" and the respawn exited 1. The session
|
||||||
|
teardown now awaits the process's real death (SIGTERM, escalating to SIGKILL)
|
||||||
|
before clearing the transcript and respawning. A dead pane also now shows the
|
||||||
|
CLI's own reason instead of a bare "exited (code 1)". (T-437)
|
||||||
|
- **Web/WASM build compiles again.** `flutter build web --wasm` (and the
|
||||||
|
`test-e2e` / `ui-dev` / `ui-smoke` harness) had been broken since the
|
||||||
|
tree-sitter/PTY `dart:ffi` pivot. Every native binding (PTY, tree-sitter, the
|
||||||
|
Lua host, the Windows watchdog, the ABI/fd-inheritance probes) now sits behind
|
||||||
|
a `dart.library.ffi` conditional import with a graceful web stub, and the FNV
|
||||||
|
hash constants are dart2js-safe. A `flutter build web --wasm` compile gate was
|
||||||
|
added to CI so the fence can't silently rot. Desktop builds are unchanged — the
|
||||||
|
web target degrades (no terminal, no native git, no syntax highlighting), it
|
||||||
|
does not compromise desktop fidelity. (T-438, D-100, Q-50)
|
||||||
|
- **Desktop-launched clide finds your installed tools.** A dock/launcher start
|
||||||
|
inherits a sparse `PATH` (no `~/.local/bin`, brew, nvm, …), so pql/git/claude
|
||||||
|
and terminal tools could go missing. clide now resolves the real login-shell
|
||||||
|
`PATH` once at startup (`$SHELL -l -c`, bounded + graceful fallback) and routes
|
||||||
|
every spawn site — PTY children, git, the toolchain probe, hosted claude —
|
||||||
|
through one shared resolver, replacing three divergent (and partly macOS-only)
|
||||||
|
PATH expanders. (T-439, follows T-347)
|
||||||
|
|
||||||
|
## [2.5.0] — 2026-06-14
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Experimental Windows desktop support.** clide builds and runs on Windows —
|
||||||
|
ConPTY-backed terminals, an AF_UNIX `clide` CLI client, PowerShell as the
|
||||||
|
default shell, and a `make build-windows` target. Preview quality: ConPTY
|
||||||
|
child-process reaping under sustained use is still being hardened. (T-424)
|
||||||
|
- **Vim `ctrl+w` window commands.** Under the vim preset, `ctrl+w` followed by
|
||||||
|
h/l (focus left/right panel), j (toggle dock), w / ctrl+w (cycle panels),
|
||||||
|
shift+w (cycle back), o (focus mode), or q/c (close editor). A new global
|
||||||
|
multi-chord matcher in the shell resolves these from any focus; bare `ctrl+w`
|
||||||
|
still closes the editor after the ambiguity timeout. (T-404)
|
||||||
|
- **Workspace tab cycling with ctrl+pagedown / ctrl+pageup.** New
|
||||||
|
`workspace.tab.next` / `workspace.tab.previous` commands cycle the workspace
|
||||||
|
tab strip with wraparound, bound across every preset. (T-405)
|
||||||
|
- **Vim normal-mode navigation works outside the editor.** Under the vim preset,
|
||||||
|
a focused file tree or conversation now responds to j/k, ctrl+d/ctrl+u, gg/G,
|
||||||
|
and (tree) h/l/o — a selection cursor in the tree, scrolling in the
|
||||||
|
conversation. Each pane runs its own sequence matcher; an `editor.focused`
|
||||||
|
flag keeps these keys as buffer motions while the editor holds focus. (T-406)
|
||||||
|
- **Claude Code Workflow runs surface in the conversation and sidebar.** A
|
||||||
|
`Workflow` tool-use renders a dedicated run card — phase groups, per-agent
|
||||||
|
rows with live spinner/check status, usage, and the script — driven by the
|
||||||
|
harness's out-of-band progress events. The Activity tab adds a WORKFLOWS
|
||||||
|
section showing each run's done/total agent count. (T-416)
|
||||||
|
- **Session controls and live usage in the Claude sidebar Activity tab.** A
|
||||||
|
SESSION strip offers clear/compact/fork/resume buttons (same code path as the
|
||||||
|
typed commands), and a refresh control fetches `/usage` — plan usage renders
|
||||||
|
as a USAGE block (session and weekly percentages). The runtime row now also
|
||||||
|
shows the session's effort level. (T-415)
|
||||||
|
- **The Claude sidebar Config tab is a live control panel.** Model, effort, and
|
||||||
|
permission mode are popover controls showing the running session's values;
|
||||||
|
picking an option drives the session through the same path as the typed slash
|
||||||
|
command. The sidebar tables also got a visual pass — larger type, accent
|
||||||
|
section headers, more breathing room. (T-414)
|
||||||
|
- **The TUI command family opens clide surfaces.** `/permissions` sets the mode
|
||||||
|
directly or opens a picker; `/status`, `/config`, `/mcp`, `/agents`, `/hooks`
|
||||||
|
jump to the matching Claude sidebar tab; `/memory` opens CLAUDE.md in the
|
||||||
|
editor; `/help` shows clide's own command summary. (T-413)
|
||||||
|
- **`/effort` works in the Claude pane.** With a level (`/effort xhigh`) the
|
||||||
|
session restarts in place carrying `--effort` — resume keeps the
|
||||||
|
conversation; bare `/effort` opens a picker with the five levels and the
|
||||||
|
current one marked. The active effort shows in the session status. (T-412)
|
||||||
|
- **TUI-only slash commands get a helpful notice instead of failing.** A typed
|
||||||
|
`/cost` or `/doctor` no longer errors raw from the CLI or leaks to the model
|
||||||
|
as literal text — known TUI-only commands route to a muted notice card with
|
||||||
|
the clide-native way. CLI-local output (like `/usage`) renders as a "clide"
|
||||||
|
card, never fake Claude prose. (T-411)
|
||||||
|
- **`/model` works in the Claude pane.** With a name (`/model sonnet`) it
|
||||||
|
switches the live session's model over the control channel; bare `/model`
|
||||||
|
opens a picker in the interaction zone with the CLI's model list and the
|
||||||
|
current model marked. A rejected name rolls back and raises a toast. (T-408)
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- **tmux is no longer a required tool.** clide stopped spawning tmux when Claude
|
||||||
|
session persistence moved to `--resume` (D-77); the toolchain no longer probes
|
||||||
|
for it or warns when it's absent, on any platform.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **`ClaudeConfig` no longer crashes on a project switch that races teardown.**
|
||||||
|
`setProjectDir` / `refresh` / `ensureProbe` now skip `notifyListeners()` if the
|
||||||
|
config was disposed during their async load (the guard `load()` already had).
|
||||||
|
|
||||||
## [2.4.1] — 2026-06-12
|
## [2.4.1] — 2026-06-12
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ 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). PTY spawning uses Dart FFI `posix_openpt()` + `posix_spawn()` directly.
|
- **`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.
|
||||||
|
|
||||||
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.
|
Claude session persistence is `--resume <session-id>` against Claude Code's transcript files (D-77, superseding the original tmux-backed D-41) — the app re-attaches on restart, no tmux required. 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: [`governance/`](governance/) (`D-NNN` confirmed, `Q-NNN` open, `R-NNN` rejected — see [`governance/README.md`](governance/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/).
|
||||||
|
|
||||||
@@ -89,6 +89,8 @@ Shell hygiene (keeps commands inside the permission allowlist, so they don't get
|
|||||||
|
|
||||||
Commit and push directly to `main` for routine work — this is a solo-dev repo and does not use a branch-first / feature-branch flow. Do **not** create a working branch just to land a change. (This overrides the generic "branch before committing on the default branch" assistant default.) The usual safety rules still hold: never `--no-verify`, never force-push `main`, and let the pre-push gate run.
|
Commit and push directly to `main` for routine work — this is a solo-dev repo and does not use a branch-first / feature-branch flow. Do **not** create a working branch just to land a change. (This overrides the generic "branch before committing on the default branch" assistant default.) The usual safety rules still hold: never `--no-verify`, never force-push `main`, and let the pre-push gate run.
|
||||||
|
|
||||||
|
**Never `git add -A` or `git add .` — stage explicit paths every time (`git add <file> …`), no exceptions.** This worktree can host concurrent Claude sessions: a blanket add vacuums another session's in-progress files — and your own unrelated edits — into your commit, mislabeling work and entangling history (this has happened). If `git status` shows files you didn't touch this turn, they are not yours to stage. **Always create commits through the [`git-commit` skill](.claude/skills/git-commit/SKILL.md)** — it encodes the message format (Conventional Commits, per [D-37](governance/decisions/process.md#d-37)), the explicit-staging rule, changelog discipline, and the safety reminders. Don't hand-roll a commit that skips it.
|
||||||
|
|
||||||
The pre-commit hook auto-exports and stages `.pql/changelog/` (the pql ticket DB) on every commit — don't hand-stage it. A ticket change only persists if the turn makes at least one commit; with no commit the hook never fires and a later branch switch can drop it.
|
The pre-commit hook auto-exports and stages `.pql/changelog/` (the pql ticket DB) on every commit — don't hand-stage it. A ticket change only persists if the turn makes at least one commit; with no commit the hook never fires and a later branch switch can drop it.
|
||||||
|
|
||||||
## Changelog discipline
|
## Changelog discipline
|
||||||
|
|||||||
+8
-4
@@ -181,14 +181,18 @@ tickets before the diff lands. Trivial typo fixes don't need one.
|
|||||||
See [D-37](governance/decisions/process.md#d-37) and the bundled
|
See [D-37](governance/decisions/process.md#d-37) and the bundled
|
||||||
[`git-commit` skill](.claude/skills/git-commit/SKILL.md). In short:
|
[`git-commit` skill](.claude/skills/git-commit/SKILL.md). In short:
|
||||||
|
|
||||||
- Imperative subject ≤ 70 chars, no Conventional Commits prefix
|
- [Conventional Commits 1.0](https://www.conventionalcommits.org/en/v1.0.0/):
|
||||||
(this isn't a Conventional Commits repo — the archived Python
|
`type(scope): imperative subject`, ≤ 72 chars including the prefix.
|
||||||
predecessor under [`legacy/`](legacy/) is, but the rebuild isn't).
|
Types are `feat`, `fix`, `docs`, `style`, `refactor`, `perf`,
|
||||||
|
`test`, `build`, `chore`; scope is the subsystem (`settings`,
|
||||||
|
`vim`, `pty`…); keep a trailing `(T-NNN)` ticket ref where one
|
||||||
|
applies — e.g. `feat(settings): category rail + navigation (T-447)`.
|
||||||
- One logical change per commit. If the subject needs "and", split it.
|
- One logical change per commit. If the subject needs "and", split it.
|
||||||
- Every user-visible commit adds an entry to `CHANGELOG.md` under
|
- Every user-visible commit adds an entry to `CHANGELOG.md` under
|
||||||
`[Unreleased]` in the right subsection (Added, Changed, Deprecated,
|
`[Unreleased]` in the right subsection (Added, Changed, Deprecated,
|
||||||
Removed, Fixed, Security). Keep entries to one or two short
|
Removed, Fixed, Security). Keep entries to one or two short
|
||||||
sentences — the 60-word cap is enforced by `ci/changelog_gate.sh`.
|
sentences — the 60-word cap is enforced by the pre-push gate
|
||||||
|
(`make changelog-gate`).
|
||||||
- Co-author trailer:
|
- Co-author trailer:
|
||||||
`Co-Authored-By: Claude <noreply@anthropic.com>` when Claude wrote
|
`Co-Authored-By: Claude <noreply@anthropic.com>` when Claude wrote
|
||||||
any of the diff.
|
any of the diff.
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ changelog-gate: ## Changelog concision gate — fails on `## [Unreleased]` bulle
|
|||||||
ci/changelog_gate.sh
|
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: gen-build-info ## Build Linux release bundle and run it under xvfb for 5s.
|
||||||
ci/smoke_bundle.sh
|
ci/smoke_bundle.sh
|
||||||
|
|
||||||
# -- web UI harness ------------------------------------------------------
|
# -- web UI harness ------------------------------------------------------
|
||||||
@@ -169,8 +169,7 @@ ui-smoke: ## Build + serve + run Playwright smoke + stop.
|
|||||||
@sh -c 'trap "tools/ui/stop.sh >/dev/null 2>&1" EXIT; cd tools/ui && npx playwright test smoke.spec.ts'
|
@sh -c 'trap "tools/ui/stop.sh >/dev/null 2>&1" EXIT; cd tools/ui && npx playwright test smoke.spec.ts'
|
||||||
|
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
build: gen-build-info clide-cli ## flutter build for the current OS (incl. the C CLI client).
|
build: clide-cli build-$(FLUTTER_OS) ## flutter build for the current OS (via build-<os>) + bundle the C CLI client.
|
||||||
flutter build $(FLUTTER_OS)
|
|
||||||
@install -m 755 $(CLIDE_CLI_BIN) $(CLI_BUNDLE_DEST)
|
@install -m 755 $(CLIDE_CLI_BIN) $(CLI_BUNDLE_DEST)
|
||||||
@echo "==> bundled C client at $(CLI_BUNDLE_DEST)"
|
@echo "==> bundled C client at $(CLI_BUNDLE_DEST)"
|
||||||
|
|
||||||
@@ -182,6 +181,14 @@ build-linux: gen-build-info ## flutter build linux (desktop bundle).
|
|||||||
build-macos: gen-build-info ## flutter build macos (desktop bundle).
|
build-macos: gen-build-info ## flutter build macos (desktop bundle).
|
||||||
flutter build macos
|
flutter build macos
|
||||||
|
|
||||||
|
.PHONY: build-windows
|
||||||
|
build-windows: gen-build-info ## flutter build windows (desktop bundle).
|
||||||
|
flutter build windows
|
||||||
|
|
||||||
|
.PHONY: release
|
||||||
|
release: ## Finalize a release: verify version/changelog/tree, run the gate, tag vX.Y.Z. Run after the `release vX.Y.Z` commit.
|
||||||
|
ci/release.sh
|
||||||
|
|
||||||
# -- install / uninstall -----------------------------------------------------
|
# -- install / uninstall -----------------------------------------------------
|
||||||
|
|
||||||
# Install prefix. Bundle lands at $(INSTALL_PREFIX)/clide/ with a
|
# Install prefix. Bundle lands at $(INSTALL_PREFIX)/clide/ with a
|
||||||
@@ -196,6 +203,9 @@ ifeq ($(FLUTTER_OS),linux)
|
|||||||
else ifeq ($(FLUTTER_OS),macos)
|
else ifeq ($(FLUTTER_OS),macos)
|
||||||
BUNDLE_DIR := build/macos/Build/Products/Release/clide.app
|
BUNDLE_DIR := build/macos/Build/Products/Release/clide.app
|
||||||
CLI_BUNDLE_DEST := $(BUNDLE_DIR)/Contents/MacOS/clide-cli
|
CLI_BUNDLE_DEST := $(BUNDLE_DIR)/Contents/MacOS/clide-cli
|
||||||
|
else ifeq ($(FLUTTER_OS),windows)
|
||||||
|
BUNDLE_DIR := build/windows/x64/runner/Release
|
||||||
|
CLI_BUNDLE_DEST := $(BUNDLE_DIR)/clide-cli.exe
|
||||||
endif
|
endif
|
||||||
|
|
||||||
ICON_SIZES := 16 32 48 128 192 256 512
|
ICON_SIZES := 16 32 48 128 192 256 512
|
||||||
@@ -254,6 +264,8 @@ else ifeq ($(FLUTTER_OS),macos)
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
# -- dugite-native (bundled git) ------------------------------------------
|
# -- dugite-native (bundled git) ------------------------------------------
|
||||||
|
# Security tracking (D-59): run `make dugite-check` quarterly, or on a git CVE,
|
||||||
|
# to compare this pin against the latest upstream release. T-88 is the calendar.
|
||||||
|
|
||||||
DUGITE_VERSION := v2.53.0-3
|
DUGITE_VERSION := v2.53.0-3
|
||||||
DUGITE_COMMIT := f49d009
|
DUGITE_COMMIT := f49d009
|
||||||
@@ -291,7 +303,11 @@ dugite-clean: ## Remove the dugite-native directory.
|
|||||||
# target picks up whatever `cc` is on PATH.
|
# target picks up whatever `cc` is on PATH.
|
||||||
|
|
||||||
CLIDE_CLI_SRC := native/clide-cli/clide.c
|
CLIDE_CLI_SRC := native/clide-cli/clide.c
|
||||||
CLIDE_CLI_BIN := native/$(if $(filter Darwin,$(shell uname -s)),macos,linux)-$(shell uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/')/clide
|
ifeq ($(FLUTTER_OS),windows)
|
||||||
|
CLIDE_CLI_BIN := native/windows-x64/clide.exe
|
||||||
|
else
|
||||||
|
CLIDE_CLI_BIN := native/$(if $(filter Darwin,$(shell uname -s)),macos,linux)-$(shell uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/')/clide
|
||||||
|
endif
|
||||||
CC ?= cc
|
CC ?= cc
|
||||||
|
|
||||||
.PHONY: clide-cli
|
.PHONY: clide-cli
|
||||||
@@ -299,7 +315,11 @@ clide-cli: $(CLIDE_CLI_BIN) ## Compile the C `clide` shell client.
|
|||||||
|
|
||||||
$(CLIDE_CLI_BIN): $(CLIDE_CLI_SRC)
|
$(CLIDE_CLI_BIN): $(CLIDE_CLI_SRC)
|
||||||
@mkdir -p $(dir $(CLIDE_CLI_BIN))
|
@mkdir -p $(dir $(CLIDE_CLI_BIN))
|
||||||
|
ifeq ($(FLUTTER_OS),windows)
|
||||||
|
ci/build_cli_windows.sh
|
||||||
|
else
|
||||||
$(CC) -std=c99 -O2 -Wall -Wextra -o $(CLIDE_CLI_BIN) $(CLIDE_CLI_SRC)
|
$(CC) -std=c99 -O2 -Wall -Wextra -o $(CLIDE_CLI_BIN) $(CLIDE_CLI_SRC)
|
||||||
|
endif
|
||||||
@echo "==> built $(CLIDE_CLI_BIN)"
|
@echo "==> built $(CLIDE_CLI_BIN)"
|
||||||
|
|
||||||
.PHONY: clide-cli-clean
|
.PHONY: clide-cli-clean
|
||||||
@@ -312,6 +332,10 @@ clide-cli-clean: ## Remove the compiled C `clide` client.
|
|||||||
security: ## Supply-chain gate — osv-scanner over pubspec.lock (CI PR-merge pipeline; run locally on demand). Fails on a known advisory.
|
security: ## Supply-chain gate — osv-scanner over pubspec.lock (CI PR-merge pipeline; run locally on demand). Fails on a known advisory.
|
||||||
ci/osv_scan.sh
|
ci/osv_scan.sh
|
||||||
|
|
||||||
|
.PHONY: dugite-check
|
||||||
|
dugite-check: ## Track dugite-native (bundled git) upstream releases for security drift (T-88 / D-59). Run quarterly, or on a git CVE. Informational, not a gate.
|
||||||
|
ci/check_dugite_version.sh
|
||||||
|
|
||||||
# -- pre-push gate --------------------------------------------------------
|
# -- pre-push gate --------------------------------------------------------
|
||||||
|
|
||||||
.PHONY: decisions-validate
|
.PHONY: decisions-validate
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
This document governs what clide is allowed to do at runtime, what it's allowed to depend on, and how contributors — human and agent — introduce code into the project. It is binding on all contributors. When in doubt, stop and ask.
|
This document governs what clide is allowed to do at runtime, what it's allowed to depend on, and how contributors — human and agent — introduce code into the project. It is binding on all contributors. When in doubt, stop and ask.
|
||||||
|
|
||||||
Rationale for specific architectural choices referenced here and in code comments (the D-### markers) lives in `decisions/`. This document sets the rules; `decisions/` records why the rules produced the code they did in a given case. If the two ever disagree, the rule in this document wins until the document itself is changed.
|
Rationale for specific architectural choices referenced here and in code comments (the D-### markers) lives in `governance/decisions/`. This document sets the rules; `governance/decisions/` records why the rules produced the code they did in a given case. If the two ever disagree, the rule in this document wins until the document itself is changed.
|
||||||
|
|
||||||
## Why this document exists
|
## Why this document exists
|
||||||
|
|
||||||
@@ -124,9 +124,9 @@ When removing a dependency:
|
|||||||
|
|
||||||
1. **Grep the entire repository** for references to the package, its exports, and any type names it contributed. `rg '<package>|<PackageType>|<prefix_>'` across the repo. Zero hits outside git history is the goal. A single lingering import will break the build; a single lingering FFI stub or type alias will compile fine and fail at runtime.
|
1. **Grep the entire repository** for references to the package, its exports, and any type names it contributed. `rg '<package>|<PackageType>|<prefix_>'` across the repo. Zero hits outside git history is the goal. A single lingering import will break the build; a single lingering FFI stub or type alias will compile fine and fail at runtime.
|
||||||
2. **Regenerate the lockfile** as part of the same PR. A `pubspec.yaml` with the dep removed but a `pubspec.lock` that still pins it is a partial removal, and CI or a fresh clone will happily continue installing the package.
|
2. **Regenerate the lockfile** as part of the same PR. A `pubspec.yaml` with the dep removed but a `pubspec.lock` that still pins it is a partial removal, and CI or a fresh clone will happily continue installing the package.
|
||||||
3. **Update `app/assets/licenses.yaml`** to drop the removed package and any transitive deps it brought in that aren't pulled by anything else. If the license manifest is auto-generated on release, verify the generation script sees the change; if it's maintained by hand, edit it in the same PR.
|
3. **Update `assets/licenses.yaml`** to drop the removed package and any transitive deps it brought in that aren't pulled by anything else. If the license manifest is auto-generated on release, verify the generation script sees the change; if it's maintained by hand, edit it in the same PR.
|
||||||
4. **Remove any vendored artifacts** tied to the dep — binaries, prebuilt assets, generated bindings — and delete their `BUILD.md` records. An orphaned vendored binary is worse than a removed one because it looks legitimate.
|
4. **Remove any vendored artifacts** tied to the dep — binaries, prebuilt assets, generated bindings — and delete their `BUILD.md` records. An orphaned vendored binary is worse than a removed one because it looks legitimate.
|
||||||
5. **Check for architectural assumptions** that the dep was carrying. If the removed package was the thing that justified a specific data flow, build step, or platform strategy, either the replacement picks up those responsibilities or the architecture has actually changed and the relevant design decision (see `decisions/`) needs updating.
|
5. **Check for architectural assumptions** that the dep was carrying. If the removed package was the thing that justified a specific data flow, build step, or platform strategy, either the replacement picks up those responsibilities or the architecture has actually changed and the relevant design decision (see `governance/decisions/`) needs updating.
|
||||||
|
|
||||||
A dependency is not removed until all five are true. "I deleted the line from pubspec.yaml" is the start of the removal, not the end.
|
A dependency is not removed until all five are true. "I deleted the line from pubspec.yaml" is the start of the removal, not the end.
|
||||||
|
|
||||||
@@ -188,18 +188,18 @@ When in doubt about a license, the dependency does not land until the question i
|
|||||||
|
|
||||||
### Attribution requirements
|
### Attribution requirements
|
||||||
|
|
||||||
- The license manifest at `app/assets/licenses.yaml` lists every dependency with its license, copyright notice, and upstream URL.
|
- The license manifest at `assets/licenses.yaml` lists every dependency with its license, copyright notice, and upstream URL.
|
||||||
- Transitive dependencies are listed, not just direct ones. If `wasm_run` pulls in `wasmtime` which pulls in `cranelift`, all three appear.
|
- Transitive dependencies are listed, not just direct ones. If `wasm_run` pulls in `wasmtime` which pulls in `cranelift`, all three appear.
|
||||||
- Apache-2.0 dependencies get their `NOTICE` file content preserved verbatim, not summarized.
|
- Apache-2.0 dependencies get their `NOTICE` file content preserved verbatim, not summarized.
|
||||||
- Apache-2.0-with-LLVM-exception (e.g., Cranelift, parts of LLVM) requires the LLVM exception text specifically, not just the Apache-2.0 boilerplate.
|
- Apache-2.0-with-LLVM-exception (e.g., Cranelift, parts of LLVM) requires the LLVM exception text specifically, not just the Apache-2.0 boilerplate.
|
||||||
- Fonts and icon sets get attributed even if the license doesn't strictly require it. It's the right thing to do.
|
- Fonts and icon sets get attributed even if the license doesn't strictly require it. It's the right thing to do.
|
||||||
- `app/assets/licenses.yaml` is regenerated as part of the release build, not maintained by hand. A release that ships a stale manifest is a release defect.
|
- `assets/licenses.yaml` is regenerated as part of the release build, not maintained by hand. A release that ships a stale manifest is a release defect.
|
||||||
|
|
||||||
Adding a dependency means updating the license manifest in the same PR. No exceptions.
|
Adding a dependency means updating the license manifest in the same PR. No exceptions.
|
||||||
|
|
||||||
## Changelog and commit conventions
|
## Changelog and commit conventions
|
||||||
|
|
||||||
clide follows [Keep a Changelog 1.1](https://keepachangelog.com/en/1.1.0/) for `CHANGELOG.md` and [Conventional Commits 1.0](https://www.conventionalcommits.org/en/v1.0.0/) for commit messages. Enforcement is handled by the project's git skill; this section exists so human contributors know the standard before their first PR, and so the connection between these conventions and the rest of the policy is explicit.
|
clide follows [Keep a Changelog 1.1](https://keepachangelog.com/en/1.1.0/) for `CHANGELOG.md` and [Conventional Commits 1.0](https://www.conventionalcommits.org/en/v1.0.0/) for commit messages (see [D-37](governance/decisions/process.md#d-37)). Enforcement is handled by the project's git skill; this section exists so human contributors know the standard before their first PR, and so the connection between these conventions and the rest of the policy is explicit.
|
||||||
|
|
||||||
Security-relevant changes — CVE responses, dependency-driven vulnerability fixes, the removal of a phoning-home transitive dep, anything where the rules in this document were the reason for the change — go under the `Security` heading of the release's changelog entry, regardless of whether the code change itself looks security-shaped. That heading is the trail future-us follows to reconstruct why a dep was bumped or removed. Lumping security fixes under `Fixed` because the diff looks like a normal bug fix loses that signal and is the wrong choice even when it's technically accurate.
|
Security-relevant changes — CVE responses, dependency-driven vulnerability fixes, the removal of a phoning-home transitive dep, anything where the rules in this document were the reason for the change — go under the `Security` heading of the release's changelog entry, regardless of whether the code change itself looks security-shaped. That heading is the trail future-us follows to reconstruct why a dep was bumped or removed. Lumping security fixes under `Fixed` because the diff looks like a normal bug fix loses that signal and is the wrong choice even when it's technically accurate.
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# clide
|
# clide
|
||||||
|
|
||||||
An IDE for Claude Code CLI. Native rendering, terminal-first interaction, pql-powered queries, canvas and graph surfaces. Linux and macOS.
|
An IDE for Claude Code CLI. Native rendering, terminal-first interaction, pql-powered queries, canvas and graph surfaces. Linux, macOS and Windows.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
Single Flutter package at the repo root. The app hosts everything in-process: IPC server, subsystem handlers (pane, files, editor, git, pql), and the extension framework. tmux owns Claude session persistence (D-41).
|
Single Flutter package at the repo root. The app hosts everything in-process: IPC server, subsystem handlers (pane, files, editor, git, pql), and the extension framework. Claude session persistence is `--resume <session-id>` against Claude Code's transcript files (D-77, superseding the tmux-backed D-41).
|
||||||
|
|
||||||
- **`lib/`** — all Dart code. Core subsystems (`lib/src/`), kernel services (`lib/kernel/`), UI widgets (`lib/widgets/`), built-in extensions (`lib/builtin/`), the extension framework (`lib/extension/`).
|
- **`lib/`** — all Dart code. Core subsystems (`lib/src/`), kernel services (`lib/kernel/`), UI widgets (`lib/widgets/`), built-in extensions (`lib/builtin/`), the extension framework (`lib/extension/`).
|
||||||
- **PTY** — `lib/src/pty/` spawns child processes via Dart FFI `posix_openpt()` + `posix_spawn()` directly; no external helper binary.
|
- **PTY** — `lib/src/pty/` spawns child processes via Dart FFI `posix_openpt()` + `posix_spawn()` directly; no external helper binary.
|
||||||
@@ -15,7 +15,7 @@ Claude drives the UI through a `clide` CLI surface (Bash, not MCP). Every CLI su
|
|||||||
|
|
||||||
## Built-in extensions
|
## Built-in extensions
|
||||||
|
|
||||||
canvas, claude, claude_control, decisions, diff, editor, extensions_ui, files, git, graph, grammars_core, ipc_status, keybindings_ui, markdown, pql, problems, settings_ui, terminal, theme_picker, tickets, todos, welcome.
|
canvas, claude, claude_control, cli_install, decisions, deeplink, default_layout, diff, editor, extensions_ui, files, git, grammars_core, graph, ipc_status, keybindings_ui, markdown, menubar, output, pql, problems, search, settings_ui, terminal, theme_picker, tickets, todos, view, vim, welcome.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ make push-check # pre-push gate: decisions + core + fast + a11y + coverage
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Pre-v2.0 (`2.0.0-dev`). Interaction model and panel system landed. The Python Textual v1.2.0 predecessor is archived under [`legacy/`](https://github.com/postmeridiem/clide/tree/main/legacy).
|
Active development; the interaction model, panel system, and settings engine have landed. The Python Textual predecessor is archived under [`legacy/`](https://github.com/postmeridiem/clide/tree/main/legacy).
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright (c) 2012-2013, The Mozilla Corporation and Telefonica S.A.
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"tab.title": { "translation": "Canvas" },
|
||||||
|
"empty": { "translation": "Open a .canvas file to view it here." },
|
||||||
|
"status.loading": { "translation": "Loading…" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
{
|
||||||
|
"tab.title": { "translation": "Claude" },
|
||||||
|
"status.attaching": { "translation": "attaching…" },
|
||||||
|
"status.no-tmux": { "translation": "no-tmux · fresh every launch" },
|
||||||
|
"status.exited": { "translation": "session exited" },
|
||||||
|
"status.primary-exited": { "translation": "session exited — restart clide to retry" },
|
||||||
|
"banner.title": { "translation": "Claude" },
|
||||||
|
"banner.warmingUp": { "translation": "Warming up — your conversation will appear here." },
|
||||||
|
"banner.role.primary": { "translation": "primary" },
|
||||||
|
"banner.role.secondary": { "translation": "session {index}" },
|
||||||
|
"composer.hint": { "translation": "Message Claude… (Enter to send · Shift+Enter for newline)" },
|
||||||
|
"composer.stop": { "translation": "Stop ⎋" },
|
||||||
|
"composer.stop.hint": { "translation": "Interrupt the running turn (Escape)" },
|
||||||
|
"composer.removeAttachment": { "translation": "Remove {name}" },
|
||||||
|
"pane.title.primary": { "translation": "claude — primary" },
|
||||||
|
"pane.title.secondary": { "translation": "claude — secondary {index}" },
|
||||||
|
"pane.starting": { "translation": "starting…" },
|
||||||
|
"pane.modeBadge.semantics": { "translation": "permission mode: {mode}" },
|
||||||
|
"running.semantics": { "translation": "Claude is running" },
|
||||||
|
"running.verb.pondering": { "translation": "Pondering" },
|
||||||
|
"running.verb.conjuring": { "translation": "Conjuring" },
|
||||||
|
"running.verb.brewing": { "translation": "Brewing" },
|
||||||
|
"running.verb.tinkering": { "translation": "Tinkering" },
|
||||||
|
"running.verb.noodling": { "translation": "Noodling" },
|
||||||
|
"running.verb.percolating": { "translation": "Percolating" },
|
||||||
|
"running.verb.computing": { "translation": "Computing" },
|
||||||
|
"running.verb.wrangling": { "translation": "Wrangling" },
|
||||||
|
"running.verb.untangling": { "translation": "Untangling" },
|
||||||
|
"running.verb.synthesizing": { "translation": "Synthesizing" },
|
||||||
|
"running.verb.cogitating": { "translation": "Cogitating" },
|
||||||
|
"running.verb.whirring": { "translation": "Whirring" },
|
||||||
|
"running.verb.mincing": { "translation": "Mincing" },
|
||||||
|
"running.verb.boiling": { "translation": "Boiling" },
|
||||||
|
"running.verb.humming": { "translation": "Humming" },
|
||||||
|
"running.verb.buzzing": { "translation": "Buzzing" },
|
||||||
|
"running.verb.magicking": { "translation": "Magicking" },
|
||||||
|
"running.verb.cliding": { "translation": "Cliding" },
|
||||||
|
"running.verb.zooming": { "translation": "Zooming" },
|
||||||
|
"running.verb.bouncing": { "translation": "Bouncing" },
|
||||||
|
"conversation.empty": { "translation": "Waiting for Claude…" },
|
||||||
|
"conversation.label.you": { "translation": "you" },
|
||||||
|
"conversation.label.claude": { "translation": "claude" },
|
||||||
|
"conversation.label.clide": { "translation": "clide" },
|
||||||
|
"conversation.label.agent": { "translation": "agent" },
|
||||||
|
"conversation.label.agentPrompt": { "translation": "agent prompt" },
|
||||||
|
"conversation.label.context": { "translation": "context" },
|
||||||
|
"conversation.label.thinking": { "translation": "thinking" },
|
||||||
|
"conversation.label.agentThinking": { "translation": "agent thinking" },
|
||||||
|
"conversation.label.image": { "translation": "image" },
|
||||||
|
"conversation.label.icon": { "translation": "icons" },
|
||||||
|
"conversation.label.drawing": { "translation": "drawing" },
|
||||||
|
"conversation.draw.viewSource": { "translation": "view d2 source" },
|
||||||
|
"conversation.label.agentRun": { "translation": "agent run" },
|
||||||
|
"conversation.label.workflow": { "translation": "workflow" },
|
||||||
|
"conversation.label.error": { "translation": "error" },
|
||||||
|
"conversation.label.result": { "translation": "result" },
|
||||||
|
"conversation.label.denied": { "translation": "denied" },
|
||||||
|
"conversation.segment.prompt": { "translation": "prompt" },
|
||||||
|
"conversation.segment.result": { "translation": "result" },
|
||||||
|
"conversation.segment.liveTail": { "translation": "live tail" },
|
||||||
|
"conversation.segment.usage": { "translation": "usage" },
|
||||||
|
"conversation.segment.script": { "translation": "script" },
|
||||||
|
"conversation.workflow.launching": { "translation": "Launching…" },
|
||||||
|
"conversation.imagePlaceholder": { "translation": "could not load {path}" },
|
||||||
|
"conversation.bashTail.empty": { "translation": "no independent source to follow" },
|
||||||
|
"conversation.bashTail.label": { "translation": "live tail" },
|
||||||
|
"conversation.cluster.activity": { "translation": "Activity" },
|
||||||
|
"conversation.cluster.edits": { "translation": "Edits" },
|
||||||
|
"conversation.counter.step": { "translation": "1 step" },
|
||||||
|
"conversation.counter.steps": { "translation": "{count} steps" },
|
||||||
|
"conversation.counter.edit": { "translation": "1 edit" },
|
||||||
|
"conversation.counter.edits": { "translation": "{count} edits" },
|
||||||
|
"conversation.counter.starting": { "translation": "starting" },
|
||||||
|
"conversation.counter.agents": { "translation": "{done}/{total} agents" },
|
||||||
|
"prompt.permission.allow": { "translation": "1. Allow" },
|
||||||
|
"prompt.permission.allowRemember": { "translation": "2. Allow & don't ask again" },
|
||||||
|
"prompt.permission.deny": { "translation": "{n}. Deny" },
|
||||||
|
"prompt.permission.denySimplify": { "translation": "{n}. Deny & simplify" },
|
||||||
|
"prompt.permission.denySimplify.tooltip": { "translation": "Deny and ask Claude to retry this action in a simpler format — complex interactions don't work well with the permission system." },
|
||||||
|
"prompt.permission.note.placeholder": { "translation": "add a note (optional) — sent to Claude" },
|
||||||
|
"prompt.permission.label": { "translation": "permission · {name}" },
|
||||||
|
"prompt.question.label": { "translation": "question" },
|
||||||
|
"prompt.review.label": { "translation": "review" },
|
||||||
|
"prompt.review.title": { "translation": "Review your answers" },
|
||||||
|
"prompt.submit": { "translation": "Submit" },
|
||||||
|
"prompt.submitAnswers": { "translation": "Submit answers" },
|
||||||
|
"prompt.back": { "translation": "‹ Back" },
|
||||||
|
"prompt.next": { "translation": "Next ›" },
|
||||||
|
"prompt.reviewNav": { "translation": "Review ›" },
|
||||||
|
"prompt.nav.review": { "translation": "Review" },
|
||||||
|
"prompt.option.other": { "translation": "Other…" },
|
||||||
|
"prompt.other.placeholder": { "translation": "type your answer…" },
|
||||||
|
"prompt.note.placeholder": { "translation": "+ note (optional)" },
|
||||||
|
"prompt.chatInstead": { "translation": "chat instead" },
|
||||||
|
"tool.edit.before": { "translation": "— before" },
|
||||||
|
"tool.edit.after": { "translation": "+ after" },
|
||||||
|
"tool.bash.background": { "translation": "background" },
|
||||||
|
"tool.name.Read": { "translation": "Read" },
|
||||||
|
"tool.name.Edit": { "translation": "Edit" },
|
||||||
|
"tool.name.MultiEdit": { "translation": "MultiEdit" },
|
||||||
|
"tool.name.Write": { "translation": "Write" },
|
||||||
|
"tool.name.NotebookEdit": { "translation": "NotebookEdit" },
|
||||||
|
"tool.name.WebFetch": { "translation": "WebFetch" },
|
||||||
|
"tool.name.WebSearch": { "translation": "WebSearch" },
|
||||||
|
"tool.name.Task": { "translation": "Task" },
|
||||||
|
"tool.name.TodoWrite": { "translation": "TodoWrite" },
|
||||||
|
"tool.name.ExitPlanMode": { "translation": "ExitPlanMode" },
|
||||||
|
"permissionControl.semantics": { "translation": "permission mode: {mode}. Activate to change." },
|
||||||
|
"permissionControl.tooltip": { "translation": "Permission mode: {mode} — change (Ctrl/Cmd+M cycles; +Shift includes bypass)" },
|
||||||
|
"permissionControl.bypassHint": { "translation": "shift-click" },
|
||||||
|
"permissionControl.bypassSemantics": { "translation": "bypassPermissions — shift-click to enable" },
|
||||||
|
"permissionBadge.tooltip": { "translation": "Permission mode: {mode}. Click to cycle default/acceptEdits/plan; Shift-click for bypassPermissions." },
|
||||||
|
"permissionBadge.semantics": { "translation": "Permission mode: {label}" },
|
||||||
|
"card.expand": { "translation": "Expand" },
|
||||||
|
"card.collapse": { "translation": "Collapse" },
|
||||||
|
"card.succeeded": { "translation": "succeeded" },
|
||||||
|
"card.failed": { "translation": "failed" },
|
||||||
|
"card.copy": { "translation": "copy" },
|
||||||
|
"taskDock.summary": { "translation": "{count} {tasks} · {done} done" },
|
||||||
|
"taskDock.task.singular": { "translation": "task" },
|
||||||
|
"taskDock.task.plural": { "translation": "tasks" },
|
||||||
|
"taskDock.collapse": { "translation": "Collapse tasks" },
|
||||||
|
"taskDock.expand": { "translation": "Expand tasks" },
|
||||||
|
"taskDock.semantics": { "translation": "Claude task list, {summary}, {state}" },
|
||||||
|
"taskDock.state.expanded": { "translation": "expanded" },
|
||||||
|
"taskDock.state.collapsed": { "translation": "collapsed" },
|
||||||
|
"taskDock.status.done": { "translation": "done" },
|
||||||
|
"taskDock.status.inProgress": { "translation": "in progress" },
|
||||||
|
"taskDock.status.pending": { "translation": "pending" },
|
||||||
|
"taskDock.row.semantics": { "translation": "{text}, {status}" },
|
||||||
|
"sessionPicker.title": { "translation": "Resume a Claude session" },
|
||||||
|
"sessionPicker.empty": { "translation": "No sessions found for this workspace." },
|
||||||
|
"modelPicker.cancel": { "translation": "cancel" },
|
||||||
|
"image.semantics": { "translation": "Image {name}" },
|
||||||
|
"activity.section.session": { "translation": "SESSION" },
|
||||||
|
"activity.control.clear": { "translation": "clear" },
|
||||||
|
"activity.control.compact": { "translation": "compact" },
|
||||||
|
"activity.control.fork": { "translation": "fork" },
|
||||||
|
"activity.control.resume": { "translation": "resume" },
|
||||||
|
"activity.control.refreshUsage": { "translation": "refresh usage" },
|
||||||
|
"activity.control.semantics": { "translation": "{label} session" },
|
||||||
|
"activity.control.tooltip": { "translation": "{label} · {command}" },
|
||||||
|
"activity.empty": { "translation": "No activity recorded yet." },
|
||||||
|
"activity.section.workflows": { "translation": "WORKFLOWS" },
|
||||||
|
"activity.workflow.fallback": { "translation": "workflow" },
|
||||||
|
"activity.workflow.done": { "translation": "done" },
|
||||||
|
"activity.workflow.starting": { "translation": "starting" },
|
||||||
|
"activity.section.usage": { "translation": "USAGE" },
|
||||||
|
"activity.row.session": { "translation": "session" },
|
||||||
|
"activity.row.weekAll": { "translation": "week (all)" },
|
||||||
|
"activity.row.weekSonnet": { "translation": "week (sonnet)" },
|
||||||
|
"activity.section.today": { "translation": "TODAY" },
|
||||||
|
"activity.row.messages": { "translation": "messages" },
|
||||||
|
"activity.row.sessions": { "translation": "sessions" },
|
||||||
|
"activity.row.toolCalls": { "translation": "tool calls" },
|
||||||
|
"activity.section.lifetime": { "translation": "LIFETIME" },
|
||||||
|
"activity.section.runtime": { "translation": "RUNTIME · primary" },
|
||||||
|
"activity.row.model": { "translation": "model" },
|
||||||
|
"activity.row.effort": { "translation": "effort" },
|
||||||
|
"activity.row.context": { "translation": "context" },
|
||||||
|
"activity.row.mode": { "translation": "mode" },
|
||||||
|
"activity.row.skills": { "translation": "skills" },
|
||||||
|
"config.empty": { "translation": "Claude environment not loaded." },
|
||||||
|
"config.section.settings": { "translation": "SETTINGS" },
|
||||||
|
"config.row.model": { "translation": "model" },
|
||||||
|
"config.row.effort": { "translation": "effort" },
|
||||||
|
"config.row.permissionMode": { "translation": "permission mode" },
|
||||||
|
"config.row.outputStyle": { "translation": "output style" },
|
||||||
|
"config.row.source": { "translation": "source" },
|
||||||
|
"config.row.source.value": { "translation": "~/.claude + .claude" },
|
||||||
|
"config.footer": { "translation": "expand a list to see all · click a skill/agent/command → opens its .md" },
|
||||||
|
"config.section.skills": { "translation": "SKILLS" },
|
||||||
|
"config.section.agents": { "translation": "AGENTS" },
|
||||||
|
"config.section.commands": { "translation": "COMMANDS" },
|
||||||
|
"config.section.hooks": { "translation": "HOOKS" },
|
||||||
|
"config.section.permissions": { "translation": "PERMISSIONS" },
|
||||||
|
"config.section.mcpServers": { "translation": "MCP SERVERS" },
|
||||||
|
"config.perm.allow": { "translation": "allow" },
|
||||||
|
"config.perm.ask": { "translation": "ask" },
|
||||||
|
"config.perm.deny": { "translation": "deny" },
|
||||||
|
"config.control.semantics": { "translation": "{label}: {value}. Click to change." },
|
||||||
|
"config.control.tooltip": { "translation": "change {label}" },
|
||||||
|
"config.control.option.semantics": { "translation": "{label}: {name}" },
|
||||||
|
"roster.bypass.confirmBody": { "translation": "Enable bypassPermissions? All tool calls will be auto-allowed." },
|
||||||
|
"roster.bypass.confirm.semantics": { "translation": "Confirm bypass" },
|
||||||
|
"roster.bypass.confirm.tooltip": { "translation": "Confirm" },
|
||||||
|
"roster.bypass.ok": { "translation": "OK" },
|
||||||
|
"roster.bypass.cancel.semantics": { "translation": "Cancel bypass" },
|
||||||
|
"roster.bypass.cancel.tooltip": { "translation": "Cancel" },
|
||||||
|
"roster.bypass.cancel": { "translation": "Cancel" },
|
||||||
|
"roster.hidePane": { "translation": "Hide pane" },
|
||||||
|
"roster.showPane": { "translation": "Show pane" },
|
||||||
|
"roster.unmute": { "translation": "Unmute messages" },
|
||||||
|
"roster.mute": { "translation": "Mute messages" },
|
||||||
|
"roster.inject": { "translation": "Inject message" },
|
||||||
|
"roster.fork": { "translation": "Fork session" },
|
||||||
|
"roster.close": { "translation": "Close session" },
|
||||||
|
"roster.inject.cancel": { "translation": "Cancel" },
|
||||||
|
"taskRow.reassign": { "translation": "Reassign task" },
|
||||||
|
"team.empty": { "translation": "No team active." },
|
||||||
|
"team.section.tasks": { "translation": "TASKS" },
|
||||||
|
"tabStrip.activity": { "translation": "Activity" },
|
||||||
|
"tabStrip.team": { "translation": "Team" },
|
||||||
|
"tabStrip.team.count": { "translation": "Team · {count}" },
|
||||||
|
"tabStrip.config": { "translation": "Config" },
|
||||||
|
"teamChat.section.messages": { "translation": "MESSAGES" },
|
||||||
|
"teamChat.popOut.semantics": { "translation": "Open full chat pane" },
|
||||||
|
"teamChat.popOut.tooltip": { "translation": "Open full chat" },
|
||||||
|
"teamChat.empty": { "translation": "No messages yet." },
|
||||||
|
"teamChat.composer.placeholder": { "translation": "@name or @team …" },
|
||||||
|
"teamChat.pane.title": { "translation": "Team Chat" },
|
||||||
|
"teamChat.interrupt.semantics": { "translation": "Interrupt target session" },
|
||||||
|
"teamChat.interrupt.label": { "translation": "Interrupt" },
|
||||||
|
"command.newSecondary": { "translation": "Claude: open a secondary session" },
|
||||||
|
"command.killAllSessions": { "translation": "Claude: kill all sessions for this repo" },
|
||||||
|
"command.sessionStorage": { "translation": "Claude: session storage (disk usage + cleanup)" },
|
||||||
|
"command.activity.foldLevel": { "translation": "Claude: cycle activity fold level" },
|
||||||
|
"command.agent.show": { "translation": "Claude: show an agent session pane" },
|
||||||
|
"command.agent.hide": { "translation": "Claude: hide an agent session pane" },
|
||||||
|
"command.agent.close": { "translation": "Claude: close (kill) an agent session" },
|
||||||
|
"command.agent.mute": { "translation": "Claude: mute broker delivery to an agent session" },
|
||||||
|
"command.agent.unmute": { "translation": "Claude: unmute broker delivery to an agent session" },
|
||||||
|
"command.agent.injectMessage": { "translation": "Claude: inject a text turn into an agent session" },
|
||||||
|
"command.agent.setPermissionMode": { "translation": "Claude: set permission mode for an agent session" },
|
||||||
|
"command.mode.cycle": { "translation": "Claude: Cycle permission mode" },
|
||||||
|
"command.task.reassign": { "translation": "Claude: reassign a shared task to an agent" },
|
||||||
|
"command.teamChat.open": { "translation": "Claude: open the team chat pane" },
|
||||||
|
"command.teamChat.post": { "translation": "Claude: post a message into the team channel as the user" },
|
||||||
|
"command.agent.fork": { "translation": "Claude: fork a managed session into a new branch session" },
|
||||||
|
"settings.activity.title": { "translation": "Activity" },
|
||||||
|
"settings.activity.conversation.label": { "translation": "Conversation" },
|
||||||
|
"settings.activity.foldLevel.label": { "translation": "Fold level" },
|
||||||
|
"settings.activity.foldLevel.help": { "translation": "How aggressively the conversation folds tool calls, thinking, and results." },
|
||||||
|
"settings.activity.opt.none": { "translation": "Show everything" },
|
||||||
|
"settings.activity.opt.tools": { "translation": "Fold tool calls" },
|
||||||
|
"settings.activity.opt.thinking": { "translation": "Fold tools + thinking" },
|
||||||
|
"settings.activity.opt.everything": { "translation": "Fold all but prose" },
|
||||||
|
"settings.claude.title": { "translation": "Claude" },
|
||||||
|
"settings.claude.newSessionDefaults.label": { "translation": "New session defaults" },
|
||||||
|
"settings.claude.model.label": { "translation": "Model" },
|
||||||
|
"settings.claude.model.help": { "translation": "Model for new sessions." },
|
||||||
|
"settings.claude.effort.label": { "translation": "Effort" },
|
||||||
|
"settings.claude.effort.help": { "translation": "Reasoning effort for new sessions (applied via --effort at spawn)." },
|
||||||
|
"settings.claude.permissionMode.label": { "translation": "Permission mode" },
|
||||||
|
"settings.claude.permissionMode.help": { "translation": "Starting permission mode for new sessions." },
|
||||||
|
"settings.claude.account.label": { "translation": "Account" },
|
||||||
|
"settings.claude.account.registry.label": { "translation": "Accounts" },
|
||||||
|
"settings.claude.account.registry.help": { "translation": "Registered Claude accounts (each a separate config dir + login)." },
|
||||||
|
"settings.claude.account.workspace.label": { "translation": "Account for this workspace" },
|
||||||
|
"settings.claude.account.workspace.help": { "translation": "Which Claude account this repo runs under; Default uses the system login." }
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"command.clide.installCli": { "translation": "clide: Install 'clide' command in PATH" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"tab.title": { "translation": "Decisions" },
|
||||||
|
"tab.detail.title": { "translation": "Decision" },
|
||||||
|
"loading": { "translation": "Loading decisions..." },
|
||||||
|
"error.load": { "translation": "failed to load decisions" },
|
||||||
|
"empty": { "translation": "No decisions found.\nRun `pql decisions sync` to index." },
|
||||||
|
"filter.hint": { "translation": "Filter decisions…" },
|
||||||
|
"refresh.tooltip": { "translation": "Refresh decisions" },
|
||||||
|
"section.confirmed": { "translation": "CONFIRMED" },
|
||||||
|
"section.questions": { "translation": "QUESTIONS" },
|
||||||
|
"section.rejected": { "translation": "REJECTED" },
|
||||||
|
"badge.resolved": { "translation": "resolved" },
|
||||||
|
"detail.loading": { "translation": "Loading…" },
|
||||||
|
"detail.empty": { "translation": "Select a decision to view details." },
|
||||||
|
"detail.section.refs": { "translation": "CROSS-REFERENCES" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"dialog.title": { "translation": "Open an external link?" },
|
||||||
|
"dialog.body": { "translation": "A clide:// link from outside the app is asking to:" },
|
||||||
|
"dialog.warning": { "translation": "Only allow this if you trust where the link came from." },
|
||||||
|
"button.cancel": { "translation": "Cancel" },
|
||||||
|
"button.open": { "translation": "Open" },
|
||||||
|
"command.deeplink.invoke": { "translation": "Open a clide:// deep link" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"command.reset": { "translation": "Layout: Reset to Classic" },
|
||||||
|
"preset.classic": { "translation": "Classic" },
|
||||||
|
"command.palette.toggle": { "translation": "Command Palette" },
|
||||||
|
"command.sidebar.collapse": { "translation": "Toggle Sidebar Collapse" },
|
||||||
|
"command.context.collapse": { "translation": "Toggle Context Panel Collapse" },
|
||||||
|
"command.panel.focus.left": { "translation": "Focus Left Panel" },
|
||||||
|
"command.panel.focus.middle": { "translation": "Focus Middle Panel" },
|
||||||
|
"command.panel.focus.right": { "translation": "Focus Right Panel" },
|
||||||
|
"command.panel.focusMode": { "translation": "Toggle Focus Mode" },
|
||||||
|
"command.panel.focusMode.exit": { "translation": "Exit Focus Mode" },
|
||||||
|
"command.editor.open": { "translation": "Open Editor" },
|
||||||
|
"command.editor.close": { "translation": "Close Editor" },
|
||||||
|
"command.workspace.tab.next": { "translation": "Next Workspace Tab" },
|
||||||
|
"command.workspace.tab.previous": { "translation": "Previous Workspace Tab" },
|
||||||
|
"command.sidebar.section.1": { "translation": "Sidebar: Section 1" },
|
||||||
|
"command.sidebar.section.2": { "translation": "Sidebar: Section 2" },
|
||||||
|
"command.sidebar.section.3": { "translation": "Sidebar: Section 3" },
|
||||||
|
"command.sidebar.section.4": { "translation": "Sidebar: Section 4" },
|
||||||
|
"command.sidebar.section.5": { "translation": "Sidebar: Section 5" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"tab.title": { "translation": "Diff" },
|
||||||
|
"view.semantics": { "translation": "diff view" },
|
||||||
|
"status.loading": { "translation": "Loading…" },
|
||||||
|
"empty.staged": { "translation": "No staged changes." },
|
||||||
|
"empty.unstaged": { "translation": "No unstaged changes." },
|
||||||
|
"toolbar.unstaged": { "translation": "Unstaged" },
|
||||||
|
"toolbar.unstaged.semantics": { "translation": "show unstaged changes" },
|
||||||
|
"toolbar.staged": { "translation": "Staged" },
|
||||||
|
"toolbar.staged.semantics": { "translation": "show staged changes" },
|
||||||
|
"meta.newFile": { "translation": "new file" },
|
||||||
|
"meta.deleted": { "translation": "deleted" },
|
||||||
|
"meta.renamedFrom": { "translation": "renamed from {path}" },
|
||||||
|
"meta.binary": { "translation": "binary" }
|
||||||
|
}
|
||||||
+3
-1
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"tab.title": { "translation": "Editor" },
|
"tab.title": { "translation": "Editor" },
|
||||||
|
"chrome.title": { "translation": "editor" },
|
||||||
"empty": { "translation": "Open a file to begin editing." },
|
"empty": { "translation": "Open a file to begin editing." },
|
||||||
"subtitle.no-buffer": { "translation": "no buffer · use `clide open <path>` or pick a file in the tree" }
|
"subtitle.no-buffer": { "translation": "no buffer · use `clide open <path>` or pick a file in the tree" },
|
||||||
|
"a11y.text-area": { "translation": "editor text area" }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"notice.title": { "translation": "Extension management is coming" },
|
||||||
|
"notice.body": { "translation": "Installing, enabling, and disabling extensions arrives with third-party (Lua) extension support. For now the built-in extensions are always on." },
|
||||||
|
"notice.tracked": { "translation": "Tracked in T-8 (Tier 6) · D-16" },
|
||||||
|
"settings.extensions.title": { "translation": "Extensions" },
|
||||||
|
"settings.extensions.section.notice": { "translation": "" },
|
||||||
|
"settings.extensions.field.notice.label": { "translation": "" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"tab.title": { "translation": "Files" },
|
||||||
|
"loading": { "translation": "Loading…" },
|
||||||
|
"empty": { "translation": "No visible files" },
|
||||||
|
"filter.hint": { "translation": "Filter files…" },
|
||||||
|
"a11y.tree": { "translation": "file tree — {name}" },
|
||||||
|
"a11y.collapse": { "translation": "Collapse {name}" },
|
||||||
|
"a11y.expand": { "translation": "Expand {name}" },
|
||||||
|
"a11y.open": { "translation": "Open {name}" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"tab.title": { "translation": "Git" },
|
||||||
|
"panel.semantics": { "translation": "git panel" },
|
||||||
|
"filter.hint": { "translation": "Filter changes…" },
|
||||||
|
"status.loading": { "translation": "Loading…" },
|
||||||
|
"status.clean": { "translation": "Nothing to commit, working tree clean." },
|
||||||
|
"group.conflicts": { "translation": "Merge conflicts" },
|
||||||
|
"group.staged": { "translation": "Staged" },
|
||||||
|
"group.changes": { "translation": "Changes" },
|
||||||
|
"group.untracked": { "translation": "Untracked" },
|
||||||
|
"action.unstageAll": { "translation": "Unstage all" },
|
||||||
|
"action.stageAll": { "translation": "Stage all" },
|
||||||
|
"commit.message.semantics": { "translation": "commit message" },
|
||||||
|
"commit.button": { "translation": "Commit" },
|
||||||
|
"commit.button.semantics": { "translation": "commit staged changes" },
|
||||||
|
"branch.detached": { "translation": "(detached)" },
|
||||||
|
"action.pull": { "translation": "Pull" },
|
||||||
|
"action.pull.semantics": { "translation": "git pull" },
|
||||||
|
"action.push": { "translation": "Push" },
|
||||||
|
"action.push.semantics": { "translation": "git push" },
|
||||||
|
"state.added": { "translation": "added" },
|
||||||
|
"state.modified": { "translation": "modified" },
|
||||||
|
"state.deleted": { "translation": "deleted" },
|
||||||
|
"state.renamed": { "translation": "renamed" },
|
||||||
|
"state.copied": { "translation": "copied" },
|
||||||
|
"state.untracked": { "translation": "untracked" },
|
||||||
|
"row.stage.semantics": { "translation": "stage {name}" },
|
||||||
|
"row.unstage.semantics": { "translation": "unstage {name}" },
|
||||||
|
"row.discard.semantics": { "translation": "discard changes to {name}" },
|
||||||
|
"discard.title": { "translation": "Discard changes?" },
|
||||||
|
"discard.body": { "translation": "Unstaged changes to {name} will be permanently lost." },
|
||||||
|
"button.cancel": { "translation": "Cancel" },
|
||||||
|
"button.discard": { "translation": "Discard" },
|
||||||
|
"branch.switch.semantics": { "translation": "switch branch — {branch}" },
|
||||||
|
"branchPicker.title": { "translation": "Switch branch" },
|
||||||
|
"branchPicker.empty": { "translation": "No branches found." },
|
||||||
|
"branchPicker.loadFailed": { "translation": "failed to load branches" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"tab.graph.title": { "translation": "Graph" },
|
||||||
|
"graph.empty": { "translation": "No linked notes in this vault." },
|
||||||
|
"graph.empty.filtered": { "translation": "No notes match the filter." },
|
||||||
|
"graph.empty.nolocal": { "translation": "Open a note to see its local graph." },
|
||||||
|
"graph.filter.glob": { "translation": "Path glob, e.g. notes/**" },
|
||||||
|
"graph.filter.depth": { "translation": "Local" },
|
||||||
|
"graph.filter.all": { "translation": "All" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"command.preset.default": { "translation": "Keymap: Default" },
|
||||||
|
"command.preset.vim": { "translation": "Keymap: Vim" },
|
||||||
|
"command.preset.vscode": { "translation": "Keymap: VS Code" },
|
||||||
|
"command.preset.jetbrains": { "translation": "Keymap: JetBrains" },
|
||||||
|
"settings.keymap.title": { "translation": "Keymap" },
|
||||||
|
"settings.keymap.section.preset": { "translation": "Preset" },
|
||||||
|
"settings.keymap.field.preset.label": { "translation": "Active preset" },
|
||||||
|
"settings.keymap.field.preset.help": { "translation": "Keyboard layout for the whole app." },
|
||||||
|
"settings.keymap.field.preset.option.default": { "translation": "Default" },
|
||||||
|
"settings.keymap.field.preset.option.vim": { "translation": "Vim" },
|
||||||
|
"settings.keymap.field.preset.option.vscode": { "translation": "VS Code" },
|
||||||
|
"settings.keymap.field.preset.option.jetbrains": { "translation": "JetBrains" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"empty": { "translation": "Select a .md file to preview it here." },
|
||||||
|
"chrome.title": { "translation": "viewer" },
|
||||||
|
"subtitle.lines": { "translation": "{count} lines" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"menu.file": { "translation": "File" },
|
||||||
|
"menu.view": { "translation": "View" },
|
||||||
|
"menu.help": { "translation": "Help" },
|
||||||
|
"about.version": { "translation": "Version" },
|
||||||
|
"about.commit": { "translation": "Commit" },
|
||||||
|
"about.built": { "translation": "Built" },
|
||||||
|
"about.repository": { "translation": "Repository" },
|
||||||
|
"about.checkUpdates": { "translation": "Check for updates" },
|
||||||
|
"about.checking": { "translation": "Checking…" },
|
||||||
|
"about.upToDate": { "translation": "You're on the latest version." },
|
||||||
|
"about.updateAvailable": { "translation": "clide {version} is available — release notes" },
|
||||||
|
"about.updateFailed": { "translation": "Couldn't check for updates" },
|
||||||
|
"licenses.heading": { "translation": "Bundled dependencies" },
|
||||||
|
"licenses.unavailable": { "translation": "Licenses unavailable." },
|
||||||
|
"licenses.loading": { "translation": "Loading…" },
|
||||||
|
"button.close": { "translation": "Close" },
|
||||||
|
"button.cancel": { "translation": "Cancel" },
|
||||||
|
"button.open": { "translation": "Open" },
|
||||||
|
"button.opening": { "translation": "Opening…" },
|
||||||
|
"button.ok": { "translation": "OK" },
|
||||||
|
"dialog.openProject.title": { "translation": "Open project" },
|
||||||
|
"dialog.openProject.body": { "translation": "Enter the path to a git repository." },
|
||||||
|
"dialog.openProject.error": { "translation": "Not a git repository" },
|
||||||
|
"dialog.notRepo.title": { "translation": "No git repo found" },
|
||||||
|
"dialog.notRepo.body": { "translation": "A clide project root requires a git repository." },
|
||||||
|
"command.file.openFolder": { "translation": "File: Open Folder…" },
|
||||||
|
"command.file.newWindow": { "translation": "File: New Window" },
|
||||||
|
"command.file.closeWorkspace": { "translation": "File: Close Project" },
|
||||||
|
"command.help.about": { "translation": "Help: About clide" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"a11y.log": { "translation": "output log" },
|
||||||
|
"empty": { "translation": "No output yet." },
|
||||||
|
"empty.filtered": { "translation": "No output matches the filter." },
|
||||||
|
"filter.hint": { "translation": "Filter…" },
|
||||||
|
"chip.level": { "translation": "Level: {level}" },
|
||||||
|
"chip.source": { "translation": "Source: {source}" },
|
||||||
|
"chip.clear": { "translation": "Clear" },
|
||||||
|
"source.all": { "translation": "all" },
|
||||||
|
"a11y.jumpToLatest": { "translation": "jump to latest" },
|
||||||
|
"jump.label": { "translation": "Jump to latest ↓" },
|
||||||
|
"a11y.toggleDock": { "translation": "toggle output dock" },
|
||||||
|
"dock.label": { "translation": "Output" },
|
||||||
|
"command.dock.toggle": { "translation": "Toggle output dock" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"tab.title": { "translation": "pql" },
|
||||||
|
"tab.links.title": { "translation": "Links" },
|
||||||
|
"filter.markdown.hint": { "translation": "Filter markdown…" },
|
||||||
|
"loading": { "translation": "Loading…" },
|
||||||
|
"empty.markdown": { "translation": "No markdown files found." },
|
||||||
|
"search.vault.hint": { "translation": "Search vault…" },
|
||||||
|
"search.query.hint": { "translation": "PQL query…" },
|
||||||
|
"backlinks.empty": { "translation": "Open a file to see its links." },
|
||||||
|
"backlinks.loading": { "translation": "Loading…" },
|
||||||
|
"backlinks.semantics": { "translation": "backlinks for {path}" },
|
||||||
|
"group.backlinks": { "translation": "Backlinks" },
|
||||||
|
"group.outlinks": { "translation": "Outlinks" },
|
||||||
|
"group.label": { "translation": "{label} ({count})" },
|
||||||
|
"group.none": { "translation": "None" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"tab.title": { "translation": "Problems" },
|
||||||
|
"semantics.panel": { "translation": "problems panel" },
|
||||||
|
"filter.hint": { "translation": "Filter problems…" },
|
||||||
|
"count": { "translation": "Problems ({count})" },
|
||||||
|
"refresh.label": { "translation": "Refresh" },
|
||||||
|
"refresh.semantics": { "translation": "refresh problems" },
|
||||||
|
"scanning": { "translation": "Scanning…" },
|
||||||
|
"empty": { "translation": "No problems found." }
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"mode.find": { "translation": "Find" },
|
||||||
|
"mode.vault": { "translation": "Vault" },
|
||||||
|
"mode.query": { "translation": "Query" },
|
||||||
|
"mode.markdown": { "translation": "Markdown" },
|
||||||
|
"find.hint": { "translation": "Search" },
|
||||||
|
"replace.hint": { "translation": "Replace" },
|
||||||
|
"include.hint": { "translation": "files to include (e.g. *.dart)" },
|
||||||
|
"exclude.hint": { "translation": "files to exclude" },
|
||||||
|
"toggle.regex": { "translation": "Regular expression" },
|
||||||
|
"toggle.caseInsensitive": { "translation": "Case insensitive" },
|
||||||
|
"status.searching": { "translation": "Searching…" },
|
||||||
|
"status.noResults": { "translation": "No results" },
|
||||||
|
"status.counts": { "translation": "{matches} in {files}" },
|
||||||
|
"a11y.openMatch": { "translation": "Open {path} line {line}" },
|
||||||
|
"button.replaceAll": { "translation": "Replace all" },
|
||||||
|
"button.ok": { "translation": "OK" },
|
||||||
|
"button.cancel": { "translation": "Cancel" },
|
||||||
|
"button.confirm": { "translation": "Confirm" },
|
||||||
|
"dialog.dirty.title": { "translation": "Working tree not clean" },
|
||||||
|
"dialog.dirty.body": { "translation": "Commit or stash your changes before replacing — git is the only undo." },
|
||||||
|
"dialog.confirm.body": { "translation": "Replace {matches} match(es) across {files} file(s)? This cannot be undone in clide." }
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"command.open": { "translation": "Settings…" },
|
||||||
|
"modal.title": { "translation": "Settings" },
|
||||||
|
"modal.close": { "translation": "Close" },
|
||||||
|
"modal.close.hint": { "translation": "Close settings without changing anything" },
|
||||||
|
"rail.header": { "translation": "Categories" },
|
||||||
|
"panel.empty": { "translation": "No settings categories are registered yet." },
|
||||||
|
"search.hint": { "translation": "Search settings…" },
|
||||||
|
"search.empty": { "translation": "No settings match your search." },
|
||||||
|
"scope.project": { "translation": "This project" },
|
||||||
|
"scope.always": { "translation": "All clide" },
|
||||||
|
"scope.default": { "translation": "Default" },
|
||||||
|
"scope.reset": { "translation": "Reset to default" },
|
||||||
|
"scope.tip.project": { "translation": "Stored in this project (.clide)" },
|
||||||
|
"scope.tip.always": { "translation": "Stored for all clide (~/.clide)" },
|
||||||
|
"scope.tip.default": { "translation": "Unset — using the default" }
|
||||||
|
}
|
||||||
+3
-1
@@ -1,7 +1,9 @@
|
|||||||
{
|
{
|
||||||
"tab.title": { "translation": "Terminal" },
|
"tab.title": { "translation": "Terminal" },
|
||||||
|
"chrome.title": { "translation": "terminal" },
|
||||||
"subtitle.spawning": { "translation": "spawning shell…" },
|
"subtitle.spawning": { "translation": "spawning shell…" },
|
||||||
"subtitle.exited": { "translation": "Shell exited." },
|
"subtitle.exited": { "translation": "Shell exited." },
|
||||||
"error.unavailable": { "translation": "Terminal unavailable" },
|
"error.unavailable": { "translation": "Terminal unavailable" },
|
||||||
"error.daemon": { "translation": "Backend not connected." }
|
"error.daemon": { "translation": "Backend not connected." },
|
||||||
|
"a11y.label": { "translation": "terminal — {subtitle}" }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"command.pick": { "translation": "Theme…" },
|
||||||
|
"modal.title": { "translation": "Select theme" },
|
||||||
|
"modal.cancel": { "translation": "Cancel" },
|
||||||
|
"modal.cancel.hint": { "translation": "Close the theme picker without changing the current theme" },
|
||||||
|
"row.select.hint": { "translation": "Activate this theme" },
|
||||||
|
"section.appearance": { "translation": "Appearance" },
|
||||||
|
"toggle.highContrast": { "translation": "High contrast" },
|
||||||
|
"settings.appearance.title": { "translation": "Appearance" },
|
||||||
|
"settings.appearance.section.theme": { "translation": "Theme" },
|
||||||
|
"settings.appearance.section.typography": { "translation": "Typography" },
|
||||||
|
"settings.appearance.field.theme.label": { "translation": "Theme" },
|
||||||
|
"settings.appearance.field.theme.help": { "translation": "Color theme; high contrast switches to the accessible variant." },
|
||||||
|
"settings.appearance.field.uiFont.label": { "translation": "UI font" },
|
||||||
|
"settings.appearance.field.uiFont.help": { "translation": "Typeface for the app interface; applies live." },
|
||||||
|
"settings.appearance.field.uiFont.option.josefinSans": { "translation": "Josefin Sans" },
|
||||||
|
"settings.appearance.field.uiFont.option.inter": { "translation": "Inter" },
|
||||||
|
"settings.appearance.field.monoFont.label": { "translation": "Monospace font" },
|
||||||
|
"settings.appearance.field.monoFont.help": { "translation": "Terminal, diffs, code, and IDs; applies live." },
|
||||||
|
"settings.appearance.field.monoFont.option.jetBrainsMono": { "translation": "JetBrains Mono" },
|
||||||
|
"settings.appearance.field.monoFont.option.firaMono": { "translation": "Fira Mono" },
|
||||||
|
"settings.appearance.section.language": { "translation": "Language" },
|
||||||
|
"settings.appearance.field.language.label": { "translation": "Language" },
|
||||||
|
"settings.appearance.field.language.help": { "translation": "Language for the app interface; applies live." }
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"tab.title": { "translation": "Tickets" },
|
||||||
|
"tab.detail.title": { "translation": "Ticket" },
|
||||||
|
"type.initiative": { "translation": "Initiative" },
|
||||||
|
"type.epic": { "translation": "Epic" },
|
||||||
|
"type.story": { "translation": "Story" },
|
||||||
|
"type.task": { "translation": "Task" },
|
||||||
|
"type.bug": { "translation": "Bug" },
|
||||||
|
"loading": { "translation": "Loading tickets..." },
|
||||||
|
"error.load": { "translation": "failed to load tickets" },
|
||||||
|
"empty": { "translation": "No tickets.\nRun `pql ticket new` to create one." },
|
||||||
|
"filter.hint": { "translation": "Filter tickets…" },
|
||||||
|
"refresh.tooltip": { "translation": "Refresh tickets" },
|
||||||
|
"section.in_progress": { "translation": "IN PROGRESS" },
|
||||||
|
"section.review": { "translation": "REVIEW" },
|
||||||
|
"section.ready": { "translation": "READY" },
|
||||||
|
"section.backlog": { "translation": "BACKLOG" },
|
||||||
|
"section.done": { "translation": "DONE" },
|
||||||
|
"section.cancelled": { "translation": "CANCELLED" },
|
||||||
|
"chip.label": { "translation": "{type} type filter" },
|
||||||
|
"chip.tooltip": { "translation": "Click to toggle · double-click to isolate" },
|
||||||
|
"badge.wip": { "translation": "WIP" },
|
||||||
|
"badge.review": { "translation": "REVIEW" },
|
||||||
|
"badge.cancelled": { "translation": "CANCELLED" },
|
||||||
|
"pickUp.tooltip": { "translation": "Pick up — hand this ticket to the Claude pane" },
|
||||||
|
"detail.loading": { "translation": "Loading…" },
|
||||||
|
"detail.empty": { "translation": "Select a ticket to view details." },
|
||||||
|
"detail.assigned": { "translation": "assigned: {name}" },
|
||||||
|
"detail.section.parents": { "translation": "PARENT TREE" },
|
||||||
|
"detail.section.decisions": { "translation": "REFERENCED DECISIONS" },
|
||||||
|
"status.backlog": { "translation": "BACKLOG" },
|
||||||
|
"status.ready": { "translation": "READY" },
|
||||||
|
"status.in_progress": { "translation": "WIP" },
|
||||||
|
"status.review": { "translation": "REVIEW" },
|
||||||
|
"status.done": { "translation": "DONE" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"command.detect": { "translation": "Re-detect tool paths" },
|
||||||
|
"settings.title": { "translation": "Tools" },
|
||||||
|
"settings.section.binaries": { "translation": "Supporter binaries" },
|
||||||
|
"settings.field.claude.label": { "translation": "Claude CLI" },
|
||||||
|
"settings.field.claude.help": { "translation": "Absolute path to the claude binary; blank to auto-resolve." },
|
||||||
|
"settings.field.d2.label": { "translation": "d2" },
|
||||||
|
"settings.field.d2.help": { "translation": "Absolute path to the d2 diagram compiler; blank to auto-resolve." },
|
||||||
|
"settings.field.detect.label": { "translation": "Re-detect" },
|
||||||
|
"settings.field.detect.help": { "translation": "Re-scan PATH and the common install dirs, overwriting the paths above." },
|
||||||
|
"settings.section.path": { "translation": "Workspace PATH" },
|
||||||
|
"settings.field.pathPreset.label": { "translation": "Prepend to PATH" },
|
||||||
|
"settings.field.pathPreset.help": { "translation": "Directories put ahead of PATH in Claude sessions and terminal panes spawned for this repo; worktrees share the repo's preset. Applies to new shells." },
|
||||||
|
"path.noWorkspace": { "translation": "Open a workspace to set its PATH preset." },
|
||||||
|
"path.sharedRoot": { "translation": "Worktree — preset shared with" },
|
||||||
|
"path.empty": { "translation": "No preset entries — spawned shells get the resolved login-shell PATH as-is." },
|
||||||
|
"path.add": { "translation": "Add entry" },
|
||||||
|
"path.invalid": { "translation": "Enter an absolute path (or ~/…)." },
|
||||||
|
"path.missing": { "translation": "missing" },
|
||||||
|
"path.moveUp": { "translation": "Move up" },
|
||||||
|
"path.moveDown": { "translation": "Move down" },
|
||||||
|
"path.remove": { "translation": "Remove" },
|
||||||
|
"path.capture": { "translation": "Suggest from login shell" },
|
||||||
|
"path.captureNone": { "translation": "Nothing to suggest — the login-shell PATH is already covered." },
|
||||||
|
"path.addSuggestion": { "translation": "Add suggested entry" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"command.view.zoomIn": { "translation": "View: Zoom In" },
|
||||||
|
"command.view.zoomOut": { "translation": "View: Zoom Out" },
|
||||||
|
"command.view.zoomReset": { "translation": "View: Reset Zoom" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"mode.normal": { "translation": "NORMAL" },
|
||||||
|
"mode.insert": { "translation": "INSERT" },
|
||||||
|
"mode.visual": { "translation": "VISUAL" },
|
||||||
|
"command.vim.mode.normal": { "translation": "Vim: Normal mode" },
|
||||||
|
"command.vim.mode.insert": { "translation": "Vim: Insert mode" },
|
||||||
|
"command.vim.mode.visual": { "translation": "Vim: Visual mode" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"title": { "translation": "clide" },
|
||||||
|
"subtitle": { "translation": "IDE for Claude Code CLI" },
|
||||||
|
"open-project": { "translation": "Open project" },
|
||||||
|
"open-project.hint": { "translation": "Pick a git repository to open as the workspace" },
|
||||||
|
"tab.title": { "translation": "Welcome" },
|
||||||
|
"section.tips": { "translation": "TIPS" },
|
||||||
|
"section.start": { "translation": "START" },
|
||||||
|
"section.recent": { "translation": "RECENT" },
|
||||||
|
"tips.quickOpen": { "translation": "Quick open" },
|
||||||
|
"tips.commandPalette": { "translation": "Command palette" },
|
||||||
|
"tips.toggleSidebar": { "translation": "Toggle sidebar" },
|
||||||
|
"tips.toggleContext": { "translation": "Toggle context" },
|
||||||
|
"tips.findInFiles": { "translation": "Find in files" },
|
||||||
|
"tips.focusMode": { "translation": "Focus mode" },
|
||||||
|
"action.openFolder": { "translation": "Open folder…" },
|
||||||
|
"action.newProject": { "translation": "New project…" },
|
||||||
|
"dialog.newProject.title": { "translation": "New project" },
|
||||||
|
"dialog.newProject.body": { "translation": "Creates a git repo + a CLAUDE.md, then opens it." },
|
||||||
|
"button.create": { "translation": "Create" },
|
||||||
|
"button.creating": { "translation": "Creating…" },
|
||||||
|
"recent.empty": { "translation": "No recent projects." },
|
||||||
|
"sticky.label": { "translation": "always open this project on launch" },
|
||||||
|
"sticky.tooltip": { "translation": "Always open this project on launch" },
|
||||||
|
"sticky.tooltip.active": { "translation": "Always open this project on launch (uncheck to restore picker)" },
|
||||||
|
"status.checking": { "translation": "checking…" },
|
||||||
|
"status.ok": { "translation": "application ok" },
|
||||||
|
"status.notFound": { "translation": "{tool} not found" },
|
||||||
|
"status.theme": { "translation": "theme: " },
|
||||||
|
"dialog.openProject.title": { "translation": "Open project" },
|
||||||
|
"dialog.openProject.body": { "translation": "Enter the path to a git repository." },
|
||||||
|
"dialog.openProject.error": { "translation": "Not a git repository" },
|
||||||
|
"button.cancel": { "translation": "Cancel" },
|
||||||
|
"button.open": { "translation": "Open" },
|
||||||
|
"button.opening": { "translation": "Opening…" },
|
||||||
|
"button.ok": { "translation": "OK" },
|
||||||
|
"dialog.notRepo.title": { "translation": "No git repo found" },
|
||||||
|
"dialog.notRepo.body": { "translation": "A clide project needs a git repository. Initialize this folder as one?" },
|
||||||
|
"dialog.notRepo.initialize": { "translation": "Initialize project" },
|
||||||
|
"button.initializing": { "translation": "Initializing…" },
|
||||||
|
"command.workspace.open-project": { "translation": "Workspace: Open project…" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"collapser.expand": { "translation": "Expand" },
|
||||||
|
"collapser.collapse": { "translation": "Collapse" },
|
||||||
|
"collapser.expanded": { "translation": "expanded" },
|
||||||
|
"collapser.collapsed": { "translation": "collapsed" },
|
||||||
|
"toast.dismiss": { "translation": "Dismiss notification" },
|
||||||
|
"lightbox.close": { "translation": "close" },
|
||||||
|
"lightbox.hint": { "translation": "scroll to zoom · double-click to reset · Esc to close" },
|
||||||
|
"tab.new": { "translation": "New tab" },
|
||||||
|
"exline.notCommand": { "translation": "Not an editor command" },
|
||||||
|
"spine.expandSuffix": { "translation": "click to expand" },
|
||||||
|
"pane.close": { "translation": "Close pane" },
|
||||||
|
"pane.header": { "translation": "pane header: {title}" },
|
||||||
|
"reader.back": { "translation": "Back" },
|
||||||
|
"reader.forward": { "translation": "Forward" },
|
||||||
|
"reader.jumpToPin": { "translation": "Jump to pin" },
|
||||||
|
"reader.edit": { "translation": "Edit in editor" },
|
||||||
|
"reader.pin": { "translation": "Pin" },
|
||||||
|
"reader.unpin": { "translation": "Unpin" },
|
||||||
|
"link.openInEditor": { "translation": "Open in editor" },
|
||||||
|
"resize.axis.width": { "translation": "width" },
|
||||||
|
"resize.axis.height": { "translation": "height" },
|
||||||
|
"resize.sidebar": { "translation": "Sidebar {axis}" },
|
||||||
|
"resize.contextPanel": { "translation": "Context panel {axis}" },
|
||||||
|
"resize.slot": { "translation": "{slot} {axis}" },
|
||||||
|
"resize.pixels": { "translation": "{n} pixels" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"tab.title": { "translation": "Canvas" },
|
||||||
|
"empty": { "translation": "Open een .canvas-bestand om het hier te bekijken." },
|
||||||
|
"status.loading": { "translation": "Laden…" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
{
|
||||||
|
"tab.title": { "translation": "Claude" },
|
||||||
|
"status.attaching": { "translation": "verbinden…" },
|
||||||
|
"status.no-tmux": { "translation": "no-tmux · elke start opnieuw" },
|
||||||
|
"status.exited": { "translation": "sessie beëindigd" },
|
||||||
|
"status.primary-exited": { "translation": "sessie beëindigd — herstart clide om opnieuw te proberen" },
|
||||||
|
"banner.title": { "translation": "Claude" },
|
||||||
|
"banner.warmingUp": { "translation": "Opstarten — je gesprek verschijnt hier." },
|
||||||
|
"banner.role.primary": { "translation": "primair" },
|
||||||
|
"banner.role.secondary": { "translation": "sessie {index}" },
|
||||||
|
"composer.hint": { "translation": "Bericht aan Claude… (Enter om te verzenden · Shift+Enter voor een nieuwe regel)" },
|
||||||
|
"composer.stop": { "translation": "Stop ⎋" },
|
||||||
|
"composer.stop.hint": { "translation": "Onderbreek de lopende beurt (Escape)" },
|
||||||
|
"composer.removeAttachment": { "translation": "{name} verwijderen" },
|
||||||
|
"pane.title.primary": { "translation": "claude — primair" },
|
||||||
|
"pane.title.secondary": { "translation": "claude — secundair {index}" },
|
||||||
|
"pane.starting": { "translation": "starten…" },
|
||||||
|
"pane.modeBadge.semantics": { "translation": "permissiemodus: {mode}" },
|
||||||
|
"running.semantics": { "translation": "Claude is bezig" },
|
||||||
|
"running.verb.pondering": { "translation": "Peinzen" },
|
||||||
|
"running.verb.conjuring": { "translation": "Toveren" },
|
||||||
|
"running.verb.brewing": { "translation": "Brouwen" },
|
||||||
|
"running.verb.tinkering": { "translation": "Knutselen" },
|
||||||
|
"running.verb.noodling": { "translation": "Prutsen" },
|
||||||
|
"running.verb.percolating": { "translation": "Pruttelen" },
|
||||||
|
"running.verb.computing": { "translation": "Rekenen" },
|
||||||
|
"running.verb.wrangling": { "translation": "Worstelen" },
|
||||||
|
"running.verb.untangling": { "translation": "Ontwarren" },
|
||||||
|
"running.verb.synthesizing": { "translation": "Synthetiseren" },
|
||||||
|
"running.verb.cogitating": { "translation": "Overpeinzen" },
|
||||||
|
"running.verb.whirring": { "translation": "Snorren" },
|
||||||
|
"running.verb.mincing": { "translation": "Hakken" },
|
||||||
|
"running.verb.boiling": { "translation": "Koken" },
|
||||||
|
"running.verb.humming": { "translation": "Neuriën" },
|
||||||
|
"running.verb.buzzing": { "translation": "Zoemen" },
|
||||||
|
"running.verb.magicking": { "translation": "Goochelen" },
|
||||||
|
"running.verb.cliding": { "translation": "Cliden" },
|
||||||
|
"running.verb.zooming": { "translation": "Zoeven" },
|
||||||
|
"running.verb.bouncing": { "translation": "Stuiteren" },
|
||||||
|
"conversation.empty": { "translation": "Wachten op Claude…" },
|
||||||
|
"conversation.label.you": { "translation": "jij" },
|
||||||
|
"conversation.label.claude": { "translation": "claude" },
|
||||||
|
"conversation.label.clide": { "translation": "clide" },
|
||||||
|
"conversation.label.agent": { "translation": "agent" },
|
||||||
|
"conversation.label.agentPrompt": { "translation": "agent-prompt" },
|
||||||
|
"conversation.label.context": { "translation": "context" },
|
||||||
|
"conversation.label.thinking": { "translation": "nadenken" },
|
||||||
|
"conversation.label.agentThinking": { "translation": "agent denkt na" },
|
||||||
|
"conversation.label.image": { "translation": "afbeelding" },
|
||||||
|
"conversation.label.icon": { "translation": "iconen" },
|
||||||
|
"conversation.label.drawing": { "translation": "tekening" },
|
||||||
|
"conversation.draw.viewSource": { "translation": "d2-bron tonen" },
|
||||||
|
"conversation.label.agentRun": { "translation": "agent-uitvoering" },
|
||||||
|
"conversation.label.workflow": { "translation": "workflow" },
|
||||||
|
"conversation.label.error": { "translation": "fout" },
|
||||||
|
"conversation.label.result": { "translation": "resultaat" },
|
||||||
|
"conversation.label.denied": { "translation": "geweigerd" },
|
||||||
|
"conversation.segment.prompt": { "translation": "prompt" },
|
||||||
|
"conversation.segment.result": { "translation": "resultaat" },
|
||||||
|
"conversation.segment.liveTail": { "translation": "live tail" },
|
||||||
|
"conversation.segment.usage": { "translation": "verbruik" },
|
||||||
|
"conversation.segment.script": { "translation": "script" },
|
||||||
|
"conversation.workflow.launching": { "translation": "Starten…" },
|
||||||
|
"conversation.imagePlaceholder": { "translation": "kon {path} niet laden" },
|
||||||
|
"conversation.bashTail.empty": { "translation": "geen onafhankelijke bron om te volgen" },
|
||||||
|
"conversation.bashTail.label": { "translation": "live tail" },
|
||||||
|
"conversation.cluster.activity": { "translation": "Activiteit" },
|
||||||
|
"conversation.cluster.edits": { "translation": "Wijzigingen" },
|
||||||
|
"conversation.counter.step": { "translation": "1 stap" },
|
||||||
|
"conversation.counter.steps": { "translation": "{count} stappen" },
|
||||||
|
"conversation.counter.edit": { "translation": "1 wijziging" },
|
||||||
|
"conversation.counter.edits": { "translation": "{count} wijzigingen" },
|
||||||
|
"conversation.counter.starting": { "translation": "starten" },
|
||||||
|
"conversation.counter.agents": { "translation": "{done}/{total} agents" },
|
||||||
|
"prompt.permission.allow": { "translation": "1. Toestaan" },
|
||||||
|
"prompt.permission.allowRemember": { "translation": "2. Toestaan en niet meer vragen" },
|
||||||
|
"prompt.permission.deny": { "translation": "{n}. Weigeren" },
|
||||||
|
"prompt.permission.denySimplify": { "translation": "{n}. Weigeren en vereenvoudigen" },
|
||||||
|
"prompt.permission.denySimplify.tooltip": { "translation": "Weiger en vraag Claude deze actie in een eenvoudigere vorm opnieuw te proberen — complexe interacties werken niet goed met het permissiesysteem." },
|
||||||
|
"prompt.permission.note.placeholder": { "translation": "voeg een notitie toe (optioneel) — wordt naar Claude gestuurd" },
|
||||||
|
"prompt.permission.label": { "translation": "permissie · {name}" },
|
||||||
|
"prompt.question.label": { "translation": "vraag" },
|
||||||
|
"prompt.review.label": { "translation": "controleren" },
|
||||||
|
"prompt.review.title": { "translation": "Controleer je antwoorden" },
|
||||||
|
"prompt.submit": { "translation": "Verzenden" },
|
||||||
|
"prompt.submitAnswers": { "translation": "Antwoorden verzenden" },
|
||||||
|
"prompt.back": { "translation": "‹ Terug" },
|
||||||
|
"prompt.next": { "translation": "Volgende ›" },
|
||||||
|
"prompt.reviewNav": { "translation": "Controleren ›" },
|
||||||
|
"prompt.nav.review": { "translation": "Controleren" },
|
||||||
|
"prompt.option.other": { "translation": "Anders…" },
|
||||||
|
"prompt.other.placeholder": { "translation": "typ je antwoord…" },
|
||||||
|
"prompt.note.placeholder": { "translation": "+ notitie (optioneel)" },
|
||||||
|
"prompt.chatInstead": { "translation": "in plaats daarvan chatten" },
|
||||||
|
"tool.edit.before": { "translation": "— voor" },
|
||||||
|
"tool.edit.after": { "translation": "+ na" },
|
||||||
|
"tool.bash.background": { "translation": "achtergrond" },
|
||||||
|
"tool.name.Read": { "translation": "Lezen" },
|
||||||
|
"tool.name.Edit": { "translation": "Bewerken" },
|
||||||
|
"tool.name.MultiEdit": { "translation": "Meervoudig bewerken" },
|
||||||
|
"tool.name.Write": { "translation": "Schrijven" },
|
||||||
|
"tool.name.NotebookEdit": { "translation": "Notebook bewerken" },
|
||||||
|
"tool.name.WebFetch": { "translation": "Web ophalen" },
|
||||||
|
"tool.name.WebSearch": { "translation": "Web zoeken" },
|
||||||
|
"tool.name.Task": { "translation": "Taak" },
|
||||||
|
"tool.name.TodoWrite": { "translation": "Takenlijst" },
|
||||||
|
"tool.name.ExitPlanMode": { "translation": "Planmodus verlaten" },
|
||||||
|
"permissionControl.semantics": { "translation": "permissiemodus: {mode}. Activeer om te wijzigen." },
|
||||||
|
"permissionControl.tooltip": { "translation": "Permissiemodus: {mode} — wijzigen (Ctrl/Cmd+M wisselt; +Shift ook bypass)" },
|
||||||
|
"permissionControl.bypassHint": { "translation": "shift-klik" },
|
||||||
|
"permissionControl.bypassSemantics": { "translation": "bypassPermissions — shift-klik om in te schakelen" },
|
||||||
|
"permissionBadge.tooltip": { "translation": "Permissiemodus: {mode}. Klik om te wisselen tussen default/acceptEdits/plan; Shift-klik voor bypassPermissions." },
|
||||||
|
"permissionBadge.semantics": { "translation": "Permissiemodus: {label}" },
|
||||||
|
"card.expand": { "translation": "Uitvouwen" },
|
||||||
|
"card.collapse": { "translation": "Invouwen" },
|
||||||
|
"card.succeeded": { "translation": "gelukt" },
|
||||||
|
"card.failed": { "translation": "mislukt" },
|
||||||
|
"card.copy": { "translation": "kopiëren" },
|
||||||
|
"taskDock.summary": { "translation": "{count} {tasks} · {done} klaar" },
|
||||||
|
"taskDock.task.singular": { "translation": "taak" },
|
||||||
|
"taskDock.task.plural": { "translation": "taken" },
|
||||||
|
"taskDock.collapse": { "translation": "Taken invouwen" },
|
||||||
|
"taskDock.expand": { "translation": "Taken uitvouwen" },
|
||||||
|
"taskDock.semantics": { "translation": "Takenlijst van Claude, {summary}, {state}" },
|
||||||
|
"taskDock.state.expanded": { "translation": "uitgevouwen" },
|
||||||
|
"taskDock.state.collapsed": { "translation": "ingevouwen" },
|
||||||
|
"taskDock.status.done": { "translation": "klaar" },
|
||||||
|
"taskDock.status.inProgress": { "translation": "bezig" },
|
||||||
|
"taskDock.status.pending": { "translation": "in wachtrij" },
|
||||||
|
"taskDock.row.semantics": { "translation": "{text}, {status}" },
|
||||||
|
"sessionPicker.title": { "translation": "Een Claude-sessie hervatten" },
|
||||||
|
"sessionPicker.empty": { "translation": "Geen sessies gevonden voor deze workspace." },
|
||||||
|
"modelPicker.cancel": { "translation": "annuleren" },
|
||||||
|
"image.semantics": { "translation": "Afbeelding {name}" },
|
||||||
|
"activity.section.session": { "translation": "SESSIE" },
|
||||||
|
"activity.control.clear": { "translation": "wissen" },
|
||||||
|
"activity.control.compact": { "translation": "compact" },
|
||||||
|
"activity.control.fork": { "translation": "fork" },
|
||||||
|
"activity.control.resume": { "translation": "hervatten" },
|
||||||
|
"activity.control.refreshUsage": { "translation": "verbruik vernieuwen" },
|
||||||
|
"activity.control.semantics": { "translation": "{label} sessie" },
|
||||||
|
"activity.control.tooltip": { "translation": "{label} · {command}" },
|
||||||
|
"activity.empty": { "translation": "Nog geen activiteit vastgelegd." },
|
||||||
|
"activity.section.workflows": { "translation": "WORKFLOWS" },
|
||||||
|
"activity.workflow.fallback": { "translation": "workflow" },
|
||||||
|
"activity.workflow.done": { "translation": "klaar" },
|
||||||
|
"activity.workflow.starting": { "translation": "starten" },
|
||||||
|
"activity.section.usage": { "translation": "VERBRUIK" },
|
||||||
|
"activity.row.session": { "translation": "sessie" },
|
||||||
|
"activity.row.weekAll": { "translation": "week (alles)" },
|
||||||
|
"activity.row.weekSonnet": { "translation": "week (sonnet)" },
|
||||||
|
"activity.section.today": { "translation": "VANDAAG" },
|
||||||
|
"activity.row.messages": { "translation": "berichten" },
|
||||||
|
"activity.row.sessions": { "translation": "sessies" },
|
||||||
|
"activity.row.toolCalls": { "translation": "tool-aanroepen" },
|
||||||
|
"activity.section.lifetime": { "translation": "TOTAAL" },
|
||||||
|
"activity.section.runtime": { "translation": "RUNTIME · primair" },
|
||||||
|
"activity.row.model": { "translation": "model" },
|
||||||
|
"activity.row.effort": { "translation": "effort" },
|
||||||
|
"activity.row.context": { "translation": "context" },
|
||||||
|
"activity.row.mode": { "translation": "modus" },
|
||||||
|
"activity.row.skills": { "translation": "skills" },
|
||||||
|
"config.empty": { "translation": "Claude-omgeving niet geladen." },
|
||||||
|
"config.section.settings": { "translation": "INSTELLINGEN" },
|
||||||
|
"config.row.model": { "translation": "model" },
|
||||||
|
"config.row.effort": { "translation": "effort" },
|
||||||
|
"config.row.permissionMode": { "translation": "permissiemodus" },
|
||||||
|
"config.row.outputStyle": { "translation": "uitvoerstijl" },
|
||||||
|
"config.row.source": { "translation": "bron" },
|
||||||
|
"config.row.source.value": { "translation": "~/.claude + .claude" },
|
||||||
|
"config.footer": { "translation": "vouw een lijst uit om alles te zien · klik op een skill/agent/command → opent de .md" },
|
||||||
|
"config.section.skills": { "translation": "SKILLS" },
|
||||||
|
"config.section.agents": { "translation": "AGENTS" },
|
||||||
|
"config.section.commands": { "translation": "COMMANDS" },
|
||||||
|
"config.section.hooks": { "translation": "HOOKS" },
|
||||||
|
"config.section.permissions": { "translation": "PERMISSIES" },
|
||||||
|
"config.section.mcpServers": { "translation": "MCP SERVERS" },
|
||||||
|
"config.perm.allow": { "translation": "toestaan" },
|
||||||
|
"config.perm.ask": { "translation": "vragen" },
|
||||||
|
"config.perm.deny": { "translation": "weigeren" },
|
||||||
|
"config.control.semantics": { "translation": "{label}: {value}. Klik om te wijzigen." },
|
||||||
|
"config.control.tooltip": { "translation": "{label} wijzigen" },
|
||||||
|
"config.control.option.semantics": { "translation": "{label}: {name}" },
|
||||||
|
"roster.bypass.confirmBody": { "translation": "bypassPermissions inschakelen? Alle tool-aanroepen worden automatisch toegestaan." },
|
||||||
|
"roster.bypass.confirm.semantics": { "translation": "Bypass bevestigen" },
|
||||||
|
"roster.bypass.confirm.tooltip": { "translation": "Bevestigen" },
|
||||||
|
"roster.bypass.ok": { "translation": "OK" },
|
||||||
|
"roster.bypass.cancel.semantics": { "translation": "Bypass annuleren" },
|
||||||
|
"roster.bypass.cancel.tooltip": { "translation": "Annuleren" },
|
||||||
|
"roster.bypass.cancel": { "translation": "Annuleren" },
|
||||||
|
"roster.hidePane": { "translation": "Paneel verbergen" },
|
||||||
|
"roster.showPane": { "translation": "Paneel tonen" },
|
||||||
|
"roster.unmute": { "translation": "Berichten weer aanzetten" },
|
||||||
|
"roster.mute": { "translation": "Berichten dempen" },
|
||||||
|
"roster.inject": { "translation": "Bericht injecteren" },
|
||||||
|
"roster.fork": { "translation": "Sessie forken" },
|
||||||
|
"roster.close": { "translation": "Sessie sluiten" },
|
||||||
|
"roster.inject.cancel": { "translation": "Annuleren" },
|
||||||
|
"taskRow.reassign": { "translation": "Taak opnieuw toewijzen" },
|
||||||
|
"team.empty": { "translation": "Geen team actief." },
|
||||||
|
"team.section.tasks": { "translation": "TAKEN" },
|
||||||
|
"tabStrip.activity": { "translation": "Activiteit" },
|
||||||
|
"tabStrip.team": { "translation": "Team" },
|
||||||
|
"tabStrip.team.count": { "translation": "Team · {count}" },
|
||||||
|
"tabStrip.config": { "translation": "Config" },
|
||||||
|
"teamChat.section.messages": { "translation": "BERICHTEN" },
|
||||||
|
"teamChat.popOut.semantics": { "translation": "Volledig chatpaneel openen" },
|
||||||
|
"teamChat.popOut.tooltip": { "translation": "Volledige chat openen" },
|
||||||
|
"teamChat.empty": { "translation": "Nog geen berichten." },
|
||||||
|
"teamChat.composer.placeholder": { "translation": "@naam of @team …" },
|
||||||
|
"teamChat.pane.title": { "translation": "Teamchat" },
|
||||||
|
"teamChat.interrupt.semantics": { "translation": "Doelsessie onderbreken" },
|
||||||
|
"teamChat.interrupt.label": { "translation": "Onderbreken" },
|
||||||
|
"command.newSecondary": { "translation": "Claude: een secundaire sessie openen" },
|
||||||
|
"command.killAllSessions": { "translation": "Claude: alle sessies voor deze repo afsluiten" },
|
||||||
|
"command.sessionStorage": { "translation": "Claude: sessieopslag (schijfgebruik + opschonen)" },
|
||||||
|
"command.activity.foldLevel": { "translation": "Claude: doorloop het invouwniveau van de activiteit" },
|
||||||
|
"command.agent.show": { "translation": "Claude: een agent-sessiepaneel tonen" },
|
||||||
|
"command.agent.hide": { "translation": "Claude: een agent-sessiepaneel verbergen" },
|
||||||
|
"command.agent.close": { "translation": "Claude: een agent-sessie sluiten (afsluiten)" },
|
||||||
|
"command.agent.mute": { "translation": "Claude: broker-aflevering naar een agent-sessie dempen" },
|
||||||
|
"command.agent.unmute": { "translation": "Claude: broker-aflevering naar een agent-sessie weer aanzetten" },
|
||||||
|
"command.agent.injectMessage": { "translation": "Claude: een tekstbeurt in een agent-sessie injecteren" },
|
||||||
|
"command.agent.setPermissionMode": { "translation": "Claude: permissiemodus voor een agent-sessie instellen" },
|
||||||
|
"command.mode.cycle": { "translation": "Claude: Permissiemodus doorlopen" },
|
||||||
|
"command.task.reassign": { "translation": "Claude: een gedeelde taak opnieuw aan een agent toewijzen" },
|
||||||
|
"command.teamChat.open": { "translation": "Claude: het teamchatpaneel openen" },
|
||||||
|
"command.teamChat.post": { "translation": "Claude: een bericht als gebruiker in het teamkanaal plaatsen" },
|
||||||
|
"command.agent.fork": { "translation": "Claude: een beheerde sessie forken naar een nieuwe branch-sessie" },
|
||||||
|
"settings.activity.title": { "translation": "Activiteit" },
|
||||||
|
"settings.activity.conversation.label": { "translation": "Gesprek" },
|
||||||
|
"settings.activity.foldLevel.label": { "translation": "Invouwniveau" },
|
||||||
|
"settings.activity.foldLevel.help": { "translation": "Hoe agressief het gesprek tool-aanroepen, nadenken en resultaten invouwt." },
|
||||||
|
"settings.activity.opt.none": { "translation": "Alles tonen" },
|
||||||
|
"settings.activity.opt.tools": { "translation": "Tool-aanroepen invouwen" },
|
||||||
|
"settings.activity.opt.thinking": { "translation": "Tools + nadenken invouwen" },
|
||||||
|
"settings.activity.opt.everything": { "translation": "Alles invouwen behalve tekst" },
|
||||||
|
"settings.claude.title": { "translation": "Claude" },
|
||||||
|
"settings.claude.newSessionDefaults.label": { "translation": "Standaardwaarden voor nieuwe sessies" },
|
||||||
|
"settings.claude.model.label": { "translation": "Model" },
|
||||||
|
"settings.claude.model.help": { "translation": "Model voor nieuwe sessies." },
|
||||||
|
"settings.claude.effort.label": { "translation": "Effort" },
|
||||||
|
"settings.claude.effort.help": { "translation": "Reasoning effort voor nieuwe sessies (toegepast via --effort bij het starten)." },
|
||||||
|
"settings.claude.permissionMode.label": { "translation": "Permissiemodus" },
|
||||||
|
"settings.claude.permissionMode.help": { "translation": "Begin-permissiemodus voor nieuwe sessies." },
|
||||||
|
"settings.claude.account.label": { "translation": "Account" },
|
||||||
|
"settings.claude.account.registry.label": { "translation": "Accounts" },
|
||||||
|
"settings.claude.account.registry.help": { "translation": "Geregistreerde Claude-accounts (elk een eigen configmap + login)." },
|
||||||
|
"settings.claude.account.workspace.label": { "translation": "Account voor deze werkmap" },
|
||||||
|
"settings.claude.account.workspace.help": { "translation": "Welk Claude-account deze repo gebruikt; Standaard gebruikt de systeemlogin." }
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"command.clide.installCli": { "translation": "clide: Installeer 'clide'-opdracht in PATH" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"tab.title": { "translation": "Beslissingen" },
|
||||||
|
"tab.detail.title": { "translation": "Beslissing" },
|
||||||
|
"loading": { "translation": "Beslissingen laden..." },
|
||||||
|
"error.load": { "translation": "laden van beslissingen mislukt" },
|
||||||
|
"empty": { "translation": "Geen beslissingen gevonden.\nVoer `pql decisions sync` uit om te indexeren." },
|
||||||
|
"filter.hint": { "translation": "Beslissingen filteren…" },
|
||||||
|
"refresh.tooltip": { "translation": "Beslissingen vernieuwen" },
|
||||||
|
"section.confirmed": { "translation": "BEVESTIGD" },
|
||||||
|
"section.questions": { "translation": "VRAGEN" },
|
||||||
|
"section.rejected": { "translation": "AFGEWEZEN" },
|
||||||
|
"badge.resolved": { "translation": "opgelost" },
|
||||||
|
"detail.loading": { "translation": "Laden…" },
|
||||||
|
"detail.empty": { "translation": "Selecteer een beslissing om de details te bekijken." },
|
||||||
|
"detail.section.refs": { "translation": "KRUISVERWIJZINGEN" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"dialog.title": { "translation": "Een externe link openen?" },
|
||||||
|
"dialog.body": { "translation": "Een clide://-link van buiten de app vraagt om:" },
|
||||||
|
"dialog.warning": { "translation": "Sta dit alleen toe als je vertrouwt waar de link vandaan komt." },
|
||||||
|
"button.cancel": { "translation": "Annuleren" },
|
||||||
|
"button.open": { "translation": "Openen" },
|
||||||
|
"command.deeplink.invoke": { "translation": "Een clide://-deeplink openen" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"command.reset": { "translation": "Indeling: Terugzetten naar Klassiek" },
|
||||||
|
"preset.classic": { "translation": "Klassiek" },
|
||||||
|
"command.palette.toggle": { "translation": "Opdrachtenpalet" },
|
||||||
|
"command.sidebar.collapse": { "translation": "Zijbalk samenvouwen aan/uit" },
|
||||||
|
"command.context.collapse": { "translation": "Contextpaneel samenvouwen aan/uit" },
|
||||||
|
"command.panel.focus.left": { "translation": "Focus op linkerpaneel" },
|
||||||
|
"command.panel.focus.middle": { "translation": "Focus op middelste paneel" },
|
||||||
|
"command.panel.focus.right": { "translation": "Focus op rechterpaneel" },
|
||||||
|
"command.panel.focusMode": { "translation": "Focusmodus aan/uit" },
|
||||||
|
"command.panel.focusMode.exit": { "translation": "Focusmodus afsluiten" },
|
||||||
|
"command.editor.open": { "translation": "Editor openen" },
|
||||||
|
"command.editor.close": { "translation": "Editor sluiten" },
|
||||||
|
"command.workspace.tab.next": { "translation": "Volgend werkruimtetabblad" },
|
||||||
|
"command.workspace.tab.previous": { "translation": "Vorig werkruimtetabblad" },
|
||||||
|
"command.sidebar.section.1": { "translation": "Zijbalk: Sectie 1" },
|
||||||
|
"command.sidebar.section.2": { "translation": "Zijbalk: Sectie 2" },
|
||||||
|
"command.sidebar.section.3": { "translation": "Zijbalk: Sectie 3" },
|
||||||
|
"command.sidebar.section.4": { "translation": "Zijbalk: Sectie 4" },
|
||||||
|
"command.sidebar.section.5": { "translation": "Zijbalk: Sectie 5" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"tab.title": { "translation": "Diff" },
|
||||||
|
"view.semantics": { "translation": "diff-weergave" },
|
||||||
|
"status.loading": { "translation": "Laden…" },
|
||||||
|
"empty.staged": { "translation": "Geen gestagede wijzigingen." },
|
||||||
|
"empty.unstaged": { "translation": "Geen niet-gestagede wijzigingen." },
|
||||||
|
"toolbar.unstaged": { "translation": "Niet gestaged" },
|
||||||
|
"toolbar.unstaged.semantics": { "translation": "niet-gestagede wijzigingen tonen" },
|
||||||
|
"toolbar.staged": { "translation": "Gestaged" },
|
||||||
|
"toolbar.staged.semantics": { "translation": "gestagede wijzigingen tonen" },
|
||||||
|
"meta.newFile": { "translation": "nieuw bestand" },
|
||||||
|
"meta.deleted": { "translation": "verwijderd" },
|
||||||
|
"meta.renamedFrom": { "translation": "hernoemd vanaf {path}" },
|
||||||
|
"meta.binary": { "translation": "binair" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"tab.title": { "translation": "Editor" },
|
||||||
|
"chrome.title": { "translation": "editor" },
|
||||||
|
"empty": { "translation": "Open een bestand om te beginnen met bewerken." },
|
||||||
|
"subtitle.no-buffer": { "translation": "geen buffer · gebruik `clide open <path>` of kies een bestand in de boom" },
|
||||||
|
"a11y.text-area": { "translation": "tekstgebied editor" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"notice.title": { "translation": "Extensiebeheer komt eraan" },
|
||||||
|
"notice.body": { "translation": "Het installeren, in- en uitschakelen van extensies arriveert samen met ondersteuning voor externe (Lua-)extensies. Voorlopig staan de ingebouwde extensies altijd aan." },
|
||||||
|
"notice.tracked": { "translation": "Bijgehouden in T-8 (Tier 6) · D-16" },
|
||||||
|
"settings.extensions.title": { "translation": "Extensies" },
|
||||||
|
"settings.extensions.section.notice": { "translation": "" },
|
||||||
|
"settings.extensions.field.notice.label": { "translation": "" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"tab.title": { "translation": "Bestanden" },
|
||||||
|
"loading": { "translation": "Laden…" },
|
||||||
|
"empty": { "translation": "Geen zichtbare bestanden" },
|
||||||
|
"filter.hint": { "translation": "Bestanden filteren…" },
|
||||||
|
"a11y.tree": { "translation": "bestandsboom — {name}" },
|
||||||
|
"a11y.collapse": { "translation": "{name} samenvouwen" },
|
||||||
|
"a11y.expand": { "translation": "{name} uitvouwen" },
|
||||||
|
"a11y.open": { "translation": "{name} openen" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"tab.title": { "translation": "Git" },
|
||||||
|
"panel.semantics": { "translation": "git-paneel" },
|
||||||
|
"filter.hint": { "translation": "Wijzigingen filteren…" },
|
||||||
|
"status.loading": { "translation": "Laden…" },
|
||||||
|
"status.clean": { "translation": "Niets om vast te leggen, werkmap is schoon." },
|
||||||
|
"group.conflicts": { "translation": "Samenvoegconflicten" },
|
||||||
|
"group.staged": { "translation": "Klaargezet" },
|
||||||
|
"group.changes": { "translation": "Wijzigingen" },
|
||||||
|
"group.untracked": { "translation": "Niet gevolgd" },
|
||||||
|
"action.unstageAll": { "translation": "Alles terugnemen" },
|
||||||
|
"action.stageAll": { "translation": "Alles klaarzetten" },
|
||||||
|
"commit.message.semantics": { "translation": "commitbericht" },
|
||||||
|
"commit.button": { "translation": "Vastleggen" },
|
||||||
|
"commit.button.semantics": { "translation": "klaargezette wijzigingen vastleggen" },
|
||||||
|
"branch.detached": { "translation": "(losgekoppeld)" },
|
||||||
|
"action.pull": { "translation": "Pullen" },
|
||||||
|
"action.pull.semantics": { "translation": "git pull" },
|
||||||
|
"action.push": { "translation": "Pushen" },
|
||||||
|
"action.push.semantics": { "translation": "git push" },
|
||||||
|
"state.added": { "translation": "toegevoegd" },
|
||||||
|
"state.modified": { "translation": "gewijzigd" },
|
||||||
|
"state.deleted": { "translation": "verwijderd" },
|
||||||
|
"state.renamed": { "translation": "hernoemd" },
|
||||||
|
"state.copied": { "translation": "gekopieerd" },
|
||||||
|
"state.untracked": { "translation": "niet gevolgd" },
|
||||||
|
"row.stage.semantics": { "translation": "{name} klaarzetten" },
|
||||||
|
"row.unstage.semantics": { "translation": "{name} terugnemen" },
|
||||||
|
"row.discard.semantics": { "translation": "wijzigingen aan {name} ongedaan maken" },
|
||||||
|
"discard.title": { "translation": "Wijzigingen ongedaan maken?" },
|
||||||
|
"discard.body": { "translation": "Niet-klaargezette wijzigingen aan {name} gaan definitief verloren." },
|
||||||
|
"button.cancel": { "translation": "Annuleren" },
|
||||||
|
"button.discard": { "translation": "Ongedaan maken" },
|
||||||
|
"branch.switch.semantics": { "translation": "branch wisselen — {branch}" },
|
||||||
|
"branchPicker.title": { "translation": "Branch wisselen" },
|
||||||
|
"branchPicker.empty": { "translation": "Geen branches gevonden." },
|
||||||
|
"branchPicker.loadFailed": { "translation": "laden van branches mislukt" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"tab.graph.title": { "translation": "Grafiek" },
|
||||||
|
"graph.empty": { "translation": "Geen gekoppelde notities in deze vault." },
|
||||||
|
"graph.empty.filtered": { "translation": "Geen notities voldoen aan het filter." },
|
||||||
|
"graph.empty.nolocal": { "translation": "Open een notitie om de lokale grafiek te zien." },
|
||||||
|
"graph.filter.glob": { "translation": "Padpatroon, bijv. notes/**" },
|
||||||
|
"graph.filter.depth": { "translation": "Lokaal" },
|
||||||
|
"graph.filter.all": { "translation": "Alles" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"connected": { "translation": "verbonden" },
|
||||||
|
"connected.hint": { "translation": "backend-isolate is bereikbaar" },
|
||||||
|
"disconnected": { "translation": "niet verbonden" },
|
||||||
|
"disconnected.hint": { "translation": "backend-isolate draait niet" }
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user