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>
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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'),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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 {});
|
||||
|
||||
@@ -61,6 +61,8 @@ class OutputExtension extends ClideExtension {
|
||||
id: 'dock.toggle',
|
||||
command: 'dock.toggle',
|
||||
title: 'Toggle output dock',
|
||||
titleKey: 'command.dock.toggle',
|
||||
i18nNamespace: id,
|
||||
defaultBinding: 'ctrl+j',
|
||||
run: (_) async {
|
||||
final ctx = _ctx;
|
||||
|
||||
@@ -27,7 +27,15 @@ class SettingsUiExtension extends ClideExtension {
|
||||
List<ContributionPoint> get contributions => [
|
||||
// Opens the Settings panel. Palette + File-menu entry come for free off
|
||||
// the title; ctrl+, is the conventional settings shortcut.
|
||||
CommandContribution(id: 'settings.open', command: 'settings.open', title: 'Settings…', defaultBinding: 'ctrl+,', run: _open),
|
||||
CommandContribution(
|
||||
id: 'settings.open',
|
||||
command: 'settings.open',
|
||||
title: 'Settings…',
|
||||
titleKey: 'command.open',
|
||||
i18nNamespace: id,
|
||||
defaultBinding: 'ctrl+,',
|
||||
run: _open,
|
||||
),
|
||||
];
|
||||
|
||||
Future<IpcResponse> _open(List<String> args) async {
|
||||
|
||||
@@ -5,6 +5,15 @@ import 'package:flutter/widgets.dart';
|
||||
/// i18n namespace shared with the settings modal shell.
|
||||
const _settingsNs = 'builtin.settings-ui';
|
||||
|
||||
/// Resolve a schema label through the catalog under the owning category's
|
||||
/// [namespace], falling back to the English [english] label when the [key] is
|
||||
/// null or the [namespace] is null/empty (T-462). Short-circuiting before the
|
||||
/// lookup keeps key-less categories warning-free and pixel-identical.
|
||||
String _localized(BuildContext context, String? key, String? namespace, String english) {
|
||||
if (key == null || namespace == null || namespace.isEmpty) return english;
|
||||
return ClideSettings.i18n.string(context, key, namespace: namespace, placeholder: english);
|
||||
}
|
||||
|
||||
/// The schema-driven field renderer (T-448, epic T-444): turns a
|
||||
/// [SettingsCategory] into carded sections of field rows. Each field binds to a
|
||||
/// `SettingsStore` key — read with `get` (falling back to the schema default),
|
||||
@@ -28,7 +37,7 @@ class SettingsCategoryView extends StatelessWidget {
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [for (final section in category.sections) _SectionCard(section: section, store: store)],
|
||||
children: [for (final section in category.sections) _SectionCard(section: section, store: store, i18nNamespace: category.i18nNamespace)],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -37,6 +46,9 @@ class SettingsCategoryView extends StatelessWidget {
|
||||
|
||||
/// True when [field] matches [query] (case-insensitive; label or help). An
|
||||
/// empty query matches everything.
|
||||
///
|
||||
/// Context-free: matches the English label/help only — display localizes the
|
||||
/// shown text, but search-by-translation is out of scope (T-462).
|
||||
bool settingsFieldMatches(SettingsField field, String query) {
|
||||
final q = query.trim().toLowerCase();
|
||||
if (q.isEmpty) return true;
|
||||
@@ -49,7 +61,8 @@ List<SettingsSection> settingsMatchingSections(SettingsCategory category, String
|
||||
final out = <SettingsSection>[];
|
||||
for (final s in category.sections) {
|
||||
final fields = s.fields.where((f) => settingsFieldMatches(f, query)).toList();
|
||||
if (fields.isNotEmpty) out.add(SettingsSection(label: s.label, fields: fields));
|
||||
// Carry labelKey so search-result section headers still localize (T-462).
|
||||
if (fields.isNotEmpty) out.add(SettingsSection(label: s.label, fields: fields, labelKey: s.labelKey));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -82,11 +95,16 @@ class SettingsSearchResults extends StatelessWidget {
|
||||
blocks.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: blocks.isEmpty ? 0 : 14, bottom: 8),
|
||||
child: ClideText(c.title, fontSize: 13, fontWeight: FontWeight.w600, color: tokens.globalForeground),
|
||||
child: ClideText(
|
||||
_localized(context, c.titleKey, c.i18nNamespace, c.title),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: tokens.globalForeground,
|
||||
),
|
||||
),
|
||||
);
|
||||
for (final s in sections) {
|
||||
blocks.add(_SectionCard(section: s, store: store));
|
||||
blocks.add(_SectionCard(section: s, store: store, i18nNamespace: c.i18nNamespace));
|
||||
}
|
||||
}
|
||||
if (blocks.isEmpty) {
|
||||
@@ -112,24 +130,28 @@ class SettingsSearchResults extends StatelessWidget {
|
||||
|
||||
/// One section: a small-caps header above an elevated card of field rows.
|
||||
class _SectionCard extends StatelessWidget {
|
||||
const _SectionCard({required this.section, required this.store});
|
||||
const _SectionCard({required this.section, required this.store, this.i18nNamespace});
|
||||
|
||||
final SettingsSection section;
|
||||
final SettingsStore store;
|
||||
|
||||
/// Owning category's i18n namespace (T-462); null for key-less categories.
|
||||
final String? i18nNamespace;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideSettings.theme.of(context).surface;
|
||||
final sectionLabel = _localized(context, section.labelKey, i18nNamespace, section.label);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 18),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (section.label.isNotEmpty)
|
||||
if (sectionLabel.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 2, bottom: 6),
|
||||
child: ClideText(
|
||||
section.label.toUpperCase(),
|
||||
sectionLabel.toUpperCase(),
|
||||
fontSize: clideFontCaption,
|
||||
color: tokens.sidebarSectionHeader,
|
||||
fontFamily: ClideSettings.fonts.monoOf(context),
|
||||
@@ -145,7 +167,10 @@ class _SectionCard extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
for (var i = 0; i < section.fields.length; i++) ...[if (i > 0) const ClideDivider(), _FieldRow(field: section.fields[i], store: store)],
|
||||
for (var i = 0; i < section.fields.length; i++) ...[
|
||||
if (i > 0) const ClideDivider(),
|
||||
_FieldRow(field: section.fields[i], store: store, i18nNamespace: i18nNamespace),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -157,22 +182,27 @@ class _SectionCard extends StatelessWidget {
|
||||
|
||||
/// A label/help block + the field's control + a reset affordance.
|
||||
class _FieldRow extends StatelessWidget {
|
||||
const _FieldRow({required this.field, required this.store});
|
||||
const _FieldRow({required this.field, required this.store, this.i18nNamespace});
|
||||
|
||||
final SettingsField field;
|
||||
final SettingsStore store;
|
||||
|
||||
/// Owning category's i18n namespace (T-462); null for key-less categories.
|
||||
final String? i18nNamespace;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideSettings.theme.of(context).surface;
|
||||
final fieldLabel = _localized(context, field.labelKey, i18nNamespace, field.label);
|
||||
final fieldHelp = field.help == null ? null : _localized(context, field.helpKey, i18nNamespace, field.help!);
|
||||
final label = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClideText(field.label, color: tokens.globalForeground),
|
||||
if (field.help != null && field.help!.isNotEmpty)
|
||||
ClideText(fieldLabel, color: tokens.globalForeground),
|
||||
if (fieldHelp != null && fieldHelp.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: ClideText(field.help!, fontSize: clideFontCaption, color: tokens.globalTextMuted),
|
||||
child: ClideText(fieldHelp, fontSize: clideFontCaption, color: tokens.globalTextMuted),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -187,7 +217,7 @@ class _FieldRow extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (field.label.isNotEmpty) ...[label, const SizedBox(height: 10)],
|
||||
if (fieldLabel.isNotEmpty) ...[label, const SizedBox(height: 10)],
|
||||
builder?.call(context) ?? const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
@@ -203,9 +233,9 @@ class _FieldRow extends StatelessWidget {
|
||||
children: [
|
||||
Expanded(child: label),
|
||||
const SizedBox(width: 16),
|
||||
_Control(field: field, value: effective, store: store),
|
||||
_Control(field: field, value: effective, store: store, i18nNamespace: i18nNamespace),
|
||||
const SizedBox(width: 10),
|
||||
_ScopeTag(field: field, store: store, effectiveValue: effective),
|
||||
_ScopeTag(field: field, store: store, effectiveValue: effective, i18nNamespace: i18nNamespace),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -214,12 +244,15 @@ class _FieldRow extends StatelessWidget {
|
||||
|
||||
/// Dispatches to the control widget for the field's [SettingsFieldKind].
|
||||
class _Control extends StatelessWidget {
|
||||
const _Control({required this.field, required this.value, required this.store});
|
||||
const _Control({required this.field, required this.value, required this.store, this.i18nNamespace});
|
||||
|
||||
final SettingsField field;
|
||||
final Object? value;
|
||||
final SettingsStore store;
|
||||
|
||||
/// Owning category's i18n namespace (T-462); null for key-less categories.
|
||||
final String? i18nNamespace;
|
||||
|
||||
void _set(Object? v) => store.set<Object?>(field.key, v);
|
||||
|
||||
@override
|
||||
@@ -231,6 +264,7 @@ class _Control extends StatelessWidget {
|
||||
return _SelectControl(
|
||||
field: field,
|
||||
value: value?.toString(),
|
||||
i18nNamespace: i18nNamespace,
|
||||
onPick: (v) {
|
||||
final prefix = field.applyCommandPrefix;
|
||||
if (prefix != null) {
|
||||
@@ -242,11 +276,11 @@ class _Control extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
case SettingsFieldKind.text:
|
||||
return _EditControl(field: field, value: value?.toString() ?? '', numeric: false, onCommit: _set);
|
||||
return _EditControl(field: field, value: value?.toString() ?? '', numeric: false, i18nNamespace: i18nNamespace, onCommit: _set);
|
||||
case SettingsFieldKind.number:
|
||||
return _EditControl(field: field, value: value?.toString() ?? '', numeric: true, onCommit: _set);
|
||||
return _EditControl(field: field, value: value?.toString() ?? '', numeric: true, i18nNamespace: i18nNamespace, onCommit: _set);
|
||||
case SettingsFieldKind.file:
|
||||
return _FileControl(field: field);
|
||||
return _FileControl(field: field, i18nNamespace: i18nNamespace);
|
||||
case SettingsFieldKind.custom:
|
||||
// Custom fields are rendered full-width by _FieldRow; never here.
|
||||
return const SizedBox.shrink();
|
||||
@@ -287,12 +321,15 @@ class _ToggleControl extends StatelessWidget {
|
||||
|
||||
/// Enum picker — current value on an anchored popover of options.
|
||||
class _SelectControl extends StatefulWidget {
|
||||
const _SelectControl({required this.field, required this.value, required this.onPick});
|
||||
const _SelectControl({required this.field, required this.value, required this.onPick, this.i18nNamespace});
|
||||
|
||||
final SettingsField field;
|
||||
final String? value;
|
||||
final void Function(String value) onPick;
|
||||
|
||||
/// Owning category's i18n namespace (T-462); null for key-less categories.
|
||||
final String? i18nNamespace;
|
||||
|
||||
@override
|
||||
State<_SelectControl> createState() => _SelectControlState();
|
||||
}
|
||||
@@ -306,9 +343,10 @@ class _SelectControlState extends State<_SelectControl> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String get _label {
|
||||
/// Localized label for the currently selected option (or the raw value).
|
||||
String _selectedLabel(BuildContext context) {
|
||||
for (final o in widget.field.options) {
|
||||
if (o.value == widget.value) return o.label;
|
||||
if (o.value == widget.value) return _localized(context, o.labelKey, widget.i18nNamespace, o.label);
|
||||
}
|
||||
return widget.value ?? '';
|
||||
}
|
||||
@@ -316,6 +354,8 @@ class _SelectControlState extends State<_SelectControl> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideSettings.theme.of(context).surface;
|
||||
final fieldLabel = _localized(context, widget.field.labelKey, widget.i18nNamespace, widget.field.label);
|
||||
final selectedLabel = _selectedLabel(context);
|
||||
return ClideAnchoredOverlay(
|
||||
controller: _overlay,
|
||||
align: ClideAnchorAlign.end,
|
||||
@@ -324,16 +364,16 @@ class _SelectControlState extends State<_SelectControl> {
|
||||
entries: [
|
||||
for (final o in widget.field.options)
|
||||
ClideMenuItem(
|
||||
label: o.label,
|
||||
label: _localized(context, o.labelKey, widget.i18nNamespace, o.label),
|
||||
active: o.value == widget.value,
|
||||
semanticLabel: '${widget.field.label}: ${o.label}',
|
||||
semanticLabel: '$fieldLabel: ${_localized(context, o.labelKey, widget.i18nNamespace, o.label)}',
|
||||
onSelect: () => widget.onPick(o.value),
|
||||
),
|
||||
],
|
||||
),
|
||||
anchor: Semantics(
|
||||
button: true,
|
||||
label: '${widget.field.label}: $_label. Click to change.',
|
||||
label: '$fieldLabel: $selectedLabel. Click to change.',
|
||||
excludeSemantics: true,
|
||||
onTap: _overlay.toggle,
|
||||
child: ClideTappable(
|
||||
@@ -349,7 +389,7 @@ class _SelectControlState extends State<_SelectControl> {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ClideText(_label, color: tokens.globalForeground),
|
||||
ClideText(selectedLabel, color: tokens.globalForeground),
|
||||
const SizedBox(width: 6),
|
||||
ClideIcon(PhosphorIcons.byName('caret-down'), size: 10, color: tokens.globalTextMuted),
|
||||
],
|
||||
@@ -364,13 +404,16 @@ class _SelectControlState extends State<_SelectControl> {
|
||||
/// Inline text / numeric editor. Commits on Enter and on focus loss; numeric
|
||||
/// fields parse + clamp to the field's bounds, ignoring unparseable input.
|
||||
class _EditControl extends StatefulWidget {
|
||||
const _EditControl({required this.field, required this.value, required this.numeric, required this.onCommit});
|
||||
const _EditControl({required this.field, required this.value, required this.numeric, required this.onCommit, this.i18nNamespace});
|
||||
|
||||
final SettingsField field;
|
||||
final String value;
|
||||
final bool numeric;
|
||||
final void Function(Object value) onCommit;
|
||||
|
||||
/// Owning category's i18n namespace (T-462); null for key-less categories.
|
||||
final String? i18nNamespace;
|
||||
|
||||
@override
|
||||
State<_EditControl> createState() => _EditControlState();
|
||||
}
|
||||
@@ -430,7 +473,7 @@ class _EditControlState extends State<_EditControl> {
|
||||
final tokens = ClideSettings.theme.of(context).surface;
|
||||
return Semantics(
|
||||
textField: true,
|
||||
label: widget.field.label,
|
||||
label: _localized(context, widget.field.labelKey, widget.i18nNamespace, widget.field.label),
|
||||
excludeSemantics: true,
|
||||
child: Container(
|
||||
width: widget.numeric ? 88 : 180,
|
||||
@@ -458,15 +501,18 @@ class _EditControlState extends State<_EditControl> {
|
||||
|
||||
/// "Opens external file" affordance — a button that runs the field's command.
|
||||
class _FileControl extends StatelessWidget {
|
||||
const _FileControl({required this.field});
|
||||
const _FileControl({required this.field, this.i18nNamespace});
|
||||
|
||||
final SettingsField field;
|
||||
|
||||
/// Owning category's i18n namespace (T-462); null for key-less categories.
|
||||
final String? i18nNamespace;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final commands = ClideKernel.of(context).commands;
|
||||
return ClideButton(
|
||||
label: field.label,
|
||||
label: _localized(context, field.labelKey, i18nNamespace, field.label),
|
||||
variant: ClideButtonVariant.subtle,
|
||||
onPressed: field.fileCommand == null ? null : () => commands.execute(field.fileCommand!),
|
||||
);
|
||||
@@ -482,12 +528,15 @@ class _FileControl extends StatelessWidget {
|
||||
/// live in either file (project overrides app); `app.*`/`project.*` keys live
|
||||
/// only in their prefix's layer, so their menu offers that one scope + reset.
|
||||
class _ScopeTag extends StatefulWidget {
|
||||
const _ScopeTag({required this.field, required this.store, required this.effectiveValue});
|
||||
const _ScopeTag({required this.field, required this.store, required this.effectiveValue, this.i18nNamespace});
|
||||
|
||||
final SettingsField field;
|
||||
final SettingsStore store;
|
||||
final Object? effectiveValue;
|
||||
|
||||
/// Owning category's i18n namespace (T-462); null for key-less categories.
|
||||
final String? i18nNamespace;
|
||||
|
||||
@override
|
||||
State<_ScopeTag> createState() => _ScopeTagState();
|
||||
}
|
||||
@@ -547,6 +596,7 @@ class _ScopeTagState extends State<_ScopeTag> {
|
||||
final current = widget.store.effectiveLayer(widget.field.key);
|
||||
final look = _appearance(current);
|
||||
final layers = widget.store.writableLayers(widget.field.key);
|
||||
final fieldLabel = _localized(context, widget.field.labelKey, widget.i18nNamespace, widget.field.label);
|
||||
return ClideAnchoredOverlay(
|
||||
controller: _overlay,
|
||||
align: ClideAnchorAlign.end,
|
||||
@@ -560,7 +610,7 @@ class _ScopeTagState extends State<_ScopeTag> {
|
||||
),
|
||||
anchor: Semantics(
|
||||
button: true,
|
||||
label: '${widget.field.label} scope: ${look.label}',
|
||||
label: '$fieldLabel scope: ${look.label}',
|
||||
excludeSemantics: true,
|
||||
onTap: _overlay.toggle,
|
||||
child: ClideTappable(
|
||||
|
||||
@@ -244,10 +244,15 @@ class _RailRow extends StatelessWidget {
|
||||
final tokens = ClideSettings.theme.of(context).surface;
|
||||
final dim = matchCount == 0;
|
||||
final fg = dim ? tokens.globalTextMuted : (selected ? tokens.globalForeground : tokens.sidebarForeground);
|
||||
// Localize the rail label via the owning category's namespace (T-462);
|
||||
// key-less categories fall back to the English title unchanged.
|
||||
final title = (category.titleKey == null || category.i18nNamespace == null || category.i18nNamespace!.isEmpty)
|
||||
? category.title
|
||||
: ClideSettings.i18n.string(context, category.titleKey!, namespace: category.i18nNamespace!, placeholder: category.title);
|
||||
return Semantics(
|
||||
button: true,
|
||||
selected: selected,
|
||||
label: category.title,
|
||||
label: title,
|
||||
excludeSemantics: true,
|
||||
child: ClideTappable(
|
||||
cursor: SystemMouseCursors.click,
|
||||
@@ -267,7 +272,7 @@ class _RailRow extends StatelessWidget {
|
||||
children: [
|
||||
if (category.iconName != null) ...[ClideIcon(PhosphorIcons.byName(category.iconName!), size: 15, color: fg), const SizedBox(width: 8)],
|
||||
Expanded(
|
||||
child: ClideText(category.title, color: fg, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
child: ClideText(title, color: fg, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
if (matchCount != null) ClideText('$matchCount', fontSize: clideFontCaption, color: tokens.globalTextMuted),
|
||||
],
|
||||
|
||||
@@ -28,7 +28,15 @@ class ThemePickerExtension extends ClideExtension {
|
||||
// "Theme…" to disambiguate from the schema-driven Settings panel's
|
||||
// `settings.open` (T-444); the theme picker folds into that panel's
|
||||
// Appearance category in T-452.
|
||||
CommandContribution(id: 'theme.pick', command: 'theme.pick', title: 'Theme…', defaultBinding: 'ctrl+k', run: _pick),
|
||||
CommandContribution(
|
||||
id: 'theme.pick',
|
||||
command: 'theme.pick',
|
||||
title: 'Theme…',
|
||||
titleKey: 'command.pick',
|
||||
i18nNamespace: id,
|
||||
defaultBinding: 'ctrl+k',
|
||||
run: _pick,
|
||||
),
|
||||
// Always-visible switcher in the far-right status bar (T-234).
|
||||
// priority >= 100 places it in the right group; registered after
|
||||
// ipc-status so it sits to its right.
|
||||
@@ -36,49 +44,59 @@ class ThemePickerExtension extends ClideExtension {
|
||||
// Appearance settings category (T-452) — the theme picker as the schema
|
||||
// engine's one custom control, registered against the control registry.
|
||||
SettingsControlContribution(id: 'theme-picker.appearance-control', customId: 'theme.picker', builder: (_) => const AppearanceThemeControl()),
|
||||
const SettingsCategoryContribution(
|
||||
SettingsCategoryContribution(
|
||||
id: 'appearance',
|
||||
category: SettingsCategory(
|
||||
id: 'appearance',
|
||||
title: 'Appearance',
|
||||
titleKey: 'settings.appearance.title',
|
||||
i18nNamespace: id,
|
||||
iconName: 'palette',
|
||||
priority: 10,
|
||||
sections: [
|
||||
SettingsSection(
|
||||
label: 'Theme',
|
||||
labelKey: 'settings.appearance.section.theme',
|
||||
fields: [
|
||||
SettingsField(
|
||||
key: 'app.theme',
|
||||
kind: SettingsFieldKind.custom,
|
||||
label: 'Theme',
|
||||
labelKey: 'settings.appearance.field.theme.label',
|
||||
help: 'Color theme; high contrast switches to the accessible variant.',
|
||||
helpKey: 'settings.appearance.field.theme.help',
|
||||
customId: 'theme.picker',
|
||||
),
|
||||
],
|
||||
),
|
||||
SettingsSection(
|
||||
label: 'Typography',
|
||||
labelKey: 'settings.appearance.section.typography',
|
||||
fields: [
|
||||
SettingsField(
|
||||
key: kUiFontSettingKey,
|
||||
kind: SettingsFieldKind.select,
|
||||
label: 'UI font',
|
||||
labelKey: 'settings.appearance.field.uiFont.label',
|
||||
help: 'Typeface for the app interface; applies live.',
|
||||
helpKey: 'settings.appearance.field.uiFont.help',
|
||||
defaultValue: 'JosefinSans',
|
||||
options: [
|
||||
SettingsOption(value: 'JosefinSans', label: 'Josefin Sans'),
|
||||
SettingsOption(value: 'Inter', label: 'Inter'),
|
||||
SettingsOption(value: 'JosefinSans', label: 'Josefin Sans', labelKey: 'settings.appearance.field.uiFont.option.josefinSans'),
|
||||
SettingsOption(value: 'Inter', label: 'Inter', labelKey: 'settings.appearance.field.uiFont.option.inter'),
|
||||
],
|
||||
),
|
||||
SettingsField(
|
||||
key: kMonoFontSettingKey,
|
||||
kind: SettingsFieldKind.select,
|
||||
label: 'Monospace font',
|
||||
labelKey: 'settings.appearance.field.monoFont.label',
|
||||
help: 'Terminal, diffs, code, and IDs; applies live.',
|
||||
helpKey: 'settings.appearance.field.monoFont.help',
|
||||
defaultValue: 'JetBrainsMono',
|
||||
options: [
|
||||
SettingsOption(value: 'JetBrainsMono', label: 'JetBrains Mono'),
|
||||
SettingsOption(value: 'FiraMono', label: 'Fira Mono'),
|
||||
SettingsOption(value: 'JetBrainsMono', label: 'JetBrains Mono', labelKey: 'settings.appearance.field.monoFont.option.jetBrainsMono'),
|
||||
SettingsOption(value: 'FiraMono', label: 'Fira Mono', labelKey: 'settings.appearance.field.monoFont.option.firaMono'),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -30,6 +30,8 @@ class ViewExtension extends ClideExtension {
|
||||
id: 'view.zoomIn',
|
||||
command: 'view.zoomIn',
|
||||
title: 'View: Zoom In',
|
||||
titleKey: 'command.view.zoomIn',
|
||||
i18nNamespace: id,
|
||||
run: (_) async {
|
||||
textZoom.increase();
|
||||
return IpcResponse.ok(id: '', data: {'scale': textZoom.scale});
|
||||
@@ -39,6 +41,8 @@ class ViewExtension extends ClideExtension {
|
||||
id: 'view.zoomOut',
|
||||
command: 'view.zoomOut',
|
||||
title: 'View: Zoom Out',
|
||||
titleKey: 'command.view.zoomOut',
|
||||
i18nNamespace: id,
|
||||
run: (_) async {
|
||||
textZoom.decrease();
|
||||
return IpcResponse.ok(id: '', data: {'scale': textZoom.scale});
|
||||
@@ -48,6 +52,8 @@ class ViewExtension extends ClideExtension {
|
||||
id: 'view.zoomReset',
|
||||
command: 'view.zoomReset',
|
||||
title: 'View: Reset Zoom',
|
||||
titleKey: 'command.view.zoomReset',
|
||||
i18nNamespace: id,
|
||||
run: (_) async {
|
||||
textZoom.reset();
|
||||
return IpcResponse.ok(id: '', data: {'scale': textZoom.scale});
|
||||
|
||||
@@ -80,6 +80,8 @@ class VimExtension extends ClideExtension {
|
||||
id: id,
|
||||
command: id,
|
||||
title: title,
|
||||
titleKey: 'command.$id',
|
||||
i18nNamespace: this.id,
|
||||
run: (_) async {
|
||||
apply();
|
||||
return IpcResponse.ok(id: '', data: {'mode': _mode?.mode.name ?? 'disabled'});
|
||||
|
||||
@@ -26,6 +26,8 @@ class WelcomeExtension extends ClideExtension {
|
||||
id: 'workspace.open-project',
|
||||
command: 'workspace.open-project',
|
||||
title: 'Workspace: Open project…',
|
||||
titleKey: 'command.workspace.open-project',
|
||||
i18nNamespace: id,
|
||||
run: (_) async => IpcResponse.ok(id: '', data: const {'note': 'project picker lands in a later tier'}),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -165,5 +165,37 @@
|
||||
"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" }
|
||||
"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" }
|
||||
}
|
||||
@@ -3,5 +3,6 @@
|
||||
"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" }
|
||||
"button.open": { "translation": "Open" },
|
||||
"command.deeplink.invoke": { "translation": "Open a clide:// deep link" }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,21 @@
|
||||
{
|
||||
"command.reset": { "translation": "Layout: Reset to Classic" },
|
||||
"preset.classic": { "translation": "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" }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"notice.title": { "translation": "Extension management is coming" },
|
||||
"notice.body": { "translation": "Installing, enabling, and disabling extensions arrives with third-party (Lua) extension support. For now the built-in extensions are always on." },
|
||||
"notice.tracked": { "translation": "Tracked in T-8 (Tier 6) · D-16" }
|
||||
"notice.tracked": { "translation": "Tracked in T-8 (Tier 6) · D-16" },
|
||||
"settings.extensions.title": { "translation": "Extensions" },
|
||||
"settings.extensions.section.notice": { "translation": "" },
|
||||
"settings.extensions.field.notice.label": { "translation": "" }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,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" }
|
||||
}
|
||||
@@ -18,5 +18,9 @@
|
||||
"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." }
|
||||
"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" }
|
||||
}
|
||||
|
||||
@@ -10,5 +10,6 @@
|
||||
"a11y.jumpToLatest": { "translation": "jump to latest" },
|
||||
"jump.label": { "translation": "Jump to latest ↓" },
|
||||
"a11y.toggleDock": { "translation": "toggle output dock" },
|
||||
"dock.label": { "translation": "Output" }
|
||||
"dock.label": { "translation": "Output" },
|
||||
"command.dock.toggle": { "translation": "Toggle output dock" }
|
||||
}
|
||||
|
||||
@@ -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,9 +1,22 @@
|
||||
{
|
||||
"command.pick": { "translation": "Theme: Pick…" },
|
||||
"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" }
|
||||
"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" }
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"mode.normal": { "translation": "NORMAL" },
|
||||
"mode.insert": { "translation": "INSERT" },
|
||||
"mode.visual": { "translation": "VISUAL" }
|
||||
"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" }
|
||||
}
|
||||
|
||||
@@ -30,5 +30,6 @@
|
||||
"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." }
|
||||
"dialog.notRepo.body": { "translation": "A clide project root requires a git repository." },
|
||||
"command.workspace.open-project": { "translation": "Workspace: Open project…" }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user