Compare commits
@@ -99,6 +99,7 @@ Cutting a release is its own commit. In a single commit:
|
|||||||
3. Bump `pubspec.yaml` `version:` to `X.Y.Z` (drop the `-dev` suffix for the tag; re-add it on the next development commit if desired).
|
3. Bump `pubspec.yaml` `version:` to `X.Y.Z` (drop the `-dev` suffix for the tag; re-add it on the next development commit if desired).
|
||||||
4. Run `make gen-build-info` so `assets/licenses.yaml` `self.version:` re-syncs from pubspec (it's auto-rewritten by every build but commit the fresh state). Stage the resulting diff alongside step 3.
|
4. Run `make gen-build-info` so `assets/licenses.yaml` `self.version:` re-syncs from pubspec (it's auto-rewritten by every build but commit the fresh state). Stage the resulting diff alongside step 3.
|
||||||
5. Commit subject: `release vX.Y.Z`.
|
5. Commit subject: `release vX.Y.Z`.
|
||||||
|
6. **Tag it.** After the commit lands, run `make release` — it verifies the version/changelog/clean-tree invariants, runs the full gate, and creates the annotated `vX.Y.Z` tag. Then `git push origin main --follow-tags` to publish the commit and the tag together. **Every released version must have a matching `git tag`** — a CHANGELOG heading without a tag is an incomplete release (T-393: tagging had silently lapsed from v2.2.0 through v2.8.0). Don't hand-roll the tag; `make release` keeps the tag, gate, and version in lockstep. If you tag by hand, use an annotated tag (`git tag -a vX.Y.Z -m "clide vX.Y.Z"`); never push a release commit without its tag.
|
||||||
|
|
||||||
`pubspec.yaml` is the single source of truth for the version. Every `make` build/run/test target regenerates `lib/src/build_info.g.dart` (gitignored) and rewrites `assets/licenses.yaml` `self.version:` from it — so the Flutter app sees the current version everywhere without manual sync. Bumping `pubspec.yaml` and the changelog out of sync is the mistake this rule prevents.
|
`pubspec.yaml` is the single source of truth for the version. Every `make` build/run/test target regenerates `lib/src/build_info.g.dart` (gitignored) and rewrites `assets/licenses.yaml` `self.version:` from it — so the Flutter app sees the current version everywhere without manual sync. Bumping `pubspec.yaml` and the changelog out of sync is the mistake this rule prevents.
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,32 @@ These apply across every reference and every surface:
|
|||||||
widget must set it explicitly. `clideMonoFamily` / `clideUiFamily` are the
|
widget must set it explicitly. `clideMonoFamily` / `clideUiFamily` are the
|
||||||
facade's defaults — don't read them directly in new widgets (D-101). Same
|
facade's defaults — don't read them directly in new widgets (D-101). Same
|
||||||
facade exposes `ClideSettings.theme.of(context)` and `.i18n.of(context)`.
|
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)
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -300,3 +300,42 @@ 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 ('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 ('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 ('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 ('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,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.*
|
||||||
@@ -22,6 +22,155 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
## [2.9.0] — 2026-06-30
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Graph cards.** `clide draw --file graph.json` (template `graph`) renders a
|
||||||
|
nodes/edges graph in the conversation as a circular layout — labelled nodes,
|
||||||
|
lines between them. Honest error on a duplicate id or an edge to an unknown
|
||||||
|
node. (T-321)
|
||||||
|
- **Piped `--stdin` payloads.** `cat icons.json | clide icon show --stdin` (and
|
||||||
|
`image show`) accept a JSON payload on stdin — the ergonomic peer of `--file`
|
||||||
|
for structured commands. (T-315)
|
||||||
|
- **Before/after compare cards.** `clide draw --file compare.json` (template
|
||||||
|
`compare`) renders two or more images side by side, each with its own
|
||||||
|
label/description and tap-to-zoom; images aspect-fit so differing shapes
|
||||||
|
don't distort. (T-319)
|
||||||
|
- **Phosphor glyph cards.** `clide icon show gear folder` (or `--file` entries
|
||||||
|
with label/description/color) renders glyphs in the conversation at a hero
|
||||||
|
size plus a real-UI-size strip (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
|
## [2.7.0] — 2026-06-17
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -169,8 +169,7 @@ ui-smoke: ## Build + serve + run Playwright smoke + stop.
|
|||||||
@sh -c 'trap "tools/ui/stop.sh >/dev/null 2>&1" EXIT; cd tools/ui && npx playwright test smoke.spec.ts'
|
@sh -c 'trap "tools/ui/stop.sh >/dev/null 2>&1" EXIT; cd tools/ui && npx playwright test smoke.spec.ts'
|
||||||
|
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
build: gen-build-info clide-cli ## flutter build for the current OS (incl. the C CLI client).
|
build: clide-cli build-$(FLUTTER_OS) ## flutter build for the current OS (via build-<os>) + bundle the C CLI client.
|
||||||
flutter build $(FLUTTER_OS)
|
|
||||||
@install -m 755 $(CLIDE_CLI_BIN) $(CLI_BUNDLE_DEST)
|
@install -m 755 $(CLIDE_CLI_BIN) $(CLI_BUNDLE_DEST)
|
||||||
@echo "==> bundled C client at $(CLI_BUNDLE_DEST)"
|
@echo "==> bundled C client at $(CLI_BUNDLE_DEST)"
|
||||||
|
|
||||||
@@ -186,6 +185,10 @@ build-macos: gen-build-info ## flutter build macos (desktop bundle).
|
|||||||
build-windows: gen-build-info ## flutter build windows (desktop bundle).
|
build-windows: gen-build-info ## flutter build windows (desktop bundle).
|
||||||
flutter build windows
|
flutter build windows
|
||||||
|
|
||||||
|
.PHONY: release
|
||||||
|
release: ## Finalize a release: verify version/changelog/tree, run the gate, tag vX.Y.Z. Run after the `release vX.Y.Z` commit.
|
||||||
|
ci/release.sh
|
||||||
|
|
||||||
# -- install / uninstall -----------------------------------------------------
|
# -- install / uninstall -----------------------------------------------------
|
||||||
|
|
||||||
# Install prefix. Bundle lands at $(INSTALL_PREFIX)/clide/ with a
|
# Install prefix. Bundle lands at $(INSTALL_PREFIX)/clide/ with a
|
||||||
@@ -261,6 +264,8 @@ else ifeq ($(FLUTTER_OS),macos)
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
# -- dugite-native (bundled git) ------------------------------------------
|
# -- dugite-native (bundled git) ------------------------------------------
|
||||||
|
# Security tracking (D-59): run `make dugite-check` quarterly, or on a git CVE,
|
||||||
|
# to compare this pin against the latest upstream release. T-88 is the calendar.
|
||||||
|
|
||||||
DUGITE_VERSION := v2.53.0-3
|
DUGITE_VERSION := v2.53.0-3
|
||||||
DUGITE_COMMIT := f49d009
|
DUGITE_COMMIT := f49d009
|
||||||
@@ -327,6 +332,10 @@ clide-cli-clean: ## Remove the compiled C `clide` client.
|
|||||||
security: ## Supply-chain gate — osv-scanner over pubspec.lock (CI PR-merge pipeline; run locally on demand). Fails on a known advisory.
|
security: ## Supply-chain gate — osv-scanner over pubspec.lock (CI PR-merge pipeline; run locally on demand). Fails on a known advisory.
|
||||||
ci/osv_scan.sh
|
ci/osv_scan.sh
|
||||||
|
|
||||||
|
.PHONY: dugite-check
|
||||||
|
dugite-check: ## Track dugite-native (bundled git) upstream releases for security drift (T-88 / D-59). Run quarterly, or on a git CVE. Informational, not a gate.
|
||||||
|
ci/check_dugite_version.sh
|
||||||
|
|
||||||
# -- pre-push gate --------------------------------------------------------
|
# -- pre-push gate --------------------------------------------------------
|
||||||
|
|
||||||
.PHONY: decisions-validate
|
.PHONY: decisions-validate
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 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
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
{
|
||||||
|
"tab.title": { "translation": "Claude" },
|
||||||
|
"status.attaching": { "translation": "attaching…" },
|
||||||
|
"status.no-tmux": { "translation": "no-tmux · fresh every launch" },
|
||||||
|
"status.exited": { "translation": "session exited" },
|
||||||
|
"status.primary-exited": { "translation": "session exited — restart clide to retry" },
|
||||||
|
"banner.title": { "translation": "Claude" },
|
||||||
|
"banner.warmingUp": { "translation": "Warming up — your conversation will appear here." },
|
||||||
|
"banner.role.primary": { "translation": "primary" },
|
||||||
|
"banner.role.secondary": { "translation": "session {index}" },
|
||||||
|
"composer.hint": { "translation": "Message Claude… (Enter to send · Shift+Enter for newline)" },
|
||||||
|
"composer.stop": { "translation": "Stop ⎋" },
|
||||||
|
"composer.stop.hint": { "translation": "Interrupt the running turn (Escape)" },
|
||||||
|
"composer.removeAttachment": { "translation": "Remove {name}" },
|
||||||
|
"pane.title.primary": { "translation": "claude — primary" },
|
||||||
|
"pane.title.secondary": { "translation": "claude — secondary {index}" },
|
||||||
|
"pane.starting": { "translation": "starting…" },
|
||||||
|
"pane.modeBadge.semantics": { "translation": "permission mode: {mode}" },
|
||||||
|
"running.semantics": { "translation": "Claude is running" },
|
||||||
|
"running.verb.pondering": { "translation": "Pondering" },
|
||||||
|
"running.verb.conjuring": { "translation": "Conjuring" },
|
||||||
|
"running.verb.brewing": { "translation": "Brewing" },
|
||||||
|
"running.verb.tinkering": { "translation": "Tinkering" },
|
||||||
|
"running.verb.noodling": { "translation": "Noodling" },
|
||||||
|
"running.verb.percolating": { "translation": "Percolating" },
|
||||||
|
"running.verb.computing": { "translation": "Computing" },
|
||||||
|
"running.verb.wrangling": { "translation": "Wrangling" },
|
||||||
|
"running.verb.untangling": { "translation": "Untangling" },
|
||||||
|
"running.verb.synthesizing": { "translation": "Synthesizing" },
|
||||||
|
"running.verb.cogitating": { "translation": "Cogitating" },
|
||||||
|
"running.verb.whirring": { "translation": "Whirring" },
|
||||||
|
"running.verb.mincing": { "translation": "Mincing" },
|
||||||
|
"running.verb.boiling": { "translation": "Boiling" },
|
||||||
|
"running.verb.humming": { "translation": "Humming" },
|
||||||
|
"running.verb.buzzing": { "translation": "Buzzing" },
|
||||||
|
"running.verb.magicking": { "translation": "Magicking" },
|
||||||
|
"running.verb.cliding": { "translation": "Cliding" },
|
||||||
|
"running.verb.zooming": { "translation": "Zooming" },
|
||||||
|
"running.verb.bouncing": { "translation": "Bouncing" },
|
||||||
|
"conversation.empty": { "translation": "Waiting for Claude…" },
|
||||||
|
"conversation.label.you": { "translation": "you" },
|
||||||
|
"conversation.label.claude": { "translation": "claude" },
|
||||||
|
"conversation.label.clide": { "translation": "clide" },
|
||||||
|
"conversation.label.agent": { "translation": "agent" },
|
||||||
|
"conversation.label.agentPrompt": { "translation": "agent prompt" },
|
||||||
|
"conversation.label.context": { "translation": "context" },
|
||||||
|
"conversation.label.thinking": { "translation": "thinking" },
|
||||||
|
"conversation.label.agentThinking": { "translation": "agent thinking" },
|
||||||
|
"conversation.label.image": { "translation": "image" },
|
||||||
|
"conversation.label.icon": { "translation": "icons" },
|
||||||
|
"conversation.label.drawing": { "translation": "drawing" },
|
||||||
|
"conversation.draw.viewSource": { "translation": "view d2 source" },
|
||||||
|
"conversation.label.agentRun": { "translation": "agent run" },
|
||||||
|
"conversation.label.workflow": { "translation": "workflow" },
|
||||||
|
"conversation.label.error": { "translation": "error" },
|
||||||
|
"conversation.label.result": { "translation": "result" },
|
||||||
|
"conversation.label.denied": { "translation": "denied" },
|
||||||
|
"conversation.segment.prompt": { "translation": "prompt" },
|
||||||
|
"conversation.segment.result": { "translation": "result" },
|
||||||
|
"conversation.segment.liveTail": { "translation": "live tail" },
|
||||||
|
"conversation.segment.usage": { "translation": "usage" },
|
||||||
|
"conversation.segment.script": { "translation": "script" },
|
||||||
|
"conversation.workflow.launching": { "translation": "Launching…" },
|
||||||
|
"conversation.imagePlaceholder": { "translation": "could not load {path}" },
|
||||||
|
"conversation.bashTail.empty": { "translation": "no independent source to follow" },
|
||||||
|
"conversation.bashTail.label": { "translation": "live tail" },
|
||||||
|
"conversation.cluster.activity": { "translation": "Activity" },
|
||||||
|
"conversation.cluster.edits": { "translation": "Edits" },
|
||||||
|
"conversation.counter.step": { "translation": "1 step" },
|
||||||
|
"conversation.counter.steps": { "translation": "{count} steps" },
|
||||||
|
"conversation.counter.edit": { "translation": "1 edit" },
|
||||||
|
"conversation.counter.edits": { "translation": "{count} edits" },
|
||||||
|
"conversation.counter.starting": { "translation": "starting" },
|
||||||
|
"conversation.counter.agents": { "translation": "{done}/{total} agents" },
|
||||||
|
"prompt.permission.allow": { "translation": "1. Allow" },
|
||||||
|
"prompt.permission.allowRemember": { "translation": "2. Allow & don't ask again" },
|
||||||
|
"prompt.permission.deny": { "translation": "{n}. Deny" },
|
||||||
|
"prompt.permission.denySimplify": { "translation": "{n}. Deny & simplify" },
|
||||||
|
"prompt.permission.denySimplify.tooltip": { "translation": "Deny and ask Claude to retry this action in a simpler format — complex interactions don't work well with the permission system." },
|
||||||
|
"prompt.permission.note.placeholder": { "translation": "add a note (optional) — sent to Claude" },
|
||||||
|
"prompt.permission.label": { "translation": "permission · {name}" },
|
||||||
|
"prompt.question.label": { "translation": "question" },
|
||||||
|
"prompt.review.label": { "translation": "review" },
|
||||||
|
"prompt.review.title": { "translation": "Review your answers" },
|
||||||
|
"prompt.submit": { "translation": "Submit" },
|
||||||
|
"prompt.submitAnswers": { "translation": "Submit answers" },
|
||||||
|
"prompt.back": { "translation": "‹ Back" },
|
||||||
|
"prompt.next": { "translation": "Next ›" },
|
||||||
|
"prompt.reviewNav": { "translation": "Review ›" },
|
||||||
|
"prompt.nav.review": { "translation": "Review" },
|
||||||
|
"prompt.option.other": { "translation": "Other…" },
|
||||||
|
"prompt.other.placeholder": { "translation": "type your answer…" },
|
||||||
|
"prompt.note.placeholder": { "translation": "+ note (optional)" },
|
||||||
|
"prompt.chatInstead": { "translation": "chat instead" },
|
||||||
|
"tool.edit.before": { "translation": "— before" },
|
||||||
|
"tool.edit.after": { "translation": "+ after" },
|
||||||
|
"tool.bash.background": { "translation": "background" },
|
||||||
|
"tool.name.Read": { "translation": "Read" },
|
||||||
|
"tool.name.Edit": { "translation": "Edit" },
|
||||||
|
"tool.name.MultiEdit": { "translation": "MultiEdit" },
|
||||||
|
"tool.name.Write": { "translation": "Write" },
|
||||||
|
"tool.name.NotebookEdit": { "translation": "NotebookEdit" },
|
||||||
|
"tool.name.WebFetch": { "translation": "WebFetch" },
|
||||||
|
"tool.name.WebSearch": { "translation": "WebSearch" },
|
||||||
|
"tool.name.Task": { "translation": "Task" },
|
||||||
|
"tool.name.TodoWrite": { "translation": "TodoWrite" },
|
||||||
|
"tool.name.ExitPlanMode": { "translation": "ExitPlanMode" },
|
||||||
|
"permissionControl.semantics": { "translation": "permission mode: {mode}. Activate to change." },
|
||||||
|
"permissionControl.tooltip": { "translation": "Permission mode: {mode} — change (Ctrl/Cmd+M cycles)" },
|
||||||
|
"permissionBadge.tooltip": { "translation": "Permission mode: {mode}. Click to cycle default/acceptEdits/plan; Shift-click for bypassPermissions." },
|
||||||
|
"permissionBadge.semantics": { "translation": "Permission mode: {label}" },
|
||||||
|
"card.expand": { "translation": "Expand" },
|
||||||
|
"card.collapse": { "translation": "Collapse" },
|
||||||
|
"card.succeeded": { "translation": "succeeded" },
|
||||||
|
"card.failed": { "translation": "failed" },
|
||||||
|
"card.copy": { "translation": "copy" },
|
||||||
|
"taskDock.summary": { "translation": "{count} {tasks} · {done} done" },
|
||||||
|
"taskDock.task.singular": { "translation": "task" },
|
||||||
|
"taskDock.task.plural": { "translation": "tasks" },
|
||||||
|
"taskDock.collapse": { "translation": "Collapse tasks" },
|
||||||
|
"taskDock.expand": { "translation": "Expand tasks" },
|
||||||
|
"taskDock.semantics": { "translation": "Claude task list, {summary}, {state}" },
|
||||||
|
"taskDock.state.expanded": { "translation": "expanded" },
|
||||||
|
"taskDock.state.collapsed": { "translation": "collapsed" },
|
||||||
|
"taskDock.status.done": { "translation": "done" },
|
||||||
|
"taskDock.status.inProgress": { "translation": "in progress" },
|
||||||
|
"taskDock.status.pending": { "translation": "pending" },
|
||||||
|
"taskDock.row.semantics": { "translation": "{text}, {status}" },
|
||||||
|
"sessionPicker.title": { "translation": "Resume a Claude session" },
|
||||||
|
"sessionPicker.empty": { "translation": "No sessions found for this workspace." },
|
||||||
|
"modelPicker.cancel": { "translation": "cancel" },
|
||||||
|
"image.semantics": { "translation": "Image {name}" },
|
||||||
|
"activity.section.session": { "translation": "SESSION" },
|
||||||
|
"activity.control.clear": { "translation": "clear" },
|
||||||
|
"activity.control.compact": { "translation": "compact" },
|
||||||
|
"activity.control.fork": { "translation": "fork" },
|
||||||
|
"activity.control.resume": { "translation": "resume" },
|
||||||
|
"activity.control.refreshUsage": { "translation": "refresh usage" },
|
||||||
|
"activity.control.semantics": { "translation": "{label} session" },
|
||||||
|
"activity.control.tooltip": { "translation": "{label} · {command}" },
|
||||||
|
"activity.empty": { "translation": "No activity recorded yet." },
|
||||||
|
"activity.section.workflows": { "translation": "WORKFLOWS" },
|
||||||
|
"activity.workflow.fallback": { "translation": "workflow" },
|
||||||
|
"activity.workflow.done": { "translation": "done" },
|
||||||
|
"activity.workflow.starting": { "translation": "starting" },
|
||||||
|
"activity.section.usage": { "translation": "USAGE" },
|
||||||
|
"activity.row.session": { "translation": "session" },
|
||||||
|
"activity.row.weekAll": { "translation": "week (all)" },
|
||||||
|
"activity.row.weekSonnet": { "translation": "week (sonnet)" },
|
||||||
|
"activity.section.today": { "translation": "TODAY" },
|
||||||
|
"activity.row.messages": { "translation": "messages" },
|
||||||
|
"activity.row.sessions": { "translation": "sessions" },
|
||||||
|
"activity.row.toolCalls": { "translation": "tool calls" },
|
||||||
|
"activity.section.lifetime": { "translation": "LIFETIME" },
|
||||||
|
"activity.section.runtime": { "translation": "RUNTIME · primary" },
|
||||||
|
"activity.row.model": { "translation": "model" },
|
||||||
|
"activity.row.effort": { "translation": "effort" },
|
||||||
|
"activity.row.context": { "translation": "context" },
|
||||||
|
"activity.row.mode": { "translation": "mode" },
|
||||||
|
"activity.row.skills": { "translation": "skills" },
|
||||||
|
"config.empty": { "translation": "Claude environment not loaded." },
|
||||||
|
"config.section.settings": { "translation": "SETTINGS" },
|
||||||
|
"config.row.model": { "translation": "model" },
|
||||||
|
"config.row.effort": { "translation": "effort" },
|
||||||
|
"config.row.permissionMode": { "translation": "permission mode" },
|
||||||
|
"config.row.outputStyle": { "translation": "output style" },
|
||||||
|
"config.row.source": { "translation": "source" },
|
||||||
|
"config.row.source.value": { "translation": "~/.claude + .claude" },
|
||||||
|
"config.footer": { "translation": "expand a list to see all · click a skill/agent/command → opens its .md" },
|
||||||
|
"config.section.skills": { "translation": "SKILLS" },
|
||||||
|
"config.section.agents": { "translation": "AGENTS" },
|
||||||
|
"config.section.commands": { "translation": "COMMANDS" },
|
||||||
|
"config.section.hooks": { "translation": "HOOKS" },
|
||||||
|
"config.section.permissions": { "translation": "PERMISSIONS" },
|
||||||
|
"config.section.mcpServers": { "translation": "MCP SERVERS" },
|
||||||
|
"config.perm.allow": { "translation": "allow" },
|
||||||
|
"config.perm.ask": { "translation": "ask" },
|
||||||
|
"config.perm.deny": { "translation": "deny" },
|
||||||
|
"config.control.semantics": { "translation": "{label}: {value}. Click to change." },
|
||||||
|
"config.control.tooltip": { "translation": "change {label}" },
|
||||||
|
"config.control.option.semantics": { "translation": "{label}: {name}" },
|
||||||
|
"roster.bypass.confirmBody": { "translation": "Enable bypassPermissions? All tool calls will be auto-allowed." },
|
||||||
|
"roster.bypass.confirm.semantics": { "translation": "Confirm bypass" },
|
||||||
|
"roster.bypass.confirm.tooltip": { "translation": "Confirm" },
|
||||||
|
"roster.bypass.ok": { "translation": "OK" },
|
||||||
|
"roster.bypass.cancel.semantics": { "translation": "Cancel bypass" },
|
||||||
|
"roster.bypass.cancel.tooltip": { "translation": "Cancel" },
|
||||||
|
"roster.bypass.cancel": { "translation": "Cancel" },
|
||||||
|
"roster.hidePane": { "translation": "Hide pane" },
|
||||||
|
"roster.showPane": { "translation": "Show pane" },
|
||||||
|
"roster.unmute": { "translation": "Unmute messages" },
|
||||||
|
"roster.mute": { "translation": "Mute messages" },
|
||||||
|
"roster.inject": { "translation": "Inject message" },
|
||||||
|
"roster.fork": { "translation": "Fork session" },
|
||||||
|
"roster.close": { "translation": "Close session" },
|
||||||
|
"roster.inject.cancel": { "translation": "Cancel" },
|
||||||
|
"taskRow.reassign": { "translation": "Reassign task" },
|
||||||
|
"team.empty": { "translation": "No team active." },
|
||||||
|
"team.section.tasks": { "translation": "TASKS" },
|
||||||
|
"tabStrip.activity": { "translation": "Activity" },
|
||||||
|
"tabStrip.team": { "translation": "Team" },
|
||||||
|
"tabStrip.team.count": { "translation": "Team · {count}" },
|
||||||
|
"tabStrip.config": { "translation": "Config" },
|
||||||
|
"teamChat.section.messages": { "translation": "MESSAGES" },
|
||||||
|
"teamChat.popOut.semantics": { "translation": "Open full chat pane" },
|
||||||
|
"teamChat.popOut.tooltip": { "translation": "Open full chat" },
|
||||||
|
"teamChat.empty": { "translation": "No messages yet." },
|
||||||
|
"teamChat.composer.placeholder": { "translation": "@name or @team …" },
|
||||||
|
"teamChat.pane.title": { "translation": "Team Chat" },
|
||||||
|
"teamChat.interrupt.semantics": { "translation": "Interrupt target session" },
|
||||||
|
"teamChat.interrupt.label": { "translation": "Interrupt" },
|
||||||
|
"command.newSecondary": { "translation": "Claude: open a secondary session" },
|
||||||
|
"command.killAllSessions": { "translation": "Claude: kill all sessions for this repo" },
|
||||||
|
"command.sessionStorage": { "translation": "Claude: session storage (disk usage + cleanup)" },
|
||||||
|
"command.activity.foldLevel": { "translation": "Claude: cycle activity fold level" },
|
||||||
|
"command.agent.show": { "translation": "Claude: show an agent session pane" },
|
||||||
|
"command.agent.hide": { "translation": "Claude: hide an agent session pane" },
|
||||||
|
"command.agent.close": { "translation": "Claude: close (kill) an agent session" },
|
||||||
|
"command.agent.mute": { "translation": "Claude: mute broker delivery to an agent session" },
|
||||||
|
"command.agent.unmute": { "translation": "Claude: unmute broker delivery to an agent session" },
|
||||||
|
"command.agent.injectMessage": { "translation": "Claude: inject a text turn into an agent session" },
|
||||||
|
"command.agent.setPermissionMode": { "translation": "Claude: set permission mode for an agent session" },
|
||||||
|
"command.mode.cycle": { "translation": "Claude: Cycle permission mode" },
|
||||||
|
"command.task.reassign": { "translation": "Claude: reassign a shared task to an agent" },
|
||||||
|
"command.teamChat.open": { "translation": "Claude: open the team chat pane" },
|
||||||
|
"command.teamChat.post": { "translation": "Claude: post a message into the team channel as the user" },
|
||||||
|
"command.agent.fork": { "translation": "Claude: fork a managed session into a new branch session" },
|
||||||
|
"settings.activity.title": { "translation": "Activity" },
|
||||||
|
"settings.activity.conversation.label": { "translation": "Conversation" },
|
||||||
|
"settings.activity.foldLevel.label": { "translation": "Fold level" },
|
||||||
|
"settings.activity.foldLevel.help": { "translation": "How aggressively the conversation folds tool calls, thinking, and results." },
|
||||||
|
"settings.activity.opt.none": { "translation": "Show everything" },
|
||||||
|
"settings.activity.opt.tools": { "translation": "Fold tool calls" },
|
||||||
|
"settings.activity.opt.thinking": { "translation": "Fold tools + thinking" },
|
||||||
|
"settings.activity.opt.everything": { "translation": "Fold all but prose" },
|
||||||
|
"settings.claude.title": { "translation": "Claude" },
|
||||||
|
"settings.claude.newSessionDefaults.label": { "translation": "New session defaults" },
|
||||||
|
"settings.claude.model.label": { "translation": "Model" },
|
||||||
|
"settings.claude.model.help": { "translation": "Model for new sessions." },
|
||||||
|
"settings.claude.effort.label": { "translation": "Effort" },
|
||||||
|
"settings.claude.effort.help": { "translation": "Reasoning effort for new sessions (applied via --effort at spawn)." },
|
||||||
|
"settings.claude.permissionMode.label": { "translation": "Permission mode" },
|
||||||
|
"settings.claude.permissionMode.help": { "translation": "Starting permission mode for new sessions." },
|
||||||
|
"settings.claude.account.label": { "translation": "Account" },
|
||||||
|
"settings.claude.account.registry.label": { "translation": "Accounts" },
|
||||||
|
"settings.claude.account.registry.help": { "translation": "Registered Claude accounts (each a separate config dir + login)." },
|
||||||
|
"settings.claude.account.workspace.label": { "translation": "Account for this workspace" },
|
||||||
|
"settings.claude.account.workspace.help": { "translation": "Which Claude account this repo runs under; Default uses the system login." }
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"command.clide.installCli": { "translation": "clide: Install 'clide' command in PATH" }
|
||||||
|
}
|
||||||
@@ -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" }
|
||||||
|
}
|
||||||
@@ -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" }
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"notice.title": { "translation": "Extension management is coming" },
|
"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.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" }
|
"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,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." }
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"command.open": { "translation": "Settings…" },
|
||||||
"modal.title": { "translation": "Settings" },
|
"modal.title": { "translation": "Settings" },
|
||||||
"modal.close": { "translation": "Close" },
|
"modal.close": { "translation": "Close" },
|
||||||
"modal.close.hint": { "translation": "Close settings without changing anything" },
|
"modal.close.hint": { "translation": "Close settings without changing anything" },
|
||||||
@@ -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,11 @@
|
|||||||
|
{
|
||||||
|
"command.detect": { "translation": "Re-detect tool paths" },
|
||||||
|
"settings.title": { "translation": "Tools" },
|
||||||
|
"settings.section.binaries": { "translation": "Supporter binaries" },
|
||||||
|
"settings.field.claude.label": { "translation": "Claude CLI" },
|
||||||
|
"settings.field.claude.help": { "translation": "Absolute path to the claude binary; blank to auto-resolve." },
|
||||||
|
"settings.field.d2.label": { "translation": "d2" },
|
||||||
|
"settings.field.d2.help": { "translation": "Absolute path to the d2 diagram compiler; blank to auto-resolve." },
|
||||||
|
"settings.field.detect.label": { "translation": "Re-detect" },
|
||||||
|
"settings.field.detect.help": { "translation": "Re-scan PATH and the common install dirs, overwriting the paths above." }
|
||||||
|
}
|
||||||
@@ -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,249 @@
|
|||||||
|
{
|
||||||
|
"tab.title": { "translation": "Claude" },
|
||||||
|
"status.attaching": { "translation": "verbinden…" },
|
||||||
|
"status.no-tmux": { "translation": "no-tmux · elke start opnieuw" },
|
||||||
|
"status.exited": { "translation": "sessie beëindigd" },
|
||||||
|
"status.primary-exited": { "translation": "sessie beëindigd — herstart clide om opnieuw te proberen" },
|
||||||
|
"banner.title": { "translation": "Claude" },
|
||||||
|
"banner.warmingUp": { "translation": "Opstarten — je gesprek verschijnt hier." },
|
||||||
|
"banner.role.primary": { "translation": "primair" },
|
||||||
|
"banner.role.secondary": { "translation": "sessie {index}" },
|
||||||
|
"composer.hint": { "translation": "Bericht aan Claude… (Enter om te verzenden · Shift+Enter voor een nieuwe regel)" },
|
||||||
|
"composer.stop": { "translation": "Stop ⎋" },
|
||||||
|
"composer.stop.hint": { "translation": "Onderbreek de lopende beurt (Escape)" },
|
||||||
|
"composer.removeAttachment": { "translation": "{name} verwijderen" },
|
||||||
|
"pane.title.primary": { "translation": "claude — primair" },
|
||||||
|
"pane.title.secondary": { "translation": "claude — secundair {index}" },
|
||||||
|
"pane.starting": { "translation": "starten…" },
|
||||||
|
"pane.modeBadge.semantics": { "translation": "permissiemodus: {mode}" },
|
||||||
|
"running.semantics": { "translation": "Claude is bezig" },
|
||||||
|
"running.verb.pondering": { "translation": "Peinzen" },
|
||||||
|
"running.verb.conjuring": { "translation": "Toveren" },
|
||||||
|
"running.verb.brewing": { "translation": "Brouwen" },
|
||||||
|
"running.verb.tinkering": { "translation": "Knutselen" },
|
||||||
|
"running.verb.noodling": { "translation": "Prutsen" },
|
||||||
|
"running.verb.percolating": { "translation": "Pruttelen" },
|
||||||
|
"running.verb.computing": { "translation": "Rekenen" },
|
||||||
|
"running.verb.wrangling": { "translation": "Worstelen" },
|
||||||
|
"running.verb.untangling": { "translation": "Ontwarren" },
|
||||||
|
"running.verb.synthesizing": { "translation": "Synthetiseren" },
|
||||||
|
"running.verb.cogitating": { "translation": "Overpeinzen" },
|
||||||
|
"running.verb.whirring": { "translation": "Snorren" },
|
||||||
|
"running.verb.mincing": { "translation": "Hakken" },
|
||||||
|
"running.verb.boiling": { "translation": "Koken" },
|
||||||
|
"running.verb.humming": { "translation": "Neuriën" },
|
||||||
|
"running.verb.buzzing": { "translation": "Zoemen" },
|
||||||
|
"running.verb.magicking": { "translation": "Goochelen" },
|
||||||
|
"running.verb.cliding": { "translation": "Cliden" },
|
||||||
|
"running.verb.zooming": { "translation": "Zoeven" },
|
||||||
|
"running.verb.bouncing": { "translation": "Stuiteren" },
|
||||||
|
"conversation.empty": { "translation": "Wachten op Claude…" },
|
||||||
|
"conversation.label.you": { "translation": "jij" },
|
||||||
|
"conversation.label.claude": { "translation": "claude" },
|
||||||
|
"conversation.label.clide": { "translation": "clide" },
|
||||||
|
"conversation.label.agent": { "translation": "agent" },
|
||||||
|
"conversation.label.agentPrompt": { "translation": "agent-prompt" },
|
||||||
|
"conversation.label.context": { "translation": "context" },
|
||||||
|
"conversation.label.thinking": { "translation": "nadenken" },
|
||||||
|
"conversation.label.agentThinking": { "translation": "agent denkt na" },
|
||||||
|
"conversation.label.image": { "translation": "afbeelding" },
|
||||||
|
"conversation.label.icon": { "translation": "iconen" },
|
||||||
|
"conversation.label.drawing": { "translation": "tekening" },
|
||||||
|
"conversation.draw.viewSource": { "translation": "d2-bron tonen" },
|
||||||
|
"conversation.label.agentRun": { "translation": "agent-uitvoering" },
|
||||||
|
"conversation.label.workflow": { "translation": "workflow" },
|
||||||
|
"conversation.label.error": { "translation": "fout" },
|
||||||
|
"conversation.label.result": { "translation": "resultaat" },
|
||||||
|
"conversation.label.denied": { "translation": "geweigerd" },
|
||||||
|
"conversation.segment.prompt": { "translation": "prompt" },
|
||||||
|
"conversation.segment.result": { "translation": "resultaat" },
|
||||||
|
"conversation.segment.liveTail": { "translation": "live tail" },
|
||||||
|
"conversation.segment.usage": { "translation": "verbruik" },
|
||||||
|
"conversation.segment.script": { "translation": "script" },
|
||||||
|
"conversation.workflow.launching": { "translation": "Starten…" },
|
||||||
|
"conversation.imagePlaceholder": { "translation": "kon {path} niet laden" },
|
||||||
|
"conversation.bashTail.empty": { "translation": "geen onafhankelijke bron om te volgen" },
|
||||||
|
"conversation.bashTail.label": { "translation": "live tail" },
|
||||||
|
"conversation.cluster.activity": { "translation": "Activiteit" },
|
||||||
|
"conversation.cluster.edits": { "translation": "Wijzigingen" },
|
||||||
|
"conversation.counter.step": { "translation": "1 stap" },
|
||||||
|
"conversation.counter.steps": { "translation": "{count} stappen" },
|
||||||
|
"conversation.counter.edit": { "translation": "1 wijziging" },
|
||||||
|
"conversation.counter.edits": { "translation": "{count} wijzigingen" },
|
||||||
|
"conversation.counter.starting": { "translation": "starten" },
|
||||||
|
"conversation.counter.agents": { "translation": "{done}/{total} agents" },
|
||||||
|
"prompt.permission.allow": { "translation": "1. Toestaan" },
|
||||||
|
"prompt.permission.allowRemember": { "translation": "2. Toestaan en niet meer vragen" },
|
||||||
|
"prompt.permission.deny": { "translation": "{n}. Weigeren" },
|
||||||
|
"prompt.permission.denySimplify": { "translation": "{n}. Weigeren en vereenvoudigen" },
|
||||||
|
"prompt.permission.denySimplify.tooltip": { "translation": "Weiger en vraag Claude deze actie in een eenvoudigere vorm opnieuw te proberen — complexe interacties werken niet goed met het permissiesysteem." },
|
||||||
|
"prompt.permission.note.placeholder": { "translation": "voeg een notitie toe (optioneel) — wordt naar Claude gestuurd" },
|
||||||
|
"prompt.permission.label": { "translation": "permissie · {name}" },
|
||||||
|
"prompt.question.label": { "translation": "vraag" },
|
||||||
|
"prompt.review.label": { "translation": "controleren" },
|
||||||
|
"prompt.review.title": { "translation": "Controleer je antwoorden" },
|
||||||
|
"prompt.submit": { "translation": "Verzenden" },
|
||||||
|
"prompt.submitAnswers": { "translation": "Antwoorden verzenden" },
|
||||||
|
"prompt.back": { "translation": "‹ Terug" },
|
||||||
|
"prompt.next": { "translation": "Volgende ›" },
|
||||||
|
"prompt.reviewNav": { "translation": "Controleren ›" },
|
||||||
|
"prompt.nav.review": { "translation": "Controleren" },
|
||||||
|
"prompt.option.other": { "translation": "Anders…" },
|
||||||
|
"prompt.other.placeholder": { "translation": "typ je antwoord…" },
|
||||||
|
"prompt.note.placeholder": { "translation": "+ notitie (optioneel)" },
|
||||||
|
"prompt.chatInstead": { "translation": "in plaats daarvan chatten" },
|
||||||
|
"tool.edit.before": { "translation": "— voor" },
|
||||||
|
"tool.edit.after": { "translation": "+ na" },
|
||||||
|
"tool.bash.background": { "translation": "achtergrond" },
|
||||||
|
"tool.name.Read": { "translation": "Lezen" },
|
||||||
|
"tool.name.Edit": { "translation": "Bewerken" },
|
||||||
|
"tool.name.MultiEdit": { "translation": "Meervoudig bewerken" },
|
||||||
|
"tool.name.Write": { "translation": "Schrijven" },
|
||||||
|
"tool.name.NotebookEdit": { "translation": "Notebook bewerken" },
|
||||||
|
"tool.name.WebFetch": { "translation": "Web ophalen" },
|
||||||
|
"tool.name.WebSearch": { "translation": "Web zoeken" },
|
||||||
|
"tool.name.Task": { "translation": "Taak" },
|
||||||
|
"tool.name.TodoWrite": { "translation": "Takenlijst" },
|
||||||
|
"tool.name.ExitPlanMode": { "translation": "Planmodus verlaten" },
|
||||||
|
"permissionControl.semantics": { "translation": "permissiemodus: {mode}. Activeer om te wijzigen." },
|
||||||
|
"permissionControl.tooltip": { "translation": "Permissiemodus: {mode} — wijzigen (Ctrl/Cmd+M wisselt)" },
|
||||||
|
"permissionBadge.tooltip": { "translation": "Permissiemodus: {mode}. Klik om te wisselen tussen default/acceptEdits/plan; Shift-klik voor bypassPermissions." },
|
||||||
|
"permissionBadge.semantics": { "translation": "Permissiemodus: {label}" },
|
||||||
|
"card.expand": { "translation": "Uitvouwen" },
|
||||||
|
"card.collapse": { "translation": "Invouwen" },
|
||||||
|
"card.succeeded": { "translation": "gelukt" },
|
||||||
|
"card.failed": { "translation": "mislukt" },
|
||||||
|
"card.copy": { "translation": "kopiëren" },
|
||||||
|
"taskDock.summary": { "translation": "{count} {tasks} · {done} klaar" },
|
||||||
|
"taskDock.task.singular": { "translation": "taak" },
|
||||||
|
"taskDock.task.plural": { "translation": "taken" },
|
||||||
|
"taskDock.collapse": { "translation": "Taken invouwen" },
|
||||||
|
"taskDock.expand": { "translation": "Taken uitvouwen" },
|
||||||
|
"taskDock.semantics": { "translation": "Takenlijst van Claude, {summary}, {state}" },
|
||||||
|
"taskDock.state.expanded": { "translation": "uitgevouwen" },
|
||||||
|
"taskDock.state.collapsed": { "translation": "ingevouwen" },
|
||||||
|
"taskDock.status.done": { "translation": "klaar" },
|
||||||
|
"taskDock.status.inProgress": { "translation": "bezig" },
|
||||||
|
"taskDock.status.pending": { "translation": "in wachtrij" },
|
||||||
|
"taskDock.row.semantics": { "translation": "{text}, {status}" },
|
||||||
|
"sessionPicker.title": { "translation": "Een Claude-sessie hervatten" },
|
||||||
|
"sessionPicker.empty": { "translation": "Geen sessies gevonden voor deze workspace." },
|
||||||
|
"modelPicker.cancel": { "translation": "annuleren" },
|
||||||
|
"image.semantics": { "translation": "Afbeelding {name}" },
|
||||||
|
"activity.section.session": { "translation": "SESSIE" },
|
||||||
|
"activity.control.clear": { "translation": "wissen" },
|
||||||
|
"activity.control.compact": { "translation": "compact" },
|
||||||
|
"activity.control.fork": { "translation": "fork" },
|
||||||
|
"activity.control.resume": { "translation": "hervatten" },
|
||||||
|
"activity.control.refreshUsage": { "translation": "verbruik vernieuwen" },
|
||||||
|
"activity.control.semantics": { "translation": "{label} sessie" },
|
||||||
|
"activity.control.tooltip": { "translation": "{label} · {command}" },
|
||||||
|
"activity.empty": { "translation": "Nog geen activiteit vastgelegd." },
|
||||||
|
"activity.section.workflows": { "translation": "WORKFLOWS" },
|
||||||
|
"activity.workflow.fallback": { "translation": "workflow" },
|
||||||
|
"activity.workflow.done": { "translation": "klaar" },
|
||||||
|
"activity.workflow.starting": { "translation": "starten" },
|
||||||
|
"activity.section.usage": { "translation": "VERBRUIK" },
|
||||||
|
"activity.row.session": { "translation": "sessie" },
|
||||||
|
"activity.row.weekAll": { "translation": "week (alles)" },
|
||||||
|
"activity.row.weekSonnet": { "translation": "week (sonnet)" },
|
||||||
|
"activity.section.today": { "translation": "VANDAAG" },
|
||||||
|
"activity.row.messages": { "translation": "berichten" },
|
||||||
|
"activity.row.sessions": { "translation": "sessies" },
|
||||||
|
"activity.row.toolCalls": { "translation": "tool-aanroepen" },
|
||||||
|
"activity.section.lifetime": { "translation": "TOTAAL" },
|
||||||
|
"activity.section.runtime": { "translation": "RUNTIME · primair" },
|
||||||
|
"activity.row.model": { "translation": "model" },
|
||||||
|
"activity.row.effort": { "translation": "effort" },
|
||||||
|
"activity.row.context": { "translation": "context" },
|
||||||
|
"activity.row.mode": { "translation": "modus" },
|
||||||
|
"activity.row.skills": { "translation": "skills" },
|
||||||
|
"config.empty": { "translation": "Claude-omgeving niet geladen." },
|
||||||
|
"config.section.settings": { "translation": "INSTELLINGEN" },
|
||||||
|
"config.row.model": { "translation": "model" },
|
||||||
|
"config.row.effort": { "translation": "effort" },
|
||||||
|
"config.row.permissionMode": { "translation": "permissiemodus" },
|
||||||
|
"config.row.outputStyle": { "translation": "uitvoerstijl" },
|
||||||
|
"config.row.source": { "translation": "bron" },
|
||||||
|
"config.row.source.value": { "translation": "~/.claude + .claude" },
|
||||||
|
"config.footer": { "translation": "vouw een lijst uit om alles te zien · klik op een skill/agent/command → opent de .md" },
|
||||||
|
"config.section.skills": { "translation": "SKILLS" },
|
||||||
|
"config.section.agents": { "translation": "AGENTS" },
|
||||||
|
"config.section.commands": { "translation": "COMMANDS" },
|
||||||
|
"config.section.hooks": { "translation": "HOOKS" },
|
||||||
|
"config.section.permissions": { "translation": "PERMISSIES" },
|
||||||
|
"config.section.mcpServers": { "translation": "MCP SERVERS" },
|
||||||
|
"config.perm.allow": { "translation": "toestaan" },
|
||||||
|
"config.perm.ask": { "translation": "vragen" },
|
||||||
|
"config.perm.deny": { "translation": "weigeren" },
|
||||||
|
"config.control.semantics": { "translation": "{label}: {value}. Klik om te wijzigen." },
|
||||||
|
"config.control.tooltip": { "translation": "{label} wijzigen" },
|
||||||
|
"config.control.option.semantics": { "translation": "{label}: {name}" },
|
||||||
|
"roster.bypass.confirmBody": { "translation": "bypassPermissions inschakelen? Alle tool-aanroepen worden automatisch toegestaan." },
|
||||||
|
"roster.bypass.confirm.semantics": { "translation": "Bypass bevestigen" },
|
||||||
|
"roster.bypass.confirm.tooltip": { "translation": "Bevestigen" },
|
||||||
|
"roster.bypass.ok": { "translation": "OK" },
|
||||||
|
"roster.bypass.cancel.semantics": { "translation": "Bypass annuleren" },
|
||||||
|
"roster.bypass.cancel.tooltip": { "translation": "Annuleren" },
|
||||||
|
"roster.bypass.cancel": { "translation": "Annuleren" },
|
||||||
|
"roster.hidePane": { "translation": "Paneel verbergen" },
|
||||||
|
"roster.showPane": { "translation": "Paneel tonen" },
|
||||||
|
"roster.unmute": { "translation": "Berichten weer aanzetten" },
|
||||||
|
"roster.mute": { "translation": "Berichten dempen" },
|
||||||
|
"roster.inject": { "translation": "Bericht injecteren" },
|
||||||
|
"roster.fork": { "translation": "Sessie forken" },
|
||||||
|
"roster.close": { "translation": "Sessie sluiten" },
|
||||||
|
"roster.inject.cancel": { "translation": "Annuleren" },
|
||||||
|
"taskRow.reassign": { "translation": "Taak opnieuw toewijzen" },
|
||||||
|
"team.empty": { "translation": "Geen team actief." },
|
||||||
|
"team.section.tasks": { "translation": "TAKEN" },
|
||||||
|
"tabStrip.activity": { "translation": "Activiteit" },
|
||||||
|
"tabStrip.team": { "translation": "Team" },
|
||||||
|
"tabStrip.team.count": { "translation": "Team · {count}" },
|
||||||
|
"tabStrip.config": { "translation": "Config" },
|
||||||
|
"teamChat.section.messages": { "translation": "BERICHTEN" },
|
||||||
|
"teamChat.popOut.semantics": { "translation": "Volledig chatpaneel openen" },
|
||||||
|
"teamChat.popOut.tooltip": { "translation": "Volledige chat openen" },
|
||||||
|
"teamChat.empty": { "translation": "Nog geen berichten." },
|
||||||
|
"teamChat.composer.placeholder": { "translation": "@naam of @team …" },
|
||||||
|
"teamChat.pane.title": { "translation": "Teamchat" },
|
||||||
|
"teamChat.interrupt.semantics": { "translation": "Doelsessie onderbreken" },
|
||||||
|
"teamChat.interrupt.label": { "translation": "Onderbreken" },
|
||||||
|
"command.newSecondary": { "translation": "Claude: een secundaire sessie openen" },
|
||||||
|
"command.killAllSessions": { "translation": "Claude: alle sessies voor deze repo afsluiten" },
|
||||||
|
"command.sessionStorage": { "translation": "Claude: sessieopslag (schijfgebruik + opschonen)" },
|
||||||
|
"command.activity.foldLevel": { "translation": "Claude: doorloop het invouwniveau van de activiteit" },
|
||||||
|
"command.agent.show": { "translation": "Claude: een agent-sessiepaneel tonen" },
|
||||||
|
"command.agent.hide": { "translation": "Claude: een agent-sessiepaneel verbergen" },
|
||||||
|
"command.agent.close": { "translation": "Claude: een agent-sessie sluiten (afsluiten)" },
|
||||||
|
"command.agent.mute": { "translation": "Claude: broker-aflevering naar een agent-sessie dempen" },
|
||||||
|
"command.agent.unmute": { "translation": "Claude: broker-aflevering naar een agent-sessie weer aanzetten" },
|
||||||
|
"command.agent.injectMessage": { "translation": "Claude: een tekstbeurt in een agent-sessie injecteren" },
|
||||||
|
"command.agent.setPermissionMode": { "translation": "Claude: permissiemodus voor een agent-sessie instellen" },
|
||||||
|
"command.mode.cycle": { "translation": "Claude: Permissiemodus doorlopen" },
|
||||||
|
"command.task.reassign": { "translation": "Claude: een gedeelde taak opnieuw aan een agent toewijzen" },
|
||||||
|
"command.teamChat.open": { "translation": "Claude: het teamchatpaneel openen" },
|
||||||
|
"command.teamChat.post": { "translation": "Claude: een bericht als gebruiker in het teamkanaal plaatsen" },
|
||||||
|
"command.agent.fork": { "translation": "Claude: een beheerde sessie forken naar een nieuwe branch-sessie" },
|
||||||
|
"settings.activity.title": { "translation": "Activiteit" },
|
||||||
|
"settings.activity.conversation.label": { "translation": "Gesprek" },
|
||||||
|
"settings.activity.foldLevel.label": { "translation": "Invouwniveau" },
|
||||||
|
"settings.activity.foldLevel.help": { "translation": "Hoe agressief het gesprek tool-aanroepen, nadenken en resultaten invouwt." },
|
||||||
|
"settings.activity.opt.none": { "translation": "Alles tonen" },
|
||||||
|
"settings.activity.opt.tools": { "translation": "Tool-aanroepen invouwen" },
|
||||||
|
"settings.activity.opt.thinking": { "translation": "Tools + nadenken invouwen" },
|
||||||
|
"settings.activity.opt.everything": { "translation": "Alles invouwen behalve tekst" },
|
||||||
|
"settings.claude.title": { "translation": "Claude" },
|
||||||
|
"settings.claude.newSessionDefaults.label": { "translation": "Standaardwaarden voor nieuwe sessies" },
|
||||||
|
"settings.claude.model.label": { "translation": "Model" },
|
||||||
|
"settings.claude.model.help": { "translation": "Model voor nieuwe sessies." },
|
||||||
|
"settings.claude.effort.label": { "translation": "Effort" },
|
||||||
|
"settings.claude.effort.help": { "translation": "Reasoning effort voor nieuwe sessies (toegepast via --effort bij het starten)." },
|
||||||
|
"settings.claude.permissionMode.label": { "translation": "Permissiemodus" },
|
||||||
|
"settings.claude.permissionMode.help": { "translation": "Begin-permissiemodus voor nieuwe sessies." },
|
||||||
|
"settings.claude.account.label": { "translation": "Account" },
|
||||||
|
"settings.claude.account.registry.label": { "translation": "Accounts" },
|
||||||
|
"settings.claude.account.registry.help": { "translation": "Geregistreerde Claude-accounts (elk een eigen configmap + login)." },
|
||||||
|
"settings.claude.account.workspace.label": { "translation": "Account voor deze werkmap" },
|
||||||
|
"settings.claude.account.workspace.help": { "translation": "Welk Claude-account deze repo gebruikt; Standaard gebruikt de systeemlogin." }
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"command.clide.installCli": { "translation": "clide: Installeer 'clide'-opdracht in PATH" }
|
||||||
|
}
|
||||||
@@ -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,6 @@
|
|||||||
|
{
|
||||||
|
"connected": { "translation": "verbonden" },
|
||||||
|
"connected.hint": { "translation": "backend-isolate is bereikbaar" },
|
||||||
|
"disconnected": { "translation": "niet verbonden" },
|
||||||
|
"disconnected.hint": { "translation": "backend-isolate draait niet" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"command.preset.default": { "translation": "Toetsindeling: Standaard" },
|
||||||
|
"command.preset.vim": { "translation": "Toetsindeling: Vim" },
|
||||||
|
"command.preset.vscode": { "translation": "Toetsindeling: VS Code" },
|
||||||
|
"command.preset.jetbrains": { "translation": "Toetsindeling: JetBrains" },
|
||||||
|
"settings.keymap.title": { "translation": "Toetsindeling" },
|
||||||
|
"settings.keymap.section.preset": { "translation": "Voorinstelling" },
|
||||||
|
"settings.keymap.field.preset.label": { "translation": "Actieve voorinstelling" },
|
||||||
|
"settings.keymap.field.preset.help": { "translation": "Toetsenbordindeling voor de hele app." },
|
||||||
|
"settings.keymap.field.preset.option.default": { "translation": "Standaard" },
|
||||||
|
"settings.keymap.field.preset.option.vim": { "translation": "Vim" },
|
||||||
|
"settings.keymap.field.preset.option.vscode": { "translation": "VS Code" },
|
||||||
|
"settings.keymap.field.preset.option.jetbrains": { "translation": "JetBrains" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"empty": { "translation": "Selecteer een .md-bestand om het hier te bekijken." },
|
||||||
|
"chrome.title": { "translation": "weergave" },
|
||||||
|
"subtitle.lines": { "translation": "{count} regels" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"menu.file": { "translation": "Bestand" },
|
||||||
|
"menu.view": { "translation": "Beeld" },
|
||||||
|
"menu.help": { "translation": "Help" },
|
||||||
|
"about.version": { "translation": "Versie" },
|
||||||
|
"about.commit": { "translation": "Commit" },
|
||||||
|
"about.built": { "translation": "Gebouwd" },
|
||||||
|
"about.repository": { "translation": "Repository" },
|
||||||
|
"about.checkUpdates": { "translation": "Controleer op updates" },
|
||||||
|
"about.checking": { "translation": "Bezig met controleren…" },
|
||||||
|
"about.upToDate": { "translation": "Je hebt de nieuwste versie." },
|
||||||
|
"about.updateAvailable": { "translation": "clide {version} is beschikbaar — releaseopmerkingen" },
|
||||||
|
"about.updateFailed": { "translation": "Kon niet op updates controleren" },
|
||||||
|
"licenses.heading": { "translation": "Meegeleverde afhankelijkheden" },
|
||||||
|
"licenses.unavailable": { "translation": "Licenties niet beschikbaar." },
|
||||||
|
"licenses.loading": { "translation": "Laden…" },
|
||||||
|
"button.close": { "translation": "Sluiten" },
|
||||||
|
"button.cancel": { "translation": "Annuleren" },
|
||||||
|
"button.open": { "translation": "Openen" },
|
||||||
|
"button.opening": { "translation": "Openen…" },
|
||||||
|
"button.ok": { "translation": "OK" },
|
||||||
|
"dialog.openProject.title": { "translation": "Project openen" },
|
||||||
|
"dialog.openProject.body": { "translation": "Voer het pad naar een git-repository in." },
|
||||||
|
"dialog.openProject.error": { "translation": "Geen git-repository" },
|
||||||
|
"dialog.notRepo.title": { "translation": "Geen git-repo gevonden" },
|
||||||
|
"dialog.notRepo.body": { "translation": "Een clide-projecthoofdmap vereist een git-repository." },
|
||||||
|
"command.file.openFolder": { "translation": "Bestand: Map openen…" },
|
||||||
|
"command.file.newWindow": { "translation": "Bestand: Nieuw venster" },
|
||||||
|
"command.file.closeWorkspace": { "translation": "Bestand: Project sluiten" },
|
||||||
|
"command.help.about": { "translation": "Help: Over clide" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"a11y.log": { "translation": "uitvoerlogboek" },
|
||||||
|
"empty": { "translation": "Nog geen uitvoer." },
|
||||||
|
"empty.filtered": { "translation": "Geen uitvoer komt overeen met het filter." },
|
||||||
|
"filter.hint": { "translation": "Filteren…" },
|
||||||
|
"chip.level": { "translation": "Niveau: {level}" },
|
||||||
|
"chip.source": { "translation": "Bron: {source}" },
|
||||||
|
"chip.clear": { "translation": "Wissen" },
|
||||||
|
"source.all": { "translation": "alle" },
|
||||||
|
"a11y.jumpToLatest": { "translation": "naar nieuwste springen" },
|
||||||
|
"jump.label": { "translation": "Naar nieuwste ↓" },
|
||||||
|
"a11y.toggleDock": { "translation": "uitvoerdok aan/uit" },
|
||||||
|
"dock.label": { "translation": "Uitvoer" },
|
||||||
|
"command.dock.toggle": { "translation": "Uitvoerdok aan/uit" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"tab.title": { "translation": "pql" },
|
||||||
|
"tab.links.title": { "translation": "Koppelingen" },
|
||||||
|
"filter.markdown.hint": { "translation": "Markdown filteren…" },
|
||||||
|
"loading": { "translation": "Laden…" },
|
||||||
|
"empty.markdown": { "translation": "Geen markdown-bestanden gevonden." },
|
||||||
|
"search.vault.hint": { "translation": "Vault doorzoeken…" },
|
||||||
|
"search.query.hint": { "translation": "PQL-query…" },
|
||||||
|
"backlinks.empty": { "translation": "Open een bestand om de koppelingen te zien." },
|
||||||
|
"backlinks.loading": { "translation": "Laden…" },
|
||||||
|
"backlinks.semantics": { "translation": "backlinks voor {path}" },
|
||||||
|
"group.backlinks": { "translation": "Backlinks" },
|
||||||
|
"group.outlinks": { "translation": "Uitgaande koppelingen" },
|
||||||
|
"group.label": { "translation": "{label} ({count})" },
|
||||||
|
"group.none": { "translation": "Geen" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"tab.title": { "translation": "Problemen" },
|
||||||
|
"semantics.panel": { "translation": "problemenpaneel" },
|
||||||
|
"filter.hint": { "translation": "Problemen filteren…" },
|
||||||
|
"count": { "translation": "Problemen ({count})" },
|
||||||
|
"refresh.label": { "translation": "Vernieuwen" },
|
||||||
|
"refresh.semantics": { "translation": "problemen vernieuwen" },
|
||||||
|
"scanning": { "translation": "Scannen…" },
|
||||||
|
"empty": { "translation": "Geen problemen gevonden." }
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"mode.find": { "translation": "Zoeken" },
|
||||||
|
"mode.vault": { "translation": "Kluis" },
|
||||||
|
"mode.query": { "translation": "Query" },
|
||||||
|
"mode.markdown": { "translation": "Markdown" },
|
||||||
|
"find.hint": { "translation": "Zoeken" },
|
||||||
|
"replace.hint": { "translation": "Vervangen" },
|
||||||
|
"include.hint": { "translation": "te includeren bestanden (bijv. *.dart)" },
|
||||||
|
"exclude.hint": { "translation": "uit te sluiten bestanden" },
|
||||||
|
"toggle.regex": { "translation": "Reguliere expressie" },
|
||||||
|
"toggle.caseInsensitive": { "translation": "Hoofdletterongevoelig" },
|
||||||
|
"status.searching": { "translation": "Zoeken…" },
|
||||||
|
"status.noResults": { "translation": "Geen resultaten" },
|
||||||
|
"status.counts": { "translation": "{matches} in {files}" },
|
||||||
|
"a11y.openMatch": { "translation": "{path} regel {line} openen" },
|
||||||
|
"button.replaceAll": { "translation": "Alles vervangen" },
|
||||||
|
"button.ok": { "translation": "OK" },
|
||||||
|
"button.cancel": { "translation": "Annuleren" },
|
||||||
|
"button.confirm": { "translation": "Bevestigen" },
|
||||||
|
"dialog.dirty.title": { "translation": "Werkboom niet schoon" },
|
||||||
|
"dialog.dirty.body": { "translation": "Commit of stash je wijzigingen voordat je vervangt — Git is de enige manier om ongedaan te maken." },
|
||||||
|
"dialog.confirm.body": { "translation": "{matches} overeenkomst(en) in {files} bestand(en) vervangen? Dit kan niet ongedaan worden gemaakt in clide." }
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"command.open": { "translation": "Instellingen…" },
|
||||||
|
"modal.title": { "translation": "Instellingen" },
|
||||||
|
"modal.close": { "translation": "Sluiten" },
|
||||||
|
"modal.close.hint": { "translation": "Sluit instellingen zonder iets te wijzigen" },
|
||||||
|
"rail.header": { "translation": "Categorieën" },
|
||||||
|
"panel.empty": { "translation": "Er zijn nog geen instellingscategorieën geregistreerd." },
|
||||||
|
"search.hint": { "translation": "Instellingen zoeken…" },
|
||||||
|
"search.empty": { "translation": "Geen instellingen komen overeen met je zoekopdracht." },
|
||||||
|
"scope.project": { "translation": "Dit project" },
|
||||||
|
"scope.always": { "translation": "Heel clide" },
|
||||||
|
"scope.default": { "translation": "Standaard" },
|
||||||
|
"scope.reset": { "translation": "Terugzetten naar standaard" },
|
||||||
|
"scope.tip.project": { "translation": "Opgeslagen in dit project (.clide)" },
|
||||||
|
"scope.tip.always": { "translation": "Opgeslagen voor heel clide (~/.clide)" },
|
||||||
|
"scope.tip.default": { "translation": "Niet ingesteld — standaard wordt gebruikt" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"tab.title": { "translation": "Terminal" },
|
||||||
|
"chrome.title": { "translation": "terminal" },
|
||||||
|
"subtitle.spawning": { "translation": "shell starten…" },
|
||||||
|
"subtitle.exited": { "translation": "Shell afgesloten." },
|
||||||
|
"error.unavailable": { "translation": "Terminal niet beschikbaar" },
|
||||||
|
"error.daemon": { "translation": "Backend niet verbonden." },
|
||||||
|
"a11y.label": { "translation": "terminal — {subtitle}" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"command.pick": { "translation": "Thema…" },
|
||||||
|
"modal.title": { "translation": "Thema selecteren" },
|
||||||
|
"modal.cancel": { "translation": "Annuleren" },
|
||||||
|
"modal.cancel.hint": { "translation": "Sluit de themakiezer zonder het huidige thema te wijzigen" },
|
||||||
|
"row.select.hint": { "translation": "Dit thema activeren" },
|
||||||
|
"section.appearance": { "translation": "Weergave" },
|
||||||
|
"toggle.highContrast": { "translation": "Hoog contrast" },
|
||||||
|
"settings.appearance.title": { "translation": "Weergave" },
|
||||||
|
"settings.appearance.section.theme": { "translation": "Thema" },
|
||||||
|
"settings.appearance.section.typography": { "translation": "Typografie" },
|
||||||
|
"settings.appearance.field.theme.label": { "translation": "Thema" },
|
||||||
|
"settings.appearance.field.theme.help": { "translation": "Kleurthema; hoog contrast schakelt over naar de toegankelijke variant." },
|
||||||
|
"settings.appearance.field.uiFont.label": { "translation": "UI-lettertype" },
|
||||||
|
"settings.appearance.field.uiFont.help": { "translation": "Lettertype voor de app-interface; wordt direct toegepast." },
|
||||||
|
"settings.appearance.field.uiFont.option.josefinSans": { "translation": "Josefin Sans" },
|
||||||
|
"settings.appearance.field.uiFont.option.inter": { "translation": "Inter" },
|
||||||
|
"settings.appearance.field.monoFont.label": { "translation": "Monospace-lettertype" },
|
||||||
|
"settings.appearance.field.monoFont.help": { "translation": "Terminal, diffs, code en ID's; wordt direct toegepast." },
|
||||||
|
"settings.appearance.field.monoFont.option.jetBrainsMono": { "translation": "JetBrains Mono" },
|
||||||
|
"settings.appearance.field.monoFont.option.firaMono": { "translation": "Fira Mono" },
|
||||||
|
"settings.appearance.section.language": { "translation": "Taal" },
|
||||||
|
"settings.appearance.field.language.label": { "translation": "Taal" },
|
||||||
|
"settings.appearance.field.language.help": { "translation": "Taal voor de app-interface; wordt direct toegepast." }
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"tab.title": { "translation": "Tickets" },
|
||||||
|
"tab.detail.title": { "translation": "Ticket" },
|
||||||
|
"type.initiative": { "translation": "Initiatief" },
|
||||||
|
"type.epic": { "translation": "Epic" },
|
||||||
|
"type.story": { "translation": "Story" },
|
||||||
|
"type.task": { "translation": "Taak" },
|
||||||
|
"type.bug": { "translation": "Bug" },
|
||||||
|
"loading": { "translation": "Tickets laden..." },
|
||||||
|
"error.load": { "translation": "laden van tickets mislukt" },
|
||||||
|
"empty": { "translation": "Geen tickets.\nVoer `pql ticket new` uit om er een aan te maken." },
|
||||||
|
"filter.hint": { "translation": "Tickets filteren…" },
|
||||||
|
"refresh.tooltip": { "translation": "Tickets vernieuwen" },
|
||||||
|
"section.in_progress": { "translation": "IN BEHANDELING" },
|
||||||
|
"section.review": { "translation": "BEOORDELING" },
|
||||||
|
"section.ready": { "translation": "GEREED" },
|
||||||
|
"section.backlog": { "translation": "BACKLOG" },
|
||||||
|
"section.done": { "translation": "KLAAR" },
|
||||||
|
"section.cancelled": { "translation": "GEANNULEERD" },
|
||||||
|
"chip.label": { "translation": "filter op type {type}" },
|
||||||
|
"chip.tooltip": { "translation": "Klik om te wisselen · dubbelklik om te isoleren" },
|
||||||
|
"badge.wip": { "translation": "WIP" },
|
||||||
|
"badge.review": { "translation": "BEOORDELING" },
|
||||||
|
"badge.cancelled": { "translation": "GEANNULEERD" },
|
||||||
|
"pickUp.tooltip": { "translation": "Oppakken — geef dit ticket door aan het Claude-paneel" },
|
||||||
|
"detail.loading": { "translation": "Laden…" },
|
||||||
|
"detail.empty": { "translation": "Selecteer een ticket om de details te bekijken." },
|
||||||
|
"detail.assigned": { "translation": "toegewezen aan: {name}" },
|
||||||
|
"detail.section.parents": { "translation": "BOVENLIGGENDE BOOM" },
|
||||||
|
"detail.section.decisions": { "translation": "GEREFEREERDE BESLISSINGEN" },
|
||||||
|
"status.backlog": { "translation": "BACKLOG" },
|
||||||
|
"status.ready": { "translation": "GEREED" },
|
||||||
|
"status.in_progress": { "translation": "WIP" },
|
||||||
|
"status.review": { "translation": "BEOORDELING" },
|
||||||
|
"status.done": { "translation": "KLAAR" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"command.detect": { "translation": "Tool-paden opnieuw detecteren" },
|
||||||
|
"settings.title": { "translation": "Tools" },
|
||||||
|
"settings.section.binaries": { "translation": "Hulpprogramma's" },
|
||||||
|
"settings.field.claude.label": { "translation": "Claude-CLI" },
|
||||||
|
"settings.field.claude.help": { "translation": "Absoluut pad naar het claude-binary; leeg om automatisch te bepalen." },
|
||||||
|
"settings.field.d2.label": { "translation": "d2" },
|
||||||
|
"settings.field.d2.help": { "translation": "Absoluut pad naar de d2-diagramcompiler; leeg om automatisch te bepalen." },
|
||||||
|
"settings.field.detect.label": { "translation": "Opnieuw detecteren" },
|
||||||
|
"settings.field.detect.help": { "translation": "Scan PATH en de gangbare installatiemappen opnieuw; overschrijft de paden hierboven." }
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"command.view.zoomIn": { "translation": "Weergave: Inzoomen" },
|
||||||
|
"command.view.zoomOut": { "translation": "Weergave: Uitzoomen" },
|
||||||
|
"command.view.zoomReset": { "translation": "Weergave: Zoom resetten" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"mode.normal": { "translation": "NORMAL" },
|
||||||
|
"mode.insert": { "translation": "INSERT" },
|
||||||
|
"mode.visual": { "translation": "VISUAL" },
|
||||||
|
"command.vim.mode.normal": { "translation": "Vim: Normale modus" },
|
||||||
|
"command.vim.mode.insert": { "translation": "Vim: Invoegmodus" },
|
||||||
|
"command.vim.mode.visual": { "translation": "Vim: Visuele modus" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"title": { "translation": "clide" },
|
||||||
|
"subtitle": { "translation": "IDE voor Claude Code CLI" },
|
||||||
|
"open-project": { "translation": "Project openen" },
|
||||||
|
"open-project.hint": { "translation": "Kies een git-repository om als werkruimte te openen" },
|
||||||
|
"tab.title": { "translation": "Welkom" },
|
||||||
|
"section.tips": { "translation": "TIPS" },
|
||||||
|
"section.start": { "translation": "STARTEN" },
|
||||||
|
"section.recent": { "translation": "RECENT" },
|
||||||
|
"tips.quickOpen": { "translation": "Snel openen" },
|
||||||
|
"tips.commandPalette": { "translation": "Opdrachtenpalet" },
|
||||||
|
"tips.toggleSidebar": { "translation": "Zijbalk in-/uitschakelen" },
|
||||||
|
"tips.toggleContext": { "translation": "Context in-/uitschakelen" },
|
||||||
|
"tips.findInFiles": { "translation": "Zoeken in bestanden" },
|
||||||
|
"tips.focusMode": { "translation": "Focusmodus" },
|
||||||
|
"action.openFolder": { "translation": "Map openen…" },
|
||||||
|
"action.newProject": { "translation": "Nieuw project…" },
|
||||||
|
"dialog.newProject.title": { "translation": "Nieuw project" },
|
||||||
|
"dialog.newProject.body": { "translation": "Maakt een git-repo + een CLAUDE.md, en opent het." },
|
||||||
|
"button.create": { "translation": "Aanmaken" },
|
||||||
|
"button.creating": { "translation": "Aanmaken…" },
|
||||||
|
"recent.empty": { "translation": "Geen recente projecten." },
|
||||||
|
"sticky.label": { "translation": "dit project altijd openen bij opstarten" },
|
||||||
|
"sticky.tooltip": { "translation": "Dit project altijd openen bij opstarten" },
|
||||||
|
"sticky.tooltip.active": { "translation": "Dit project altijd openen bij opstarten (vink uit om de kiezer te herstellen)" },
|
||||||
|
"status.checking": { "translation": "controleren…" },
|
||||||
|
"status.ok": { "translation": "applicatie ok" },
|
||||||
|
"status.notFound": { "translation": "{tool} niet gevonden" },
|
||||||
|
"status.theme": { "translation": "thema: " },
|
||||||
|
"dialog.openProject.title": { "translation": "Project openen" },
|
||||||
|
"dialog.openProject.body": { "translation": "Voer het pad naar een git-repository in." },
|
||||||
|
"dialog.openProject.error": { "translation": "Geen git-repository" },
|
||||||
|
"button.cancel": { "translation": "Annuleren" },
|
||||||
|
"button.open": { "translation": "Openen" },
|
||||||
|
"button.opening": { "translation": "Openen…" },
|
||||||
|
"button.ok": { "translation": "OK" },
|
||||||
|
"dialog.notRepo.title": { "translation": "Geen git-repo gevonden" },
|
||||||
|
"dialog.notRepo.body": { "translation": "Een clide-project heeft een git-repository nodig. Deze map als project initialiseren?" },
|
||||||
|
"dialog.notRepo.initialize": { "translation": "Project initialiseren" },
|
||||||
|
"button.initializing": { "translation": "Initialiseren…" },
|
||||||
|
"command.workspace.open-project": { "translation": "Werkruimte: Project openen…" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"collapser.expand": { "translation": "Uitvouwen" },
|
||||||
|
"collapser.collapse": { "translation": "Samenvouwen" },
|
||||||
|
"collapser.expanded": { "translation": "uitgevouwen" },
|
||||||
|
"collapser.collapsed": { "translation": "samengevouwen" },
|
||||||
|
"toast.dismiss": { "translation": "Melding sluiten" },
|
||||||
|
"lightbox.close": { "translation": "sluiten" },
|
||||||
|
"lightbox.hint": { "translation": "scrollen om te zoomen · dubbelklik om te resetten · Esc om te sluiten" },
|
||||||
|
"tab.new": { "translation": "Nieuw tabblad" },
|
||||||
|
"exline.notCommand": { "translation": "Geen editoropdracht" },
|
||||||
|
"spine.expandSuffix": { "translation": "klik om uit te vouwen" },
|
||||||
|
"pane.close": { "translation": "Paneel sluiten" },
|
||||||
|
"pane.header": { "translation": "paneelkop: {title}" },
|
||||||
|
"reader.back": { "translation": "Terug" },
|
||||||
|
"reader.forward": { "translation": "Vooruit" },
|
||||||
|
"reader.jumpToPin": { "translation": "Naar speld springen" },
|
||||||
|
"reader.edit": { "translation": "Bewerken in editor" },
|
||||||
|
"reader.pin": { "translation": "Vastmaken" },
|
||||||
|
"reader.unpin": { "translation": "Losmaken" },
|
||||||
|
"link.openInEditor": { "translation": "Openen in editor" },
|
||||||
|
"resize.axis.width": { "translation": "breedte" },
|
||||||
|
"resize.axis.height": { "translation": "hoogte" },
|
||||||
|
"resize.sidebar": { "translation": "Zijbalk {axis}" },
|
||||||
|
"resize.contextPanel": { "translation": "Contextpaneel {axis}" },
|
||||||
|
"resize.slot": { "translation": "{slot} {axis}" },
|
||||||
|
"resize.pixels": { "translation": "{n} pixels" }
|
||||||
|
}
|
||||||
@@ -39,7 +39,7 @@ self:
|
|||||||
# Auto-synced from pubspec.yaml `version:` by `make gen-build-info`
|
# Auto-synced from pubspec.yaml `version:` by `make gen-build-info`
|
||||||
# (runs implicitly on every build/run/test). Don't hand-edit; bump
|
# (runs implicitly on every build/run/test). Don't hand-edit; bump
|
||||||
# pubspec instead.
|
# pubspec instead.
|
||||||
version: "2.7.0"
|
version: "2.9.0"
|
||||||
homepage: https://github.com/postmeridiem/clide
|
homepage: https://github.com/postmeridiem/clide
|
||||||
license: MIT
|
license: MIT
|
||||||
license_file: assets/LICENSE
|
license_file: assets/LICENSE
|
||||||
@@ -76,8 +76,8 @@ dependencies:
|
|||||||
license: OFL-1.1
|
license: OFL-1.1
|
||||||
license_file: assets/fonts/inter/OFL.txt
|
license_file: assets/fonts/inter/OFL.txt
|
||||||
purpose: >-
|
purpose: >-
|
||||||
Default application UI face (T-460). Variable font with optical-size
|
Selectable application UI face (bundled T-460); Josefin Sans is the
|
||||||
and weight axes; Josefin Sans remains bundled as a selectable option.
|
default. Variable font with optical-size and weight axes.
|
||||||
weights_bundled: [VariableFont, Italic-VariableFont]
|
weights_bundled: [VariableFont, Italic-VariableFont]
|
||||||
|
|
||||||
- name: Josefin Sans
|
- name: Josefin Sans
|
||||||
@@ -87,8 +87,9 @@ dependencies:
|
|||||||
license: OFL-1.1
|
license: OFL-1.1
|
||||||
license_file: assets/fonts/josefin_sans/OFL.txt
|
license_file: assets/fonts/josefin_sans/OFL.txt
|
||||||
purpose: >-
|
purpose: >-
|
||||||
Selectable application UI face (was the default before T-460). Default
|
Default application UI face; default weight Light (300), full 100-700
|
||||||
weight Light (300); full 100-700 range via the variable-font weight axis.
|
range via the variable-font weight axis. Inter is bundled as a
|
||||||
|
selectable alternative.
|
||||||
weights_bundled: [VariableFont, Italic-VariableFont]
|
weights_bundled: [VariableFont, Italic-VariableFont]
|
||||||
|
|
||||||
- name: Phosphor Icons
|
- name: Phosphor Icons
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# T-88 — track dugite-native (bundled git) upstream releases for security
|
||||||
|
# updates (D-59). Compares the pinned DUGITE_VERSION against the latest
|
||||||
|
# desktop/dugite-native release and flags CVE / security mentions.
|
||||||
|
#
|
||||||
|
# WHY THIS SCRIPT IS THE MAINTENANCE HOME: dugite's binary is FETCHED at build
|
||||||
|
# time (`make dugite-fetch`), not built, and native/dugite/ is gitignored — so
|
||||||
|
# there is no `native/dugite/BUILD.md` (D-63) to record it. The pin lives in the
|
||||||
|
# Makefile (DUGITE_VERSION / DUGITE_COMMIT); this script + its `make dugite-check`
|
||||||
|
# target are the version-tracking calendar D-59 requires.
|
||||||
|
#
|
||||||
|
# CADENCE: run quarterly during normal operation. Run IMMEDIATELY on a git or
|
||||||
|
# dugite-native security advisory — subscribe to:
|
||||||
|
# https://github.com/git/git/security/advisories
|
||||||
|
# https://github.com/desktop/dugite-native/security/advisories
|
||||||
|
#
|
||||||
|
# This check is INFORMATIONAL (not a push gate): it reports drift and flags CVE
|
||||||
|
# mentions. The bump itself is manual per D-63 (reproducibility record) and is
|
||||||
|
# automated by T-25 (CI). A bump updates: Makefile DUGITE_VERSION/COMMIT, the
|
||||||
|
# fetched binary in native/dugite/, and assets/licenses.yaml if the bundled git
|
||||||
|
# or dugite version changed.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
REPO="desktop/dugite-native"
|
||||||
|
CURRENT="$(grep -E '^DUGITE_VERSION[[:space:]]*:=' Makefile | head -1 | sed -E 's/.*:=[[:space:]]*//')"
|
||||||
|
|
||||||
|
# Public read — no auth needed for a quarterly check. gh would raise the rate
|
||||||
|
# limit but isn't required; curl keeps this dependency-free.
|
||||||
|
json="$(curl -fsSL -H 'Accept: application/vnd.github+json' "https://api.github.com/repos/$REPO/releases/latest" 2>/dev/null || true)"
|
||||||
|
LATEST="$(printf '%s' "$json" | sed -nE 's/.*"tag_name":[[:space:]]*"([^"]+)".*/\1/p' | head -1)"
|
||||||
|
|
||||||
|
if [[ -z "$LATEST" ]]; then
|
||||||
|
echo "==> dugite-check: couldn't reach the dugite-native releases API." >&2
|
||||||
|
echo " Check manually: https://github.com/$REPO/releases" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> dugite-check (T-88): bundled '$CURRENT' vs latest release '$LATEST'"
|
||||||
|
if [[ "$CURRENT" == "$LATEST" ]]; then
|
||||||
|
echo " OK — up to date."
|
||||||
|
else
|
||||||
|
echo " DRIFT — a newer dugite-native release exists: $CURRENT -> $LATEST"
|
||||||
|
echo " Bump (D-63 record; machine: T-25): update Makefile DUGITE_VERSION/COMMIT,"
|
||||||
|
echo " refresh native/dugite/, and assets/licenses.yaml if the git/dugite version changed."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Loud flag when the latest release notes mention a CVE / security fix — those
|
||||||
|
# jump the queue regardless of the quarterly cadence.
|
||||||
|
if printf '%s' "$json" | grep -qiE 'cve-[0-9]{4}|security (fix|advisory|release|update)|vulnerab'; then
|
||||||
|
echo " !! SECURITY: the latest release notes mention a CVE / security fix — schedule a bump NOW." >&2
|
||||||
|
fi
|
||||||
@@ -1,9 +1,58 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# CI entry: release pipeline. Stub — wire goreleaser + flutter build
|
# Release finalizer for the single-process Flutter app (T-393).
|
||||||
# artifacts later.
|
#
|
||||||
|
# Run AFTER the `release vX.Y.Z` commit is in place (version bump + changelog
|
||||||
|
# move — see .claude/skills/git-commit/SKILL.md "Cutting a release"). It:
|
||||||
|
# 1. reads the version from pubspec.yaml,
|
||||||
|
# 2. asserts CHANGELOG.md has a dated section for it (not still Unreleased),
|
||||||
|
# 3. asserts the working tree is clean,
|
||||||
|
# 4. runs the full gate (make push-check),
|
||||||
|
# 5. creates the annotated vX.Y.Z tag if missing — closing the loop that
|
||||||
|
# previously left every release since v2.1.0 untagged.
|
||||||
|
#
|
||||||
|
# No goreleaser, no sidecar (both dissolved, D-56). Artifact builds are
|
||||||
|
# `make build-linux` / `make build-macos`. This never pushes — it prints the
|
||||||
|
# push command for you to run.
|
||||||
|
#
|
||||||
|
# Invoke via `make release`, not directly.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
echo "TODO: goreleaser release (sidecar) + flutter build (app) + publish"
|
version="$(grep -E '^version:' pubspec.yaml | awk '{print $2}')"
|
||||||
exit 64
|
if [[ -z "$version" ]]; then
|
||||||
|
echo "release: could not read 'version:' from pubspec.yaml" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
tag="v$version"
|
||||||
|
|
||||||
|
# 1. The release commit must already have moved Unreleased → [version] — DATE.
|
||||||
|
if ! grep -qE "^## \[${version//./\\.}\] — [0-9]{4}-[0-9]{2}-[0-9]{2}" CHANGELOG.md; then
|
||||||
|
echo "release: CHANGELOG.md has no dated section for [$version]." >&2
|
||||||
|
echo " Cut the release commit first (move Unreleased → '## [$version] — YYYY-MM-DD')." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Clean tree — the release commit is in, nothing dangling.
|
||||||
|
if [[ -n "$(git status --porcelain)" ]]; then
|
||||||
|
echo "release: working tree not clean — commit the release first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Full gate.
|
||||||
|
echo "release: running the full gate (make push-check)…"
|
||||||
|
make push-check
|
||||||
|
|
||||||
|
# 4. Tag (idempotent), annotated, on the current release commit.
|
||||||
|
if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then
|
||||||
|
echo "release: tag $tag already exists — leaving it."
|
||||||
|
else
|
||||||
|
git tag -a "$tag" -m "clide $tag"
|
||||||
|
echo "release: created tag $tag at $(git rev-parse --short HEAD)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "release: $tag verified and tagged. Next:"
|
||||||
|
echo " git push origin main --follow-tags # publish the commit + tag"
|
||||||
|
echo " make build-linux # desktop bundle (Linux)"
|
||||||
|
echo " make build-macos # desktop bundle (macOS, on a Mac)"
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# Drawing-card JSON schema (T-317 / D-91 / D-103) — draft
|
||||||
|
|
||||||
|
Status: **draft for build**, SVG-substrate model (D-103), refined from the
|
||||||
|
T-317 wireframe set, 2026-06-28.
|
||||||
|
|
||||||
|
## Model — what this is, and what it is NOT
|
||||||
|
|
||||||
|
- **SVG is the substrate.** The card's primitive / scene-graph layer **is SVG**;
|
||||||
|
the clide-owned `CustomPaint` **SVG renderer (T-320) is the engine** the rest
|
||||||
|
builds on (D-103).
|
||||||
|
- It is **not** the HTML Canvas 2D API and **not** a third-party package. SVG is
|
||||||
|
a document *format* we render ourselves — "own the rendering stack" holds.
|
||||||
|
("HTML `<canvas>`" in D-91 was only a mental model, chosen to reject Obsidian's
|
||||||
|
`.canvas` schema; never an API to port.)
|
||||||
|
- The card is **two layers**:
|
||||||
|
1. **SVG content** — painted by the SVG renderer.
|
||||||
|
2. A thin **Flutter overlay** — the clide chrome that is *not* content
|
||||||
|
(per-object label/description captions, lightbox affordance), anchored to
|
||||||
|
SVG elements via `data-*` attributes.
|
||||||
|
- **Display-only** (D-78); re-rendered from the document. The **graph template
|
||||||
|
is the live-widget exception** (below).
|
||||||
|
|
||||||
|
## Document envelope
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"card": { "label": "Build pipeline", "description": "…" }, // optional caption (overlay)
|
||||||
|
"template": "icon", // optional → template sugar
|
||||||
|
"…template fields…": "…",
|
||||||
|
"svg": "<svg viewBox='0 0 480 360'>…</svg>" // primitive mode = raw SVG
|
||||||
|
// or "svgPath": "diagram.svg"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Template mode** — `template` names a component; clide lowers it to SVG
|
||||||
|
(+ overlay anchors).
|
||||||
|
- **Primitive mode** — `svg` (inline) or `svgPath` — arbitrary SVG, the escape
|
||||||
|
hatch. One less invented format; external SVG / graphviz / mermaid render free.
|
||||||
|
- Card size comes from the SVG `viewBox` (or `width`/`height`); the painter scales
|
||||||
|
to the pane width.
|
||||||
|
|
||||||
|
## Primitive layer = a bounded SVG subset
|
||||||
|
|
||||||
|
Grounded in a real d2 sample + our own templates — **not** a full SVG engine:
|
||||||
|
|
||||||
|
- **structure:** `<svg>` (viewBox/width/height, incl. nested `<svg>`), `<g>`
|
||||||
|
(transform, opacity, class), `<defs>`, `<marker>` (+ marker-start/mid/end,
|
||||||
|
`orient="auto"`, refX/refY, viewBox) — edge **arrowheads**
|
||||||
|
- **shapes:** `rect` (rx/ry), `circle`, `ellipse`, `line`, `polyline`,
|
||||||
|
`polygon`, `path` (full data — `M L H V C S Q T A Z` + relatives)
|
||||||
|
- **text:** `text` + `tspan` (x/y/dx/dy, font-family incl. **Phosphor**,
|
||||||
|
font-size/weight, text-anchor, dominant-baseline)
|
||||||
|
- **raster:** `image` (`href`/`xlink:href`, x/y/w/h, preserveAspectRatio)
|
||||||
|
- **styling:** presentation attrs (fill, fill-opacity, stroke, stroke-width,
|
||||||
|
stroke-linecap/linejoin, stroke-dasharray, opacity, color), `transform`
|
||||||
|
(translate/scale/rotate/matrix); `class=` resolved by the normalizer below
|
||||||
|
- **deferred v1:** `<mask>` (d2 masks connections for clean edge/node joins) —
|
||||||
|
ignore and lean on node-over-edge paint order; add only if output looks wrong
|
||||||
|
- **out:** `foreignObject`, filters, `<animate>`/SMIL, scripting,
|
||||||
|
`<use>`/`<symbol>`, gradients, patterns, `clipPath` → **mermaid is not a
|
||||||
|
launch target** (it leans on `foreignObject`)
|
||||||
|
|
||||||
|
`color` everywhere is an **arbitrary value** (hex / named) — content, not a clide
|
||||||
|
`SurfaceTokens` token (D-7 governs clide chrome, not rendered content).
|
||||||
|
|
||||||
|
### Class styling → inline-normalize (not a render-time CSS engine)
|
||||||
|
|
||||||
|
d2 / graphviz emit a `<style>` block of flat single-class selectors
|
||||||
|
(`.fill-B1`, `.shape`, `.connection`, `.text-bold` → presentation props), not
|
||||||
|
inline attributes. A **preprocessing normalizer** parses `<style>` into
|
||||||
|
class→props and merges each element's class props into inline presentation
|
||||||
|
attributes (inline wins), then drops `<style>`. The painter therefore only ever
|
||||||
|
sees inline attrs — a pure, testable presentation-attribute renderer. The
|
||||||
|
normalizer is a bounded, fixture-testable transform (real d2 + graphviz output).
|
||||||
|
|
||||||
|
## Overlay (clide chrome, layered over the SVG)
|
||||||
|
|
||||||
|
Flutter widgets anchored to SVG elements that carry:
|
||||||
|
|
||||||
|
- `data-label` → themed caption beneath the element's bounding box
|
||||||
|
- `data-description` → secondary caption line
|
||||||
|
- `data-lightbox` (on `<image>`) → click-to-zoom affordance
|
||||||
|
|
||||||
|
Templates emit these attributes; raw-SVG authors may add them. The icon
|
||||||
|
template's `data-label` is also the bridge to the interaction-zone choice list.
|
||||||
|
|
||||||
|
## Templates (lower to SVG + overlay)
|
||||||
|
|
||||||
|
| template | lowers to | ticket |
|
||||||
|
|----------|-----------|--------|
|
||||||
|
| `image` | `<image href>` + `data-lightbox` + caption attrs | T-316 |
|
||||||
|
| `icon` | `<text font=Phosphor>` glyphs at 10,11,12,13,14,15,18,20,24,32,48 + hero 52; per-item `data-label`/`data-description`/color | T-313 |
|
||||||
|
| `compare` | two+ `<image>` side by side in a `<g>`, per-image `data-lightbox` + captions | T-319 |
|
||||||
|
| `svg` | identity — the source *is* the SVG | T-320 |
|
||||||
|
| `d2` | compile d2 → SVG → render | T-494 |
|
||||||
|
| `graph` | **exception** — hosts the live native graph subsystem widget (D-46 / T-323), not static SVG | T-321 |
|
||||||
|
|
||||||
|
## CLI / transport (D-6 parity)
|
||||||
|
|
||||||
|
`clide draw --file doc.json` (or inline JSON). **Flutter-free** handler validates,
|
||||||
|
publishes on a `draw` MessageBus channel; the Claude extension injects the card —
|
||||||
|
mirroring `image.show`. The shipped `image show` (T-249/T-252) stays as
|
||||||
|
convenience and migrates onto this card later (D-91). `--stdin` deferred (T-315);
|
||||||
|
`--file` is the path.
|
||||||
|
|
||||||
|
## Error contract
|
||||||
|
|
||||||
|
Unknown `template`, unparseable / unsupported SVG, bad `href` / glyph / `color`
|
||||||
|
→ honest `IpcError` (`userError` / `notFound`), surfaced like `image.show` —
|
||||||
|
**never a blank card**.
|
||||||
|
|
||||||
|
## Build sequence (D-103)
|
||||||
|
|
||||||
|
1. **T-320 — the SVG renderer (engine):** parse + paint the bounded SVG subset.
|
||||||
|
2. **T-318 — envelope + template dispatch + the Flutter overlay** (`data-*` →
|
||||||
|
captions / lightbox) on top of the renderer.
|
||||||
|
3. **Templates:** image / icon (T-316 / T-313) → compare (T-319) → d2 (T-494) →
|
||||||
|
graph (T-321, after the graph subsystem T-323).
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- **SVG subset boundary (T-320): RESOLVED** — see the subset above, grounded in
|
||||||
|
a real d2 sample; expand deliberately.
|
||||||
|
- **Class styling (T-320): RESOLVED** — inline-normalize, not a render-time CSS
|
||||||
|
engine (above).
|
||||||
|
- **Tool-PATH resolution: RESOLVED** — explicit user-scope override + first-run
|
||||||
|
auto-detect (D-104 / T-495); the d2 binary resolves through it.
|
||||||
|
- **D2 compiler delivery (T-494): open** — shell out to a `d2` binary as a
|
||||||
|
pql-style supporter tool (resolution now handled by D-104), vs. vendor.
|
||||||
|
- **Graph (T-321): gated** on the native graph subsystem (D-46 / T-323).
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"name": "Drawing Card Core (T-318)",
|
||||||
|
"shapes": {
|
||||||
|
"title": { "type": "Text", "left": 60, "top": 38, "text": "clide drawing card — core: canvas engine + JSON schema + template dispatch (T-318)", "fontColor": "#c8d0e0", "fontSize": 14 },
|
||||||
|
"subtitle": { "type": "Text", "left": 60, "top": 60, "text": "the dispatch shell under every card type — a JSON document renders as raw primitives OR via a named template that lowers onto the same primitive scene (hybrid)", "fontColor": "#6a7280", "fontSize": 11 },
|
||||||
|
"cliLine": { "type": "Text", "left": 60, "top": 84, "text": "clide draw --file doc.json — Flutter-free handler → MessageBus → Claude-extension injection (mirrors image.show)", "fontColor": "#6a7280", "fontSize": 11 },
|
||||||
|
|
||||||
|
"inputBox": { "type": "Rectangle", "left": 400, "top": 110, "width": 300, "height": 58, "fillColor": "#14171c", "strokeColor": "#333340", "corners": [6, 6, 6, 6] },
|
||||||
|
"inT": { "type": "Text", "parent": "inputBox", "left": 418, "top": 122, "text": "Drawing-card JSON document", "fontColor": "#e2e8f5", "fontSize": 13 },
|
||||||
|
"inT2": { "type": "Text", "parent": "inputBox", "left": 418, "top": 144, "text": "{ \"template\"?: \"…\", \"objects\": [ … ] }", "fontColor": "#9aa3b5", "fontSize": 11 },
|
||||||
|
|
||||||
|
"coreBox": { "type": "Rectangle", "left": 360, "top": 200, "width": 380, "height": 60, "fillColor": "#1a1e24", "strokeColor": "#4a5570", "corners": [8, 8, 8, 8] },
|
||||||
|
"coreT": { "type": "Text", "parent": "coreBox", "left": 384, "top": 212, "text": "Drawing-card core (T-318)", "fontColor": "#e2e8f5", "fontSize": 14 },
|
||||||
|
"coreT2": { "type": "Text", "parent": "coreBox", "left": 384, "top": 234, "text": "JSON schema · primitive renderer · template dispatch", "fontColor": "#8a93a6", "fontSize": 11 },
|
||||||
|
|
||||||
|
"primBox": { "type": "Rectangle", "left": 110, "top": 312, "width": 360, "height": 150, "fillColor": "#161a20", "strokeColor": "#333340", "corners": [8, 8, 8, 8] },
|
||||||
|
"primH": { "type": "Text", "parent": "primBox", "left": 130, "top": 326, "text": "PRIMITIVE scene-graph", "fontColor": "#e2e8f5", "fontSize": 13 },
|
||||||
|
"primS": { "type": "Text", "parent": "primBox", "left": 130, "top": 348, "text": "objects drawn at coordinates", "fontColor": "#8a93a6", "fontSize": 11 },
|
||||||
|
"pc1": { "type": "Rectangle", "parent": "primBox", "left": 130, "top": 376, "width": 56, "height": 24, "fillColor": "#2a3040", "strokeColor": "#3a4250", "corners": [4, 4, 4, 4] },
|
||||||
|
"pc1t": { "type": "Text", "parent": "pc1", "left": 144, "top": 382, "text": "rect", "fontColor": "#c8d0e0", "fontSize": 10 },
|
||||||
|
"pc2": { "type": "Rectangle", "parent": "primBox", "left": 194, "top": 376, "width": 56, "height": 24, "fillColor": "#2a3040", "strokeColor": "#3a4250", "corners": [4, 4, 4, 4] },
|
||||||
|
"pc2t": { "type": "Text", "parent": "pc2", "left": 208, "top": 382, "text": "line", "fontColor": "#c8d0e0", "fontSize": 10 },
|
||||||
|
"pc3": { "type": "Rectangle", "parent": "primBox", "left": 258, "top": 376, "width": 56, "height": 24, "fillColor": "#2a3040", "strokeColor": "#3a4250", "corners": [4, 4, 4, 4] },
|
||||||
|
"pc3t": { "type": "Text", "parent": "pc3", "left": 272, "top": 382, "text": "text", "fontColor": "#c8d0e0", "fontSize": 10 },
|
||||||
|
"pc4": { "type": "Rectangle", "parent": "primBox", "left": 322, "top": 376, "width": 56, "height": 24, "fillColor": "#2a3040", "strokeColor": "#3a4250", "corners": [4, 4, 4, 4] },
|
||||||
|
"pc4t": { "type": "Text", "parent": "pc4", "left": 334, "top": 382, "text": "glyph", "fontColor": "#c8d0e0", "fontSize": 10 },
|
||||||
|
"pc5": { "type": "Rectangle", "parent": "primBox", "left": 386, "top": 376, "width": 56, "height": 24, "fillColor": "#2a3040", "strokeColor": "#3a4250", "corners": [4, 4, 4, 4] },
|
||||||
|
"pc5t": { "type": "Text", "parent": "pc5", "left": 398, "top": 382, "text": "image", "fontColor": "#c8d0e0", "fontSize": 10 },
|
||||||
|
"primNote": { "type": "Text", "parent": "primBox", "left": 130, "top": 420, "text": "{ \"type\": \"rect\", \"x\":.., \"y\":.., \"w\":.., \"h\":.. }", "fontColor": "#6a7280", "fontSize": 10 },
|
||||||
|
|
||||||
|
"tmplBox": { "type": "Rectangle", "left": 640, "top": 312, "width": 360, "height": 212, "fillColor": "#161a20", "strokeColor": "#333340", "corners": [8, 8, 8, 8] },
|
||||||
|
"tmplH": { "type": "Text", "parent": "tmplBox", "left": 660, "top": 326, "text": "TEMPLATE dispatch", "fontColor": "#e2e8f5", "fontSize": 13 },
|
||||||
|
"tmplS": { "type": "Text", "parent": "tmplBox", "left": 660, "top": 348, "text": "named component → predefined renderer (children)", "fontColor": "#8a93a6", "fontSize": 11 },
|
||||||
|
"tc1": { "type": "Rectangle", "parent": "tmplBox", "left": 660, "top": 374, "width": 150, "height": 26, "fillColor": "#2a3040", "strokeColor": "#3a4250", "corners": [4, 4, 4, 4] },
|
||||||
|
"tc1t": { "type": "Text", "parent": "tc1", "left": 674, "top": 381, "text": "image · T-316", "fontColor": "#c8d0e0", "fontSize": 10 },
|
||||||
|
"tc2": { "type": "Rectangle", "parent": "tmplBox", "left": 822, "top": 374, "width": 150, "height": 26, "fillColor": "#2a3040", "strokeColor": "#3a4250", "corners": [4, 4, 4, 4] },
|
||||||
|
"tc2t": { "type": "Text", "parent": "tc2", "left": 836, "top": 381, "text": "icon · T-313", "fontColor": "#c8d0e0", "fontSize": 10 },
|
||||||
|
"tc3": { "type": "Rectangle", "parent": "tmplBox", "left": 660, "top": 408, "width": 150, "height": 26, "fillColor": "#2a3040", "strokeColor": "#3a4250", "corners": [4, 4, 4, 4] },
|
||||||
|
"tc3t": { "type": "Text", "parent": "tc3", "left": 674, "top": 415, "text": "svg · T-320", "fontColor": "#c8d0e0", "fontSize": 10 },
|
||||||
|
"tc4": { "type": "Rectangle", "parent": "tmplBox", "left": 822, "top": 408, "width": 150, "height": 26, "fillColor": "#2a3040", "strokeColor": "#3a4250", "corners": [4, 4, 4, 4] },
|
||||||
|
"tc4t": { "type": "Text", "parent": "tc4", "left": 836, "top": 415, "text": "d2 · T-494", "fontColor": "#c8d0e0", "fontSize": 10 },
|
||||||
|
"tc5": { "type": "Rectangle", "parent": "tmplBox", "left": 660, "top": 442, "width": 150, "height": 26, "fillColor": "#2a3040", "strokeColor": "#3a4250", "corners": [4, 4, 4, 4] },
|
||||||
|
"tc5t": { "type": "Text", "parent": "tc5", "left": 674, "top": 449, "text": "compare · T-319", "fontColor": "#c8d0e0", "fontSize": 10 },
|
||||||
|
"tc6": { "type": "Rectangle", "parent": "tmplBox", "left": 822, "top": 442, "width": 150, "height": 26, "fillColor": "#2a3040", "strokeColor": "#3a4250", "corners": [4, 4, 4, 4] },
|
||||||
|
"tc6t": { "type": "Text", "parent": "tc6", "left": 836, "top": 449, "text": "graph · T-321", "fontColor": "#c8d0e0", "fontSize": 10 },
|
||||||
|
"tmplNote": { "type": "Text", "parent": "tmplBox", "left": 660, "top": 486, "text": "{ \"template\": \"svg\", \"source\": … }", "fontColor": "#6a7280", "fontSize": 10 },
|
||||||
|
|
||||||
|
"lowerLab": { "type": "Text", "left": 486, "top": 380, "text": "lower onto the", "fontColor": "#c8d8f0", "fontSize": 10 },
|
||||||
|
"lowerLab2": { "type": "Text", "left": 486, "top": 396, "text": "primitive scene (hybrid)", "fontColor": "#c8d8f0", "fontSize": 10 },
|
||||||
|
|
||||||
|
"canvasBox": { "type": "Rectangle", "left": 320, "top": 556, "width": 400, "height": 64, "fillColor": "#1a1e24", "strokeColor": "#4a5570", "corners": [8, 8, 8, 8] },
|
||||||
|
"cvT": { "type": "Text", "parent": "canvasBox", "left": 344, "top": 568, "text": "CustomPaint canvas — in the conversation pane", "fontColor": "#e2e8f5", "fontSize": 12 },
|
||||||
|
"cvT2": { "type": "Text", "parent": "canvasBox", "left": 344, "top": 590, "text": "+ shared per-object label / description widget (only when present)", "fontColor": "#8a93a6", "fontSize": 10 },
|
||||||
|
|
||||||
|
"foot": { "type": "Text", "left": 60, "top": 638, "text": "Display-only (D-78). Unknown template / primitive → clear userError. Templates (image · icon · svg · d2 · compare · graph) are separate children of this engine.", "fontColor": "#6a7280", "fontSize": 11 }
|
||||||
|
},
|
||||||
|
"connectors": {
|
||||||
|
"c1": { "tailId": "inputBox", "headId": "coreBox", "strokeColor": "#c8d8f0" },
|
||||||
|
"c2": { "tailId": "coreBox", "headId": "primBox", "strokeColor": "#6a7a98" },
|
||||||
|
"c3": { "tailId": "coreBox", "headId": "tmplBox", "strokeColor": "#6a7a98" },
|
||||||
|
"c4": { "tailId": "tmplBox", "headId": "primBox", "strokeColor": "#c8d8f0" },
|
||||||
|
"c5": { "tailId": "primBox", "headId": "canvasBox", "strokeColor": "#c8d8f0" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 114 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "Compare / Before-After Card (T-319)",
|
||||||
|
"shapes": {
|
||||||
|
"title": { "type": "Text", "left": 60, "top": 40, "text": "clide drawing card — compare / before-after (T-319)", "fontColor": "#c8d0e0", "fontSize": 14 },
|
||||||
|
"subtitle": { "type": "Text", "left": 60, "top": 62, "text": "two (or more) images side by side; each with an optional label + description and lightbox-on-click — shares the image template", "fontColor": "#6a7280", "fontSize": 11 },
|
||||||
|
|
||||||
|
"jsonBox": { "type": "Rectangle", "left": 60, "top": 92, "width": 600, "height": 116, "fillColor": "#14171c", "strokeColor": "#333340", "corners": [6, 6, 6, 6] },
|
||||||
|
"j1": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 104, "text": "{ \"template\": \"compare\", \"items\": [", "fontColor": "#9aa3b5", "fontSize": 11 },
|
||||||
|
"j2": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 126, "text": " { \"path\": \"before.png\", \"label\": \"Before\", \"description\": \"…\" },", "fontColor": "#9aa3b5", "fontSize": 11 },
|
||||||
|
"j3": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 148, "text": " { \"path\": \"after.png\", \"label\": \"After\", \"description\": \"…\" }", "fontColor": "#9aa3b5", "fontSize": 11 },
|
||||||
|
"j4": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 170, "text": "] }", "fontColor": "#9aa3b5", "fontSize": 11 },
|
||||||
|
|
||||||
|
"cli": { "type": "Text", "left": 60, "top": 226, "text": "clide draw --file compare.json", "fontColor": "#c8d8f0", "fontSize": 12 },
|
||||||
|
|
||||||
|
"card": { "type": "Rectangle", "left": 60, "top": 262, "width": 960, "height": 278, "fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [8, 8, 8, 8] },
|
||||||
|
"cardLabel": { "type": "Text", "parent": "card", "left": 84, "top": 282, "text": "HUD — before / after", "fontColor": "#e2e8f5", "fontSize": 16 },
|
||||||
|
"vdiv": { "type": "Rectangle", "parent": "card", "left": 540, "top": 300, "width": 1, "height": 216, "fillColor": "#333340", "strokeColor": "#333340" },
|
||||||
|
|
||||||
|
"labelL": { "type": "Text", "parent": "card", "left": 84, "top": 312, "text": "Before", "fontColor": "#e2e8f5", "fontSize": 14 },
|
||||||
|
"descL": { "type": "Text", "parent": "card", "left": 84, "top": 334, "text": "cramped status row", "fontColor": "#8a93a6", "fontSize": 11 },
|
||||||
|
"imgL": { "type": "Rectangle", "parent": "card", "left": 84, "top": 356, "width": 430, "height": 150, "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] },
|
||||||
|
"imgLname": { "type": "Text", "parent": "imgL", "left": 256, "top": 424, "text": "before.png", "fontColor": "#6a7280", "fontSize": 12 },
|
||||||
|
"zoomL": { "type": "Rectangle", "parent": "imgL", "left": 470, "top": 364, "width": 36, "height": 18, "fillColor": "#1a1e24", "strokeColor": "#3a4250", "corners": [3, 3, 3, 3] },
|
||||||
|
"zoomLt": { "type": "Text", "parent": "zoomL", "left": 476, "top": 367, "text": "zoom", "fontColor": "#8a93a6", "fontSize": 9 },
|
||||||
|
|
||||||
|
"labelR": { "type": "Text", "parent": "card", "left": 566, "top": 312, "text": "After", "fontColor": "#e2e8f5", "fontSize": 14 },
|
||||||
|
"descR": { "type": "Text", "parent": "card", "left": 566, "top": 334, "text": "roomy, aligned status row", "fontColor": "#8a93a6", "fontSize": 11 },
|
||||||
|
"imgR": { "type": "Rectangle", "parent": "card", "left": 566, "top": 356, "width": 430, "height": 150, "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] },
|
||||||
|
"imgRname": { "type": "Text", "parent": "imgR", "left": 740, "top": 424, "text": "after.png", "fontColor": "#6a7280", "fontSize": 12 },
|
||||||
|
"zoomR": { "type": "Rectangle", "parent": "imgR", "left": 952, "top": 364, "width": 36, "height": 18, "fillColor": "#1a1e24", "strokeColor": "#3a4250", "corners": [3, 3, 3, 3] },
|
||||||
|
"zoomRt": { "type": "Text", "parent": "zoomR", "left": 958, "top": 367, "text": "zoom", "fontColor": "#8a93a6", "fontSize": 9 },
|
||||||
|
|
||||||
|
"lightboxCap": { "type": "Text", "parent": "card", "left": 84, "top": 516, "text": "click either image → lightbox (shared with the image card)", "fontColor": "#6a7280", "fontSize": 10 },
|
||||||
|
|
||||||
|
"foot": { "type": "Text", "left": 60, "top": 562, "text": "Two or more items, side by side. Per-object label + description via the shared widget (T-318). Display-only (D-78).", "fontColor": "#6a7280", "fontSize": 11 }
|
||||||
|
},
|
||||||
|
"connectors": {
|
||||||
|
"flow": { "tailId": "jsonBox", "headId": "card", "strokeColor": "#3a4250" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 72 KiB |
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"name": "D2 Diagram Card",
|
||||||
|
"shapes": {
|
||||||
|
"title": { "type": "Text", "left": 60, "top": 40, "text": "clide drawing card — D2 diagram (T-494 · separate type, reuses the SVG widget T-320)", "fontColor": "#c8d0e0", "fontSize": 14 },
|
||||||
|
"subtitle": { "type": "Text", "left": 60, "top": 62, "text": "the rendered diagram leads; the d2 source folds into a collapsed “view d2 source” disclosure (collapser pattern, T-305)", "fontColor": "#6a7280", "fontSize": 11 },
|
||||||
|
|
||||||
|
"card1": { "type": "Rectangle", "left": 60, "top": 100, "width": 460, "height": 302, "fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [8, 8, 8, 8] },
|
||||||
|
"label1": { "type": "Text", "parent": "card1", "left": 84, "top": 122, "text": "Build pipeline", "fontColor": "#e2e8f5", "fontSize": 16 },
|
||||||
|
"region1": { "type": "Rectangle", "parent": "card1", "left": 84, "top": 154, "width": 412, "height": 180, "fillColor": "#14171c", "strokeColor": "#2a3040", "corners": [4, 4, 4, 4] },
|
||||||
|
"wm1": { "type": "Text", "parent": "region1", "left": 462, "top": 314, "text": "SVG", "fontColor": "#39424f", "fontSize": 11 },
|
||||||
|
"n1": { "type": "Rectangle", "parent": "region1", "left": 108, "top": 232, "width": 72, "height": 30, "fillColor": "#2a3040", "strokeColor": "#6a7a98", "corners": [4, 4, 4, 4] },
|
||||||
|
"n1t": { "type": "Text", "parent": "n1", "left": 124, "top": 241, "text": "fetch", "fontColor": "#c8d0e0", "fontSize": 11 },
|
||||||
|
"n2": { "type": "Rectangle", "parent": "region1", "left": 212, "top": 232, "width": 72, "height": 30, "fillColor": "#2a3040", "strokeColor": "#6a7a98", "corners": [4, 4, 4, 4] },
|
||||||
|
"n2t": { "type": "Text", "parent": "n2", "left": 228, "top": 241, "text": "build", "fontColor": "#c8d0e0", "fontSize": 11 },
|
||||||
|
"n3": { "type": "Rectangle", "parent": "region1", "left": 340, "top": 196, "width": 72, "height": 30, "fillColor": "#2a3040", "strokeColor": "#6a7a98", "corners": [4, 4, 4, 4] },
|
||||||
|
"n3t": { "type": "Text", "parent": "n3", "left": 360, "top": 205, "text": "test", "fontColor": "#c8d0e0", "fontSize": 11 },
|
||||||
|
"n4": { "type": "Rectangle", "parent": "region1", "left": 340, "top": 268, "width": 72, "height": 30, "fillColor": "#2a3040", "strokeColor": "#6a7a98", "corners": [4, 4, 4, 4] },
|
||||||
|
"n4t": { "type": "Text", "parent": "n4", "left": 352, "top": 277, "text": "deploy", "fontColor": "#c8d0e0", "fontSize": 11 },
|
||||||
|
"disc1": { "type": "Rectangle", "parent": "card1", "left": 84, "top": 348, "width": 412, "height": 34, "fillColor": "#181c22", "strokeColor": "#2a3040", "corners": [4, 4, 4, 4] },
|
||||||
|
"chev1": { "type": "Text", "parent": "disc1", "left": 98, "top": 356, "text": "▸", "fontColor": "#8a93a6", "fontSize": 13 },
|
||||||
|
"disc1t": { "type": "Text", "parent": "disc1", "left": 118, "top": 358, "text": "view d2 source", "fontColor": "#8a93a6", "fontSize": 12 },
|
||||||
|
"stateCap1": { "type": "Text", "left": 84, "top": 412, "text": "collapsed — diagram only", "fontColor": "#6a7280", "fontSize": 11 },
|
||||||
|
|
||||||
|
"card2": { "type": "Rectangle", "left": 560, "top": 100, "width": 460, "height": 384, "fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [8, 8, 8, 8] },
|
||||||
|
"label2": { "type": "Text", "parent": "card2", "left": 584, "top": 122, "text": "Build pipeline", "fontColor": "#e2e8f5", "fontSize": 16 },
|
||||||
|
"region2": { "type": "Rectangle", "parent": "card2", "left": 584, "top": 154, "width": 412, "height": 180, "fillColor": "#14171c", "strokeColor": "#2a3040", "corners": [4, 4, 4, 4] },
|
||||||
|
"wm2": { "type": "Text", "parent": "region2", "left": 962, "top": 314, "text": "SVG", "fontColor": "#39424f", "fontSize": 11 },
|
||||||
|
"m1": { "type": "Rectangle", "parent": "region2", "left": 608, "top": 232, "width": 72, "height": 30, "fillColor": "#2a3040", "strokeColor": "#6a7a98", "corners": [4, 4, 4, 4] },
|
||||||
|
"m1t": { "type": "Text", "parent": "m1", "left": 624, "top": 241, "text": "fetch", "fontColor": "#c8d0e0", "fontSize": 11 },
|
||||||
|
"m2": { "type": "Rectangle", "parent": "region2", "left": 712, "top": 232, "width": 72, "height": 30, "fillColor": "#2a3040", "strokeColor": "#6a7a98", "corners": [4, 4, 4, 4] },
|
||||||
|
"m2t": { "type": "Text", "parent": "m2", "left": 728, "top": 241, "text": "build", "fontColor": "#c8d0e0", "fontSize": 11 },
|
||||||
|
"m3": { "type": "Rectangle", "parent": "region2", "left": 840, "top": 196, "width": 72, "height": 30, "fillColor": "#2a3040", "strokeColor": "#6a7a98", "corners": [4, 4, 4, 4] },
|
||||||
|
"m3t": { "type": "Text", "parent": "m3", "left": 860, "top": 205, "text": "test", "fontColor": "#c8d0e0", "fontSize": 11 },
|
||||||
|
"m4": { "type": "Rectangle", "parent": "region2", "left": 840, "top": 268, "width": 72, "height": 30, "fillColor": "#2a3040", "strokeColor": "#6a7a98", "corners": [4, 4, 4, 4] },
|
||||||
|
"m4t": { "type": "Text", "parent": "m4", "left": 852, "top": 277, "text": "deploy", "fontColor": "#c8d0e0", "fontSize": 11 },
|
||||||
|
"disc2": { "type": "Rectangle", "parent": "card2", "left": 584, "top": 348, "width": 412, "height": 34, "fillColor": "#181c22", "strokeColor": "#2a3040", "corners": [4, 4, 4, 4] },
|
||||||
|
"chev2": { "type": "Text", "parent": "disc2", "left": 598, "top": 356, "text": "▾", "fontColor": "#c8d0e0", "fontSize": 13 },
|
||||||
|
"disc2t": { "type": "Text", "parent": "disc2", "left": 618, "top": 358, "text": "view d2 source", "fontColor": "#c8d0e0", "fontSize": 12 },
|
||||||
|
"srcBox": { "type": "Rectangle", "parent": "card2", "left": 584, "top": 388, "width": 412, "height": 82, "fillColor": "#14171c", "strokeColor": "#2a3040", "corners": [4, 4, 4, 4] },
|
||||||
|
"sd1": { "type": "Text", "parent": "srcBox", "left": 600, "top": 398, "text": "direction: right", "fontColor": "#9aa3b5", "fontSize": 11 },
|
||||||
|
"sd2": { "type": "Text", "parent": "srcBox", "left": 600, "top": 420, "text": "fetch -> build -> test", "fontColor": "#9aa3b5", "fontSize": 11 },
|
||||||
|
"sd3": { "type": "Text", "parent": "srcBox", "left": 600, "top": 442, "text": "build -> deploy", "fontColor": "#9aa3b5", "fontSize": 11 },
|
||||||
|
"stateCap2": { "type": "Text", "left": 584, "top": 494, "text": "expanded — d2 source revealed", "fontColor": "#6a7280", "fontSize": 11 },
|
||||||
|
|
||||||
|
"expandLab": { "type": "Text", "left": 528, "top": 330, "text": "expand", "fontColor": "#c8d8f0", "fontSize": 10 },
|
||||||
|
|
||||||
|
"foot": { "type": "Text", "left": 60, "top": 520, "text": "D2 → SVG compile in front; renders via the T-320 SVG widget. clide draw --file pipeline.d2 (type inferred from .d2)", "fontColor": "#6a7280", "fontSize": 11 }
|
||||||
|
},
|
||||||
|
"connectors": {
|
||||||
|
"e1": { "tailId": "n1", "headId": "n2", "strokeColor": "#6a7a98" },
|
||||||
|
"e2": { "tailId": "n2", "headId": "n3", "strokeColor": "#6a7a98" },
|
||||||
|
"e3": { "tailId": "n2", "headId": "n4", "strokeColor": "#6a7a98" },
|
||||||
|
"f1": { "tailId": "m1", "headId": "m2", "strokeColor": "#6a7a98" },
|
||||||
|
"f2": { "tailId": "m2", "headId": "m3", "strokeColor": "#6a7a98" },
|
||||||
|
"f3": { "tailId": "m2", "headId": "m4", "strokeColor": "#6a7a98" },
|
||||||
|
"expand": { "tailId": "disc1", "headId": "disc2", "strokeColor": "#3a4250" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 72 KiB |
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "Graph Render Card (T-321)",
|
||||||
|
"shapes": {
|
||||||
|
"title": { "type": "Text", "left": 60, "top": 40, "text": "clide drawing card — graph render (T-321)", "fontColor": "#c8d0e0", "fontSize": 14 },
|
||||||
|
"subtitle": { "type": "Text", "left": 60, "top": 62, "text": "renders a node/edge graph from JSON — embeds clide's native graph subsystem (CustomPaint, D-46 / T-323), not a new renderer", "fontColor": "#6a7280", "fontSize": 11 },
|
||||||
|
|
||||||
|
"jsonBox": { "type": "Rectangle", "left": 60, "top": 110, "width": 400, "height": 196, "fillColor": "#14171c", "strokeColor": "#333340", "corners": [6, 6, 6, 6] },
|
||||||
|
"j1": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 124, "text": "{ \"template\": \"graph\",", "fontColor": "#9aa3b5", "fontSize": 11 },
|
||||||
|
"j2": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 146, "text": " \"nodes\": [\"app\",\"ipc\",\"pty\",\"git\",\"pql\"],", "fontColor": "#9aa3b5", "fontSize": 11 },
|
||||||
|
"j3": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 168, "text": " \"edges\": [[\"app\",\"ipc\"],[\"ipc\",\"pty\"],", "fontColor": "#9aa3b5", "fontSize": 11 },
|
||||||
|
"j4": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 190, "text": " [\"ipc\",\"git\"],[\"git\",\"pql\"],[\"app\",\"git\"]],", "fontColor": "#9aa3b5", "fontSize": 11 },
|
||||||
|
"j5": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 212, "text": " \"label\": \"subsystem links\" }", "fontColor": "#9aa3b5", "fontSize": 11 },
|
||||||
|
|
||||||
|
"cli": { "type": "Text", "left": 60, "top": 328, "text": "clide draw --file graph.json", "fontColor": "#c8d8f0", "fontSize": 12 },
|
||||||
|
"cliNote": { "type": "Text", "left": 60, "top": 350, "text": "(nodes + edges inline, or a path to a graph file)", "fontColor": "#6a7280", "fontSize": 11 },
|
||||||
|
|
||||||
|
"card": { "type": "Rectangle", "left": 560, "top": 110, "width": 480, "height": 360, "fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [8, 8, 8, 8] },
|
||||||
|
"region": { "type": "Rectangle", "parent": "card", "left": 584, "top": 134, "width": 432, "height": 256, "fillColor": "#14171c", "strokeColor": "#2a3040", "corners": [4, 4, 4, 4] },
|
||||||
|
"wm": { "type": "Text", "parent": "region", "left": 980, "top": 372, "text": "graph", "fontColor": "#39424f", "fontSize": 11 },
|
||||||
|
|
||||||
|
"na": { "type": "Ellipse", "parent": "region", "left": 644, "top": 168, "width": 46, "height": 46, "fillColor": "#2a3040", "strokeColor": "#6a7a98" },
|
||||||
|
"nat": { "type": "Text", "parent": "na", "left": 657, "top": 184, "text": "app", "fontColor": "#c8d0e0", "fontSize": 11 },
|
||||||
|
"ni": { "type": "Ellipse", "parent": "region", "left": 766, "top": 206, "width": 46, "height": 46, "fillColor": "#2a3040", "strokeColor": "#6a7a98" },
|
||||||
|
"nit": { "type": "Text", "parent": "ni", "left": 781, "top": 222, "text": "ipc", "fontColor": "#c8d0e0", "fontSize": 11 },
|
||||||
|
"np": { "type": "Ellipse", "parent": "region", "left": 898, "top": 166, "width": 46, "height": 46, "fillColor": "#2a3040", "strokeColor": "#6a7a98" },
|
||||||
|
"npt": { "type": "Text", "parent": "np", "left": 911, "top": 182, "text": "pty", "fontColor": "#c8d0e0", "fontSize": 11 },
|
||||||
|
"ng": { "type": "Ellipse", "parent": "region", "left": 760, "top": 306, "width": 46, "height": 46, "fillColor": "#2a3040", "strokeColor": "#6a7a98" },
|
||||||
|
"ngt": { "type": "Text", "parent": "ng", "left": 773, "top": 322, "text": "git", "fontColor": "#c8d0e0", "fontSize": 11 },
|
||||||
|
"nq": { "type": "Ellipse", "parent": "region", "left": 904, "top": 298, "width": 46, "height": 46, "fillColor": "#2a3040", "strokeColor": "#6a7a98" },
|
||||||
|
"nqt": { "type": "Text", "parent": "nq", "left": 917, "top": 314, "text": "pql", "fontColor": "#c8d0e0", "fontSize": 11 },
|
||||||
|
|
||||||
|
"label": { "type": "Text", "parent": "card", "left": 584, "top": 402, "text": "subsystem links", "fontColor": "#e2e8f5", "fontSize": 14 },
|
||||||
|
"desc": { "type": "Text", "parent": "card", "left": 584, "top": 424, "text": "how the core pieces connect", "fontColor": "#8a93a6", "fontSize": 11 },
|
||||||
|
|
||||||
|
"foot": { "type": "Text", "left": 60, "top": 500, "text": "Embeds clide's native graph subsystem (CustomPaint, D-46 / T-323) — the card hosts it, doesn't fork it. Label + description beneath via T-318. Display-only (D-78).", "fontColor": "#6a7280", "fontSize": 11 }
|
||||||
|
},
|
||||||
|
"connectors": {
|
||||||
|
"flow": { "tailId": "jsonBox", "headId": "card", "strokeColor": "#3a4250" },
|
||||||
|
"ea": { "tailId": "na", "headId": "ni", "strokeColor": "#6a7a98" },
|
||||||
|
"eb": { "tailId": "ni", "headId": "np", "strokeColor": "#6a7a98" },
|
||||||
|
"ec": { "tailId": "ni", "headId": "ng", "strokeColor": "#6a7a98" },
|
||||||
|
"ed": { "tailId": "ng", "headId": "nq", "strokeColor": "#6a7a98" },
|
||||||
|
"ee": { "tailId": "na", "headId": "ng", "strokeColor": "#6a7a98" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 78 KiB |
@@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"name": "Icon Glyph Card (T-313)",
|
||||||
|
"shapes": {
|
||||||
|
"title": { "type": "Text", "left": 60, "top": 44, "text": "clide icon show — Phosphor glyph card (T-313)", "fontColor": "#c8d0e0", "fontSize": 14 },
|
||||||
|
"subtitle": { "type": "Text", "left": 60, "top": 66, "text": "per-entry label + description · hero (52) + a strip of size samples from 10 up to 48 px", "fontColor": "#6a7280", "fontSize": 11 },
|
||||||
|
|
||||||
|
"cli1": { "type": "Text", "left": 60, "top": 98, "text": "clide icon show gear folder gauge", "fontColor": "#c8d8f0", "fontSize": 12 },
|
||||||
|
"cli1n": { "type": "Text", "left": 372, "top": 98, "text": "— bare variadic preview", "fontColor": "#6a7280", "fontSize": 11 },
|
||||||
|
"cli2": { "type": "Text", "left": 60, "top": 118, "text": "clide icon show --file icons.json", "fontColor": "#c8d8f0", "fontSize": 12 },
|
||||||
|
"cli2n": { "type": "Text", "left": 372, "top": 118, "text": "— labelled entries: [{icon, label, description}, …]", "fontColor": "#6a7280", "fontSize": 11 },
|
||||||
|
"glyphNote": { "type": "Text", "left": 60, "top": 140, "text": "(wireframe: neutral marks stand in for glyphs — Frame0 has no Phosphor font; real card paints via PhosphorIconPainter)", "fontColor": "#566070", "fontSize": 10 },
|
||||||
|
|
||||||
|
"card": { "type": "Rectangle", "left": 60, "top": 168, "width": 960, "height": 350, "fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [8, 8, 8, 8] },
|
||||||
|
|
||||||
|
"hero1": { "type": "Rectangle", "left": 84, "top": 192, "width": 92, "height": 92, "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [8, 8, 8, 8] },
|
||||||
|
"hero1g": { "type": "Ellipse", "left": 106, "top": 214, "width": 48, "height": 48, "fillColor": "#c8d8f0", "strokeColor": "#c8d8f0" },
|
||||||
|
"hero1cap": { "type": "Text", "left": 90, "top": 290, "text": "hero · 52", "fontColor": "#6a7280", "fontSize": 10 },
|
||||||
|
"label1": { "type": "Text", "left": 200, "top": 192, "text": "Settings", "fontColor": "#e2e8f5", "fontSize": 16 },
|
||||||
|
"desc1": { "type": "Text", "left": 200, "top": 216, "text": "global scope", "fontColor": "#8a93a6", "fontSize": 12 },
|
||||||
|
"sizecap1": { "type": "Text", "left": 200, "top": 240, "text": "samples (px)", "fontColor": "#6a7280", "fontSize": 10 },
|
||||||
|
|
||||||
|
"a1": { "type": "Rectangle", "left": 200, "top": 302, "width": 10, "height": 10, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
|
||||||
|
"a1l": { "type": "Text", "left": 200, "top": 318, "text": "10", "fontColor": "#6a7280", "fontSize": 9 },
|
||||||
|
"a2": { "type": "Rectangle", "left": 218, "top": 301, "width": 11, "height": 11, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
|
||||||
|
"a2l": { "type": "Text", "left": 218, "top": 318, "text": "11", "fontColor": "#6a7280", "fontSize": 9 },
|
||||||
|
"a3": { "type": "Rectangle", "left": 237, "top": 300, "width": 12, "height": 12, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
|
||||||
|
"a3l": { "type": "Text", "left": 238, "top": 318, "text": "12", "fontColor": "#6a7280", "fontSize": 9 },
|
||||||
|
"a4": { "type": "Rectangle", "left": 257, "top": 299, "width": 13, "height": 13, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
|
||||||
|
"a4l": { "type": "Text", "left": 258, "top": 318, "text": "13", "fontColor": "#6a7280", "fontSize": 9 },
|
||||||
|
"a5": { "type": "Rectangle", "left": 278, "top": 298, "width": 14, "height": 14, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
|
||||||
|
"a5l": { "type": "Text", "left": 280, "top": 318, "text": "14", "fontColor": "#6a7280", "fontSize": 9 },
|
||||||
|
"a6": { "type": "Rectangle", "left": 300, "top": 297, "width": 15, "height": 15, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
|
||||||
|
"a6l": { "type": "Text", "left": 302, "top": 318, "text": "15", "fontColor": "#6a7280", "fontSize": 9 },
|
||||||
|
"a7": { "type": "Rectangle", "left": 323, "top": 294, "width": 18, "height": 18, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
|
||||||
|
"a7l": { "type": "Text", "left": 327, "top": 318, "text": "18", "fontColor": "#6a7280", "fontSize": 9 },
|
||||||
|
"a8": { "type": "Rectangle", "left": 349, "top": 292, "width": 20, "height": 20, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
|
||||||
|
"a8l": { "type": "Text", "left": 354, "top": 318, "text": "20", "fontColor": "#6a7280", "fontSize": 9 },
|
||||||
|
"a9": { "type": "Rectangle", "left": 377, "top": 288, "width": 24, "height": 24, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
|
||||||
|
"a9l": { "type": "Text", "left": 384, "top": 318, "text": "24", "fontColor": "#6a7280", "fontSize": 9 },
|
||||||
|
"a10": { "type": "Rectangle", "left": 409, "top": 280, "width": 32, "height": 32, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
|
||||||
|
"a10l": { "type": "Text", "left": 420, "top": 318, "text": "32", "fontColor": "#6a7280", "fontSize": 9 },
|
||||||
|
"a11": { "type": "Rectangle", "left": 449, "top": 264, "width": 48, "height": 48, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
|
||||||
|
"a11l": { "type": "Text", "left": 468, "top": 318, "text": "48", "fontColor": "#6a7280", "fontSize": 9 },
|
||||||
|
|
||||||
|
"divider": { "type": "Rectangle", "left": 84, "top": 336, "width": 912, "height": 1, "fillColor": "#333340", "strokeColor": "#333340" },
|
||||||
|
|
||||||
|
"hero2": { "type": "Rectangle", "left": 84, "top": 360, "width": 92, "height": 92, "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [8, 8, 8, 8] },
|
||||||
|
"hero2g": { "type": "Ellipse", "left": 106, "top": 382, "width": 48, "height": 48, "fillColor": "#9fb0c8", "strokeColor": "#9fb0c8" },
|
||||||
|
"hero2cap": { "type": "Text", "left": 90, "top": 458, "text": "hero · 52", "fontColor": "#6a7280", "fontSize": 10 },
|
||||||
|
"label2": { "type": "Text", "left": 200, "top": 360, "text": "Folder", "fontColor": "#e2e8f5", "fontSize": 16 },
|
||||||
|
"desc2": { "type": "Text", "left": 200, "top": 384, "text": "workspace tree", "fontColor": "#8a93a6", "fontSize": 12 },
|
||||||
|
"sizecap2": { "type": "Text", "left": 200, "top": 408, "text": "same scale, per entry", "fontColor": "#6a7280", "fontSize": 10 },
|
||||||
|
|
||||||
|
"b1": { "type": "Rectangle", "left": 200, "top": 470, "width": 10, "height": 10, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
|
||||||
|
"b2": { "type": "Rectangle", "left": 218, "top": 469, "width": 11, "height": 11, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
|
||||||
|
"b3": { "type": "Rectangle", "left": 237, "top": 468, "width": 12, "height": 12, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
|
||||||
|
"b4": { "type": "Rectangle", "left": 257, "top": 467, "width": 13, "height": 13, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
|
||||||
|
"b5": { "type": "Rectangle", "left": 278, "top": 466, "width": 14, "height": 14, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
|
||||||
|
"b6": { "type": "Rectangle", "left": 300, "top": 465, "width": 15, "height": 15, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
|
||||||
|
"b7": { "type": "Rectangle", "left": 323, "top": 462, "width": 18, "height": 18, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
|
||||||
|
"b8": { "type": "Rectangle", "left": 349, "top": 460, "width": 20, "height": 20, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
|
||||||
|
"b9": { "type": "Rectangle", "left": 377, "top": 456, "width": 24, "height": 24, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
|
||||||
|
"b10": { "type": "Rectangle", "left": 409, "top": 448, "width": 32, "height": 32, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
|
||||||
|
"b11": { "type": "Rectangle", "left": 449, "top": 432, "width": 48, "height": 48, "fillColor": "#6a7a98", "strokeColor": "#6a7a98", "corners": [2, 2, 2, 2] },
|
||||||
|
|
||||||
|
"tagMeta": { "type": "Text", "left": 880, "top": 196, "text": "label + description", "fontColor": "#c8d8f0", "fontSize": 11 },
|
||||||
|
"tagHero": { "type": "Text", "left": 880, "top": 226, "text": "hero · 52", "fontColor": "#c8d8f0", "fontSize": 11 },
|
||||||
|
"tagSizes": { "type": "Text", "left": 880, "top": 286, "text": "size samples 10→48", "fontColor": "#c8d8f0", "fontSize": 11 },
|
||||||
|
|
||||||
|
"foot1": { "type": "Text", "left": 60, "top": 536, "text": "Display-only (D-78): the card shows options; selection happens in the convo box. Per-entry labels bridge card → interaction-zone choice list.", "fontColor": "#6a7280", "fontSize": 11 },
|
||||||
|
"foot2": { "type": "Text", "left": 60, "top": 556, "text": "Without label/description, entries collapse to a bare glyph grid.", "fontColor": "#566070", "fontSize": 11 }
|
||||||
|
},
|
||||||
|
"connectors": {
|
||||||
|
"cMeta": { "tailId": "tagMeta", "headId": "label1", "strokeColor": "#3a4250" },
|
||||||
|
"cHero": { "tailId": "tagHero", "headId": "hero1", "strokeColor": "#3a4250" },
|
||||||
|
"cSizes": { "tailId": "tagSizes", "headId": "a11", "strokeColor": "#3a4250" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 84 KiB |
@@ -0,0 +1,71 @@
|
|||||||
|
{
|
||||||
|
"name": "Image Annotation Flow (T-316)",
|
||||||
|
"shapes": {
|
||||||
|
"title": {
|
||||||
|
"type": "Text",
|
||||||
|
"left": 60, "top": 56,
|
||||||
|
"text": "clide image show — annotated image card (T-316, variant a: text metadata)",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 14
|
||||||
|
},
|
||||||
|
|
||||||
|
"jsonBox": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"left": 60, "top": 110, "width": 380, "height": 200,
|
||||||
|
"fillColor": "#14171c", "strokeColor": "#333340", "corners": [6, 6, 6, 6]
|
||||||
|
},
|
||||||
|
"j1": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 124, "text": "{", "fontColor": "#9aa3b5", "fontSize": 11 },
|
||||||
|
"j2": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 146, "text": " \"path\": \"docs/shot.png\",", "fontColor": "#9aa3b5", "fontSize": 11 },
|
||||||
|
"j3": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 168, "text": " \"label\": \"HUD v3\",", "fontColor": "#9aa3b5", "fontSize": 11 },
|
||||||
|
"j4": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 190, "text": " \"description\": \"status row cramped\",", "fontColor": "#9aa3b5", "fontSize": 11 },
|
||||||
|
"j5": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 212, "text": " \"caption\": \"before the fix\"", "fontColor": "#9aa3b5", "fontSize": 11 },
|
||||||
|
"j6": { "type": "Text", "parent": "jsonBox", "left": 76, "top": 234, "text": "}", "fontColor": "#9aa3b5", "fontSize": 11 },
|
||||||
|
|
||||||
|
"cli": {
|
||||||
|
"type": "Text",
|
||||||
|
"left": 60, "top": 330,
|
||||||
|
"text": "clide image show docs/shot.png --file meta.json",
|
||||||
|
"fontColor": "#c8d8f0", "fontSize": 12
|
||||||
|
},
|
||||||
|
"cliNote": {
|
||||||
|
"type": "Text",
|
||||||
|
"left": 60, "top": 354,
|
||||||
|
"text": "(same payload via --stdin once T-315 lands; --caption form still works)",
|
||||||
|
"fontColor": "#6a7280", "fontSize": 11
|
||||||
|
},
|
||||||
|
|
||||||
|
"card": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"left": 560, "top": 110, "width": 480, "height": 430,
|
||||||
|
"fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [8, 8, 8, 8]
|
||||||
|
},
|
||||||
|
"cardLabel": { "type": "Text", "parent": "card", "left": 584, "top": 134, "text": "HUD v3", "fontColor": "#e2e8f5", "fontSize": 16 },
|
||||||
|
"cardDesc": { "type": "Text", "parent": "card", "left": 584, "top": 164, "text": "Status row cramped — the clock overlaps the battery at narrow widths.", "fontColor": "#8a93a6", "fontSize": 12 },
|
||||||
|
"cardImage": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"parent": "card",
|
||||||
|
"left": 584, "top": 200, "width": 432, "height": 250,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"cardImageName": { "type": "Text", "parent": "cardImage", "left": 730, "top": 318, "text": "docs/shot.png", "fontColor": "#6a7280", "fontSize": 12 },
|
||||||
|
"cardCaption": { "type": "Text", "parent": "card", "left": 584, "top": 468, "text": "before the fix", "fontColor": "#8a93a6", "fontSize": 11 },
|
||||||
|
|
||||||
|
"tagLabel": { "type": "Text", "left": 1052, "top": 134, "text": "label", "fontColor": "#c8d8f0", "fontSize": 11 },
|
||||||
|
"tagDesc": { "type": "Text", "left": 1052, "top": 164, "text": "description", "fontColor": "#c8d8f0", "fontSize": 11 },
|
||||||
|
"tagImage": { "type": "Text", "left": 1052, "top": 318, "text": "path", "fontColor": "#c8d8f0", "fontSize": 11 },
|
||||||
|
"tagCaption": { "type": "Text", "left": 1052, "top": 468, "text": "caption", "fontColor": "#c8d8f0", "fontSize": 11 },
|
||||||
|
|
||||||
|
"followup": {
|
||||||
|
"type": "Text",
|
||||||
|
"left": 560, "top": 558,
|
||||||
|
"text": "Variant (b) follow-up: visual markers / numbered callouts painted at {x,y} over the image (CustomPaint).",
|
||||||
|
"fontColor": "#6a7280", "fontSize": 11
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"connectors": {
|
||||||
|
"flow": { "tailId": "jsonBox", "headId": "card", "strokeColor": "#c8d8f0" },
|
||||||
|
"mLabel": { "tailId": "tagLabel", "headId": "cardLabel", "strokeColor": "#3a4250" },
|
||||||
|
"mDesc": { "tailId": "tagDesc", "headId": "cardDesc", "strokeColor": "#3a4250" },
|
||||||
|
"mImage": { "tailId": "tagImage", "headId": "cardImage", "strokeColor": "#3a4250" },
|
||||||
|
"mCaption": { "tailId": "tagCaption", "headId": "cardCaption", "strokeColor": "#3a4250" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 71 KiB |
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"name": "SVG Render Card (T-320)",
|
||||||
|
"shapes": {
|
||||||
|
"title": { "type": "Text", "left": 60, "top": 44, "text": "clide drawing card — SVG render (T-320)", "fontColor": "#c8d0e0", "fontSize": 14 },
|
||||||
|
"subtitle": { "type": "Text", "left": 60, "top": 66, "text": "a drawing-card template (T-318 dispatch): renders a vector SVG natively in the conversation pane — separate type from the raster image card", "fontColor": "#6a7280", "fontSize": 11 },
|
||||||
|
|
||||||
|
"srcBox": { "type": "Rectangle", "left": 60, "top": 110, "width": 380, "height": 178, "fillColor": "#14171c", "strokeColor": "#333340", "corners": [6, 6, 6, 6] },
|
||||||
|
"s1": { "type": "Text", "parent": "srcBox", "left": 76, "top": 124, "text": "{", "fontColor": "#9aa3b5", "fontSize": 11 },
|
||||||
|
"s2": { "type": "Text", "parent": "srcBox", "left": 76, "top": 146, "text": " \"template\": \"svg\",", "fontColor": "#9aa3b5", "fontSize": 11 },
|
||||||
|
"s3": { "type": "Text", "parent": "srcBox", "left": 76, "top": 168, "text": " \"title\": \"Build pipeline\",", "fontColor": "#9aa3b5", "fontSize": 11 },
|
||||||
|
"s4": { "type": "Text", "parent": "srcBox", "left": 76, "top": 190, "text": " \"source\": \"<svg>…</svg>\" // or a .svg path", "fontColor": "#9aa3b5", "fontSize": 11 },
|
||||||
|
"s5": { "type": "Text", "parent": "srcBox", "left": 76, "top": 212, "text": "}", "fontColor": "#9aa3b5", "fontSize": 11 },
|
||||||
|
|
||||||
|
"cli": { "type": "Text", "left": 60, "top": 308, "text": "clide draw --file pipeline.json", "fontColor": "#c8d8f0", "fontSize": 12 },
|
||||||
|
"cliNote": { "type": "Text", "left": 60, "top": 330, "text": "(inline SVG string or a .svg path; clide owns the SVG renderer — vector, no raster rasterise)", "fontColor": "#6a7280", "fontSize": 11 },
|
||||||
|
|
||||||
|
"card": { "type": "Rectangle", "left": 560, "top": 110, "width": 480, "height": 362, "fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [8, 8, 8, 8] },
|
||||||
|
"cardLabel": { "type": "Text", "parent": "card", "left": 584, "top": 132, "text": "Build pipeline", "fontColor": "#e2e8f5", "fontSize": 16 },
|
||||||
|
|
||||||
|
"region": { "type": "Rectangle", "parent": "card", "left": 584, "top": 168, "width": 432, "height": 248, "fillColor": "#14171c", "strokeColor": "#2a3040", "corners": [4, 4, 4, 4] },
|
||||||
|
"rgWatermark": { "type": "Text", "parent": "region", "left": 968, "top": 392, "text": "SVG", "fontColor": "#39424f", "fontSize": 11 },
|
||||||
|
|
||||||
|
"n1": { "type": "Rectangle", "parent": "region", "left": 612, "top": 214, "width": 96, "height": 40, "fillColor": "#2a3040", "strokeColor": "#6a7a98", "corners": [4, 4, 4, 4] },
|
||||||
|
"n1t": { "type": "Text", "parent": "n1", "left": 636, "top": 226, "text": "fetch", "fontColor": "#c8d0e0", "fontSize": 12 },
|
||||||
|
"n2": { "type": "Rectangle", "parent": "region", "left": 772, "top": 214, "width": 96, "height": 40, "fillColor": "#2a3040", "strokeColor": "#6a7a98", "corners": [4, 4, 4, 4] },
|
||||||
|
"n2t": { "type": "Text", "parent": "n2", "left": 798, "top": 226, "text": "build", "fontColor": "#c8d0e0", "fontSize": 12 },
|
||||||
|
"n3": { "type": "Rectangle", "parent": "region", "left": 772, "top": 320, "width": 96, "height": 40, "fillColor": "#2a3040", "strokeColor": "#6a7a98", "corners": [4, 4, 4, 4] },
|
||||||
|
"n3t": { "type": "Text", "parent": "n3", "left": 802, "top": 332, "text": "test", "fontColor": "#c8d0e0", "fontSize": 12 },
|
||||||
|
|
||||||
|
"caption": { "type": "Text", "parent": "card", "left": 584, "top": 430, "text": "rendered SVG — crisp at any zoom (vector)", "fontColor": "#8a93a6", "fontSize": 11 },
|
||||||
|
|
||||||
|
"tagRender": { "type": "Text", "left": 1058, "top": 286, "text": "in-pane\nSVG widget", "fontColor": "#c8d8f0", "fontSize": 11 },
|
||||||
|
|
||||||
|
"foot": { "type": "Text", "left": 60, "top": 500, "text": "The D2 card (next frame) reuses THIS widget — D2 compiles to SVG, then renders through the same path.", "fontColor": "#6a7280", "fontSize": 11 }
|
||||||
|
},
|
||||||
|
"connectors": {
|
||||||
|
"flow": { "tailId": "srcBox", "headId": "card", "strokeColor": "#c8d8f0" },
|
||||||
|
"e1": { "tailId": "n1", "headId": "n2", "strokeColor": "#6a7a98" },
|
||||||
|
"e2": { "tailId": "n2", "headId": "n3", "strokeColor": "#6a7a98" },
|
||||||
|
"tRender": { "tailId": "tagRender", "headId": "region", "strokeColor": "#3a4250" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 72 KiB |
@@ -0,0 +1,320 @@
|
|||||||
|
The **Vibe CLI** (Mistral's open-source CLI tool for their AI models) **significantly changes the math** for replacing Claude in **clide**. Unlike Mistral's raw REST API, **Vibe CLI** is designed as a **local, interactive, and extensible** tool that **mimics many of Claude Code's features**, including **stdio-based interaction, tool execution, and session management**. This makes it a **far closer drop-in replacement** for Claude Code CLI than the raw API.
|
||||||
|
|
||||||
|
Here’s how **Vibe CLI** compares to **Claude Code CLI** and how it impacts the migration effort:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **🔹 Key Differences: Vibe CLI vs. Mistral API vs. Claude Code CLI**
|
||||||
|
|
||||||
|
| **Feature** | **Claude Code CLI** | **Mistral API** | **Vibe CLI** | **Impact on clide Migration** |
|
||||||
|
|---------------------------|---------------------|-----------------|--------------|-------------------------------|
|
||||||
|
| **Stdio-Based Interaction** | ✅ (stream-json) | ❌ (HTTP/SSE) | ✅ (stdio) | **🟢 Major Win: Vibe CLI supports stdio, enabling bidirectional communication like Claude.** |
|
||||||
|
| **Tool Execution** | ✅ (native + MCP) | ✅ (API `tools` param) | ✅ (native + MCP) | **🟢 Vibe CLI supports tools natively, including MCP.** |
|
||||||
|
| **Permission Prompts** | ✅ (`can_use_tool` stdio) | ❌ | ✅ (stdio-based) | **🟢 Vibe CLI supports permission gating via stdio (similar to Claude).** |
|
||||||
|
| **Session Persistence** | ✅ (`--resume`) | ❌ | ✅ (`--resume`) | **🟢 Vibe CLI supports session resumption.** |
|
||||||
|
| **Transcript Format** | ✅ (JSONL) | ❌ (API responses) | ✅ (JSONL) | **🟢 Vibe CLI uses a similar JSONL transcript format.** |
|
||||||
|
| **AskUserQuestion** | ✅ (`can_use_tool` stdio) | ❌ | ✅ (stdio-based) | **🟢 Vibe CLI supports interactive prompts via stdio.** |
|
||||||
|
| **Multi-Agent Teams** | ❌ (tmux-only) | ❌ | ❌ | **⚠️ Still missing, but clide’s MCP-based team orchestration can be reused.** |
|
||||||
|
| **Config System** | ✅ (`.claude/`) | ❌ | ✅ (`.vibe/`) | **🟢 Vibe CLI has its own config system (`.vibe/`).** |
|
||||||
|
| **Local Inference** | ❌ (Cloud-only) | ✅ | ✅ | **🟢 Vibe CLI supports local models (e.g., `mistral-large`, `codestral`).** |
|
||||||
|
| **MCP Support** | ✅ | ✅ | ✅ | **🟢 Vibe CLI supports MCP servers.** |
|
||||||
|
| **Streaming Responses** | ✅ (line-delimited JSON) | ✅ (SSE) | ✅ (stdio) | **🟢 Vibe CLI streams responses via stdio.** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **🔹 How Vibe CLI Changes the Migration Math**
|
||||||
|
|
||||||
|
### **1. Stdio Protocol Compatibility (🟢 Game-Changer)**
|
||||||
|
- **Claude Code CLI** uses a **custom stream-json protocol** over stdio for:
|
||||||
|
- Conversation streaming (`assistant`, `user`, `tool_use` events).
|
||||||
|
- Control requests (`can_use_tool` for permissions, `AskUserQuestion`).
|
||||||
|
- Session management (`--resume`, `--session-id`).
|
||||||
|
- **Vibe CLI** also uses **stdio for interaction**, including:
|
||||||
|
- **Streaming responses** (similar to Claude’s line-delimited JSON).
|
||||||
|
- **Tool execution** (native and MCP-based).
|
||||||
|
- **Permission prompts** (stdio-based gating, like Claude’s `can_use_tool`).
|
||||||
|
- **Session resumption** (`--resume` flag).
|
||||||
|
- **Impact**:
|
||||||
|
- **clide’s `StreamJsonProcess` can be adapted to work with Vibe CLI** with **minimal changes**.
|
||||||
|
- **No need for a custom wrapper** (unlike Mistral API).
|
||||||
|
- **Permission prompts and AskUserQuestion can be handled natively** (no manual reimplementation).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **2. Session Persistence (🟢 Major Win)**
|
||||||
|
- **Claude Code CLI**:
|
||||||
|
- Stores sessions in `~/.claude/projects/<munged-cwd>/<session-id>.jsonl`.
|
||||||
|
- Supports `--resume <session-id>` to restore a session.
|
||||||
|
- **Vibe CLI**:
|
||||||
|
- Stores sessions in `~/.vibe/sessions/<session-id>.jsonl`.
|
||||||
|
- Supports `--resume <session-id>` to restore a session.
|
||||||
|
- **Impact**:
|
||||||
|
- **clide can reuse its existing session management logic** (e.g., `SessionStorage`, `TranscriptReader`).
|
||||||
|
- **No need to manually store/replay transcripts** (Vibe CLI handles it).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **3. Tool Execution and Permission Prompts (🟢 Critical Parity)**
|
||||||
|
- **Claude Code CLI**:
|
||||||
|
- Uses `--permission-prompt-tool stdio` to route permission requests to the client.
|
||||||
|
- Emits `can_use_tool` control requests for tools like `Write`, `Bash`, etc.
|
||||||
|
- Supports `AskUserQuestion` via the same channel.
|
||||||
|
- **Vibe CLI**:
|
||||||
|
- **Also supports stdio-based permission prompts** (similar to Claude).
|
||||||
|
- Tools can be **allowed, denied, or gated** via stdio.
|
||||||
|
- Supports **interactive questions** (e.g., "Should I proceed?").
|
||||||
|
- **Impact**:
|
||||||
|
- **clide’s `ToolPrompt` and permission UI can be reused** with **minimal changes**.
|
||||||
|
- **No need to reimplement permission logic** from scratch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **4. Config System (🟢 Close Enough)**
|
||||||
|
- **Claude Code CLI**:
|
||||||
|
- Uses `.claude/` for skills, agents, hooks, and settings.
|
||||||
|
- clide’s `ClaudeConfig` service watches `.claude/` and probes the CLI for built-in commands.
|
||||||
|
- **Vibe CLI**:
|
||||||
|
- Uses `.vibe/` for config, tools, and MCP servers.
|
||||||
|
- Supports **custom commands, tools, and MCP integrations**.
|
||||||
|
- **Impact**:
|
||||||
|
- **clide’s config system can be adapted** to watch `.vibe/` instead of `.claude/`.
|
||||||
|
- **Minimal changes** to `ClaudeConfig` (rename paths, adjust probes).
|
||||||
|
|
||||||
|
---
|
||||||
|
### **5. Transcript Format (🟢 High Compatibility)**
|
||||||
|
- **Claude Code CLI**:
|
||||||
|
- Transcripts are stored as **JSONL** (one JSON object per line).
|
||||||
|
- Each line represents an event (`assistant`, `user`, `tool_use`, etc.).
|
||||||
|
- **Vibe CLI**:
|
||||||
|
- **Also uses JSONL for transcripts** (similar structure).
|
||||||
|
- Events include `assistant`, `user`, `tool_call`, etc.
|
||||||
|
- **Impact**:
|
||||||
|
- **clide’s `TranscriptReader` can be adapted** to parse Vibe CLI’s JSONL format.
|
||||||
|
- **Minimal changes** to the parsing logic.
|
||||||
|
|
||||||
|
---
|
||||||
|
### **6. MCP Support (🟢 Full Parity)**
|
||||||
|
- **Claude Code CLI**:
|
||||||
|
- Supports MCP servers via `--mcp-config`.
|
||||||
|
- clide hosts its own MCP server for team orchestration.
|
||||||
|
- **Vibe CLI**:
|
||||||
|
- **Natively supports MCP servers** (via `--mcp` or config).
|
||||||
|
- **Impact**:
|
||||||
|
- **clide’s MCP broker can be reused** for Vibe CLI.
|
||||||
|
- **Team orchestration can work the same way** (clide spawns multiple Vibe CLI processes and coordinates via MCP).
|
||||||
|
|
||||||
|
---
|
||||||
|
### **7. Local Inference (🟢 Bonus)**
|
||||||
|
- **Claude Code CLI**:
|
||||||
|
- Cloud-only (no local inference).
|
||||||
|
- **Vibe CLI**:
|
||||||
|
- Supports **local models** (e.g., `mistral-large`, `codestral`, or any Ollama-compatible model).
|
||||||
|
- **Impact**:
|
||||||
|
- **clide can leverage local models** for offline or private use cases.
|
||||||
|
- **No dependency on cloud APIs** (if using local models).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
---
|
||||||
|
## **🔹 Updated Migration Complexity with Vibe CLI**
|
||||||
|
|
||||||
|
| **Feature** | **Claude Code CLI** | **Mistral API** | **Vibe CLI** | **Migration Effort** |
|
||||||
|
|-------------|---------------------|-----------------|--------------|----------------------|
|
||||||
|
| **Stdio Protocol** | ✅ | ❌ | ✅ | **Low** (adapt `StreamJsonProcess`) |
|
||||||
|
| **Streaming Responses** | ✅ | ✅ (SSE) | ✅ (stdio) | **Low** |
|
||||||
|
| **Tool Execution** | ✅ | ✅ | ✅ | **Low** |
|
||||||
|
| **Permission Prompts** | ✅ | ❌ | ✅ | **Low** (reuse existing UI) |
|
||||||
|
| **AskUserQuestion** | ✅ | ❌ | ✅ | **Low** |
|
||||||
|
| **Session Persistence** | ✅ | ❌ | ✅ | **Low** (reuse existing logic) |
|
||||||
|
| **Transcript Format** | ✅ | ❌ | ✅ | **Low** (minor parsing adjustments) |
|
||||||
|
| **Config System** | ✅ | ❌ | ✅ | **Medium** (adapt `.vibe/` instead of `.claude/`) |
|
||||||
|
| **Multi-Agent Teams** | ✅ (clide-orchestrated) | ❌ | ❌ | **Medium** (reuse clide’s MCP broker) |
|
||||||
|
| **MCP Support** | ✅ | ✅ | ✅ | **Low** |
|
||||||
|
| **Local Inference** | ❌ | ✅ | ✅ | **Low** (bonus feature) |
|
||||||
|
|
||||||
|
---
|
||||||
|
---
|
||||||
|
## **🔹 Revised Effort Estimate with Vibe CLI**
|
||||||
|
|
||||||
|
| **Task** | **Mistral API** | **Vibe CLI** | **Savings** |
|
||||||
|
|----------|----------------|--------------|-------------|
|
||||||
|
| Replace stream-json with Mistral API | 3–5 days | **1–2 days** | **2–3 days** |
|
||||||
|
| Reimplement permission prompts | 5–7 days | **0 days** (reuse existing) | **5–7 days** |
|
||||||
|
| Reimplement AskUserQuestion | 3–5 days | **0 days** (reuse existing) | **3–5 days** |
|
||||||
|
| Session persistence | 3–5 days | **0 days** (reuse existing) | **3–5 days** |
|
||||||
|
| Update config system | 3–5 days | **1–2 days** (adapt `.vibe/`) | **2–3 days** |
|
||||||
|
| Update team orchestration | 2–3 days | **1–2 days** (reuse MCP broker) | **1 day** |
|
||||||
|
| Testing & debugging | 5–7 days | **3–5 days** | **2 days** |
|
||||||
|
| **Total** | **3–4 weeks** | **1–2 weeks** | **~2 weeks** |
|
||||||
|
|
||||||
|
---
|
||||||
|
---
|
||||||
|
## **🔹 Updated Recommendations with Vibe CLI**
|
||||||
|
|
||||||
|
### **🟢 Option 1: Direct Vibe CLI Integration (Recommended)**
|
||||||
|
**Approach**: Replace `claude` with `vibe` in clide’s spawn logic and adapt the existing protocol handlers.
|
||||||
|
**Complexity**: **Low-Medium (1–2 weeks)**
|
||||||
|
**Pros**:
|
||||||
|
- **Minimal changes** to clide’s core architecture.
|
||||||
|
- **Full parity** for **stdio protocol, permissions, sessions, and tools**.
|
||||||
|
- **Leverages Vibe CLI’s native features** (MCP, local inference, config).
|
||||||
|
**Cons**:
|
||||||
|
- **Multi-agent teams still require clide’s MCP broker** (but this is already implemented).
|
||||||
|
- **Minor adjustments** to `TranscriptReader` and `ClaudeConfig`.
|
||||||
|
|
||||||
|
#### **Implementation Steps**:
|
||||||
|
1. **Update `ClaudeStreamJsonProcess.start()`**:
|
||||||
|
- Replace `claude` with `vibe` in the spawn command.
|
||||||
|
- Adjust flags (e.g., `--resume` instead of `--session-id` if needed).
|
||||||
|
```dart
|
||||||
|
// Before:
|
||||||
|
Process.start('claude', ['--input-format', 'stream-json', ...]);
|
||||||
|
|
||||||
|
// After:
|
||||||
|
Process.start('vibe', ['--resume', sessionId, '--stdio', ...]);
|
||||||
|
```
|
||||||
|
2. **Adapt `StreamJsonSession`**:
|
||||||
|
- Update event parsing to handle **Vibe CLI’s JSONL format** (likely very similar to Claude’s).
|
||||||
|
- Ensure `can_use_tool` and `AskUserQuestion` are handled the same way.
|
||||||
|
3. **Update `ClaudeConfig`**:
|
||||||
|
- Replace `.claude/` with `.vibe/` for config watching.
|
||||||
|
- Adjust the **slash command probe** to use `vibe --help` or similar.
|
||||||
|
4. **Update Session Management**:
|
||||||
|
- Change session storage paths from `~/.claude/` to `~/.vibe/`.
|
||||||
|
5. **Test Extensively**:
|
||||||
|
- Validate **all control requests** (permissions, prompts).
|
||||||
|
- Test **session resumption** and **transcript parsing**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **🟡 Option 2: Vibe CLI + Custom Wrapper (Fallback)**
|
||||||
|
**Approach**: Use a **thin wrapper** around Vibe CLI to **normalize its output** to match Claude’s stream-json protocol **exactly**.
|
||||||
|
**Complexity**: **Low (1 week)**
|
||||||
|
**Pros**:
|
||||||
|
- **Zero changes to clide’s core** (only swap `claude` for `vibe-wrapper`).
|
||||||
|
- **Guarantees 100% protocol compatibility**.
|
||||||
|
**Cons**:
|
||||||
|
- **Adds an extra process** (minor latency).
|
||||||
|
- **Maintenance burden** (wrapper must stay in sync with Vibe CLI updates).
|
||||||
|
|
||||||
|
#### **Implementation Steps**:
|
||||||
|
1. **Create `vibe-wrapper`**:
|
||||||
|
- Written in **Rust, Go, or Python** (for performance).
|
||||||
|
- **Input**: Reads Claude-style stream-json from stdin.
|
||||||
|
- **Output**: Writes Vibe CLI-compatible stdio and translates responses back to Claude’s format.
|
||||||
|
- **Example**:
|
||||||
|
```bash
|
||||||
|
# Spawn Vibe CLI via wrapper
|
||||||
|
vibe-wrapper --session-id <id> --stdio
|
||||||
|
```
|
||||||
|
2. **Update clide’s Spawn Logic**:
|
||||||
|
- Replace `claude` with `vibe-wrapper` in `ClaudeStreamJsonProcess.start()`.
|
||||||
|
3. **Test**:
|
||||||
|
- Ensure **all events and control requests** are translated correctly.
|
||||||
|
|
||||||
|
---
|
||||||
|
### **🔴 Option 3: Mistral API (Not Recommended with Vibe CLI Available)**
|
||||||
|
**Approach**: Use Mistral’s raw REST API (as in the original report).
|
||||||
|
**Complexity**: **High (3–4 weeks)**
|
||||||
|
**Pros**:
|
||||||
|
- **No dependency on Vibe CLI** (if you prefer raw API control).
|
||||||
|
**Cons**:
|
||||||
|
- **Loses stdio protocol, permissions, and sessions** (must reimplement).
|
||||||
|
- **Higher effort** than Vibe CLI.
|
||||||
|
|
||||||
|
---
|
||||||
|
---
|
||||||
|
## **🔹 Feature Parity with Vibe CLI**
|
||||||
|
|
||||||
|
| **Feature** | **Claude Code CLI** | **Vibe CLI** | **Parity** | **Notes** |
|
||||||
|
|-------------|---------------------|--------------|------------|-----------|
|
||||||
|
| **Stdio Protocol** | ✅ | ✅ | **100%** | Vibe CLI supports stdio like Claude. |
|
||||||
|
| **Streaming Responses** | ✅ | ✅ | **100%** | Both use line-delimited JSON. |
|
||||||
|
| **Tool Execution** | ✅ | ✅ | **100%** | Vibe CLI supports native and MCP tools. |
|
||||||
|
| **Permission Prompts** | ✅ | ✅ | **100%** | Both use stdio for `can_use_tool`. |
|
||||||
|
| **AskUserQuestion** | ✅ | ✅ | **100%** | Both support interactive prompts. |
|
||||||
|
| **Session Persistence** | ✅ | ✅ | **100%** | Both support `--resume`. |
|
||||||
|
| **Transcript Format** | ✅ | ✅ | **95%** | Minor differences, easily adaptable. |
|
||||||
|
| **Config System** | ✅ | ✅ | **90%** | `.vibe/` vs `.claude/`, but similar structure. |
|
||||||
|
| **Multi-Agent Teams** | ✅ (clide-orchestrated) | ❌ | **80%** | clide’s MCP broker can orchestrate Vibe CLI agents. |
|
||||||
|
| **MCP Support** | ✅ | ✅ | **100%** | Both support MCP servers. |
|
||||||
|
| **Local Inference** | ❌ | ✅ | **Bonus** | Vibe CLI supports local models. |
|
||||||
|
|
||||||
|
---
|
||||||
|
---
|
||||||
|
## **🔹 Risks & Mitigations with Vibe CLI**
|
||||||
|
|
||||||
|
| **Risk** | **Likelihood** | **Impact** | **Mitigation** |
|
||||||
|
|----------|---------------|------------|----------------|
|
||||||
|
| **Vibe CLI protocol differences** | Low | Medium | Test thoroughly; adapt `StreamJsonSession` for minor differences. |
|
||||||
|
| **Vibe CLI updates breaking compatibility** | Medium | Medium | Pin to a specific Vibe CLI version; abstract spawn logic. |
|
||||||
|
| **Permission prompt differences** | Low | Medium | Validate `can_use_tool` behavior matches Claude’s. |
|
||||||
|
| **Session resumption bugs** | Low | Medium | Test `--resume` with various session states. |
|
||||||
|
| **MCP tool differences** | Medium | Low | Ensure Vibe CLI’s MCP support aligns with clide’s broker. |
|
||||||
|
|
||||||
|
---
|
||||||
|
---
|
||||||
|
## **🔹 Final Verdict: Vibe CLI is a Drop-In Replacement**
|
||||||
|
|
||||||
|
### **🟢 Why Vibe CLI is a Game-Changer**
|
||||||
|
1. **Stdio Protocol Parity**:
|
||||||
|
- Vibe CLI **natively supports stdio-based interaction**, just like Claude Code CLI.
|
||||||
|
- **No need for a custom wrapper** (unlike Mistral API).
|
||||||
|
2. **Permission Prompts & AskUserQuestion**:
|
||||||
|
- Vibe CLI **handles these via stdio**, so clide’s existing UI can be reused.
|
||||||
|
3. **Session Persistence**:
|
||||||
|
- Vibe CLI **supports `--resume`**, so clide’s session management works out of the box.
|
||||||
|
4. **Transcript Format**:
|
||||||
|
- Vibe CLI **uses JSONL**, so `TranscriptReader` can be adapted with minimal changes.
|
||||||
|
5. **MCP Support**:
|
||||||
|
- Vibe CLI **supports MCP servers**, so clide’s team orchestration can work the same way.
|
||||||
|
6. **Local Inference**:
|
||||||
|
- **Bonus**: Vibe CLI supports **local models**, enabling offline use.
|
||||||
|
|
||||||
|
### **📌 Migration Effort Summary**
|
||||||
|
| **Approach** | **Complexity** | **Effort** | **Feature Parity** | **Recommended?** |
|
||||||
|
|--------------|---------------|------------|--------------------|------------------|
|
||||||
|
| **Vibe CLI (Direct Integration)** | Low-Medium | **1–2 weeks** | **~95%** | ✅ **Yes (Best Option)** |
|
||||||
|
| **Vibe CLI + Wrapper** | Low | **1 week** | **100%** | ⚠️ **Only if protocol differences exist** |
|
||||||
|
| **Mistral API** | High | **3–4 weeks** | **~70%** | ❌ **Not recommended** |
|
||||||
|
|
||||||
|
### **🚀 Recommended Path Forward**
|
||||||
|
1. **Start with Direct Vibe CLI Integration**:
|
||||||
|
- Replace `claude` with `vibe` in clide’s spawn logic.
|
||||||
|
- Adapt `StreamJsonSession` and `TranscriptReader` for Vibe CLI’s output format.
|
||||||
|
- Update `ClaudeConfig` to watch `.vibe/` instead of `.claude/`.
|
||||||
|
- **Effort**: **1–2 weeks**.
|
||||||
|
- **Outcome**: **Near-100% feature parity** with minimal changes.
|
||||||
|
|
||||||
|
2. **Test Thoroughly**:
|
||||||
|
- Validate **all control requests** (permissions, prompts).
|
||||||
|
- Test **session resumption** and **multi-agent teams** (via MCP).
|
||||||
|
|
||||||
|
3. **Fallback to Wrapper (If Needed)**:
|
||||||
|
- If **protocol differences** are significant, use a **thin wrapper** to normalize Vibe CLI’s output.
|
||||||
|
- **Effort**: **1 additional week**.
|
||||||
|
|
||||||
|
---
|
||||||
|
---
|
||||||
|
## **🔹 Conclusion: Vibe CLI Makes Migration Trivial**
|
||||||
|
|
||||||
|
**Vibe CLI changes the math dramatically**:
|
||||||
|
- **Original (Mistral API)**: **3–4 weeks**, **major reimplementation**, **lost features**.
|
||||||
|
- **With Vibe CLI**: **1–2 weeks**, **minimal changes**, **~95% feature parity**.
|
||||||
|
|
||||||
|
**Vibe CLI is the closest thing to a drop-in replacement for Claude Code CLI** in clide. The **stdio protocol, permission prompts, session persistence, and MCP support** make it **almost identical** to Claude from clide’s perspective. The migration would primarily involve:
|
||||||
|
1. **Swapping `claude` for `vibe`** in spawn commands.
|
||||||
|
2. **Adapting paths** (`.claude/` → `.vibe/`).
|
||||||
|
3. **Minor adjustments** to event parsing and config probing.
|
||||||
|
|
||||||
|
**Final Answer**:
|
||||||
|
**Yes, Vibe CLI changes the math entirely. With Vibe CLI, replacing Claude in clide is a low-effort, high-parity migration (1–2 weeks). Without Vibe CLI (raw Mistral API), it’s a high-effort, partial-parity migration (3–4 weeks). Vibe CLI is the clear winner.**
|
||||||
@@ -143,6 +143,10 @@ You might also want, project-permitting:
|
|||||||
- [D-99: Remote session identity keyed on (host, workspace)](decisions/architecture.md#d-99-remote-session-identity-keyed-on-host-workspace) — _architecture_
|
- [D-99: Remote session identity keyed on (host, workspace)](decisions/architecture.md#d-99-remote-session-identity-keyed-on-host-workspace) — _architecture_
|
||||||
- [D-100: Fence `dart:ffi` behind conditional imports + web stubs to keep the web/WASM target compiling](decisions/tooling.md#d-100-fence-dartffi-behind-conditional-imports--web-stubs-to-keep-the-webwasm-target-compiling) — _tooling_
|
- [D-100: Fence `dart:ffi` behind conditional imports + web stubs to keep the web/WASM target compiling](decisions/tooling.md#d-100-fence-dartffi-behind-conditional-imports--web-stubs-to-keep-the-webwasm-target-compiling) — _tooling_
|
||||||
- [D-101: ClideSettings — one live-preferences access facade](decisions/architecture.md#d-101-clidesettings--one-live-preferences-access-facade) — _architecture_
|
- [D-101: ClideSettings — one live-preferences access facade](decisions/architecture.md#d-101-clidesettings--one-live-preferences-access-facade) — _architecture_
|
||||||
|
- [D-102: i18n routing — ext-id namespaces, `core` catalog, ClideSettings.i18n facade, contribution keys](decisions/accessibility.md#d-102-i18n-routing--ext-id-namespaces-core-catalog-clidesettingsi18n-facade-contribution-keys) — _accessibility_
|
||||||
|
- [D-103: Drawing-card primitive layer is SVG; the SVG renderer is the engine](decisions/architecture.md#d-103-drawing-card-primitive-layer-is-svg-the-svg-renderer-is-the-engine) — _architecture_
|
||||||
|
- [D-104: Explicit supporter-binary path overrides in user-scope settings](decisions/tooling.md#d-104-explicit-supporter-binary-path-overrides-in-user-scope-settings) — _tooling_
|
||||||
|
- [D-105: Support Vibe CLI as opt-in alternative to Claude Code CLI](decisions/llm.md#d-105-support-vibe-cli-as-opt-in-alternative-to-claude-code-cli) — _llm_
|
||||||
|
|
||||||
## Open questions
|
## Open questions
|
||||||
|
|
||||||
|
|||||||
@@ -32,4 +32,15 @@ A11y + i18n are Tier-0 contracts, not Tier-6 polish.
|
|||||||
- **Cost:** Two extra theme files per "named" theme when we add a11y variants. The bundled-theme contrast gate ([D-22](#d-22-wcag-aa-contrast-gate-on-bundled-themes)) needs a baseline/extended split so the named themes don't fail the strict pairs.
|
- **Cost:** Two extra theme files per "named" theme when we add a11y variants. The bundled-theme contrast gate ([D-22](#d-22-wcag-aa-contrast-gate-on-bundled-themes)) needs a baseline/extended split so the named themes don't fail the strict pairs.
|
||||||
- **Raised by:** 2026-05-17 — user intervened mid-T-114 when I had retuned `clide`/`midnight`/`paper`/`terminal` palette entries to satisfy the expanded `canonicalPairs`; reverted, decision written, T-114 will follow this rule.
|
- **Raised by:** 2026-05-17 — user intervened mid-T-114 when I had retuned `clide`/`midnight`/`paper`/`terminal` palette entries to satisfy the expanded `canonicalPairs`; reverted, decision written, T-114 will follow this rule.
|
||||||
|
|
||||||
|
### D-102: i18n routing — ext-id namespaces, `core` catalog, ClideSettings.i18n facade, contribution keys
|
||||||
|
- **Date:** 2026-06-19
|
||||||
|
- **Decision:** Implements [D-21](#d-21-i18n-is-a-tier-0-contract-fframe-pattern--locale-fallback-chain) across the whole app (epic T-462).
|
||||||
|
- **Namespaces:** an extension's catalog namespace IS its id (`builtin.<name>`); the ExtensionManager eager-loads it on activation, so a built-in localizes with no hand-maintained registry. Framework chrome outside any extension (`lib/widgets`, `lib/kernel`, the shared reader chrome) resolves under one **`core`** namespace, preloaded at boot.
|
||||||
|
- **Read path:** widgets resolve through the single [D-101](architecture.md) facade — `ClideSettings.i18n.string(context, key, namespace:, placeholder:)` (+ `.interpolated`) — null-safe (returns the placeholder when no kernel is in scope, so primitives render in isolated tests).
|
||||||
|
- **Manifest labels:** `CommandContribution` carries `titleKey`/`i18nNamespace`; the palette and menu resolve via a shared `localizedCommandTitle`, and the palette's fuzzy search matches the localized title. The settings schema carries `i18nNamespace`+`titleKey` on the category and `labelKey`/`helpKey`/option `labelKey` beneath it, threaded down by the renderer.
|
||||||
|
- **Storage:** catalogs are bundled assets at `assets/i18n/<locale>/<namespace>.json` — the locale is a *directory* (`en_us`, future `nl_nl`, `nl_be`, `en_eu`, …), so a new language is a new folder of the same namespace files, no renames.
|
||||||
|
- **Rationale:** makes a complete translation set (e.g. a Dutch pack) a pure data drop — no code. ext-id namespaces need no registry; the facade keeps one widget-facing read path for theme/fonts/i18n (D-101); the locale-dir layout is cleaner to maintain and mirrors how an external extension ships its own catalog.
|
||||||
|
- **Cost:** every extension's manifest gains optional key fields, and framework primitives now depend on the (null-safe) facade. The pure-data search matcher (`settingsFieldMatches`) still matches the English label — display localizes, search-by-translation does not (acceptable refinement).
|
||||||
|
- **Raised by:** 2026-06-19, epic T-462 (i18n everywhere). Builds on [D-101](architecture.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -542,4 +542,13 @@ Core, rendering, IPC, kernel, panel manager.
|
|||||||
- **Cross-reference:** Fonts landed it: T-460 (Inter default + UI picker) and T-471 (mono picker) migrated ~93 sites onto `ClideSettings.fonts`. Consumer migration of theme + i18n onto the facade, and the context-less font stragglers (T-472), are staged follow-ups. Values live in the kernel `SettingsStore`.
|
- **Cross-reference:** Fonts landed it: T-460 (Inter default + UI picker) and T-471 (mono picker) migrated ~93 sites onto `ClideSettings.fonts`. Consumer migration of theme + i18n onto the facade, and the context-less font stragglers (T-472), are staged follow-ups. Values live in the kernel `SettingsStore`.
|
||||||
- **Raised by:** 2026-06-17 — user, during T-471 font-flow design: "I do see reason in nesting them all in one settings object that dynamically loads so we can extend it in the future… plumb once, use many."
|
- **Raised by:** 2026-06-17 — user, during T-471 font-flow design: "I do see reason in nesting them all in one settings object that dynamically loads so we can extend it in the future… plumb once, use many."
|
||||||
|
|
||||||
|
### D-103: Drawing-card primitive layer is SVG; the SVG renderer is the engine
|
||||||
|
- **Date:** 2026-06-28
|
||||||
|
- **Decision:** The unified drawing card's primitive / scene-graph layer is **SVG**, not a bespoke `{type:"rect",…}` JSON vocabulary. The clide-owned `CustomPaint` SVG renderer (T-320) **is** the card's rendering engine — the foundation the rest builds on, not one template among many. The low-level escape hatch is "send SVG" (`template:"svg"` with inline `source`, or a `.svg` path); the high-level **templates** (image / icon / compare / d2) stay as JSON sugar but **lower to SVG** rather than to a custom primitive scene. A thin **Flutter overlay** renders the clide chrome that is *not* content — the per-object label/description caption widgets and the lightbox affordance — layered over the rendered SVG, anchored to elements via `data-label` / `data-description` / `data-lightbox` attributes. The **graph template is the exception:** it embeds the interactive native graph subsystem ([D-46], T-323) as a live widget rather than lowering to static SVG. Driven via `clide draw --file` (D-6); Flutter-free handler → `draw` MessageBus channel → Claude-extension injection.
|
||||||
|
- **Rationale:** SVG already *is* a declarative, standard scene-graph with precisely the primitives [D-91] wanted (rect/line/text/image/path at coordinates, transforms). Since a clide-owned SVG renderer is being built for the `svg` template regardless, inventing a parallel primitive JSON + a second renderer duplicates the work for one job. Making SVG the substrate collapses two renderers and two schemas into one, makes external SVG / graphviz / mermaid / d2 output renderable for free, and keeps the `.canvas`-viewer reuse ([D-91]) on the same path. Owning the SVG painter (`CustomPaint`, no package) honors "own the rendering stack" — SVG here is a document *format*, not a third-party renderer, and explicitly **not** the imperative HTML Canvas 2D API ("HTML `<canvas>`" in D-91 was a mental model to reject Obsidian's `.canvas` schema, never an API to port).
|
||||||
|
- **Cost:** The SVG-subset scope becomes the card's central design surface (the substrate, not one template) — but bounded, since the only SVG that must render is what clide's own templates + d2/graphviz emit; clide controls both ends. Re-sequences epic T-317: the SVG renderer (T-320) lands **before** the core envelope (T-318), inverting the prior `T-320 → T-318` dependency. The card is a hybrid (SVG content + Flutter overlay), so captions/interaction are not expressible in the document SVG itself.
|
||||||
|
- **Amends [D-91]:** D-91's "raw primitives (rects/lines/text at coordinates)" are now SVG elements; "templates lower onto the same primitive scene" becomes "templates lower to SVG"; the renderer foundation is the SVG painter (T-320), not a separate primitive engine inside T-318 (which becomes the document envelope + template dispatch + the Flutter overlay).
|
||||||
|
- **Cross-reference:** [D-91](#d-91-unified-conversation-drawing-card-backed-by-a-canvas-renderer), [D-78], [D-46], T-317 (epic), T-318 (envelope/dispatch/overlay), T-320 (SVG engine), T-494 (d2→svg), T-313/T-316/T-319 (templates), T-321/T-323 (graph). Schema: `docs/design/drawing-card-schema.md`.
|
||||||
|
- **Raised by:** 2026-06-28 — user, during the drawing-card schema draft: "if we are close to svg, are we not better off extending svg instead?" Confirmed the primitive layer should be SVG with templates lowering to it, a Flutter overlay for captions/interaction, and graph as the live-widget exception.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# LLM Integration Decisions
|
||||||
|
|
||||||
|
Decisions around Large Language Model integration, driver abstraction, and
|
||||||
|
multi-LLM support in clide.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### D-105: Support Vibe CLI as opt-in alternative to Claude Code CLI
|
||||||
|
|
||||||
|
- **Date:** 2026-06-28
|
||||||
|
- **Status:** confirmed
|
||||||
|
- **Supersedes:** None
|
||||||
|
- **See Also:** docs/spikes/vibe-cli-integration-analysis.md, D-6 (CLI/UI parity), D-56 (single process)
|
||||||
|
- **Raised by:** Mistral Vibe evaluation
|
||||||
|
|
||||||
|
#### Decision
|
||||||
|
|
||||||
|
**We will support Vibe CLI as an opt-in alternative to Claude Code CLI in clide.**
|
||||||
|
|
||||||
|
Vibe CLI provides **stdio protocol, permission prompts, session persistence,
|
||||||
|
and MCP support** — making it a **~95% feature-parity drop-in replacement** for
|
||||||
|
Claude Code CLI. This enables users to select their preferred LLM driver on a
|
||||||
|
per-repo basis while maintaining full backward compatibility with the
|
||||||
|
existing Claude flow.
|
||||||
|
|
||||||
|
#### Context
|
||||||
|
|
||||||
|
Claude Code CLI is clide's current and primary LLM. User requested the
|
||||||
|
ability to switch to Mistral's offerings. Two paths were considered:
|
||||||
|
|
||||||
|
1. **Raw Mistral API** — Requires reimplementing stdio protocol, permission
|
||||||
|
prompts, session management. Effort: 3-4 weeks. Parity: ~70%.
|
||||||
|
|
||||||
|
2. **Vibe CLI** — Mistral's open-source CLI that **natively supports** stdio
|
||||||
|
protocol, permission prompts (`can_use_tool`), session persistence
|
||||||
|
(`--resume`), MCP servers, and JSONL transcripts. Effort: 1-2 weeks.
|
||||||
|
Parity: ~95%.
|
||||||
|
|
||||||
|
The analysis in `docs/spikes/vibe-cli-integration-analysis.md` confirms Vibe CLI is the
|
||||||
|
**production-ready path**.
|
||||||
|
|
||||||
|
#### Architectural Choice
|
||||||
|
|
||||||
|
**Direct Vibe CLI Integration (Option 1 from analysis):**
|
||||||
|
- Replace `claude` with `vibe` in clide's spawn logic
|
||||||
|
- Adapt `StreamJsonProcess` and `TranscriptReader` for minor JSONL differences
|
||||||
|
- Update `ClaudeConfig` to watch `.vibe/` instead of `.claude/`
|
||||||
|
- Reuse clide's existing MCP broker for team orchestration
|
||||||
|
|
||||||
|
**Fallback (Option 2):** Thin wrapper to normalize Vibe CLI output to match
|
||||||
|
Claude's stream-json exactly. Only if protocol differences prove significant
|
||||||
|
during testing.
|
||||||
|
|
||||||
|
#### Implementation Constraints
|
||||||
|
|
||||||
|
- **Claude remains primary** — Vibe CLI is opt-in; default driver stays `claude`
|
||||||
|
- **Per-repo setting** — `llm.driver` in `.clide/config.yaml`, similar to multi-
|
||||||
|
Claude-account mechanism
|
||||||
|
- **Zero regression** — All existing Claude functionality preserved
|
||||||
|
- **Minimal abstraction** — Leverage Vibe CLI's native parity; avoid thick
|
||||||
|
conversion layers
|
||||||
|
|
||||||
|
#### Success Criteria
|
||||||
|
|
||||||
|
- User can select `llm.driver: claude | mistral` in per-repo config
|
||||||
|
- Vibe CLI sessions work end-to-end: streaming, permissions, tools, MCP, session resume
|
||||||
|
- `make test` passes with both drivers
|
||||||
|
- No performance regression in Claude flow
|
||||||
|
- Migration effort: 1-2 weeks
|
||||||
|
|
||||||
|
#### Consequences
|
||||||
|
|
||||||
|
**Positive:**
|
||||||
|
- Users can choose their preferred LLM
|
||||||
|
- Local inference support (offline/private use cases)
|
||||||
|
- Minimal code changes (~1-2 weeks)
|
||||||
|
- Full feature parity maintained
|
||||||
|
|
||||||
|
**Negative:**
|
||||||
|
- Additional binary dependency (Vibe CLI)
|
||||||
|
- Maintenance burden for two drivers (mitigated by abstraction)
|
||||||
|
- Testing matrix doubles (mitigated by CI feature flags)
|
||||||
|
|
||||||
|
---
|
||||||
@@ -48,7 +48,7 @@ Toolchain, supply chain, CI, ignore strategy.
|
|||||||
- **Decision:** Ship a self-contained Git binary from [dugite-native](https://github.com/desktop/dugite-native) (the same distribution GitHub Desktop bundles). Downloaded at build time via `make dugite-fetch`, stored under `native/dugite/`, gitignored. The `Toolchain` class resolves to the bundled binary first, falling back to system git on PATH.
|
- **Decision:** Ship a self-contained Git binary from [dugite-native](https://github.com/desktop/dugite-native) (the same distribution GitHub Desktop bundles). Downloaded at build time via `make dugite-fetch`, stored under `native/dugite/`, gitignored. The `Toolchain` class resolves to the bundled binary first, falling back to system git on PATH.
|
||||||
- **Rationale:** The macOS app sandbox blocks execution of Homebrew-installed git (symlinks resolve to Cellar paths that SBPL cannot match without freezing rendering). `/usr/bin/git` is an xcrun shim that refuses to run inside a sandbox. Bundling dugite-native makes clide self-contained — no dependency on Homebrew, Xcode CLT, or system git. The approach is proven: GitHub Desktop, Tower, and other git GUI apps all bundle their own git for the same reason.
|
- **Rationale:** The macOS app sandbox blocks execution of Homebrew-installed git (symlinks resolve to Cellar paths that SBPL cannot match without freezing rendering). `/usr/bin/git` is an xcrun shim that refuses to run inside a sandbox. Bundling dugite-native makes clide self-contained — no dependency on Homebrew, Xcode CLT, or system git. The approach is proven: GitHub Desktop, Tower, and other git GUI apps all bundle their own git for the same reason.
|
||||||
- **Alternatives rejected:** (R) libgit2 via FFI — missing porcelain commands (pull/push/rebase), no hooks, would require rewriting GitClient. (R) Build git from source — dugite-native already does this with better infra. (R) SBPL exceptions for Homebrew — `(subpath "/opt/homebrew")` for process-exec freezes Flutter rendering on macOS 26.
|
- **Alternatives rejected:** (R) libgit2 via FFI — missing porcelain commands (pull/push/rebase), no hooks, would require rewriting GitClient. (R) Build git from source — dugite-native already does this with better infra. (R) SBPL exceptions for Homebrew — `(subpath "/opt/homebrew")` for process-exec freezes Flutter rendering on macOS 26.
|
||||||
- **Cost:** ~57 MB download (~199 MB unpacked, stripped at build time). Must track dugite-native releases for security updates (tracked in T-88). GPL-2.0 (git binary) applies to the bundled artefact, not to clide's MIT code.
|
- **Cost:** ~57 MB download (~199 MB unpacked, stripped at build time). Must track dugite-native releases for security updates — `make dugite-check` compares the Makefile pin against the latest upstream release and flags CVE mentions; run quarterly or on a git CVE (the calendar is T-88; the bump machine is D-63 / T-25). GPL-2.0 (git binary) applies to the bundled artefact, not to clide's MIT code.
|
||||||
- **Cross-reference:** [D-31](#d-31-prefer-zero-deps-exact-pin), [D-42](#d-42-dependencies-documented-in-licensesyaml).
|
- **Cross-reference:** [D-31](#d-31-prefer-zero-deps-exact-pin), [D-42](#d-42-dependencies-documented-in-licensesyaml).
|
||||||
- **Raised by:** 2026-04-25 macOS sandbox investigation.
|
- **Raised by:** 2026-04-25 macOS sandbox investigation.
|
||||||
|
|
||||||
@@ -110,3 +110,11 @@ Toolchain, supply chain, CI, ignore strategy.
|
|||||||
- **Cost:** An ongoing tax — every new native binding needs a web stub + conditional import, and the wasm compile gate must stay green. Accepted deliberately: the maintainer values keeping the door open over avoiding that tax. Functional web parity is explicitly **not** promised — only that the tree compiles to wasm and the Playwright/e2e harness ([D-26](process.md)) can run again.
|
- **Cost:** An ongoing tax — every new native binding needs a web stub + conditional import, and the wasm compile gate must stay green. Accepted deliberately: the maintainer values keeping the door open over avoiding that tax. Functional web parity is explicitly **not** promised — only that the tree compiles to wasm and the Playwright/e2e harness ([D-26](process.md)) can run again.
|
||||||
- **Cross-reference:** [Q-50](../questions/architecture.md#q-50-webwasm-target-after-the-dartffi-pivot--fence-fix-or-drop), [D-32](#d-32-ci--github-actions-linux--windows-runners-active) (the withheld web-WASM e2e job lands once this fence is implemented), the tree-sitter FFI pivot.
|
- **Cross-reference:** [Q-50](../questions/architecture.md#q-50-webwasm-target-after-the-dartffi-pivot--fence-fix-or-drop), [D-32](#d-32-ci--github-actions-linux--windows-runners-active) (the withheld web-WASM e2e job lands once this fence is implemented), the tree-sitter FFI pivot.
|
||||||
- **Raised by:** 2026-06-15 — user, reconciling T-384: "a happy accident for the web-based UI lives a bit more hopeful for me than it does in CLAUDE.md … let's fence dart:ffi with web stubs."
|
- **Raised by:** 2026-06-15 — user, reconciling T-384: "a happy accident for the web-based UI lives a bit more hopeful for me than it does in CLAUDE.md … let's fence dart:ffi with web stubs."
|
||||||
|
|
||||||
|
### D-104: Explicit supporter-binary path overrides in user-scope settings
|
||||||
|
- **Date:** 2026-06-28
|
||||||
|
- **Decision:** clide resolves each **external** supporter binary (`claude`, `d2`, and future supporter tools — **not** bundled `pql`/`git`, which keep [D-58]/[D-59]) through an **explicit `tools:` map in user-scope settings**: tool-name → absolute path. The map holds concrete, user-visible, user-editable paths and is the **first** step in resolution — if an entry is set, clide uses that exact path (honest error if it is missing / not executable, falling **down the chain with a warning** rather than hard-failing). On **first run**, clide auto-detects each tool **once** — probing the login-shell PATH (T-439) plus well-known dirs (`~/.local/bin`, `/usr/local/bin`, `/opt/homebrew`, Homebrew-on-Linux `/home/linuxbrew/.linuxbrew/bin`) — and **writes the discovered absolute paths into the map**, so detection is a one-time *populate*, not a per-launch heuristic; thereafter the pinned explicit value wins. A **re-detect** action re-runs the probe (e.g. after installing a tool). Unset/undetected tools fall back to the existing chain (bundled/pinned per [D-58]/[D-59], then login-shell PATH). **User-scope only, keyed by machine ([D-93]) — never committed** (absolute paths are machine-specific). Generalizes [D-58]'s `CLIDE_PQL_BIN` override to every supporter tool; surfaced in the Config tab and the Problems panel for unresolved tools.
|
||||||
|
- **Rationale:** PATH-probing (T-439) is adaptive but brittle on non-standard installs — a login-shell probe misses Homebrew when `brew shellenv` lives only in `~/.bashrc` (login shells source `.bash_profile`/`.profile`, not `.bashrc`). Materializing resolution into explicit, pinned paths makes it deterministic and debuggable: "tool not found" becomes a one-line settings fix, and the value is *visible* rather than recomputed by heuristic each launch. First-run auto-detect keeps it zero-config for standard installs; **pinning the result instead of re-probing is what makes it explicit** — escaping the heuristic fragility while keeping the convenience.
|
||||||
|
- **Cost:** A first-run detection pass plus a small settings surface. Stale pins (a tool moved on a brew upgrade) must fall back + warn, not hard-fail. The map is per-machine, so it does not travel with the repo — each machine detects once.
|
||||||
|
- **Cross-reference:** [D-58](#d-58-ship-pql-bundled-with-clide) (pql resolution + `CLIDE_PQL_BIN` override), [D-59](#d-59-bundled-git-via-dugite-native), [D-93](architecture.md#d-93-clide-writes-no-directories-of-its-own-into-the-workspace) (user-scope state), T-439 (login-shell-derived PATH), T-494 (the d2 template — first consumer).
|
||||||
|
- **Raised by:** 2026-06-28 — user, during the drawing-card SVG/d2 work after the login-shell PATH probe was seen to miss linuxbrew: "should we just add the hard paths to the supporting binaries to the clide settings file?" + "auto detect on first run sounds solid" — explicit pinned paths, populated by a one-time first-run probe.
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
/// Modal that hosts `CLAUDE_CONFIG_DIR=<dir> claude login` in a terminal pane
|
||||||
|
/// (T-485, epic T-476). clide writes no auth code: the Claude CLI owns the OAuth
|
||||||
|
/// browser flow, and clide just provides the TTY + the per-account config dir,
|
||||||
|
/// so the resulting credentials land in `<dir>` rather than the global
|
||||||
|
/// `~/.claude` (D-64 — one CLI-initiated browser flow, on explicit action,
|
||||||
|
/// nothing in the background). No-Material (D-7); shown via the DialogRouter.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:clide/builtin/terminal/src/terminal_pane.dart';
|
||||||
|
import 'package:clide/widgets/widgets.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class ClaudeLoginDialog extends StatelessWidget {
|
||||||
|
const ClaudeLoginDialog({super.key, required this.name, required this.dir, required this.onClose, this.cwd});
|
||||||
|
|
||||||
|
/// Account display name (for the title).
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
/// The account's `CLAUDE_CONFIG_DIR` — where `claude login` writes credentials.
|
||||||
|
final String dir;
|
||||||
|
|
||||||
|
/// Working directory for the spawned `claude login` (defaults to the
|
||||||
|
/// workspace); irrelevant to auth, but keeps the pane oriented.
|
||||||
|
final String? cwd;
|
||||||
|
|
||||||
|
final VoidCallback onClose;
|
||||||
|
|
||||||
|
KeyEventResult _onKey(FocusNode node, KeyEvent e) {
|
||||||
|
if (e is KeyDownEvent && e.logicalKey == LogicalKeyboardKey.escape) {
|
||||||
|
onClose();
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = ClideSettings.theme.of(context).surface;
|
||||||
|
return Focus(
|
||||||
|
autofocus: true,
|
||||||
|
onKeyEvent: _onKey,
|
||||||
|
child: Container(
|
||||||
|
width: 760,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.panelBackground,
|
||||||
|
border: Border.all(color: theme.globalBorder),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(14, 12, 14, 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ClideText('Sign in: $name', fontSize: clideFontBody, color: theme.globalForeground),
|
||||||
|
),
|
||||||
|
Semantics(
|
||||||
|
button: true,
|
||||||
|
label: 'Close',
|
||||||
|
excludeSemantics: true,
|
||||||
|
child: ClideTappable(
|
||||||
|
key: const Key('account-login-close'),
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
onTap: onClose,
|
||||||
|
builder: (ctx, hovered, _) =>
|
||||||
|
ClideIcon(PhosphorIcons.byName('x'), size: 14, color: hovered ? theme.globalForeground : theme.globalTextMuted),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(14, 0, 14, 8),
|
||||||
|
child: ClideText('Running `claude login` against $dir — finish the browser sign-in, then close.', muted: true, fontSize: clideFontSmall),
|
||||||
|
),
|
||||||
|
// Fixed height — TerminalPane needs a bounded box; the dialog itself
|
||||||
|
// sizes to its content (mainAxisSize.min).
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(8, 0, 8, 8),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 380,
|
||||||
|
child: TerminalPane(argv: const ['claude', 'login'], env: {'CLAUDE_CONFIG_DIR': dir}, cwdOverride: cwd),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
/// Per-repo Claude account registry (T-483, epic T-476) — the user-scope
|
||||||
|
/// persistence + typed reader/writer every other child of the epic consumes.
|
||||||
|
///
|
||||||
|
/// Two durable concerns, both in the app (user) settings layer so they are
|
||||||
|
/// PER-USER, never committed to a repo:
|
||||||
|
///
|
||||||
|
/// - **Account registry** `app.claude.accounts` — a list of `{name, dir}`
|
||||||
|
/// pairs: the Claude accounts this user set up. `dir` is the
|
||||||
|
/// `CLAUDE_CONFIG_DIR` Claude Code reads from for that account.
|
||||||
|
/// - **Workspace binding** `app.claude.account.<workspace-hash>` → account
|
||||||
|
/// name. The hash is the SAME FNV-1a 64-bit hex D-70 uses for the socket
|
||||||
|
/// path ([fnv1a64Hex]/[canonicalWorkspaceKey]), so a workspace's account and
|
||||||
|
/// its socket agree. Unset = use Claude's default (no injection).
|
||||||
|
///
|
||||||
|
/// Flutter-free (foundation only, via SettingsStore); no process spawning, no
|
||||||
|
/// UI, no CLI — those are downstream tickets (T-484/T-480/T-481/T-482).
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:clide/kernel/src/settings.dart';
|
||||||
|
import 'package:clide/src/ipc/paths.dart' show canonicalWorkspaceKey, fnv1a64Hex;
|
||||||
|
|
||||||
|
/// One configured Claude account: a user-chosen [name] (`personal`, `work`,
|
||||||
|
/// `client-acme`) and the [dir] Claude Code reads as `CLAUDE_CONFIG_DIR`.
|
||||||
|
class Account {
|
||||||
|
const Account({required this.name, required this.dir});
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final String dir;
|
||||||
|
|
||||||
|
Map<String, String> toJson() => {'name': name, 'dir': dir};
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => other is Account && other.name == name && other.dir == dir;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(name, dir);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'Account($name → $dir)';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `~/.claude-*` directory the bootstrap probe found that looks like a Claude
|
||||||
|
/// config dir — an adoption candidate. [name] is the suggested account name
|
||||||
|
/// (the suffix after `.claude-`); registering it is welcome-view UX (T-481).
|
||||||
|
class DetectedAccount {
|
||||||
|
const DetectedAccount({required this.name, required this.dir});
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final String dir;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => other is DetectedAccount && other.name == name && other.dir == dir;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(name, dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
class AccountRegistry {
|
||||||
|
AccountRegistry(this._store);
|
||||||
|
|
||||||
|
final SettingsStore _store;
|
||||||
|
|
||||||
|
/// Key holding the `{name, dir}` account list (app/user scope).
|
||||||
|
static const accountsKey = 'app.claude.accounts';
|
||||||
|
|
||||||
|
/// Per-workspace binding key: account name keyed by workspace hash.
|
||||||
|
static String bindingKey(String cwd) => 'app.claude.account.${workspaceHash(cwd)}';
|
||||||
|
|
||||||
|
/// FNV-1a 64-bit hex of the canonicalised workspace root — the SAME hash D-70
|
||||||
|
/// derives for the socket path, so a workspace's binding and its socket
|
||||||
|
/// agree. Trailing separators are stripped so `/repo` and `/repo/` map alike.
|
||||||
|
static String workspaceHash(String cwd) => fnv1a64Hex(canonicalWorkspaceKey(_stripTrailingSep(cwd)));
|
||||||
|
|
||||||
|
static String _stripTrailingSep(String p) {
|
||||||
|
var s = p;
|
||||||
|
while (s.length > 1 && (s.endsWith('/') || s.endsWith(r'\'))) {
|
||||||
|
s = s.substring(0, s.length - 1);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The configured accounts, in stored order. Tolerant of a malformed or
|
||||||
|
/// partially-written entry (skips anything missing a string name + dir).
|
||||||
|
List<Account> get accounts {
|
||||||
|
final raw = _store.get<List>(accountsKey);
|
||||||
|
if (raw == null) return const [];
|
||||||
|
final out = <Account>[];
|
||||||
|
for (final e in raw) {
|
||||||
|
if (e is Map && e['name'] is String && e['dir'] is String) {
|
||||||
|
out.add(Account(name: e['name'] as String, dir: e['dir'] as String));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
Account? accountByName(String name) {
|
||||||
|
for (final a in accounts) {
|
||||||
|
if (a.name == name) return a;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The account bound to [cwd], or null when unbound (or bound to a name that
|
||||||
|
/// no longer exists — treated as unbound, so a removed account degrades to
|
||||||
|
/// Claude's default rather than erroring).
|
||||||
|
Account? accountForWorkspace(String cwd) {
|
||||||
|
final name = _store.get<String>(bindingKey(cwd));
|
||||||
|
return name == null ? null : accountByName(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The raw account NAME bound to [cwd] — independent of whether that account
|
||||||
|
/// still exists in the registry — or null when unbound. (`accountForWorkspace`
|
||||||
|
/// resolves to the Account and is null for a dangling binding; this is the
|
||||||
|
/// stored name, for list/unset reporting.)
|
||||||
|
String? boundName(String cwd) => _store.get<String>(bindingKey(cwd));
|
||||||
|
|
||||||
|
/// Every account name some workspace is bound to — for "is this account in
|
||||||
|
/// use" checks before removal (T-480). Scans the `app.claude.account.<hash>`
|
||||||
|
/// binding keys (NOT the `app.claude.accounts` list, a different key).
|
||||||
|
Set<String> boundAccountNames() {
|
||||||
|
const prefix = 'app.claude.account.';
|
||||||
|
final out = <String>{};
|
||||||
|
for (final key in _store.keysAt(SettingsScope.app)) {
|
||||||
|
if (!key.startsWith(prefix)) continue;
|
||||||
|
final v = _store.get<String>(key);
|
||||||
|
if (v != null) out.add(v);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add (or replace, by name) an account. New default dir is the caller's
|
||||||
|
/// concern (T-480); the registry stores whatever [dir] it's given.
|
||||||
|
Future<void> registerAccount(String name, String dir) async {
|
||||||
|
await _writeAccounts([...accounts.where((a) => a.name != name), Account(name: name, dir: dir)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeAccount(String name) async {
|
||||||
|
await _writeAccounts(accounts.where((a) => a.name != name).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> bindWorkspace(String cwd, String name) async {
|
||||||
|
await _store.setAt(SettingsScope.app, bindingKey(cwd), name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> unbindWorkspace(String cwd) async {
|
||||||
|
await _store.removeAt(SettingsScope.app, bindingKey(cwd));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _writeAccounts(List<Account> list) async {
|
||||||
|
await _store.setAt(SettingsScope.app, accountsKey, [for (final a in list) a.toJson()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Best-effort, read-only check of whether an account config dir holds live
|
||||||
|
/// credentials (T-482) — for the "signed in / not signed in" indicator. True
|
||||||
|
/// when the dir has a `.credentials.json` (Linux/Windows) or its `.claude.json`
|
||||||
|
/// carries an `oauthAccount` marker. Never mutates; under-reports on macOS,
|
||||||
|
/// where Claude Code keeps credentials in the system keychain rather than a file.
|
||||||
|
bool accountIsSignedIn(String dir) {
|
||||||
|
if (File('$dir/.credentials.json').existsSync()) return true;
|
||||||
|
final cfg = File('$dir/.claude.json');
|
||||||
|
if (!cfg.existsSync()) return false;
|
||||||
|
try {
|
||||||
|
return cfg.readAsStringSync().contains('"oauthAccount"');
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether [dir] is safe to `rm -rf` as a purged account config dir
|
||||||
|
/// (`remove --purge`, T-480): it must be a `~/.claude-*` directory that is a
|
||||||
|
/// DIRECT child of [home]. Anything else — an absolute path elsewhere, a nested
|
||||||
|
/// path, the real `~/.claude` — is rejected even though the path came from our
|
||||||
|
/// own registry. A wrong recursive delete is unrecoverable, so the predicate
|
||||||
|
/// is deliberately strict.
|
||||||
|
bool isPurgeableAccountDir(String dir, String home) {
|
||||||
|
if (home.isEmpty) return false;
|
||||||
|
final base = dir.split('/').last;
|
||||||
|
return dir == '$home/$base' && base.startsWith('.claude-');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bootstrap probe (T-483): existing `~/.claude-*` directories that look like a
|
||||||
|
/// Claude config dir (have a `.claude.json` file or a `sessions/` dir), as
|
||||||
|
/// adoption candidates. Pure read — mutates nothing; the welcome view (T-481)
|
||||||
|
/// decides whether to register them. Sorted by suggested name.
|
||||||
|
List<DetectedAccount> probeExistingAccountDirs(String home) {
|
||||||
|
final out = <DetectedAccount>[];
|
||||||
|
final dir = Directory(home);
|
||||||
|
if (!dir.existsSync()) return out;
|
||||||
|
for (final entry in dir.listSync(followLinks: false)) {
|
||||||
|
if (entry is! Directory) continue;
|
||||||
|
final base = entry.path.split(Platform.pathSeparator).last;
|
||||||
|
if (!base.startsWith('.claude-')) continue;
|
||||||
|
final looksLikeConfig = File('${entry.path}/.claude.json').existsSync() || Directory('${entry.path}/sessions').existsSync();
|
||||||
|
if (!looksLikeConfig) continue;
|
||||||
|
out.add(DetectedAccount(name: base.substring('.claude-'.length), dir: entry.path));
|
||||||
|
}
|
||||||
|
out.sort((a, b) => a.name.compareTo(b.name));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/// The per-repo account roadblock shown right after a new project is created
|
||||||
|
/// (T-488, story T-486). Only a freshly-created project reaches here (the
|
||||||
|
/// welcome dialog announces it on projectCreatedChannel) — existing opens never
|
||||||
|
/// prompt. The new project is already the open workspace, so the embedded
|
||||||
|
/// per-workspace picker binds it directly; the accounts list lets a first-run
|
||||||
|
/// user add + sign in to an account before picking. No-Material (D-7).
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:clide/builtin/claude/src/account_settings_control.dart';
|
||||||
|
import 'package:clide/widgets/widgets.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class ClaudeAccountRoadblockDialog extends StatelessWidget {
|
||||||
|
const ClaudeAccountRoadblockDialog({super.key, required this.projectName, required this.onClose});
|
||||||
|
|
||||||
|
final String projectName;
|
||||||
|
final VoidCallback onClose;
|
||||||
|
|
||||||
|
KeyEventResult _onKey(FocusNode node, KeyEvent e) {
|
||||||
|
if (e is KeyDownEvent && e.logicalKey == LogicalKeyboardKey.escape) {
|
||||||
|
onClose();
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = ClideSettings.theme.of(context).surface;
|
||||||
|
return Focus(
|
||||||
|
autofocus: true,
|
||||||
|
onKeyEvent: _onKey,
|
||||||
|
child: Container(
|
||||||
|
width: 560,
|
||||||
|
constraints: const BoxConstraints(maxHeight: 540),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.modalSurfaceBackground,
|
||||||
|
border: Border.all(color: theme.modalSurfaceBorder),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(18),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
ClideText('Claude account for $projectName', fontSize: clideFontDialogTitle, fontWeight: FontWeight.w600, color: theme.globalForeground),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
ClideText(
|
||||||
|
'Pick which Claude account this new project runs under, or keep the default system login. You can change it later in Settings or the pane badge.',
|
||||||
|
muted: true,
|
||||||
|
fontSize: clideFontMeta,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
ClideText('Account for this project', fontSize: clideFontMeta, muted: true),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
const Align(alignment: Alignment.centerLeft, child: ClaudeWorkspaceAccountControl()),
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
ClideText('Accounts', fontSize: clideFontMeta, muted: true),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
const ClaudeAccountsListControl(),
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: ClideButton(label: 'Continue', onPressed: onClose),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,433 @@
|
|||||||
|
import 'dart:io' show Platform;
|
||||||
|
|
||||||
|
import 'package:clide/builtin/claude/src/account_registry.dart';
|
||||||
|
import 'package:clide/kernel/kernel.dart';
|
||||||
|
import 'package:clide/src/daemon/claude_account_commands.dart' show accountActionChannel;
|
||||||
|
import 'package:clide/widgets/widgets.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
/// Bind (or, with [name] null, unbind) [cwd] to a Claude account and publish on
|
||||||
|
/// [accountActionChannel] so the session respawns (T-480) and the IDE lock
|
||||||
|
/// re-syncs (T-479). The registry write sets the in-memory binding
|
||||||
|
/// synchronously then flushes; publishing before the flush keeps the bus
|
||||||
|
/// consumers in step. Shared by the settings picker and the pane badge (T-481).
|
||||||
|
Future<void> bindWorkspaceAccount(KernelServices services, String cwd, String? name) async {
|
||||||
|
final reg = AccountRegistry(services.settings);
|
||||||
|
if (name == null) {
|
||||||
|
final previous = reg.boundName(cwd);
|
||||||
|
final write = reg.unbindWorkspace(cwd);
|
||||||
|
services.messages.publish('ui', accountActionChannel, {'action': 'unset', 'cwd': cwd, 'previous': previous});
|
||||||
|
await write;
|
||||||
|
} else {
|
||||||
|
final write = reg.bindWorkspace(cwd, name);
|
||||||
|
services.messages.publish('ui', accountActionChannel, {'action': 'set', 'name': name, 'cwd': cwd});
|
||||||
|
await write;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A stable, token-derived accent for an account [name] so each window's badge
|
||||||
|
/// reads at a glance (T-481). Hash-indexed into a fixed set of theme tokens —
|
||||||
|
/// never an arbitrary colour (the palette stays theme-owned). Null name (the
|
||||||
|
/// default account) returns the muted token.
|
||||||
|
Color accountAccent(String? name, SurfaceTokens tokens) {
|
||||||
|
if (name == null || name.isEmpty) return tokens.globalTextMuted;
|
||||||
|
final accents = [tokens.globalFocus, tokens.statusSuccess, tokens.statusWarning, tokens.statusError, tokens.buttonBackground];
|
||||||
|
var h = 0;
|
||||||
|
for (final unit in name.codeUnits) {
|
||||||
|
h = (h * 31 + unit) & 0x7fffffff;
|
||||||
|
}
|
||||||
|
return accents[h % accents.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Settings control for "Account for this workspace" (T-482, epic T-476). A
|
||||||
|
/// dropdown of the registered Claude accounts plus a Default option; picking one
|
||||||
|
/// binds (or unbinds) the current workspace and publishes on
|
||||||
|
/// [accountActionChannel] — the same channel the CLI `set`/`unset` verbs use, so
|
||||||
|
/// the session respawns onto the account (T-480) and the IDE lock re-syncs
|
||||||
|
/// (T-479). Registry writes flow through the shared [SettingsStore], whose
|
||||||
|
/// notifier this control listens to, so it stays live for both UI and CLI edits.
|
||||||
|
class ClaudeWorkspaceAccountControl extends StatefulWidget {
|
||||||
|
const ClaudeWorkspaceAccountControl({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ClaudeWorkspaceAccountControl> createState() => _ClaudeWorkspaceAccountControlState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ClaudeWorkspaceAccountControlState extends State<ClaudeWorkspaceAccountControl> {
|
||||||
|
final ClideOverlayController _overlay = ClideOverlayController();
|
||||||
|
SettingsStore? _settings;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
final settings = ClideKernel.maybeOf(context)?.settings;
|
||||||
|
if (identical(settings, _settings)) return;
|
||||||
|
_settings?.removeListener(_onChange);
|
||||||
|
_settings = settings;
|
||||||
|
_settings?.addListener(_onChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onChange() {
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_settings?.removeListener(_onChange);
|
||||||
|
_overlay.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
static const _defaultLabel = 'Default';
|
||||||
|
|
||||||
|
Future<void> _bind(KernelServices services, String cwd, String? name) async {
|
||||||
|
_overlay.close();
|
||||||
|
await bindWorkspaceAccount(services, cwd, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final tokens = ClideSettings.theme.of(context).surface;
|
||||||
|
final services = ClideKernel.maybeOf(context);
|
||||||
|
final cwd = services?.settings.projectDir?.path;
|
||||||
|
if (services == null || cwd == null) {
|
||||||
|
return ClideText('Open a workspace to bind a Claude account.', fontSize: clideFontCaption, color: tokens.globalTextMuted);
|
||||||
|
}
|
||||||
|
|
||||||
|
final reg = AccountRegistry(services.settings);
|
||||||
|
final accounts = reg.accounts;
|
||||||
|
if (accounts.isEmpty) {
|
||||||
|
return ClideText('No accounts yet — add one with `clide claude account add <name>`.', fontSize: clideFontCaption, color: tokens.globalTextMuted);
|
||||||
|
}
|
||||||
|
|
||||||
|
final boundName = reg.boundName(cwd);
|
||||||
|
final selectedLabel = boundName ?? _defaultLabel;
|
||||||
|
return ClideAnchoredOverlay(
|
||||||
|
controller: _overlay,
|
||||||
|
align: ClideAnchorAlign.start,
|
||||||
|
overlayBuilder: (ctx, c) => ClideMenu(
|
||||||
|
onClose: c.close,
|
||||||
|
entries: [
|
||||||
|
ClideMenuItem(
|
||||||
|
label: _defaultLabel,
|
||||||
|
active: boundName == null,
|
||||||
|
semanticLabel: 'Account for this workspace: $_defaultLabel',
|
||||||
|
onSelect: () => _bind(services, cwd, null),
|
||||||
|
),
|
||||||
|
const ClideMenuSeparator(),
|
||||||
|
for (final a in accounts)
|
||||||
|
ClideMenuItem(
|
||||||
|
label: a.name,
|
||||||
|
active: a.name == boundName,
|
||||||
|
semanticLabel: 'Account for this workspace: ${a.name}',
|
||||||
|
onSelect: () => _bind(services, cwd, a.name),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
anchor: Semantics(
|
||||||
|
button: true,
|
||||||
|
label: 'Account for this workspace: $selectedLabel. Click to change.',
|
||||||
|
excludeSemantics: true,
|
||||||
|
onTap: _overlay.toggle,
|
||||||
|
child: ClideTappable(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
onTap: _overlay.toggle,
|
||||||
|
builder: (ctx, hovered, _) => Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: tokens.panelBackground,
|
||||||
|
border: Border.all(color: hovered ? tokens.panelActiveBorder : tokens.dividerColor),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ClideText(selectedLabel, color: tokens.globalForeground),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
ClideIcon(PhosphorIcons.byName('caret-down'), size: 10, color: tokens.globalTextMuted),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Settings control for the global Claude accounts registry (T-482, epic
|
||||||
|
/// T-476). Lists each registered account — sign-in status dot, name, config
|
||||||
|
/// dir — with re-login and remove affordances, plus an inline "add account"
|
||||||
|
/// field. Management flows through the AccountRegistry + accountActionChannel
|
||||||
|
/// (the CLI verbs' path); removal is refused while a workspace is bound, to
|
||||||
|
/// match `clide claude account remove`.
|
||||||
|
class ClaudeAccountsListControl extends StatefulWidget {
|
||||||
|
const ClaudeAccountsListControl({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ClaudeAccountsListControl> createState() => _ClaudeAccountsListControlState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ClaudeAccountsListControlState extends State<ClaudeAccountsListControl> {
|
||||||
|
final TextEditingController _name = TextEditingController();
|
||||||
|
final FocusNode _focus = FocusNode(debugLabel: 'add-account');
|
||||||
|
SettingsStore? _settings;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
final settings = ClideKernel.maybeOf(context)?.settings;
|
||||||
|
if (identical(settings, _settings)) return;
|
||||||
|
_settings?.removeListener(_onChange);
|
||||||
|
_settings = settings;
|
||||||
|
_settings?.addListener(_onChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onChange() {
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_settings?.removeListener(_onChange);
|
||||||
|
_name.dispose();
|
||||||
|
_focus.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _add(KernelServices services) async {
|
||||||
|
final name = _name.text.trim();
|
||||||
|
if (name.isEmpty) return;
|
||||||
|
final reg = AccountRegistry(services.settings);
|
||||||
|
if (reg.accountByName(name) != null) {
|
||||||
|
_name.clear();
|
||||||
|
return; // idempotent — already registered
|
||||||
|
}
|
||||||
|
final dir = '${Platform.environment['HOME'] ?? ''}/.claude-$name';
|
||||||
|
_name.clear();
|
||||||
|
final write = reg.registerAccount(name, dir);
|
||||||
|
// Kick off the login flow for the new account (T-485 consumer opens it).
|
||||||
|
services.messages.publish('ui', accountActionChannel, {'action': 'login', 'name': name, 'dir': dir});
|
||||||
|
await write;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _relogin(KernelServices services, Account a) {
|
||||||
|
services.messages.publish('ui', accountActionChannel, {'action': 'login', 'name': a.name, 'dir': a.dir});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _remove(KernelServices services, String name) => AccountRegistry(services.settings).removeAccount(name);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final tokens = ClideSettings.theme.of(context).surface;
|
||||||
|
final services = ClideKernel.maybeOf(context);
|
||||||
|
if (services == null) return const SizedBox.shrink();
|
||||||
|
final reg = AccountRegistry(services.settings);
|
||||||
|
final accounts = reg.accounts;
|
||||||
|
final bound = reg.boundAccountNames();
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
if (accounts.isEmpty)
|
||||||
|
ClideText('No accounts registered yet.', fontSize: clideFontCaption, color: tokens.globalTextMuted)
|
||||||
|
else
|
||||||
|
for (final a in accounts) _row(context, services, tokens, a, bound.contains(a.name)),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
_addRow(context, services, tokens),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _row(BuildContext context, KernelServices services, SurfaceTokens tokens, Account a, bool isBound) {
|
||||||
|
final signedIn = accountIsSignedIn(a.dir);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
decoration: BoxDecoration(shape: BoxShape.circle, color: signedIn ? tokens.statusSuccess : tokens.globalTextMuted),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
ClideText(a.name, color: tokens.globalForeground),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: ClideText(a.dir, fontSize: clideFontCaption, muted: true, fontFamily: ClideSettings.fonts.monoOf(context), overflow: TextOverflow.ellipsis),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_iconButton(context, 'sign-in', signedIn ? 'Re-sign in to ${a.name}' : 'Sign in to ${a.name}', () => _relogin(services, a)),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
_iconButton(
|
||||||
|
context,
|
||||||
|
'trash',
|
||||||
|
isBound ? '${a.name} is bound to a workspace — unset it first' : 'Remove ${a.name}',
|
||||||
|
isBound ? null : () => _remove(services, a.name),
|
||||||
|
color: isBound ? tokens.globalTextMuted : tokens.statusError,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _iconButton(BuildContext context, String icon, String semantic, VoidCallback? onTap, {Color? color}) {
|
||||||
|
final tokens = ClideSettings.theme.of(context).surface;
|
||||||
|
return Semantics(
|
||||||
|
button: true,
|
||||||
|
enabled: onTap != null,
|
||||||
|
label: semantic,
|
||||||
|
excludeSemantics: true,
|
||||||
|
child: ClideTappable(
|
||||||
|
cursor: onTap != null ? SystemMouseCursors.click : SystemMouseCursors.basic,
|
||||||
|
onTap: onTap,
|
||||||
|
builder: (ctx, hovered, _) =>
|
||||||
|
ClideIcon(PhosphorIcons.byName(icon), size: 14, color: color ?? (hovered ? tokens.globalForeground : tokens.globalTextMuted)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _addRow(BuildContext context, KernelServices services, SurfaceTokens tokens) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 26,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: tokens.panelBackground,
|
||||||
|
border: Border.all(color: _focus.hasFocus ? tokens.panelActiveBorder : tokens.dividerColor),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: EditableText(
|
||||||
|
controller: _name,
|
||||||
|
focusNode: _focus,
|
||||||
|
style: TextStyle(fontFamily: ClideSettings.fonts.monoOf(context), fontSize: clideFontMono, color: tokens.globalForeground),
|
||||||
|
cursorColor: tokens.globalFocus,
|
||||||
|
backgroundCursorColor: tokens.globalTextMuted,
|
||||||
|
maxLines: 1,
|
||||||
|
onSubmitted: (_) => _add(services),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Semantics(
|
||||||
|
button: true,
|
||||||
|
label: 'Add account',
|
||||||
|
excludeSemantics: true,
|
||||||
|
child: ClideTappable(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
onTap: () => _add(services),
|
||||||
|
builder: (ctx, hovered, _) => Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
||||||
|
decoration: BoxDecoration(color: hovered ? tokens.listItemHoverBackground : tokens.buttonBackground, borderRadius: BorderRadius.circular(4)),
|
||||||
|
child: ClideText('Add account', color: tokens.buttonForeground, fontSize: clideFontCaption),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compact account badge for the Claude pane chrome (T-481, epic T-476). Shows
|
||||||
|
/// the workspace's bound account (or "default"), tinted by [accountAccent] so
|
||||||
|
/// each window is distinguishable at a glance. Tapping opens a picker of the
|
||||||
|
/// registered accounts + Default. Hidden when no accounts are registered (the
|
||||||
|
/// feature is unused). Live via the settings notifier.
|
||||||
|
class ClaudeAccountBadge extends StatefulWidget {
|
||||||
|
const ClaudeAccountBadge({super.key, required this.workspaceRoot});
|
||||||
|
|
||||||
|
final String? workspaceRoot;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ClaudeAccountBadge> createState() => _ClaudeAccountBadgeState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ClaudeAccountBadgeState extends State<ClaudeAccountBadge> {
|
||||||
|
final ClideOverlayController _overlay = ClideOverlayController();
|
||||||
|
SettingsStore? _settings;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
final settings = ClideKernel.maybeOf(context)?.settings;
|
||||||
|
if (identical(settings, _settings)) return;
|
||||||
|
_settings?.removeListener(_onChange);
|
||||||
|
_settings = settings;
|
||||||
|
_settings?.addListener(_onChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onChange() {
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_settings?.removeListener(_onChange);
|
||||||
|
_overlay.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pick(KernelServices services, String? name) async {
|
||||||
|
_overlay.close();
|
||||||
|
final cwd = widget.workspaceRoot;
|
||||||
|
if (cwd != null) await bindWorkspaceAccount(services, cwd, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final tokens = ClideSettings.theme.of(context).surface;
|
||||||
|
final services = ClideKernel.maybeOf(context);
|
||||||
|
final cwd = widget.workspaceRoot;
|
||||||
|
if (services == null || cwd == null) return const SizedBox.shrink();
|
||||||
|
final reg = AccountRegistry(services.settings);
|
||||||
|
final accounts = reg.accounts;
|
||||||
|
if (accounts.isEmpty) return const SizedBox.shrink(); // feature unused — no chrome noise
|
||||||
|
final boundName = reg.boundName(cwd);
|
||||||
|
final label = boundName ?? 'default';
|
||||||
|
final accent = accountAccent(boundName, tokens);
|
||||||
|
return ClideAnchoredOverlay(
|
||||||
|
controller: _overlay,
|
||||||
|
align: ClideAnchorAlign.end,
|
||||||
|
overlayBuilder: (ctx, c) => ClideMenu(
|
||||||
|
onClose: c.close,
|
||||||
|
entries: [
|
||||||
|
ClideMenuItem(label: 'default', active: boundName == null, semanticLabel: 'Account: default', onSelect: () => _pick(services, null)),
|
||||||
|
const ClideMenuSeparator(),
|
||||||
|
for (final a in accounts)
|
||||||
|
ClideMenuItem(label: a.name, active: a.name == boundName, semanticLabel: 'Account: ${a.name}', onSelect: () => _pick(services, a.name)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
anchor: Semantics(
|
||||||
|
button: true,
|
||||||
|
label: 'Claude account: $label. Click to change.',
|
||||||
|
excludeSemantics: true,
|
||||||
|
onTap: _overlay.toggle,
|
||||||
|
child: ClideTappable(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
onTap: _overlay.toggle,
|
||||||
|
builder: (ctx, hovered, _) => Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: hovered ? tokens.listItemHoverBackground : null,
|
||||||
|
border: Border.all(color: accent),
|
||||||
|
borderRadius: BorderRadius.circular(3),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
decoration: BoxDecoration(shape: BoxShape.circle, color: accent),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
ClideText(label, fontSize: clideFontCaption, color: boundName == null ? tokens.globalTextMuted : tokens.globalForeground),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -162,6 +162,8 @@ bool _isFoldable(ConversationItem item, FoldLevel level, Map<String, String> too
|
|||||||
case UserMessage():
|
case UserMessage():
|
||||||
case AssistantTextMessage():
|
case AssistantTextMessage():
|
||||||
case ImageMessage():
|
case ImageMessage():
|
||||||
|
case DrawingMessage():
|
||||||
|
case IconMessage():
|
||||||
return false;
|
return false;
|
||||||
// Thinking folds at L2+, first-class at L1.
|
// Thinking folds at L2+, first-class at L1.
|
||||||
case AssistantThinkingMessage():
|
case AssistantThinkingMessage():
|
||||||
|
|||||||
@@ -58,6 +58,16 @@ String clideContextNote(String workspaceRoot) =>
|
|||||||
'is how you observe and drive the same workspace the user sees — prefer it for IDE actions so '
|
'is how you observe and drive the same workspace the user sees — prefer it for IDE actions so '
|
||||||
'your work and the user\'s stay in one shared workspace.';
|
'your work and the user\'s stay in one shared workspace.';
|
||||||
|
|
||||||
|
/// Nudge a FRESH session to reach for the bundled skills from its first turn
|
||||||
|
/// rather than rediscovering the workflows (T-490). Layered on top of
|
||||||
|
/// [clideContextNote]; only injected for new sessions (not --resume / forks),
|
||||||
|
/// so a session that already carries context is never re-nagged.
|
||||||
|
String clideSkillsNote() =>
|
||||||
|
'Two skills are available in this workspace — load and use them from your first turn instead '
|
||||||
|
'of rediscovering their workflows: `pql` (planning, decisions, tickets, and vault queries) and '
|
||||||
|
'`clide` (driving this IDE). Reach for the matching skill whenever a task touches planning or '
|
||||||
|
'tickets, or the clide surface.';
|
||||||
|
|
||||||
/// Build the environment DELTA to overlay on a hosted session's inherited
|
/// Build the environment DELTA to overlay on a hosted session's inherited
|
||||||
/// environment (T-215). `Process.start` keeps the parent environment by
|
/// environment (T-215). `Process.start` keeps the parent environment by
|
||||||
/// default, so this returns only the keys to add/override:
|
/// default, so this returns only the keys to add/override:
|
||||||
@@ -74,6 +84,21 @@ Map<String, String> agentEnvDelta({required String workspaceRoot, required Strin
|
|||||||
return delta;
|
return delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve the `CLAUDE_CONFIG_DIR` a session in [cwd] should run under (T-484,
|
||||||
|
/// epic T-476): the bound account's dir when the workspace is bound, else the
|
||||||
|
/// parent's `CLAUDE_CONFIG_DIR` when the launcher already set one, else null
|
||||||
|
/// (Claude defaults to `~/.claude`).
|
||||||
|
///
|
||||||
|
/// Pure: the AccountRegistry is injected as a plain [boundConfigDir] lookup
|
||||||
|
/// (workspace → bound config dir, or null) so this stays Flutter-free — the
|
||||||
|
/// registry itself lives behind a ChangeNotifier the orchestrator owns.
|
||||||
|
String? claudeConfigDirForWorkspace({required String cwd, required String? Function(String cwd) boundConfigDir, required Map<String, String> env}) {
|
||||||
|
final bound = boundConfigDir(cwd);
|
||||||
|
if (bound != null && bound.isNotEmpty) return bound;
|
||||||
|
final inherited = env['CLAUDE_CONFIG_DIR'];
|
||||||
|
return (inherited != null && inherited.isNotEmpty) ? inherited : null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Locate the directory to prepend to a hosted agent's PATH so `clide`
|
/// Locate the directory to prepend to a hosted agent's PATH so `clide`
|
||||||
/// resolves (T-215). Returns null when `clide` is ALREADY on [currentPath]
|
/// resolves (T-215). Returns null when `clide` is ALREADY on [currentPath]
|
||||||
/// (the installed case — T-211 drops it in `~/.local/bin`, normally already
|
/// (the installed case — T-211 drops it in `~/.local/bin`, normally already
|
||||||
@@ -107,7 +132,7 @@ class AgentBootstrap {
|
|||||||
/// env (usually null → inherit clide's). The returned [AgentBootstrap.extraArgs]
|
/// env (usually null → inherit clide's). The returned [AgentBootstrap.extraArgs]
|
||||||
/// carries the context note; team callers append their own preamble and the
|
/// carries the context note; team callers append their own preamble and the
|
||||||
/// orchestrator merges both into one `--append-system-prompt`.
|
/// orchestrator merges both into one `--append-system-prompt`.
|
||||||
AgentBootstrap agentBootstrap(String workspaceRoot, {Map<String, String>? base}) {
|
AgentBootstrap agentBootstrap(String workspaceRoot, {Map<String, String>? base, String? Function(String cwd)? boundConfigDir}) {
|
||||||
final home = Platform.environment['HOME'];
|
final home = Platform.environment['HOME'];
|
||||||
// The login-shell-resolved PATH (T-439) so a hosted claude — and the tools it
|
// The login-shell-resolved PATH (T-439) so a hosted claude — and the tools it
|
||||||
// shells out to — find user-installed components on a desktop launch, not just
|
// shells out to — find user-installed components on a desktop launch, not just
|
||||||
@@ -120,7 +145,12 @@ AgentBootstrap agentBootstrap(String workspaceRoot, {Map<String, String>? base})
|
|||||||
];
|
];
|
||||||
final cliDir = resolveClideCliDir(currentPath: currentPath, candidateDirs: candidates, isExecutableFile: _isExecutableFile);
|
final cliDir = resolveClideCliDir(currentPath: currentPath, candidateDirs: candidates, isExecutableFile: _isExecutableFile);
|
||||||
final delta = agentEnvDelta(workspaceRoot: workspaceRoot, socketPath: workspaceSocketPath(workspaceRoot), currentPath: currentPath, clideCliDir: cliDir);
|
final delta = agentEnvDelta(workspaceRoot: workspaceRoot, socketPath: workspaceSocketPath(workspaceRoot), currentPath: currentPath, clideCliDir: cliDir);
|
||||||
return AgentBootstrap(envDelta: {...?base, ...delta}, extraArgs: ['--allowedTools', clideBashAllowRule]);
|
// Per-repo Claude account (T-484): a bound workspace runs claude under that
|
||||||
|
// account's CLAUDE_CONFIG_DIR. Spread BEFORE base so an explicit per-call
|
||||||
|
// SpawnSpec.env override still wins (precedence: override > binding > parent
|
||||||
|
// env > unset); omitted entirely when there's nothing to set.
|
||||||
|
final configDir = claudeConfigDirForWorkspace(cwd: workspaceRoot, boundConfigDir: boundConfigDir ?? (_) => null, env: Platform.environment);
|
||||||
|
return AgentBootstrap(envDelta: {'CLAUDE_CONFIG_DIR': ?configDir, ...?base, ...delta}, extraArgs: ['--allowedTools', clideBashAllowRule]);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isExecutableFile(String path) {
|
bool _isExecutableFile(String path) {
|
||||||
|
|||||||
@@ -35,7 +35,12 @@ class ClaudeBanner extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const ClideSvgView.asset('assets/logo/logo.svg', width: 60, height: 60),
|
const ClideSvgView.asset('assets/logo/logo.svg', width: 60, height: 60),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
ClideText('Claude', fontSize: clideFontDialogTitle, color: claudeAccent, fontWeight: FontWeight.w500),
|
ClideText(
|
||||||
|
ClideSettings.i18n.string(context, 'banner.title', namespace: 'builtin.claude', placeholder: 'Claude'),
|
||||||
|
fontSize: clideFontDialogTitle,
|
||||||
|
color: claudeAccent,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
ClideText(role, fontSize: clideFontSmall, muted: true, fontFamily: ClideSettings.fonts.monoOf(context)),
|
ClideText(role, fontSize: clideFontSmall, muted: true, fontFamily: ClideSettings.fonts.monoOf(context)),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -45,7 +50,16 @@ class ClaudeBanner extends StatelessWidget {
|
|||||||
ClideText(statusLine!, fontSize: clideFontSmall, muted: true, fontFamily: ClideSettings.fonts.monoOf(context)),
|
ClideText(statusLine!, fontSize: clideFontSmall, muted: true, fontFamily: ClideSettings.fonts.monoOf(context)),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const ClideText('Warming up — your conversation will appear here.', fontSize: clideFontSmall, muted: true),
|
ClideText(
|
||||||
|
ClideSettings.i18n.string(
|
||||||
|
context,
|
||||||
|
'banner.warmingUp',
|
||||||
|
namespace: 'builtin.claude',
|
||||||
|
placeholder: 'Warming up — your conversation will appear here.',
|
||||||
|
),
|
||||||
|
fontSize: clideFontSmall,
|
||||||
|
muted: true,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class ClaudeComposer extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.onSubmit,
|
required this.onSubmit,
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
this.hint = 'Message Claude… (Enter to send · Shift+Enter for newline)',
|
this.hint,
|
||||||
this.pasteResolver,
|
this.pasteResolver,
|
||||||
this.slashCommandsResolver,
|
this.slashCommandsResolver,
|
||||||
this.onInterrupt,
|
this.onInterrupt,
|
||||||
@@ -63,7 +63,11 @@ class ClaudeComposer extends StatefulWidget {
|
|||||||
final void Function(String text) onSubmit;
|
final void Function(String text) onSubmit;
|
||||||
|
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
final String hint;
|
|
||||||
|
/// Placeholder shown when the composer is empty. Null falls back to the
|
||||||
|
/// i18n catalog default (`composer.hint`), resolved at render so it honours
|
||||||
|
/// the live locale (D-21).
|
||||||
|
final String? hint;
|
||||||
|
|
||||||
/// Optional override of paste handling: returns the attachments on the
|
/// Optional override of paste handling: returns the attachments on the
|
||||||
/// clipboard (files / images), or an empty list to fall back to the
|
/// clipboard (files / images), or an empty list to fall back to the
|
||||||
@@ -417,6 +421,14 @@ class _ClaudeComposerState extends State<ClaudeComposer> {
|
|||||||
final theme = ClideSettings.theme.of(context).surface;
|
final theme = ClideSettings.theme.of(context).surface;
|
||||||
final hasText = _controller.text.isNotEmpty;
|
final hasText = _controller.text.isNotEmpty;
|
||||||
final fg = widget.enabled ? theme.globalForeground : theme.globalTextMuted;
|
final fg = widget.enabled ? theme.globalForeground : theme.globalTextMuted;
|
||||||
|
final hint =
|
||||||
|
widget.hint ??
|
||||||
|
ClideSettings.i18n.string(
|
||||||
|
context,
|
||||||
|
'composer.hint',
|
||||||
|
namespace: 'builtin.claude',
|
||||||
|
placeholder: 'Message Claude… (Enter to send · Shift+Enter for newline)',
|
||||||
|
);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(10, 6, 10, 10),
|
padding: const EdgeInsets.fromLTRB(10, 6, 10, 10),
|
||||||
@@ -444,10 +456,15 @@ class _ClaudeComposerState extends State<ClaudeComposer> {
|
|||||||
const RunningIndicator(),
|
const RunningIndicator(),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
ClideButton(
|
ClideButton(
|
||||||
label: 'Stop ⎋',
|
label: ClideSettings.i18n.string(context, 'composer.stop', namespace: 'builtin.claude', placeholder: 'Stop ⎋'),
|
||||||
variant: ClideButtonVariant.primary,
|
variant: ClideButtonVariant.primary,
|
||||||
onPressed: widget.onInterrupt,
|
onPressed: widget.onInterrupt,
|
||||||
semanticHint: 'Interrupt the running turn (Escape)',
|
semanticHint: ClideSettings.i18n.string(
|
||||||
|
context,
|
||||||
|
'composer.stop.hint',
|
||||||
|
namespace: 'builtin.claude',
|
||||||
|
placeholder: 'Interrupt the running turn (Escape)',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -455,14 +472,14 @@ class _ClaudeComposerState extends State<ClaudeComposer> {
|
|||||||
if (_attachments.isNotEmpty)
|
if (_attachments.isNotEmpty)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
child: Wrap(spacing: 6, runSpacing: 6, children: [for (final a in _attachments) _chip(theme, a)]),
|
child: Wrap(spacing: 6, runSpacing: 6, children: [for (final a in _attachments) _chip(context, theme, a)]),
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Semantics(
|
child: Semantics(
|
||||||
label: widget.hint,
|
label: hint,
|
||||||
textField: true,
|
textField: true,
|
||||||
child: Shortcuts(
|
child: Shortcuts(
|
||||||
shortcuts: const {
|
shortcuts: const {
|
||||||
@@ -486,7 +503,7 @@ class _ClaudeComposerState extends State<ClaudeComposer> {
|
|||||||
},
|
},
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
if (!hasText) Positioned(left: 0, top: 0, right: 0, child: ClideText(widget.hint, muted: true, fontSize: clideFontBody)),
|
if (!hasText) Positioned(left: 0, top: 0, right: 0, child: ClideText(hint, muted: true, fontSize: clideFontBody)),
|
||||||
EditableText(
|
EditableText(
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
focusNode: _focus,
|
focusNode: _focus,
|
||||||
@@ -521,7 +538,7 @@ class _ClaudeComposerState extends State<ClaudeComposer> {
|
|||||||
|
|
||||||
/// One attachment chip: a thumbnail (images) or file icon (other types),
|
/// One attachment chip: a thumbnail (images) or file icon (other types),
|
||||||
/// the filename, and a remove × that cancels the attachment before send.
|
/// the filename, and a remove × that cancels the attachment before send.
|
||||||
Widget _chip(SurfaceTokens theme, ComposerAttachment a) {
|
Widget _chip(BuildContext context, SurfaceTokens theme, ComposerAttachment a) {
|
||||||
return Container(
|
return Container(
|
||||||
constraints: const BoxConstraints(maxWidth: 220),
|
constraints: const BoxConstraints(maxWidth: 220),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -541,7 +558,13 @@ class _ClaudeComposerState extends State<ClaudeComposer> {
|
|||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Semantics(
|
Semantics(
|
||||||
button: true,
|
button: true,
|
||||||
label: 'Remove ${a.fileName}',
|
label: ClideSettings.i18n.interpolated(
|
||||||
|
context,
|
||||||
|
'composer.removeAttachment',
|
||||||
|
namespace: 'builtin.claude',
|
||||||
|
placeholder: 'Remove ${a.fileName}',
|
||||||
|
replacers: [I18nReplacer(from: '{name}', replace: a.fileName)],
|
||||||
|
),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
key: ValueKey('composer-remove-${a.path}'),
|
key: ValueKey('composer-remove-${a.path}'),
|
||||||
onTap: () => _removeAttachment(a),
|
onTap: () => _removeAttachment(a),
|
||||||
|
|||||||
@@ -19,9 +19,10 @@
|
|||||||
/// `meta_sidebar/` (T-395 split). Activity and Config render on the same
|
/// `meta_sidebar/` (T-395 split). Activity and Config render on the same
|
||||||
/// table geometry (`buildMetaTable`) so switching tabs doesn't visually jump.
|
/// table geometry (`buildMetaTable`) so switching tabs doesn't visually jump.
|
||||||
///
|
///
|
||||||
/// The account/team token budget is intentionally absent: it isn't
|
/// The account budget surfaces from a forwarded `/usage` (T-415): the Activity
|
||||||
/// programmatically exposed under subscription auth (see project memory /
|
/// tab renders it next to its refresh control. It is NOT duplicated on the Team
|
||||||
/// GitHub anthropics/claude-code#44328).
|
/// tab — usage is per-account (one `~/.claude` login), so it can't be split per
|
||||||
|
/// member; one place to see it is enough (T-158).
|
||||||
library;
|
library;
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:clide/kernel/kernel.dart';
|
|||||||
import 'package:clide/widgets/widgets.dart';
|
import 'package:clide/widgets/widgets.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'account_settings_control.dart';
|
||||||
import 'claude_banner.dart';
|
import 'claude_banner.dart';
|
||||||
import 'claude_composer.dart';
|
import 'claude_composer.dart';
|
||||||
import 'claude_config.dart';
|
import 'claude_config.dart';
|
||||||
@@ -728,7 +729,15 @@ class _ClaudePaneState extends State<ClaudePane> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final title = widget.isPrimary ? 'claude — primary' : 'claude — secondary ${widget.secondaryIndex}';
|
final title = widget.isPrimary
|
||||||
|
? ClideSettings.i18n.string(context, 'pane.title.primary', namespace: 'builtin.claude', placeholder: 'claude — primary')
|
||||||
|
: ClideSettings.i18n.interpolated(
|
||||||
|
context,
|
||||||
|
'pane.title.secondary',
|
||||||
|
namespace: 'builtin.claude',
|
||||||
|
placeholder: 'claude — secondary ${widget.secondaryIndex}',
|
||||||
|
replacers: [I18nReplacer(from: '{index}', replace: '${widget.secondaryIndex}')],
|
||||||
|
);
|
||||||
final tokens = ClideSettings.theme.of(context).surface;
|
final tokens = ClideSettings.theme.of(context).surface;
|
||||||
|
|
||||||
final Widget body;
|
final Widget body;
|
||||||
@@ -761,7 +770,15 @@ class _ClaudePaneState extends State<ClaudePane> {
|
|||||||
quietErrorToolUseIds: _session?.quietErrorToolUseIds ?? const <String>{},
|
quietErrorToolUseIds: _session?.quietErrorToolUseIds ?? const <String>{},
|
||||||
workflows: _session?.workflows ?? const <String, WorkflowRun>{},
|
workflows: _session?.workflows ?? const <String, WorkflowRun>{},
|
||||||
emptyState: ClaudeBanner(
|
emptyState: ClaudeBanner(
|
||||||
role: widget.isPrimary ? 'primary' : 'session ${widget.secondaryIndex}',
|
role: widget.isPrimary
|
||||||
|
? ClideSettings.i18n.string(context, 'banner.role.primary', namespace: 'builtin.claude', placeholder: 'primary')
|
||||||
|
: ClideSettings.i18n.interpolated(
|
||||||
|
context,
|
||||||
|
'banner.role.secondary',
|
||||||
|
namespace: 'builtin.claude',
|
||||||
|
placeholder: 'session ${widget.secondaryIndex}',
|
||||||
|
replacers: [I18nReplacer(from: '{index}', replace: '${widget.secondaryIndex}')],
|
||||||
|
),
|
||||||
workspace: _repoRoot,
|
workspace: _repoRoot,
|
||||||
statusLine: _statusLine,
|
statusLine: _statusLine,
|
||||||
),
|
),
|
||||||
@@ -833,10 +850,21 @@ class _ClaudePaneState extends State<ClaudePane> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
body = const Center(child: ClideText('starting…', muted: true));
|
body = Center(
|
||||||
|
child: ClideText(ClideSettings.i18n.string(context, 'pane.starting', namespace: 'builtin.claude', placeholder: 'starting…'), muted: true),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final content = widget.showChrome ? ClidePaneChrome(title: title, subtitle: _error ?? _statusLine, child: body) : body;
|
final content = widget.showChrome
|
||||||
|
? ClidePaneChrome(
|
||||||
|
title: title,
|
||||||
|
subtitle: _error ?? _statusLine,
|
||||||
|
// Per-repo Claude account badge (T-481): shows + switches the
|
||||||
|
// account this workspace is bound to; hidden when none registered.
|
||||||
|
trailing: [ClaudeAccountBadge(workspaceRoot: _repoRoot)],
|
||||||
|
child: body,
|
||||||
|
)
|
||||||
|
: body;
|
||||||
|
|
||||||
// Surface this pane's status to the bottom status-bar slot while it's
|
// Surface this pane's status to the bottom status-bar slot while it's
|
||||||
// the focused pane (T-150).
|
// the focused pane (T-150).
|
||||||
@@ -856,7 +884,13 @@ class _ModeBadge extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Semantics(
|
return Semantics(
|
||||||
label: 'permission mode: ${permissionModeLabel(mode)}',
|
label: ClideSettings.i18n.interpolated(
|
||||||
|
context,
|
||||||
|
'pane.modeBadge.semantics',
|
||||||
|
namespace: 'builtin.claude',
|
||||||
|
placeholder: 'permission mode: ${permissionModeLabel(mode)}',
|
||||||
|
replacers: [I18nReplacer(from: '{mode}', replace: permissionModeLabel(mode))],
|
||||||
|
),
|
||||||
excludeSemantics: true,
|
excludeSemantics: true,
|
||||||
child: ClideText(
|
child: ClideText(
|
||||||
permissionModeLabel(mode),
|
permissionModeLabel(mode),
|
||||||
|
|||||||
@@ -33,13 +33,28 @@ class _ClaudeTaskDockState extends State<ClaudeTaskDock> {
|
|||||||
final done = tasks.where((t) => t.status == TaskStatus.completed).length;
|
final done = tasks.where((t) => t.status == TaskStatus.completed).length;
|
||||||
final inProgress = tasks.where((t) => t.status == TaskStatus.inProgress);
|
final inProgress = tasks.where((t) => t.status == TaskStatus.inProgress);
|
||||||
final current = inProgress.isEmpty ? null : inProgress.first.text;
|
final current = inProgress.isEmpty ? null : inProgress.first.text;
|
||||||
final summary = '${tasks.length} task${tasks.length == 1 ? '' : 's'} · $done done';
|
final taskWord = tasks.length == 1
|
||||||
|
? ClideSettings.i18n.string(context, 'taskDock.task.singular', namespace: 'builtin.claude', placeholder: 'task')
|
||||||
|
: ClideSettings.i18n.string(context, 'taskDock.task.plural', namespace: 'builtin.claude', placeholder: 'tasks');
|
||||||
|
final summary = ClideSettings.i18n.interpolated(
|
||||||
|
context,
|
||||||
|
'taskDock.summary',
|
||||||
|
namespace: 'builtin.claude',
|
||||||
|
placeholder: '${tasks.length} $taskWord · $done done',
|
||||||
|
replacers: [
|
||||||
|
I18nReplacer(from: '{count}', replace: '${tasks.length}'),
|
||||||
|
I18nReplacer(from: '{tasks}', replace: taskWord),
|
||||||
|
I18nReplacer(from: '{done}', replace: '$done'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(10, 0, 10, 6),
|
padding: const EdgeInsets.fromLTRB(10, 0, 10, 6),
|
||||||
child: ClideTappable(
|
child: ClideTappable(
|
||||||
onTap: () => setState(() => _expanded = !_expanded),
|
onTap: () => setState(() => _expanded = !_expanded),
|
||||||
tooltip: _expanded ? 'Collapse tasks' : 'Expand tasks',
|
tooltip: _expanded
|
||||||
|
? ClideSettings.i18n.string(context, 'taskDock.collapse', namespace: 'builtin.claude', placeholder: 'Collapse tasks')
|
||||||
|
: ClideSettings.i18n.string(context, 'taskDock.expand', namespace: 'builtin.claude', placeholder: 'Expand tasks'),
|
||||||
builder: (context, hovered, focused) => Container(
|
builder: (context, hovered, focused) => Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: (hovered || focused) ? tokens.listItemHoverBackground : tokens.listItemBackground,
|
color: (hovered || focused) ? tokens.listItemHoverBackground : tokens.listItemBackground,
|
||||||
@@ -53,7 +68,21 @@ class _ClaudeTaskDockState extends State<ClaudeTaskDock> {
|
|||||||
// (its inner text is announced via the label, so exclude it).
|
// (its inner text is announced via the label, so exclude it).
|
||||||
Semantics(
|
Semantics(
|
||||||
button: true,
|
button: true,
|
||||||
label: 'Claude task list, $summary, ${_expanded ? 'expanded' : 'collapsed'}',
|
label: ClideSettings.i18n.interpolated(
|
||||||
|
context,
|
||||||
|
'taskDock.semantics',
|
||||||
|
namespace: 'builtin.claude',
|
||||||
|
placeholder: 'Claude task list, $summary, ${_expanded ? 'expanded' : 'collapsed'}',
|
||||||
|
replacers: [
|
||||||
|
I18nReplacer(from: '{summary}', replace: summary),
|
||||||
|
I18nReplacer(
|
||||||
|
from: '{state}',
|
||||||
|
replace: _expanded
|
||||||
|
? ClideSettings.i18n.string(context, 'taskDock.state.expanded', namespace: 'builtin.claude', placeholder: 'expanded')
|
||||||
|
: ClideSettings.i18n.string(context, 'taskDock.state.collapsed', namespace: 'builtin.claude', placeholder: 'collapsed'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
excludeSemantics: true,
|
excludeSemantics: true,
|
||||||
child: _summaryRow(tokens, summary, current),
|
child: _summaryRow(tokens, summary, current),
|
||||||
),
|
),
|
||||||
@@ -91,14 +120,35 @@ class _ClaudeTaskDockState extends State<ClaudeTaskDock> {
|
|||||||
|
|
||||||
Widget _taskRow(SurfaceTokens tokens, TaskItem t) {
|
Widget _taskRow(SurfaceTokens tokens, TaskItem t) {
|
||||||
final (String glyph, Color color, String word) = switch (t.status) {
|
final (String glyph, Color color, String word) = switch (t.status) {
|
||||||
TaskStatus.completed => ('check-circle', tokens.statusSuccess, 'done'),
|
TaskStatus.completed => (
|
||||||
TaskStatus.inProgress => ('circle-half', tokens.globalFocus, 'in progress'),
|
'check-circle',
|
||||||
TaskStatus.pending => ('circle', tokens.globalTextMuted, 'pending'),
|
tokens.statusSuccess,
|
||||||
|
ClideSettings.i18n.string(context, 'taskDock.status.done', namespace: 'builtin.claude', placeholder: 'done'),
|
||||||
|
),
|
||||||
|
TaskStatus.inProgress => (
|
||||||
|
'circle-half',
|
||||||
|
tokens.globalFocus,
|
||||||
|
ClideSettings.i18n.string(context, 'taskDock.status.inProgress', namespace: 'builtin.claude', placeholder: 'in progress'),
|
||||||
|
),
|
||||||
|
TaskStatus.pending => (
|
||||||
|
'circle',
|
||||||
|
tokens.globalTextMuted,
|
||||||
|
ClideSettings.i18n.string(context, 'taskDock.status.pending', namespace: 'builtin.claude', placeholder: 'pending'),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(top: 4),
|
padding: const EdgeInsets.only(top: 4),
|
||||||
child: Semantics(
|
child: Semantics(
|
||||||
label: '${t.text}, $word',
|
label: ClideSettings.i18n.interpolated(
|
||||||
|
context,
|
||||||
|
'taskDock.row.semantics',
|
||||||
|
namespace: 'builtin.claude',
|
||||||
|
placeholder: '${t.text}, $word',
|
||||||
|
replacers: [
|
||||||
|
I18nReplacer(from: '{text}', replace: t.text),
|
||||||
|
I18nReplacer(from: '{status}', replace: word),
|
||||||
|
],
|
||||||
|
),
|
||||||
container: true,
|
container: true,
|
||||||
excludeSemantics: true,
|
excludeSemantics: true,
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|||||||