18 Commits
Author SHA1 Message Date
jpmschweitzerandClaude Opus 4.8 c6db2fa7dc release v2.8.0
The i18n epic (T-462): every user-facing label — panels, dialogs, command
palette, menus, settings, framework chrome — now resolves through the i18n
catalog instead of a hardcoded string. Catalogs are bundled per locale under
assets/i18n/<locale>/<namespace>.json. Adds a live UI-language selector
(Settings → Appearance → Language) and a full Dutch (nl-NL) pack; English
stays the default. Architecture in D-102.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 09:54:56 +02:00
166 changed files with 4288 additions and 618 deletions
+26
View File
@@ -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)
+16
View File
@@ -5930,3 +5930,19 @@ 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 ('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;
+2
View File
@@ -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);
+129
View File
@@ -7250,3 +7250,132 @@ 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 ('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);
+16
View File
@@ -22,6 +22,22 @@ 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
+201
View File
@@ -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" }
}
+16
View File
@@ -0,0 +1,16 @@
{
"tab.title": { "translation": "Decisions" },
"tab.detail.title": { "translation": "Decision" },
"loading": { "translation": "Loading decisions..." },
"error.load": { "translation": "failed to load decisions" },
"empty": { "translation": "No decisions found.\nRun `pql decisions sync` to index." },
"filter.hint": { "translation": "Filter decisions…" },
"refresh.tooltip": { "translation": "Refresh decisions" },
"section.confirmed": { "translation": "CONFIRMED" },
"section.questions": { "translation": "QUESTIONS" },
"section.rejected": { "translation": "REJECTED" },
"badge.resolved": { "translation": "resolved" },
"detail.loading": { "translation": "Loading…" },
"detail.empty": { "translation": "Select a decision to view details." },
"detail.section.refs": { "translation": "CROSS-REFERENCES" }
}
+8
View File
@@ -0,0 +1,8 @@
{
"dialog.title": { "translation": "Open an external link?" },
"dialog.body": { "translation": "A clide:// link from outside the app is asking to:" },
"dialog.warning": { "translation": "Only allow this if you trust where the link came from." },
"button.cancel": { "translation": "Cancel" },
"button.open": { "translation": "Open" },
"command.deeplink.invoke": { "translation": "Open a clide:// deep link" }
}
@@ -0,0 +1,21 @@
{
"command.reset": { "translation": "Layout: Reset to Classic" },
"preset.classic": { "translation": "Classic" },
"command.palette.toggle": { "translation": "Command Palette" },
"command.sidebar.collapse": { "translation": "Toggle Sidebar Collapse" },
"command.context.collapse": { "translation": "Toggle Context Panel Collapse" },
"command.panel.focus.left": { "translation": "Focus Left Panel" },
"command.panel.focus.middle": { "translation": "Focus Middle Panel" },
"command.panel.focus.right": { "translation": "Focus Right Panel" },
"command.panel.focusMode": { "translation": "Toggle Focus Mode" },
"command.panel.focusMode.exit": { "translation": "Exit Focus Mode" },
"command.editor.open": { "translation": "Open Editor" },
"command.editor.close": { "translation": "Close Editor" },
"command.workspace.tab.next": { "translation": "Next Workspace Tab" },
"command.workspace.tab.previous": { "translation": "Previous Workspace Tab" },
"command.sidebar.section.1": { "translation": "Sidebar: Section 1" },
"command.sidebar.section.2": { "translation": "Sidebar: Section 2" },
"command.sidebar.section.3": { "translation": "Sidebar: Section 3" },
"command.sidebar.section.4": { "translation": "Sidebar: Section 4" },
"command.sidebar.section.5": { "translation": "Sidebar: Section 5" }
}
+15
View File
@@ -0,0 +1,15 @@
{
"tab.title": { "translation": "Diff" },
"view.semantics": { "translation": "diff view" },
"status.loading": { "translation": "Loading…" },
"empty.staged": { "translation": "No staged changes." },
"empty.unstaged": { "translation": "No unstaged changes." },
"toolbar.unstaged": { "translation": "Unstaged" },
"toolbar.unstaged.semantics": { "translation": "show unstaged changes" },
"toolbar.staged": { "translation": "Staged" },
"toolbar.staged.semantics": { "translation": "show staged changes" },
"meta.newFile": { "translation": "new file" },
"meta.deleted": { "translation": "deleted" },
"meta.renamedFrom": { "translation": "renamed from {path}" },
"meta.binary": { "translation": "binary" }
}
@@ -1,5 +1,7 @@
{
"tab.title": { "translation": "Editor" },
"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" }
}
@@ -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": "" }
}
+10
View File
@@ -0,0 +1,10 @@
{
"tab.title": { "translation": "Files" },
"loading": { "translation": "Loading…" },
"empty": { "translation": "No visible files" },
"filter.hint": { "translation": "Filter files…" },
"a11y.tree": { "translation": "file tree — {name}" },
"a11y.collapse": { "translation": "Collapse {name}" },
"a11y.expand": { "translation": "Expand {name}" },
"a11y.open": { "translation": "Open {name}" }
}
+38
View File
@@ -0,0 +1,38 @@
{
"tab.title": { "translation": "Git" },
"panel.semantics": { "translation": "git panel" },
"filter.hint": { "translation": "Filter changes…" },
"status.loading": { "translation": "Loading…" },
"status.clean": { "translation": "Nothing to commit, working tree clean." },
"group.conflicts": { "translation": "Merge conflicts" },
"group.staged": { "translation": "Staged" },
"group.changes": { "translation": "Changes" },
"group.untracked": { "translation": "Untracked" },
"action.unstageAll": { "translation": "Unstage all" },
"action.stageAll": { "translation": "Stage all" },
"commit.message.semantics": { "translation": "commit message" },
"commit.button": { "translation": "Commit" },
"commit.button.semantics": { "translation": "commit staged changes" },
"branch.detached": { "translation": "(detached)" },
"action.pull": { "translation": "Pull" },
"action.pull.semantics": { "translation": "git pull" },
"action.push": { "translation": "Push" },
"action.push.semantics": { "translation": "git push" },
"state.added": { "translation": "added" },
"state.modified": { "translation": "modified" },
"state.deleted": { "translation": "deleted" },
"state.renamed": { "translation": "renamed" },
"state.copied": { "translation": "copied" },
"state.untracked": { "translation": "untracked" },
"row.stage.semantics": { "translation": "stage {name}" },
"row.unstage.semantics": { "translation": "unstage {name}" },
"row.discard.semantics": { "translation": "discard changes to {name}" },
"discard.title": { "translation": "Discard changes?" },
"discard.body": { "translation": "Unstaged changes to {name} will be permanently lost." },
"button.cancel": { "translation": "Cancel" },
"button.discard": { "translation": "Discard" },
"branch.switch.semantics": { "translation": "switch branch — {branch}" },
"branchPicker.title": { "translation": "Switch branch" },
"branchPicker.empty": { "translation": "No branches found." },
"branchPicker.loadFailed": { "translation": "failed to load branches" }
}
@@ -0,0 +1,14 @@
{
"command.preset.default": { "translation": "Keymap: Default" },
"command.preset.vim": { "translation": "Keymap: Vim" },
"command.preset.vscode": { "translation": "Keymap: VS Code" },
"command.preset.jetbrains": { "translation": "Keymap: JetBrains" },
"settings.keymap.title": { "translation": "Keymap" },
"settings.keymap.section.preset": { "translation": "Preset" },
"settings.keymap.field.preset.label": { "translation": "Active preset" },
"settings.keymap.field.preset.help": { "translation": "Keyboard layout for the whole app." },
"settings.keymap.field.preset.option.default": { "translation": "Default" },
"settings.keymap.field.preset.option.vim": { "translation": "Vim" },
"settings.keymap.field.preset.option.vscode": { "translation": "VS Code" },
"settings.keymap.field.preset.option.jetbrains": { "translation": "JetBrains" }
}
+5
View File
@@ -0,0 +1,5 @@
{
"empty": { "translation": "Select a .md file to preview it here." },
"chrome.title": { "translation": "viewer" },
"subtitle.lines": { "translation": "{count} lines" }
}
+26
View File
@@ -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" }
}
+15
View File
@@ -0,0 +1,15 @@
{
"a11y.log": { "translation": "output log" },
"empty": { "translation": "No output yet." },
"empty.filtered": { "translation": "No output matches the filter." },
"filter.hint": { "translation": "Filter…" },
"chip.level": { "translation": "Level: {level}" },
"chip.source": { "translation": "Source: {source}" },
"chip.clear": { "translation": "Clear" },
"source.all": { "translation": "all" },
"a11y.jumpToLatest": { "translation": "jump to latest" },
"jump.label": { "translation": "Jump to latest ↓" },
"a11y.toggleDock": { "translation": "toggle output dock" },
"dock.label": { "translation": "Output" },
"command.dock.toggle": { "translation": "Toggle output dock" }
}
+16
View File
@@ -0,0 +1,16 @@
{
"tab.title": { "translation": "pql" },
"tab.links.title": { "translation": "Links" },
"filter.markdown.hint": { "translation": "Filter markdown…" },
"loading": { "translation": "Loading…" },
"empty.markdown": { "translation": "No markdown files found." },
"search.vault.hint": { "translation": "Search vault…" },
"search.query.hint": { "translation": "PQL query…" },
"backlinks.empty": { "translation": "Open a file to see its links." },
"backlinks.loading": { "translation": "Loading…" },
"backlinks.semantics": { "translation": "backlinks for {path}" },
"group.backlinks": { "translation": "Backlinks" },
"group.outlinks": { "translation": "Outlinks" },
"group.label": { "translation": "{label} ({count})" },
"group.none": { "translation": "None" }
}
+10
View File
@@ -0,0 +1,10 @@
{
"tab.title": { "translation": "Problems" },
"semantics.panel": { "translation": "problems panel" },
"filter.hint": { "translation": "Filter problems…" },
"count": { "translation": "Problems ({count})" },
"refresh.label": { "translation": "Refresh" },
"refresh.semantics": { "translation": "refresh problems" },
"scanning": { "translation": "Scanning…" },
"empty": { "translation": "No problems found." }
}
+23
View File
@@ -0,0 +1,23 @@
{
"mode.find": { "translation": "Find" },
"mode.vault": { "translation": "Vault" },
"mode.query": { "translation": "Query" },
"mode.markdown": { "translation": "Markdown" },
"find.hint": { "translation": "Search" },
"replace.hint": { "translation": "Replace" },
"include.hint": { "translation": "files to include (e.g. *.dart)" },
"exclude.hint": { "translation": "files to exclude" },
"toggle.regex": { "translation": "Regular expression" },
"toggle.caseInsensitive": { "translation": "Case insensitive" },
"status.searching": { "translation": "Searching…" },
"status.noResults": { "translation": "No results" },
"status.counts": { "translation": "{matches} in {files}" },
"a11y.openMatch": { "translation": "Open {path} line {line}" },
"button.replaceAll": { "translation": "Replace all" },
"button.ok": { "translation": "OK" },
"button.cancel": { "translation": "Cancel" },
"button.confirm": { "translation": "Confirm" },
"dialog.dirty.title": { "translation": "Working tree not clean" },
"dialog.dirty.body": { "translation": "Commit or stash your changes before replacing — git is the only undo." },
"dialog.confirm.body": { "translation": "Replace {matches} match(es) across {files} file(s)? This cannot be undone in clide." }
}
@@ -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" },
@@ -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." }
}
+36
View File
@@ -0,0 +1,36 @@
{
"tab.title": { "translation": "Tickets" },
"tab.detail.title": { "translation": "Ticket" },
"type.initiative": { "translation": "Initiative" },
"type.epic": { "translation": "Epic" },
"type.story": { "translation": "Story" },
"type.task": { "translation": "Task" },
"type.bug": { "translation": "Bug" },
"loading": { "translation": "Loading tickets..." },
"error.load": { "translation": "failed to load tickets" },
"empty": { "translation": "No tickets.\nRun `pql ticket new` to create one." },
"filter.hint": { "translation": "Filter tickets…" },
"refresh.tooltip": { "translation": "Refresh tickets" },
"section.in_progress": { "translation": "IN PROGRESS" },
"section.review": { "translation": "REVIEW" },
"section.ready": { "translation": "READY" },
"section.backlog": { "translation": "BACKLOG" },
"section.done": { "translation": "DONE" },
"section.cancelled": { "translation": "CANCELLED" },
"chip.label": { "translation": "{type} type filter" },
"chip.tooltip": { "translation": "Click to toggle · double-click to isolate" },
"badge.wip": { "translation": "WIP" },
"badge.review": { "translation": "REVIEW" },
"badge.cancelled": { "translation": "CANCELLED" },
"pickUp.tooltip": { "translation": "Pick up — hand this ticket to the Claude pane" },
"detail.loading": { "translation": "Loading…" },
"detail.empty": { "translation": "Select a ticket to view details." },
"detail.assigned": { "translation": "assigned: {name}" },
"detail.section.parents": { "translation": "PARENT TREE" },
"detail.section.decisions": { "translation": "REFERENCED DECISIONS" },
"status.backlog": { "translation": "BACKLOG" },
"status.ready": { "translation": "READY" },
"status.in_progress": { "translation": "WIP" },
"status.review": { "translation": "REVIEW" },
"status.done": { "translation": "DONE" }
}
+5
View File
@@ -0,0 +1,5 @@
{
"command.view.zoomIn": { "translation": "View: Zoom In" },
"command.view.zoomOut": { "translation": "View: Zoom Out" },
"command.view.zoomReset": { "translation": "View: Reset Zoom" }
}
+8
View File
@@ -0,0 +1,8 @@
{
"mode.normal": { "translation": "NORMAL" },
"mode.insert": { "translation": "INSERT" },
"mode.visual": { "translation": "VISUAL" },
"command.vim.mode.normal": { "translation": "Vim: Normal mode" },
"command.vim.mode.insert": { "translation": "Vim: Insert mode" },
"command.vim.mode.visual": { "translation": "Vim: Visual mode" }
}
+35
View File
@@ -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…" }
}
+27
View File
@@ -0,0 +1,27 @@
{
"collapser.expand": { "translation": "Expand" },
"collapser.collapse": { "translation": "Collapse" },
"collapser.expanded": { "translation": "expanded" },
"collapser.collapsed": { "translation": "collapsed" },
"toast.dismiss": { "translation": "Dismiss notification" },
"lightbox.close": { "translation": "close" },
"lightbox.hint": { "translation": "scroll to zoom · double-click to reset · Esc to close" },
"tab.new": { "translation": "New tab" },
"exline.notCommand": { "translation": "Not an editor command" },
"spine.expandSuffix": { "translation": "click to expand" },
"pane.close": { "translation": "Close pane" },
"pane.header": { "translation": "pane header: {title}" },
"reader.back": { "translation": "Back" },
"reader.forward": { "translation": "Forward" },
"reader.jumpToPin": { "translation": "Jump to pin" },
"reader.edit": { "translation": "Edit in editor" },
"reader.pin": { "translation": "Pin" },
"reader.unpin": { "translation": "Unpin" },
"link.openInEditor": { "translation": "Open in editor" },
"resize.axis.width": { "translation": "width" },
"resize.axis.height": { "translation": "height" },
"resize.sidebar": { "translation": "Sidebar {axis}" },
"resize.contextPanel": { "translation": "Context panel {axis}" },
"resize.slot": { "translation": "{slot} {axis}" },
"resize.pixels": { "translation": "{n} pixels" }
}
+201
View File
@@ -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" }
}
+16
View File
@@ -0,0 +1,16 @@
{
"tab.title": { "translation": "Beslissingen" },
"tab.detail.title": { "translation": "Beslissing" },
"loading": { "translation": "Beslissingen laden..." },
"error.load": { "translation": "laden van beslissingen mislukt" },
"empty": { "translation": "Geen beslissingen gevonden.\nVoer `pql decisions sync` uit om te indexeren." },
"filter.hint": { "translation": "Beslissingen filteren…" },
"refresh.tooltip": { "translation": "Beslissingen vernieuwen" },
"section.confirmed": { "translation": "BEVESTIGD" },
"section.questions": { "translation": "VRAGEN" },
"section.rejected": { "translation": "AFGEWEZEN" },
"badge.resolved": { "translation": "opgelost" },
"detail.loading": { "translation": "Laden…" },
"detail.empty": { "translation": "Selecteer een beslissing om de details te bekijken." },
"detail.section.refs": { "translation": "KRUISVERWIJZINGEN" }
}
+8
View File
@@ -0,0 +1,8 @@
{
"dialog.title": { "translation": "Een externe link openen?" },
"dialog.body": { "translation": "Een clide://-link van buiten de app vraagt om:" },
"dialog.warning": { "translation": "Sta dit alleen toe als je vertrouwt waar de link vandaan komt." },
"button.cancel": { "translation": "Annuleren" },
"button.open": { "translation": "Openen" },
"command.deeplink.invoke": { "translation": "Een clide://-deeplink openen" }
}
@@ -0,0 +1,21 @@
{
"command.reset": { "translation": "Indeling: Terugzetten naar Klassiek" },
"preset.classic": { "translation": "Klassiek" },
"command.palette.toggle": { "translation": "Opdrachtenpalet" },
"command.sidebar.collapse": { "translation": "Zijbalk samenvouwen aan/uit" },
"command.context.collapse": { "translation": "Contextpaneel samenvouwen aan/uit" },
"command.panel.focus.left": { "translation": "Focus op linkerpaneel" },
"command.panel.focus.middle": { "translation": "Focus op middelste paneel" },
"command.panel.focus.right": { "translation": "Focus op rechterpaneel" },
"command.panel.focusMode": { "translation": "Focusmodus aan/uit" },
"command.panel.focusMode.exit": { "translation": "Focusmodus afsluiten" },
"command.editor.open": { "translation": "Editor openen" },
"command.editor.close": { "translation": "Editor sluiten" },
"command.workspace.tab.next": { "translation": "Volgend werkruimtetabblad" },
"command.workspace.tab.previous": { "translation": "Vorig werkruimtetabblad" },
"command.sidebar.section.1": { "translation": "Zijbalk: Sectie 1" },
"command.sidebar.section.2": { "translation": "Zijbalk: Sectie 2" },
"command.sidebar.section.3": { "translation": "Zijbalk: Sectie 3" },
"command.sidebar.section.4": { "translation": "Zijbalk: Sectie 4" },
"command.sidebar.section.5": { "translation": "Zijbalk: Sectie 5" }
}
+15
View File
@@ -0,0 +1,15 @@
{
"tab.title": { "translation": "Diff" },
"view.semantics": { "translation": "diff-weergave" },
"status.loading": { "translation": "Laden…" },
"empty.staged": { "translation": "Geen gestagede wijzigingen." },
"empty.unstaged": { "translation": "Geen niet-gestagede wijzigingen." },
"toolbar.unstaged": { "translation": "Niet gestaged" },
"toolbar.unstaged.semantics": { "translation": "niet-gestagede wijzigingen tonen" },
"toolbar.staged": { "translation": "Gestaged" },
"toolbar.staged.semantics": { "translation": "gestagede wijzigingen tonen" },
"meta.newFile": { "translation": "nieuw bestand" },
"meta.deleted": { "translation": "verwijderd" },
"meta.renamedFrom": { "translation": "hernoemd vanaf {path}" },
"meta.binary": { "translation": "binair" }
}
+7
View File
@@ -0,0 +1,7 @@
{
"tab.title": { "translation": "Editor" },
"chrome.title": { "translation": "editor" },
"empty": { "translation": "Open een bestand om te beginnen met bewerken." },
"subtitle.no-buffer": { "translation": "geen buffer · gebruik `clide open <path>` of kies een bestand in de boom" },
"a11y.text-area": { "translation": "tekstgebied editor" }
}
@@ -0,0 +1,8 @@
{
"notice.title": { "translation": "Extensiebeheer komt eraan" },
"notice.body": { "translation": "Het installeren, in- en uitschakelen van extensies arriveert samen met ondersteuning voor externe (Lua-)extensies. Voorlopig staan de ingebouwde extensies altijd aan." },
"notice.tracked": { "translation": "Bijgehouden in T-8 (Tier 6) · D-16" },
"settings.extensions.title": { "translation": "Extensies" },
"settings.extensions.section.notice": { "translation": "" },
"settings.extensions.field.notice.label": { "translation": "" }
}
+10
View File
@@ -0,0 +1,10 @@
{
"tab.title": { "translation": "Bestanden" },
"loading": { "translation": "Laden…" },
"empty": { "translation": "Geen zichtbare bestanden" },
"filter.hint": { "translation": "Bestanden filteren…" },
"a11y.tree": { "translation": "bestandsboom — {name}" },
"a11y.collapse": { "translation": "{name} samenvouwen" },
"a11y.expand": { "translation": "{name} uitvouwen" },
"a11y.open": { "translation": "{name} openen" }
}
+38
View File
@@ -0,0 +1,38 @@
{
"tab.title": { "translation": "Git" },
"panel.semantics": { "translation": "git-paneel" },
"filter.hint": { "translation": "Wijzigingen filteren…" },
"status.loading": { "translation": "Laden…" },
"status.clean": { "translation": "Niets om vast te leggen, werkmap is schoon." },
"group.conflicts": { "translation": "Samenvoegconflicten" },
"group.staged": { "translation": "Klaargezet" },
"group.changes": { "translation": "Wijzigingen" },
"group.untracked": { "translation": "Niet gevolgd" },
"action.unstageAll": { "translation": "Alles terugnemen" },
"action.stageAll": { "translation": "Alles klaarzetten" },
"commit.message.semantics": { "translation": "commitbericht" },
"commit.button": { "translation": "Vastleggen" },
"commit.button.semantics": { "translation": "klaargezette wijzigingen vastleggen" },
"branch.detached": { "translation": "(losgekoppeld)" },
"action.pull": { "translation": "Pullen" },
"action.pull.semantics": { "translation": "git pull" },
"action.push": { "translation": "Pushen" },
"action.push.semantics": { "translation": "git push" },
"state.added": { "translation": "toegevoegd" },
"state.modified": { "translation": "gewijzigd" },
"state.deleted": { "translation": "verwijderd" },
"state.renamed": { "translation": "hernoemd" },
"state.copied": { "translation": "gekopieerd" },
"state.untracked": { "translation": "niet gevolgd" },
"row.stage.semantics": { "translation": "{name} klaarzetten" },
"row.unstage.semantics": { "translation": "{name} terugnemen" },
"row.discard.semantics": { "translation": "wijzigingen aan {name} ongedaan maken" },
"discard.title": { "translation": "Wijzigingen ongedaan maken?" },
"discard.body": { "translation": "Niet-klaargezette wijzigingen aan {name} gaan definitief verloren." },
"button.cancel": { "translation": "Annuleren" },
"button.discard": { "translation": "Ongedaan maken" },
"branch.switch.semantics": { "translation": "branch wisselen — {branch}" },
"branchPicker.title": { "translation": "Branch wisselen" },
"branchPicker.empty": { "translation": "Geen branches gevonden." },
"branchPicker.loadFailed": { "translation": "laden van branches mislukt" }
}
@@ -0,0 +1,6 @@
{
"connected": { "translation": "verbonden" },
"connected.hint": { "translation": "backend-isolate is bereikbaar" },
"disconnected": { "translation": "niet verbonden" },
"disconnected.hint": { "translation": "backend-isolate draait niet" }
}
@@ -0,0 +1,14 @@
{
"command.preset.default": { "translation": "Toetsindeling: Standaard" },
"command.preset.vim": { "translation": "Toetsindeling: Vim" },
"command.preset.vscode": { "translation": "Toetsindeling: VS Code" },
"command.preset.jetbrains": { "translation": "Toetsindeling: JetBrains" },
"settings.keymap.title": { "translation": "Toetsindeling" },
"settings.keymap.section.preset": { "translation": "Voorinstelling" },
"settings.keymap.field.preset.label": { "translation": "Actieve voorinstelling" },
"settings.keymap.field.preset.help": { "translation": "Toetsenbordindeling voor de hele app." },
"settings.keymap.field.preset.option.default": { "translation": "Standaard" },
"settings.keymap.field.preset.option.vim": { "translation": "Vim" },
"settings.keymap.field.preset.option.vscode": { "translation": "VS Code" },
"settings.keymap.field.preset.option.jetbrains": { "translation": "JetBrains" }
}
+5
View File
@@ -0,0 +1,5 @@
{
"empty": { "translation": "Selecteer een .md-bestand om het hier te bekijken." },
"chrome.title": { "translation": "weergave" },
"subtitle.lines": { "translation": "{count} regels" }
}
+26
View File
@@ -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" }
}
+15
View File
@@ -0,0 +1,15 @@
{
"a11y.log": { "translation": "uitvoerlogboek" },
"empty": { "translation": "Nog geen uitvoer." },
"empty.filtered": { "translation": "Geen uitvoer komt overeen met het filter." },
"filter.hint": { "translation": "Filteren…" },
"chip.level": { "translation": "Niveau: {level}" },
"chip.source": { "translation": "Bron: {source}" },
"chip.clear": { "translation": "Wissen" },
"source.all": { "translation": "alle" },
"a11y.jumpToLatest": { "translation": "naar nieuwste springen" },
"jump.label": { "translation": "Naar nieuwste ↓" },
"a11y.toggleDock": { "translation": "uitvoerdok aan/uit" },
"dock.label": { "translation": "Uitvoer" },
"command.dock.toggle": { "translation": "Uitvoerdok aan/uit" }
}
+16
View File
@@ -0,0 +1,16 @@
{
"tab.title": { "translation": "pql" },
"tab.links.title": { "translation": "Koppelingen" },
"filter.markdown.hint": { "translation": "Markdown filteren…" },
"loading": { "translation": "Laden…" },
"empty.markdown": { "translation": "Geen markdown-bestanden gevonden." },
"search.vault.hint": { "translation": "Vault doorzoeken…" },
"search.query.hint": { "translation": "PQL-query…" },
"backlinks.empty": { "translation": "Open een bestand om de koppelingen te zien." },
"backlinks.loading": { "translation": "Laden…" },
"backlinks.semantics": { "translation": "backlinks voor {path}" },
"group.backlinks": { "translation": "Backlinks" },
"group.outlinks": { "translation": "Uitgaande koppelingen" },
"group.label": { "translation": "{label} ({count})" },
"group.none": { "translation": "Geen" }
}
+10
View File
@@ -0,0 +1,10 @@
{
"tab.title": { "translation": "Problemen" },
"semantics.panel": { "translation": "problemenpaneel" },
"filter.hint": { "translation": "Problemen filteren…" },
"count": { "translation": "Problemen ({count})" },
"refresh.label": { "translation": "Vernieuwen" },
"refresh.semantics": { "translation": "problemen vernieuwen" },
"scanning": { "translation": "Scannen…" },
"empty": { "translation": "Geen problemen gevonden." }
}
+23
View File
@@ -0,0 +1,23 @@
{
"mode.find": { "translation": "Zoeken" },
"mode.vault": { "translation": "Kluis" },
"mode.query": { "translation": "Query" },
"mode.markdown": { "translation": "Markdown" },
"find.hint": { "translation": "Zoeken" },
"replace.hint": { "translation": "Vervangen" },
"include.hint": { "translation": "te includeren bestanden (bijv. *.dart)" },
"exclude.hint": { "translation": "uit te sluiten bestanden" },
"toggle.regex": { "translation": "Reguliere expressie" },
"toggle.caseInsensitive": { "translation": "Hoofdletterongevoelig" },
"status.searching": { "translation": "Zoeken…" },
"status.noResults": { "translation": "Geen resultaten" },
"status.counts": { "translation": "{matches} in {files}" },
"a11y.openMatch": { "translation": "{path} regel {line} openen" },
"button.replaceAll": { "translation": "Alles vervangen" },
"button.ok": { "translation": "OK" },
"button.cancel": { "translation": "Annuleren" },
"button.confirm": { "translation": "Bevestigen" },
"dialog.dirty.title": { "translation": "Werkboom niet schoon" },
"dialog.dirty.body": { "translation": "Commit of stash je wijzigingen voordat je vervangt — Git is de enige manier om ongedaan te maken." },
"dialog.confirm.body": { "translation": "{matches} overeenkomst(en) in {files} bestand(en) vervangen? Dit kan niet ongedaan worden gemaakt in clide." }
}
@@ -0,0 +1,17 @@
{
"command.open": { "translation": "Instellingen…" },
"modal.title": { "translation": "Instellingen" },
"modal.close": { "translation": "Sluiten" },
"modal.close.hint": { "translation": "Sluit instellingen zonder iets te wijzigen" },
"rail.header": { "translation": "Categorieën" },
"panel.empty": { "translation": "Er zijn nog geen instellingscategorieën geregistreerd." },
"search.hint": { "translation": "Instellingen zoeken…" },
"search.empty": { "translation": "Geen instellingen komen overeen met je zoekopdracht." },
"scope.project": { "translation": "Dit project" },
"scope.always": { "translation": "Heel clide" },
"scope.default": { "translation": "Standaard" },
"scope.reset": { "translation": "Terugzetten naar standaard" },
"scope.tip.project": { "translation": "Opgeslagen in dit project (.clide)" },
"scope.tip.always": { "translation": "Opgeslagen voor heel clide (~/.clide)" },
"scope.tip.default": { "translation": "Niet ingesteld — standaard wordt gebruikt" }
}
+9
View File
@@ -0,0 +1,9 @@
{
"tab.title": { "translation": "Terminal" },
"chrome.title": { "translation": "terminal" },
"subtitle.spawning": { "translation": "shell starten…" },
"subtitle.exited": { "translation": "Shell afgesloten." },
"error.unavailable": { "translation": "Terminal niet beschikbaar" },
"error.daemon": { "translation": "Backend niet verbonden." },
"a11y.label": { "translation": "terminal — {subtitle}" }
}
@@ -0,0 +1,25 @@
{
"command.pick": { "translation": "Thema…" },
"modal.title": { "translation": "Thema selecteren" },
"modal.cancel": { "translation": "Annuleren" },
"modal.cancel.hint": { "translation": "Sluit de themakiezer zonder het huidige thema te wijzigen" },
"row.select.hint": { "translation": "Dit thema activeren" },
"section.appearance": { "translation": "Weergave" },
"toggle.highContrast": { "translation": "Hoog contrast" },
"settings.appearance.title": { "translation": "Weergave" },
"settings.appearance.section.theme": { "translation": "Thema" },
"settings.appearance.section.typography": { "translation": "Typografie" },
"settings.appearance.field.theme.label": { "translation": "Thema" },
"settings.appearance.field.theme.help": { "translation": "Kleurthema; hoog contrast schakelt over naar de toegankelijke variant." },
"settings.appearance.field.uiFont.label": { "translation": "UI-lettertype" },
"settings.appearance.field.uiFont.help": { "translation": "Lettertype voor de app-interface; wordt direct toegepast." },
"settings.appearance.field.uiFont.option.josefinSans": { "translation": "Josefin Sans" },
"settings.appearance.field.uiFont.option.inter": { "translation": "Inter" },
"settings.appearance.field.monoFont.label": { "translation": "Monospace-lettertype" },
"settings.appearance.field.monoFont.help": { "translation": "Terminal, diffs, code en ID's; wordt direct toegepast." },
"settings.appearance.field.monoFont.option.jetBrainsMono": { "translation": "JetBrains Mono" },
"settings.appearance.field.monoFont.option.firaMono": { "translation": "Fira Mono" },
"settings.appearance.section.language": { "translation": "Taal" },
"settings.appearance.field.language.label": { "translation": "Taal" },
"settings.appearance.field.language.help": { "translation": "Taal voor de app-interface; wordt direct toegepast." }
}
+36
View File
@@ -0,0 +1,36 @@
{
"tab.title": { "translation": "Tickets" },
"tab.detail.title": { "translation": "Ticket" },
"type.initiative": { "translation": "Initiatief" },
"type.epic": { "translation": "Epic" },
"type.story": { "translation": "Story" },
"type.task": { "translation": "Taak" },
"type.bug": { "translation": "Bug" },
"loading": { "translation": "Tickets laden..." },
"error.load": { "translation": "laden van tickets mislukt" },
"empty": { "translation": "Geen tickets.\nVoer `pql ticket new` uit om er een aan te maken." },
"filter.hint": { "translation": "Tickets filteren…" },
"refresh.tooltip": { "translation": "Tickets vernieuwen" },
"section.in_progress": { "translation": "IN BEHANDELING" },
"section.review": { "translation": "BEOORDELING" },
"section.ready": { "translation": "GEREED" },
"section.backlog": { "translation": "BACKLOG" },
"section.done": { "translation": "KLAAR" },
"section.cancelled": { "translation": "GEANNULEERD" },
"chip.label": { "translation": "filter op type {type}" },
"chip.tooltip": { "translation": "Klik om te wisselen · dubbelklik om te isoleren" },
"badge.wip": { "translation": "WIP" },
"badge.review": { "translation": "BEOORDELING" },
"badge.cancelled": { "translation": "GEANNULEERD" },
"pickUp.tooltip": { "translation": "Oppakken — geef dit ticket door aan het Claude-paneel" },
"detail.loading": { "translation": "Laden…" },
"detail.empty": { "translation": "Selecteer een ticket om de details te bekijken." },
"detail.assigned": { "translation": "toegewezen aan: {name}" },
"detail.section.parents": { "translation": "BOVENLIGGENDE BOOM" },
"detail.section.decisions": { "translation": "GEREFEREERDE BESLISSINGEN" },
"status.backlog": { "translation": "BACKLOG" },
"status.ready": { "translation": "GEREED" },
"status.in_progress": { "translation": "WIP" },
"status.review": { "translation": "BEOORDELING" },
"status.done": { "translation": "KLAAR" }
}
+5
View File
@@ -0,0 +1,5 @@
{
"command.view.zoomIn": { "translation": "Weergave: Inzoomen" },
"command.view.zoomOut": { "translation": "Weergave: Uitzoomen" },
"command.view.zoomReset": { "translation": "Weergave: Zoom resetten" }
}
+8
View File
@@ -0,0 +1,8 @@
{
"mode.normal": { "translation": "NORMAL" },
"mode.insert": { "translation": "INSERT" },
"mode.visual": { "translation": "VISUAL" },
"command.vim.mode.normal": { "translation": "Vim: Normale modus" },
"command.vim.mode.insert": { "translation": "Vim: Invoegmodus" },
"command.vim.mode.visual": { "translation": "Vim: Visuele modus" }
}
+35
View File
@@ -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…" }
}
+27
View File
@@ -0,0 +1,27 @@
{
"collapser.expand": { "translation": "Uitvouwen" },
"collapser.collapse": { "translation": "Samenvouwen" },
"collapser.expanded": { "translation": "uitgevouwen" },
"collapser.collapsed": { "translation": "samengevouwen" },
"toast.dismiss": { "translation": "Melding sluiten" },
"lightbox.close": { "translation": "sluiten" },
"lightbox.hint": { "translation": "scrollen om te zoomen · dubbelklik om te resetten · Esc om te sluiten" },
"tab.new": { "translation": "Nieuw tabblad" },
"exline.notCommand": { "translation": "Geen editoropdracht" },
"spine.expandSuffix": { "translation": "klik om uit te vouwen" },
"pane.close": { "translation": "Paneel sluiten" },
"pane.header": { "translation": "paneelkop: {title}" },
"reader.back": { "translation": "Terug" },
"reader.forward": { "translation": "Vooruit" },
"reader.jumpToPin": { "translation": "Naar speld springen" },
"reader.edit": { "translation": "Bewerken in editor" },
"reader.pin": { "translation": "Vastmaken" },
"reader.unpin": { "translation": "Losmaken" },
"link.openInEditor": { "translation": "Openen in editor" },
"resize.axis.width": { "translation": "breedte" },
"resize.axis.height": { "translation": "hoogte" },
"resize.sidebar": { "translation": "Zijbalk {axis}" },
"resize.contextPanel": { "translation": "Contextpaneel {axis}" },
"resize.slot": { "translation": "{slot} {axis}" },
"resize.pixels": { "translation": "{n} pixels" }
}
+1 -1
View File
@@ -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.1"
version: "2.8.0"
homepage: https://github.com/postmeridiem/clide
license: MIT
license_file: assets/LICENSE
+1
View File
@@ -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
+11
View File
@@ -32,4 +32,15 @@ A11y + i18n are Tier-0 contracts, not Tier-6 polish.
- **Cost:** Two extra theme files per "named" theme when we add a11y variants. The bundled-theme contrast gate ([D-22](#d-22-wcag-aa-contrast-gate-on-bundled-themes)) needs a baseline/extended split so the named themes don't fail the strict pairs.
- **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).
---
+16 -2
View File
@@ -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,
),
],
),
);
+32 -9
View File
@@ -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),
+10 -2
View File
@@ -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),
+57 -7
View File
@@ -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));
}
+78 -38
View File
@@ -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),
+51 -5
View File
@@ -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) {
+7 -1
View File
@@ -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,
+140 -30
View File
@@ -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)
+10 -2
View File
@@ -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(
+54 -10
View File
@@ -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),
],
+29 -9
View File
@@ -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),
),
),
],
],
+2
View File
@@ -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(),
),
+37 -6
View File
@@ -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),
),
],
),
],
+83 -8
View File
@@ -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),
),
+41 -11
View File
@@ -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,
+14 -4
View File
@@ -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(
+13 -2
View File
@@ -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',
),
],
),
],
),
+38 -5
View File
@@ -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),
);
+126 -32
View File
@@ -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,
),
],
),
],
+22 -5
View File
@@ -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(
+12 -5
View File
@@ -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'),
],
),
],
+15 -3
View File
@@ -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(
+38 -7
View File
@@ -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(
+8
View File
@@ -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 {});
+49 -8
View File
@@ -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(),
),
],
),
],
),
+22 -2
View File
@@ -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);
}
}
+8 -2
View File
@@ -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