feat(ui): show platform-specific shortcut labels

This commit is contained in:
ChangeHow
2026-08-06 14:18:15 +08:00
parent 8d968f3d71
commit 45a792d657
34 changed files with 135 additions and 71 deletions
@@ -5,7 +5,12 @@ import React from 'react';
import { Icon } from '@/components/icon/Icon';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useI18n } from '@/lib/i18n';
import { cn, isMacOS } from '@/lib/utils';
import {
formatShortcutForDisplay,
getEffectiveShortcutCombo,
} from '@/lib/shortcuts';
import { cn } from '@/lib/utils';
import { useUIStore } from '@/stores/useUIStore';
type FocusModeButtonProps = {
footerIconButtonClass: string;
@@ -17,6 +22,12 @@ type FocusModeButtonProps = {
export const FocusModeButton = React.memo(function FocusModeButton(props: FocusModeButtonProps) {
const { footerIconButtonClass, iconSizeClass, isExpandedInput, onToggle } = props;
const { t } = useI18n();
const expandInputShortcutOverride = useUIStore((state) => state.shortcutOverrides.expand_input);
const expandInputCombo = getEffectiveShortcutCombo(
'expand_input',
expandInputShortcutOverride === undefined ? undefined : { expand_input: expandInputShortcutOverride },
);
const shortcut = expandInputCombo ? formatShortcutForDisplay(expandInputCombo) : null;
return (
<Tooltip>
@@ -43,9 +54,7 @@ export const FocusModeButton = React.memo(function FocusModeButton(props: FocusM
<TooltipContent side="top" sideOffset={8}>
<div className="flex flex-col gap-0.5 text-center">
<span>{t('chat.chatInput.focusMode.label')}</span>
<span className="font-mono opacity-60">
{isMacOS() ? '⌘⇧E' : 'Ctrl+Shift+E'}
</span>
{shortcut ? <span className="font-mono opacity-60">{shortcut}</span> : null}
</div>
</TooltipContent>
</Tooltip>
@@ -5,6 +5,7 @@ import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import { useDeviceInfo } from '@/lib/device';
import { useI18n } from '@/lib/i18n';
import { formatShortcutForDisplay } from '@/lib/shortcuts';
export interface InlineCommentInputProps {
initialText?: string;
@@ -35,6 +36,7 @@ export function InlineCommentInput({
const { isMobile } = useDeviceInfo();
const [text, setText] = React.useState(initialText);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const saveShortcut = formatShortcutForDisplay('mod+enter');
const handleTextChange = (value: string) => {
setText(value);
@@ -154,7 +156,9 @@ export function InlineCommentInput({
value={text}
onChange={(e) => handleTextChange(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={isMobile ? t('inlineComment.input.placeholderShort') : t('inlineComment.input.placeholder')}
placeholder={isMobile
? t('inlineComment.input.placeholderShort')
: t('inlineComment.input.placeholder', { shortcut: saveShortcut })}
outerClassName="rounded-[var(--radius-xl)] bg-[var(--surface-subtle)] ring-1 ring-inset ring-border/60 focus-within:ring-2 focus-within:ring-[var(--interactive-focus-ring)]"
className="min-h-[80px] px-3 py-2.5 text-sm resize-y"
/>
@@ -64,6 +64,7 @@ import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import type { TerminalShellOption } from '@/lib/api/types';
import { isTerminalShell } from '@/lib/terminalShell';
import { subscribeRuntimeEndpointChanged } from '@/lib/runtime-switch';
import { formatShortcutForDisplay } from '@/lib/shortcuts';
interface Option<T extends string> {
id: T;
@@ -1543,7 +1544,10 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
label={t('settings.openchamber.visual.field.terminalQuickKeys')}
ariaLabel={t('settings.openchamber.visual.field.terminalQuickKeysAria')}
settingsItem="appearance.terminal-quick-keys"
info={t('settings.openchamber.visual.field.terminalQuickKeysTooltip')}
info={t('settings.openchamber.visual.field.terminalQuickKeysTooltip', {
control: formatShortcutForDisplay('ctrl'),
alt: formatShortcutForDisplay('alt'),
})}
/>
)}
{showTerminalShellSetting && (
@@ -22,6 +22,7 @@ import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
import { Icon } from "@/components/icon/Icon";
import { opencodeClient } from '@/lib/opencode/client';
import { useI18n } from '@/lib/i18n';
import { formatShortcutForDisplay } from '@/lib/shortcuts';
interface DirectoryExplorerDialogProps {
open: boolean;
@@ -338,9 +339,7 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
const hasHighlightedBrowseItem = Boolean(
highlightedRow && (highlightedRow.type === 'up' || (highlightedRow.type === 'directory' && !highlightedRow.disabled))
);
const submitModifierLabel = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform)
? '⌘'
: 'Ctrl';
const submitModifierLabel = formatShortcutForDisplay('mod');
const submitActionLabel = isAlreadyAdded
? t('directoryExplorerDialog.actions.alreadyAdded')
: isCloneMode
+2 -4
View File
@@ -11,7 +11,6 @@ import { useUIStore } from "@/stores/useUIStore";
import {
getEffectiveShortcutCombo,
getShortcutAction,
getModifierLabel,
formatShortcutForDisplay,
type ShortcutActionId,
} from "@/lib/shortcuts";
@@ -44,7 +43,6 @@ export const HelpDialog: React.FC = () => {
const isHelpDialogOpen = useUIStore((state) => state.isHelpDialogOpen);
const setHelpDialogOpen = useUIStore((state) => state.setHelpDialogOpen);
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
const mod = getModifierLabel();
const isVSCode = isVSCodeRuntime();
const shortcuts: ShortcutSection[] = [
@@ -104,7 +102,7 @@ export const HelpDialog: React.FC = () => {
keys: '',
},
{
keys: [`Shift + Alt + ${mod} + N`],
keys: [formatShortcutForDisplay('mod+shift+alt+n')],
descriptionKey: "helpDialog.item.newWindow",
icon: "window",
},
@@ -195,7 +193,7 @@ export const HelpDialog: React.FC = () => {
keys: '',
},
{
keys: [`${mod} + 1...0`],
keys: [`${formatShortcutForDisplay('mod')} + 1...0`],
descriptionKey: "helpDialog.item.switchContextSurface",
icon: "layout-right",
},
@@ -53,7 +53,7 @@ import { getOutsideFileGrant } from '@/lib/outsideFileGrants';
import { DiagramEditor } from '@/components/diagram';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { useKeybind, useKeybinds } from '@/hooks/useKeybind';
import { getModifierLabel } from '@/lib/shortcuts';
import { formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts';
import { EditorView } from '@codemirror/view';
import type { Extension } from '@codemirror/state';
import { useThemeSystem } from '@/contexts/useThemeSystem';
@@ -3167,6 +3167,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
}
const docked = layout === 'docked';
const saveShortcut = formatShortcutForDisplay(getEffectiveShortcutCombo('save_file'));
const wrapperCls = docked
? 'pointer-events-auto flex flex-wrap items-center gap-1'
: 'pointer-events-auto flex items-center gap-1 rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)] p-1 shadow-sm';
@@ -3196,14 +3197,14 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
<Icon name="check" className="size-3.5" />
{t('filesView.editor.saved')}
</span>
) : isDirty ? withTooltip(t(autoSaveEnabled ? 'filesView.editor.saveNowTitle' : 'filesView.editor.saveNowManualTitle', { shortcut: `${getModifierLabel()}+S` }),
) : isDirty ? withTooltip(t(autoSaveEnabled ? 'filesView.editor.saveNowTitle' : 'filesView.editor.saveNowManualTitle', { shortcut: saveShortcut }),
<Button
variant="ghost"
size="sm"
onClick={() => void saveDraft()}
className="h-6 gap-1 px-1 text-muted-foreground opacity-80 hover:bg-transparent hover:opacity-100 focus-visible:bg-transparent active:bg-transparent"
title={t(autoSaveEnabled ? 'filesView.editor.saveNowTitle' : 'filesView.editor.saveNowManualTitle', { shortcut: `${getModifierLabel()}+S` })}
aria-label={t('filesView.editor.saveAria', { shortcut: `${getModifierLabel()}+S` })}
title={t(autoSaveEnabled ? 'filesView.editor.saveNowTitle' : 'filesView.editor.saveNowManualTitle', { shortcut: saveShortcut })}
aria-label={t('filesView.editor.saveAria', { shortcut: saveShortcut })}
>
<Icon name="save-3" className="size-4" />
</Button>
@@ -1,6 +1,9 @@
import React from 'react';
import { cn } from '@/lib/utils';
import { getModifierLabel } from '@/lib/shortcuts';
import {
formatShortcutForDisplay,
getEffectiveShortcutCombo,
} from '@/lib/shortcuts';
import { useUIStore } from '@/stores/useUIStore';
import { useProjectsStore } from '@/stores/useProjectsStore';
import { useAgentsStore } from '@/stores/useAgentsStore';
@@ -240,6 +243,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
const settingsPageRaw = useUIStore((state) => state.settingsPage);
const isSettingsDialogOpen = useUIStore((state) => state.isSettingsDialogOpen);
const setSettingsPage = useUIStore((state) => state.setSettingsPage);
const openSettingsShortcutOverride = useUIStore((state) => state.shortcutOverrides.open_settings);
const settingsSlug = resolveSettingsSlug(settingsPageRaw);
const [mobileStage, setMobileStage] = React.useState<MobileStage>(initialMobileStage);
@@ -751,7 +755,15 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
: showBackButton
? t('settings.view.actions.backToSettings')
: t('settings.view.actions.closeSettings');
const shortcutKey = getModifierLabel();
const openSettingsCombo = getEffectiveShortcutCombo(
'open_settings',
openSettingsShortcutOverride === undefined ? undefined : { open_settings: openSettingsShortcutOverride },
);
const closeSettingsTitle = openSettingsCombo
? t('settings.view.actions.closeSettingsWithShortcut', {
shortcut: formatShortcutForDisplay(openSettingsCombo),
})
: t('settings.view.actions.closeSettings');
const pushMobileSplitDetailHistory = React.useCallback((slug: SettingsPageSlug) => {
if (typeof window === 'undefined' || runtimeCtx.isVSCode) {
@@ -1119,7 +1131,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
type="button"
onClick={onClose}
aria-label={t('settings.view.actions.closeSettings')}
title={t('settings.view.actions.closeSettingsWithShortcut', { shortcut: shortcutKey })}
title={closeSettingsTitle}
className="inline-flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg p-2 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
>
<Icon name="close" className="h-5 w-5" />
@@ -1147,7 +1159,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
type="button"
onClick={onClose}
aria-label={t('settings.view.actions.closeSettings')}
title={t('settings.view.actions.closeSettingsWithShortcut', { shortcut: shortcutKey })}
title={closeSettingsTitle}
className="inline-flex h-7 w-7 items-center justify-center rounded-md p-0.5 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
>
<Icon name="close" className="h-5 w-5" />
@@ -21,6 +21,7 @@ import { useI18n } from '@/lib/i18n';
import { PROJECT_ACTION_ICON_MAP, type ProjectActionIconKey } from '@/lib/projectActions';
import { useInlineCommentDraftStore } from '@/stores/useInlineCommentDraftStore';
import { applyTerminalModifier, terminalControlCharacter, terminalSequenceForKey, type TerminalModifier as Modifier, type TerminalQuickKey as MobileKey } from '@/lib/terminalInput';
import { formatShortcutForDisplay } from '@/lib/shortcuts';
type TerminalViewProps = {
visible?: boolean;
@@ -924,7 +925,7 @@ export const TerminalView: React.FC<TerminalViewProps> = ({ visible }) => {
onClick={() => handleModifierToggle('ctrl')}
disabled={quickKeysDisabled}
>
<span className="text-xs font-medium">{t('terminalView.quickKeys.controlLabel')}</span>
<span className="text-xs font-medium">{formatShortcutForDisplay('ctrl')}</span>
<span className="sr-only">{t('terminalView.quickKeys.controlModifierAria')}</span>
</Button>
<Button
@@ -937,7 +938,7 @@ export const TerminalView: React.FC<TerminalViewProps> = ({ visible }) => {
onClick={() => handleModifierToggle('alt')}
disabled={quickKeysDisabled}
>
<span className="text-xs font-medium">{t('terminalView.quickKeys.altLabel')}</span>
<span className="text-xs font-medium">{formatShortcutForDisplay('alt')}</span>
<span className="sr-only">{t('terminalView.quickKeys.altModifierAria')}</span>
</Button>
<Button
@@ -40,7 +40,7 @@ export const settingsDict = {
'settings.view.actions.backToSettings': 'Zurück zu Einstellungen',
'settings.view.actions.closeSettings': 'Einstellungen schließen',
'settings.view.actions.openSectionList': 'Abschnittsliste öffnen',
'settings.view.actions.closeSettingsWithShortcut': 'Einstellungen schließen ({shortcut}+,)',
'settings.view.actions.closeSettingsWithShortcut': 'Einstellungen schließen ({shortcut})',
'settings.view.actions.back': 'Zurück',
'settings.view.actions.resizeNavigation': 'Einstellungsnavigation skalieren',
'settings.view.search.placeholder': 'Einstellungen durchsuchen',
@@ -1857,7 +1857,7 @@ export const settingsDict = {
'settings.openchamber.visual.actions.resetInputBarOffsetAria': 'Versatz der Eingabeleiste zurücksetzen',
'settings.openchamber.visual.field.terminalQuickKeysAria': 'Schnelltasten des Terminals',
'settings.openchamber.visual.field.terminalQuickKeys': 'Schnelltasten des Terminals',
'settings.openchamber.visual.field.terminalQuickKeysTooltip': 'Esc, Strg, Pfeiltasten in der Terminalansicht anzeigen',
'settings.openchamber.visual.field.terminalQuickKeysTooltip': 'Esc, {control}, {alt} und Pfeiltasten in der Terminalansicht anzeigen',
'settings.openchamber.visual.field.fileEditorKeymap': 'Tastaturlayout für Datei-Editor',
'settings.openchamber.visual.option.fileEditorKeymap.default': 'Standard',
'settings.openchamber.visual.option.fileEditorKeymap.vim': 'Vim',
+1 -1
View File
@@ -1368,7 +1368,7 @@ export const dict = {
'rightSidebar.contextNotesTodo.toast.planImported': 'Plan importiert',
'rightSidebar.contextNotesTodo.toast.readPlanFileFailed': 'Fehler beim Lesen der Plan-Datei',
'inlineComment.range.lines': 'Zeilen {start}-{end}',
'inlineComment.input.placeholder': 'Kommentar hinzufügen... (Cmd+Enter zum Speichern)',
'inlineComment.input.placeholder': 'Kommentar hinzufügen... ({shortcut} zum Speichern)',
'inlineComment.input.placeholderShort': 'Kommentar hinzufügen...',
'inlineComment.actions.cancel': 'Abbrechen',
'inlineComment.actions.save': 'Speichern',
@@ -43,7 +43,7 @@ export const settingsDict = {
'settings.view.actions.backToSettings': 'Back to Settings',
'settings.view.actions.closeSettings': 'Close settings',
'settings.view.actions.openSectionList': 'Open section list',
'settings.view.actions.closeSettingsWithShortcut': 'Close Settings ({shortcut}+,)',
'settings.view.actions.closeSettingsWithShortcut': 'Close Settings ({shortcut})',
'settings.view.actions.back': 'Back',
'settings.view.actions.resizeNavigation': 'Resize settings navigation',
'settings.view.search.placeholder': 'Search settings',
@@ -1938,7 +1938,7 @@ export const settingsDict = {
'settings.openchamber.visual.actions.resetInputBarOffsetAria': 'Reset input bar offset',
'settings.openchamber.visual.field.terminalQuickKeysAria': 'Terminal quick keys',
'settings.openchamber.visual.field.terminalQuickKeys': 'Terminal Quick Keys',
'settings.openchamber.visual.field.terminalQuickKeysTooltip': 'Show Esc, Ctrl, Arrows in terminal view',
'settings.openchamber.visual.field.terminalQuickKeysTooltip': 'Show Esc, {control}, {alt}, and arrow keys in the terminal view',
'settings.openchamber.visual.field.fileEditorKeymap': 'File editor keymap',
'settings.openchamber.visual.option.fileEditorKeymap.default': 'Default',
'settings.openchamber.visual.option.fileEditorKeymap.vim': 'Vim',
+1 -1
View File
@@ -1521,7 +1521,7 @@ export const dict = {
'rightSidebar.contextNotesTodo.toast.planImported': 'Plan imported',
'rightSidebar.contextNotesTodo.toast.readPlanFileFailed': 'Failed to read plan file',
'inlineComment.range.lines': 'Lines {start}-{end}',
'inlineComment.input.placeholder': 'Add a comment... (Cmd+Enter to save)',
'inlineComment.input.placeholder': 'Add a comment... ({shortcut} to save)',
'inlineComment.input.placeholderShort': 'Add a comment...',
'inlineComment.actions.cancel': 'Cancel',
'inlineComment.actions.save': 'Save',
@@ -43,7 +43,7 @@ export const settingsDict = {
"settings.view.actions.backToSettings": "Volver a Configuración",
"settings.view.actions.closeSettings": "Cerrar configuración",
"settings.view.actions.openSectionList": "Abrir lista de secciones",
"settings.view.actions.closeSettingsWithShortcut": "Cerrar configuración ({shortcut}+,)",
"settings.view.actions.closeSettingsWithShortcut": "Cerrar configuración ({shortcut})",
"settings.view.actions.back": "Atrás",
"settings.view.actions.resizeNavigation": "Ajustar tamaño de la navegación",
"settings.view.search.placeholder": "Buscar configuración",
@@ -1914,7 +1914,7 @@ export const settingsDict = {
"settings.openchamber.visual.actions.resetInputBarOffsetAria": "Restablecer desplazamiento de la barra de entrada",
"settings.openchamber.visual.field.terminalQuickKeysAria": "Teclas rápidas del terminal",
"settings.openchamber.visual.field.terminalQuickKeys": "Teclas rápidas del terminal",
"settings.openchamber.visual.field.terminalQuickKeysTooltip": "Mostrar Esc, Ctrl y flechas en la vista del terminal",
"settings.openchamber.visual.field.terminalQuickKeysTooltip": "Mostrar Esc, {control}, {alt} y las flechas en la vista del terminal",
"settings.openchamber.visual.field.fileEditorKeymap": "Mapa de teclas del editor de archivos",
"settings.openchamber.visual.option.fileEditorKeymap.default": "Predeterminado",
"settings.openchamber.visual.option.fileEditorKeymap.vim": "Vim",
+1 -1
View File
@@ -1499,7 +1499,7 @@ export const dict: Record<I18nKey, string> = {
"rightSidebar.contextNotesTodo.toast.planImported": "Plan importado",
"rightSidebar.contextNotesTodo.toast.readPlanFileFailed": "No se pudo leer el archivo del plan",
"inlineComment.range.lines": "Líneas {start}-{end}",
"inlineComment.input.placeholder": "Añadir un comentario... (Cmd+Enter para guardar)",
"inlineComment.input.placeholder": "Añadir un comentario... ({shortcut} para guardar)",
"inlineComment.input.placeholderShort": "Añadir un comentario...",
"inlineComment.actions.cancel": "Cancelar",
"inlineComment.actions.save": "Guardar",
@@ -43,7 +43,7 @@ export const settingsDict = {
'settings.view.actions.backToSettings': 'Retour aux paramètres',
'settings.view.actions.closeSettings': 'Fermer les paramètres',
'settings.view.actions.openSectionList': 'Ouvrir la liste des sections',
'settings.view.actions.closeSettingsWithShortcut': 'Fermer les paramètres ({shortcut}+,)',
'settings.view.actions.closeSettingsWithShortcut': 'Fermer les paramètres ({shortcut})',
'settings.view.actions.back': 'Retour',
'settings.view.actions.resizeNavigation': 'Redimensionner la navigation dans les paramètres',
'settings.page.projects.title': 'Projets',
@@ -1822,7 +1822,7 @@ export const settingsDict = {
'settings.openchamber.visual.actions.resetInputBarOffsetAria': 'Réinitialiser le décalage de la barre d\'entrée',
'settings.openchamber.visual.field.terminalQuickKeysAria': 'Touches rapides du terminal',
'settings.openchamber.visual.field.terminalQuickKeys': 'Touches rapides du terminal',
'settings.openchamber.visual.field.terminalQuickKeysTooltip': 'Afficher Esc, Ctrl, Flèches dans la vue du terminal',
'settings.openchamber.visual.field.terminalQuickKeysTooltip': 'Afficher Échap, {control}, {alt} et les flèches dans la vue du terminal',
'settings.openchamber.visual.field.activityDefaultModeAria': 'Mode d\'activité par défaut : {option}',
'settings.openchamber.visual.field.showExpandedBashToolsAria': 'Afficher les outils bash étendus',
'settings.openchamber.visual.field.showExpandedEditToolsAria': 'Afficher les outils d\'édition étendus',
+1 -1
View File
@@ -1341,7 +1341,7 @@ export const dict = {
'rightSidebar.contextNotesTodo.toast.planImported': 'Forfait importé',
'rightSidebar.contextNotesTodo.toast.readPlanFileFailed': 'Échec de la lecture du fichier de plan',
'inlineComment.range.lines': 'Lignes {start}-{end}',
'inlineComment.input.placeholder': 'Ajouter un commentaire... (Cmd+Entrée pour enregistrer)',
'inlineComment.input.placeholder': 'Ajouter un commentaire... ({shortcut} pour enregistrer)',
'inlineComment.actions.cancel': 'Annuler',
'inlineComment.actions.save': 'Sauvegarder',
'inlineComment.actions.comment': 'Commentaire',
@@ -43,7 +43,7 @@ export const settingsDict = {
'settings.view.actions.backToSettings': '設定に戻る',
'settings.view.actions.closeSettings': '設定を閉じる',
'settings.view.actions.openSectionList': 'セクション一覧を開く',
'settings.view.actions.closeSettingsWithShortcut': '設定を閉じる ({shortcut}+,)',
'settings.view.actions.closeSettingsWithShortcut': '設定を閉じる ({shortcut})',
'settings.view.actions.back': '戻る',
'settings.view.actions.resizeNavigation': '設定ナビゲーションのサイズ変更',
'settings.view.search.placeholder': '設定を検索',
@@ -1947,7 +1947,7 @@ export const settingsDict = {
'settings.openchamber.visual.actions.resetInputBarOffsetAria': '入力バーオフセットをリセット',
'settings.openchamber.visual.field.terminalQuickKeysAria': 'ターミナルクイックキー',
'settings.openchamber.visual.field.terminalQuickKeys': 'ターミナルクイックキー',
'settings.openchamber.visual.field.terminalQuickKeysTooltip': 'ターミナルビューに Esc、Ctrl、矢印を表示',
'settings.openchamber.visual.field.terminalQuickKeysTooltip': 'ターミナルビューに Esc、{control}、{alt}、矢印キーを表示',
'settings.openchamber.visual.field.fileEditorKeymap': 'ファイルエディターキーマップ',
'settings.openchamber.visual.option.fileEditorKeymap.default': 'デフォルト',
'settings.openchamber.visual.option.fileEditorKeymap.vim': 'Vim',
+1 -1
View File
@@ -1517,7 +1517,7 @@ export const dict: Record<I18nKey, string> = {
'rightSidebar.contextNotesTodo.toast.planImported': '計画をインポートしました',
'rightSidebar.contextNotesTodo.toast.readPlanFileFailed': '計画ファイルの読み込みに失敗しました',
'inlineComment.range.lines': '{start}行目~{end}行目',
'inlineComment.input.placeholder': 'コメントを追加...Cmd+Enterで保存)',
'inlineComment.input.placeholder': 'コメントを追加...{shortcut}で保存)',
'inlineComment.input.placeholderShort': 'コメントを追加...',
'inlineComment.actions.cancel': 'キャンセル',
'inlineComment.actions.save': '保存',
@@ -43,7 +43,7 @@ export const settingsDict = {
'settings.view.actions.backToSettings': '설정으로 돌아가기',
'settings.view.actions.closeSettings': '설정 닫기',
'settings.view.actions.openSectionList': '섹션 목록 열기',
'settings.view.actions.closeSettingsWithShortcut': '설정 닫기 ({shortcut}+,)',
'settings.view.actions.closeSettingsWithShortcut': '설정 닫기 ({shortcut})',
'settings.view.actions.back': '뒤로',
'settings.view.actions.resizeNavigation': '설정 내비게이션 크기 조정',
'settings.view.search.placeholder': '설정 검색',
@@ -1914,7 +1914,7 @@ export const settingsDict = {
'settings.openchamber.visual.actions.resetInputBarOffsetAria': '입력 바 오프셋 초기화',
'settings.openchamber.visual.field.terminalQuickKeysAria': '터미널 빠른 키',
'settings.openchamber.visual.field.terminalQuickKeys': '터미널 빠른 키',
'settings.openchamber.visual.field.terminalQuickKeysTooltip': '터미널 보기에서 Esc, Ctrl, 화살표를 표시합니다',
'settings.openchamber.visual.field.terminalQuickKeysTooltip': '터미널 보기에서 Esc, {control}, {alt}, 화살표를 표시합니다',
'settings.openchamber.visual.field.fileEditorKeymap': '파일 편집기 키맵',
'settings.openchamber.visual.option.fileEditorKeymap.default': '기본값',
'settings.openchamber.visual.option.fileEditorKeymap.vim': 'Vim',
+1 -1
View File
@@ -1524,7 +1524,7 @@ export const dict: Record<I18nKey, string> = {
'rightSidebar.contextNotesTodo.toast.planImported': '플랜 가져옴',
'rightSidebar.contextNotesTodo.toast.readPlanFileFailed': '플랜 파일 읽기 실패',
'inlineComment.range.lines': '줄 {start}-{end}',
'inlineComment.input.placeholder': '댓글 추가… (Cmd+Enter로 저장)',
'inlineComment.input.placeholder': '댓글 추가… ({shortcut}로 저장)',
'inlineComment.input.placeholderShort': '댓글 추가…',
'inlineComment.actions.cancel': '취소',
'inlineComment.actions.save': '저장',
@@ -1109,7 +1109,7 @@ export const settingsDict = {
'settings.openchamber.visual.field.editorFontSize': 'Rozmiar czcionki edytora',
'settings.openchamber.visual.field.terminalQuickKeys': 'Szybkie klawisze terminala',
'settings.openchamber.visual.field.terminalQuickKeysAria': 'Szybkie klawisze terminala',
'settings.openchamber.visual.field.terminalQuickKeysTooltip': 'Pokaż Esc, Ctrl i strzałki w widoku terminala',
'settings.openchamber.visual.field.terminalQuickKeysTooltip': 'Pokaż Esc, {control}, {alt} i klawisze strzałek w widoku terminala',
'settings.openchamber.visual.field.fileEditorKeymap': 'Mapa klawiszy edytora plików',
'settings.openchamber.visual.option.fileEditorKeymap.default': 'Domyślna',
'settings.openchamber.visual.option.fileEditorKeymap.vim': 'Vim',
@@ -2004,7 +2004,7 @@ export const settingsDict = {
'settings.view.actions.back': 'Wstecz',
'settings.view.actions.backToSettings': 'Powrót do ustawień',
'settings.view.actions.closeSettings': 'Zamknij ustawienia',
'settings.view.actions.closeSettingsWithShortcut': 'Zamknij ustawienia ({shortcut}+,)',
'settings.view.actions.closeSettingsWithShortcut': 'Zamknij ustawienia ({shortcut})',
'settings.view.actions.openSectionList': 'Otwórz listę sekcji',
'settings.view.actions.reloadOpenCode': 'Przeładuj OpenCode',
'settings.view.actions.reloadOpenCodeTooltip': 'Uruchom ponownie OpenCode i przeładuj jego konfigurację.',
+1 -1
View File
@@ -2364,7 +2364,7 @@ export const dict: Record<I18nKey, string> = {
'inlineComment.actions.save': 'Zapisz',
'inlineComment.actions.showLess': 'Show less',
'inlineComment.actions.showMore': 'Show more',
'inlineComment.input.placeholder': 'Add a comment... (Cmd+Enter to save)',
'inlineComment.input.placeholder': 'Dodaj komentarz... ({shortcut}, aby zapisać)',
'inlineComment.input.placeholderShort': 'Dodaj komentarz...',
'inlineComment.range.lines': 'Lines {start}-{end}',
'inlineComment.toast.selectSessionToSave': 'Select a session to save comment',
@@ -43,7 +43,7 @@ export const settingsDict = {
"settings.view.actions.backToSettings": "Voltar às configurações",
"settings.view.actions.closeSettings": "Fechar configurações",
"settings.view.actions.openSectionList": "Abrir lista de seções",
"settings.view.actions.closeSettingsWithShortcut": "Fechar configurações ({shortcut}+,)",
"settings.view.actions.closeSettingsWithShortcut": "Fechar configurações ({shortcut})",
"settings.view.actions.back": "Voltar",
"settings.view.actions.resizeNavigation": "Ajustar tamanho da navegação",
"settings.view.search.placeholder": "Buscar configurações",
@@ -1914,7 +1914,7 @@ export const settingsDict = {
"settings.openchamber.visual.actions.resetInputBarOffsetAria": "Redefinir deslocamento da barra de entrada",
"settings.openchamber.visual.field.terminalQuickKeysAria": "Teclas rápidas do terminal",
"settings.openchamber.visual.field.terminalQuickKeys": "Teclas rápidas do terminal",
"settings.openchamber.visual.field.terminalQuickKeysTooltip": "Mostrar Esc, Ctrl e flechas na vista do terminal",
"settings.openchamber.visual.field.terminalQuickKeysTooltip": "Mostrar Esc, {control}, {alt} e as setas na vista do terminal",
"settings.openchamber.visual.field.fileEditorKeymap": "Mapa de teclas do editor de arquivos",
"settings.openchamber.visual.option.fileEditorKeymap.default": "Padrão",
"settings.openchamber.visual.option.fileEditorKeymap.vim": "Vim",
+1 -1
View File
@@ -1499,7 +1499,7 @@ export const dict: Record<I18nKey, string> = {
"rightSidebar.contextNotesTodo.toast.planImported": "Plano importado",
"rightSidebar.contextNotesTodo.toast.readPlanFileFailed": "Não foi possível ler o arquivo do plano",
"inlineComment.range.lines": "Linhas {start}-{end}",
"inlineComment.input.placeholder": "Adicionar um comentário... (Cmd+Enter para salvar)",
"inlineComment.input.placeholder": "Adicionar um comentário... ({shortcut} para salvar)",
"inlineComment.input.placeholderShort": "Adicionar um comentário...",
"inlineComment.actions.cancel": "Cancelar",
"inlineComment.actions.save": "Salvar",
@@ -43,7 +43,7 @@ export const settingsDict = {
"settings.view.actions.backToSettings": "Назад до налаштувань",
"settings.view.actions.closeSettings": "Закрити налаштування",
"settings.view.actions.openSectionList": "Відкрити список розділів",
"settings.view.actions.closeSettingsWithShortcut": "Закрити налаштування ({shortcut}+,)",
"settings.view.actions.closeSettingsWithShortcut": "Закрити налаштування ({shortcut})",
"settings.view.actions.back": "Назад",
"settings.view.actions.resizeNavigation": "Змінити розмір навігації налаштувань",
"settings.view.search.placeholder": "Пошук налаштувань",
@@ -1914,7 +1914,7 @@ export const settingsDict = {
"settings.openchamber.visual.actions.resetInputBarOffsetAria": "Скинути зміщення панелі вводу",
"settings.openchamber.visual.field.terminalQuickKeysAria": "Швидкі клавіші терміналу",
"settings.openchamber.visual.field.terminalQuickKeys": "Швидкі клавіші терміналу",
"settings.openchamber.visual.field.terminalQuickKeysTooltip": "Показати Esc, Ctrl, стрілки в поданні терміналу",
"settings.openchamber.visual.field.terminalQuickKeysTooltip": "Показувати Esc, {control}, {alt} і клавіші зі стрілками в поданні термінала",
"settings.openchamber.visual.field.fileEditorKeymap": "Розкладка клавіш редактора файлів",
"settings.openchamber.visual.option.fileEditorKeymap.default": "Типова",
"settings.openchamber.visual.option.fileEditorKeymap.vim": "Vim",
+1 -1
View File
@@ -1499,7 +1499,7 @@ export const dict: Record<I18nKey, string> = {
"rightSidebar.contextNotesTodo.toast.planImported": "План імпортовано",
"rightSidebar.contextNotesTodo.toast.readPlanFileFailed": "Не вдалося прочитати файл плану",
"inlineComment.range.lines": "Рядки {start}-{end}",
"inlineComment.input.placeholder": "Додайте коментар... (Cmd+Enter, щоб зберегти)",
"inlineComment.input.placeholder": "Додайте коментар... ({shortcut}, щоб зберегти)",
"inlineComment.input.placeholderShort": "Додайте коментар...",
"inlineComment.actions.cancel": "Скасувати",
"inlineComment.actions.save": "Зберегти",
@@ -43,7 +43,7 @@ export const settingsDict = {
'settings.view.actions.backToSettings': '返回设置',
'settings.view.actions.closeSettings': '关闭设置',
'settings.view.actions.openSectionList': '打开分组列表',
'settings.view.actions.closeSettingsWithShortcut': '关闭设置({shortcut}+,',
'settings.view.actions.closeSettingsWithShortcut': '关闭设置({shortcut}',
'settings.view.actions.back': '返回',
'settings.view.actions.resizeNavigation': '调整设置导航宽度',
'settings.view.search.placeholder': '搜索设置',
@@ -1914,7 +1914,7 @@ export const settingsDict = {
'settings.openchamber.visual.actions.resetInputBarOffsetAria': '重置输入栏偏移',
'settings.openchamber.visual.field.terminalQuickKeysAria': '终端快捷键',
'settings.openchamber.visual.field.terminalQuickKeys': '终端快捷键',
'settings.openchamber.visual.field.terminalQuickKeysTooltip': '在终端视图显示 Esc、Ctrl、方向键',
'settings.openchamber.visual.field.terminalQuickKeysTooltip': '在终端视图显示 Esc、{control}、{alt} 和方向键',
'settings.openchamber.visual.field.fileEditorKeymap': '文件编辑器键位映射',
'settings.openchamber.visual.option.fileEditorKeymap.default': '默认',
'settings.openchamber.visual.option.fileEditorKeymap.vim': 'Vim',
+1 -1
View File
@@ -1487,7 +1487,7 @@ export const dict: Record<I18nKey, string> = {
'rightSidebar.contextNotesTodo.toast.planImported': '计划已导入',
'rightSidebar.contextNotesTodo.toast.readPlanFileFailed': '读取计划文件失败',
'inlineComment.range.lines': '行 {start}-{end}',
'inlineComment.input.placeholder': '添加评论...Cmd+Enter 保存)',
'inlineComment.input.placeholder': '添加评论...{shortcut} 保存)',
'inlineComment.input.placeholderShort': '添加评论…',
'inlineComment.actions.cancel': '取消',
'inlineComment.actions.save': '保存',
@@ -41,7 +41,7 @@
'settings.view.actions.backToSettings': '返回設定頁',
'settings.view.actions.closeSettings': '關閉設定',
'settings.view.actions.openSectionList': '開啟群組清單',
'settings.view.actions.closeSettingsWithShortcut': '關閉設定({shortcut}+,',
'settings.view.actions.closeSettingsWithShortcut': '關閉設定({shortcut}',
'settings.view.actions.back': '返回',
'settings.view.actions.resizeNavigation': '調整設定導覽寬度',
'settings.view.search.placeholder': '搜尋設定',
@@ -1820,7 +1820,7 @@
'settings.openchamber.visual.actions.resetInputBarOffsetAria': '重設輸入列偏移',
'settings.openchamber.visual.field.terminalQuickKeysAria': '終端機快速鍵',
'settings.openchamber.visual.field.terminalQuickKeys': '終端機快速鍵',
'settings.openchamber.visual.field.terminalQuickKeysTooltip': '在終端機檢視顯示 Esc、Ctrl、方向鍵',
'settings.openchamber.visual.field.terminalQuickKeysTooltip': '在終端機檢視顯示 Esc、{control}、{alt} 和方向鍵',
'settings.openchamber.visual.field.fileEditorKeymap': '檔案編輯器鍵位映射',
'settings.openchamber.visual.option.fileEditorKeymap.default': '預設',
'settings.openchamber.visual.option.fileEditorKeymap.vim': 'Vim',
+1 -1
View File
@@ -1497,7 +1497,7 @@ export const dict: Record<I18nKey, string> = {
'rightSidebar.contextNotesTodo.toast.planImported': '計畫已匯入',
'rightSidebar.contextNotesTodo.toast.readPlanFileFailed': '讀取計畫檔案失敗',
'inlineComment.range.lines': '行 {start}-{end}',
'inlineComment.input.placeholder': '新增留言...Cmd+Enter 儲存)',
'inlineComment.input.placeholder': '新增留言...{shortcut} 儲存)',
'inlineComment.input.placeholderShort': '新增留言…',
'inlineComment.actions.cancel': '取消',
'inlineComment.actions.save': '儲存',
@@ -25,7 +25,7 @@ Component interaction keys that are not application commands, such as list navig
# Binding rules
Bindings remain persisted as `Record<string, string>`. Each binding has one chord or at most two space-separated chords, such as `mod+s p`. `normalizeCombo`, `parseShortcut`, `formatShortcutForDisplay`, and `getShortcutConflict` provide the shared parsing and validation behavior. A single chord conflicts with a sequence sharing its first chord; sibling sequences are valid.
Bindings remain persisted as `Record<string, string>`. 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.
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.
@@ -106,3 +106,23 @@ describe('shortcut sequences', () => {
expect(isRiskyBrowserShortcut('mod+s p')).toBe(true);
});
});
describe('platform shortcut labels', () => {
test('normalizes Command and Option to platform-neutral modifiers', () => {
expect(normalizeCombo('command+option+n')).toBe('mod+alt+n');
});
test('uses macOS modifier symbols', () => {
expect(formatShortcutForDisplay('mod+ctrl+shift+alt+n', 'Unassigned', 'macos')).toBe(
'⌘ + ⌃ + ⇧ + ⌥ + N',
);
expect(formatShortcutForDisplay('alt', 'Unassigned', 'macos')).toBe('⌥');
});
test('uses named modifiers on other platforms', () => {
expect(formatShortcutForDisplay('mod+shift+alt+n', 'Unassigned', 'other')).toBe(
'Ctrl + Shift + Alt + N',
);
expect(formatShortcutForDisplay('alt', 'Unassigned', 'other')).toBe('Alt');
});
});
+30 -13
View File
@@ -3,6 +3,7 @@ import { isDesktopShell } from '@/lib/desktop';
import { isMacOS } from '@/lib/utils';
type ShortcutModifier = 'mod' | 'shift' | 'alt' | 'ctrl';
type ShortcutDisplayPlatform = 'macos' | 'other';
type ShortcutKey = string;
export type ShortcutCombo = string;
@@ -30,11 +31,19 @@ const MODIFIER_KEY_MAP: Record<string, ShortcutModifier> = {
command: 'mod',
};
const DISPLAY_LABEL_MAP: Record<ShortcutModifier, string> = {
mod: isMacOS() && isDesktopShell() ? '⌘' : 'Ctrl',
shift: '',
alt: '',
ctrl: '',
const MODIFIER_LABELS: Record<ShortcutDisplayPlatform, Record<ShortcutModifier, string>> = {
macos: {
mod: '',
shift: '',
alt: '',
ctrl: '⌃',
},
other: {
mod: 'Ctrl',
shift: 'Shift',
alt: 'Alt',
ctrl: 'Ctrl',
},
};
const KEY_LABEL_MAP: Record<string, string> = {
@@ -176,19 +185,31 @@ export function parseShortcut(combo: ShortcutCombo): ParsedShortcut | undefined
};
}
export function formatShortcutForDisplay(combo: ShortcutCombo, unassignedLabel = 'Unassigned'): string {
function getShortcutDisplayPlatform(): ShortcutDisplayPlatform {
return isMacOS() ? 'macos' : 'other';
}
export function formatShortcutForDisplay(
combo: ShortcutCombo,
unassignedLabel = 'Unassigned',
platform = getShortcutDisplayPlatform(),
): string {
if (isUnassignedShortcut(combo)) return unassignedLabel;
const parsed = parseShortcut(combo);
if (!parsed || parsed.chords.some((chord) => !chord.key && chord.modifiers.size === 0)) {
return unassignedLabel;
}
return parsed.chords.map(formatChordForDisplay).join(', ');
return parsed.chords.map((chord) => formatChordForDisplay(chord, platform)).join(', ');
}
function formatChordForDisplay(parsed: ParsedShortcutChord): string {
function formatChordForDisplay(
parsed: ParsedShortcutChord,
platform: ShortcutDisplayPlatform,
): string {
const modifierLabels = MODIFIER_LABELS[platform];
const parts = MODIFIER_PRIORITY
.filter((modifier) => parsed.modifiers.has(modifier))
.map((modifier) => DISPLAY_LABEL_MAP[modifier]);
.map((modifier) => modifierLabels[modifier]);
if (parsed.key) {
parts.push(KEY_LABEL_MAP[parsed.key.toLowerCase()] || parsed.key.toUpperCase());
}
@@ -308,7 +329,3 @@ export function eventMatchesShortcutPrefix(
return !chord.key || Boolean(heldKeys?.has(chord.key.toLowerCase()));
}
export function getModifierLabel(): string {
return isMacOS() && isDesktopShell() ? '⌘' : 'Ctrl';
}
-1
View File
@@ -2,7 +2,6 @@ export {
eventMatchesShortcut,
eventMatchesShortcutPrefix,
formatShortcutForDisplay,
getModifierLabel,
getShortcutConflict,
isRiskyBrowserShortcut,
isShortcutPrefixHeld,