Compare commits
@@ -45,6 +45,32 @@ These apply across every reference and every surface:
|
||||
widget must set it explicitly. `clideMonoFamily` / `clideUiFamily` are the
|
||||
facade's defaults — don't read them directly in new widgets (D-101). Same
|
||||
facade exposes `ClideSettings.theme.of(context)` and `.i18n.of(context)`.
|
||||
- User-facing strings resolve through the catalog, never a hardcoded literal
|
||||
(D-21/D-102): `ClideSettings.i18n.string(context, 'dotted.key', namespace:
|
||||
<ext id or 'core'>, placeholder: '<English>')` (or `.interpolated` for
|
||||
templated). Add the key→English to `assets/i18n/en_us/<namespace>.json`. The
|
||||
`placeholder` is the English fallback; the extension's own id is its
|
||||
namespace (framework chrome uses `core`). Contribution manifests carry
|
||||
`titleKey`/`labelKey` for the same reason.
|
||||
|
||||
## Localization & string length (D-21/D-102)
|
||||
|
||||
- **Config / layout.** Catalogs are bundled assets at
|
||||
`assets/i18n/<locale>/<namespace>.json` — the locale is a *directory*
|
||||
(`en_us`, `nl_nl`, `nl_be`, `en_eu`, …); a new language is a new folder of the
|
||||
same namespace files. The active language is `app.locale` (Settings →
|
||||
Appearance → Language), applied live by `root_shell` via `i18n.setLocale`;
|
||||
add the `Locale` to `availableLocales` in `main.dart` and a folder under
|
||||
`assets/i18n/`. `en_US` is default; `nl_NL` ships.
|
||||
- **Design for length variation.** Translations are not the same width — Dutch
|
||||
runs ~20% longer than English, German more. So **never hard-size a surface to
|
||||
its English label.** Tight surfaces (status-bar items, chips, buttons, tab
|
||||
titles, menu items) must tolerate ~30% growth: let them wrap, ellipsis, or
|
||||
`Flexible`/`Expanded`, not a fixed width tuned to English. When you add or
|
||||
translate a label, sanity-check the length delta on those tight surfaces (an
|
||||
`*.semantics` label is screen-reader-only, so its length never deforms
|
||||
layout). A quick audit: compare `len(nl)/len(en)` per key and eyeball the
|
||||
short-but-grew cases on real (non-semantics) surfaces.
|
||||
|
||||
## Conversation-panel cards (T-305)
|
||||
|
||||
|
||||
@@ -5929,3 +5929,20 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDDTCGG5Z0KNQKF89W1VZSQ8', 'status', 'backlog', 'done', NULL, '2026-06-17 19:35:22', '2026-06-17 19:35:22.690', '2026-06-17 19:35:22.690', NULL, '580448456994d6acecc88cfbd40a595f', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDDX5KEAJ8KRWVV7ZSG6H7A0', 'status', 'backlog', 'done', NULL, '2026-06-17 19:37:39', '2026-06-17 19:37:39.813', '2026-06-17 19:37:39.813', NULL, 'd59d0df2b693a0616265dd5e39559dc0', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDDX5GVH3FTCVDEC1QAFACY4', 'status', 'backlog', 'done', NULL, '2026-06-17 19:47:02', '2026-06-17 19:47:02.941', '2026-06-17 19:47:02.941', NULL, '28625100d88cacac15cabe97754fcdc5', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDEMY3DQWDNY535PMMSB0CSW', 'status', 'backlog', 'done', NULL, '2026-06-17 20:51:21', '2026-06-17 20:51:21.006', '2026-06-17 20:51:21.006', NULL, '20516cde17cc921f419aa5f12ea49bc1', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0TPYSWEM10RP0Q76XAP58', 'status', 'backlog', 'ready', NULL, '2026-06-18 07:40:45', '2026-06-18 07:40:45.691', '2026-06-18 07:40:45.691', NULL, '8cc60761398868f5b9117db824f8651b', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0TPYSWEM10RP0Q76XAP58', 'status', 'ready', 'in_progress', NULL, '2026-06-18 07:46:07', '2026-06-18 07:46:07.612', '2026-06-18 07:46:07.612', NULL, 'e8e84ffd0634306f2dc612d1d03152b4', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0VPB99NGS4J6Z5B2RJM2M', 'status', 'backlog', 'in_progress', NULL, '2026-06-18 07:46:07', '2026-06-18 07:46:07.636', '2026-06-18 07:46:07.636', NULL, 'e9e9c6719b8a49a39309bf8268eeee55', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0VPB99NGS4J6Z5B2RJM2M', 'status', 'in_progress', 'done', NULL, '2026-06-18 07:56:10', '2026-06-18 07:56:10.133', '2026-06-18 07:56:10.133', NULL, '666602e799384337b8631789be144e53', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0X5WFME83GS0DKYG3WVJ8', 'status', 'backlog', 'done', NULL, '2026-06-18 13:41:29', '2026-06-18 13:41:29.322', '2026-06-18 13:41:29.322', NULL, 'debbcd81ff12cb112671ba428f1a826b', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0WCPG458GF5FXTXM9VY98', 'status', 'backlog', 'in_progress', NULL, '2026-06-18 13:52:52', '2026-06-18 13:52:52.920', '2026-06-18 13:52:52.920', NULL, 'c6f7f43d0c2d88dabcc176783b1e7006', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0YCY2WGSG8W46RHQ5Z9MW', 'status', 'backlog', 'in_progress', NULL, '2026-06-18 13:52:52', '2026-06-18 13:52:52.931', '2026-06-18 13:52:52.931', NULL, '4d4b842c99b6ff11a2820f9bccd0e73c', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0XMA9BQV4RHJ55BFN4JR4', 'status', 'backlog', 'in_progress', NULL, '2026-06-18 13:52:52', '2026-06-18 13:52:52.931', '2026-06-18 13:52:52.931', NULL, '8a7fef08c302d2119d32ceb18a58346e', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0YYCJ0NK8C4HMDYKJHRDG', 'status', 'backlog', 'in_progress', NULL, '2026-06-18 13:52:52', '2026-06-18 13:52:52.932', '2026-06-18 13:52:52.932', NULL, 'f0b77fdf0ef68d872155912ef3882cb1', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0WCPG458GF5FXTXM9VY98', 'status', 'in_progress', 'done', NULL, '2026-06-18 19:14:41', '2026-06-18 19:14:41.116', '2026-06-18 19:14:41.116', NULL, '2bf6c6fbef7a2e950fc1f31541f8dc22', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0XMA9BQV4RHJ55BFN4JR4', 'status', 'in_progress', 'done', NULL, '2026-06-18 19:19:25', '2026-06-18 19:19:25.269', '2026-06-18 19:19:25.269', NULL, 'f1660567ecfe0c7220c23244206bc4d2', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0YCY2WGSG8W46RHQ5Z9MW', 'status', 'in_progress', 'done', NULL, '2026-06-18 20:13:50', '2026-06-18 20:13:50.352', '2026-06-18 20:13:50.352', NULL, 'a885fb7b56c053669a8ef5f0e6032feb', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0YYCJ0NK8C4HMDYKJHRDG', 'status', 'in_progress', 'done', NULL, '2026-06-18 20:16:03', '2026-06-18 20:16:03.747', '2026-06-18 20:16:03.747', NULL, '5e67beaa758d779f1b9212aecb82e050', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA10DRQE3SED10JAW2CZDCR', 'status', 'backlog', 'done', NULL, '2026-06-18 21:21:27', '2026-06-18 21:21:27.304', '2026-06-18 21:21:27.304', NULL, '0ab235b21c54d46de3e3e4d1529de041', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'description', NULL, 'Today clide uses Claude Code''s system-wide login (the CLI''s global auth), so every repo/workspace shares one Claude account. Allow selecting/logging into a separate Claude account per repo, so different projects can run under different accounts (e.g. personal vs work). Needs: per-workspace auth storage + a way for the spawned Claude session to use that repo''s account instead of the global one; UI to sign in/switch per repo; fall back to the system login when none is set.', NULL, '2026-06-19 07:42:08', '2026-06-19 07:42:08.761', '2026-06-19 07:42:08.761', NULL, '04b83ed9bd668d2cb22e75d18d1c2c94', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0TPYSWEM10RP0Q76XAP58', 'status', 'in_progress', 'done', NULL, '2026-06-19 08:20:44', '2026-06-19 08:20:44.218', '2026-06-19 08:20:44.218', NULL, 'f5289951d9bfdf8695a1cb77c212c3fb', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
|
||||
@@ -300,3 +300,5 @@ 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 ('06FDDX5GVH3FTCVDEC1QAFACY4', 'T-473', '2026-06-17 19:00:20.828', '2026-06-17 19:00:20.828', NULL, '647782edc3fbb4e7285e4f82a1fb84be', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDDX5KEAJ8KRWVV7ZSG6H7A0', 'T-474', '2026-06-17 19:00:21.490', '2026-06-17 19:00:21.490', NULL, '79576f18553ed885d3a2b745becdf2f5', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDEMY3DQWDNY535PMMSB0CSW', 'T-475', '2026-06-17 20:44:11.501', '2026-06-17 20:44:11.501', NULL, 'bb2b56ea9a76d967123238f588c70aba', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'T-476', '2026-06-19 07:42:08.735', '2026-06-19 07:42:08.735', NULL, 'f75a0a8c00b0ec40c110c1169e42fe2c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
|
||||
@@ -7248,3 +7248,134 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDDTCGG5Z0KNQKF89W1VZSQ8', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Migrate 11 context-less mono-font sites onto ClideSettings.fonts (thread family through helpers)', 'Per D-101. The 11 sites that need a BuildContext threaded from the nearest context-bearing caller (resolve ClideSettings.fonts.monoOf(context) and pass the family in): prompt_card.dart toolEditBody/toolReadLikeBody/toolPathLine (top-level fns); conversation_view.dart _toolResult; welcome_view.dart _tipRow; search_panel_view.dart _base getter; clide_markdown.dart _inlineElementSpan/_recordLinkSpan/_fileLinkSpan (static span chain). Until done, these render the default JetBrains Mono regardless of the mono-font setting.', 'done', 'medium', NULL, NULL, NULL, '2026-06-17 18:48:11.649', '2026-06-17 19:35:22.690', NULL, '7dd6dae4197fc95d0bc6becfad4eecbb', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDDX5KEAJ8KRWVV7ZSG6H7A0', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Migrate i18n consumers (ClideKernel.i18n → ClideSettings.i18n.of) per D-101', 'Per D-101. Migrate i18n reads (ClideKernel.of(context).i18n[.string]) to ClideSettings.i18n.of(context). The facade delegates to the same I18n service, so behaviour is unchanged; mechanical sweep in batches.', 'done', 'medium', NULL, NULL, NULL, '2026-06-17 19:00:21.490', '2026-06-17 19:37:39.813', NULL, 'fc64918991f0c9133419ee51227e1939', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDDX5GVH3FTCVDEC1QAFACY4', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Migrate theme consumers (ClideTheme.of → ClideSettings.theme.of) per D-101', 'Per D-101. Migrate ClideTheme.of(context) consumers (hundreds of sites) to ClideSettings.theme.of(context). The facade already delegates to ClideTheme, so this is a mechanical, no-behavior-change sweep — do it in focused batches with analyze between, ideally via a scripted find/replace + import fixups.', 'done', 'medium', NULL, NULL, NULL, '2026-06-17 19:00:20.828', '2026-06-17 19:47:02.941', NULL, '8ea58b6162c81778a610f46b07585b05', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDEMY3DQWDNY535PMMSB0CSW', 'bug', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Markdown prose ignores the UI-font setting (clideUiFamily const)', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-17 20:44:11.501', '2026-06-17 20:44:11.501', NULL, '5861faa02d7cc34271dc72cf964f63cf', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDEMY3DQWDNY535PMMSB0CSW', 'bug', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Markdown prose ignores the UI-font setting (clideUiFamily const)', NULL, 'done', 'medium', NULL, NULL, NULL, '2026-06-17 20:44:11.501', '2026-06-17 20:51:21.006', NULL, '6d53da1fcfe6ad27b54a6ea07c1ccc7e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0TPYSWEM10RP0Q76XAP58', 'epic', NULL, 'i18n coverage: route hardcoded UI labels through the catalog', 'D-21 makes i18n a Tier-0 contract: "All user-facing strings resolve through a namespaced i18n catalogue loader." In practice coverage is ~7%. A June 2026 audit found:
|
||||
|
||||
- Only 2 of 30 builtin extensions actually call .string()/.interpolated(): builtin.settings-ui and builtin.theme-picker. Total live i18n call sites: 17.
|
||||
- ~144 hardcoded user-facing string literals (Text/ClideText, tooltip:, hintText:, dialog/toast text) bypass i18n across the other 28 extensions + lib/widgets + lib/kernel.
|
||||
- 12 of the 14 existing catalogs are effectively orphaned: the strings are pre-translated but the code hardcodes the label anyway (e.g. welcome ships a 10-key catalog AND 15 hardcoded strings; claude ships 10 keys + 35 hardcoded).
|
||||
|
||||
Because every call site and TabContribution supplies an inline English placeholder, nothing renders blank at runtime — so this is invisible in the running app. The cost is that ~93% of labels can never be localized: they never reach the catalog lookup. That is a Tier-0 (D-21) gap, not polish.
|
||||
|
||||
Prior work: T-155 (done) fixed the catalog plumbing + boot warnings and created tab.title catalogs; T-371 (backlog, high, parent T-359) fixes the a11y/i18n gate tests that under-enumerate namespaces. Neither migrates the hardcoded labels — that is this epic.
|
||||
|
||||
This epic = migrate the ~144 hardcoded labels to i18n, per extension, extending the existing catalogs where present. T-371 should land alongside so the gate fails when a new namespace ships uncovered and the work cannot re-drift.
|
||||
|
||||
Audit per-extension hardcoded counts (approx): claude 35, welcome 15, git 9, diff 7, tickets 6, pql 6, menubar 6, decisions 6, output 4, search 3, problems 3, deeplink 3, editor 1, files 1, terminal 1, markdown 1, vim 1; lib/widgets 9; lib/kernel 1.', 'ready', 'high', NULL, NULL, 'D-21', '2026-06-17 09:57:06.422', '2026-06-18 07:40:45.691', NULL, '81fd18dccd660abfa9d8ff5ff16dc119', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0TPYSWEM10RP0Q76XAP58', 'epic', NULL, 'i18n coverage: route hardcoded UI labels through the catalog', 'D-21 makes i18n a Tier-0 contract: "All user-facing strings resolve through a namespaced i18n catalogue loader." In practice coverage is ~7%. A June 2026 audit found:
|
||||
|
||||
- Only 2 of 30 builtin extensions actually call .string()/.interpolated(): builtin.settings-ui and builtin.theme-picker. Total live i18n call sites: 17.
|
||||
- ~144 hardcoded user-facing string literals (Text/ClideText, tooltip:, hintText:, dialog/toast text) bypass i18n across the other 28 extensions + lib/widgets + lib/kernel.
|
||||
- 12 of the 14 existing catalogs are effectively orphaned: the strings are pre-translated but the code hardcodes the label anyway (e.g. welcome ships a 10-key catalog AND 15 hardcoded strings; claude ships 10 keys + 35 hardcoded).
|
||||
|
||||
Because every call site and TabContribution supplies an inline English placeholder, nothing renders blank at runtime — so this is invisible in the running app. The cost is that ~93% of labels can never be localized: they never reach the catalog lookup. That is a Tier-0 (D-21) gap, not polish.
|
||||
|
||||
Prior work: T-155 (done) fixed the catalog plumbing + boot warnings and created tab.title catalogs; T-371 (backlog, high, parent T-359) fixes the a11y/i18n gate tests that under-enumerate namespaces. Neither migrates the hardcoded labels — that is this epic.
|
||||
|
||||
This epic = migrate the ~144 hardcoded labels to i18n, per extension, extending the existing catalogs where present. T-371 should land alongside so the gate fails when a new namespace ships uncovered and the work cannot re-drift.
|
||||
|
||||
Audit per-extension hardcoded counts (approx): claude 35, welcome 15, git 9, diff 7, tickets 6, pql 6, menubar 6, decisions 6, output 4, search 3, problems 3, deeplink 3, editor 1, files 1, terminal 1, markdown 1, vim 1; lib/widgets 9; lib/kernel 1.', 'in_progress', 'high', NULL, NULL, 'D-21', '2026-06-17 09:57:06.422', '2026-06-18 07:46:07.612', NULL, '28daeb118a8ceb5181723c33de18f71a', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0VPB99NGS4J6Z5B2RJM2M', 'task', '06FDA0TPYSWEM10RP0Q76XAP58', 'i18n: fix concrete catalog gaps (theme-picker missing keys + diff has no catalog)', 'Two confirmed catalog-behind-code gaps found in the June 2026 audit. Small, isolated fix — do this first.
|
||||
|
||||
1) builtin.theme-picker requests two keys that are NOT in lib/kernel/src/i18n/catalog/builtin.theme-picker_en_us.json, so they silently fall through to the inline placeholder:
|
||||
- section.appearance (lib/builtin/theme_picker/src/settings_view.dart:75)
|
||||
- toggle.highContrast (lib/builtin/theme_picker/src/settings_view.dart:83)
|
||||
Add both keys with their current placeholder text as the translation.
|
||||
|
||||
2) builtin.diff contributes a TabContribution with titleKey ''tab.title'' + i18nNamespace ''builtin.diff'' (lib/builtin/diff/src/extension.dart) but ships NO catalog file. There is no lib/kernel/src/i18n/catalog/builtin.diff_en_us.json. Create it with tab.title (= ''Diff'', matching the placeholder).
|
||||
|
||||
Acceptance: both theme-picker keys resolve from the catalog (not the placeholder); a builtin.diff catalog exists with tab.title; no ''missing key'' i18n warnings on boot for these namespaces.', 'in_progress', 'high', NULL, NULL, 'D-21', '2026-06-17 09:57:14.458', '2026-06-18 07:46:07.636', NULL, 'a950ae3e425a624a39254264c6f2cb5e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0VPB99NGS4J6Z5B2RJM2M', 'task', '06FDA0TPYSWEM10RP0Q76XAP58', 'i18n: fix concrete catalog gaps (theme-picker missing keys + diff has no catalog)', 'Two confirmed catalog-behind-code gaps found in the June 2026 audit. Small, isolated fix — do this first.
|
||||
|
||||
1) builtin.theme-picker requests two keys that are NOT in lib/kernel/src/i18n/catalog/builtin.theme-picker_en_us.json, so they silently fall through to the inline placeholder:
|
||||
- section.appearance (lib/builtin/theme_picker/src/settings_view.dart:75)
|
||||
- toggle.highContrast (lib/builtin/theme_picker/src/settings_view.dart:83)
|
||||
Add both keys with their current placeholder text as the translation.
|
||||
|
||||
2) builtin.diff contributes a TabContribution with titleKey ''tab.title'' + i18nNamespace ''builtin.diff'' (lib/builtin/diff/src/extension.dart) but ships NO catalog file. There is no lib/kernel/src/i18n/catalog/builtin.diff_en_us.json. Create it with tab.title (= ''Diff'', matching the placeholder).
|
||||
|
||||
Acceptance: both theme-picker keys resolve from the catalog (not the placeholder); a builtin.diff catalog exists with tab.title; no ''missing key'' i18n warnings on boot for these namespaces.', 'done', 'high', NULL, NULL, 'D-21', '2026-06-17 09:57:14.458', '2026-06-18 07:56:10.133', NULL, '2d182b7c0f40848dc5a59771b73e4da1', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0X5WFME83GS0DKYG3WVJ8', 'task', '06FDA0TPYSWEM10RP0Q76XAP58', 'i18n: migrate builtin.welcome UI labels to the catalog (~15 strings)', 'builtin.welcome ships a full 10-key catalog (title, subtitle, open-project, open-project.hint, etc.) that is entirely orphaned — the view hardcodes ~15 strings instead. Wire the existing catalog up and add any missing keys.
|
||||
|
||||
Hotspots from the audit: lib/builtin/welcome/src/welcome_view.dart — e.g. :108 ''TIPS'', :173 ''START'', :258 ''RECENT'', plus the title/subtitle/open-project block the catalog already has translations for.
|
||||
|
||||
Acceptance: no hardcoded user-facing string literals remain in lib/builtin/welcome/; the pre-existing builtin.welcome catalog keys are actually consumed (no orphans); section headers added as keys.', 'done', 'medium', NULL, NULL, 'D-21', '2026-06-17 09:57:26.627', '2026-06-18 13:41:29.322', NULL, '9ce7dc6dd92151c29617d26088c3c85c', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0WCPG458GF5FXTXM9VY98', 'task', '06FDA0TPYSWEM10RP0Q76XAP58', 'i18n: migrate builtin.claude UI labels to the catalog (~35 strings)', 'Largest single offender in the audit: ~35 hardcoded user-facing strings in builtin.claude bypass i18n, despite builtin.claude_en_us.json already existing (currently tab.title + 4 orphaned status.* keys).
|
||||
|
||||
Route the hardcoded ClideText/Text/tooltip literals through ctx.i18n.string(key, namespace: ''builtin.claude'', placeholder: <current literal>), and add the keys to the catalog. Hotspots from the audit include lib/builtin/claude/src/prompt_card.dart (e.g. :314 ''Review your answers'', :372 ''Review'') and the permission/AskUserQuestion interaction widgets — sweep the whole extension, not just those lines.
|
||||
|
||||
Watch the interaction-zone widgets (D-78): keep them display-only; the i18n change is label-only, no behavioural change.
|
||||
|
||||
Acceptance: no hardcoded user-facing string literals remain in lib/builtin/claude/ (single-char glyphs like ''<'' / ''+'' used as icons are exempt — note any left); all new keys present in the catalog; existing orphaned status.* keys either wired up or removed.', 'in_progress', 'high', NULL, NULL, 'D-21', '2026-06-17 09:57:20.180', '2026-06-18 13:52:52.920', NULL, '056dfb04194ffe61a7e2eb3102b88aa5', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0XMA9BQV4RHJ55BFN4JR4', 'task', '06FDA0TPYSWEM10RP0Q76XAP58', 'i18n: migrate git + diff panel labels to the catalog (~16 strings)', 'builtin.git (~9 hardcoded) and builtin.diff (~7 hardcoded) panel/view labels bypass i18n. builtin.git has a tab.title-only catalog; builtin.diff''s catalog is created by the gaps task (T-463) — extend both.
|
||||
|
||||
Hotspots from the audit:
|
||||
- lib/builtin/git/src/git_status_item.dart:167 ''Switch branch'', :179 ''Loading…''
|
||||
- lib/builtin/git/src/git_panel_view.dart:94 ''Nothing to commit, working tree clean.''
|
||||
- lib/builtin/diff/src/diff_view.dart:105 ''No staged changes.''/''No unstaged changes.'', :153 ''Unstaged'', :163 ''Staged''
|
||||
|
||||
Acceptance: no hardcoded user-facing literals in lib/builtin/git/ or lib/builtin/diff/; keys added to builtin.git and builtin.diff catalogs. Depends on T-463 for the builtin.diff catalog file.', 'in_progress', 'medium', NULL, NULL, 'D-21', '2026-06-17 09:57:30.322', '2026-06-18 13:52:52.931', NULL, '0c8b2df9caa133215ac9c0b1b1a46296', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0YCY2WGSG8W46RHQ5Z9MW', 'task', '06FDA0TPYSWEM10RP0Q76XAP58', 'i18n: migrate planning-pane labels — tickets, pql, decisions, problems (~21 strings)', 'The four pql/planning-surface extensions each have a tab.title-only catalog and hardcode the rest: tickets ~6, pql ~6, decisions ~6, problems ~3. Sweep all four, extend each catalog.
|
||||
|
||||
For each: route hardcoded ClideText/Text/tooltip literals through ctx.i18n.string(..., namespace: ''builtin.<ext>'', placeholder: <current literal>) and add keys.
|
||||
|
||||
Acceptance: no hardcoded user-facing literals remain in lib/builtin/{tickets,pql,decisions,problems}/; keys present in each respective catalog.', 'in_progress', 'medium', NULL, NULL, 'D-21', '2026-06-17 09:57:36.624', '2026-06-18 13:52:52.931', NULL, '6a60e0b3b5cc6a4b09b0b65a1bf05e79', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0YYCJ0NK8C4HMDYKJHRDG', 'task', '06FDA0TPYSWEM10RP0Q76XAP58', 'i18n: migrate remaining extension labels — menubar, output, search, deeplink, editor, files, terminal, markdown, vim (~24 strings)', 'Tail of the audit: the remaining extensions with hardcoded labels. menubar ~6 (no catalog), output ~4 (no catalog), search ~3 (no catalog), deeplink ~3 (no catalog), editor ~1, files ~1, terminal ~1, markdown ~1 (no catalog), vim ~1 (no catalog).
|
||||
|
||||
Several have no catalog file yet — create lib/kernel/src/i18n/catalog/builtin.<ext>_en_us.json for menubar, output, search, deeplink, markdown, vim. editor/files/terminal already have catalogs (with orphaned keys) — wire their hardcoded literals up and consume the orphans where they match.
|
||||
|
||||
Can be split into smaller tasks if a single PR is too large; grouped here to keep the long tail in one tracked place.
|
||||
|
||||
Acceptance: no hardcoded user-facing literals remain in any of these extensions; each has a catalog file with the keys it uses; no orphaned keys left in editor/files/terminal.', 'in_progress', 'medium', NULL, NULL, 'D-21', '2026-06-17 09:57:41.092', '2026-06-18 13:52:52.932', NULL, '3313d03f3d8428d61aa3528c2f4371db', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0WCPG458GF5FXTXM9VY98', 'task', '06FDA0TPYSWEM10RP0Q76XAP58', 'i18n: migrate builtin.claude UI labels to the catalog (~35 strings)', 'Largest single offender in the audit: ~35 hardcoded user-facing strings in builtin.claude bypass i18n, despite builtin.claude_en_us.json already existing (currently tab.title + 4 orphaned status.* keys).
|
||||
|
||||
Route the hardcoded ClideText/Text/tooltip literals through ctx.i18n.string(key, namespace: ''builtin.claude'', placeholder: <current literal>), and add the keys to the catalog. Hotspots from the audit include lib/builtin/claude/src/prompt_card.dart (e.g. :314 ''Review your answers'', :372 ''Review'') and the permission/AskUserQuestion interaction widgets — sweep the whole extension, not just those lines.
|
||||
|
||||
Watch the interaction-zone widgets (D-78): keep them display-only; the i18n change is label-only, no behavioural change.
|
||||
|
||||
Acceptance: no hardcoded user-facing string literals remain in lib/builtin/claude/ (single-char glyphs like ''<'' / ''+'' used as icons are exempt — note any left); all new keys present in the catalog; existing orphaned status.* keys either wired up or removed.', 'done', 'high', NULL, NULL, 'D-21', '2026-06-17 09:57:20.180', '2026-06-18 19:14:41.116', NULL, 'f231ab99f6066bf5bf45085d0c7b02eb', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0XMA9BQV4RHJ55BFN4JR4', 'task', '06FDA0TPYSWEM10RP0Q76XAP58', 'i18n: migrate git + diff panel labels to the catalog (~16 strings)', 'builtin.git (~9 hardcoded) and builtin.diff (~7 hardcoded) panel/view labels bypass i18n. builtin.git has a tab.title-only catalog; builtin.diff''s catalog is created by the gaps task (T-463) — extend both.
|
||||
|
||||
Hotspots from the audit:
|
||||
- lib/builtin/git/src/git_status_item.dart:167 ''Switch branch'', :179 ''Loading…''
|
||||
- lib/builtin/git/src/git_panel_view.dart:94 ''Nothing to commit, working tree clean.''
|
||||
- lib/builtin/diff/src/diff_view.dart:105 ''No staged changes.''/''No unstaged changes.'', :153 ''Unstaged'', :163 ''Staged''
|
||||
|
||||
Acceptance: no hardcoded user-facing literals in lib/builtin/git/ or lib/builtin/diff/; keys added to builtin.git and builtin.diff catalogs. Depends on T-463 for the builtin.diff catalog file.', 'done', 'medium', NULL, NULL, 'D-21', '2026-06-17 09:57:30.322', '2026-06-18 19:19:25.268', NULL, 'c14bcdd5a816e86b78af9b5ac795b8a7', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0YCY2WGSG8W46RHQ5Z9MW', 'task', '06FDA0TPYSWEM10RP0Q76XAP58', 'i18n: migrate planning-pane labels — tickets, pql, decisions, problems (~21 strings)', 'The four pql/planning-surface extensions each have a tab.title-only catalog and hardcode the rest: tickets ~6, pql ~6, decisions ~6, problems ~3. Sweep all four, extend each catalog.
|
||||
|
||||
For each: route hardcoded ClideText/Text/tooltip literals through ctx.i18n.string(..., namespace: ''builtin.<ext>'', placeholder: <current literal>) and add keys.
|
||||
|
||||
Acceptance: no hardcoded user-facing literals remain in lib/builtin/{tickets,pql,decisions,problems}/; keys present in each respective catalog.', 'done', 'medium', NULL, NULL, 'D-21', '2026-06-17 09:57:36.624', '2026-06-18 20:13:50.352', NULL, '919efbfb38b655d5157f73572aa20fad', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0YYCJ0NK8C4HMDYKJHRDG', 'task', '06FDA0TPYSWEM10RP0Q76XAP58', 'i18n: migrate remaining extension labels — menubar, output, search, deeplink, editor, files, terminal, markdown, vim (~24 strings)', 'Tail of the audit: the remaining extensions with hardcoded labels. menubar ~6 (no catalog), output ~4 (no catalog), search ~3 (no catalog), deeplink ~3 (no catalog), editor ~1, files ~1, terminal ~1, markdown ~1 (no catalog), vim ~1 (no catalog).
|
||||
|
||||
Several have no catalog file yet — create lib/kernel/src/i18n/catalog/builtin.<ext>_en_us.json for menubar, output, search, deeplink, markdown, vim. editor/files/terminal already have catalogs (with orphaned keys) — wire their hardcoded literals up and consume the orphans where they match.
|
||||
|
||||
Can be split into smaller tasks if a single PR is too large; grouped here to keep the long tail in one tracked place.
|
||||
|
||||
Acceptance: no hardcoded user-facing literals remain in any of these extensions; each has a catalog file with the keys it uses; no orphaned keys left in editor/files/terminal.', 'done', 'medium', NULL, NULL, 'D-21', '2026-06-17 09:57:41.092', '2026-06-18 20:16:03.746', NULL, '8212472a6e32ec3000b829a7abee3cb9', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA10DRQE3SED10JAW2CZDCR', 'task', '06FDA0TPYSWEM10RP0Q76XAP58', 'i18n: route framework-level labels in lib/widgets + lib/kernel (~10 strings)', '~9 hardcoded user-facing strings in lib/widgets/ and ~1 in lib/kernel/ live outside any extension, so they have no obvious i18n namespace. Resolve the namespace question, then migrate.
|
||||
|
||||
Hotspots from the audit:
|
||||
- lib/widgets/src/ex_line_overlay.dart:163 '':'' , :183 ''Not an editor command'' (these are vim ex-line — may belong to builtin.vim''s namespace rather than a generic one)
|
||||
- lib/widgets/src/multitab_pane.dart:358 ''+'' (likely an icon glyph — exempt if so)
|
||||
|
||||
Open question to settle in this task: do framework widgets get a ''core''/''kernel'' namespace, or does each caller pass its owning extension''s namespace down? Pick one and note it (small D- or Q- record if it sets precedent). Single-char icon glyphs (''+'', '':'') are exempt — note any left.
|
||||
|
||||
Acceptance: real user-facing sentences in lib/widgets + lib/kernel resolve through i18n under a documented namespace; glyph exemptions noted.', 'done', 'low', NULL, NULL, 'D-21', '2026-06-17 09:57:53.221', '2026-06-18 21:21:27.304', NULL, '9817d717fa77916d07547d9674d14fcb', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'story', NULL, 'Per-repo Claude account login (not system-wide)', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-19 07:42:08.735', '2026-06-19 07:42:08.735', NULL, '35baee58ff25f10d9ba81902bd4072b8', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'story', NULL, 'Per-repo Claude account login (not system-wide)', 'Today clide uses Claude Code''s system-wide login (the CLI''s global auth), so every repo/workspace shares one Claude account. Allow selecting/logging into a separate Claude account per repo, so different projects can run under different accounts (e.g. personal vs work). Needs: per-workspace auth storage + a way for the spawned Claude session to use that repo''s account instead of the global one; UI to sign in/switch per repo; fall back to the system login when none is set.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-19 07:42:08.735', '2026-06-19 07:42:08.761', NULL, '65bb4a1083e1ecfba38bfd1c3837e133', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0TPYSWEM10RP0Q76XAP58', 'epic', NULL, 'i18n coverage: route hardcoded UI labels through the catalog', 'D-21 makes i18n a Tier-0 contract: "All user-facing strings resolve through a namespaced i18n catalogue loader." In practice coverage is ~7%. A June 2026 audit found:
|
||||
|
||||
- Only 2 of 30 builtin extensions actually call .string()/.interpolated(): builtin.settings-ui and builtin.theme-picker. Total live i18n call sites: 17.
|
||||
- ~144 hardcoded user-facing string literals (Text/ClideText, tooltip:, hintText:, dialog/toast text) bypass i18n across the other 28 extensions + lib/widgets + lib/kernel.
|
||||
- 12 of the 14 existing catalogs are effectively orphaned: the strings are pre-translated but the code hardcodes the label anyway (e.g. welcome ships a 10-key catalog AND 15 hardcoded strings; claude ships 10 keys + 35 hardcoded).
|
||||
|
||||
Because every call site and TabContribution supplies an inline English placeholder, nothing renders blank at runtime — so this is invisible in the running app. The cost is that ~93% of labels can never be localized: they never reach the catalog lookup. That is a Tier-0 (D-21) gap, not polish.
|
||||
|
||||
Prior work: T-155 (done) fixed the catalog plumbing + boot warnings and created tab.title catalogs; T-371 (backlog, high, parent T-359) fixes the a11y/i18n gate tests that under-enumerate namespaces. Neither migrates the hardcoded labels — that is this epic.
|
||||
|
||||
This epic = migrate the ~144 hardcoded labels to i18n, per extension, extending the existing catalogs where present. T-371 should land alongside so the gate fails when a new namespace ships uncovered and the work cannot re-drift.
|
||||
|
||||
Audit per-extension hardcoded counts (approx): claude 35, welcome 15, git 9, diff 7, tickets 6, pql 6, menubar 6, decisions 6, output 4, search 3, problems 3, deeplink 3, editor 1, files 1, terminal 1, markdown 1, vim 1; lib/widgets 9; lib/kernel 1.', 'done', 'high', NULL, NULL, 'D-21', '2026-06-17 09:57:06.422', '2026-06-19 08:20:44.217', NULL, '8ee6f221b3d13163bbe9e149111aca64', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
|
||||
@@ -22,6 +22,37 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
||||
|
||||
### Fixed
|
||||
|
||||
## [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.7.1] — 2026-06-18
|
||||
|
||||
### Changed
|
||||
|
||||
- **Josefin Sans is the default UI font again.** Reverts the Inter default from
|
||||
2.7.0; Inter stays bundled and selectable in Settings → Appearance. JetBrains
|
||||
Mono remains the default monospace face (Fira Mono selectable). (T-460)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Markdown prose honours the UI-font setting.** Claude's conversation prose
|
||||
and inline links render through the markdown engine, which pinned the bundled
|
||||
UI face and ignored the Appearance UI-font pick; it now follows the setting
|
||||
live, like the rest of the app. (T-475)
|
||||
|
||||
## [2.7.0] — 2026-06-17
|
||||
|
||||
### Added
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
{
|
||||
"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." },
|
||||
"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.starting": { "translation": "starting…" },
|
||||
"pane.modeBadge.semantics": { "translation": "permission mode: {mode}" },
|
||||
"running.semantics": { "translation": "Claude is running" },
|
||||
"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.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" },
|
||||
"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" },
|
||||
"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." }
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"command.clide.installCli": { "translation": "clide: Install 'clide' command in PATH" }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"tab.title": { "translation": "Decisions" },
|
||||
"tab.detail.title": { "translation": "Decision" },
|
||||
"loading": { "translation": "Loading decisions..." },
|
||||
"error.load": { "translation": "failed to load decisions" },
|
||||
"empty": { "translation": "No decisions found.\nRun `pql decisions sync` to index." },
|
||||
"filter.hint": { "translation": "Filter decisions…" },
|
||||
"refresh.tooltip": { "translation": "Refresh decisions" },
|
||||
"section.confirmed": { "translation": "CONFIRMED" },
|
||||
"section.questions": { "translation": "QUESTIONS" },
|
||||
"section.rejected": { "translation": "REJECTED" },
|
||||
"badge.resolved": { "translation": "resolved" },
|
||||
"detail.loading": { "translation": "Loading…" },
|
||||
"detail.empty": { "translation": "Select a decision to view details." },
|
||||
"detail.section.refs": { "translation": "CROSS-REFERENCES" }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"dialog.title": { "translation": "Open an external link?" },
|
||||
"dialog.body": { "translation": "A clide:// link from outside the app is asking to:" },
|
||||
"dialog.warning": { "translation": "Only allow this if you trust where the link came from." },
|
||||
"button.cancel": { "translation": "Cancel" },
|
||||
"button.open": { "translation": "Open" },
|
||||
"command.deeplink.invoke": { "translation": "Open a clide:// deep link" }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"command.reset": { "translation": "Layout: Reset to Classic" },
|
||||
"preset.classic": { "translation": "Classic" },
|
||||
"command.palette.toggle": { "translation": "Command Palette" },
|
||||
"command.sidebar.collapse": { "translation": "Toggle Sidebar Collapse" },
|
||||
"command.context.collapse": { "translation": "Toggle Context Panel Collapse" },
|
||||
"command.panel.focus.left": { "translation": "Focus Left Panel" },
|
||||
"command.panel.focus.middle": { "translation": "Focus Middle Panel" },
|
||||
"command.panel.focus.right": { "translation": "Focus Right Panel" },
|
||||
"command.panel.focusMode": { "translation": "Toggle Focus Mode" },
|
||||
"command.panel.focusMode.exit": { "translation": "Exit Focus Mode" },
|
||||
"command.editor.open": { "translation": "Open Editor" },
|
||||
"command.editor.close": { "translation": "Close Editor" },
|
||||
"command.workspace.tab.next": { "translation": "Next Workspace Tab" },
|
||||
"command.workspace.tab.previous": { "translation": "Previous Workspace Tab" },
|
||||
"command.sidebar.section.1": { "translation": "Sidebar: Section 1" },
|
||||
"command.sidebar.section.2": { "translation": "Sidebar: Section 2" },
|
||||
"command.sidebar.section.3": { "translation": "Sidebar: Section 3" },
|
||||
"command.sidebar.section.4": { "translation": "Sidebar: Section 4" },
|
||||
"command.sidebar.section.5": { "translation": "Sidebar: Section 5" }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"tab.title": { "translation": "Diff" },
|
||||
"view.semantics": { "translation": "diff view" },
|
||||
"status.loading": { "translation": "Loading…" },
|
||||
"empty.staged": { "translation": "No staged changes." },
|
||||
"empty.unstaged": { "translation": "No unstaged changes." },
|
||||
"toolbar.unstaged": { "translation": "Unstaged" },
|
||||
"toolbar.unstaged.semantics": { "translation": "show unstaged changes" },
|
||||
"toolbar.staged": { "translation": "Staged" },
|
||||
"toolbar.staged.semantics": { "translation": "show staged changes" },
|
||||
"meta.newFile": { "translation": "new file" },
|
||||
"meta.deleted": { "translation": "deleted" },
|
||||
"meta.renamedFrom": { "translation": "renamed from {path}" },
|
||||
"meta.binary": { "translation": "binary" }
|
||||
}
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"tab.title": { "translation": "Editor" },
|
||||
"chrome.title": { "translation": "editor" },
|
||||
"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" }
|
||||
}
|
||||
+4
-1
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"notice.title": { "translation": "Extension management is coming" },
|
||||
"notice.body": { "translation": "Installing, enabling, and disabling extensions arrives with third-party (Lua) extension support. For now the built-in extensions are always on." },
|
||||
"notice.tracked": { "translation": "Tracked in T-8 (Tier 6) · D-16" }
|
||||
"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,26 @@
|
||||
{
|
||||
"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" },
|
||||
"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
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"command.open": { "translation": "Settings…" },
|
||||
"modal.title": { "translation": "Settings" },
|
||||
"modal.close": { "translation": "Close" },
|
||||
"modal.close.hint": { "translation": "Close settings without changing anything" },
|
||||
+3
-1
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"tab.title": { "translation": "Terminal" },
|
||||
"chrome.title": { "translation": "terminal" },
|
||||
"subtitle.spawning": { "translation": "spawning shell…" },
|
||||
"subtitle.exited": { "translation": "Shell exited." },
|
||||
"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,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,35 @@
|
||||
{
|
||||
"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…" },
|
||||
"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 root requires a git repository." },
|
||||
"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,201 @@
|
||||
{
|
||||
"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." },
|
||||
"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.starting": { "translation": "starten…" },
|
||||
"pane.modeBadge.semantics": { "translation": "permissiemodus: {mode}" },
|
||||
"running.semantics": { "translation": "Claude is bezig" },
|
||||
"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.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" },
|
||||
"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" },
|
||||
"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." }
|
||||
}
|
||||
@@ -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,26 @@
|
||||
{
|
||||
"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" },
|
||||
"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,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,35 @@
|
||||
{
|
||||
"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…" },
|
||||
"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-projecthoofdmap vereist een git-repository." },
|
||||
"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`
|
||||
# (runs implicitly on every build/run/test). Don't hand-edit; bump
|
||||
# pubspec instead.
|
||||
version: "2.7.0"
|
||||
version: "2.8.0"
|
||||
homepage: https://github.com/postmeridiem/clide
|
||||
license: MIT
|
||||
license_file: assets/LICENSE
|
||||
@@ -76,8 +76,8 @@ dependencies:
|
||||
license: OFL-1.1
|
||||
license_file: assets/fonts/inter/OFL.txt
|
||||
purpose: >-
|
||||
Default application UI face (T-460). Variable font with optical-size
|
||||
and weight axes; Josefin Sans remains bundled as a selectable option.
|
||||
Selectable application UI face (bundled T-460); Josefin Sans is the
|
||||
default. Variable font with optical-size and weight axes.
|
||||
weights_bundled: [VariableFont, Italic-VariableFont]
|
||||
|
||||
- name: Josefin Sans
|
||||
@@ -87,8 +87,9 @@ dependencies:
|
||||
license: OFL-1.1
|
||||
license_file: assets/fonts/josefin_sans/OFL.txt
|
||||
purpose: >-
|
||||
Selectable application UI face (was the default before T-460). Default
|
||||
weight Light (300); full 100-700 range via the variable-font weight axis.
|
||||
Default application UI face; default weight Light (300), full 100-700
|
||||
range via the variable-font weight axis. Inter is bundled as a
|
||||
selectable alternative.
|
||||
weights_bundled: [VariableFont, Italic-VariableFont]
|
||||
|
||||
- name: Phosphor Icons
|
||||
|
||||
@@ -143,6 +143,7 @@ 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-100: Fence `dart:ffi` behind conditional imports + web stubs to keep the web/WASM target compiling](decisions/tooling.md#d-100-fence-dartffi-behind-conditional-imports--web-stubs-to-keep-the-webwasm-target-compiling) — _tooling_
|
||||
- [D-101: ClideSettings — one live-preferences access facade](decisions/architecture.md#d-101-clidesettings--one-live-preferences-access-facade) — _architecture_
|
||||
- [D-102: i18n routing — ext-id namespaces, `core` catalog, ClideSettings.i18n facade, contribution keys](decisions/accessibility.md#d-102-i18n-routing--ext-id-namespaces-core-catalog-clidesettingsi18n-facade-contribution-keys) — _accessibility_
|
||||
|
||||
## 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.
|
||||
- **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).
|
||||
|
||||
---
|
||||
|
||||
@@ -35,7 +35,12 @@ class ClaudeBanner extends StatelessWidget {
|
||||
children: [
|
||||
const ClideSvgView.asset('assets/logo/logo.svg', width: 60, height: 60),
|
||||
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),
|
||||
ClideText(role, fontSize: clideFontSmall, muted: true, fontFamily: ClideSettings.fonts.monoOf(context)),
|
||||
const SizedBox(height: 16),
|
||||
@@ -45,7 +50,16 @@ class ClaudeBanner extends StatelessWidget {
|
||||
ClideText(statusLine!, fontSize: clideFontSmall, muted: true, fontFamily: ClideSettings.fonts.monoOf(context)),
|
||||
],
|
||||
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,
|
||||
required this.onSubmit,
|
||||
this.enabled = true,
|
||||
this.hint = 'Message Claude… (Enter to send · Shift+Enter for newline)',
|
||||
this.hint,
|
||||
this.pasteResolver,
|
||||
this.slashCommandsResolver,
|
||||
this.onInterrupt,
|
||||
@@ -63,7 +63,11 @@ class ClaudeComposer extends StatefulWidget {
|
||||
final void Function(String text) onSubmit;
|
||||
|
||||
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
|
||||
/// 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 hasText = _controller.text.isNotEmpty;
|
||||
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(
|
||||
padding: const EdgeInsets.fromLTRB(10, 6, 10, 10),
|
||||
@@ -444,10 +456,15 @@ class _ClaudeComposerState extends State<ClaudeComposer> {
|
||||
const RunningIndicator(),
|
||||
const Spacer(),
|
||||
ClideButton(
|
||||
label: 'Stop ⎋',
|
||||
label: ClideSettings.i18n.string(context, 'composer.stop', namespace: 'builtin.claude', placeholder: 'Stop ⎋'),
|
||||
variant: ClideButtonVariant.primary,
|
||||
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)
|
||||
Padding(
|
||||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Semantics(
|
||||
label: widget.hint,
|
||||
label: hint,
|
||||
textField: true,
|
||||
child: Shortcuts(
|
||||
shortcuts: const {
|
||||
@@ -486,7 +503,7 @@ class _ClaudeComposerState extends State<ClaudeComposer> {
|
||||
},
|
||||
child: Stack(
|
||||
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(
|
||||
controller: _controller,
|
||||
focusNode: _focus,
|
||||
@@ -521,7 +538,7 @@ class _ClaudeComposerState extends State<ClaudeComposer> {
|
||||
|
||||
/// One attachment chip: a thumbnail (images) or file icon (other types),
|
||||
/// 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(
|
||||
constraints: const BoxConstraints(maxWidth: 220),
|
||||
decoration: BoxDecoration(
|
||||
@@ -541,7 +558,13 @@ class _ClaudeComposerState extends State<ClaudeComposer> {
|
||||
const SizedBox(width: 4),
|
||||
Semantics(
|
||||
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(
|
||||
key: ValueKey('composer-remove-${a.path}'),
|
||||
onTap: () => _removeAttachment(a),
|
||||
|
||||
@@ -833,7 +833,9 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
},
|
||||
);
|
||||
} 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;
|
||||
@@ -856,7 +858,13 @@ class _ModeBadge extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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,
|
||||
child: ClideText(
|
||||
permissionModeLabel(mode),
|
||||
|
||||
@@ -33,13 +33,28 @@ class _ClaudeTaskDockState extends State<ClaudeTaskDock> {
|
||||
final done = tasks.where((t) => t.status == TaskStatus.completed).length;
|
||||
final inProgress = tasks.where((t) => t.status == TaskStatus.inProgress);
|
||||
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(
|
||||
padding: const EdgeInsets.fromLTRB(10, 0, 10, 6),
|
||||
child: ClideTappable(
|
||||
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(
|
||||
decoration: BoxDecoration(
|
||||
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).
|
||||
Semantics(
|
||||
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,
|
||||
child: _summaryRow(tokens, summary, current),
|
||||
),
|
||||
@@ -91,14 +120,35 @@ class _ClaudeTaskDockState extends State<ClaudeTaskDock> {
|
||||
|
||||
Widget _taskRow(SurfaceTokens tokens, TaskItem t) {
|
||||
final (String glyph, Color color, String word) = switch (t.status) {
|
||||
TaskStatus.completed => ('check-circle', tokens.statusSuccess, 'done'),
|
||||
TaskStatus.inProgress => ('circle-half', tokens.globalFocus, 'in progress'),
|
||||
TaskStatus.pending => ('circle', tokens.globalTextMuted, 'pending'),
|
||||
TaskStatus.completed => (
|
||||
'check-circle',
|
||||
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(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
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,
|
||||
excludeSemantics: true,
|
||||
child: Row(
|
||||
|
||||
@@ -270,7 +270,9 @@ class _ConversationCardState extends State<ConversationCard> {
|
||||
}
|
||||
|
||||
Widget _caret(SurfaceTokens tokens) {
|
||||
final label = _collapsed ? 'Expand' : 'Collapse';
|
||||
final label = _collapsed
|
||||
? ClideSettings.i18n.string(context, 'card.expand', namespace: 'builtin.claude', placeholder: 'Expand')
|
||||
: ClideSettings.i18n.string(context, 'card.collapse', namespace: 'builtin.claude', placeholder: 'Collapse');
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: label,
|
||||
@@ -298,11 +300,11 @@ class _ConversationCardState extends State<ConversationCard> {
|
||||
case ConversationCardStatus.success:
|
||||
icon = const CheckIcon();
|
||||
color = tokens.statusSuccess;
|
||||
label = 'succeeded';
|
||||
label = ClideSettings.i18n.string(context, 'card.succeeded', namespace: 'builtin.claude', placeholder: 'succeeded');
|
||||
case ConversationCardStatus.error:
|
||||
icon = const CloseIcon();
|
||||
color = tokens.statusError;
|
||||
label = 'failed';
|
||||
label = ClideSettings.i18n.string(context, 'card.failed', namespace: 'builtin.claude', placeholder: 'failed');
|
||||
case ConversationCardStatus.none:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
@@ -331,7 +333,9 @@ class _ConversationCardState extends State<ConversationCard> {
|
||||
|
||||
List<Widget> _actions(SurfaceTokens tokens) {
|
||||
final items = <_ActionItem>[];
|
||||
if (widget.copyText != null) items.add(_ActionItem('copy', _copy));
|
||||
if (widget.copyText != null) {
|
||||
items.add(_ActionItem(ClideSettings.i18n.string(context, 'card.copy', namespace: 'builtin.claude', placeholder: 'copy'), _copy));
|
||||
}
|
||||
for (final a in widget.actions) {
|
||||
items.add(_ActionItem(a.label, a.onInvoke));
|
||||
}
|
||||
|
||||
@@ -303,7 +303,14 @@ class _ConversationViewState extends State<ConversationView> {
|
||||
if (items.isEmpty) {
|
||||
return ColoredBox(
|
||||
color: tokens.panelBackground,
|
||||
child: widget.emptyState ?? const Center(child: ClideText('Waiting for Claude…', muted: true)),
|
||||
child:
|
||||
widget.emptyState ??
|
||||
Center(
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(context, 'conversation.empty', namespace: 'builtin.claude', placeholder: 'Waiting for Claude…'),
|
||||
muted: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -530,12 +537,20 @@ class _BashLiveTailState extends State<_BashLiveTail> {
|
||||
Widget build(BuildContext context) {
|
||||
final term = _terminal;
|
||||
if (term == null) {
|
||||
return ClideText('no independent source to follow', muted: true, fontSize: clideFontMeta);
|
||||
return ClideText(
|
||||
ClideSettings.i18n.string(context, 'conversation.bashTail.empty', namespace: 'builtin.claude', placeholder: 'no independent source to follow'),
|
||||
muted: true,
|
||||
fontSize: clideFontMeta,
|
||||
);
|
||||
}
|
||||
return SizedBox(
|
||||
height: 160,
|
||||
child: ClipRect(
|
||||
child: ClidePtyView(terminal: term, label: 'live tail', fontSize: clideFontMeta),
|
||||
child: ClidePtyView(
|
||||
terminal: term,
|
||||
label: ClideSettings.i18n.string(context, 'conversation.bashTail.label', namespace: 'builtin.claude', placeholder: 'live tail'),
|
||||
fontSize: clideFontMeta,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -614,7 +629,9 @@ class _ConversationTurn extends StatelessWidget {
|
||||
// carded tool calls.
|
||||
variant: ConversationCardVariant.bordered,
|
||||
accent: tokens.globalTextMuted,
|
||||
label: i.isSidechain ? 'agent prompt' : 'context',
|
||||
label: i.isSidechain
|
||||
? ClideSettings.i18n.string(context, 'conversation.label.agentPrompt', namespace: 'builtin.claude', placeholder: 'agent prompt')
|
||||
: ClideSettings.i18n.string(context, 'conversation.label.context', namespace: 'builtin.claude', placeholder: 'context'),
|
||||
copyText: i.text,
|
||||
collapsible: true,
|
||||
collapsedByDefault: true,
|
||||
@@ -624,7 +641,7 @@ class _ConversationTurn extends StatelessWidget {
|
||||
),
|
||||
UserMessage() => ConversationCard(
|
||||
accent: tokens.globalFocus,
|
||||
label: 'you',
|
||||
label: ClideSettings.i18n.string(context, 'conversation.label.you', namespace: 'builtin.claude', placeholder: 'you'),
|
||||
copyText: i.text,
|
||||
margin: _childMargin,
|
||||
// Pasted-image @path tokens render as inline thumbnails that open the
|
||||
@@ -644,7 +661,7 @@ class _ConversationTurn extends StatelessWidget {
|
||||
AssistantTextMessage() when i.synthetic => ConversationCard(
|
||||
variant: ConversationCardVariant.bordered,
|
||||
accent: tokens.globalTextMuted,
|
||||
label: 'clide',
|
||||
label: ClideSettings.i18n.string(context, 'conversation.label.clide', namespace: 'builtin.claude', placeholder: 'clide'),
|
||||
copyText: i.text,
|
||||
margin: _childMargin,
|
||||
body: ClideText(i.text, muted: true, fontSize: clideFontMeta),
|
||||
@@ -654,7 +671,9 @@ class _ConversationTurn extends StatelessWidget {
|
||||
// coral claudeAccent is reserved for the real main-thread Claude.
|
||||
AssistantTextMessage() => ConversationCard(
|
||||
accent: i.isSidechain ? tokens.globalTextMuted : claudeAccent,
|
||||
label: i.isSidechain ? 'agent' : 'claude',
|
||||
label: i.isSidechain
|
||||
? ClideSettings.i18n.string(context, 'conversation.label.agent', namespace: 'builtin.claude', placeholder: 'agent')
|
||||
: ClideSettings.i18n.string(context, 'conversation.label.claude', namespace: 'builtin.claude', placeholder: 'claude'),
|
||||
copyText: i.text,
|
||||
margin: _childMargin,
|
||||
body: ClideMarkdown(
|
||||
@@ -669,7 +688,9 @@ class _ConversationTurn extends StatelessWidget {
|
||||
// Framed + muted like the context card (T-306).
|
||||
variant: ConversationCardVariant.bordered,
|
||||
accent: tokens.globalTextMuted,
|
||||
label: i.isSidechain ? 'agent thinking' : 'thinking',
|
||||
label: i.isSidechain
|
||||
? ClideSettings.i18n.string(context, 'conversation.label.agentThinking', namespace: 'builtin.claude', placeholder: 'agent thinking')
|
||||
: ClideSettings.i18n.string(context, 'conversation.label.thinking', namespace: 'builtin.claude', placeholder: 'thinking'),
|
||||
copyText: i.thinking,
|
||||
collapsible: true,
|
||||
collapsedByDefault: true,
|
||||
@@ -677,8 +698,8 @@ class _ConversationTurn extends StatelessWidget {
|
||||
margin: _childMargin,
|
||||
body: ClideText(i.thinking, muted: true, fontSize: clideFontMeta),
|
||||
),
|
||||
AssistantToolUse() => collapseTools ? _toolUseCollapser(i) : _toolContentCard(i),
|
||||
ToolResultMessage() => _toolResult(i),
|
||||
AssistantToolUse() => collapseTools ? _toolUseCollapser(context, i) : _toolContentCard(context, i),
|
||||
ToolResultMessage() => _toolResult(context, i),
|
||||
ImageMessage() => _image(context, i),
|
||||
};
|
||||
}
|
||||
@@ -692,7 +713,7 @@ class _ConversationTurn extends StatelessWidget {
|
||||
final caption = m.caption;
|
||||
return ConversationCard(
|
||||
accent: tokens.globalTextMuted,
|
||||
label: 'image',
|
||||
label: ClideSettings.i18n.string(context, 'conversation.label.image', namespace: 'builtin.claude', placeholder: 'image'),
|
||||
copyText: m.path,
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -712,7 +733,7 @@ class _ConversationTurn extends StatelessWidget {
|
||||
image: ClideFileImage(m.path),
|
||||
fit: BoxFit.contain,
|
||||
alignment: Alignment.centerLeft,
|
||||
errorBuilder: (_, _, _) => _imagePlaceholder(m.path),
|
||||
errorBuilder: (_, _, _) => _imagePlaceholder(context, m.path),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -728,12 +749,12 @@ class _ConversationTurn extends StatelessWidget {
|
||||
ClideKernel.of(context).dialog.show<Object>(
|
||||
(ctx, dismiss) => ClideLightbox(
|
||||
onDismiss: dismiss,
|
||||
child: Image(image: ClideFileImage(path), fit: BoxFit.contain, errorBuilder: (_, _, _) => _imagePlaceholder(path)),
|
||||
child: Image(image: ClideFileImage(path), fit: BoxFit.contain, errorBuilder: (_, _, _) => _imagePlaceholder(ctx, path)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _imagePlaceholder(String path) => Container(
|
||||
Widget _imagePlaceholder(BuildContext context, String path) => Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: tokens.panelBorder),
|
||||
@@ -745,7 +766,18 @@ class _ConversationTurn extends StatelessWidget {
|
||||
ClideIcon(PhosphorIcons.byName('image'), size: 16, color: tokens.globalTextMuted),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: ClideText('could not load $path', fontSize: clideFontMeta, color: tokens.globalTextMuted, maxLines: 1),
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'conversation.imagePlaceholder',
|
||||
namespace: 'builtin.claude',
|
||||
placeholder: 'could not load $path',
|
||||
replacers: [I18nReplacer(from: '{path}', replace: path)],
|
||||
),
|
||||
fontSize: clideFontMeta,
|
||||
color: tokens.globalTextMuted,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -757,13 +789,13 @@ class _ConversationTurn extends StatelessWidget {
|
||||
/// — pushed up from the item; the inner card holds the call body + segments
|
||||
/// and its own per-item mark. An Agent/Task call also nests its visible
|
||||
/// sub-agent run in a second collapser below (T-264).
|
||||
Widget _toolUseCollapser(AssistantToolUse t) {
|
||||
Widget _toolUseCollapser(BuildContext context, AssistantToolUse t) {
|
||||
// A Workflow tool-use with a live run (T-416) renders the dedicated run
|
||||
// card — phases, agent rows, status — instead of the generic tool card. No
|
||||
// run yet (pre-progress, or on reload where the system events are gone)
|
||||
// falls through to the generic collapser below.
|
||||
if (t.name == 'Workflow' && workflows[t.toolUseId] != null) {
|
||||
return _workflowCard(t, workflows[t.toolUseId]!);
|
||||
return _workflowCard(context, t, workflows[t.toolUseId]!);
|
||||
}
|
||||
final outcome = toolUseOutcomes[t.toolUseId];
|
||||
final color = outcome == null ? tokens.globalFocus : (outcome ? tokens.statusSuccess : tokens.statusError);
|
||||
@@ -773,7 +805,7 @@ class _ConversationTurn extends StatelessWidget {
|
||||
collapsedSummary: _toolUseSummary(t),
|
||||
counter: '1 step',
|
||||
status: _toolRunStatus(t),
|
||||
children: [_toolContentCard(t)],
|
||||
children: [_toolContentCard(context, t)],
|
||||
);
|
||||
|
||||
final runItems = _isAgentTool(t.name) ? (runByToolUseId[t.toolUseId] ?? const <ConversationItem>[]) : const <ConversationItem>[];
|
||||
@@ -785,7 +817,7 @@ class _ConversationTurn extends StatelessWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 12),
|
||||
child: ClideCollapserCard(
|
||||
label: 'agent run',
|
||||
label: ClideSettings.i18n.string(context, 'conversation.label.agentRun', namespace: 'builtin.claude', placeholder: 'agent run'),
|
||||
collapsedSummary: _summarizeActivity(runItems.last),
|
||||
counter: runItems.length == 1 ? '1 step' : '${runItems.length} steps',
|
||||
children: [
|
||||
@@ -814,23 +846,23 @@ class _ConversationTurn extends StatelessWidget {
|
||||
/// while running, check when done) and a `done/total agents` counter; the body
|
||||
/// lists each fanned-out agent — grouped under phase headers when the workflow
|
||||
/// declared phases — plus the run's usage and the orchestration script.
|
||||
Widget _workflowCard(AssistantToolUse t, WorkflowRun run) {
|
||||
final title = run.name ?? 'workflow';
|
||||
Widget _workflowCard(BuildContext context, AssistantToolUse t, WorkflowRun run) {
|
||||
final title = run.name ?? ClideSettings.i18n.string(context, 'conversation.label.workflow', namespace: 'builtin.claude', placeholder: 'workflow');
|
||||
final color = run.done ? tokens.statusSuccess : tokens.globalFocus;
|
||||
final counter = run.agentCount == 0 ? 'starting' : '${run.doneCount}/${run.agentCount} agents';
|
||||
final detail = run.done ? (run.summary ?? run.description) : run.description;
|
||||
final collapsedSummary = (detail == null || detail == title) ? title : '$title · $detail';
|
||||
return ClideCollapserCard(
|
||||
label: 'workflow',
|
||||
label: ClideSettings.i18n.string(context, 'conversation.label.workflow', namespace: 'builtin.claude', placeholder: 'workflow'),
|
||||
color: color,
|
||||
collapsedSummary: collapsedSummary,
|
||||
counter: counter,
|
||||
status: run.done ? ClideRunStatus.success : ClideRunStatus.running,
|
||||
children: [_workflowBody(t, run)],
|
||||
children: [_workflowBody(context, t, run)],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _workflowBody(AssistantToolUse t, WorkflowRun run) {
|
||||
Widget _workflowBody(BuildContext context, AssistantToolUse t, WorkflowRun run) {
|
||||
final agents = run.orderedAgents;
|
||||
final phases = run.orderedPhases;
|
||||
final rows = <Widget>[];
|
||||
@@ -851,25 +883,31 @@ class _ConversationTurn extends StatelessWidget {
|
||||
rows.addAll(agents.where((a) => a.phaseIndex == null).map(_workflowAgentRow));
|
||||
}
|
||||
if (rows.isEmpty) {
|
||||
rows.add(ClideText('Launching…', muted: true, fontSize: clideFontMeta));
|
||||
rows.add(
|
||||
ClideText(
|
||||
ClideSettings.i18n.string(context, 'conversation.workflow.launching', namespace: 'builtin.claude', placeholder: 'Launching…'),
|
||||
muted: true,
|
||||
fontSize: clideFontMeta,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final script = t.input['script'];
|
||||
return ConversationCard(
|
||||
variant: ConversationCardVariant.bordered,
|
||||
accent: run.done ? tokens.statusSuccess : tokens.globalFocus,
|
||||
label: run.name ?? 'workflow',
|
||||
label: run.name ?? ClideSettings.i18n.string(context, 'conversation.label.workflow', namespace: 'builtin.claude', placeholder: 'workflow'),
|
||||
copyText: script is String ? script : const JsonEncoder.withIndent(' ').convert(t.input),
|
||||
body: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: rows),
|
||||
extraSegments: [
|
||||
if (run.totalTokens != null && run.totalTokens! > 0)
|
||||
CardSegment(
|
||||
label: 'usage',
|
||||
label: ClideSettings.i18n.string(context, 'conversation.segment.usage', namespace: 'builtin.claude', placeholder: 'usage'),
|
||||
child: ClideText('${run.totalTokens} tokens${run.durationMs != null ? ' · ${run.durationMs} ms' : ''}', muted: true, fontSize: clideFontMeta),
|
||||
),
|
||||
if (script is String)
|
||||
CardSegment(
|
||||
label: 'script',
|
||||
label: ClideSettings.i18n.string(context, 'conversation.segment.script', namespace: 'builtin.claude', placeholder: 'script'),
|
||||
child: ClideCodeBlock(source: script, language: 'javascript'),
|
||||
),
|
||||
],
|
||||
@@ -906,7 +944,7 @@ class _ConversationTurn extends StatelessWidget {
|
||||
/// CALL/PROMPT/RESULT segments + its own per-item status mark, with NO own
|
||||
/// collapse caret — the enclosing collapser owns collapse. Used both as a
|
||||
/// run/edit child and as the single child of a standalone tool's collapser.
|
||||
Widget _toolContentCard(AssistantToolUse t) {
|
||||
Widget _toolContentCard(BuildContext context, AssistantToolUse t) {
|
||||
// T-262: fold the paired result into this card. A successful result becomes
|
||||
// a "result" segment below the call + a green header check; a failed result
|
||||
// stamps a red header cross. No result yet (in-flight) → no mark, no segment.
|
||||
@@ -926,12 +964,12 @@ class _ConversationTurn extends StatelessWidget {
|
||||
final segments = <CardSegment>[
|
||||
for (final p in promptsByToolUseId[t.toolUseId] ?? const <UserMessage>[])
|
||||
CardSegment(
|
||||
label: 'prompt',
|
||||
label: ClideSettings.i18n.string(context, 'conversation.segment.prompt', namespace: 'builtin.claude', placeholder: 'prompt'),
|
||||
child: ClideText(p.text, muted: true, fontSize: clideFontMeta),
|
||||
),
|
||||
if (succeeded && !(isAgent && hasRun))
|
||||
CardSegment(
|
||||
label: 'result',
|
||||
label: ClideSettings.i18n.string(context, 'conversation.segment.result', namespace: 'builtin.claude', placeholder: 'result'),
|
||||
child: ClideCodeBlock(source: result.content, language: _resultLanguage(t)),
|
||||
),
|
||||
// T-325: a Bash card that follows a file (`tail -f …`) gets a live,
|
||||
@@ -939,7 +977,7 @@ class _ConversationTurn extends StatelessWidget {
|
||||
// while the card is expanded (the collapser builds segments on expand).
|
||||
if (t.name == 'Bash' && t.input['command'] is String && bashHasTailIntent(t.input['command'] as String))
|
||||
CardSegment(
|
||||
label: 'live tail',
|
||||
label: ClideSettings.i18n.string(context, 'conversation.segment.liveTail', namespace: 'builtin.claude', placeholder: 'live tail'),
|
||||
child: _BashLiveTail(command: t.input['command'] as String),
|
||||
),
|
||||
];
|
||||
@@ -955,7 +993,7 @@ class _ConversationTurn extends StatelessWidget {
|
||||
label: t.name,
|
||||
copyText: const JsonEncoder.withIndent(' ').convert(t.input),
|
||||
status: status,
|
||||
body: toolInputBody(tokens, t.name, t.input, mono),
|
||||
body: toolInputBody(context, tokens, t.name, t.input, mono),
|
||||
extraSegments: segments,
|
||||
// Inside a collapser the surrounding padding is even on all sides
|
||||
// (T-305): a matching bottom margin is the canvas's bottom inset and the
|
||||
@@ -987,10 +1025,12 @@ class _ConversationTurn extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _toolResult(ToolResultMessage t) {
|
||||
Widget _toolResult(BuildContext context, ToolResultMessage t) {
|
||||
final paired = toolUseById[t.toolUseId];
|
||||
final accent = t.isError ? tokens.statusError : tokens.globalTextMuted;
|
||||
final label = t.isError ? 'error' : 'result';
|
||||
final label = t.isError
|
||||
? ClideSettings.i18n.string(context, 'conversation.label.error', namespace: 'builtin.claude', placeholder: 'error')
|
||||
: ClideSettings.i18n.string(context, 'conversation.label.result', namespace: 'builtin.claude', placeholder: 'result');
|
||||
|
||||
// Error result: render the error message prominently (T-168). If we have
|
||||
// the paired tool_use, show the tool name as a sub-label so the user can
|
||||
@@ -1002,7 +1042,7 @@ class _ConversationTurn extends StatelessWidget {
|
||||
if (t.isError) {
|
||||
final quiet = quietErrorToolUseIds.contains(t.toolUseId);
|
||||
final multiline = t.content.contains('\n');
|
||||
final errLabel = quiet ? 'denied' : label;
|
||||
final errLabel = quiet ? ClideSettings.i18n.string(context, 'conversation.label.denied', namespace: 'builtin.claude', placeholder: 'denied') : label;
|
||||
return ConversationCard(
|
||||
variant: ConversationCardVariant.bordered,
|
||||
accent: quiet ? tokens.globalTextMuted : accent,
|
||||
@@ -1086,7 +1126,7 @@ class _ActivityCard extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final count = items.length;
|
||||
return ClideCollapserCard(
|
||||
label: 'Activity',
|
||||
label: ClideSettings.i18n.string(context, 'conversation.cluster.activity', namespace: 'builtin.claude', placeholder: 'Activity'),
|
||||
collapsedSummary: _summarizeActivity(items.last),
|
||||
counter: count == 1 ? '1 step' : '$count steps',
|
||||
status: _runStatus(items, resultByToolUseId),
|
||||
@@ -1152,7 +1192,7 @@ class _EditRunCard extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final count = edits.length;
|
||||
return ClideCollapserCard(
|
||||
label: 'Edits',
|
||||
label: ClideSettings.i18n.string(context, 'conversation.cluster.edits', namespace: 'builtin.claude', placeholder: 'Edits'),
|
||||
collapsedSummary: _summarizeActivity(edits.last),
|
||||
counter: count == 1 ? '1 edit' : '$count edits',
|
||||
status: _runStatus(edits, resultByToolUseId),
|
||||
|
||||
@@ -76,6 +76,8 @@ class ClaudeExtension extends ClideExtension {
|
||||
id: 'claude.new-secondary',
|
||||
command: 'claude.new-secondary',
|
||||
title: 'Claude: open a secondary session',
|
||||
titleKey: 'command.newSecondary',
|
||||
i18nNamespace: id,
|
||||
run: (_) async {
|
||||
_hostKey.currentState?.addSecondary();
|
||||
return IpcResponse.ok(id: '', data: const {'status': 'spawned'});
|
||||
@@ -85,12 +87,16 @@ class ClaudeExtension extends ClideExtension {
|
||||
id: 'claude.kill-all-sessions',
|
||||
command: 'claude.kill-all-sessions',
|
||||
title: 'Claude: kill all sessions for this repo',
|
||||
titleKey: 'command.killAllSessions',
|
||||
i18nNamespace: id,
|
||||
run: _killAllSessions,
|
||||
),
|
||||
CommandContribution(
|
||||
id: 'claude.session-storage',
|
||||
command: 'claude.session-storage',
|
||||
title: 'Claude: session storage (disk usage + cleanup)',
|
||||
titleKey: 'command.sessionStorage',
|
||||
i18nNamespace: id,
|
||||
run: _manageStorage,
|
||||
),
|
||||
// T-235: cycle how aggressively the activity card folds meta steps
|
||||
@@ -100,33 +106,40 @@ class ClaudeExtension extends ClideExtension {
|
||||
id: 'claude.activity.fold-level',
|
||||
command: 'claude.activity.fold-level',
|
||||
title: 'Claude: cycle activity fold level',
|
||||
titleKey: 'command.activity.foldLevel',
|
||||
i18nNamespace: id,
|
||||
run: _cycleFoldLevel,
|
||||
),
|
||||
// Activity settings category (T-453) — the fold level as a schema field,
|
||||
// written to kActivityFoldLevelKey; the panes already rebuild off the
|
||||
// settings notifier, so picking a level applies live.
|
||||
const SettingsCategoryContribution(
|
||||
SettingsCategoryContribution(
|
||||
id: 'activity',
|
||||
category: SettingsCategory(
|
||||
id: 'activity',
|
||||
title: 'Activity',
|
||||
titleKey: 'settings.activity.title',
|
||||
i18nNamespace: id,
|
||||
iconName: 'cards-three',
|
||||
priority: 50,
|
||||
sections: [
|
||||
SettingsSection(
|
||||
label: 'Conversation',
|
||||
labelKey: 'settings.activity.conversation.label',
|
||||
fields: [
|
||||
SettingsField(
|
||||
key: kActivityFoldLevelKey,
|
||||
kind: SettingsFieldKind.select,
|
||||
label: 'Fold level',
|
||||
labelKey: 'settings.activity.foldLevel.label',
|
||||
help: 'How aggressively the conversation folds tool calls, thinking, and results.',
|
||||
helpKey: 'settings.activity.foldLevel.help',
|
||||
defaultValue: 'tools',
|
||||
options: [
|
||||
SettingsOption(value: 'none', label: 'Show everything'),
|
||||
SettingsOption(value: 'tools', label: 'Fold tool calls'),
|
||||
SettingsOption(value: 'thinking', label: 'Fold tools + thinking'),
|
||||
SettingsOption(value: 'everything', label: 'Fold all but prose'),
|
||||
SettingsOption(value: 'none', label: 'Show everything', labelKey: 'settings.activity.opt.none'),
|
||||
SettingsOption(value: 'tools', label: 'Fold tool calls', labelKey: 'settings.activity.opt.tools'),
|
||||
SettingsOption(value: 'thinking', label: 'Fold tools + thinking', labelKey: 'settings.activity.opt.thinking'),
|
||||
SettingsOption(value: 'everything', label: 'Fold all but prose', labelKey: 'settings.activity.opt.everything'),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -142,17 +155,22 @@ class ClaudeExtension extends ClideExtension {
|
||||
category: SettingsCategory(
|
||||
id: 'claude',
|
||||
title: 'Claude',
|
||||
titleKey: 'settings.claude.title',
|
||||
i18nNamespace: id,
|
||||
iconName: 'sparkle',
|
||||
priority: 40,
|
||||
sections: [
|
||||
SettingsSection(
|
||||
label: 'New session defaults',
|
||||
labelKey: 'settings.claude.newSessionDefaults.label',
|
||||
fields: [
|
||||
SettingsField(
|
||||
key: kDefaultModelKey,
|
||||
kind: SettingsFieldKind.select,
|
||||
label: 'Model',
|
||||
labelKey: 'settings.claude.model.label',
|
||||
help: 'Model for new sessions.',
|
||||
helpKey: 'settings.claude.model.help',
|
||||
defaultValue: 'default',
|
||||
options: [for (final m in kFallbackModels) SettingsOption(value: m.value, label: m.displayName)],
|
||||
),
|
||||
@@ -160,7 +178,9 @@ class ClaudeExtension extends ClideExtension {
|
||||
key: kDefaultEffortKey,
|
||||
kind: SettingsFieldKind.select,
|
||||
label: 'Effort',
|
||||
labelKey: 'settings.claude.effort.label',
|
||||
help: 'Reasoning effort for new sessions (applied via --effort at spawn).',
|
||||
helpKey: 'settings.claude.effort.help',
|
||||
defaultValue: 'high',
|
||||
options: [for (final l in kEffortLevels) SettingsOption(value: l.value, label: l.displayName)],
|
||||
),
|
||||
@@ -168,7 +188,9 @@ class ClaudeExtension extends ClideExtension {
|
||||
key: kDefaultPermissionModeKey,
|
||||
kind: SettingsFieldKind.select,
|
||||
label: 'Permission mode',
|
||||
labelKey: 'settings.claude.permissionMode.label',
|
||||
help: 'Starting permission mode for new sessions.',
|
||||
helpKey: 'settings.claude.permissionMode.help',
|
||||
defaultValue: 'default',
|
||||
options: [for (final p in kPermissionModes) SettingsOption(value: p.value, label: p.displayName)],
|
||||
),
|
||||
@@ -183,6 +205,8 @@ class ClaudeExtension extends ClideExtension {
|
||||
id: 'claude.agent.show',
|
||||
command: 'claude.agent.show',
|
||||
title: 'Claude: show an agent session pane',
|
||||
titleKey: 'command.agent.show',
|
||||
i18nNamespace: id,
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return _userErr('missing session id');
|
||||
@@ -194,6 +218,8 @@ class ClaudeExtension extends ClideExtension {
|
||||
id: 'claude.agent.hide',
|
||||
command: 'claude.agent.hide',
|
||||
title: 'Claude: hide an agent session pane',
|
||||
titleKey: 'command.agent.hide',
|
||||
i18nNamespace: id,
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return _userErr('missing session id');
|
||||
@@ -205,6 +231,8 @@ class ClaudeExtension extends ClideExtension {
|
||||
id: 'claude.agent.close',
|
||||
command: 'claude.agent.close',
|
||||
title: 'Claude: close (kill) an agent session',
|
||||
titleKey: 'command.agent.close',
|
||||
i18nNamespace: id,
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return _userErr('missing session id');
|
||||
@@ -216,6 +244,8 @@ class ClaudeExtension extends ClideExtension {
|
||||
id: 'claude.agent.mute',
|
||||
command: 'claude.agent.mute',
|
||||
title: 'Claude: mute broker delivery to an agent session',
|
||||
titleKey: 'command.agent.mute',
|
||||
i18nNamespace: id,
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return _userErr('missing session id');
|
||||
@@ -227,6 +257,8 @@ class ClaudeExtension extends ClideExtension {
|
||||
id: 'claude.agent.unmute',
|
||||
command: 'claude.agent.unmute',
|
||||
title: 'Claude: unmute broker delivery to an agent session',
|
||||
titleKey: 'command.agent.unmute',
|
||||
i18nNamespace: id,
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return _userErr('missing session id');
|
||||
@@ -239,6 +271,8 @@ class ClaudeExtension extends ClideExtension {
|
||||
id: 'claude.agent.inject-message',
|
||||
command: 'claude.agent.inject-message',
|
||||
title: 'Claude: inject a text turn into an agent session',
|
||||
titleKey: 'command.agent.injectMessage',
|
||||
i18nNamespace: id,
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return _userErr('missing session id');
|
||||
@@ -257,6 +291,8 @@ class ClaudeExtension extends ClideExtension {
|
||||
id: 'claude.agent.set-permission-mode',
|
||||
command: 'claude.agent.set-permission-mode',
|
||||
title: 'Claude: set permission mode for an agent session',
|
||||
titleKey: 'command.agent.setPermissionMode',
|
||||
i18nNamespace: id,
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return _userErr('missing session id');
|
||||
@@ -276,6 +312,8 @@ class ClaudeExtension extends ClideExtension {
|
||||
id: 'claude.mode.cycle',
|
||||
command: 'claude.mode.cycle',
|
||||
title: 'Claude: Cycle permission mode',
|
||||
titleKey: 'command.mode.cycle',
|
||||
i18nNamespace: id,
|
||||
run: (_) async {
|
||||
final managed = _orchestrator?.byId('primary');
|
||||
if (managed == null) return _notFound('no primary session');
|
||||
@@ -289,6 +327,8 @@ class ClaudeExtension extends ClideExtension {
|
||||
id: 'claude.task.reassign',
|
||||
command: 'claude.task.reassign',
|
||||
title: 'Claude: reassign a shared task to an agent',
|
||||
titleKey: 'command.task.reassign',
|
||||
i18nNamespace: id,
|
||||
run: (args) async {
|
||||
if (args.length < 2) return _userErr('usage: <taskId> <sessionId>');
|
||||
final taskId = args[0];
|
||||
@@ -319,6 +359,8 @@ class ClaudeExtension extends ClideExtension {
|
||||
id: 'claude.team-chat.open',
|
||||
command: 'claude.team-chat.open',
|
||||
title: 'Claude: open the team chat pane',
|
||||
titleKey: 'command.teamChat.open',
|
||||
i18nNamespace: id,
|
||||
run: (args) async {
|
||||
_ctx?.panels.activateTab(Slots.workspace, 'claude.team-chat');
|
||||
return IpcResponse.ok(id: '', data: const {'status': 'opened'});
|
||||
@@ -331,6 +373,8 @@ class ClaudeExtension extends ClideExtension {
|
||||
id: 'claude.team-chat.post',
|
||||
command: 'claude.team-chat.post',
|
||||
title: 'Claude: post a message into the team channel as the user',
|
||||
titleKey: 'command.teamChat.post',
|
||||
i18nNamespace: id,
|
||||
run: (args) async {
|
||||
if (args.isEmpty) return _userErr('usage: [@name] <text>');
|
||||
final raw = args.join(' ');
|
||||
@@ -357,6 +401,8 @@ class ClaudeExtension extends ClideExtension {
|
||||
id: 'claude.agent.fork',
|
||||
command: 'claude.agent.fork',
|
||||
title: 'Claude: fork a managed session into a new branch session',
|
||||
titleKey: 'command.agent.fork',
|
||||
i18nNamespace: id,
|
||||
run: (args) async {
|
||||
final sourceId = args.firstOrNull;
|
||||
if (sourceId == null) {
|
||||
|
||||
@@ -49,7 +49,13 @@ class ImageThumbnail extends StatelessWidget {
|
||||
final t = ClideSettings.theme.of(context).surface;
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: 'Image $_fileName',
|
||||
label: ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'image.semantics',
|
||||
namespace: 'builtin.claude',
|
||||
placeholder: 'Image $_fileName',
|
||||
replacers: [I18nReplacer(from: '{name}', replace: _fileName)],
|
||||
),
|
||||
excludeSemantics: true,
|
||||
child: ClideTappable(
|
||||
onTap: () => openImageLightbox(context, path),
|
||||
|
||||
@@ -49,21 +49,37 @@ class ActivityTabView extends StatelessWidget {
|
||||
final latest = stats.latest;
|
||||
final u = usage;
|
||||
final sections = <MetaSection>[
|
||||
..._workflowSection(tokens),
|
||||
..._workflowSection(context, tokens),
|
||||
if (u != null)
|
||||
MetaSection('USAGE', [
|
||||
if (u.session != null) MetaRow('session', u.session!),
|
||||
if (u.week != null) MetaRow('week (all)', u.week!),
|
||||
if (u.weekSonnet != null) MetaRow('week (sonnet)', u.weekSonnet!),
|
||||
MetaSection(ClideSettings.i18n.string(context, 'activity.section.usage', namespace: 'builtin.claude', placeholder: 'USAGE'), [
|
||||
if (u.session != null)
|
||||
MetaRow(ClideSettings.i18n.string(context, 'activity.row.session', namespace: 'builtin.claude', placeholder: 'session'), u.session!),
|
||||
if (u.week != null)
|
||||
MetaRow(ClideSettings.i18n.string(context, 'activity.row.weekAll', namespace: 'builtin.claude', placeholder: 'week (all)'), u.week!),
|
||||
if (u.weekSonnet != null)
|
||||
MetaRow(ClideSettings.i18n.string(context, 'activity.row.weekSonnet', namespace: 'builtin.claude', placeholder: 'week (sonnet)'), u.weekSonnet!),
|
||||
]),
|
||||
if (latest != null)
|
||||
MetaSection('TODAY', [
|
||||
MetaRow('messages', '${latest.messageCount}'),
|
||||
MetaRow('sessions', '${latest.sessionCount}'),
|
||||
MetaRow('tool calls', '${latest.toolCallCount}'),
|
||||
MetaSection(ClideSettings.i18n.string(context, 'activity.section.today', namespace: 'builtin.claude', placeholder: 'TODAY'), [
|
||||
MetaRow(ClideSettings.i18n.string(context, 'activity.row.messages', namespace: 'builtin.claude', placeholder: 'messages'), '${latest.messageCount}'),
|
||||
MetaRow(ClideSettings.i18n.string(context, 'activity.row.sessions', namespace: 'builtin.claude', placeholder: 'sessions'), '${latest.sessionCount}'),
|
||||
MetaRow(
|
||||
ClideSettings.i18n.string(context, 'activity.row.toolCalls', namespace: 'builtin.claude', placeholder: 'tool calls'),
|
||||
'${latest.toolCallCount}',
|
||||
),
|
||||
]),
|
||||
if (latest != null) MetaSection('LIFETIME', [MetaRow('messages', '${stats.lifetimeMessages}'), MetaRow('sessions', '${stats.lifetimeSessions}')]),
|
||||
..._runtimeSection(tokens),
|
||||
if (latest != null)
|
||||
MetaSection(ClideSettings.i18n.string(context, 'activity.section.lifetime', namespace: 'builtin.claude', placeholder: 'LIFETIME'), [
|
||||
MetaRow(
|
||||
ClideSettings.i18n.string(context, 'activity.row.messages', namespace: 'builtin.claude', placeholder: 'messages'),
|
||||
'${stats.lifetimeMessages}',
|
||||
),
|
||||
MetaRow(
|
||||
ClideSettings.i18n.string(context, 'activity.row.sessions', namespace: 'builtin.claude', placeholder: 'sessions'),
|
||||
'${stats.lifetimeSessions}',
|
||||
),
|
||||
]),
|
||||
..._runtimeSection(context, tokens),
|
||||
];
|
||||
|
||||
return ListView(
|
||||
@@ -73,20 +89,57 @@ class ActivityTabView extends StatelessWidget {
|
||||
// the builtin.claude/command bus — identical to typing the command.
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: ClideText('SESSION', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader),
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(context, 'activity.section.session', namespace: 'builtin.claude', placeholder: 'SESSION'),
|
||||
fontSize: clideFontSmall,
|
||||
color: tokens.sidebarSectionHeader,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
_control(context, tokens, 'clear', 'trash', '/clear'),
|
||||
_control(context, tokens, 'compact', 'arrows-in-simple', '/compact'),
|
||||
_control(context, tokens, 'fork', 'git-branch', '/fork'),
|
||||
_control(context, tokens, 'resume', 'clock-counter-clockwise', '/resume'),
|
||||
_control(
|
||||
context,
|
||||
tokens,
|
||||
ClideSettings.i18n.string(context, 'activity.control.clear', namespace: 'builtin.claude', placeholder: 'clear'),
|
||||
'trash',
|
||||
'/clear',
|
||||
),
|
||||
_control(
|
||||
context,
|
||||
tokens,
|
||||
ClideSettings.i18n.string(context, 'activity.control.compact', namespace: 'builtin.claude', placeholder: 'compact'),
|
||||
'arrows-in-simple',
|
||||
'/compact',
|
||||
),
|
||||
_control(
|
||||
context,
|
||||
tokens,
|
||||
ClideSettings.i18n.string(context, 'activity.control.fork', namespace: 'builtin.claude', placeholder: 'fork'),
|
||||
'git-branch',
|
||||
'/fork',
|
||||
),
|
||||
_control(
|
||||
context,
|
||||
tokens,
|
||||
ClideSettings.i18n.string(context, 'activity.control.resume', namespace: 'builtin.claude', placeholder: 'resume'),
|
||||
'clock-counter-clockwise',
|
||||
'/resume',
|
||||
),
|
||||
const Spacer(),
|
||||
_control(context, tokens, 'refresh usage', 'arrow-clockwise', '/usage'),
|
||||
_control(
|
||||
context,
|
||||
tokens,
|
||||
ClideSettings.i18n.string(context, 'activity.control.refreshUsage', namespace: 'builtin.claude', placeholder: 'refresh usage'),
|
||||
'arrow-clockwise',
|
||||
'/usage',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
if (sections.isEmpty) metaPlaceholder('No activity recorded yet.') else ...metaTableChildren(tokens, sections),
|
||||
if (sections.isEmpty)
|
||||
metaPlaceholder(ClideSettings.i18n.string(context, 'activity.empty', namespace: 'builtin.claude', placeholder: 'No activity recorded yet.'))
|
||||
else
|
||||
...metaTableChildren(tokens, sections),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -94,11 +147,26 @@ class ActivityTabView extends StatelessWidget {
|
||||
Widget _control(BuildContext context, SurfaceTokens tokens, String label, String glyph, String command) {
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: '$label session',
|
||||
label: ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'activity.control.semantics',
|
||||
namespace: 'builtin.claude',
|
||||
placeholder: '$label session',
|
||||
replacers: [I18nReplacer(from: '{label}', replace: label)],
|
||||
),
|
||||
excludeSemantics: true,
|
||||
onTap: () => _command(context, command),
|
||||
child: ClideTappable(
|
||||
tooltip: '$label · $command',
|
||||
tooltip: ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'activity.control.tooltip',
|
||||
namespace: 'builtin.claude',
|
||||
placeholder: '$label · $command',
|
||||
replacers: [
|
||||
I18nReplacer(from: '{label}', replace: label),
|
||||
I18nReplacer(from: '{command}', replace: command),
|
||||
],
|
||||
),
|
||||
onTap: () => _command(context, command),
|
||||
builder: (ctx, hovered, _) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
@@ -111,31 +179,50 @@ class ActivityTabView extends StatelessWidget {
|
||||
/// An aggregate WORKFLOWS section while one or more workflow runs exist this
|
||||
/// session (T-416): a row per run — its name and `done/total agents`, tinted
|
||||
/// focus while running and success once complete.
|
||||
List<MetaSection> _workflowSection(SurfaceTokens tokens) {
|
||||
List<MetaSection> _workflowSection(BuildContext context, SurfaceTokens tokens) {
|
||||
final runs = workflows.values.toList();
|
||||
if (runs.isEmpty) return const [];
|
||||
final done = ClideSettings.i18n.string(context, 'activity.workflow.done', namespace: 'builtin.claude', placeholder: 'done');
|
||||
final starting = ClideSettings.i18n.string(context, 'activity.workflow.starting', namespace: 'builtin.claude', placeholder: 'starting');
|
||||
final fallback = ClideSettings.i18n.string(context, 'activity.workflow.fallback', namespace: 'builtin.claude', placeholder: 'workflow');
|
||||
return [
|
||||
MetaSection('WORKFLOWS', [
|
||||
MetaSection(ClideSettings.i18n.string(context, 'activity.section.workflows', namespace: 'builtin.claude', placeholder: 'WORKFLOWS'), [
|
||||
for (final r in runs)
|
||||
MetaRow(
|
||||
r.name ?? r.taskId ?? 'workflow',
|
||||
r.agentCount == 0 ? (r.done ? 'done' : 'starting') : '${r.doneCount}/${r.agentCount} agents${r.done ? ' ✓' : ''}',
|
||||
r.name ?? r.taskId ?? fallback,
|
||||
r.agentCount == 0 ? (r.done ? done : starting) : '${r.doneCount}/${r.agentCount} agents${r.done ? ' ✓' : ''}',
|
||||
valueColor: r.done ? tokens.statusSuccess : tokens.globalFocus,
|
||||
),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
List<MetaSection> _runtimeSection(SurfaceTokens tokens) {
|
||||
List<MetaSection> _runtimeSection(BuildContext context, SurfaceTokens tokens) {
|
||||
final st = primaryStatus;
|
||||
final skills = config?.skills.length;
|
||||
final rows = <MetaRow>[
|
||||
if (st?.model != null) MetaRow('model', shortModelLabel(st!.model!), valueColor: tokens.globalFocus),
|
||||
if (st?.effort != null) MetaRow('effort', st!.effort!),
|
||||
if (st?.contextTokens != null) MetaRow('context', '${formatTokenCount(st!.contextTokens!)} ctx'),
|
||||
if (st?.permissionMode != null) MetaRow('mode', permissionModeLabel(st!.permissionMode!)),
|
||||
if (skills != null) MetaRow('skills', '$skills'),
|
||||
if (st?.model != null)
|
||||
MetaRow(
|
||||
ClideSettings.i18n.string(context, 'activity.row.model', namespace: 'builtin.claude', placeholder: 'model'),
|
||||
shortModelLabel(st!.model!),
|
||||
valueColor: tokens.globalFocus,
|
||||
),
|
||||
if (st?.effort != null)
|
||||
MetaRow(ClideSettings.i18n.string(context, 'activity.row.effort', namespace: 'builtin.claude', placeholder: 'effort'), st!.effort!),
|
||||
if (st?.contextTokens != null)
|
||||
MetaRow(
|
||||
ClideSettings.i18n.string(context, 'activity.row.context', namespace: 'builtin.claude', placeholder: 'context'),
|
||||
'${formatTokenCount(st!.contextTokens!)} ctx',
|
||||
),
|
||||
if (st?.permissionMode != null)
|
||||
MetaRow(
|
||||
ClideSettings.i18n.string(context, 'activity.row.mode', namespace: 'builtin.claude', placeholder: 'mode'),
|
||||
permissionModeLabel(st!.permissionMode!),
|
||||
),
|
||||
if (skills != null) MetaRow(ClideSettings.i18n.string(context, 'activity.row.skills', namespace: 'builtin.claude', placeholder: 'skills'), '$skills'),
|
||||
];
|
||||
return rows.isEmpty ? const [] : [MetaSection('RUNTIME · primary', rows)];
|
||||
return rows.isEmpty
|
||||
? const []
|
||||
: [MetaSection(ClideSettings.i18n.string(context, 'activity.section.runtime', namespace: 'builtin.claude', placeholder: 'RUNTIME · primary'), rows)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ class ConfigTabView extends StatelessWidget {
|
||||
final tokens = ClideSettings.theme.of(context).surface;
|
||||
final cfg = config;
|
||||
if (cfg == null) {
|
||||
return metaPlaceholder('Claude environment not loaded.');
|
||||
return metaPlaceholder(ClideSettings.i18n.string(context, 'config.empty', namespace: 'builtin.claude', placeholder: 'Claude environment not loaded.'));
|
||||
}
|
||||
final settings = cfg.settings;
|
||||
final model = status?.model ?? cfg.probe?.model ?? settings['model']?.toString() ?? 'default';
|
||||
@@ -56,26 +56,40 @@ class ConfigTabView extends StatelessWidget {
|
||||
// Pinned SETTINGS control panel — not collapsible.
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: ClideText('SETTINGS', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader),
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(context, 'config.section.settings', namespace: 'builtin.claude', placeholder: 'SETTINGS'),
|
||||
fontSize: clideFontSmall,
|
||||
color: tokens.sidebarSectionHeader,
|
||||
),
|
||||
),
|
||||
SettingControlRow(
|
||||
label: 'model',
|
||||
label: ClideSettings.i18n.string(context, 'config.row.model', namespace: 'builtin.claude', placeholder: 'model'),
|
||||
value: model,
|
||||
valueColor: tokens.globalFocus,
|
||||
options: (models == null || models!.isEmpty) ? kFallbackModels : models!,
|
||||
isActive: (o) => o.value == model || model.toLowerCase().contains(o.value.toLowerCase()),
|
||||
command: 'model',
|
||||
),
|
||||
SettingControlRow(label: 'effort', value: effort, options: kEffortLevels, isActive: (o) => o.value == effort, command: 'effort'),
|
||||
SettingControlRow(
|
||||
label: 'permission mode',
|
||||
label: ClideSettings.i18n.string(context, 'config.row.effort', namespace: 'builtin.claude', placeholder: 'effort'),
|
||||
value: effort,
|
||||
options: kEffortLevels,
|
||||
isActive: (o) => o.value == effort,
|
||||
command: 'effort',
|
||||
),
|
||||
SettingControlRow(
|
||||
label: ClideSettings.i18n.string(context, 'config.row.permissionMode', namespace: 'builtin.claude', placeholder: 'permission mode'),
|
||||
value: permissionModeLabel(mode),
|
||||
options: kPermissionModes,
|
||||
isActive: (o) => o.value == mode,
|
||||
command: 'permissions',
|
||||
),
|
||||
_configRow(tokens, 'output style', outputStyle),
|
||||
_configRow(tokens, 'source', '~/.claude + .claude'),
|
||||
_configRow(tokens, ClideSettings.i18n.string(context, 'config.row.outputStyle', namespace: 'builtin.claude', placeholder: 'output style'), outputStyle),
|
||||
_configRow(
|
||||
tokens,
|
||||
ClideSettings.i18n.string(context, 'config.row.source', namespace: 'builtin.claude', placeholder: 'source'),
|
||||
ClideSettings.i18n.string(context, 'config.row.source.value', namespace: 'builtin.claude', placeholder: '~/.claude + .claude'),
|
||||
),
|
||||
|
||||
// ---- Accordion sections ----
|
||||
for (final section in ConfigSection.values) _accordion(context, tokens, cfg, section),
|
||||
@@ -83,7 +97,16 @@ class ConfigTabView extends StatelessWidget {
|
||||
// Footer hint.
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: ClideText('expand a list to see all · click a skill/agent/command → opens its .md', muted: true, fontSize: clideFontSmall),
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(
|
||||
context,
|
||||
'config.footer',
|
||||
namespace: 'builtin.claude',
|
||||
placeholder: 'expand a list to see all · click a skill/agent/command → opens its .md',
|
||||
),
|
||||
muted: true,
|
||||
fontSize: clideFontSmall,
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -109,13 +132,13 @@ class ConfigTabView extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
String _sectionLabel(ConfigSection section) => switch (section) {
|
||||
ConfigSection.skills => 'SKILLS',
|
||||
ConfigSection.agents => 'AGENTS',
|
||||
ConfigSection.commands => 'COMMANDS',
|
||||
ConfigSection.hooks => 'HOOKS',
|
||||
ConfigSection.permissions => 'PERMISSIONS',
|
||||
ConfigSection.mcpServers => 'MCP SERVERS',
|
||||
String _sectionLabel(BuildContext context, ConfigSection section) => switch (section) {
|
||||
ConfigSection.skills => ClideSettings.i18n.string(context, 'config.section.skills', namespace: 'builtin.claude', placeholder: 'SKILLS'),
|
||||
ConfigSection.agents => ClideSettings.i18n.string(context, 'config.section.agents', namespace: 'builtin.claude', placeholder: 'AGENTS'),
|
||||
ConfigSection.commands => ClideSettings.i18n.string(context, 'config.section.commands', namespace: 'builtin.claude', placeholder: 'COMMANDS'),
|
||||
ConfigSection.hooks => ClideSettings.i18n.string(context, 'config.section.hooks', namespace: 'builtin.claude', placeholder: 'HOOKS'),
|
||||
ConfigSection.permissions => ClideSettings.i18n.string(context, 'config.section.permissions', namespace: 'builtin.claude', placeholder: 'PERMISSIONS'),
|
||||
ConfigSection.mcpServers => ClideSettings.i18n.string(context, 'config.section.mcpServers', namespace: 'builtin.claude', placeholder: 'MCP SERVERS'),
|
||||
};
|
||||
|
||||
int _sectionCount(ClaudeConfig config, ConfigSection section) => switch (section) {
|
||||
@@ -131,7 +154,7 @@ class ConfigTabView extends StatelessWidget {
|
||||
final isExpanded = expanded.contains(section);
|
||||
final children = isExpanded ? _sectionChildren(context, tokens, config, section) : const <Widget>[];
|
||||
return ClideAccordion(
|
||||
label: _sectionLabel(section),
|
||||
label: _sectionLabel(context, section),
|
||||
count: _sectionCount(config, section),
|
||||
expanded: isExpanded,
|
||||
onToggle: () => onToggleSection(section),
|
||||
@@ -166,7 +189,7 @@ class ConfigTabView extends StatelessWidget {
|
||||
),
|
||||
];
|
||||
case ConfigSection.permissions:
|
||||
return _permissionRows(tokens, config.permissions);
|
||||
return _permissionRows(context, tokens, config.permissions);
|
||||
case ConfigSection.mcpServers:
|
||||
return [
|
||||
for (final srv in config.mcpServers)
|
||||
@@ -205,7 +228,7 @@ class ConfigTabView extends StatelessWidget {
|
||||
}
|
||||
|
||||
/// Renders grouped allow/ask/deny permission rows, colour-coded by kind.
|
||||
List<Widget> _permissionRows(SurfaceTokens tokens, ClaudePermissions perms) {
|
||||
List<Widget> _permissionRows(BuildContext context, SurfaceTokens tokens, ClaudePermissions perms) {
|
||||
// allow → statusSuccess, ask → statusWarning, deny → statusError
|
||||
Color kindColor(ConfigPermKind k) => switch (k) {
|
||||
ConfigPermKind.allow => tokens.statusSuccess,
|
||||
@@ -214,9 +237,9 @@ class ConfigTabView extends StatelessWidget {
|
||||
};
|
||||
|
||||
String kindLabel(ConfigPermKind k) => switch (k) {
|
||||
ConfigPermKind.allow => 'allow',
|
||||
ConfigPermKind.ask => 'ask',
|
||||
ConfigPermKind.deny => 'deny',
|
||||
ConfigPermKind.allow => ClideSettings.i18n.string(context, 'config.perm.allow', namespace: 'builtin.claude', placeholder: 'allow'),
|
||||
ConfigPermKind.ask => ClideSettings.i18n.string(context, 'config.perm.ask', namespace: 'builtin.claude', placeholder: 'ask'),
|
||||
ConfigPermKind.deny => ClideSettings.i18n.string(context, 'config.perm.deny', namespace: 'builtin.claude', placeholder: 'deny'),
|
||||
};
|
||||
|
||||
final groups = [(ConfigPermKind.allow, perms.allow), (ConfigPermKind.ask, perms.ask), (ConfigPermKind.deny, perms.deny)];
|
||||
@@ -320,18 +343,42 @@ class _SettingControlRowState extends State<SettingControlRow> {
|
||||
ClideMenuItem(
|
||||
label: o.description.isEmpty ? o.displayName : '${o.displayName} — ${o.description}',
|
||||
active: widget.isActive(o),
|
||||
semanticLabel: '${widget.label}: ${o.displayName}',
|
||||
semanticLabel: ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'config.control.option.semantics',
|
||||
namespace: 'builtin.claude',
|
||||
placeholder: '${widget.label}: ${o.displayName}',
|
||||
replacers: [
|
||||
I18nReplacer(from: '{label}', replace: widget.label),
|
||||
I18nReplacer(from: '{name}', replace: o.displayName),
|
||||
],
|
||||
),
|
||||
onSelect: () => _pick(o.value),
|
||||
),
|
||||
],
|
||||
),
|
||||
anchor: Semantics(
|
||||
button: true,
|
||||
label: '${widget.label}: ${widget.value}. Click to change.',
|
||||
label: ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'config.control.semantics',
|
||||
namespace: 'builtin.claude',
|
||||
placeholder: '${widget.label}: ${widget.value}. Click to change.',
|
||||
replacers: [
|
||||
I18nReplacer(from: '{label}', replace: widget.label),
|
||||
I18nReplacer(from: '{value}', replace: widget.value),
|
||||
],
|
||||
),
|
||||
excludeSemantics: true,
|
||||
onTap: _overlay.toggle,
|
||||
child: ClideTappable(
|
||||
tooltip: 'change ${widget.label}',
|
||||
tooltip: ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'config.control.tooltip',
|
||||
namespace: 'builtin.claude',
|
||||
placeholder: 'change ${widget.label}',
|
||||
replacers: [I18nReplacer(from: '{label}', replace: widget.label)],
|
||||
),
|
||||
onTap: _overlay.toggle,
|
||||
builder: (ctx, hovered, _) => DecoratedBox(
|
||||
decoration: BoxDecoration(color: hovered ? tokens.listItemHoverBackground : null, borderRadius: BorderRadius.circular(4)),
|
||||
|
||||
@@ -40,15 +40,27 @@ class PermissionModeBadge extends StatelessWidget {
|
||||
final isBypass = mode == 'bypassPermissions';
|
||||
final badgeColor = isBypass ? const Color(0xFFF06C6F) : tokens.globalFocus;
|
||||
|
||||
final tooltip =
|
||||
'Permission mode: ${permissionModeLabel(mode)}. '
|
||||
'Click to cycle default/acceptEdits/plan; Shift-click for bypassPermissions.';
|
||||
final tooltip = ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'permissionBadge.tooltip',
|
||||
namespace: 'builtin.claude',
|
||||
placeholder:
|
||||
'Permission mode: ${permissionModeLabel(mode)}. '
|
||||
'Click to cycle default/acceptEdits/plan; Shift-click for bypassPermissions.',
|
||||
replacers: [I18nReplacer(from: '{mode}', replace: permissionModeLabel(mode))],
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 3),
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: 'Permission mode: $label',
|
||||
label: ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'permissionBadge.semantics',
|
||||
namespace: 'builtin.claude',
|
||||
placeholder: 'Permission mode: $label',
|
||||
replacers: [I18nReplacer(from: '{label}', replace: label)],
|
||||
),
|
||||
excludeSemantics: true,
|
||||
onTap: () {
|
||||
if (HardwareKeyboard.instance.isShiftPressed) {
|
||||
|
||||
@@ -154,27 +154,40 @@ class _AgentRosterRowState extends State<AgentRosterRow> {
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ClideText('Enable bypassPermissions? All tool calls will be auto-allowed.', fontSize: clideFontSmall, color: tokens.globalTextMuted),
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(
|
||||
context,
|
||||
'roster.bypass.confirmBody',
|
||||
namespace: 'builtin.claude',
|
||||
placeholder: 'Enable bypassPermissions? All tool calls will be auto-allowed.',
|
||||
),
|
||||
fontSize: clideFontSmall,
|
||||
color: tokens.globalTextMuted,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
// Confirm
|
||||
Semantics(
|
||||
button: true,
|
||||
label: 'Confirm bypass',
|
||||
label: ClideSettings.i18n.string(context, 'roster.bypass.confirm.semantics', namespace: 'builtin.claude', placeholder: 'Confirm bypass'),
|
||||
excludeSemantics: true,
|
||||
onTap: () {
|
||||
setState(() => _confirmingBypass = false);
|
||||
widget.onSetPermissionMode(widget.member.name, 'bypassPermissions');
|
||||
},
|
||||
child: ClideTappable(
|
||||
tooltip: 'Confirm',
|
||||
tooltip: ClideSettings.i18n.string(context, 'roster.bypass.confirm.tooltip', namespace: 'builtin.claude', placeholder: 'Confirm'),
|
||||
onTap: () {
|
||||
setState(() => _confirmingBypass = false);
|
||||
widget.onSetPermissionMode(widget.member.name, 'bypassPermissions');
|
||||
},
|
||||
builder: (ctx, hovered, _) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 2),
|
||||
child: ClideText('OK', fontSize: clideFontSmall, color: hovered ? tokens.globalForeground : tokens.globalFocus),
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(context, 'roster.bypass.ok', namespace: 'builtin.claude', placeholder: 'OK'),
|
||||
fontSize: clideFontSmall,
|
||||
color: hovered ? tokens.globalForeground : tokens.globalFocus,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -182,15 +195,19 @@ class _AgentRosterRowState extends State<AgentRosterRow> {
|
||||
// Cancel
|
||||
Semantics(
|
||||
button: true,
|
||||
label: 'Cancel bypass',
|
||||
label: ClideSettings.i18n.string(context, 'roster.bypass.cancel.semantics', namespace: 'builtin.claude', placeholder: 'Cancel bypass'),
|
||||
excludeSemantics: true,
|
||||
onTap: () => setState(() => _confirmingBypass = false),
|
||||
child: ClideTappable(
|
||||
tooltip: 'Cancel',
|
||||
tooltip: ClideSettings.i18n.string(context, 'roster.bypass.cancel.tooltip', namespace: 'builtin.claude', placeholder: 'Cancel'),
|
||||
onTap: () => setState(() => _confirmingBypass = false),
|
||||
builder: (ctx, hovered, _) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 2),
|
||||
child: ClideText('Cancel', fontSize: clideFontSmall, color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(context, 'roster.bypass.cancel', namespace: 'builtin.claude', placeholder: 'Cancel'),
|
||||
fontSize: clideFontSmall,
|
||||
color: hovered ? tokens.globalForeground : tokens.globalTextMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -206,7 +223,9 @@ class _AgentRosterRowState extends State<AgentRosterRow> {
|
||||
// Show / hide
|
||||
MetaIconButton(
|
||||
painter: isVisible ? PhosphorIcons.byName('eye') : PhosphorIcons.byName('eye-slash'),
|
||||
tooltip: isVisible ? 'Hide pane' : 'Show pane',
|
||||
tooltip: isVisible
|
||||
? ClideSettings.i18n.string(context, 'roster.hidePane', namespace: 'builtin.claude', placeholder: 'Hide pane')
|
||||
: ClideSettings.i18n.string(context, 'roster.showPane', namespace: 'builtin.claude', placeholder: 'Show pane'),
|
||||
color: tokens.globalTextMuted,
|
||||
onTap: () => isVisible ? widget.orchestrator!.hide(managed.id) : widget.orchestrator!.show(managed.id),
|
||||
),
|
||||
@@ -216,28 +235,30 @@ class _AgentRosterRowState extends State<AgentRosterRow> {
|
||||
// NOTE: We use eye/eyeSlash as stand-ins until a dedicated speaker
|
||||
// icon is added to PhosphorIcons (no speaker codepoint yet).
|
||||
// The semantic tooltip still says mute/unmute so AT users are clear.
|
||||
tooltip: isMuted ? 'Unmute messages' : 'Mute messages',
|
||||
tooltip: isMuted
|
||||
? ClideSettings.i18n.string(context, 'roster.unmute', namespace: 'builtin.claude', placeholder: 'Unmute messages')
|
||||
: ClideSettings.i18n.string(context, 'roster.mute', namespace: 'builtin.claude', placeholder: 'Mute messages'),
|
||||
color: isMuted ? tokens.globalFocus : tokens.globalTextMuted,
|
||||
onTap: () => isMuted ? widget.orchestrator!.unmute(managed.id) : widget.orchestrator!.mute(managed.id),
|
||||
),
|
||||
// Inject message
|
||||
MetaIconButton(
|
||||
painter: PhosphorIcons.byName('chat-circle'),
|
||||
tooltip: 'Inject message',
|
||||
tooltip: ClideSettings.i18n.string(context, 'roster.inject', namespace: 'builtin.claude', placeholder: 'Inject message'),
|
||||
color: isInjecting ? tokens.globalFocus : tokens.globalTextMuted,
|
||||
onTap: () => widget.onToggleInject(widget.member.name),
|
||||
),
|
||||
// Fork session (T-172): branch into a new pane without touching the original.
|
||||
MetaIconButton(
|
||||
painter: PhosphorIcons.byName('git-branch'),
|
||||
tooltip: 'Fork session',
|
||||
tooltip: ClideSettings.i18n.string(context, 'roster.fork', namespace: 'builtin.claude', placeholder: 'Fork session'),
|
||||
color: tokens.globalTextMuted,
|
||||
onTap: () => widget.onFork(widget.member.name),
|
||||
),
|
||||
// Close session
|
||||
MetaIconButton(
|
||||
painter: PhosphorIcons.byName('x'),
|
||||
tooltip: 'Close session',
|
||||
tooltip: ClideSettings.i18n.string(context, 'roster.close', namespace: 'builtin.claude', placeholder: 'Close session'),
|
||||
color: tokens.globalTextMuted,
|
||||
onTap: () => widget.onClose(widget.member.name),
|
||||
),
|
||||
@@ -262,7 +283,7 @@ class _AgentRosterRowState extends State<AgentRosterRow> {
|
||||
const SizedBox(width: 4),
|
||||
MetaIconButton(
|
||||
painter: PhosphorIcons.byName('x'),
|
||||
tooltip: 'Cancel',
|
||||
tooltip: ClideSettings.i18n.string(context, 'roster.inject.cancel', namespace: 'builtin.claude', placeholder: 'Cancel'),
|
||||
color: tokens.globalTextMuted,
|
||||
onTap: () => widget.onToggleInject(widget.member.name),
|
||||
),
|
||||
|
||||
@@ -29,7 +29,7 @@ class SidebarTabStrip extends StatelessWidget {
|
||||
child: Semantics(
|
||||
button: true,
|
||||
selected: t == current,
|
||||
label: _label(t),
|
||||
label: _label(context, t),
|
||||
excludeSemantics: true,
|
||||
onTap: () => onPick(t),
|
||||
child: ClideTappable(
|
||||
@@ -39,7 +39,11 @@ class SidebarTabStrip extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: t == current ? tokens.globalFocus : const Color(0x00000000), width: 2)),
|
||||
),
|
||||
child: ClideText(_label(t), fontSize: clideFontSmall, color: t == current || hovered ? tokens.globalForeground : tokens.globalTextMuted),
|
||||
child: ClideText(
|
||||
_label(context, t),
|
||||
fontSize: clideFontSmall,
|
||||
color: t == current || hovered ? tokens.globalForeground : tokens.globalTextMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -49,9 +53,18 @@ class SidebarTabStrip extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
String _label(SidebarTab t) => switch (t) {
|
||||
SidebarTab.activity => 'Activity',
|
||||
SidebarTab.team => memberCount == 0 ? 'Team' : 'Team · $memberCount',
|
||||
SidebarTab.config => 'Config',
|
||||
String _label(BuildContext context, SidebarTab t) => switch (t) {
|
||||
SidebarTab.activity => ClideSettings.i18n.string(context, 'tabStrip.activity', namespace: 'builtin.claude', placeholder: 'Activity'),
|
||||
SidebarTab.team =>
|
||||
memberCount == 0
|
||||
? ClideSettings.i18n.string(context, 'tabStrip.team', namespace: 'builtin.claude', placeholder: 'Team')
|
||||
: ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'tabStrip.team.count',
|
||||
namespace: 'builtin.claude',
|
||||
placeholder: 'Team · $memberCount',
|
||||
replacers: [I18nReplacer(from: '{count}', replace: '$memberCount')],
|
||||
),
|
||||
SidebarTab.config => ClideSettings.i18n.string(context, 'tabStrip.config', namespace: 'builtin.claude', placeholder: 'Config'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class TaskRow extends StatelessWidget {
|
||||
if (broker != null && broker!.members.length > 1)
|
||||
MetaIconButton(
|
||||
painter: PhosphorIcons.byName('arrow-clockwise'),
|
||||
tooltip: 'Reassign task',
|
||||
tooltip: ClideSettings.i18n.string(context, 'taskRow.reassign', namespace: 'builtin.claude', placeholder: 'Reassign task'),
|
||||
color: tokens.globalTextMuted,
|
||||
onTap: () => _reassign(context),
|
||||
),
|
||||
|
||||
@@ -50,7 +50,7 @@ class TeamTabView extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideSettings.theme.of(context).surface;
|
||||
if (members.isEmpty) {
|
||||
return metaPlaceholder('No team active.');
|
||||
return metaPlaceholder(ClideSettings.i18n.string(context, 'team.empty', namespace: 'builtin.claude', placeholder: 'No team active.'));
|
||||
}
|
||||
final children = <Widget>[
|
||||
for (final m in members)
|
||||
@@ -71,7 +71,7 @@ class TeamTabView extends StatelessWidget {
|
||||
|
||||
if (tasks.isNotEmpty) {
|
||||
children.add(const SizedBox(height: 12));
|
||||
children.add(_taskSection(tokens));
|
||||
children.add(_taskSection(context, tokens));
|
||||
}
|
||||
|
||||
// MESSAGES section (T-180): live broker chat feed + quick-post composer.
|
||||
@@ -85,11 +85,15 @@ class TeamTabView extends StatelessWidget {
|
||||
return ListView(padding: const EdgeInsets.all(12), children: children);
|
||||
}
|
||||
|
||||
Widget _taskSection(SurfaceTokens tokens) {
|
||||
Widget _taskSection(BuildContext context, SurfaceTokens tokens) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClideText('TASKS', fontSize: clideFontSmall, color: tokens.globalTextMuted),
|
||||
ClideText(
|
||||
ClideSettings.i18n.string(context, 'team.section.tasks', namespace: 'builtin.claude', placeholder: 'TASKS'),
|
||||
fontSize: clideFontSmall,
|
||||
color: tokens.globalTextMuted,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
for (final t in tasks) TaskRow(task: t, members: members, broker: orchestrator?.broker),
|
||||
],
|
||||
|
||||
@@ -162,7 +162,11 @@ class _ModelPickerCardState extends State<ModelPickerCard> {
|
||||
Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
ClideButton(label: 'cancel', variant: ClideButtonVariant.subtle, onPressed: widget.onCancel),
|
||||
ClideButton(
|
||||
label: ClideSettings.i18n.string(context, 'modelPicker.cancel', namespace: 'builtin.claude', placeholder: 'cancel'),
|
||||
variant: ClideButtonVariant.subtle,
|
||||
onPressed: widget.onCancel,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -101,11 +101,23 @@ class _PermissionModeControlState extends State<PermissionModeControl> {
|
||||
final open = _overlay.isOpen;
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: 'permission mode: ${permissionModeLabel(widget.mode)}. Activate to change.',
|
||||
label: ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'permissionControl.semantics',
|
||||
namespace: 'builtin.claude',
|
||||
placeholder: 'permission mode: ${permissionModeLabel(widget.mode)}. Activate to change.',
|
||||
replacers: [I18nReplacer(from: '{mode}', replace: permissionModeLabel(widget.mode))],
|
||||
),
|
||||
excludeSemantics: true,
|
||||
child: ClideTappable(
|
||||
onTap: _overlay.toggle,
|
||||
tooltip: 'Permission mode: ${permissionModeLabel(widget.mode)} — change (Ctrl/Cmd+M cycles)',
|
||||
tooltip: ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'permissionControl.tooltip',
|
||||
namespace: 'builtin.claude',
|
||||
placeholder: 'Permission mode: ${permissionModeLabel(widget.mode)} — change (Ctrl/Cmd+M cycles)',
|
||||
replacers: [I18nReplacer(from: '{mode}', replace: permissionModeLabel(widget.mode))],
|
||||
),
|
||||
builder: (ctx, hovered, _) => Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
|
||||
@@ -244,7 +244,13 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
|
||||
final canRemember = p.permissionSuggestions.isNotEmpty;
|
||||
return (
|
||||
tokens.statusWarning,
|
||||
'permission · ${p.displayName}',
|
||||
ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'prompt.permission.label',
|
||||
namespace: 'builtin.claude',
|
||||
placeholder: 'permission · ${p.displayName}',
|
||||
replacers: [I18nReplacer(from: '{name}', replace: p.displayName)],
|
||||
),
|
||||
[
|
||||
// Claude's description for Write/Edit is often just the file_path,
|
||||
// which the body already shows via _pathLine — suppress in that case
|
||||
@@ -265,25 +271,66 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
ClideButton(label: '1. Allow', variant: ClideButtonVariant.primary, onPressed: () => _permAllow()),
|
||||
if (canRemember) ClideButton(label: "2. Allow & don't ask again", onPressed: () => _permAllow(remember: true)),
|
||||
ClideButton(label: '${canRemember ? '3' : '2'}. Deny', onPressed: _permDeny),
|
||||
ClideButton(
|
||||
label: '${canRemember ? '4' : '3'}. Deny & simplify',
|
||||
tooltip: 'Deny and ask Claude to retry this action in a simpler format — complex interactions don\'t work well with the permission system.',
|
||||
label: ClideSettings.i18n.string(context, 'prompt.permission.allow', namespace: 'builtin.claude', placeholder: '1. Allow'),
|
||||
variant: ClideButtonVariant.primary,
|
||||
onPressed: () => _permAllow(),
|
||||
),
|
||||
if (canRemember)
|
||||
ClideButton(
|
||||
label: ClideSettings.i18n.string(
|
||||
context,
|
||||
'prompt.permission.allowRemember',
|
||||
namespace: 'builtin.claude',
|
||||
placeholder: "2. Allow & don't ask again",
|
||||
),
|
||||
onPressed: () => _permAllow(remember: true),
|
||||
),
|
||||
ClideButton(
|
||||
label: ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'prompt.permission.deny',
|
||||
namespace: 'builtin.claude',
|
||||
placeholder: '${canRemember ? '3' : '2'}. Deny',
|
||||
replacers: [I18nReplacer(from: '{n}', replace: canRemember ? '3' : '2')],
|
||||
),
|
||||
onPressed: _permDeny,
|
||||
),
|
||||
ClideButton(
|
||||
label: ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'prompt.permission.denySimplify',
|
||||
namespace: 'builtin.claude',
|
||||
placeholder: '${canRemember ? '4' : '3'}. Deny & simplify',
|
||||
replacers: [I18nReplacer(from: '{n}', replace: canRemember ? '4' : '3')],
|
||||
),
|
||||
tooltip: ClideSettings.i18n.string(
|
||||
context,
|
||||
'prompt.permission.denySimplify.tooltip',
|
||||
namespace: 'builtin.claude',
|
||||
placeholder: 'Deny and ask Claude to retry this action in a simpler format — complex interactions don\'t work well with the permission system.',
|
||||
),
|
||||
onPressed: _permDenySimplify,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_NoteField(controller: _note, placeholder: 'add a note (optional) — sent to Claude'),
|
||||
_NoteField(
|
||||
controller: _note,
|
||||
placeholder: ClideSettings.i18n.string(
|
||||
context,
|
||||
'prompt.permission.note.placeholder',
|
||||
namespace: 'builtin.claude',
|
||||
placeholder: 'add a note (optional) — sent to Claude',
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Render the tool input in the shape that best fits the tool. Delegates to
|
||||
/// the shared top-level helpers (also used by ConversationView — T-168).
|
||||
Widget _inputBody(SurfaceTokens tokens, String mono, ToolPrompt p) => toolInputBody(tokens, p.toolName, p.input, mono);
|
||||
Widget _inputBody(SurfaceTokens tokens, String mono, ToolPrompt p) => toolInputBody(context, tokens, p.toolName, p.input, mono);
|
||||
|
||||
// -- AskUserQuestion: single = bare, multi = stepper + review --------------
|
||||
|
||||
@@ -291,13 +338,17 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
|
||||
if (_q.length <= 1) {
|
||||
return (
|
||||
tokens.statusInfo,
|
||||
'question',
|
||||
ClideSettings.i18n.string(context, 'prompt.question.label', namespace: 'builtin.claude', placeholder: 'question'),
|
||||
[
|
||||
if (_q.isNotEmpty) _questionBody(tokens, 0),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
ClideButton(label: 'Submit', variant: ClideButtonVariant.primary, onPressed: (_q.isNotEmpty && _answer(0).isNotEmpty) ? _submit : null),
|
||||
ClideButton(
|
||||
label: ClideSettings.i18n.string(context, 'prompt.submit', namespace: 'builtin.claude', placeholder: 'Submit'),
|
||||
variant: ClideButtonVariant.primary,
|
||||
onPressed: (_q.isNotEmpty && _answer(0).isNotEmpty) ? _submit : null,
|
||||
),
|
||||
const Spacer(),
|
||||
_chatInstead(tokens),
|
||||
],
|
||||
@@ -308,17 +359,28 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
|
||||
if (_step >= _q.length) {
|
||||
return (
|
||||
tokens.statusInfo,
|
||||
'review',
|
||||
ClideSettings.i18n.string(context, 'prompt.review.label', namespace: 'builtin.claude', placeholder: 'review'),
|
||||
[
|
||||
ClideText('Review your answers', color: tokens.globalForeground, fontSize: clideFontBody),
|
||||
ClideText(
|
||||
ClideSettings.i18n.string(context, 'prompt.review.title', namespace: 'builtin.claude', placeholder: 'Review your answers'),
|
||||
color: tokens.globalForeground,
|
||||
fontSize: clideFontBody,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
for (var i = 0; i < _q.length; i++) _reviewRow(tokens, i),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
ClideButton(label: '‹ Back', onPressed: () => setState(() => _step = _q.length - 1)),
|
||||
ClideButton(
|
||||
label: ClideSettings.i18n.string(context, 'prompt.back', namespace: 'builtin.claude', placeholder: '‹ Back'),
|
||||
onPressed: () => setState(() => _step = _q.length - 1),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ClideButton(label: 'Submit answers', variant: ClideButtonVariant.primary, onPressed: _allAnswered ? _submit : null),
|
||||
ClideButton(
|
||||
label: ClideSettings.i18n.string(context, 'prompt.submitAnswers', namespace: 'builtin.claude', placeholder: 'Submit answers'),
|
||||
variant: ClideButtonVariant.primary,
|
||||
onPressed: _allAnswered ? _submit : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -328,7 +390,7 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
|
||||
final last = _step == _q.length - 1;
|
||||
return (
|
||||
tokens.statusInfo,
|
||||
'question',
|
||||
ClideSettings.i18n.string(context, 'prompt.question.label', namespace: 'builtin.claude', placeholder: 'question'),
|
||||
[
|
||||
_navBar(tokens),
|
||||
const SizedBox(height: 10),
|
||||
@@ -336,8 +398,20 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
if (_step > 0) ...[ClideButton(label: '‹ Back', onPressed: () => setState(() => _step--)), const SizedBox(width: 8)],
|
||||
ClideButton(label: last ? 'Review ›' : 'Next ›', variant: ClideButtonVariant.primary, onPressed: answered ? () => setState(() => _step++) : null),
|
||||
if (_step > 0) ...[
|
||||
ClideButton(
|
||||
label: ClideSettings.i18n.string(context, 'prompt.back', namespace: 'builtin.claude', placeholder: '‹ Back'),
|
||||
onPressed: () => setState(() => _step--),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
ClideButton(
|
||||
label: last
|
||||
? ClideSettings.i18n.string(context, 'prompt.reviewNav', namespace: 'builtin.claude', placeholder: 'Review ›')
|
||||
: ClideSettings.i18n.string(context, 'prompt.next', namespace: 'builtin.claude', placeholder: 'Next ›'),
|
||||
variant: ClideButtonVariant.primary,
|
||||
onPressed: answered ? () => setState(() => _step++) : null,
|
||||
),
|
||||
const Spacer(),
|
||||
_chatInstead(tokens),
|
||||
],
|
||||
@@ -375,7 +449,14 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
|
||||
);
|
||||
}
|
||||
}
|
||||
chips.add(ClideText('Review', fontSize: clideFontMeta, fontFamily: ClideSettings.fonts.monoOf(context), color: tokens.globalTextMuted));
|
||||
chips.add(
|
||||
ClideText(
|
||||
ClideSettings.i18n.string(context, 'prompt.nav.review', namespace: 'builtin.claude', placeholder: 'Review'),
|
||||
fontSize: clideFontMeta,
|
||||
fontFamily: ClideSettings.fonts.monoOf(context),
|
||||
color: tokens.globalTextMuted,
|
||||
),
|
||||
);
|
||||
chips.add(ClideText('›', color: tokens.globalTextMuted, fontSize: 15));
|
||||
return Wrap(spacing: 10, runSpacing: 6, crossAxisAlignment: WrapCrossAlignment.center, children: chips);
|
||||
}
|
||||
@@ -417,12 +498,28 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
|
||||
children: [
|
||||
for (var oi = 0; oi < q.options.length; oi++)
|
||||
_optButton(qi, q.options[oi].label, q.options[oi].label, q.multiSelect, q.options[oi].description, oi + 1),
|
||||
_optButton(qi, _kOther, 'Other…', q.multiSelect, '', q.options.length + 1),
|
||||
_optButton(
|
||||
qi,
|
||||
_kOther,
|
||||
ClideSettings.i18n.string(context, 'prompt.option.other', namespace: 'builtin.claude', placeholder: 'Other…'),
|
||||
q.multiSelect,
|
||||
'',
|
||||
q.options.length + 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (hasOther) ...[const SizedBox(height: 8), _NoteField(controller: _other[qi], placeholder: 'type your answer…')],
|
||||
if (hasOther) ...[
|
||||
const SizedBox(height: 8),
|
||||
_NoteField(
|
||||
controller: _other[qi],
|
||||
placeholder: ClideSettings.i18n.string(context, 'prompt.other.placeholder', namespace: 'builtin.claude', placeholder: 'type your answer…'),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
_NoteField(controller: _qnote[qi], placeholder: '+ note (optional)'),
|
||||
_NoteField(
|
||||
controller: _qnote[qi],
|
||||
placeholder: ClideSettings.i18n.string(context, 'prompt.note.placeholder', namespace: 'builtin.claude', placeholder: '+ note (optional)'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -452,7 +549,7 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
|
||||
|
||||
Widget _chatInstead(SurfaceTokens tokens) {
|
||||
return ClideButton(
|
||||
label: 'chat instead',
|
||||
label: ClideSettings.i18n.string(context, 'prompt.chatInstead', namespace: 'builtin.claude', placeholder: 'chat instead'),
|
||||
variant: ClideButtonVariant.subtle,
|
||||
onPressed: () => widget.onResolve(widget.prompt.promptId, const DenyTool('User chose to chat instead.')),
|
||||
);
|
||||
@@ -491,15 +588,15 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
|
||||
///
|
||||
/// Shared between [ToolPromptCard] (permission prompt body) and the
|
||||
/// [ConversationView] tool-use card bodies (T-168).
|
||||
Widget toolInputBody(SurfaceTokens tokens, String toolName, Map<String, dynamic> input, String mono) {
|
||||
Widget toolInputBody(BuildContext context, SurfaceTokens tokens, String toolName, Map<String, dynamic> input, String mono) {
|
||||
switch (toolName) {
|
||||
case 'Bash':
|
||||
return toolBashBody(tokens, input);
|
||||
return toolBashBody(context, tokens, input);
|
||||
case 'Write':
|
||||
return toolWriteBody(tokens, input, mono);
|
||||
case 'Edit':
|
||||
case 'MultiEdit':
|
||||
return toolEditBody(tokens, input, mono);
|
||||
return toolEditBody(context, tokens, input, mono);
|
||||
case 'Read':
|
||||
case 'Grep':
|
||||
case 'LS':
|
||||
@@ -511,9 +608,12 @@ Widget toolInputBody(SurfaceTokens tokens, String toolName, Map<String, dynamic>
|
||||
|
||||
/// Bash tool body: the command as a shell code block, with optional background
|
||||
/// / timeout annotations.
|
||||
Widget toolBashBody(SurfaceTokens tokens, Map<String, dynamic> input) {
|
||||
Widget toolBashBody(BuildContext context, SurfaceTokens tokens, Map<String, dynamic> input) {
|
||||
final cmd = (input['command'] as String? ?? '').trimRight();
|
||||
final notes = <String>[if (input['run_in_background'] == true) 'background', if (input['timeout'] is num) 'timeout ${input['timeout']}ms'];
|
||||
final notes = <String>[
|
||||
if (input['run_in_background'] == true) ClideSettings.i18n.string(context, 'tool.bash.background', namespace: 'builtin.claude', placeholder: 'background'),
|
||||
if (input['timeout'] is num) 'timeout ${input['timeout']}ms',
|
||||
];
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -543,7 +643,7 @@ Widget toolWriteBody(SurfaceTokens tokens, Map<String, dynamic> input, String mo
|
||||
}
|
||||
|
||||
/// Edit / MultiEdit tool body: before/after diff view.
|
||||
Widget toolEditBody(SurfaceTokens tokens, Map<String, dynamic> input, String mono) {
|
||||
Widget toolEditBody(BuildContext context, SurfaceTokens tokens, Map<String, dynamic> input, String mono) {
|
||||
final path = input['file_path'] as String? ?? '';
|
||||
final oldStr = input['old_string'] as String? ?? '';
|
||||
final newStr = input['new_string'] as String? ?? '';
|
||||
@@ -553,11 +653,21 @@ Widget toolEditBody(SurfaceTokens tokens, Map<String, dynamic> input, String mon
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (path.isNotEmpty) toolPathLine(tokens, path, mono),
|
||||
ClideText('— before', fontSize: clideFontMeta, color: tokens.globalTextMuted, fontFamily: mono),
|
||||
ClideText(
|
||||
ClideSettings.i18n.string(context, 'tool.edit.before', namespace: 'builtin.claude', placeholder: '— before'),
|
||||
fontSize: clideFontMeta,
|
||||
color: tokens.globalTextMuted,
|
||||
fontFamily: mono,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
ClideCodeBlock(source: oldStr, language: lang),
|
||||
const SizedBox(height: 8),
|
||||
ClideText('+ after', fontSize: clideFontMeta, color: tokens.globalTextMuted, fontFamily: mono),
|
||||
ClideText(
|
||||
ClideSettings.i18n.string(context, 'tool.edit.after', namespace: 'builtin.claude', placeholder: '+ after'),
|
||||
fontSize: clideFontMeta,
|
||||
color: tokens.globalTextMuted,
|
||||
fontFamily: mono,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
ClideCodeBlock(source: newStr, language: lang),
|
||||
],
|
||||
|
||||
@@ -87,7 +87,7 @@ class _RunningIndicatorState extends State<RunningIndicator> with SingleTickerPr
|
||||
final reduced = MediaQuery.maybeOf(context)?.disableAnimations ?? false;
|
||||
// The animated text is decorative; AT gets one stable label.
|
||||
return Semantics(
|
||||
label: 'Claude is running',
|
||||
label: ClideSettings.i18n.string(context, 'running.semantics', namespace: 'builtin.claude', placeholder: 'Claude is running'),
|
||||
child: ExcludeSemantics(
|
||||
child: reduced
|
||||
? ClideText('${_verbs.first}…', color: claudeAccent, fontSize: clideFontMeta)
|
||||
|
||||
@@ -77,12 +77,20 @@ class _SessionPickerDialogState extends State<SessionPickerDialog> {
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 12, 14, 8),
|
||||
child: ClideText('Resume a Claude session', fontSize: clideFontBody, color: theme.globalForeground),
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(context, 'sessionPicker.title', namespace: 'builtin.claude', placeholder: 'Resume a Claude session'),
|
||||
fontSize: clideFontBody,
|
||||
color: theme.globalForeground,
|
||||
),
|
||||
),
|
||||
if (widget.sessions.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 4, 14, 16),
|
||||
child: ClideText('No sessions found for this workspace.', muted: true, fontSize: clideFontSmall),
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(context, 'sessionPicker.empty', namespace: 'builtin.claude', placeholder: 'No sessions found for this workspace.'),
|
||||
muted: true,
|
||||
fontSize: clideFontSmall,
|
||||
),
|
||||
)
|
||||
else
|
||||
Flexible(
|
||||
|
||||
@@ -132,15 +132,19 @@ class _TeamChatSidebarState extends State<TeamChatSidebar> {
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ClideText('MESSAGES', fontSize: clideFontSmall, color: tokens.globalTextMuted),
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(context, 'teamChat.section.messages', namespace: 'builtin.claude', placeholder: 'MESSAGES'),
|
||||
fontSize: clideFontSmall,
|
||||
color: tokens.globalTextMuted,
|
||||
),
|
||||
),
|
||||
Semantics(
|
||||
button: true,
|
||||
label: 'Open full chat pane',
|
||||
label: ClideSettings.i18n.string(context, 'teamChat.popOut.semantics', namespace: 'builtin.claude', placeholder: 'Open full chat pane'),
|
||||
excludeSemantics: true,
|
||||
onTap: widget.onPopOut,
|
||||
child: ClideTappable(
|
||||
tooltip: 'Open full chat',
|
||||
tooltip: ClideSettings.i18n.string(context, 'teamChat.popOut.tooltip', namespace: 'builtin.claude', placeholder: 'Open full chat'),
|
||||
onTap: widget.onPopOut,
|
||||
builder: (ctx, hovered, _) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 1),
|
||||
@@ -155,7 +159,11 @@ class _TeamChatSidebarState extends State<TeamChatSidebar> {
|
||||
if (messages.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: ClideText('No messages yet.', muted: true, fontSize: clideFontSmall),
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(context, 'teamChat.empty', namespace: 'builtin.claude', placeholder: 'No messages yet.'),
|
||||
muted: true,
|
||||
fontSize: clideFontSmall,
|
||||
),
|
||||
)
|
||||
else
|
||||
for (final msg in messages.length > 5 ? messages.sublist(messages.length - 5) : messages)
|
||||
@@ -168,7 +176,13 @@ class _TeamChatSidebarState extends State<TeamChatSidebar> {
|
||||
formatLabel: (n) => '@$n',
|
||||
child: Focus(
|
||||
onKeyEvent: _handleKeyEvent,
|
||||
child: _ChatInputField(controller: _controller, focusNode: _focusNode, tokens: tokens, onSubmit: _submit, placeholder: '@name or @team …'),
|
||||
child: _ChatInputField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
tokens: tokens,
|
||||
onSubmit: _submit,
|
||||
placeholder: ClideSettings.i18n.string(context, 'teamChat.composer.placeholder', namespace: 'builtin.claude', placeholder: '@name or @team …'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -303,12 +317,22 @@ class _TeamChatPaneState extends State<TeamChatPane> {
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: tokens.panelBorder)),
|
||||
),
|
||||
child: ClideText('Team Chat', fontSize: clideFontSmall, color: tokens.globalTextMuted),
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(context, 'teamChat.pane.title', namespace: 'builtin.claude', placeholder: 'Team Chat'),
|
||||
fontSize: clideFontSmall,
|
||||
color: tokens.globalTextMuted,
|
||||
),
|
||||
),
|
||||
// Timeline.
|
||||
Expanded(
|
||||
child: messages.isEmpty
|
||||
? Center(child: ClideText('No messages yet.', muted: true, fontSize: clideFontSmall))
|
||||
? Center(
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(context, 'teamChat.empty', namespace: 'builtin.claude', placeholder: 'No messages yet.'),
|
||||
muted: true,
|
||||
fontSize: clideFontSmall,
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
@@ -335,7 +359,12 @@ class _TeamChatPaneState extends State<TeamChatPane> {
|
||||
children: [
|
||||
Semantics(
|
||||
checked: _interrupt,
|
||||
label: 'Interrupt target session',
|
||||
label: ClideSettings.i18n.string(
|
||||
context,
|
||||
'teamChat.interrupt.semantics',
|
||||
namespace: 'builtin.claude',
|
||||
placeholder: 'Interrupt target session',
|
||||
),
|
||||
excludeSemantics: true,
|
||||
onTap: () => setState(() => _interrupt = !_interrupt),
|
||||
child: Container(
|
||||
@@ -350,7 +379,11 @@ class _TeamChatPaneState extends State<TeamChatPane> {
|
||||
child: _interrupt ? Center(child: ClideIcon(PhosphorIcons.byName('check'), size: 9, color: tokens.globalFocus)) : null,
|
||||
),
|
||||
),
|
||||
ClideText('Interrupt', fontSize: clideFontSmall, color: _interrupt ? tokens.globalForeground : tokens.globalTextMuted),
|
||||
ClideText(
|
||||
ClideSettings.i18n.string(context, 'teamChat.interrupt.label', namespace: 'builtin.claude', placeholder: 'Interrupt'),
|
||||
fontSize: clideFontSmall,
|
||||
color: _interrupt ? tokens.globalForeground : tokens.globalTextMuted,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -363,7 +396,18 @@ class _TeamChatPaneState extends State<TeamChatPane> {
|
||||
formatLabel: (n) => '@$n',
|
||||
child: Focus(
|
||||
onKeyEvent: _handleKeyEvent,
|
||||
child: _ChatInputField(controller: _controller, focusNode: _focusNode, tokens: tokens, onSubmit: _submit, placeholder: '@name or @team …'),
|
||||
child: _ChatInputField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
tokens: tokens,
|
||||
onSubmit: _submit,
|
||||
placeholder: ClideSettings.i18n.string(
|
||||
context,
|
||||
'teamChat.composer.placeholder',
|
||||
namespace: 'builtin.claude',
|
||||
placeholder: '@name or @team …',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -65,6 +65,8 @@ class CliInstallExtension extends ClideExtension {
|
||||
id: 'clide.installCli',
|
||||
command: 'clide.installCli',
|
||||
title: "clide: Install 'clide' command in PATH",
|
||||
titleKey: 'command.clide.installCli',
|
||||
i18nNamespace: id,
|
||||
run: (_) async {
|
||||
final r = _resolved.install();
|
||||
final ctx = _ctx;
|
||||
|
||||
@@ -88,9 +88,21 @@ class _DecisionDetailViewState extends State<DecisionDetailView> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_loading) return const Center(child: ClideText('Loading…', muted: true));
|
||||
if (_loading) {
|
||||
return Center(
|
||||
child: ClideText(ClideSettings.i18n.string(context, 'detail.loading', namespace: 'builtin.decisions', placeholder: 'Loading…'), muted: true),
|
||||
);
|
||||
}
|
||||
final d = _decision;
|
||||
if (d == null) return const Padding(padding: EdgeInsets.all(12), child: ClideText('Select a decision to view details.', muted: true));
|
||||
if (d == null) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(context, 'detail.empty', namespace: 'builtin.decisions', placeholder: 'Select a decision to view details.'),
|
||||
muted: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final tokens = ClideSettings.theme.of(context).surface;
|
||||
final isDark = ClideSettings.theme.of(context).dark;
|
||||
@@ -171,7 +183,12 @@ class _DecisionDetailViewState extends State<DecisionDetailView> {
|
||||
if (body != null && body.isNotEmpty) ...[const SizedBox(height: 12), ClideMarkdown(body, onRecordTap: (id) => _navigateToRecord(context, id))],
|
||||
if (refs.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
ClideText('CROSS-REFERENCES', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader, fontFamily: ClideSettings.fonts.monoOf(context)),
|
||||
ClideText(
|
||||
ClideSettings.i18n.string(context, 'detail.section.refs', namespace: 'builtin.decisions', placeholder: 'CROSS-REFERENCES'),
|
||||
fontSize: clideFontSmall,
|
||||
color: tokens.sidebarSectionHeader,
|
||||
fontFamily: ClideSettings.fonts.monoOf(context),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
for (final ref in refs) _RefCard(ref: ref, tokens: tokens),
|
||||
],
|
||||
|
||||
@@ -74,7 +74,8 @@ class _DecisionsViewState extends State<DecisionsView> {
|
||||
if (!mounted) return;
|
||||
if (!resp.ok) {
|
||||
setState(() {
|
||||
_error = resp.error?.message ?? 'failed to load decisions';
|
||||
_error =
|
||||
resp.error?.message ?? ClideSettings.i18n.string(context, 'error.load', namespace: 'builtin.decisions', placeholder: 'failed to load decisions');
|
||||
_loading = false;
|
||||
});
|
||||
return;
|
||||
@@ -129,10 +130,20 @@ class _DecisionsViewState extends State<DecisionsView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideSettings.theme.of(context).surface;
|
||||
if (_loading) return const Center(child: ClideText('Loading decisions...', muted: true));
|
||||
if (_loading) {
|
||||
return Center(
|
||||
child: ClideText(ClideSettings.i18n.string(context, 'loading', namespace: 'builtin.decisions', placeholder: 'Loading decisions...'), muted: true),
|
||||
);
|
||||
}
|
||||
if (_error != null) return Padding(padding: const EdgeInsets.all(12), child: ClideText(_error!, muted: true));
|
||||
if (_decisions.isEmpty) {
|
||||
return const Padding(padding: EdgeInsets.all(12), child: ClideText('No decisions found.\nRun `pql decisions sync` to index.', muted: true));
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(context, 'empty', namespace: 'builtin.decisions', placeholder: 'No decisions found.\nRun `pql decisions sync` to index.'),
|
||||
muted: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final lf = _filter.toLowerCase();
|
||||
@@ -161,13 +172,17 @@ class _DecisionsViewState extends State<DecisionsView> {
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ClideFilterBox(address: 'decisions.panel', hint: 'Filter decisions…', onChanged: (v) => setState(() => _filter = v)),
|
||||
child: ClideFilterBox(
|
||||
address: 'decisions.panel',
|
||||
hint: ClideSettings.i18n.string(context, 'filter.hint', namespace: 'builtin.decisions', placeholder: 'Filter decisions…'),
|
||||
onChanged: (v) => setState(() => _filter = v),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: ClideTappable(
|
||||
onTap: _refreshing ? null : _refresh,
|
||||
tooltip: 'Refresh decisions',
|
||||
tooltip: ClideSettings.i18n.string(context, 'refresh.tooltip', namespace: 'builtin.decisions', placeholder: 'Refresh decisions'),
|
||||
builder: (ctx, hovered, _) =>
|
||||
ClideIcon(PhosphorIcons.byName('arrow-clockwise'), size: 13, color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
|
||||
),
|
||||
@@ -182,7 +197,7 @@ class _DecisionsViewState extends State<DecisionsView> {
|
||||
children: [
|
||||
if (confirmed.isNotEmpty)
|
||||
ClideAccordion(
|
||||
label: 'CONFIRMED',
|
||||
label: ClideSettings.i18n.string(context, 'section.confirmed', namespace: 'builtin.decisions', placeholder: 'CONFIRMED'),
|
||||
count: confirmed.length,
|
||||
leading: Container(
|
||||
width: 8,
|
||||
@@ -204,7 +219,7 @@ class _DecisionsViewState extends State<DecisionsView> {
|
||||
),
|
||||
if (questions.isNotEmpty)
|
||||
ClideAccordion(
|
||||
label: 'QUESTIONS',
|
||||
label: ClideSettings.i18n.string(context, 'section.questions', namespace: 'builtin.decisions', placeholder: 'QUESTIONS'),
|
||||
count: questions.length,
|
||||
leading: Container(
|
||||
width: 8,
|
||||
@@ -226,7 +241,7 @@ class _DecisionsViewState extends State<DecisionsView> {
|
||||
),
|
||||
if (rejected.isNotEmpty)
|
||||
ClideAccordion(
|
||||
label: 'REJECTED',
|
||||
label: ClideSettings.i18n.string(context, 'section.rejected', namespace: 'builtin.decisions', placeholder: 'REJECTED'),
|
||||
count: rejected.length,
|
||||
leading: Container(
|
||||
width: 8,
|
||||
@@ -322,7 +337,12 @@ class _DecisionCard extends StatelessWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(color: tokens.statusSuccess.withAlpha(0x30), borderRadius: BorderRadius.circular(3)),
|
||||
child: ClideText('resolved', fontSize: clideFontBadge, color: tokens.statusSuccess, fontFamily: ClideSettings.fonts.monoOf(context)),
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(context, 'badge.resolved', namespace: 'builtin.decisions', placeholder: 'resolved'),
|
||||
fontSize: clideFontBadge,
|
||||
color: tokens.statusSuccess,
|
||||
fontFamily: ClideSettings.fonts.monoOf(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
|
||||
@@ -50,6 +50,8 @@ class DecisionsExtension extends ClideExtension {
|
||||
id: 'decisions.detail',
|
||||
slot: Slots.contextPanel,
|
||||
title: 'Decision',
|
||||
titleKey: 'tab.detail.title',
|
||||
i18nNamespace: id,
|
||||
icon: PhosphorIcons.byName('lightbulb'),
|
||||
build: (_) => const DecisionDetailView(),
|
||||
),
|
||||
|
||||
@@ -24,7 +24,14 @@ class DeepLinkExtension extends ClideExtension {
|
||||
|
||||
@override
|
||||
List<ContributionPoint> get contributions => [
|
||||
CommandContribution(id: 'deeplink.invoke', command: 'deeplink.invoke', title: 'Open a clide:// deep link', run: _invoke),
|
||||
CommandContribution(
|
||||
id: 'deeplink.invoke',
|
||||
command: 'deeplink.invoke',
|
||||
title: 'Open a clide:// deep link',
|
||||
titleKey: 'command.deeplink.invoke',
|
||||
i18nNamespace: id,
|
||||
run: _invoke,
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -78,20 +85,44 @@ class _DeepLinkConfirmDialog extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClideText('Open an external link?', fontSize: clideFontBody, color: t.globalForeground),
|
||||
ClideText(
|
||||
ClideSettings.i18n.string(context, 'dialog.title', namespace: 'builtin.deeplink', placeholder: 'Open an external link?'),
|
||||
fontSize: clideFontBody,
|
||||
color: t.globalForeground,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
ClideText('A clide:// link from outside the app is asking to:', muted: true, fontSize: clideFontSmall),
|
||||
ClideText(
|
||||
ClideSettings.i18n.string(context, 'dialog.body', namespace: 'builtin.deeplink', placeholder: 'A clide:// link from outside the app is asking to:'),
|
||||
muted: true,
|
||||
fontSize: clideFontSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ClideText(action.describe, fontFamily: ClideSettings.fonts.monoOf(context), fontSize: clideFontSmall, color: t.globalForeground),
|
||||
const SizedBox(height: 8),
|
||||
ClideText('Only allow this if you trust where the link came from.', fontSize: clideFontMeta, color: t.statusWarning),
|
||||
ClideText(
|
||||
ClideSettings.i18n.string(
|
||||
context,
|
||||
'dialog.warning',
|
||||
namespace: 'builtin.deeplink',
|
||||
placeholder: 'Only allow this if you trust where the link came from.',
|
||||
),
|
||||
fontSize: clideFontMeta,
|
||||
color: t.statusWarning,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ClideButton(label: 'Cancel', onPressed: () => onResolve(false)),
|
||||
ClideButton(
|
||||
label: ClideSettings.i18n.string(context, 'button.cancel', namespace: 'builtin.deeplink', placeholder: 'Cancel'),
|
||||
onPressed: () => onResolve(false),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ClideButton(label: 'Open', variant: ClideButtonVariant.primary, onPressed: () => onResolve(true)),
|
||||
ClideButton(
|
||||
label: ClideSettings.i18n.string(context, 'button.open', namespace: 'builtin.deeplink', placeholder: 'Open'),
|
||||
variant: ClideButtonVariant.primary,
|
||||
onPressed: () => onResolve(true),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -16,13 +16,30 @@ class DefaultLayoutExtension extends ClideExtension {
|
||||
@override
|
||||
List<ContributionPoint> get contributions => [
|
||||
_preset ?? classicPreset(),
|
||||
CommandContribution(id: 'layout.reset', command: 'layout.reset', title: 'Layout: Reset to Classic', run: _reset),
|
||||
CommandContribution(id: 'palette.toggle', command: 'palette.toggle', title: 'Command Palette', defaultBinding: 'ctrl+shift+p', run: _togglePalette),
|
||||
CommandContribution(
|
||||
id: 'layout.reset',
|
||||
command: 'layout.reset',
|
||||
title: 'Layout: Reset to Classic',
|
||||
titleKey: 'command.reset',
|
||||
i18nNamespace: id,
|
||||
run: _reset,
|
||||
),
|
||||
CommandContribution(
|
||||
id: 'palette.toggle',
|
||||
command: 'palette.toggle',
|
||||
title: 'Command Palette',
|
||||
titleKey: 'command.palette.toggle',
|
||||
i18nNamespace: id,
|
||||
defaultBinding: 'ctrl+shift+p',
|
||||
run: _togglePalette,
|
||||
),
|
||||
// Collapse toggles (D-051, D-054)
|
||||
CommandContribution(
|
||||
id: 'sidebar.collapse',
|
||||
command: 'sidebar.collapse',
|
||||
title: 'Toggle Sidebar Collapse',
|
||||
titleKey: 'command.sidebar.collapse',
|
||||
i18nNamespace: id,
|
||||
defaultBinding: 'ctrl+shift+1',
|
||||
run: _collapseSidebar,
|
||||
),
|
||||
@@ -30,19 +47,55 @@ class DefaultLayoutExtension extends ClideExtension {
|
||||
id: 'context.collapse',
|
||||
command: 'context.collapse',
|
||||
title: 'Toggle Context Panel Collapse',
|
||||
titleKey: 'command.context.collapse',
|
||||
i18nNamespace: id,
|
||||
defaultBinding: 'ctrl+shift+3',
|
||||
run: _collapseContext,
|
||||
),
|
||||
// Panel focus (D-054)
|
||||
CommandContribution(id: 'panel.focus.left', command: 'panel.focus.left', title: 'Focus Left Panel', defaultBinding: 'ctrl+1', run: _focusLeft),
|
||||
CommandContribution(id: 'panel.focus.middle', command: 'panel.focus.middle', title: 'Focus Middle Panel', defaultBinding: 'ctrl+2', run: _focusMiddle),
|
||||
CommandContribution(id: 'panel.focus.right', command: 'panel.focus.right', title: 'Focus Right Panel', defaultBinding: 'ctrl+3', run: _focusRight),
|
||||
CommandContribution(
|
||||
id: 'panel.focus.left',
|
||||
command: 'panel.focus.left',
|
||||
title: 'Focus Left Panel',
|
||||
titleKey: 'command.panel.focus.left',
|
||||
i18nNamespace: id,
|
||||
defaultBinding: 'ctrl+1',
|
||||
run: _focusLeft,
|
||||
),
|
||||
CommandContribution(
|
||||
id: 'panel.focus.middle',
|
||||
command: 'panel.focus.middle',
|
||||
title: 'Focus Middle Panel',
|
||||
titleKey: 'command.panel.focus.middle',
|
||||
i18nNamespace: id,
|
||||
defaultBinding: 'ctrl+2',
|
||||
run: _focusMiddle,
|
||||
),
|
||||
CommandContribution(
|
||||
id: 'panel.focus.right',
|
||||
command: 'panel.focus.right',
|
||||
title: 'Focus Right Panel',
|
||||
titleKey: 'command.panel.focus.right',
|
||||
i18nNamespace: id,
|
||||
defaultBinding: 'ctrl+3',
|
||||
run: _focusRight,
|
||||
),
|
||||
// Focus mode (D-052, D-054)
|
||||
CommandContribution(id: 'panel.focusMode', command: 'panel.focusMode', title: 'Toggle Focus Mode', defaultBinding: 'ctrl+.', run: _toggleFocusMode),
|
||||
CommandContribution(
|
||||
id: 'panel.focusMode',
|
||||
command: 'panel.focusMode',
|
||||
title: 'Toggle Focus Mode',
|
||||
titleKey: 'command.panel.focusMode',
|
||||
i18nNamespace: id,
|
||||
defaultBinding: 'ctrl+.',
|
||||
run: _toggleFocusMode,
|
||||
),
|
||||
CommandContribution(
|
||||
id: 'panel.focusMode.exit',
|
||||
command: 'panel.focusMode.exit',
|
||||
title: 'Exit Focus Mode',
|
||||
titleKey: 'command.panel.focusMode.exit',
|
||||
i18nNamespace: id,
|
||||
defaultBinding: 'escape',
|
||||
// Stand down in Vim insert/visual mode so Esc returns to normal
|
||||
// mode instead of closing the editor (T-257). Symmetric with
|
||||
@@ -51,8 +104,24 @@ class DefaultLayoutExtension extends ClideExtension {
|
||||
run: _exitFocusMode,
|
||||
),
|
||||
// Editor split (D-049, D-054)
|
||||
CommandContribution(id: 'editor.open', command: 'editor.open', title: 'Open Editor', defaultBinding: 'ctrl+e', run: _openEditor),
|
||||
CommandContribution(id: 'editor.close', command: 'editor.close', title: 'Close Editor', defaultBinding: 'ctrl+w', run: _closeEditor),
|
||||
CommandContribution(
|
||||
id: 'editor.open',
|
||||
command: 'editor.open',
|
||||
title: 'Open Editor',
|
||||
titleKey: 'command.editor.open',
|
||||
i18nNamespace: id,
|
||||
defaultBinding: 'ctrl+e',
|
||||
run: _openEditor,
|
||||
),
|
||||
CommandContribution(
|
||||
id: 'editor.close',
|
||||
command: 'editor.close',
|
||||
title: 'Close Editor',
|
||||
titleKey: 'command.editor.close',
|
||||
i18nNamespace: id,
|
||||
defaultBinding: 'ctrl+w',
|
||||
run: _closeEditor,
|
||||
),
|
||||
// Workspace tab cycling (T-405). Preset-neutral ctrl+pagedown/up across every
|
||||
// preset; the vim preset additionally binds gt/gT to these (T-405 part 2,
|
||||
// once a global multi-chord matcher lands — see T-404).
|
||||
@@ -60,6 +129,8 @@ class DefaultLayoutExtension extends ClideExtension {
|
||||
id: 'workspace.tab.next',
|
||||
command: 'workspace.tab.next',
|
||||
title: 'Next Workspace Tab',
|
||||
titleKey: 'command.workspace.tab.next',
|
||||
i18nNamespace: id,
|
||||
defaultBinding: 'ctrl+pagedown',
|
||||
run: _nextWorkspaceTab,
|
||||
),
|
||||
@@ -67,6 +138,8 @@ class DefaultLayoutExtension extends ClideExtension {
|
||||
id: 'workspace.tab.previous',
|
||||
command: 'workspace.tab.previous',
|
||||
title: 'Previous Workspace Tab',
|
||||
titleKey: 'command.workspace.tab.previous',
|
||||
i18nNamespace: id,
|
||||
defaultBinding: 'ctrl+pageup',
|
||||
run: _prevWorkspaceTab,
|
||||
),
|
||||
@@ -76,6 +149,8 @@ class DefaultLayoutExtension extends ClideExtension {
|
||||
id: 'sidebar.section.${i + 1}',
|
||||
command: 'sidebar.section.${i + 1}',
|
||||
title: 'Sidebar: Section ${i + 1}',
|
||||
titleKey: 'command.sidebar.section.${i + 1}',
|
||||
i18nNamespace: id,
|
||||
defaultBinding: 'alt+${i + 1}',
|
||||
run: (args) => _switchSidebarSection(i),
|
||||
),
|
||||
|
||||
@@ -88,7 +88,7 @@ class _DiffViewState extends State<DiffView> {
|
||||
builder: (context, _) {
|
||||
final tokens = ClideSettings.theme.of(context).surface;
|
||||
return Semantics(
|
||||
label: 'diff view',
|
||||
label: ClideSettings.i18n.string(context, 'view.semantics', namespace: 'builtin.diff', placeholder: 'diff view'),
|
||||
container: true,
|
||||
explicitChildNodes: true,
|
||||
child: Column(
|
||||
@@ -100,9 +100,21 @@ class _DiffViewState extends State<DiffView> {
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: ClideText(c.error!, color: tokens.statusError),
|
||||
),
|
||||
if (c.loading && c.diffs.isEmpty) const Padding(padding: EdgeInsets.all(12), child: ClideText('Loading…', muted: true)),
|
||||
if (c.loading && c.diffs.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: ClideText(ClideSettings.i18n.string(context, 'status.loading', namespace: 'builtin.diff', placeholder: 'Loading…'), muted: true),
|
||||
),
|
||||
if (!c.loading && c.diffs.isEmpty && c.error == null)
|
||||
Padding(padding: const EdgeInsets.all(12), child: ClideText(c.showStaged ? 'No staged changes.' : 'No unstaged changes.', muted: true)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: ClideText(
|
||||
c.showStaged
|
||||
? ClideSettings.i18n.string(context, 'empty.staged', namespace: 'builtin.diff', placeholder: 'No staged changes.')
|
||||
: ClideSettings.i18n.string(context, 'empty.unstaged', namespace: 'builtin.diff', placeholder: 'No unstaged changes.'),
|
||||
muted: true,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
controller: _scroll,
|
||||
@@ -147,20 +159,28 @@ class _DiffToolbar extends StatelessWidget {
|
||||
Semantics(
|
||||
button: true,
|
||||
toggled: !controller.showStaged,
|
||||
label: 'show unstaged changes',
|
||||
label: ClideSettings.i18n.string(context, 'toolbar.unstaged.semantics', namespace: 'builtin.diff', placeholder: 'show unstaged changes'),
|
||||
child: GestureDetector(
|
||||
onTap: controller.showStaged ? controller.toggleStaged : null,
|
||||
child: ClideText('Unstaged', fontSize: clideFontCaption, color: controller.showStaged ? tokens.globalTextMuted : tokens.globalForeground),
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(context, 'toolbar.unstaged', namespace: 'builtin.diff', placeholder: 'Unstaged'),
|
||||
fontSize: clideFontCaption,
|
||||
color: controller.showStaged ? tokens.globalTextMuted : tokens.globalForeground,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Semantics(
|
||||
button: true,
|
||||
toggled: controller.showStaged,
|
||||
label: 'show staged changes',
|
||||
label: ClideSettings.i18n.string(context, 'toolbar.staged.semantics', namespace: 'builtin.diff', placeholder: 'show staged changes'),
|
||||
child: GestureDetector(
|
||||
onTap: controller.showStaged ? null : controller.toggleStaged,
|
||||
child: ClideText('Staged', fontSize: clideFontCaption, color: controller.showStaged ? tokens.globalForeground : tokens.globalTextMuted),
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(context, 'toolbar.staged', namespace: 'builtin.diff', placeholder: 'Staged'),
|
||||
fontSize: clideFontCaption,
|
||||
color: controller.showStaged ? tokens.globalForeground : tokens.globalTextMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -191,13 +211,23 @@ class _FileDiff extends StatelessWidget {
|
||||
final hunks = (diff['hunks'] as List?) ?? const [];
|
||||
|
||||
final meta = <String>[];
|
||||
if (isNew) meta.add('new file');
|
||||
if (isDeleted) meta.add('deleted');
|
||||
if (isNew) meta.add(ClideSettings.i18n.string(context, 'meta.newFile', namespace: 'builtin.diff', placeholder: 'new file'));
|
||||
if (isDeleted) meta.add(ClideSettings.i18n.string(context, 'meta.deleted', namespace: 'builtin.diff', placeholder: 'deleted'));
|
||||
if (isRenamed) {
|
||||
final oldPath = diff['oldPath'] as String?;
|
||||
if (oldPath != null) meta.add('renamed from $oldPath');
|
||||
if (oldPath != null) {
|
||||
meta.add(
|
||||
ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'meta.renamedFrom',
|
||||
namespace: 'builtin.diff',
|
||||
placeholder: 'renamed from {path}',
|
||||
replacers: [I18nReplacer(from: '{path}', replace: oldPath)],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (isBinary) meta.add('binary');
|
||||
if (isBinary) meta.add(ClideSettings.i18n.string(context, 'meta.binary', namespace: 'builtin.diff', placeholder: 'binary'));
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
|
||||
@@ -320,9 +320,19 @@ class _EditorViewState extends State<EditorView> {
|
||||
builder: (context, _) {
|
||||
if (c.buffers.isEmpty) {
|
||||
return ClidePaneChrome(
|
||||
title: 'editor',
|
||||
subtitle: 'no buffer · use `clide open <path>` or pick a file in the tree',
|
||||
child: const Center(child: ClideText('Open a file to begin editing.', muted: true)),
|
||||
title: ClideSettings.i18n.string(context, 'chrome.title', namespace: 'builtin.editor', placeholder: 'editor'),
|
||||
subtitle: ClideSettings.i18n.string(
|
||||
context,
|
||||
'subtitle.no-buffer',
|
||||
namespace: 'builtin.editor',
|
||||
placeholder: 'no buffer · use `clide open <path>` or pick a file in the tree',
|
||||
),
|
||||
child: Center(
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(context, 'empty', namespace: 'builtin.editor', placeholder: 'Open a file to begin editing.'),
|
||||
muted: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return MultitabPane<String>(
|
||||
@@ -392,7 +402,7 @@ class _TextBody extends StatelessWidget {
|
||||
showCursor: true,
|
||||
);
|
||||
return Semantics(
|
||||
label: 'editor text area',
|
||||
label: ClideSettings.i18n.string(context, 'a11y.text-area', namespace: 'builtin.editor', placeholder: 'editor text area'),
|
||||
textField: true,
|
||||
multiline: true,
|
||||
child: ColoredBox(
|
||||
|
||||
@@ -20,17 +20,28 @@ class ExtensionsUiExtension extends ClideExtension {
|
||||
@override
|
||||
List<ContributionPoint> get contributions => [
|
||||
SettingsControlContribution(id: 'extensions-ui.notice-control', customId: 'extensions.notice', builder: (_) => const ExtensionsNotice()),
|
||||
const SettingsCategoryContribution(
|
||||
SettingsCategoryContribution(
|
||||
id: 'extensions',
|
||||
category: SettingsCategory(
|
||||
id: 'extensions',
|
||||
title: 'Extensions',
|
||||
titleKey: 'settings.extensions.title',
|
||||
i18nNamespace: id,
|
||||
iconName: 'puzzle-piece',
|
||||
priority: 80,
|
||||
sections: [
|
||||
SettingsSection(
|
||||
label: '',
|
||||
fields: [SettingsField(key: 'app.extensions._notice', kind: SettingsFieldKind.custom, label: '', customId: 'extensions.notice')],
|
||||
labelKey: 'settings.extensions.section.notice',
|
||||
fields: [
|
||||
SettingsField(
|
||||
key: 'app.extensions._notice',
|
||||
kind: SettingsFieldKind.custom,
|
||||
label: '',
|
||||
labelKey: 'settings.extensions.field.notice.label',
|
||||
customId: 'extensions.notice',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -105,7 +105,10 @@ class _FileTreeViewState extends State<FileTreeView> {
|
||||
}
|
||||
final root = c.rootPath;
|
||||
if (root == null) {
|
||||
return const Padding(padding: EdgeInsets.all(12), child: ClideText('Loading…', muted: true));
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: ClideText(ClideSettings.i18n.string(context, 'loading', namespace: 'builtin.files', placeholder: 'Loading…'), muted: true),
|
||||
);
|
||||
}
|
||||
final rootName = root.split(Platform.pathSeparator).last;
|
||||
final selected = c.selectedPath;
|
||||
@@ -127,10 +130,20 @@ class _FileTreeViewState extends State<FileTreeView> {
|
||||
);
|
||||
return Column(
|
||||
children: [
|
||||
ClideFilterBox(address: 'files.tree', hint: 'Filter files…', onChanged: (v) => setState(() => _filter = v)),
|
||||
ClideFilterBox(
|
||||
address: 'files.tree',
|
||||
hint: ClideSettings.i18n.string(context, 'filter.hint', namespace: 'builtin.files', placeholder: 'Filter files…'),
|
||||
onChanged: (v) => setState(() => _filter = v),
|
||||
),
|
||||
Expanded(
|
||||
child: Semantics(
|
||||
label: 'file tree — $rootName',
|
||||
label: ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'a11y.tree',
|
||||
namespace: 'builtin.files',
|
||||
placeholder: 'file tree — {name}',
|
||||
replacers: [I18nReplacer(from: '{name}', replace: rootName)],
|
||||
),
|
||||
container: true,
|
||||
explicitChildNodes: true,
|
||||
// Vim nav (j/k/h/l/gg/G/o) drives a selection cursor while this
|
||||
@@ -206,7 +219,21 @@ class _DirRow extends StatelessWidget {
|
||||
final selected = path == selectedPath;
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: '${expanded ? 'Collapse' : 'Expand'} $name',
|
||||
label: expanded
|
||||
? ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'a11y.collapse',
|
||||
namespace: 'builtin.files',
|
||||
placeholder: 'Collapse {name}',
|
||||
replacers: [I18nReplacer(from: '{name}', replace: name)],
|
||||
)
|
||||
: ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'a11y.expand',
|
||||
namespace: 'builtin.files',
|
||||
placeholder: 'Expand {name}',
|
||||
replacers: [I18nReplacer(from: '{name}', replace: name)],
|
||||
),
|
||||
onTap: () => controller.toggle(path),
|
||||
child: _Row(
|
||||
key: selected ? selectedKey : null,
|
||||
@@ -235,7 +262,13 @@ class _FileRow extends StatelessWidget {
|
||||
final selected = path == selectedPath;
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: 'Open $name',
|
||||
label: ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'a11y.open',
|
||||
namespace: 'builtin.files',
|
||||
placeholder: 'Open {name}',
|
||||
replacers: [I18nReplacer(from: '{name}', replace: name)],
|
||||
),
|
||||
onTap: () => _openFile(context, path),
|
||||
child: _Row(key: selected ? selectedKey : null, depth: depth, onTap: () => _openFile(context, path), label: name, selected: selected),
|
||||
);
|
||||
|
||||
@@ -70,12 +70,16 @@ class _GitPanelViewState extends State<GitPanelView> {
|
||||
builder: (context, _) {
|
||||
final tokens = ClideSettings.theme.of(context).surface;
|
||||
return Semantics(
|
||||
label: 'git panel',
|
||||
label: ClideSettings.i18n.string(context, 'panel.semantics', namespace: 'builtin.git', placeholder: 'git panel'),
|
||||
container: true,
|
||||
explicitChildNodes: true,
|
||||
child: Column(
|
||||
children: [
|
||||
ClideFilterBox(address: 'git.panel', hint: 'Filter changes…', onChanged: (v) => setState(() => _filter = v)),
|
||||
ClideFilterBox(
|
||||
address: 'git.panel',
|
||||
hint: ClideSettings.i18n.string(context, 'filter.hint', namespace: 'builtin.git', placeholder: 'Filter changes…'),
|
||||
onChanged: (v) => setState(() => _filter = v),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
@@ -89,34 +93,62 @@ class _GitPanelViewState extends State<GitPanelView> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
child: ClideText(c.error!, color: tokens.statusError, fontSize: clideFontCaption, maxLines: 3),
|
||||
),
|
||||
if (c.loading && c.isClean) const Padding(padding: EdgeInsets.all(12), child: ClideText('Loading…', muted: true)),
|
||||
if (c.loading && c.isClean)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(context, 'status.loading', namespace: 'builtin.git', placeholder: 'Loading…'),
|
||||
muted: true,
|
||||
),
|
||||
),
|
||||
if (!c.loading && c.isClean && c.error == null)
|
||||
const Padding(padding: EdgeInsets.all(12), child: ClideText('Nothing to commit, working tree clean.', muted: true)),
|
||||
if (c.conflicted.isNotEmpty) _FileGroup(label: 'Merge conflicts', entries: _applyFilter(c.conflicted), actions: const []),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(context, 'status.clean', namespace: 'builtin.git', placeholder: 'Nothing to commit, working tree clean.'),
|
||||
muted: true,
|
||||
),
|
||||
),
|
||||
if (c.conflicted.isNotEmpty)
|
||||
_FileGroup(
|
||||
label: ClideSettings.i18n.string(context, 'group.conflicts', namespace: 'builtin.git', placeholder: 'Merge conflicts'),
|
||||
entries: _applyFilter(c.conflicted),
|
||||
actions: const [],
|
||||
),
|
||||
if (c.staged.isNotEmpty) ...[
|
||||
_FileGroup(
|
||||
label: 'Staged',
|
||||
label: ClideSettings.i18n.string(context, 'group.staged', namespace: 'builtin.git', placeholder: 'Staged'),
|
||||
entries: _applyFilter(c.staged),
|
||||
actions: [_GroupAction(label: 'Unstage all', onTap: () => unawaited(c.unstage(const [])))],
|
||||
actions: [
|
||||
_GroupAction(
|
||||
label: ClideSettings.i18n.string(context, 'action.unstageAll', namespace: 'builtin.git', placeholder: 'Unstage all'),
|
||||
onTap: () => unawaited(c.unstage(const [])),
|
||||
),
|
||||
],
|
||||
onUnstage: (path) => unawaited(c.unstage([path])),
|
||||
),
|
||||
_CommitInput(commitMsg: _commitMsg, commitFocus: _commitFocus, controller: c),
|
||||
],
|
||||
if (c.unstaged.isNotEmpty)
|
||||
_FileGroup(
|
||||
label: 'Changes',
|
||||
label: ClideSettings.i18n.string(context, 'group.changes', namespace: 'builtin.git', placeholder: 'Changes'),
|
||||
entries: _applyFilter(c.unstaged),
|
||||
actions: [_GroupAction(label: 'Stage all', onTap: () => unawaited(c.stageAll()))],
|
||||
actions: [
|
||||
_GroupAction(
|
||||
label: ClideSettings.i18n.string(context, 'action.stageAll', namespace: 'builtin.git', placeholder: 'Stage all'),
|
||||
onTap: () => unawaited(c.stageAll()),
|
||||
),
|
||||
],
|
||||
onStage: (path) => unawaited(c.stage([path])),
|
||||
onDiscard: (path) => _confirmDiscard(context, c, path),
|
||||
),
|
||||
if (c.untracked.isNotEmpty)
|
||||
_FileGroup(
|
||||
label: 'Untracked',
|
||||
label: ClideSettings.i18n.string(context, 'group.untracked', namespace: 'builtin.git', placeholder: 'Untracked'),
|
||||
entries: _applyFilter(c.untracked),
|
||||
actions: [
|
||||
_GroupAction(
|
||||
label: 'Stage all',
|
||||
label: ClideSettings.i18n.string(context, 'action.stageAll', namespace: 'builtin.git', placeholder: 'Stage all'),
|
||||
onTap: () {
|
||||
final paths = [for (final e in c.untracked) e['path'] as String];
|
||||
unawaited(c.stage(paths));
|
||||
@@ -144,7 +176,7 @@ class _BranchHeader extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideSettings.theme.of(context).surface;
|
||||
final branch = controller.branch ?? '(detached)';
|
||||
final branch = controller.branch ?? ClideSettings.i18n.string(context, 'branch.detached', namespace: 'builtin.git', placeholder: '(detached)');
|
||||
final parts = <String>[branch];
|
||||
if (controller.ahead > 0) parts.add('↑${controller.ahead}');
|
||||
if (controller.behind > 0) parts.add('↓${controller.behind}');
|
||||
@@ -155,9 +187,17 @@ class _BranchHeader extends StatelessWidget {
|
||||
Expanded(
|
||||
child: ClideText(parts.join(' '), fontSize: clideFontCaption, color: tokens.sidebarForeground),
|
||||
),
|
||||
_SmallAction(label: 'Pull', semanticsLabel: 'git pull', onTap: () => unawaited(controller.pull())),
|
||||
_SmallAction(
|
||||
label: ClideSettings.i18n.string(context, 'action.pull', namespace: 'builtin.git', placeholder: 'Pull'),
|
||||
semanticsLabel: ClideSettings.i18n.string(context, 'action.pull.semantics', namespace: 'builtin.git', placeholder: 'git pull'),
|
||||
onTap: () => unawaited(controller.pull()),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
_SmallAction(label: 'Push', semanticsLabel: 'git push', onTap: () => unawaited(controller.push())),
|
||||
_SmallAction(
|
||||
label: ClideSettings.i18n.string(context, 'action.push', namespace: 'builtin.git', placeholder: 'Push'),
|
||||
semanticsLabel: ClideSettings.i18n.string(context, 'action.push.semantics', namespace: 'builtin.git', placeholder: 'git push'),
|
||||
onTap: () => unawaited(controller.push()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -181,7 +221,7 @@ class _CommitInput extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Semantics(
|
||||
label: 'commit message',
|
||||
label: ClideSettings.i18n.string(context, 'commit.message.semantics', namespace: 'builtin.git', placeholder: 'commit message'),
|
||||
textField: true,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(border: Border.all(color: tokens.globalBorder)),
|
||||
@@ -200,9 +240,9 @@ class _CommitInput extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
ClideButton(
|
||||
label: 'Commit',
|
||||
label: ClideSettings.i18n.string(context, 'commit.button', namespace: 'builtin.git', placeholder: 'Commit'),
|
||||
onPressed: _doCommit,
|
||||
semanticLabel: 'commit staged changes',
|
||||
semanticLabel: ClideSettings.i18n.string(context, 'commit.button.semantics', namespace: 'builtin.git', placeholder: 'commit staged changes'),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
),
|
||||
],
|
||||
@@ -277,7 +317,7 @@ class _GitFileRow extends StatelessWidget {
|
||||
final indexState = entry['indexState'] as String?;
|
||||
final workTreeState = entry['workTreeState'] as String?;
|
||||
final state = indexState ?? workTreeState ?? '';
|
||||
final stateLabel = _stateLabel(state);
|
||||
final stateLabel = _stateLabel(context, state);
|
||||
|
||||
return Semantics(
|
||||
button: true,
|
||||
@@ -298,9 +338,42 @@ class _GitFileRow extends StatelessWidget {
|
||||
child: ClideText(name, maxLines: 1, overflow: TextOverflow.ellipsis, color: tokens.sidebarForeground),
|
||||
),
|
||||
if (hovered) ...[
|
||||
if (onStage != null) _SmallAction(label: '+', semanticsLabel: 'stage $name', onTap: () => onStage!(path)),
|
||||
if (onUnstage != null) _SmallAction(label: '-', semanticsLabel: 'unstage $name', onTap: () => onUnstage!(path)),
|
||||
if (onDiscard != null) _SmallAction(label: 'x', semanticsLabel: 'discard changes to $name', onTap: () => onDiscard!(path)),
|
||||
if (onStage != null)
|
||||
_SmallAction(
|
||||
label: '+',
|
||||
semanticsLabel: ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'row.stage.semantics',
|
||||
namespace: 'builtin.git',
|
||||
placeholder: 'stage {name}',
|
||||
replacers: [I18nReplacer(from: '{name}', replace: name)],
|
||||
),
|
||||
onTap: () => onStage!(path),
|
||||
),
|
||||
if (onUnstage != null)
|
||||
_SmallAction(
|
||||
label: '-',
|
||||
semanticsLabel: ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'row.unstage.semantics',
|
||||
namespace: 'builtin.git',
|
||||
placeholder: 'unstage {name}',
|
||||
replacers: [I18nReplacer(from: '{name}', replace: name)],
|
||||
),
|
||||
onTap: () => onUnstage!(path),
|
||||
),
|
||||
if (onDiscard != null)
|
||||
_SmallAction(
|
||||
label: 'x',
|
||||
semanticsLabel: ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'row.discard.semantics',
|
||||
namespace: 'builtin.git',
|
||||
placeholder: 'discard changes to {name}',
|
||||
replacers: [I18nReplacer(from: '{name}', replace: name)],
|
||||
),
|
||||
onTap: () => onDiscard!(path),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
@@ -321,14 +394,15 @@ class _GitFileRow extends StatelessWidget {
|
||||
};
|
||||
}
|
||||
|
||||
static String _stateLabel(String state) {
|
||||
static String _stateLabel(BuildContext context, String state) {
|
||||
String t(String key, String english) => ClideSettings.i18n.string(context, key, namespace: 'builtin.git', placeholder: english);
|
||||
return switch (state) {
|
||||
'added' => 'added',
|
||||
'modified' => 'modified',
|
||||
'deleted' => 'deleted',
|
||||
'renamed' => 'renamed',
|
||||
'copied' => 'copied',
|
||||
'untracked' => 'untracked',
|
||||
'added' => t('state.added', 'added'),
|
||||
'modified' => t('state.modified', 'modified'),
|
||||
'deleted' => t('state.deleted', 'deleted'),
|
||||
'renamed' => t('state.renamed', 'renamed'),
|
||||
'copied' => t('state.copied', 'copied'),
|
||||
'untracked' => t('state.untracked', 'untracked'),
|
||||
_ => '',
|
||||
};
|
||||
}
|
||||
@@ -390,16 +464,36 @@ class _DiscardConfirmDialog extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClideText('Discard changes?', color: tokens.globalForeground),
|
||||
ClideText(
|
||||
ClideSettings.i18n.string(context, 'discard.title', namespace: 'builtin.git', placeholder: 'Discard changes?'),
|
||||
color: tokens.globalForeground,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ClideText('Unstaged changes to $name will be permanently lost.', fontSize: clideFontCaption, color: tokens.statusError),
|
||||
ClideText(
|
||||
ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'discard.body',
|
||||
namespace: 'builtin.git',
|
||||
placeholder: 'Unstaged changes to {name} will be permanently lost.',
|
||||
replacers: [I18nReplacer(from: '{name}', replace: name)],
|
||||
),
|
||||
fontSize: clideFontCaption,
|
||||
color: tokens.statusError,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ClideButton(label: 'Cancel', variant: ClideButtonVariant.subtle, onPressed: onCancel),
|
||||
ClideButton(
|
||||
label: ClideSettings.i18n.string(context, 'button.cancel', namespace: 'builtin.git', placeholder: 'Cancel'),
|
||||
variant: ClideButtonVariant.subtle,
|
||||
onPressed: onCancel,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ClideButton(label: 'Discard', onPressed: onConfirm),
|
||||
ClideButton(
|
||||
label: ClideSettings.i18n.string(context, 'button.discard', namespace: 'builtin.git', placeholder: 'Discard'),
|
||||
onPressed: onConfirm,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -63,7 +63,13 @@ class _GitStatusItemState extends State<GitStatusItem> {
|
||||
if (_behind > 0) parts.add('↓$_behind');
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: 'switch branch — $_branch',
|
||||
label: ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'branch.switch.semantics',
|
||||
namespace: 'builtin.git',
|
||||
placeholder: 'switch branch — {branch}',
|
||||
replacers: [I18nReplacer(from: '{branch}', replace: _branch!)],
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: _openBranchPicker,
|
||||
child: MouseRegion(
|
||||
@@ -123,7 +129,8 @@ class _BranchPickerState extends State<_BranchPicker> {
|
||||
if (r.ok) {
|
||||
_branches = [for (final b in (r.data['branches'] as List? ?? const [])) (b as Map).cast<String, Object?>()];
|
||||
} else {
|
||||
_error = r.error?.message ?? 'failed to load branches';
|
||||
_error =
|
||||
r.error?.message ?? ClideSettings.i18n.string(context, 'branchPicker.loadFailed', namespace: 'builtin.git', placeholder: 'failed to load branches');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -165,7 +172,7 @@ class _BranchPickerState extends State<_BranchPicker> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: ClideText(
|
||||
'Switch branch',
|
||||
ClideSettings.i18n.string(context, 'branchPicker.title', namespace: 'builtin.git', placeholder: 'Switch branch'),
|
||||
fontSize: clideFontCaption,
|
||||
color: tokens.globalTextMuted,
|
||||
fontFamily: ClideSettings.fonts.monoOf(context),
|
||||
@@ -181,10 +188,20 @@ class _BranchPickerState extends State<_BranchPicker> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_loading) const Padding(padding: EdgeInsets.all(12), child: ClideText('Loading…', muted: true)),
|
||||
if (_loading)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: ClideText(ClideSettings.i18n.string(context, 'status.loading', namespace: 'builtin.git', placeholder: 'Loading…'), muted: true),
|
||||
),
|
||||
if (_error != null) Padding(padding: const EdgeInsets.all(12), child: ClideText(_error!, muted: true)),
|
||||
if (!_loading && _error == null && _branches.isEmpty)
|
||||
const Padding(padding: EdgeInsets.all(12), child: ClideText('No branches found.', muted: true)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(context, 'branchPicker.empty', namespace: 'builtin.git', placeholder: 'No branches found.'),
|
||||
muted: true,
|
||||
),
|
||||
),
|
||||
if (_branches.isNotEmpty)
|
||||
Flexible(
|
||||
child: ListView.builder(
|
||||
|
||||
@@ -37,6 +37,8 @@ class KeybindingsUiExtension extends ClideExtension {
|
||||
id: 'keymap.preset.${entry.key}',
|
||||
command: 'keymap.preset.${entry.key}',
|
||||
title: entry.value,
|
||||
titleKey: 'command.preset.${entry.key}',
|
||||
i18nNamespace: id,
|
||||
run: (_) async {
|
||||
await _keymap?.setPreset(entry.key);
|
||||
return IpcResponse.ok(id: '', data: {'preset': entry.key});
|
||||
@@ -46,29 +48,34 @@ class KeybindingsUiExtension extends ClideExtension {
|
||||
// from kKeymapPresetSetting; picking one runs `keymap.preset.<value>`
|
||||
// (applyCommandPrefix), which calls KeymapService.setPreset — persisting
|
||||
// and reloading the layered keymap live.
|
||||
const SettingsCategoryContribution(
|
||||
SettingsCategoryContribution(
|
||||
id: 'keymap',
|
||||
category: SettingsCategory(
|
||||
id: 'keymap',
|
||||
title: 'Keymap',
|
||||
titleKey: 'settings.keymap.title',
|
||||
i18nNamespace: id,
|
||||
iconName: 'keyboard',
|
||||
priority: 20,
|
||||
sections: [
|
||||
SettingsSection(
|
||||
label: 'Preset',
|
||||
labelKey: 'settings.keymap.section.preset',
|
||||
fields: [
|
||||
SettingsField(
|
||||
key: kKeymapPresetSetting,
|
||||
kind: SettingsFieldKind.select,
|
||||
label: 'Active preset',
|
||||
labelKey: 'settings.keymap.field.preset.label',
|
||||
help: 'Keyboard layout for the whole app.',
|
||||
helpKey: 'settings.keymap.field.preset.help',
|
||||
defaultValue: 'default',
|
||||
applyCommandPrefix: 'keymap.preset.',
|
||||
options: [
|
||||
SettingsOption(value: 'default', label: 'Default'),
|
||||
SettingsOption(value: 'vim', label: 'Vim'),
|
||||
SettingsOption(value: 'vscode', label: 'VS Code'),
|
||||
SettingsOption(value: 'jetbrains', label: 'JetBrains'),
|
||||
SettingsOption(value: 'default', label: 'Default', labelKey: 'settings.keymap.field.preset.option.default'),
|
||||
SettingsOption(value: 'vim', label: 'Vim', labelKey: 'settings.keymap.field.preset.option.vim'),
|
||||
SettingsOption(value: 'vscode', label: 'VS Code', labelKey: 'settings.keymap.field.preset.option.vscode'),
|
||||
SettingsOption(value: 'jetbrains', label: 'JetBrains', labelKey: 'settings.keymap.field.preset.option.jetbrains'),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -95,11 +95,23 @@ class _MarkdownViewerState extends State<MarkdownViewer> {
|
||||
return Padding(padding: const EdgeInsets.all(12), child: ClideText(_error!, muted: true));
|
||||
}
|
||||
if (_content == null) {
|
||||
return const Padding(padding: EdgeInsets.all(12), child: ClideText('Select a .md file to preview it here.', muted: true));
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(context, 'empty', namespace: 'builtin.markdown', placeholder: 'Select a .md file to preview it here.'),
|
||||
muted: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
return ClidePaneChrome(
|
||||
title: _path ?? 'viewer',
|
||||
subtitle: '${_content!.split('\n').length} lines',
|
||||
title: _path ?? ClideSettings.i18n.string(context, 'chrome.title', namespace: 'builtin.markdown', placeholder: 'viewer'),
|
||||
subtitle: ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'subtitle.lines',
|
||||
namespace: 'builtin.markdown',
|
||||
placeholder: '{count} lines',
|
||||
replacers: [I18nReplacer(from: '{count}', replace: '${_content!.split('\n').length}')],
|
||||
),
|
||||
leading: ReaderPinButton(pinned: _nav?.hasPinned ?? false, onTap: _path != null ? _onPin : null),
|
||||
trailing: [
|
||||
ReaderActionBar(
|
||||
|
||||
@@ -32,18 +32,43 @@ class AboutDialog extends StatelessWidget {
|
||||
const SizedBox(height: 2),
|
||||
const ClideText(clideTagline, muted: true, fontSize: 13),
|
||||
const SizedBox(height: 16),
|
||||
_Kv(label: 'Version', value: clideVersion, tokens: tokens),
|
||||
_Kv(label: 'Commit', value: clideCommit, tokens: tokens),
|
||||
_Kv(label: 'Built', value: _formatDate(clideDate), tokens: tokens),
|
||||
_Kv(label: 'Repository', value: clideRepository, tokens: tokens),
|
||||
_Kv(
|
||||
label: ClideSettings.i18n.string(context, 'about.version', namespace: 'builtin.menubar', placeholder: 'Version'),
|
||||
value: clideVersion,
|
||||
tokens: tokens,
|
||||
),
|
||||
_Kv(
|
||||
label: ClideSettings.i18n.string(context, 'about.commit', namespace: 'builtin.menubar', placeholder: 'Commit'),
|
||||
value: clideCommit,
|
||||
tokens: tokens,
|
||||
),
|
||||
_Kv(
|
||||
label: ClideSettings.i18n.string(context, 'about.built', namespace: 'builtin.menubar', placeholder: 'Built'),
|
||||
value: _formatDate(clideDate),
|
||||
tokens: tokens,
|
||||
),
|
||||
_Kv(
|
||||
label: ClideSettings.i18n.string(context, 'about.repository', namespace: 'builtin.menubar', placeholder: 'Repository'),
|
||||
value: clideRepository,
|
||||
tokens: tokens,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ClideText('Bundled dependencies', fontSize: clideFontCaption, color: tokens.globalTextMuted),
|
||||
ClideText(
|
||||
ClideSettings.i18n.string(context, 'licenses.heading', namespace: 'builtin.menubar', placeholder: 'Bundled dependencies'),
|
||||
fontSize: clideFontCaption,
|
||||
color: tokens.globalTextMuted,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
_Licenses(tokens: tokens),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [ClideButton(label: 'Close', onPressed: onDismiss)],
|
||||
children: [
|
||||
ClideButton(
|
||||
label: ClideSettings.i18n.string(context, 'button.close', namespace: 'builtin.menubar', placeholder: 'Close'),
|
||||
onPressed: onDismiss,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -86,7 +111,13 @@ class _Licenses extends StatelessWidget {
|
||||
future: loadLicenses(),
|
||||
builder: (context, snap) {
|
||||
if (!snap.hasData) {
|
||||
return ClideText(snap.hasError ? 'Licenses unavailable.' : 'Loading…', fontSize: 12, color: tokens.globalTextMuted);
|
||||
return ClideText(
|
||||
snap.hasError
|
||||
? ClideSettings.i18n.string(context, 'licenses.unavailable', namespace: 'builtin.menubar', placeholder: 'Licenses unavailable.')
|
||||
: ClideSettings.i18n.string(context, 'licenses.loading', namespace: 'builtin.menubar', placeholder: 'Loading…'),
|
||||
fontSize: 12,
|
||||
color: tokens.globalTextMuted,
|
||||
);
|
||||
}
|
||||
final deps = snap.data!.dependencies;
|
||||
return Container(
|
||||
|
||||
@@ -30,6 +30,8 @@ class MenuBarExtension extends ClideExtension {
|
||||
id: 'file.openFolder',
|
||||
command: 'file.openFolder',
|
||||
title: 'File: Open Folder…',
|
||||
titleKey: 'command.file.openFolder',
|
||||
i18nNamespace: id,
|
||||
run: (_) async {
|
||||
await _file.openFolder();
|
||||
return IpcResponse.ok(id: '', data: const {});
|
||||
@@ -39,6 +41,8 @@ class MenuBarExtension extends ClideExtension {
|
||||
id: 'file.newWindow',
|
||||
command: 'file.newWindow',
|
||||
title: 'File: New Window',
|
||||
titleKey: 'command.file.newWindow',
|
||||
i18nNamespace: id,
|
||||
run: (_) async {
|
||||
_file.newWindow();
|
||||
return IpcResponse.ok(id: '', data: const {});
|
||||
@@ -48,6 +52,8 @@ class MenuBarExtension extends ClideExtension {
|
||||
id: 'file.closeWorkspace',
|
||||
command: 'file.closeWorkspace',
|
||||
title: 'File: Close Project',
|
||||
titleKey: 'command.file.closeWorkspace',
|
||||
i18nNamespace: id,
|
||||
run: (_) async {
|
||||
_file.closeWorkspace();
|
||||
return IpcResponse.ok(id: '', data: const {});
|
||||
@@ -57,6 +63,8 @@ class MenuBarExtension extends ClideExtension {
|
||||
id: 'help.about',
|
||||
command: 'help.about',
|
||||
title: 'Help: About clide',
|
||||
titleKey: 'command.help.about',
|
||||
i18nNamespace: id,
|
||||
run: (_) async {
|
||||
services.dialog.show<Object>((ctx, dismiss) => AboutDialog(onDismiss: () => dismiss()));
|
||||
return IpcResponse.ok(id: '', data: const {});
|
||||
|
||||
@@ -123,9 +123,17 @@ class _OpenFolderDialogState extends State<OpenFolderDialog> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const ClideText('Open project', fontSize: 16, fontWeight: FontWeight.w600),
|
||||
ClideText(
|
||||
ClideSettings.i18n.string(context, 'dialog.openProject.title', namespace: 'builtin.menubar', placeholder: 'Open project'),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const ClideText('Enter the path to a git repository.', muted: true, fontSize: 13),
|
||||
ClideText(
|
||||
ClideSettings.i18n.string(context, 'dialog.openProject.body', namespace: 'builtin.menubar', placeholder: 'Enter the path to a git repository.'),
|
||||
muted: true,
|
||||
fontSize: 13,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
@@ -148,14 +156,29 @@ class _OpenFolderDialogState extends State<OpenFolderDialog> {
|
||||
onSubmitted: (_) => unawaited(_submit()),
|
||||
),
|
||||
),
|
||||
if (_error != null) ...[const SizedBox(height: 8), ClideText(_error!, color: tokens.statusError, fontSize: 12)],
|
||||
if (_error != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
ClideText(
|
||||
ClideSettings.i18n.string(context, 'dialog.openProject.error', namespace: 'builtin.menubar', placeholder: 'Not a git repository'),
|
||||
color: tokens.statusError,
|
||||
fontSize: 12,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ClideButton(label: 'Cancel', onPressed: widget.onCancel),
|
||||
ClideButton(
|
||||
label: ClideSettings.i18n.string(context, 'button.cancel', namespace: 'builtin.menubar', placeholder: 'Cancel'),
|
||||
onPressed: widget.onCancel,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ClideButton(label: _loading ? 'Opening…' : 'Open', onPressed: _loading ? null : _submit),
|
||||
ClideButton(
|
||||
label: _loading
|
||||
? ClideSettings.i18n.string(context, 'button.opening', namespace: 'builtin.menubar', placeholder: 'Opening…')
|
||||
: ClideSettings.i18n.string(context, 'button.open', namespace: 'builtin.menubar', placeholder: 'Open'),
|
||||
onPressed: _loading ? null : _submit,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -186,15 +209,33 @@ class NotARepoDialog extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const ClideText('No git repo found', fontSize: 16, fontWeight: FontWeight.w600),
|
||||
ClideText(
|
||||
ClideSettings.i18n.string(context, 'dialog.notRepo.title', namespace: 'builtin.menubar', placeholder: 'No git repo found'),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ClideText(path, muted: true, fontSize: 13),
|
||||
const SizedBox(height: 8),
|
||||
const ClideText('A clide project root requires a git repository.', muted: true, fontSize: 13),
|
||||
ClideText(
|
||||
ClideSettings.i18n.string(
|
||||
context,
|
||||
'dialog.notRepo.body',
|
||||
namespace: 'builtin.menubar',
|
||||
placeholder: 'A clide project root requires a git repository.',
|
||||
),
|
||||
muted: true,
|
||||
fontSize: 13,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [ClideButton(label: 'OK', onPressed: () => onDismiss())],
|
||||
children: [
|
||||
ClideButton(
|
||||
label: ClideSettings.i18n.string(context, 'button.ok', namespace: 'builtin.menubar', placeholder: 'OK'),
|
||||
onPressed: () => onDismiss(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -72,7 +72,13 @@ class MenuBar extends StatelessWidget {
|
||||
return ListenableBuilder(
|
||||
listenable: Listenable.merge([controller, kernel.commands, kernel.project]),
|
||||
builder: (ctx, _) {
|
||||
final menus = resolveMenus(buildClideMenuTree(), kernel.commands, kernel, bindingLabel: (id) => keymapBindingLabel(kernel.keymap, id));
|
||||
final menus = resolveMenus(
|
||||
buildClideMenuTree(),
|
||||
kernel.commands,
|
||||
kernel,
|
||||
bindingLabel: (id) => keymapBindingLabel(kernel.keymap, id),
|
||||
localizeTitle: (c) => localizedCommandTitle(context, c),
|
||||
);
|
||||
controller.setMnemonics([for (final m in menus) m.title[m.mnemonic].toLowerCase()]);
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -187,10 +193,24 @@ class _TopMenuButtonState extends State<_TopMenuButton> {
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(horizontal: clideInsetStandard),
|
||||
color: open || hovered ? tokens.listItemHoverBackground : null,
|
||||
child: ClideText(widget.menu.title, fontSize: 12, color: open || hovered ? tokens.globalForeground : tokens.chromeForeground),
|
||||
child: ClideText(_menuTitle(ctx, widget.menu.title), fontSize: 12, color: open || hovered ? tokens.globalForeground : tokens.chromeForeground),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Localize a curated top-menu title. The curated tree authors English
|
||||
/// titles ('File'/'View'/'Help'); resolve each to its catalog entry, falling
|
||||
/// back to the authored English (which also drives the Alt mnemonic).
|
||||
String _menuTitle(BuildContext context, String title) {
|
||||
final key = switch (title) {
|
||||
'File' => 'menu.file',
|
||||
'View' => 'menu.view',
|
||||
'Help' => 'menu.help',
|
||||
_ => null,
|
||||
};
|
||||
if (key == null) return title;
|
||||
return ClideSettings.i18n.string(context, key, namespace: 'builtin.menubar', placeholder: title);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,13 @@ String? keymapBindingLabel(KeymapService keymap, String commandId) {
|
||||
/// menus. [bindingLabel] supplies the keybinding string for a command id
|
||||
/// (typically [keymapBindingLabel] bound to the keymap); when it returns null
|
||||
/// the command's own `defaultBinding` is used as a fallback.
|
||||
List<ResolvedMenu> resolveMenus(List<TopMenu> tree, CommandRegistry registry, KernelServices services, {String? Function(String commandId)? bindingLabel}) {
|
||||
List<ResolvedMenu> resolveMenus(
|
||||
List<TopMenu> tree,
|
||||
CommandRegistry registry,
|
||||
KernelServices services, {
|
||||
String? Function(String commandId)? bindingLabel,
|
||||
String Function(CommandContribution cmd)? localizeTitle,
|
||||
}) {
|
||||
final placed = <String>{
|
||||
for (final m in tree)
|
||||
for (final n in m.nodes)
|
||||
@@ -130,7 +136,7 @@ List<ResolvedMenu> resolveMenus(List<TopMenu> tree, CommandRegistry registry, Ke
|
||||
ResolvedItem resolveItem(MenuCommandItem item) {
|
||||
final cmd = registry.get(item.commandId);
|
||||
final enabled = cmd != null && (item.enabledWhen?.call(services) ?? true);
|
||||
final raw = cmd?.title ?? item.fallbackTitle ?? item.commandId;
|
||||
final raw = (cmd != null ? (localizeTitle?.call(cmd) ?? cmd.title) : null) ?? item.fallbackTitle ?? item.commandId;
|
||||
return ResolvedItem(commandId: item.commandId, title: _stripCategory(raw), enabled: enabled, keybinding: label(item.commandId));
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user