diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8dda82ad..89e2b34e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file.
- **Faster session switching in large workspaces** (thanks @c-w-xiaohei): switching sessions no longer rebuilds the whole sidebar, returning to a recently viewed session restores its rendered messages instead of re-rendering them (file links included), and scrolling long conversations costs less. In a workspace with thousands of loaded sessions, end-to-end switch time dropped by roughly half.
- **Session tabs (opt-in):** the web/desktop header can show your open sessions as browser-style tabs — turn them on in Settings → General → Navigation → Session tabs. Every session you open joins the strip, clicking a tab switches the whole workspace (chat, project, panels), and closing one (its × button, middle-click, or Alt+W — rebindable in Shortcuts) never touches the session itself. Tabs reorder by drag, scroll behind the header buttons when there are many and carry the sidebar's running/unread dot. Each tab has the full session menu — on the "..." button or right-click — plus Close other tabs; renaming works right in the tab.
- **Keyboard shortcuts learned sequences** (thanks @ChangeHow): shortcuts can now be two steps — press Cmd/Ctrl+S, then a letter: P opens the draft's project picker, G its branch picker, L the session list. A held sequence shows no menu but forgives you: Escape or three quiet seconds cancel it, and typing into a text field never triggers one armed elsewhere. Dropdown menus and pickers now also answer Ctrl+N/Ctrl+P for down/up, the session switcher opens focused on your current session, and every tooltip and menu label shows the binding you actually have set, not the default.
+- **Keyboard shortcuts redesigned.** Defaults now follow one model: single chords for everyday actions, a two-step Cmd/Ctrl+K leader for open/go actions (K then P/G/L — project picker, branch picker, session list; T timeline, N prompt navigator, I services, H shortcut help, C theme), held Cmd/Ctrl+digit for header session tabs and held Cmd/Ctrl+Option+digit for context panel surfaces. Cmd/Ctrl+B now toggles the sidebar; when it's already open, the session-list shortcut jumps into its search. Rare actions moved into the command palette instead of carrying obscure default bindings, and custom bindings recorded under the old layout are reset once. Shortcuts also stopped requiring an English keyboard layout — bindings follow the physical key on non-Latin layouts (and Option-modified digits on macOS), including when recording custom ones.
- Chat: comment on a reply — select text in a chat message (or in a rendered markdown preview in Files) and choose Comment to attach exactly that quote, with a source line range when it can be located, plus your note to the next message. The selection stays highlighted while you type, and the selection menu was restyled — Add to chat is now Add to input.
- Diff: comment like a review — hovering a line shows a + button in the gutter; clicking it, clicking a line, or dragging across lines opens the comment editor for that line or range. The comment editor and saved-comment cards match the chat's comment style.
- Composer: hovering or tapping a context chip above the input opens a stacked preview of everything attached, where a comment can be edited in place or an item removed before sending.
diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx
index b9d90cc4..9cd9e831 100644
--- a/packages/ui/src/App.tsx
+++ b/packages/ui/src/App.tsx
@@ -20,7 +20,6 @@ import { useAgentMemorySync } from '@/hooks/useAgentMemorySync';
import { usePwaInstallPrompt } from '@/hooks/usePwaInstallPrompt';
import { useWindowTitle } from '@/hooks/useWindowTitle';
import { useConfigStore } from '@/stores/useConfigStore';
-import { useKeybind } from '@/hooks/useKeybind';
import { isDesktopLocalOriginActive, isDesktopShell, restartDesktopApp, invokeDesktop } from '@/lib/desktop';
import {
getInjectedBootOutcome,
@@ -723,10 +722,13 @@ function App({ apis }: AppProps) {
useSessionStatusBootstrap({ enabled: embeddedBackgroundWorkEnabled });
- useKeybind('toggle_memory_debug', () => {
- if (embeddedSessionChat) return false;
- setShowMemoryDebug((previous) => !previous);
- });
+ // Palette-only action: the memory debug panel has no keyboard shortcut.
+ React.useEffect(() => {
+ if (embeddedSessionChat) return;
+ const handleToggle = () => setShowMemoryDebug((previous) => !previous);
+ window.addEventListener('openchamber:memory-debug-toggle', handleToggle);
+ return () => window.removeEventListener('openchamber:memory-debug-toggle', handleToggle);
+ }, [embeddedSessionChat]);
React.useEffect(() => {
if (embeddedSessionChat) {
diff --git a/packages/ui/src/components/layout/Header.tsx b/packages/ui/src/components/layout/Header.tsx
index 3c56891d..09fad963 100644
--- a/packages/ui/src/components/layout/Header.tsx
+++ b/packages/ui/src/components/layout/Header.tsx
@@ -1471,17 +1471,6 @@ export const Header: React.FC = () => {
setIsDesktopServicesOpen(true);
void refreshCurrentInstanceLabel();
},
- // The desktop menu holds one destination now, so this shortcut opens it
- // rather than cycling. The binding is kept: it is user-configurable and
- // silently dropping it would break existing setups.
- cycle_services_tab: () => {
- if (servicesTabs.length === 0) return false;
- setIsDesktopServicesOpen(true);
- void refreshCurrentInstanceLabel();
- },
- toggle_context_plan: () => {
- handleOpenContextPlan();
- },
});
const desktopSidebarActions = (
diff --git a/packages/ui/src/components/sections/openchamber/KeyboardShortcutsSettings.tsx b/packages/ui/src/components/sections/openchamber/KeyboardShortcutsSettings.tsx
index bd36e40d..4fd8f752 100644
--- a/packages/ui/src/components/sections/openchamber/KeyboardShortcutsSettings.tsx
+++ b/packages/ui/src/components/sections/openchamber/KeyboardShortcutsSettings.tsx
@@ -53,17 +53,19 @@ export const KeyboardShortcutsSettings: React.FC = () => {
persist(nextOverrides);
};
const shortcutDisplay = (action: CustomizableShortcutAction): string => {
- const isSurfaceSwitch = action.id === 'switch_context_surface';
- const combo = isSurfaceSwitch
+ const isPrefixStyle = 'prefixStyle' in action && action.prefixStyle;
+ const combo = isPrefixStyle
? getEffectiveShortcutPrefix(action.id, shortcutOverrides)
: getEffectiveShortcutCombo(action.id, shortcutOverrides);
const formatted = formatShortcutForDisplay(
combo,
t('settings.openchamber.keyboardShortcuts.unassigned'),
);
- return isSurfaceSwitch && combo && combo !== UNASSIGNED_SHORTCUT
- ? `${formatted}${t('settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix')}`
- : formatted;
+ if (!isPrefixStyle || !combo || combo === UNASSIGNED_SHORTCUT) return formatted;
+ const suffix = action.id === 'switch_session_tab'
+ ? t('settings.openchamber.keyboardShortcuts.action.switch_session_tab.suffix')
+ : t('settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix');
+ return `${formatted}${suffix}`;
};
return (
@@ -104,15 +106,17 @@ export const KeyboardShortcutsSettings: React.FC = () => {
>
{t('settings.openchamber.keyboardShortcuts.actions.edit')}
-
+ {action.id in shortcutOverrides ? (
+
+ ) : null}
))}
diff --git a/packages/ui/src/components/sections/openchamber/ShortcutRecordingDialog.test.ts b/packages/ui/src/components/sections/openchamber/ShortcutRecordingDialog.test.ts
index 256383a1..b0cce183 100644
--- a/packages/ui/src/components/sections/openchamber/ShortcutRecordingDialog.test.ts
+++ b/packages/ui/src/components/sections/openchamber/ShortcutRecordingDialog.test.ts
@@ -4,7 +4,8 @@ import { settleShortcutRecordingState, updateShortcutRecordingState } from './Sh
const emptyState = { chords: [], livePreview: null, settled: false };
function keyEvent(key: string, modifiers: Partial> = {}) {
- return { key, repeat: false, isComposing: false, altKey: false, ctrlKey: false, metaKey: false, shiftKey: false, ...modifiers };
+ const code = /^[a-z]$/i.test(key) ? `Key${key.toUpperCase()}` : /^[0-9]$/.test(key) ? `Digit${key}` : key;
+ return { key, code, repeat: false, isComposing: false, altKey: false, ctrlKey: false, metaKey: false, shiftKey: false, ...modifiers };
}
describe('ShortcutRecordingDialog recording state', () => {
diff --git a/packages/ui/src/components/sections/openchamber/ShortcutRecordingDialog.tsx b/packages/ui/src/components/sections/openchamber/ShortcutRecordingDialog.tsx
index a04fb8a1..a75d2333 100644
--- a/packages/ui/src/components/sections/openchamber/ShortcutRecordingDialog.tsx
+++ b/packages/ui/src/components/sections/openchamber/ShortcutRecordingDialog.tsx
@@ -13,6 +13,7 @@ import {
getShortcutBindingConflicts,
isRiskyBrowserShortcut,
keyToShortcutToken,
+ resolveShortcutEventKey,
normalizeCombo,
type ShortcutActionId,
type ShortcutBindingConflict,
@@ -27,6 +28,7 @@ const SECOND_CHORD_TIMEOUT_MS = 3000;
interface RecordingKeyboardEvent {
altKey: boolean;
+ code: string;
ctrlKey: boolean;
isComposing: boolean;
key: string;
@@ -84,7 +86,7 @@ function keyboardEventToCombo(event: RecordingKeyboardEvent): ShortcutCombo | nu
if (MODIFIER_KEYS.has(event.key.toLowerCase())) return null;
if (getPhysicalKeyCount(event, true) > MAX_SHORTCUT_KEY_COUNT) return null;
- const key = keyToShortcutToken(event.key);
+ const key = keyToShortcutToken(resolveShortcutEventKey(event));
if (!key) return null;
const parts: string[] = [];
@@ -200,7 +202,8 @@ export const ShortcutRecordingDialog: React.FC = (
event.preventDefault();
event.stopPropagation();
- if (phase === 'keyup' && action?.id === 'switch_context_surface' && recording.chords.length === 0) {
+ const isPrefixStyleAction = Boolean(action && 'prefixStyle' in action && action.prefixStyle);
+ if (phase === 'keyup' && isPrefixStyleAction && recording.chords.length === 0) {
const modifierCombo = modifierKeyUpToCombo(event);
if (modifierCombo) {
setRecording({ chords: [modifierCombo], livePreview: null, settled: true });
@@ -209,6 +212,7 @@ export const ShortcutRecordingDialog: React.FC = (
}
const nextRecording = updateShortcutRecordingState(recording, {
altKey: event.altKey,
+ code: event.nativeEvent.code,
ctrlKey: event.ctrlKey,
isComposing: event.nativeEvent.isComposing,
key: event.key,
@@ -216,7 +220,7 @@ export const ShortcutRecordingDialog: React.FC = (
repeat: event.repeat,
shiftKey: event.shiftKey,
}, phase);
- setRecording(action?.id === 'switch_context_surface' && nextRecording.chords.length > 1
+ setRecording(isPrefixStyleAction && nextRecording.chords.length > 1
? recording
: nextRecording);
};
diff --git a/packages/ui/src/components/session/sidebar/shell/useSessionSearchEffects.ts b/packages/ui/src/components/session/sidebar/shell/useSessionSearchEffects.ts
index a4543458..439d514e 100644
--- a/packages/ui/src/components/session/sidebar/shell/useSessionSearchEffects.ts
+++ b/packages/ui/src/components/session/sidebar/shell/useSessionSearchEffects.ts
@@ -26,6 +26,21 @@ export const useSessionSearchEffects = ({
return () => window.cancelAnimationFrame(raf);
}, [enabled, isSessionSearchOpen, sessionSearchInputRef]);
+ // The open_session_list shortcut lands here when the sidebar is visible:
+ // the session list is already on screen, so the shortcut opens its search.
+ React.useEffect(() => {
+ if (!enabled || typeof window === 'undefined') {
+ return;
+ }
+ const handleOpenRequest = () => {
+ setIsSessionSearchOpen(true);
+ sessionSearchInputRef.current?.focus();
+ sessionSearchInputRef.current?.select();
+ };
+ window.addEventListener('openchamber:sidebar-session-search', handleOpenRequest);
+ return () => window.removeEventListener('openchamber:sidebar-session-search', handleOpenRequest);
+ }, [enabled, setIsSessionSearchOpen, sessionSearchInputRef]);
+
React.useEffect(() => {
if (!enabled || !isSessionSearchOpen || typeof document === 'undefined') {
return;
diff --git a/packages/ui/src/components/ui/CommandPalette.tsx b/packages/ui/src/components/ui/CommandPalette.tsx
index 77a111e6..3acc62b4 100644
--- a/packages/ui/src/components/ui/CommandPalette.tsx
+++ b/packages/ui/src/components/ui/CommandPalette.tsx
@@ -37,7 +37,8 @@ import { toast } from '@/components/ui';
import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
import type { Session } from '@opencode-ai/sdk/v2';
import { createWorktreeSession } from '@/lib/worktreeSessionCreator';
-import { formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts';
+import { formatShortcutForDisplay, getEffectiveShortcutCombo, shortcutRegistry } from '@/lib/shortcuts';
+import { showOpenCodeStatus } from '@/lib/openCodeStatus';
import { canUseElectronDesktopIPC, invokeDesktop, isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
import { SETTINGS_PAGE_METADATA, type SettingsRuntimeContext } from '@/lib/settings/metadata';
@@ -230,6 +231,25 @@ export const CommandPalette: React.FC = () => {
if (currentDirectory) openContextOverview(currentDirectory);
}),
},
+ {
+ id: 'cycle-theme',
+ title: t('commandPalette.item.cycleTheme'),
+ icon: ,
+ shortcutId: 'cycle_theme',
+ searchText: t('commandPalette.item.cycleTheme'),
+ onSelect: run(() => {
+ shortcutRegistry.invoke('cycle_theme');
+ }),
+ },
+ {
+ id: 'open-status',
+ title: t('commandPalette.item.showOpenCodeStatus'),
+ icon: ,
+ searchText: t('commandPalette.item.showOpenCodeStatus'),
+ onSelect: run(() => {
+ void showOpenCodeStatus();
+ }),
+ },
{
id: 'open-settings',
title: t('commandPalette.item.openSettings'),
@@ -239,6 +259,15 @@ export const CommandPalette: React.FC = () => {
onSelect: run(() => setSettingsDialogOpen(true)),
},
];
+ list.push({
+ id: 'toggle-memory-debug',
+ title: t('commandPalette.item.toggleMemoryDebug'),
+ icon: ,
+ searchText: t('commandPalette.item.toggleMemoryDebug'),
+ onSelect: run(() => {
+ window.dispatchEvent(new CustomEvent('openchamber:memory-debug-toggle'));
+ }),
+ });
if (canUseElectronDesktopIPC()) {
list.splice(1, 0, {
id: 'new-mini-chat',
diff --git a/packages/ui/src/components/ui/HelpDialog.tsx b/packages/ui/src/components/ui/HelpDialog.tsx
index 6080ac29..f5f776b5 100644
--- a/packages/ui/src/components/ui/HelpDialog.tsx
+++ b/packages/ui/src/components/ui/HelpDialog.tsx
@@ -10,6 +10,7 @@ import { Icon } from "@/components/icon/Icon";
import { useUIStore } from "@/stores/useUIStore";
import {
getEffectiveShortcutCombo,
+ getEffectiveShortcutPrefix,
getShortcutAction,
formatShortcutForDisplay,
type ShortcutActionId,
@@ -156,24 +157,6 @@ export const HelpDialog: React.FC = () => {
{
categoryKey: "helpDialog.section.panels",
items: [
- {
- id: 'toggle_right_sidebar',
- descriptionKey: 'helpDialog.item.toggleRightSidebar',
- icon: "layout-right",
- keys: '',
- },
- {
- id: 'open_right_sidebar_git',
- descriptionKey: 'helpDialog.item.openRightSidebarGitTab',
- icon: "git-branch",
- keys: '',
- },
- {
- id: 'open_right_sidebar_files',
- descriptionKey: 'helpDialog.item.openRightSidebarFilesTab',
- icon: "layout-right",
- keys: '',
- },
{
id: 'toggle_terminal',
descriptionKey: 'helpDialog.item.toggleTerminalDock',
@@ -187,14 +170,13 @@ export const HelpDialog: React.FC = () => {
keys: '',
},
{
- id: 'toggle_context_plan',
- descriptionKey: 'helpDialog.item.togglePlanContextPanel',
- icon: "time",
- keys: '',
+ keys: [`${formatShortcutForDisplay(getEffectiveShortcutPrefix('switch_context_surface', shortcutOverrides))} + 1...0`],
+ descriptionKey: "helpDialog.item.switchContextSurface",
+ icon: "layout-right",
},
{
- keys: [`${formatShortcutForDisplay('mod')} + 1...0`],
- descriptionKey: "helpDialog.item.switchContextSurface",
+ keys: [`${formatShortcutForDisplay(getEffectiveShortcutPrefix('switch_session_tab', shortcutOverrides))} + 1...9`],
+ descriptionKey: "helpDialog.item.switchSessionTab",
icon: "layout-right",
},
],
@@ -214,12 +196,6 @@ export const HelpDialog: React.FC = () => {
icon: "stack",
keys: '',
},
- {
- id: 'cycle_services_tab',
- descriptionKey: 'helpDialog.item.cycleServicesTab',
- icon: "stack",
- keys: '',
- },
{
id: 'open_settings',
descriptionKey: "helpDialog.item.openSettings",
@@ -258,6 +234,12 @@ export const HelpDialog: React.FC = () => {
const descriptionKey = shortcut.descriptionKey
?? (action?.customizable ? action.settingsLabelKey : undefined);
if (!descriptionKey) return null;
+ // This dialog lists what the keyboard can do right now;
+ // an action without a binding belongs to the command
+ // palette and Settings, not here.
+ if (shortcut.id && !getEffectiveShortcutCombo(shortcut.id, shortcutOverrides)) {
+ return null;
+ }
const displayKeys = shortcut.id
? renderShortcut(
shortcut.id,
@@ -320,7 +302,7 @@ export const HelpDialog: React.FC = () => {
• {t('helpDialog.proTips.recentSessions')}
- • {t('helpDialog.proTips.themeCycling')}
+ • {t('helpDialog.proTips.leaderSequences')}
diff --git a/packages/ui/src/components/ui/dropdown-navigation.ts b/packages/ui/src/components/ui/dropdown-navigation.ts
index 2bd90bf7..185e2fc9 100644
--- a/packages/ui/src/components/ui/dropdown-navigation.ts
+++ b/packages/ui/src/components/ui/dropdown-navigation.ts
@@ -2,16 +2,18 @@ import type React from 'react';
import { isIMECompositionEvent } from '@/lib/ime';
-function getDropdownNavigationKey(event: Pick): 'ArrowDown' | 'ArrowUp' | null {
+function getDropdownNavigationKey(event: Pick): 'ArrowDown' | 'ArrowUp' | null {
if (!event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return null;
- if (event.key.toLowerCase() === 'n') return 'ArrowDown';
- if (event.key.toLowerCase() === 'p') return 'ArrowUp';
+ // `code` covers non-Latin layouts, where `key` is the layout's own letter.
+ if (event.key.toLowerCase() === 'n' || event.code === 'KeyN') return 'ArrowDown';
+ if (event.key.toLowerCase() === 'p' || event.code === 'KeyP') return 'ArrowUp';
return null;
}
type DropdownNavigationEvent = Pick<
React.KeyboardEvent,
| 'altKey'
+ | 'code'
| 'ctrlKey'
| 'defaultPrevented'
| 'isPropagationStopped'
diff --git a/packages/ui/src/hooks/useKeyboardShortcuts.ts b/packages/ui/src/hooks/useKeyboardShortcuts.ts
index a9da9370..09ef0327 100644
--- a/packages/ui/src/hooks/useKeyboardShortcuts.ts
+++ b/packages/ui/src/hooks/useKeyboardShortcuts.ts
@@ -1,7 +1,7 @@
import React from 'react';
import { isTerminalEventTarget } from '@/lib/terminalFocus';
import { useSessionUIStore } from '@/sync/session-ui-store';
-import { closeSessionTabAndActivateNeighbour } from '@/lib/sessionTabs';
+import { activateSessionTabByIndex, closeSessionTabAndActivateNeighbour } from '@/lib/sessionTabs';
import { useSelectionStore } from '@/sync/selection-store';
import * as sessionActions from '@/sync/session-actions';
import { normalizeContextPanelDirectoryKey, useUIStore } from '@/stores/useUIStore';
@@ -11,13 +11,14 @@ import { useKeybinds } from '@/hooks/useKeybind';
import { createWorktreeSession } from '@/lib/worktreeSessionCreator';
import { useConfigStore } from '@/stores/useConfigStore';
import { canUseElectronDesktopIPC, invokeDesktop, isVSCodeRuntime } from '@/lib/desktop';
-import { showOpenCodeStatus } from '@/lib/openCodeStatus';
import {
eventMatchesShortcut,
eventMatchesShortcutPrefix,
getEffectiveShortcutCombo,
getEffectiveShortcutPrefix,
normalizeCombo,
+ resolveShortcutEventDigit,
+ resolveShortcutEventKey,
ShortcutDispatcher,
shortcutRegistry,
type ShortcutActionId,
@@ -123,8 +124,18 @@ export const useKeyboardShortcuts = () => {
},
open_session_list: () => {
const state = useUIStore.getState();
- if (state.isMobile) state.setSessionSwitcherOpen(true);
- else state.setSessionDropdownOpen(true);
+ if (state.isMobile) {
+ state.setSessionSwitcherOpen(true);
+ return;
+ }
+ // The switcher dropdown only mounts while the sidebar is collapsed;
+ // with the sidebar visible the list is already on screen, so the
+ // shortcut opens the sidebar's session search instead.
+ if (state.isSidebarOpen) {
+ window.dispatchEvent(new CustomEvent('openchamber:sidebar-session-search'));
+ return;
+ }
+ state.setSessionDropdownOpen(true);
},
toggle_prompt_navigator: () => {
const state = useUIStore.getState();
@@ -146,9 +157,6 @@ export const useKeyboardShortcuts = () => {
}
state.togglePromptNavigatorPanel();
},
- open_status: () => {
- void showOpenCodeStatus();
- },
open_help: () => {
useUIStore.getState().toggleHelpDialog();
},
@@ -230,25 +238,6 @@ export const useKeyboardShortcuts = () => {
useSelectionStore.getState().saveSessionAgentSelection(sessionId, next);
}
},
- toggle_right_sidebar: () => {
- const state = useUIStore.getState();
- if (state.isMobile || !currentDirectory) return false;
- const directory = normalizeContextPanelDirectoryKey(currentDirectory);
- const panel = state.contextPanelByDirectory[directory];
- if (panel?.isOpen) state.closeContextPanel(directory);
- else if (panel?.activeTabId) state.setActiveContextPanelTab(directory, panel.activeTabId);
- else state.openContextSurface(directory, 'git');
- },
- open_right_sidebar_git: () => {
- const state = useUIStore.getState();
- if (state.isMobile || !currentDirectory) return false;
- state.openContextSurface(normalizeContextPanelDirectoryKey(currentDirectory), 'git');
- },
- open_right_sidebar_files: () => {
- const state = useUIStore.getState();
- if (state.isMobile || !currentDirectory) return false;
- state.openContextSurface(normalizeContextPanelDirectoryKey(currentDirectory), 'file');
- },
toggle_terminal: () => {
if (useUIStore.getState().isMobile) return false;
return toggleTerminalSurface();
@@ -482,8 +471,9 @@ export const useKeyboardShortcuts = () => {
return;
}
- const switchSurfaceDigit = event.key.length === 1 && event.key >= '0' && event.key <= '9'
- ? (event.key === '0' ? 10 : Number(event.key))
+ const rawDigit = resolveShortcutEventDigit(event);
+ const switchSurfaceDigit = rawDigit !== null
+ ? (rawDigit === '0' ? 10 : Number(rawDigit))
: null;
const switchSurfacePrefix = getEffectiveShortcutPrefix(
'switch_context_surface',
@@ -514,13 +504,32 @@ export const useKeyboardShortcuts = () => {
}
}
+ const sessionTabDigit = rawDigit !== null && rawDigit !== '0' ? Number(rawDigit) : null;
+ if (
+ sessionTabDigit !== null
+ && !event.repeat
+ && !isVSCodeRuntime()
+ && useUIStore.getState().sessionTabsEnabled
+ && eventMatchesShortcutPrefix(
+ event,
+ getEffectiveShortcutPrefix('switch_session_tab', useUIStore.getState().shortcutOverrides),
+ heldKeysRef.current,
+ )
+ && activateSessionTabByIndex(sessionTabDigit - 1)
+ ) {
+ event.preventDefault();
+ return;
+ }
+
if (dispatcher.dispatch(event)) event.preventDefault();
};
const handleKeyHoldDown = (event: KeyboardEvent) => {
heldKeysRef.current.add(event.key.toLowerCase());
+ heldKeysRef.current.add(resolveShortcutEventKey(event).toLowerCase());
};
const handleKeyUp = (event: KeyboardEvent) => {
heldKeysRef.current.delete(event.key.toLowerCase());
+ heldKeysRef.current.delete(resolveShortcutEventKey(event).toLowerCase());
};
const handleBlur = () => {
heldKeysRef.current.clear();
diff --git a/packages/ui/src/lib/i18n/messages/de.settings.ts b/packages/ui/src/lib/i18n/messages/de.settings.ts
index c52e96e8..6f8c5352 100644
--- a/packages/ui/src/lib/i18n/messages/de.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/de.settings.ts
@@ -1080,9 +1080,8 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': 'Terminal erweitert umschalten',
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': 'Auswahl zum Chat hinzufügen',
'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': 'Seitenleiste umschalten',
- 'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': 'Rechte Seitenleiste umschalten',
- 'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Git-Tab der rechten Seitenleiste öffnen',
- 'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label': 'Datei-Tab der rechten Seitenleiste öffnen',
+ 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.label': 'Sitzungs-Tab wechseln',
+ 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.suffix': ' + 1…9',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'Kontextpanel-Oberfläche wechseln',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'Neue Sitzung',
@@ -1090,9 +1089,7 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'Neuer Worktree-Entwurf',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'Neues Mini-Chat-Fenster',
'settings.openchamber.keyboardShortcuts.action.open_help.label': 'Tastenkürzel öffnen',
- 'settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label': 'Plan-Kontextpanel umschalten',
'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': 'Dienstemenü umschalten',
- 'settings.openchamber.keyboardShortcuts.action.cycle_services_tab.label': 'Dienste-Tab durchschalten',
'settings.openchamber.keyboardShortcuts.action.cycle_theme.label': 'Thema wechseln',
'settings.openchamber.keyboardShortcuts.action.cycle_agent.label': 'Agent wechseln',
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': 'Favorites Modell vorwärts durchschalten',
diff --git a/packages/ui/src/lib/i18n/messages/de.ts b/packages/ui/src/lib/i18n/messages/de.ts
index 15735541..adc8076b 100644
--- a/packages/ui/src/lib/i18n/messages/de.ts
+++ b/packages/ui/src/lib/i18n/messages/de.ts
@@ -1684,22 +1684,18 @@ export const dict = {
'helpDialog.item.focusChatInput': 'Chat-Eingabe fokussieren',
'helpDialog.item.togglePromptNavigator': 'Aufforderungs-Navigator umschalten',
'helpDialog.item.abortActiveRun': 'Aktuelle Ausführung abbrechen (Doppeltaste)',
- 'helpDialog.item.toggleRightSidebar': 'Rechte Seitenleiste umschalten',
- 'helpDialog.item.openRightSidebarGitTab': 'Git-Registerkarte der rechten Seitenleiste öffnen',
- 'helpDialog.item.openRightSidebarFilesTab': 'Datei-Registerkarte der rechten Seitenleiste öffnen',
'helpDialog.item.toggleTerminalDock': 'Terminal-Dock umschalten',
'helpDialog.item.toggleTerminalExpanded': 'Terminal erweitert umschalten',
- 'helpDialog.item.togglePlanContextPanel': 'Plan-Kontext-Panel umschalten',
'helpDialog.item.cycleTheme': 'Thema wechseln (Hell → Dunkel → System)',
+ 'helpDialog.item.switchSessionTab': 'Sitzungs-Tab wechseln',
'helpDialog.item.switchContextSurface': 'Kontextpanel-Oberfläche wechseln (Zahlentaste)',
'helpDialog.item.toggleServicesMenu': 'Dienstemenü umschalten',
- 'helpDialog.item.cycleServicesTab': 'Dienste-Registerkarte durchgehen',
'helpDialog.item.openSettings': 'Einstellungen öffnen',
'helpDialog.keyCombiner.or': 'oder',
'helpDialog.proTips.title': 'Pro-Tipps:',
'helpDialog.proTips.commandPalette': 'Verwenden Sie die Befehlspalette ({shortcut}), um schnell auf alle Aktionen zuzugreifen',
'helpDialog.proTips.recentSessions': 'Die 5 zuletzt verwendeten Sitzungen erscheinen in der Befehlspalette',
- 'helpDialog.proTips.themeCycling': 'Themenwechsel merken sich Ihre Einstellung über Sitzungen hinweg',
+ 'helpDialog.proTips.leaderSequences': 'Zweistufige Kürzel: erst die Kombination, dann die zweite Taste — Esc bricht ab',
'header.actions.rightSidebarWithShortcut': 'Rechte Seitenleiste ({shortcut})',
'header.actions.toggleRightSidebarAria': 'Rechte Seitenleiste umschalten',
'header.actions.openAppMenu': 'OpenChamber-Menü',
@@ -2293,6 +2289,9 @@ export const dict = {
'commandPalette.item.toggleSidebar': 'Seitenleiste umschalten',
'commandPalette.item.showContextUsage': 'Kontextnutzung anzeigen',
'commandPalette.item.toggleTerminal': 'Terminal umschalten',
+ 'commandPalette.item.cycleTheme': 'Thema wechseln',
+ 'commandPalette.item.showOpenCodeStatus': 'OpenCode-Status anzeigen',
+ 'commandPalette.item.toggleMemoryDebug': 'Memory-Debug-Panel umschalten',
'commandPalette.item.openSettings': 'Einstellungen öffnen...',
'commandPalette.session.untitled': 'Unbenannte Sitzung',
'openCodeStatusDialog.title': 'OpenCode-Status',
diff --git a/packages/ui/src/lib/i18n/messages/en.settings.ts b/packages/ui/src/lib/i18n/messages/en.settings.ts
index f48bdd83..ef9d6265 100644
--- a/packages/ui/src/lib/i18n/messages/en.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/en.settings.ts
@@ -1142,9 +1142,8 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': 'Toggle terminal expanded',
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': 'Add selection to chat',
'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': 'Toggle sidebar',
- 'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': 'Toggle context panel',
- 'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Open Git surface',
- 'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label': 'Open Files surface',
+ 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.label': 'Switch session tab',
+ 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.suffix': ' + 1…9',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'Switch context panel surface',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'New session',
@@ -1152,9 +1151,7 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'New worktree draft',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'New Mini Chat window',
'settings.openchamber.keyboardShortcuts.action.open_help.label': 'Open keyboard shortcuts',
- 'settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label': 'Toggle plan context panel',
'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': 'Toggle services menu',
- 'settings.openchamber.keyboardShortcuts.action.cycle_services_tab.label': 'Cycle services tab',
'settings.openchamber.keyboardShortcuts.action.cycle_theme.label': 'Cycle theme',
'settings.openchamber.keyboardShortcuts.action.cycle_agent.label': 'Cycle agent',
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': 'Cycle favorite model forward',
diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts
index c33afc63..686698de 100644
--- a/packages/ui/src/lib/i18n/messages/en.ts
+++ b/packages/ui/src/lib/i18n/messages/en.ts
@@ -1858,22 +1858,18 @@ export const dict = {
'helpDialog.item.focusChatInput': 'Focus Chat Input',
'helpDialog.item.togglePromptNavigator': 'Toggle Prompt Navigator',
'helpDialog.item.abortActiveRun': 'Abort active run (double press)',
- 'helpDialog.item.toggleRightSidebar': 'Toggle context panel',
- 'helpDialog.item.openRightSidebarGitTab': 'Open Git surface',
- 'helpDialog.item.openRightSidebarFilesTab': 'Open Files surface',
'helpDialog.item.toggleTerminalDock': 'Toggle Terminal Dock',
'helpDialog.item.toggleTerminalExpanded': 'Toggle Terminal Expanded',
- 'helpDialog.item.togglePlanContextPanel': 'Toggle Plan Context Panel',
+ 'helpDialog.item.switchSessionTab': 'Switch Session Tab',
'helpDialog.item.switchContextSurface': 'Switch Context Panel Surface (number key)',
'helpDialog.item.cycleTheme': 'Cycle Theme (Light → Dark → System)',
'helpDialog.item.toggleServicesMenu': 'Toggle Services Menu',
- 'helpDialog.item.cycleServicesTab': 'Cycle Services Tab',
'helpDialog.item.openSettings': 'Open Settings',
'helpDialog.keyCombiner.or': 'or',
'helpDialog.proTips.title': 'Pro Tips:',
'helpDialog.proTips.commandPalette': 'Use Command Palette ({shortcut}) to quickly access all actions',
'helpDialog.proTips.recentSessions': 'The 5 most recent sessions appear in the Command Palette',
- 'helpDialog.proTips.themeCycling': 'Theme cycling remembers your preference across sessions',
+ 'helpDialog.proTips.leaderSequences': 'Two-step shortcuts: press the first combo, then the second key — Esc cancels',
'header.actions.rightSidebarWithShortcut': 'Right sidebar ({shortcut})',
'header.actions.toggleRightSidebarAria': 'Toggle right sidebar',
'header.actions.openAppMenu': 'OpenChamber menu',
@@ -2483,6 +2479,9 @@ export const dict = {
'commandPalette.item.toggleSidebar': 'Toggle Sidebar',
'commandPalette.item.showContextUsage': 'Show Context Usage',
'commandPalette.item.toggleTerminal': 'Toggle Terminal',
+ 'commandPalette.item.cycleTheme': 'Cycle theme',
+ 'commandPalette.item.showOpenCodeStatus': 'Show OpenCode status',
+ 'commandPalette.item.toggleMemoryDebug': 'Toggle memory debug panel',
'commandPalette.item.openSettings': 'Open Settings...',
'commandPalette.session.untitled': 'Untitled Session',
'openCodeStatusDialog.title': 'OpenCode Status',
diff --git a/packages/ui/src/lib/i18n/messages/es.settings.ts b/packages/ui/src/lib/i18n/messages/es.settings.ts
index 67ea6135..63f646fb 100644
--- a/packages/ui/src/lib/i18n/messages/es.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/es.settings.ts
@@ -1110,9 +1110,8 @@ export const settingsDict = {
"settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label": "Expandir o contraer terminal",
"settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label": "Agregar selección al chat",
"settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label": "Mostrar u ocultar barra lateral",
- "settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label": 'Alternar panel de contexto',
- "settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label": 'Abrir superficie de Git',
- "settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label": 'Abrir superficie de archivos',
+ "settings.openchamber.keyboardShortcuts.action.switch_session_tab.label": "Cambiar pestaña de sesión",
+ "settings.openchamber.keyboardShortcuts.action.switch_session_tab.suffix": " + 1…9",
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.label": "Cambiar superficie del panel de contexto",
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix": " + 1…0",
"settings.openchamber.keyboardShortcuts.action.new_chat.label": "Nueva sesión",
@@ -1120,9 +1119,7 @@ export const settingsDict = {
"settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label": "Nuevo borrador de worktree",
"settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Nueva ventana Mini Chat",
"settings.openchamber.keyboardShortcuts.action.open_help.label": "Abrir atajos de teclado",
- "settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label": "Alternar panel de plan de contexto",
"settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label": "Mostrar u ocultar menú de servicios",
- "settings.openchamber.keyboardShortcuts.action.cycle_services_tab.label": "Cambiar pestaña de servicios",
"settings.openchamber.keyboardShortcuts.action.cycle_theme.label": "Cambiar tema",
"settings.openchamber.keyboardShortcuts.action.cycle_agent.label": "Cambiar agente",
"settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label": "Siguiente modelo favorito",
diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts
index 7999cdb7..031d9b82 100644
--- a/packages/ui/src/lib/i18n/messages/es.ts
+++ b/packages/ui/src/lib/i18n/messages/es.ts
@@ -1836,22 +1836,18 @@ export const dict: Record = {
"helpDialog.item.focusChatInput": "Enfocar entrada de chat",
"helpDialog.item.togglePromptNavigator": "Mostrar u ocultar navegador de prompts",
"helpDialog.item.abortActiveRun": "Detener ejecución activa (doble presionar)",
- "helpDialog.item.toggleRightSidebar": 'Alternar panel de contexto',
- "helpDialog.item.openRightSidebarGitTab": 'Abrir superficie de Git',
- "helpDialog.item.openRightSidebarFilesTab": 'Abrir superficie de archivos',
"helpDialog.item.toggleTerminalDock": "Mostrar u ocultar dock de terminal",
"helpDialog.item.toggleTerminalExpanded": "Expandir o contraer terminal",
- "helpDialog.item.togglePlanContextPanel": "Alternar panel de contexto del plan",
"helpDialog.item.cycleTheme": "Cambiar tema (Claro → Oscuro → Sistema)",
+ "helpDialog.item.switchSessionTab": "Cambiar pestaña de sesión",
"helpDialog.item.switchContextSurface": "Cambiar superficie del panel de contexto (tecla numérica)",
"helpDialog.item.toggleServicesMenu": "Mostrar u ocultar menú de servicios",
- "helpDialog.item.cycleServicesTab": "Cambiar pestaña de servicios",
"helpDialog.item.openSettings": "Abrir configuración",
"helpDialog.keyCombiner.or": "o",
"helpDialog.proTips.title": "Consejos:",
"helpDialog.proTips.commandPalette": "Usa la paleta de comandos ({shortcut}) para acceder rápidamente a todas las acciones",
"helpDialog.proTips.recentSessions": "Las cinco sesiones más recientes aparecen en la paleta de comandos",
- "helpDialog.proTips.themeCycling": "El ciclo de tema recuerda tu preferencia entre sesiones",
+ "helpDialog.proTips.leaderSequences": "Atajos en dos pasos: pulsa la combinación y luego la segunda tecla; Esc cancela",
"header.actions.rightSidebarWithShortcut": "Barra lateral derecha ({shortcut})",
"header.actions.toggleRightSidebarAria": "Mostrar u ocultar barra lateral derecha",
"header.actions.openAppMenu": "Menú de OpenChamber",
@@ -2449,6 +2445,9 @@ export const dict: Record = {
"commandPalette.item.toggleSidebar": "Mostrar u ocultar barra lateral",
"commandPalette.item.showContextUsage": "Mostrar uso del contexto",
"commandPalette.item.toggleTerminal": "Mostrar u ocultar terminal",
+ "commandPalette.item.cycleTheme": "Cambiar tema",
+ "commandPalette.item.showOpenCodeStatus": "Mostrar estado de OpenCode",
+ "commandPalette.item.toggleMemoryDebug": "Alternar panel de depuración de memoria",
"commandPalette.item.openSettings": "Abrir configuración...",
"commandPalette.session.untitled": "Sesión sin título",
"openCodeStatusDialog.title": "Estado de OpenCode",
diff --git a/packages/ui/src/lib/i18n/messages/fr.settings.ts b/packages/ui/src/lib/i18n/messages/fr.settings.ts
index 80d982e6..f4df769b 100644
--- a/packages/ui/src/lib/i18n/messages/fr.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/fr.settings.ts
@@ -1028,9 +1028,8 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': 'Terminal à bascule étendu',
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': 'Ajouter la sélection au chat',
'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': 'Basculer la barre latérale',
- 'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': 'Afficher/masquer le panneau de contexte',
- 'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Ouvrir la surface Git',
- 'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label': 'Ouvrir la surface Fichiers',
+ 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.label': 'Basculer l’onglet de session',
+ 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.suffix': ' + 1…9',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'Basculer la surface du panneau contextuel',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'Nouvelle session',
@@ -1038,9 +1037,7 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'Nouvelle ébauche d\'worktree',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'Nouvelle fenêtre de mini-chat',
'settings.openchamber.keyboardShortcuts.action.open_help.label': 'Ouvrir les raccourcis clavier',
- 'settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label': 'Basculer le panneau contextuel du plan',
'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': 'Basculer le menu des services',
- 'settings.openchamber.keyboardShortcuts.action.cycle_services_tab.label': 'Onglet Services vélo',
'settings.openchamber.keyboardShortcuts.action.cycle_theme.label': 'Thème du cycle',
'settings.openchamber.keyboardShortcuts.action.cycle_agent.label': 'Agent de cycle',
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': 'Faire avancer le modèle favori',
diff --git a/packages/ui/src/lib/i18n/messages/fr.ts b/packages/ui/src/lib/i18n/messages/fr.ts
index f16dee7b..982cfd30 100644
--- a/packages/ui/src/lib/i18n/messages/fr.ts
+++ b/packages/ui/src/lib/i18n/messages/fr.ts
@@ -1616,22 +1616,18 @@ export const dict = {
'helpDialog.item.focusChatInput': 'Concentration sur la saisie du chat',
'helpDialog.item.togglePromptNavigator': 'Afficher ou masquer le navigateur de prompts',
'helpDialog.item.abortActiveRun': 'Abandonner l’exécution active (double pression)',
- 'helpDialog.item.toggleRightSidebar': 'Afficher/masquer le panneau de contexte',
- 'helpDialog.item.openRightSidebarGitTab': 'Ouvrir la surface Git',
- 'helpDialog.item.openRightSidebarFilesTab': 'Ouvrir la surface Fichiers',
'helpDialog.item.toggleTerminalDock': 'Basculer la station d\'accueil du terminal',
'helpDialog.item.toggleTerminalExpanded': 'Terminal à bascule étendu',
- 'helpDialog.item.togglePlanContextPanel': 'Toggle Panneau contextuel du plan',
'helpDialog.item.cycleTheme': 'Basculer le thème (clair → sombre → système)',
+ 'helpDialog.item.switchSessionTab': 'Basculer l’onglet de session',
'helpDialog.item.switchContextSurface': 'Basculer la surface du panneau contextuel (touche numérique)',
'helpDialog.item.toggleServicesMenu': 'Basculer le menu des services',
- 'helpDialog.item.cycleServicesTab': 'Onglet Services de vélo',
'helpDialog.item.openSettings': 'Ouvrir les paramètres',
'helpDialog.keyCombiner.or': 'ou',
'helpDialog.proTips.title': 'Conseils de pro :',
'helpDialog.proTips.commandPalette': 'Utilisez la palette de commandes ({shortcut}) pour accéder rapidement à toutes les actions',
'helpDialog.proTips.recentSessions': 'Les 5 sessions les plus récentes apparaissent dans la palette de commandes',
- 'helpDialog.proTips.themeCycling': 'Le cyclisme thématique mémorise vos préférences au fil des sessions',
+ 'helpDialog.proTips.leaderSequences': 'Raccourcis en deux temps : appuyez sur la combinaison, puis sur la seconde touche — Échap annule',
'header.actions.rightSidebarWithShortcut': 'Barre latérale droite ({shortcut})',
'header.actions.toggleRightSidebarAria': 'Basculer la barre latérale droite',
'header.actions.openAppMenu': 'Menu de OpenChamber',
@@ -2187,6 +2183,9 @@ export const dict = {
'commandPalette.item.toggleSidebar': 'Basculer la barre latérale',
'commandPalette.item.showContextUsage': 'Afficher l\'utilisation du contexte',
'commandPalette.item.toggleTerminal': 'Basculer le terminal',
+ 'commandPalette.item.cycleTheme': 'Changer de thème',
+ 'commandPalette.item.showOpenCodeStatus': 'Afficher le statut OpenCode',
+ 'commandPalette.item.toggleMemoryDebug': 'Basculer le panneau de débogage mémoire',
'commandPalette.item.openSettings': 'Ouvrez les paramètres...',
'commandPalette.session.untitled': 'Session sans titre',
'openCodeStatusDialog.title': 'Statut OpenCode',
diff --git a/packages/ui/src/lib/i18n/messages/ja.settings.ts b/packages/ui/src/lib/i18n/messages/ja.settings.ts
index 98d45ab8..63e54c6b 100644
--- a/packages/ui/src/lib/i18n/messages/ja.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/ja.settings.ts
@@ -1143,9 +1143,8 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': 'ターミナル拡大の切替',
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': '選択範囲をチャットに追加',
'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': 'サイドバーの切替',
- 'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': 'コンテキストパネルの表示切替',
- 'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Git サーフェスを開く',
- 'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label': 'ファイルサーフェスを開く',
+ 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.label': 'セッションタブを切り替え',
+ 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.suffix': ' + 1…9',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'コンテキストパネルのサーフェスを切り替え',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': '新しい Session',
@@ -1153,9 +1152,7 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '新しい Worktree 下書き',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '新しいミニチャットウィンドウ',
'settings.openchamber.keyboardShortcuts.action.open_help.label': 'キーボードショートカットを開く',
- 'settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label': '計画コンテキストパネルの切替',
'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': 'サービスメニューの切替',
- 'settings.openchamber.keyboardShortcuts.action.cycle_services_tab.label': 'サービスタブを順に切替',
'settings.openchamber.keyboardShortcuts.action.cycle_theme.label': 'テーマを順に切替',
'settings.openchamber.keyboardShortcuts.action.cycle_agent.label': 'Agent を順に切替',
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': 'お気に入りモデルを次へ',
diff --git a/packages/ui/src/lib/i18n/messages/ja.ts b/packages/ui/src/lib/i18n/messages/ja.ts
index 3b511318..d0c55cfc 100644
--- a/packages/ui/src/lib/i18n/messages/ja.ts
+++ b/packages/ui/src/lib/i18n/messages/ja.ts
@@ -1854,22 +1854,18 @@ export const dict: Record = {
'helpDialog.item.focusChatInput': 'チャット入力にフォーカス',
'helpDialog.item.togglePromptNavigator': 'プロンプトナビゲーターの表示切替',
'helpDialog.item.abortActiveRun': 'アクティブな実行を中止(ダブルプレス)',
- 'helpDialog.item.toggleRightSidebar': 'コンテキストパネルの表示切替',
- 'helpDialog.item.openRightSidebarGitTab': 'Git サーフェスを開く',
- 'helpDialog.item.openRightSidebarFilesTab': 'ファイルサーフェスを開く',
'helpDialog.item.toggleTerminalDock': 'ターミナルドックの切り替え',
'helpDialog.item.toggleTerminalExpanded': 'ターミナル展開の切り替え',
- 'helpDialog.item.togglePlanContextPanel': '計画コンテキストパネルの切り替え',
'helpDialog.item.cycleTheme': 'テーマ切り替え(ライト→ダーク→システム)',
+ 'helpDialog.item.switchSessionTab': 'セッションタブを切り替え',
'helpDialog.item.switchContextSurface': 'コンテキストパネルのサーフェスを切り替え(数字キー)',
'helpDialog.item.toggleServicesMenu': 'サービスの切り替え',
- 'helpDialog.item.cycleServicesTab': 'サービス変数の切り替え',
'helpDialog.item.openSettings': '設定を開く',
'helpDialog.keyCombiner.or': 'または',
'helpDialog.proTips.title': 'プロのヒント:',
'helpDialog.proTips.commandPalette': 'コマンドパレット({shortcut})を使うとすべての操作にすばやくアクセスできます',
'helpDialog.proTips.recentSessions': '最近の5つのセッションがコマンドパレットに表示されます',
- 'helpDialog.proTips.themeCycling': 'テーマの切り替えはセッション間で設定が記憶されます',
+ 'helpDialog.proTips.leaderSequences': '2段階ショートカット:組み合わせを押してから2つ目のキーを押します(Escで取消)',
'header.actions.rightSidebarWithShortcut': '右サイドバー({shortcut})',
'header.actions.toggleRightSidebarAria': '右サイドバーの切り替え',
'header.actions.openAppMenu': 'OpenChamberメニュー',
@@ -2482,6 +2478,9 @@ export const dict: Record = {
'commandPalette.item.toggleSidebar': 'サイドバーの切り替え',
'commandPalette.item.showContextUsage': 'コンテキスト使用量を表示',
'commandPalette.item.toggleTerminal': 'ターミナルの切り替え',
+ 'commandPalette.item.cycleTheme': 'テーマを順に切替',
+ 'commandPalette.item.showOpenCodeStatus': 'OpenCode のステータスを表示',
+ 'commandPalette.item.toggleMemoryDebug': 'メモリデバッグパネルの切替',
'commandPalette.item.openSettings': '設定を開く...',
'commandPalette.session.untitled': '無題のセッション',
'openCodeStatusDialog.title': 'OpenCodeステータス',
diff --git a/packages/ui/src/lib/i18n/messages/ko.settings.ts b/packages/ui/src/lib/i18n/messages/ko.settings.ts
index 70314bdb..a9ec57e3 100644
--- a/packages/ui/src/lib/i18n/messages/ko.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/ko.settings.ts
@@ -1110,9 +1110,8 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': '터미널 확장 토글',
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': '선택 내용을 채팅에 추가',
'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': '사이드바 토글',
- 'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': '컨텍스트 패널 표시 전환',
- 'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Git 서피스 열기',
- 'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label': '파일 서피스 열기',
+ 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.label': '세션 탭 전환',
+ 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.suffix': ' + 1…9',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': '컨텍스트 패널 서피스 전환',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': '새 세션',
@@ -1120,9 +1119,7 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '새 worktree 초안',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '새 Mini Chat 창',
'settings.openchamber.keyboardShortcuts.action.open_help.label': '키보드 단축키 열기',
- 'settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label': '계획 컨텍스트 패널 토글',
'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': '서비스 메뉴 토글',
- 'settings.openchamber.keyboardShortcuts.action.cycle_services_tab.label': '서비스 탭 순환',
'settings.openchamber.keyboardShortcuts.action.cycle_theme.label': '테마 순환',
'settings.openchamber.keyboardShortcuts.action.cycle_agent.label': '에이전트 순환',
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': '즐겨찾기 모델 앞으로 순환',
diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts
index 4fef8b87..cba5551d 100644
--- a/packages/ui/src/lib/i18n/messages/ko.ts
+++ b/packages/ui/src/lib/i18n/messages/ko.ts
@@ -1860,22 +1860,18 @@ export const dict: Record = {
'helpDialog.item.focusChatInput': '채팅 입력창으로 포커스 이동',
'helpDialog.item.togglePromptNavigator': '프롬프트 탐색기 표시/숨기기',
'helpDialog.item.abortActiveRun': '활성 실행 중단(두 번 누르기)',
- 'helpDialog.item.toggleRightSidebar': '컨텍스트 패널 표시 전환',
- 'helpDialog.item.openRightSidebarGitTab': 'Git 서피스 열기',
- 'helpDialog.item.openRightSidebarFilesTab': '파일 서피스 열기',
'helpDialog.item.toggleTerminalDock': '터미널 독 전환',
'helpDialog.item.toggleTerminalExpanded': '터미널 펼치기/접기',
- 'helpDialog.item.togglePlanContextPanel': '플랜 컨텍스트 패널 전환',
'helpDialog.item.cycleTheme': '테마 순환(라이트 → 다크 → 시스템)',
+ 'helpDialog.item.switchSessionTab': '세션 탭 전환',
'helpDialog.item.switchContextSurface': '컨텍스트 패널 서피스 전환(숫자 키)',
'helpDialog.item.toggleServicesMenu': '서비스 메뉴 전환',
- 'helpDialog.item.cycleServicesTab': '서비스 탭 순환',
'helpDialog.item.openSettings': '설정 열기',
'helpDialog.keyCombiner.or': '또는',
'helpDialog.proTips.title': '팁:',
'helpDialog.proTips.commandPalette': '명령 팔레트({shortcut})로 모든 작업에 빠르게 접근하세요',
'helpDialog.proTips.recentSessions': '최근 세션 5개가 명령 팔레트에 표시됩니다',
- 'helpDialog.proTips.themeCycling': '테마 순환은 세션 간에도 선호 설정을 기억합니다',
+ 'helpDialog.proTips.leaderSequences': '2단계 단축키: 조합을 누른 뒤 두 번째 키를 누르세요 (Esc로 취소)',
'header.actions.rightSidebarWithShortcut': '오른쪽 사이드바 ({shortcut})',
'header.actions.toggleRightSidebarAria': '오른쪽 사이드바 토글',
'header.actions.openAppMenu': 'OpenChamber 메뉴',
@@ -2483,6 +2479,9 @@ export const dict: Record = {
'commandPalette.item.toggleSidebar': '토글 사이드바',
'commandPalette.item.showContextUsage': '컨텍스트 사용량 표시',
'commandPalette.item.toggleTerminal': '토글 터미널',
+ 'commandPalette.item.cycleTheme': '테마 순환',
+ 'commandPalette.item.showOpenCodeStatus': 'OpenCode 상태 표시',
+ 'commandPalette.item.toggleMemoryDebug': '메모리 디버그 패널 토글',
'commandPalette.item.openSettings': '설정... 열기',
'commandPalette.session.untitled': '제목 없는 세션',
'openCodeStatusDialog.title': 'OpenCode 상태',
diff --git a/packages/ui/src/lib/i18n/messages/pl.settings.ts b/packages/ui/src/lib/i18n/messages/pl.settings.ts
index 5a9fccc0..b14793e1 100644
--- a/packages/ui/src/lib/i18n/messages/pl.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/pl.settings.ts
@@ -818,7 +818,6 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_backward.label': 'Przełącz ulubiony model wstecz',
'settings.openchamber.keyboardShortcuts.action.open_model_selector.label': 'Otwórz wybór modelu',
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': 'Przełącz ulubiony model w przód',
- 'settings.openchamber.keyboardShortcuts.action.cycle_services_tab.label': 'Przełącz zakładkę usług',
'settings.openchamber.keyboardShortcuts.action.cycle_theme.label': 'Przełącz motyw',
'settings.openchamber.keyboardShortcuts.action.cycle_agent.label': 'Przełącz agenta',
'settings.openchamber.keyboardShortcuts.action.expand_input.label': 'Rozwiń pole wprowadzania',
@@ -831,13 +830,11 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.open_command_palette.label': 'Otwórz paletę poleceń',
'settings.openchamber.keyboardShortcuts.action.open_go_to_line.label': 'Przejdź do linii (edytor plików)',
'settings.openchamber.keyboardShortcuts.action.open_help.label': 'Otwórz skróty klawiszowe',
- 'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label': 'Otwórz powierzchnię plików',
+ 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.label': 'Przełącz kartę sesji',
+ 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.suffix': ' + 1…9',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'Przełącz powierzchnię panelu kontekstu',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
- 'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Otwórz powierzchnię Git',
'settings.openchamber.keyboardShortcuts.action.open_settings.label': 'Otwórz ustawienia',
- 'settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label': 'Przełącz panel kontekstu planu',
- 'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': 'Przełącz panel kontekstu',
'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': 'Przełącz menu usług',
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': 'Dodaj zaznaczenie do czatu',
'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': 'Przełącz pasek boczny',
diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts
index 476aff49..43d81bad 100644
--- a/packages/ui/src/lib/i18n/messages/pl.ts
+++ b/packages/ui/src/lib/i18n/messages/pl.ts
@@ -1452,6 +1452,9 @@ export const dict: Record = {
'commandPalette.item.showSessionSwitcher': 'Pokaż przełącznik sesji',
'commandPalette.item.toggleSidebar': 'Przełącz panel boczny',
'commandPalette.item.toggleTerminal': 'Przełącz terminal',
+ 'commandPalette.item.cycleTheme': 'Przełącz motyw',
+ 'commandPalette.item.showOpenCodeStatus': 'Pokaż status OpenCode',
+ 'commandPalette.item.toggleMemoryDebug': 'Przełącz panel debugowania pamięci',
'commandPalette.session.untitled': 'Nienazwana sesja',
'commandPalette.title': 'Paleta poleceń',
'contextPanel.actions.closePanel': 'Zamknij panel',
@@ -2452,7 +2455,6 @@ export const dict: Record = {
'helpDialog.item.createNewSession': 'Utwórz nową sesję',
'helpDialog.item.createNewWorktreeDraft': 'Utwórz nowy szkic drzewa pracy',
'helpDialog.item.cycleAgent': 'Przełącz agenta (w polu czatu)',
- 'helpDialog.item.cycleServicesTab': 'Przełącz kartę usług',
'helpDialog.item.cycleTheme': 'Przełącz motyw (Jasny → Ciemny → Systemowy)',
'helpDialog.item.cycleThinkingVariant': 'Przełącz wariant myślenia (skrót globalny)',
'helpDialog.item.focusChatInput': 'Ustaw fokus na polu czatu',
@@ -2461,13 +2463,10 @@ export const dict: Record = {
'helpDialog.item.newWindow': 'Nowe okno (tylko desktop)',
'helpDialog.item.openCommandPalette': 'Otwórz paletę poleceń',
'helpDialog.item.openModelSelector': 'Otwórz selektor modeli',
- 'helpDialog.item.openRightSidebarFilesTab': 'Otwórz powierzchnię plików',
- 'helpDialog.item.openRightSidebarGitTab': 'Otwórz powierzchnię Git',
'helpDialog.item.openSettings': 'Otwórz ustawienia',
'helpDialog.item.showKeyboardShortcuts': 'Pokaż skróty klawiaturowe (to okno)',
+ 'helpDialog.item.switchSessionTab': 'Przełącz kartę sesji',
'helpDialog.item.switchContextSurface': 'Przełącz powierzchnię panelu kontekstu (klawisz liczbowy)',
- 'helpDialog.item.togglePlanContextPanel': 'Przełącz panel kontekstu planu',
- 'helpDialog.item.toggleRightSidebar': 'Przełącz panel kontekstu',
'helpDialog.item.toggleServicesMenu': 'Przełącz menu usług',
'helpDialog.item.toggleSessionSidebar': 'Przełącz panel sesji',
'helpDialog.item.addSelectionToChat': 'Dodaj zaznaczenie do czatu',
@@ -2476,7 +2475,7 @@ export const dict: Record = {
'helpDialog.keyCombiner.or': 'lub',
'helpDialog.proTips.commandPalette': 'Użyj Palety poleceń ({shortcut}), aby szybko uzyskać dostęp do wszystkich akcji',
'helpDialog.proTips.recentSessions': '5 ostatnich sesji pojawia się w Palecie poleceń',
- 'helpDialog.proTips.themeCycling': 'Przełączanie motywów zapamiętuje twoje preferencje między sesjami',
+ 'helpDialog.proTips.leaderSequences': 'Skróty dwustopniowe: naciśnij kombinację, potem drugi klawisz — Esc anuluje',
'helpDialog.proTips.title': 'Wskazówki:',
'helpDialog.section.interface': 'Interfejs',
'helpDialog.section.navigationCommands': 'Nawigacja i polecenia',
diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts
index f536f6e3..6f5a4319 100644
--- a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts
@@ -1110,9 +1110,8 @@ export const settingsDict = {
"settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label": "Expandir ou recolher terminal",
"settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label": "Adicionar seleção ao chat",
"settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label": "Mostrar ou ocultar barra lateral",
- "settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label": 'Alternar painel de contexto',
- "settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label": 'Abrir superfície do Git',
- "settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label": 'Abrir superfície de arquivos',
+ "settings.openchamber.keyboardShortcuts.action.switch_session_tab.label": "Alternar aba de sessão",
+ "settings.openchamber.keyboardShortcuts.action.switch_session_tab.suffix": " + 1…9",
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.label": "Alternar superfície do painel de contexto",
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix": " + 1…0",
"settings.openchamber.keyboardShortcuts.action.new_chat.label": "Nova sessão",
@@ -1120,9 +1119,7 @@ export const settingsDict = {
"settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label": "Novo rascunho de worktree",
"settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Nova janela Mini Chat",
"settings.openchamber.keyboardShortcuts.action.open_help.label": "Abrir atalhos de teclado",
- "settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label": "Alternar painel de plano de contexto",
"settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label": "Mostrar ou ocultar menu de serviços",
- "settings.openchamber.keyboardShortcuts.action.cycle_services_tab.label": "Alternar aba de serviços",
"settings.openchamber.keyboardShortcuts.action.cycle_theme.label": "Alternar tema",
"settings.openchamber.keyboardShortcuts.action.cycle_agent.label": "Alternar agente",
"settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label": "Próximo modelo favorito",
diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts
index 36a06ede..cdc7c27c 100644
--- a/packages/ui/src/lib/i18n/messages/pt-BR.ts
+++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts
@@ -1836,22 +1836,18 @@ export const dict: Record = {
"helpDialog.item.focusChatInput": "Focar entrada do chat",
"helpDialog.item.togglePromptNavigator": "Mostrar ou ocultar navegador de prompts",
"helpDialog.item.abortActiveRun": "Interromper execução ativa (duplo clique)",
- "helpDialog.item.toggleRightSidebar": 'Alternar painel de contexto',
- "helpDialog.item.openRightSidebarGitTab": 'Abrir superfície do Git',
- "helpDialog.item.openRightSidebarFilesTab": 'Abrir superfície de arquivos',
"helpDialog.item.toggleTerminalDock": "Mostrar ou ocultar dock de terminal",
"helpDialog.item.toggleTerminalExpanded": "Expandir ou recolher o terminal",
- "helpDialog.item.togglePlanContextPanel": "Alternar painel de contexto do plano",
"helpDialog.item.cycleTheme": "Alternar tema (Claro → Escuro → Sistema)",
+ "helpDialog.item.switchSessionTab": "Alternar aba de sessão",
"helpDialog.item.switchContextSurface": "Alternar superfície do painel de contexto (tecla numérica)",
"helpDialog.item.toggleServicesMenu": "Mostrar ou ocultar menu de serviços",
- "helpDialog.item.cycleServicesTab": "Alternar aba de serviços",
"helpDialog.item.openSettings": "Abrir configurações",
"helpDialog.keyCombiner.or": "ou",
"helpDialog.proTips.title": "Dicas:",
"helpDialog.proTips.commandPalette": "Use a paleta de comandos ({shortcut}) para acessar rapidamente todas as ações",
"helpDialog.proTips.recentSessions": "As cinco sessões mais recentes aparecem na paleta de comandos",
- "helpDialog.proTips.themeCycling": "A alternância de tema lembra sua preferência entre sessões",
+ "helpDialog.proTips.leaderSequences": "Atalhos em duas etapas: pressione a combinação e depois a segunda tecla — Esc cancela",
"header.actions.rightSidebarWithShortcut": "Barra lateral direita ({shortcut})",
"header.actions.toggleRightSidebarAria": "Mostrar ou ocultar barra lateral direita",
"header.actions.openAppMenu": "Menu do OpenChamber",
@@ -2449,6 +2445,9 @@ export const dict: Record = {
"commandPalette.item.toggleSidebar": "Mostrar ou ocultar barra lateral",
"commandPalette.item.showContextUsage": "Mostrar uso do contexto",
"commandPalette.item.toggleTerminal": "Mostrar ou ocultar terminal",
+ "commandPalette.item.cycleTheme": "Alternar tema",
+ "commandPalette.item.showOpenCodeStatus": "Mostrar status do OpenCode",
+ "commandPalette.item.toggleMemoryDebug": "Alternar painel de depuração de memória",
"commandPalette.item.openSettings": "Abrir configurações...",
"commandPalette.session.untitled": "Sessão sem título",
"openCodeStatusDialog.title": "Status do OpenCode",
diff --git a/packages/ui/src/lib/i18n/messages/uk.settings.ts b/packages/ui/src/lib/i18n/messages/uk.settings.ts
index 11afb49c..0af768cb 100644
--- a/packages/ui/src/lib/i18n/messages/uk.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/uk.settings.ts
@@ -1110,9 +1110,8 @@ export const settingsDict = {
"settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label": "Розгорнути або згорнути термінал",
"settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label": "Додати виділення в чат",
"settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label": "Перемкнути бічну панель",
- "settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label": 'Перемкнути контекстну панель',
- "settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label": 'Відкрити поверхню Git',
- "settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label": 'Відкрити поверхню файлів',
+ "settings.openchamber.keyboardShortcuts.action.switch_session_tab.label": "Перемкнути вкладку сесії",
+ "settings.openchamber.keyboardShortcuts.action.switch_session_tab.suffix": " + 1…9",
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.label": "Перемкнути поверхню панелі контексту",
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix": " + 1…0",
"settings.openchamber.keyboardShortcuts.action.new_chat.label": "Нова сесія",
@@ -1120,9 +1119,7 @@ export const settingsDict = {
"settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label": "Нова чернетка worktree",
"settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Нове вікно Mini Chat",
"settings.openchamber.keyboardShortcuts.action.open_help.label": "Відкрити комбінації клавіш",
- "settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label": "Перемкнути контекстну панель плану",
"settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label": "Перемкнути меню сервісів",
- "settings.openchamber.keyboardShortcuts.action.cycle_services_tab.label": "Перемкнути вкладку сервісів",
"settings.openchamber.keyboardShortcuts.action.cycle_theme.label": "Перемкнути тему",
"settings.openchamber.keyboardShortcuts.action.cycle_agent.label": "Перемкнути агента",
"settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label": "Перемкнути улюблену модель вперед",
diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts
index 61163aa0..8d17da5a 100644
--- a/packages/ui/src/lib/i18n/messages/uk.ts
+++ b/packages/ui/src/lib/i18n/messages/uk.ts
@@ -1836,22 +1836,18 @@ export const dict: Record = {
"helpDialog.item.focusChatInput": "Фокус на полі вводу чату",
"helpDialog.item.togglePromptNavigator": "Показати або приховати навігатор промптів",
"helpDialog.item.abortActiveRun": "Перервати активний запуск (подвійне натискання)",
- "helpDialog.item.toggleRightSidebar": 'Перемкнути контекстну панель',
- "helpDialog.item.openRightSidebarGitTab": 'Відкрити поверхню Git',
- "helpDialog.item.openRightSidebarFilesTab": 'Відкрити поверхню файлів',
"helpDialog.item.toggleTerminalDock": "Перемкнути панель терміналу",
"helpDialog.item.toggleTerminalExpanded": "Розгорнути або згорнути термінал",
- "helpDialog.item.togglePlanContextPanel": "Перемкнути панель контексту плану",
"helpDialog.item.cycleTheme": "Перемкнути тему (Світла → Темна → Системна)",
+ "helpDialog.item.switchSessionTab": "Перемкнути вкладку сесії",
"helpDialog.item.switchContextSurface": "Перемкнути поверхню панелі контексту (цифрова клавіша)",
"helpDialog.item.toggleServicesMenu": "Перемкнути меню сервісів",
- "helpDialog.item.cycleServicesTab": "Перемкнути вкладку сервісів",
"helpDialog.item.openSettings": "Відкрити налаштування",
"helpDialog.keyCombiner.or": "або",
"helpDialog.proTips.title": "Поради:",
"helpDialog.proTips.commandPalette": "Використовуйте палітру команд ({shortcut}), щоб швидко перейти до будь-якої дії",
"helpDialog.proTips.recentSessions": "5 останніх сесій відображаються на панелі команд",
- "helpDialog.proTips.themeCycling": "Перемикання теми запам’ятовує ваші переваги протягом сесій",
+ "helpDialog.proTips.leaderSequences": "Двокрокові шорткати: натисни комбінацію, потім другу клавішу — Esc скасовує",
"header.actions.rightSidebarWithShortcut": "Права бічна панель ({shortcut})",
"header.actions.toggleRightSidebarAria": "Перемкнути праву бічну панель",
"header.actions.openAppMenu": "Меню OpenChamber",
@@ -2449,6 +2445,9 @@ export const dict: Record = {
"commandPalette.item.toggleSidebar": "Перемкнути бічну панель",
"commandPalette.item.showContextUsage": "Показати використання контексту",
"commandPalette.item.toggleTerminal": "Перемкнути термінал",
+ "commandPalette.item.cycleTheme": "Перемкнути тему",
+ "commandPalette.item.showOpenCodeStatus": "Показати статус OpenCode",
+ "commandPalette.item.toggleMemoryDebug": "Показати/сховати панель memory debug",
"commandPalette.item.openSettings": "Відкрити налаштування...",
"commandPalette.session.untitled": "Сесія без назви",
"openCodeStatusDialog.title": "Статус OpenCode",
diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts
index 59714a11..f898b951 100644
--- a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts
@@ -1110,9 +1110,8 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': '切换终端展开',
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': '将选中内容添加到聊天',
'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': '切换侧边栏',
- 'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': '切换上下文面板',
- 'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': '打开 Git 界面',
- 'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label': '打开文件界面',
+ 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.label': '切换会话标签页',
+ 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.suffix': ' + 1…9',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': '切换上下文面板界面',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': '新建会话',
@@ -1120,9 +1119,7 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '新建工作树草稿',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '新建 Mini Chat 窗口',
'settings.openchamber.keyboardShortcuts.action.open_help.label': '打开键盘快捷键',
- 'settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label': '切换上下文面板中的计划',
'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': '切换服务菜单',
- 'settings.openchamber.keyboardShortcuts.action.cycle_services_tab.label': '轮换服务菜单标签',
'settings.openchamber.keyboardShortcuts.action.cycle_theme.label': '轮换主题',
'settings.openchamber.keyboardShortcuts.action.cycle_agent.label': '轮换智能体',
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': '向前轮换收藏模型',
diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts
index 6ebc17c6..3e0d8295 100644
--- a/packages/ui/src/lib/i18n/messages/zh-CN.ts
+++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts
@@ -1824,22 +1824,18 @@ export const dict: Record = {
'helpDialog.item.focusChatInput': '聚焦聊天输入框',
'helpDialog.item.togglePromptNavigator': '显示或隐藏提示词导航',
'helpDialog.item.abortActiveRun': '中止当前运行(双击)',
- 'helpDialog.item.toggleRightSidebar': '切换上下文面板',
- 'helpDialog.item.openRightSidebarGitTab': '打开 Git 界面',
- 'helpDialog.item.openRightSidebarFilesTab': '打开文件界面',
'helpDialog.item.toggleTerminalDock': '切换终端停靠栏',
'helpDialog.item.toggleTerminalExpanded': '切换终端展开状态',
- 'helpDialog.item.togglePlanContextPanel': '切换计划上下文面板',
'helpDialog.item.cycleTheme': '循环切换主题(浅色 → 深色 → 跟随系统)',
+ 'helpDialog.item.switchSessionTab': '切换会话标签页',
'helpDialog.item.switchContextSurface': '切换上下文面板界面(数字键)',
'helpDialog.item.toggleServicesMenu': '切换服务菜单',
- 'helpDialog.item.cycleServicesTab': '循环服务标签',
'helpDialog.item.openSettings': '打开设置',
'helpDialog.keyCombiner.or': '或',
'helpDialog.proTips.title': '使用提示:',
'helpDialog.proTips.commandPalette': '使用命令面板({shortcut})可快速访问所有操作',
'helpDialog.proTips.recentSessions': '最近 5 个会话会显示在命令面板中',
- 'helpDialog.proTips.themeCycling': '主题循环会记住你在各会话中的偏好',
+ 'helpDialog.proTips.leaderSequences': '两段式快捷键:先按组合键,再按第二个键(Esc 取消)',
'header.actions.rightSidebarWithShortcut': '右侧边栏({shortcut})',
'header.actions.toggleRightSidebarAria': '切换右侧边栏',
'header.actions.openAppMenu': 'OpenChamber 菜单',
@@ -2449,6 +2445,9 @@ export const dict: Record = {
'commandPalette.item.toggleSidebar': '切换侧边栏',
'commandPalette.item.showContextUsage': '显示上下文用量',
'commandPalette.item.toggleTerminal': '切换终端',
+ 'commandPalette.item.cycleTheme': '轮换主题',
+ 'commandPalette.item.showOpenCodeStatus': '显示 OpenCode 状态',
+ 'commandPalette.item.toggleMemoryDebug': '切换内存调试面板',
'commandPalette.item.openSettings': '打开设置...',
'commandPalette.session.untitled': '未命名会话',
'openCodeStatusDialog.title': 'OpenCode 状态',
diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts
index af6b2fcb..fe7d3153 100644
--- a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts
@@ -1017,9 +1017,8 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': '切換終端機展開',
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': '將選取內容加入聊天',
'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': '切換側邊欄',
- 'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': '切換上下文面板',
- 'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': '開啟 Git 介面',
- 'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label': '開啟檔案介面',
+ 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.label': '切換工作階段分頁',
+ 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.suffix': ' + 1…9',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': '切換上下文面板介面',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': '新建工作階段',
@@ -1027,9 +1026,7 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '新增 worktree 草稿',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '新建 Mini Chat 視窗',
'settings.openchamber.keyboardShortcuts.action.open_help.label': '開啟鍵盤快速鍵',
- 'settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label': '切換上下文面板中的計畫',
'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': '切換服務選單',
- 'settings.openchamber.keyboardShortcuts.action.cycle_services_tab.label': '輪換服務選單分頁',
'settings.openchamber.keyboardShortcuts.action.cycle_theme.label': '輪換主題',
'settings.openchamber.keyboardShortcuts.action.cycle_agent.label': '輪換 agent',
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': '向前輪換收藏模型',
diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.ts b/packages/ui/src/lib/i18n/messages/zh-TW.ts
index c3cecc5e..52468dab 100644
--- a/packages/ui/src/lib/i18n/messages/zh-TW.ts
+++ b/packages/ui/src/lib/i18n/messages/zh-TW.ts
@@ -1828,22 +1828,18 @@ export const dict: Record = {
'helpDialog.item.focusChatInput': '聚焦聊天輸入框',
'helpDialog.item.togglePromptNavigator': '顯示或隱藏提示詞導覽',
'helpDialog.item.abortActiveRun': '中止目前執行(連按兩下)',
- 'helpDialog.item.toggleRightSidebar': '切換上下文面板',
- 'helpDialog.item.openRightSidebarGitTab': '開啟 Git 介面',
- 'helpDialog.item.openRightSidebarFilesTab': '開啟檔案介面',
'helpDialog.item.toggleTerminalDock': '切換終端機停靠欄',
'helpDialog.item.toggleTerminalExpanded': '切換終端機展開狀態',
- 'helpDialog.item.togglePlanContextPanel': '切換計畫上下文面板',
'helpDialog.item.cycleTheme': '循環切換主題(淺色 → 深色 → 跟隨系統)',
+ 'helpDialog.item.switchSessionTab': '切換工作階段分頁',
'helpDialog.item.switchContextSurface': '切換上下文面板介面(數字鍵)',
'helpDialog.item.toggleServicesMenu': '切換服務選單',
- 'helpDialog.item.cycleServicesTab': '循環服務標籤',
'helpDialog.item.openSettings': '開啟設定',
'helpDialog.keyCombiner.or': '或',
'helpDialog.proTips.title': '使用提示:',
'helpDialog.proTips.commandPalette': '使用命令面板({shortcut})可快速存取所有操作',
'helpDialog.proTips.recentSessions': '最近 5 個會话會顯示在命令面板中',
- 'helpDialog.proTips.themeCycling': '主題循環會記住你在各會話中的偏好',
+ 'helpDialog.proTips.leaderSequences': '兩段式快捷鍵:先按組合鍵,再按第二個鍵(Esc 取消)',
'header.actions.rightSidebarWithShortcut': '右側邊欄({shortcut})',
'header.actions.toggleRightSidebarAria': '切換右側邊欄',
'header.actions.openAppMenu': 'OpenChamber 選單',
@@ -2453,6 +2449,9 @@ export const dict: Record = {
'commandPalette.item.toggleSidebar': '切換側邊欄',
'commandPalette.item.showContextUsage': '顯示上下文用量',
'commandPalette.item.toggleTerminal': '切換終端機',
+ 'commandPalette.item.cycleTheme': '輪換主題',
+ 'commandPalette.item.showOpenCodeStatus': '顯示 OpenCode 狀態',
+ 'commandPalette.item.toggleMemoryDebug': '切換記憶體偵錯面板',
'commandPalette.item.openSettings': '開啟設定...',
'commandPalette.session.untitled': '未命名會話',
'openCodeStatusDialog.title': 'OpenCode 狀態',
diff --git a/packages/ui/src/lib/sessionTabs.ts b/packages/ui/src/lib/sessionTabs.ts
index b3ce2bb6..a1b490b0 100644
--- a/packages/ui/src/lib/sessionTabs.ts
+++ b/packages/ui/src/lib/sessionTabs.ts
@@ -9,6 +9,23 @@ import { useSessionUIStore } from '@/sync/session-ui-store';
* count as neighbours — the same rule the strip uses for rendering. The
* session itself is never touched.
*/
+/**
+ * Activate the nth (0-based) header session tab, counting only tabs whose
+ * session is present in the loaded session list — the same rule the strip
+ * uses for rendering, so the digit matches what the user sees.
+ */
+export const activateSessionTabByIndex = (index: number): boolean => {
+ const { tabIds } = useSessionTabsStore.getState();
+ const sessionsById = new Map(
+ useGlobalSessionsStore.getState().activeSessions.map((session) => [session.id, session] as const),
+ );
+ const renderable = tabIds.filter((id) => sessionsById.has(id));
+ const session = renderable[index] ? sessionsById.get(renderable[index]) : null;
+ if (!session) return false;
+ useSessionUIStore.getState().setCurrentSession(session.id, resolveGlobalSessionDirectory(session));
+ return true;
+};
+
export const closeSessionTabAndActivateNeighbour = (sessionId: string): void => {
const { tabIds, closeTab } = useSessionTabsStore.getState();
if (!tabIds.includes(sessionId)) return;
diff --git a/packages/ui/src/lib/shortcuts/DOCUMENTATION.md b/packages/ui/src/lib/shortcuts/DOCUMENTATION.md
index e2d3db84..06a68b4f 100644
--- a/packages/ui/src/lib/shortcuts/DOCUMENTATION.md
+++ b/packages/ui/src/lib/shortcuts/DOCUMENTATION.md
@@ -25,9 +25,9 @@ Component interaction keys that are not application commands, such as list navig
# Binding rules
-Bindings remain persisted as `Record`. Each binding has one chord or at most two space-separated chords, such as `mod+s p`. `mod` is the platform-neutral primary modifier (Command on macOS, Control elsewhere), while `alt` is the platform-neutral alternate modifier (Option on macOS, Alt elsewhere); `command`, `cmd`, `meta`, and `option` are accepted input aliases but normalize to those canonical tokens. `normalizeCombo`, `parseShortcut`, `formatShortcutForDisplay`, and `getShortcutConflict` provide the shared parsing and validation behavior. Display formatting uses macOS keyboard symbols (`⌘`, `⌥`, `⌃`, `⇧`) on macOS and named modifiers (`Ctrl`, `Alt`, `Shift`) elsewhere, including tooltip and accessible text consumers. A single chord conflicts with a sequence sharing its first chord; sibling sequences are valid.
+Bindings remain persisted as `Record`. Each binding has one chord or at most two space-separated chords, such as `mod+k p`. `mod` is the platform-neutral primary modifier (Command on macOS, Control elsewhere), while `alt` is the platform-neutral alternate modifier (Option on macOS, Alt elsewhere); `command`, `cmd`, `meta`, and `option` are accepted input aliases but normalize to those canonical tokens. `normalizeCombo`, `parseShortcut`, `formatShortcutForDisplay`, and `getShortcutConflict` provide the shared parsing and validation behavior. Display formatting uses macOS keyboard symbols (`⌘`, `⌥`, `⌃`, `⇧`) on macOS and named modifiers (`Ctrl`, `Alt`, `Shift`) elsewhere, including tooltip and accessible text consumers. A single chord conflicts with a sequence sharing its first chord; sibling sequences are valid.
-Contextual internal commands may deliberately share a sequence leader. The single-chord handler gets the first chance to handle the event; returning `false` lets the dispatcher start the sequence. The active file editor therefore owns `mod+s` for saving, while a mounted but unfocused editor yields `mod+s p`, `mod+s g`, and `mod+s l` to the draft target pickers and session list.
+The default layout follows three modes: single chords for everyday actions, the `mod+k` leader for open/go actions (`mod+k p`, `mod+k g`, `mod+k l`, `mod+k t`, `mod+k n`, `mod+k i`, `mod+k h`), and held digit prefixes — held `mod` + digit switches header session tabs, held `mod+alt` + digit switches context panel surfaces. Every schema action ships with a default binding; palette-only commands (context surfaces, OpenCode status, memory debug) live outside the schema and the palette invokes their owning modules directly. Single-chord handlers still get the first chance at a leader's chord; returning `false` lets the dispatcher arm the sequence.
The internal `switch_tab_*` bindings remain available to mobile handlers. Desktop numeric context-surface switching is resolved by the configurable `switch_context_surface` prefix before normal dispatcher matching and falls through on mobile.
diff --git a/packages/ui/src/lib/shortcuts/bindings.test.ts b/packages/ui/src/lib/shortcuts/bindings.test.ts
index be955841..893057c0 100644
--- a/packages/ui/src/lib/shortcuts/bindings.test.ts
+++ b/packages/ui/src/lib/shortcuts/bindings.test.ts
@@ -1,6 +1,7 @@
import { describe, expect, test } from 'bun:test';
import {
+ eventMatchesShortcut,
eventMatchesShortcutPrefix,
formatShortcutForDisplay,
getEffectiveShortcutPrefix,
@@ -9,12 +10,13 @@ import {
isShortcutPrefixHeld,
normalizeCombo,
parseShortcut,
+ resolveShortcutEventDigit,
UNASSIGNED_SHORTCUT,
} from './index';
describe('getEffectiveShortcutPrefix', () => {
- test('falls back to the action default (bare mod) when unset', () => {
- expect(getEffectiveShortcutPrefix('switch_context_surface', {})).toBe('mod');
+ test('falls back to the action default (bare mod+alt) when unset', () => {
+ expect(getEffectiveShortcutPrefix('switch_context_surface', {})).toBe('mod+alt');
});
test('honors modifier + key overrides', () => {
@@ -126,3 +128,31 @@ describe('platform shortcut labels', () => {
expect(formatShortcutForDisplay('alt', 'Unassigned', 'other')).toBe('Alt');
});
});
+
+describe('layout-independent key matching', () => {
+ const event = (overrides: Partial): KeyboardEvent =>
+ // SAFETY: the matcher only reads the modifier flags, key, and code
+ // provided here; a full KeyboardEvent is not constructible in bun tests.
+ ({ altKey: false, ctrlKey: false, metaKey: false, shiftKey: false, key: '', code: '', ...overrides }) as KeyboardEvent;
+
+ test('a non-Latin layout letter matches through the physical key code', () => {
+ expect(eventMatchesShortcut(event({ ctrlKey: true, key: 'л', code: 'KeyK' }), 'mod+k')).toBe(true);
+ expect(eventMatchesShortcut(event({ key: 'з', code: 'KeyP' }), 'p')).toBe(true);
+ });
+
+ test('macOS Option symbol substitution matches through the digit code', () => {
+ expect(eventMatchesShortcut(event({ ctrlKey: true, altKey: true, key: '¡', code: 'Digit1' }), 'mod+alt+1')).toBe(true);
+ });
+
+ test('Latin layouts that move keys keep their key-based meaning', () => {
+ // Dvorak: physical KeyT produces "y"; the binding follows the character.
+ expect(eventMatchesShortcut(event({ ctrlKey: true, key: 'y', code: 'KeyT' }), 'mod+y')).toBe(true);
+ expect(eventMatchesShortcut(event({ ctrlKey: true, key: 'y', code: 'KeyT' }), 'mod+t')).toBe(false);
+ });
+
+ test('resolveShortcutEventDigit reads the digit from the code under Option', () => {
+ expect(resolveShortcutEventDigit({ key: '¡', code: 'Digit1' })).toBe('1');
+ expect(resolveShortcutEventDigit({ key: '5', code: 'Digit5' })).toBe('5');
+ expect(resolveShortcutEventDigit({ key: 'a', code: 'KeyA' })).toBe(null);
+ });
+});
diff --git a/packages/ui/src/lib/shortcuts/bindings.ts b/packages/ui/src/lib/shortcuts/bindings.ts
index fa45a358..1a1b259b 100644
--- a/packages/ui/src/lib/shortcuts/bindings.ts
+++ b/packages/ui/src/lib/shortcuts/bindings.ts
@@ -247,6 +247,50 @@ export function isRiskyBrowserShortcut(combo: ShortcutCombo): boolean {
});
}
+const CODE_KEY_MAP = new Map([
+ ['Comma', ','],
+ ['Period', '.'],
+ ['Slash', '/'],
+ ['Backquote', '`'],
+ ['BracketLeft', '['],
+ ['BracketRight', ']'],
+ ['Semicolon', ';'],
+ ['Quote', "'"],
+ ['Minus', '-'],
+ ['Equal', '='],
+]);
+
+function keyFromEventCode(code: string): string | null {
+ if (code.startsWith('Key') && code.length === 4) return code.slice(3).toLowerCase();
+ if (code.startsWith('Digit') && code.length === 6) return code.slice(5);
+ return CODE_KEY_MAP.get(code) ?? null;
+}
+
+/**
+ * The character a physical key press should match against bindings. `key`
+ * carries the layout-produced character: Option on macOS substitutes symbols
+ * ("¡" for ⌥1) and non-Latin layouts substitute their own alphabet ("л" for
+ * K). Both keep the physical key in `code`, so those two cases fall back to
+ * it; Latin layouts that MOVE keys (Dvorak, AZERTY) keep their `key`-based
+ * meaning untouched.
+ */
+export function resolveShortcutEventKey(
+ event: Pick,
+): string {
+ const raw = event.key;
+ if (event.altKey) return keyFromEventCode(event.code) ?? raw;
+ if (raw.length === 1 && raw.charCodeAt(0) > 127) return keyFromEventCode(event.code) ?? raw;
+ return raw;
+}
+
+/** The digit a press addresses, layout- and Option-proof via `code`. */
+export function resolveShortcutEventDigit(
+ event: Pick,
+): string | null {
+ if (event.code.startsWith('Digit') && event.code.length === 6) return event.code.slice(5);
+ return event.key.length === 1 && event.key >= '0' && event.key <= '9' ? event.key : null;
+}
+
export function eventMatchesShortcut(
event: KeyboardEvent | React.KeyboardEvent,
combo: ShortcutCombo,
@@ -280,16 +324,7 @@ export function eventMatchesShortcut(
if (event.ctrlKey && !ctrlUsedAsMod) return false;
}
- let eventKeyRaw = event.key;
- if (event.altKey) {
- if (event.code.startsWith('Key') && event.code.length === 4) {
- eventKeyRaw = event.code.slice(3).toLowerCase();
- } else if (event.code.startsWith('Digit') && event.code.length === 6) {
- eventKeyRaw = event.code.slice(5);
- }
- }
-
- return keyToShortcutToken(eventKeyRaw) === keyToShortcutToken(chord.key);
+ return keyToShortcutToken(resolveShortcutEventKey(event)) === keyToShortcutToken(chord.key);
}
export function isShortcutPrefixHeld(prefixCombo: ShortcutCombo, heldKeys: ReadonlySet): boolean {
diff --git a/packages/ui/src/lib/shortcuts/config.ts b/packages/ui/src/lib/shortcuts/config.ts
index 0e18660d..0cdf024b 100644
--- a/packages/ui/src/lib/shortcuts/config.ts
+++ b/packages/ui/src/lib/shortcuts/config.ts
@@ -5,7 +5,6 @@ type ShortcutCategory = 'session' | 'models' | 'panels' | 'navigation' | 'applic
type ShortcutConfig = {
id: string;
defaultBinding: ShortcutCombo;
- allowsSequenceFallback?: true;
/** The binding is a bare-modifier chord prefix (completed by another key);
conflict resolution compares its prefix rather than a full combo. */
prefixStyle?: true;
@@ -17,12 +16,18 @@ type ShortcutConfig = {
}
);
+// Default layout, unified around three modes:
+// - Single chords for everyday actions.
+// - The mod+k leader for "open/go" actions, second key mnemonic.
+// - Held mod + digit switches header session tabs; held mod+alt + digit
+// switches context panel surfaces (mod+shift+digit is reserved by macOS
+// screenshots).
+// Everything else lives only in the command palette, outside this schema.
const SHORTCUT_GROUPS = {
session: [
{
id: 'add_selection_to_chat',
defaultBinding: 'mod+l',
- allowsSequenceFallback: true,
customizable: true,
settingsLabelKey:
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label',
@@ -35,7 +40,7 @@ const SHORTCUT_GROUPS = {
},
{
id: 'open_timeline_dialog',
- defaultBinding: 'mod+t',
+ defaultBinding: 'mod+k t',
customizable: true,
settingsLabelKey:
'settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label',
@@ -54,21 +59,21 @@ const SHORTCUT_GROUPS = {
},
{
id: 'open_draft_project_picker',
- defaultBinding: 'mod+s p',
+ defaultBinding: 'mod+k p',
customizable: true,
settingsLabelKey:
'settings.openchamber.keyboardShortcuts.action.open_draft_project_picker.label',
},
{
id: 'open_draft_worktree_picker',
- defaultBinding: 'mod+s g',
+ defaultBinding: 'mod+k g',
customizable: true,
settingsLabelKey:
'settings.openchamber.keyboardShortcuts.action.open_draft_worktree_picker.label',
},
{
id: 'open_session_list',
- defaultBinding: 'mod+s l',
+ defaultBinding: 'mod+k l',
customizable: true,
settingsLabelKey:
'settings.openchamber.keyboardShortcuts.action.open_session_list.label',
@@ -146,65 +151,45 @@ const SHORTCUT_GROUPS = {
},
{
id: 'toggle_sidebar',
- defaultBinding: 'mod+alt+l',
+ defaultBinding: 'mod+b',
customizable: true,
settingsLabelKey: 'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label',
},
{
id: 'toggle_prompt_navigator',
- defaultBinding: 'mod+alt+p',
+ defaultBinding: 'mod+k n',
customizable: true,
settingsLabelKey:
'settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label',
},
{
- id: 'toggle_right_sidebar',
- defaultBinding: 'mod+b',
- customizable: true,
- settingsLabelKey:
- 'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label',
- },
- {
- id: 'open_right_sidebar_git',
- defaultBinding: 'mod+shift+g',
- customizable: true,
- settingsLabelKey:
- 'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label',
- },
- {
- id: 'open_right_sidebar_files',
- defaultBinding: 'mod+shift+f',
- customizable: true,
- settingsLabelKey:
- 'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label',
- },
- {
- id: 'switch_context_surface',
+ id: 'switch_session_tab',
defaultBinding: 'mod',
// The binding is a bare modifier acting as a chord prefix (completed by
// a digit); conflict resolution must compare its PREFIX, not a combo.
prefixStyle: true,
customizable: true,
+ settingsLabelKey:
+ 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.label',
+ },
+ {
+ id: 'switch_context_surface',
+ defaultBinding: 'mod+alt',
+ prefixStyle: true,
+ customizable: true,
settingsLabelKey:
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label',
},
- {
- id: 'toggle_context_plan',
- defaultBinding: 'mod+shift+p',
- customizable: true,
- settingsLabelKey:
- 'settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label',
- },
{
id: 'toggle_services_menu',
- defaultBinding: 'mod+shift+s',
+ defaultBinding: 'mod+k i',
customizable: true,
settingsLabelKey:
'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label',
},
],
navigation: [
- { id: 'save_file', defaultBinding: 'mod+s', customizable: false, allowsSequenceFallback: true },
+ { id: 'save_file', defaultBinding: 'mod+s', customizable: false },
{ id: 'find_in_file', defaultBinding: 'mod+f', customizable: false },
{
id: 'open_go_to_line',
@@ -212,13 +197,6 @@ const SHORTCUT_GROUPS = {
customizable: true,
settingsLabelKey: 'settings.openchamber.keyboardShortcuts.action.open_go_to_line.label',
},
- {
- id: 'cycle_services_tab',
- defaultBinding: 'mod+shift+[',
- customizable: true,
- settingsLabelKey:
- 'settings.openchamber.keyboardShortcuts.action.cycle_services_tab.label',
- },
],
application: [
{
@@ -228,7 +206,6 @@ const SHORTCUT_GROUPS = {
settingsLabelKey:
'settings.openchamber.keyboardShortcuts.action.open_command_palette.label',
},
- { id: 'open_status', defaultBinding: 'mod+shift+o', customizable: false },
{
id: 'open_settings',
defaultBinding: 'mod+comma',
@@ -237,17 +214,16 @@ const SHORTCUT_GROUPS = {
},
{
id: 'open_help',
- defaultBinding: 'mod+.',
+ defaultBinding: 'mod+k h',
customizable: true,
settingsLabelKey: 'settings.openchamber.keyboardShortcuts.action.open_help.label',
},
{
id: 'cycle_theme',
- defaultBinding: 'mod+/',
+ defaultBinding: 'mod+k c',
customizable: true,
settingsLabelKey: 'settings.openchamber.keyboardShortcuts.action.cycle_theme.label',
},
- { id: 'toggle_memory_debug', defaultBinding: 'mod+shift+d', customizable: false },
],
} as const satisfies Record;
diff --git a/packages/ui/src/lib/shortcuts/index.ts b/packages/ui/src/lib/shortcuts/index.ts
index b75c3200..28208074 100644
--- a/packages/ui/src/lib/shortcuts/index.ts
+++ b/packages/ui/src/lib/shortcuts/index.ts
@@ -8,6 +8,8 @@ export {
keyToShortcutToken,
normalizeCombo,
parseShortcut,
+ resolveShortcutEventDigit,
+ resolveShortcutEventKey,
UNASSIGNED_SHORTCUT,
} from './bindings';
export type { ShortcutCombo } from './bindings';
diff --git a/packages/ui/src/lib/shortcuts/registry.ts b/packages/ui/src/lib/shortcuts/registry.ts
index dca7971c..2f6ffb27 100644
--- a/packages/ui/src/lib/shortcuts/registry.ts
+++ b/packages/ui/src/lib/shortcuts/registry.ts
@@ -39,6 +39,14 @@ export class ShortcutRegistry {
return this.handlers.get(actionId)?.[0]?.handler;
}
+ /** Runs an action outside keyboard dispatch (command palette). Bypasses
+ suspension: the invoking surface, not the keyboard, owns the gesture. */
+ invoke(actionId: ShortcutActionId): boolean {
+ const handler = this.handlers.get(actionId)?.[0]?.handler;
+ if (!handler) return false;
+ return handler(new KeyboardEvent('keydown')) !== false;
+ }
+
/** Temporarily disables every registered application shortcut. */
suspend(): () => void {
this.suspensionCount += 1;
diff --git a/packages/ui/src/lib/shortcuts/schema.test.ts b/packages/ui/src/lib/shortcuts/schema.test.ts
index fa278128..c82c3077 100644
--- a/packages/ui/src/lib/shortcuts/schema.test.ts
+++ b/packages/ui/src/lib/shortcuts/schema.test.ts
@@ -49,13 +49,31 @@ describe('shortcut schema', () => {
))).toBe(true);
});
- test('includes session prefix bindings and metadata', () => {
- expect(getShortcutAction('open_draft_project_picker')?.defaultBinding).toBe('mod+s p');
- expect(getShortcutAction('open_draft_worktree_picker')?.defaultBinding).toBe('mod+s g');
- expect(getShortcutAction('open_session_list')?.defaultBinding).toBe('mod+s l');
+ test('keeps the mod+k leader for open/go actions', () => {
+ expect(getShortcutAction('open_draft_project_picker')?.defaultBinding).toBe('mod+k p');
+ expect(getShortcutAction('open_draft_worktree_picker')?.defaultBinding).toBe('mod+k g');
+ expect(getShortcutAction('open_session_list')?.defaultBinding).toBe('mod+k l');
+ expect(getShortcutAction('open_timeline_dialog')?.defaultBinding).toBe('mod+k t');
+ expect(getShortcutAction('toggle_prompt_navigator')?.defaultBinding).toBe('mod+k n');
+ expect(getShortcutAction('toggle_services_menu')?.defaultBinding).toBe('mod+k i');
+ expect(getShortcutAction('open_help')?.defaultBinding).toBe('mod+k h');
+ expect(getShortcutAction('cycle_theme')?.defaultBinding).toBe('mod+k c');
expect(getShortcutAction('focus_input')?.category).toBe('session');
});
+ test('splits the held digit prefixes between session tabs and surfaces', () => {
+ expect(getShortcutAction('switch_session_tab')?.defaultBinding).toBe('mod');
+ expect(getShortcutAction('switch_context_surface')?.defaultBinding).toBe('mod+alt');
+ });
+
+ test('every action ships with a default binding', () => {
+ // Palette-only commands live outside this schema entirely; an action in
+ // the schema without a binding would be dead weight in Settings.
+ for (const action of SHORTCUT_SCHEMA) {
+ expect(getEffectiveShortcutCombo(action.id)).not.toBe('');
+ }
+ });
+
test('preserves valid overrides and falls back from malformed bindings', () => {
expect(getEffectiveShortcutCombo('new_chat', { new_chat: 'mod+k' })).toBe('mod+k');
expect(getEffectiveShortcutCombo('new_chat', { new_chat: 'mod+k x y' })).toBe('mod+n');
@@ -73,10 +91,8 @@ describe('shortcut schema', () => {
.find((conflict) => conflict.action.id === 'find_in_file');
const internalPrefixConflict = getShortcutBindingConflicts('new_chat', 'mod+s x')
.find((conflict) => conflict.action.id === 'save_file');
- const contextualPrefixConflict = getShortcutBindingConflicts('focus_input', 'mod+l l')
- .find((conflict) => conflict.action.id === 'add_selection_to_chat');
- const contextualLeaderConflict = getShortcutBindingConflicts('add_selection_to_chat', 'mod+s')
- .find((conflict) => conflict.action.id === 'open_draft_project_picker');
+ const leaderPrefixConflict = getShortcutBindingConflicts('new_chat', 'mod+k')
+ .find((conflict) => conflict.action.id === 'open_session_list');
const blockingPrefixConflict = getShortcutBindingConflicts('new_chat', 'mod+p x')
.find((conflict) => conflict.action.id === 'open_command_palette');
@@ -84,10 +100,9 @@ describe('shortcut schema', () => {
expect(customizableConflict?.action.customizable).toBe(true);
expect(internalConflict?.kind).toBe('exact');
expect(internalConflict?.action.customizable).toBe(false);
- expect(internalPrefixConflict?.kind).toBe('contextual-prefix');
+ expect(internalPrefixConflict?.kind).toBe('prefix');
expect(internalPrefixConflict?.action.customizable).toBe(false);
- expect(contextualPrefixConflict?.kind).toBe('contextual-prefix');
- expect(contextualLeaderConflict?.kind).toBe('contextual-prefix');
+ expect(leaderPrefixConflict?.kind).toBe('prefix');
expect(blockingPrefixConflict?.kind).toBe('prefix');
});
});
diff --git a/packages/ui/src/lib/shortcuts/schema.ts b/packages/ui/src/lib/shortcuts/schema.ts
index 6afe81ec..e35977a5 100644
--- a/packages/ui/src/lib/shortcuts/schema.ts
+++ b/packages/ui/src/lib/shortcuts/schema.ts
@@ -15,29 +15,14 @@ export type ShortcutAction = (typeof SHORTCUT_SCHEMA)[number];
export type ShortcutActionId = ShortcutAction['id'];
export type ShortcutCategory = ShortcutAction['category'];
export type CustomizableShortcutAction = Extract;
+/** 'contextual-prefix' is kept in the union for the recording dialog's
+ messaging even though no default layout produces it any more. */
export type ShortcutBindingConflictKind = ShortcutConflict | 'contextual-prefix';
export type ShortcutBindingConflict = {
action: ShortcutAction;
kind: ShortcutBindingConflictKind;
};
-function allowsContextualPrefix(
- action: ShortcutAction,
- combo: ShortcutCombo,
- candidate: ShortcutAction,
- candidateCombo: ShortcutCombo,
-): boolean {
- const chordCount = parseShortcut(combo)?.chords.length;
- const candidateChordCount = parseShortcut(candidateCombo)?.chords.length;
- if (chordCount === 1 && candidateChordCount === 2) {
- return 'allowsSequenceFallback' in action && action.allowsSequenceFallback;
- }
- if (chordCount === 2 && candidateChordCount === 1) {
- return 'allowsSequenceFallback' in candidate && candidate.allowsSequenceFallback;
- }
- return false;
-}
-
export function getShortcutAction(id: string): ShortcutAction | undefined {
return SHORTCUT_SCHEMA.find((action) => action.id === id);
}
@@ -54,7 +39,8 @@ export function getEffectiveShortcutCombo(
): ShortcutCombo {
const action = getShortcutAction(actionId);
if (!action) return '';
- if (!action.customizable) return action.defaultBinding;
+ const defaultBinding = action.defaultBinding === UNASSIGNED_SHORTCUT ? '' : action.defaultBinding;
+ if (!action.customizable) return defaultBinding;
const override = overrides?.[actionId];
if (typeof override === 'string') {
@@ -63,7 +49,7 @@ export function getEffectiveShortcutCombo(
if (isValidShortcutCombo(normalized)) return normalized;
}
- return action.defaultBinding;
+ return defaultBinding;
}
export function getEffectiveShortcutPrefix(
@@ -100,12 +86,7 @@ export function getShortcutBindingConflicts(
: getEffectiveShortcutCombo(candidate.id, overrides);
const kind = getShortcutConflict(combo, candidateCombo);
if (!kind) continue;
- conflicts.push({
- action: candidate,
- kind: kind === 'prefix' && allowsContextualPrefix(action, combo, candidate, candidateCombo)
- ? 'contextual-prefix'
- : kind,
- });
+ conflicts.push({ action: candidate, kind });
}
return conflicts;
}
diff --git a/packages/ui/src/stores/useUIStore.ts b/packages/ui/src/stores/useUIStore.ts
index e24e8ead..c377f27b 100644
--- a/packages/ui/src/stores/useUIStore.ts
+++ b/packages/ui/src/stores/useUIStore.ts
@@ -2412,7 +2412,7 @@ export const useUIStore = create()(
{
name: 'ui-store',
storage: createDeferredSafeJSONStorage(),
- version: 17,
+ version: 18,
migrate: (persistedState, version) => {
if (!persistedState || typeof persistedState !== 'object') {
return persistedState;
@@ -2433,6 +2433,15 @@ export const useUIStore = create()(
delete state.expandedEditorToolbar;
}
+ // v17 -> v18: the default shortcut layout was redesigned around the
+ // mod+k leader and the held digit prefixes. Old overrides were
+ // recorded against the previous defaults (e.g. a bare 'mod' surface
+ // prefix now collides with session tabs), so custom bindings start
+ // fresh on the new system.
+ if (version < 18) {
+ delete state.shortcutOverrides;
+ }
+
// v13 -> v14: the separate 'preview' surface merged into 'browser'.
// Stored preview tabs keep their URL and become browser tabs; their
// id encodes the mode, so it is rebuilt rather than left dangling.
diff --git a/packages/ui/tests/ego-lite/shortcut-registry.checklist.yaml b/packages/ui/tests/ego-lite/shortcut-registry.checklist.yaml
index f072de87..b2c3b072 100644
--- a/packages/ui/tests/ego-lite/shortcut-registry.checklist.yaml
+++ b/packages/ui/tests/ego-lite/shortcut-registry.checklist.yaml
@@ -172,12 +172,12 @@ checks:
preconditions:
- Open a draft session with project and worktree selectors mounted.
steps:
- - Trigger Mod + S, P and verify the project picker opens.
+ - Trigger Mod + K, P and verify the project picker opens.
- Press Escape once and verify it closes.
- - Trigger Mod + S, G and verify the worktree picker opens.
+ - Trigger Mod + K, G and verify the worktree picker opens.
- Press Escape once and verify it closes.
assertions:
- - A contextual Mod + S owner yields when its context is inactive.
+ - The Mod + K leader arms without a visible menu and completes on the second key.
- Each sequence opens only its target picker.
- One non-IME Escape closes either controlled picker.
evidence: