From c7195469bdfed9501f54d517e600deaccb0fdc5c Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Fri, 13 Feb 2026 23:44:28 +0000 Subject: [PATCH] fix(ui): add keyboard shortcut hints toggle Hide shortcut hints in WebUI and allow toggling in native desktop apps. --- packages/ui/src/App.tsx | 9 ++++++++ .../ui/src/components/command-palette.tsx | 13 +++++++++-- .../components/filesystem-browser-dialog.tsx | 2 +- .../src/components/folder-selection-view.tsx | 6 ++--- packages/ui/src/components/hint-row.tsx | 2 +- .../src/components/instance-welcome-view.tsx | 4 ++-- .../components/instance/instance-shell2.tsx | 4 ++-- .../ui/src/components/message-list-header.tsx | 2 +- .../ui/src/components/message-section.tsx | 2 +- packages/ui/src/components/prompt-input.tsx | 2 +- packages/ui/src/components/session-picker.tsx | 2 +- packages/ui/src/lib/commands.ts | 1 + packages/ui/src/lib/hooks/use-commands.ts | 22 +++++++++++++++++++ .../ui/src/lib/i18n/messages/en/commands.ts | 6 +++++ .../ui/src/lib/i18n/messages/es/commands.ts | 6 +++++ .../ui/src/lib/i18n/messages/fr/commands.ts | 6 +++++ .../ui/src/lib/i18n/messages/ja/commands.ts | 6 +++++ .../ui/src/lib/i18n/messages/ru/commands.ts | 6 +++++ .../src/lib/i18n/messages/zh-Hans/commands.ts | 6 +++++ packages/ui/src/stores/preferences.tsx | 10 +++++++++ packages/ui/src/styles/panels/modal.css | 5 +++++ packages/ui/src/styles/utilities.css | 13 +++++++++++ 22 files changed, 120 insertions(+), 15 deletions(-) diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index f4092ad3..0f2a85ff 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -60,6 +60,7 @@ const App: Component = () => { preferences, recordWorkspaceLaunch, toggleShowThinkingBlocks, + toggleKeyboardShortcutHints, toggleShowTimelineTools, toggleAutoCleanupBlankSessions, toggleUsageMetrics, @@ -80,6 +81,13 @@ const App: Component = () => { const [remoteAccessOpen, setRemoteAccessOpen] = createSignal(false) const [instanceTabBarHeight, setInstanceTabBarHeight] = createSignal(0) + createEffect(() => { + if (typeof document === "undefined") return + const shouldShow = + runtimeEnv.host !== "web" && runtimeEnv.platform !== "mobile" && (preferences().showKeyboardShortcutHints ?? true) + document.documentElement.dataset.keyboardHints = shouldShow ? "show" : "hide" + }) + const updateInstanceTabBarHeight = () => { if (typeof document === "undefined") return const element = document.querySelector(".tab-bar-instance") @@ -293,6 +301,7 @@ const App: Component = () => { preferences, toggleAutoCleanupBlankSessions, toggleShowThinkingBlocks, + toggleKeyboardShortcutHints, toggleShowTimelineTools, toggleUsageMetrics, togglePromptSubmitOnEnter, diff --git a/packages/ui/src/components/command-palette.tsx b/packages/ui/src/components/command-palette.tsx index 36416617..87947b5a 100644 --- a/packages/ui/src/components/command-palette.tsx +++ b/packages/ui/src/components/command-palette.tsx @@ -112,6 +112,10 @@ const CommandPalette: Component = (props) => { const groupedCommandList = () => processedCommands().groups const orderedCommands = () => processedCommands().ordered + + const isCommandDisabled = (command: Command) => { + return command.disabled ? Boolean(resolveResolvable(command.disabled)) : false + } const selectedIndex = createMemo(() => { const ordered = orderedCommands() if (ordered.length === 0) return -1 @@ -138,10 +142,11 @@ const CommandPalette: Component = (props) => { } return } - + const currentId = selectedCommandId() if (!currentId || !ordered.some((cmd) => cmd.id === currentId)) { - setSelectedCommandId(ordered[0].id) + const firstEnabled = ordered.find((cmd) => !isCommandDisabled(cmd)) + setSelectedCommandId((firstEnabled || ordered[0])?.id ?? null) } }) @@ -195,12 +200,14 @@ const CommandPalette: Component = (props) => { if (index < 0 || index >= ordered.length) return const command = ordered[index] if (!command) return + if (isCommandDisabled(command)) return props.onExecute(command) props.onClose() } } function handleCommandClick(command: Command) { + if (isCommandDisabled(command)) return props.onExecute(command) props.onClose() } @@ -265,11 +272,13 @@ const CommandPalette: Component = (props) => { {(command, localIndex) => { const commandIndex = group.startIndex + localIndex() + const disabled = isCommandDisabled(command) return ( @@ -573,7 +573,7 @@ const FolderSelectionView: Component = (props) => { - @@ -539,7 +539,7 @@ const InstanceWelcomeView: Component = (props) => { - -
+
= (props) => {
- + Cmd+Enter diff --git a/packages/ui/src/lib/commands.ts b/packages/ui/src/lib/commands.ts index a38b2fec..f158da5f 100644 --- a/packages/ui/src/lib/commands.ts +++ b/packages/ui/src/lib/commands.ts @@ -18,6 +18,7 @@ export interface Command { description: Resolvable keywords?: Resolvable shortcut?: KeyboardShortcut + disabled?: Resolvable action: () => void | Promise category?: Resolvable } diff --git a/packages/ui/src/lib/hooks/use-commands.ts b/packages/ui/src/lib/hooks/use-commands.ts index bdd2e8d5..c89a95d4 100644 --- a/packages/ui/src/lib/hooks/use-commands.ts +++ b/packages/ui/src/lib/hooks/use-commands.ts @@ -14,6 +14,7 @@ import { getLogger } from "../logger" import { requestData } from "../opencode-api" import { emitSessionSidebarRequest } from "../session-sidebar-events" import { tGlobal } from "../i18n" +import { runtimeEnv } from "../runtime-env" const log = getLogger("actions") @@ -28,6 +29,7 @@ function splitKeywords(key: string): string[] { export interface UseCommandsOptions { preferences: Accessor toggleShowThinkingBlocks: () => void + toggleKeyboardShortcutHints: () => void toggleShowTimelineTools: () => void toggleUsageMetrics: () => void toggleAutoCleanupBlankSessions: () => void @@ -454,6 +456,26 @@ export function useCommands(options: UseCommandsOptions) { action: options.toggleShowTimelineTools, }) + commandRegistry.register({ + id: "keyboard-shortcut-hints", + label: () => + tGlobal( + options.preferences().showKeyboardShortcutHints + ? "commands.keyboardShortcutHints.label.hide" + : "commands.keyboardShortcutHints.label.show", + ), + description: () => + tGlobal( + runtimeEnv.host === "web" + ? "commands.keyboardShortcutHints.description.disabledWeb" + : "commands.keyboardShortcutHints.description", + ), + category: "System", + keywords: () => splitKeywords("commands.keyboardShortcutHints.keywords"), + disabled: () => runtimeEnv.host === "web", + action: options.toggleKeyboardShortcutHints, + }) + commandRegistry.register({ id: "thinking-default-visibility", label: () => { diff --git a/packages/ui/src/lib/i18n/messages/en/commands.ts b/packages/ui/src/lib/i18n/messages/en/commands.ts index 66ff78f7..5b26c0c9 100644 --- a/packages/ui/src/lib/i18n/messages/en/commands.ts +++ b/packages/ui/src/lib/i18n/messages/en/commands.ts @@ -97,6 +97,12 @@ export const commandMessages = { "commands.timelineToolCalls.description": "Toggle tool call entries in the message timeline", "commands.timelineToolCalls.keywords": "timeline, tool, toggle", + "commands.keyboardShortcutHints.label.show": "Show Keyboard Shortcut Hints", + "commands.keyboardShortcutHints.label.hide": "Hide Keyboard Shortcut Hints", + "commands.keyboardShortcutHints.description": "Show or hide keyboard shortcut hints across the UI", + "commands.keyboardShortcutHints.description.disabledWeb": "Disabled in WebUI (shortcut hints are always hidden)", + "commands.keyboardShortcutHints.keywords": "shortcut, shortcuts, keyboard, keybind, hints", + "commands.common.expanded": "Expanded", "commands.common.collapsed": "Collapsed", "commands.common.visible": "Visible", diff --git a/packages/ui/src/lib/i18n/messages/es/commands.ts b/packages/ui/src/lib/i18n/messages/es/commands.ts index 0bad4e2f..c6a75e7e 100644 --- a/packages/ui/src/lib/i18n/messages/es/commands.ts +++ b/packages/ui/src/lib/i18n/messages/es/commands.ts @@ -97,6 +97,12 @@ export const commandMessages = { "commands.timelineToolCalls.description": "Alternar entradas de llamadas de herramienta en la línea de tiempo de mensajes", "commands.timelineToolCalls.keywords": "línea de tiempo, herramienta, alternar", + "commands.keyboardShortcutHints.label.show": "Mostrar atajos de teclado", + "commands.keyboardShortcutHints.label.hide": "Ocultar atajos de teclado", + "commands.keyboardShortcutHints.description": "Mostrar u ocultar sugerencias de atajos de teclado en la interfaz", + "commands.keyboardShortcutHints.description.disabledWeb": "Desactivado en WebUI (los atajos siempre se ocultan)", + "commands.keyboardShortcutHints.keywords": "atajo, atajos, teclado, keybind, pistas", + "commands.common.expanded": "Expandido", "commands.common.collapsed": "Colapsado", "commands.common.visible": "Visible", diff --git a/packages/ui/src/lib/i18n/messages/fr/commands.ts b/packages/ui/src/lib/i18n/messages/fr/commands.ts index 52bdea76..63e7c666 100644 --- a/packages/ui/src/lib/i18n/messages/fr/commands.ts +++ b/packages/ui/src/lib/i18n/messages/fr/commands.ts @@ -97,6 +97,12 @@ export const commandMessages = { "commands.timelineToolCalls.description": "Afficher/masquer les entrées d'appel d'outil dans la timeline des messages", "commands.timelineToolCalls.keywords": "timeline, outil, basculer", + "commands.keyboardShortcutHints.label.show": "Afficher les raccourcis clavier", + "commands.keyboardShortcutHints.label.hide": "Masquer les raccourcis clavier", + "commands.keyboardShortcutHints.description": "Afficher ou masquer les indices de raccourcis clavier dans l'interface", + "commands.keyboardShortcutHints.description.disabledWeb": "Désactivé en WebUI (les raccourcis sont toujours masqués)", + "commands.keyboardShortcutHints.keywords": "raccourci, raccourcis, clavier, keybind, indices", + "commands.common.expanded": "Développé", "commands.common.collapsed": "Réduit", "commands.common.visible": "Visible", diff --git a/packages/ui/src/lib/i18n/messages/ja/commands.ts b/packages/ui/src/lib/i18n/messages/ja/commands.ts index 30a2adc5..75c1c5f3 100644 --- a/packages/ui/src/lib/i18n/messages/ja/commands.ts +++ b/packages/ui/src/lib/i18n/messages/ja/commands.ts @@ -97,6 +97,12 @@ export const commandMessages = { "commands.timelineToolCalls.description": "メッセージタイムラインのツールコール表示を切り替え", "commands.timelineToolCalls.keywords": "タイムライン, ツール, 切り替え, timeline, tool, toggle", + "commands.keyboardShortcutHints.label.show": "キーボードショートカットのヒントを表示", + "commands.keyboardShortcutHints.label.hide": "キーボードショートカットのヒントを非表示", + "commands.keyboardShortcutHints.description": "UI 全体のキーボードショートカットヒントを表示/非表示", + "commands.keyboardShortcutHints.description.disabledWeb": "WebUI では無効(ヒントは常に非表示)", + "commands.keyboardShortcutHints.keywords": "ショートカット, キーボード, ヒント, shortcuts, keyboard, hints", + "commands.common.expanded": "展開", "commands.common.collapsed": "折りたたみ", "commands.common.visible": "表示", diff --git a/packages/ui/src/lib/i18n/messages/ru/commands.ts b/packages/ui/src/lib/i18n/messages/ru/commands.ts index 6c3f28ec..068f020d 100644 --- a/packages/ui/src/lib/i18n/messages/ru/commands.ts +++ b/packages/ui/src/lib/i18n/messages/ru/commands.ts @@ -97,6 +97,12 @@ export const commandMessages = { "commands.timelineToolCalls.description": "Переключить отображение вызовов инструментов в таймлайне сообщений", "commands.timelineToolCalls.keywords": "таймлайн, tool, переключить", + "commands.keyboardShortcutHints.label.show": "Показать подсказки сочетаний", + "commands.keyboardShortcutHints.label.hide": "Скрыть подсказки сочетаний", + "commands.keyboardShortcutHints.description": "Показать или скрыть подсказки сочетаний клавиш в интерфейсе", + "commands.keyboardShortcutHints.description.disabledWeb": "Отключено в WebUI (подсказки всегда скрыты)", + "commands.keyboardShortcutHints.keywords": "shortcut, shortcuts, keyboard, keybind, подсказки", + "commands.common.expanded": "Развернуто", "commands.common.collapsed": "Свернуто", "commands.common.visible": "Видимо", diff --git a/packages/ui/src/lib/i18n/messages/zh-Hans/commands.ts b/packages/ui/src/lib/i18n/messages/zh-Hans/commands.ts index 9c95f63e..85997488 100644 --- a/packages/ui/src/lib/i18n/messages/zh-Hans/commands.ts +++ b/packages/ui/src/lib/i18n/messages/zh-Hans/commands.ts @@ -97,6 +97,12 @@ export const commandMessages = { "commands.timelineToolCalls.description": "切换消息时间轴中的工具调用条目", "commands.timelineToolCalls.keywords": "timeline, tool, toggle, 时间轴, 工具, 切换", + "commands.keyboardShortcutHints.label.show": "显示键盘快捷键提示", + "commands.keyboardShortcutHints.label.hide": "隐藏键盘快捷键提示", + "commands.keyboardShortcutHints.description": "显示或隐藏界面中的键盘快捷键提示", + "commands.keyboardShortcutHints.description.disabledWeb": "WebUI 中已禁用(提示始终隐藏)", + "commands.keyboardShortcutHints.keywords": "shortcuts, keyboard, hints, 快捷键, 键盘, 提示", + "commands.common.expanded": "展开", "commands.common.collapsed": "折叠", "commands.common.visible": "可见", diff --git a/packages/ui/src/stores/preferences.tsx b/packages/ui/src/stores/preferences.tsx index 5907e78e..11298811 100644 --- a/packages/ui/src/stores/preferences.tsx +++ b/packages/ui/src/stores/preferences.tsx @@ -34,6 +34,7 @@ export type ListeningMode = "local" | "all" export interface Preferences { showThinkingBlocks: boolean + showKeyboardShortcutHints: boolean thinkingBlocksExpansion: ExpansionPreference showTimelineTools: boolean promptSubmitOnEnter: boolean @@ -78,6 +79,7 @@ const MAX_FAVORITE_MODELS = 50 const defaultPreferences: Preferences = { showThinkingBlocks: false, + showKeyboardShortcutHints: true, thinkingBlocksExpansion: "expanded", showTimelineTools: true, promptSubmitOnEnter: false, @@ -131,6 +133,7 @@ function normalizePreferences(pref?: Partial & { agentModelSelectio return { showThinkingBlocks: sanitized.showThinkingBlocks ?? defaultPreferences.showThinkingBlocks, + showKeyboardShortcutHints: sanitized.showKeyboardShortcutHints ?? defaultPreferences.showKeyboardShortcutHints, thinkingBlocksExpansion: sanitized.thinkingBlocksExpansion ?? defaultPreferences.thinkingBlocksExpansion, showTimelineTools: sanitized.showTimelineTools ?? defaultPreferences.showTimelineTools, promptSubmitOnEnter: sanitized.promptSubmitOnEnter ?? defaultPreferences.promptSubmitOnEnter, @@ -393,6 +396,10 @@ function toggleShowThinkingBlocks(): void { updatePreferences({ showThinkingBlocks: !preferences().showThinkingBlocks }) } +function toggleKeyboardShortcutHints(): void { + updatePreferences({ showKeyboardShortcutHints: !preferences().showKeyboardShortcutHints }) +} + function toggleShowTimelineTools(): void { updatePreferences({ showTimelineTools: !preferences().showTimelineTools }) } @@ -511,6 +518,7 @@ interface ConfigContextValue { setThemePreference: typeof setThemePreference updateConfig: typeof updateConfig toggleShowThinkingBlocks: typeof toggleShowThinkingBlocks + toggleKeyboardShortcutHints: typeof toggleKeyboardShortcutHints toggleShowTimelineTools: typeof toggleShowTimelineTools toggleUsageMetrics: typeof toggleUsageMetrics toggleAutoCleanupBlankSessions: typeof toggleAutoCleanupBlankSessions @@ -548,6 +556,7 @@ const configContextValue: ConfigContextValue = { setThemePreference, updateConfig, toggleShowThinkingBlocks, + toggleKeyboardShortcutHints, toggleShowTimelineTools, toggleUsageMetrics, toggleAutoCleanupBlankSessions, @@ -608,6 +617,7 @@ export { updateConfig, updatePreferences, toggleShowThinkingBlocks, + toggleKeyboardShortcutHints, toggleShowTimelineTools, toggleAutoCleanupBlankSessions, toggleUsageMetrics, diff --git a/packages/ui/src/styles/panels/modal.css b/packages/ui/src/styles/panels/modal.css index 2fa598b5..4edc3b81 100644 --- a/packages/ui/src/styles/panels/modal.css +++ b/packages/ui/src/styles/panels/modal.css @@ -46,6 +46,11 @@ color: var(--text-primary); } +.modal-item:disabled { + opacity: 0.55; + cursor: not-allowed; +} + .modal-list-container[data-pointer-mode="pointer"] .modal-item:hover { background-color: var(--surface-hover); } diff --git a/packages/ui/src/styles/utilities.css b/packages/ui/src/styles/utilities.css index 230f9bd0..44aaa1fd 100644 --- a/packages/ui/src/styles/utilities.css +++ b/packages/ui/src/styles/utilities.css @@ -153,6 +153,19 @@ @apply opacity-50; } +/* + Shortcut hints are useful on desktop native apps, but are noisy/irrelevant on + touch-first devices and in WebUI where browser shortcuts often conflict. +*/ +html[data-runtime-host="web"] .keyboard-hints, +html[data-runtime-host="web"] .kbd-hint, +html[data-runtime-platform="mobile"] .keyboard-hints, +html[data-runtime-platform="mobile"] .kbd-hint, +html[data-keyboard-hints="hide"] .keyboard-hints, +html[data-keyboard-hints="hide"] .kbd-hint { + display: none !important; +} + /* Truncate from the start (keeps end visible; good for paths) */ .truncate-start { overflow: hidden;