diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts
index 4adf945b61..74f43209e2 100644
--- a/packages/app/src/i18n/resources/ar.ts
+++ b/packages/app/src/i18n/resources/ar.ts
@@ -2333,6 +2333,7 @@ export const ar: TranslationResources = {
searchPlaceholder: "البحث في الاختصارات",
unavailableOnMobile: "اختصارات لوحة المفاتيح متاحة فقط على سطح المكتب",
capturePrompt: "اضغط على الاختصار...",
+ captureIndexPrompt: "اختم برقم من 1 إلى 9.",
unassigned: "غير معين",
actions: {
menu: "إجراءات {{name}}",
diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts
index 9bb0460036..5aedaad115 100644
--- a/packages/app/src/i18n/resources/en.ts
+++ b/packages/app/src/i18n/resources/en.ts
@@ -2456,6 +2456,7 @@ export const en = {
searchPlaceholder: "Search shortcuts",
unavailableOnMobile: "Keyboard shortcuts are only available on desktop",
capturePrompt: "Press shortcut...",
+ captureIndexPrompt: "End with a number from 1 to 9.",
unassigned: "Not set",
actions: {
menu: "Actions for {{name}}",
diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts
index 9ed1e85572..a996fc97de 100644
--- a/packages/app/src/i18n/resources/es.ts
+++ b/packages/app/src/i18n/resources/es.ts
@@ -2387,6 +2387,7 @@ export const es: TranslationResources = {
searchPlaceholder: "Buscar atajos",
unavailableOnMobile: "Los atajos de teclado solo están disponibles en el escritorio",
capturePrompt: "Presione el acceso directo...",
+ captureIndexPrompt: "Termina con un número del 1 al 9.",
unassigned: "Sin asignar",
actions: {
menu: "Acciones para {{name}}",
diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts
index 6563b1100e..253b4135f9 100644
--- a/packages/app/src/i18n/resources/fr.ts
+++ b/packages/app/src/i18n/resources/fr.ts
@@ -2387,6 +2387,7 @@ export const fr: TranslationResources = {
searchPlaceholder: "Rechercher des raccourcis",
unavailableOnMobile: "Les raccourcis clavier ne sont disponibles que sur l’app de bureau",
capturePrompt: "Appuyez sur le raccourci…",
+ captureIndexPrompt: "Terminez par un chiffre de 1 à 9.",
unassigned: "Non attribué",
actions: {
menu: "Actions pour {{name}}",
diff --git a/packages/app/src/i18n/resources/ja.ts b/packages/app/src/i18n/resources/ja.ts
index eb22dd663a..7e22c7d5b9 100644
--- a/packages/app/src/i18n/resources/ja.ts
+++ b/packages/app/src/i18n/resources/ja.ts
@@ -2354,6 +2354,7 @@ export const ja: TranslationResources = {
searchPlaceholder: "ショートカットを検索",
unavailableOnMobile: "キーボードショートカットはデスクトップでのみ利用できます",
capturePrompt: "ショートカットを押してください...",
+ captureIndexPrompt: "最後に1〜9の数字を入力してください。",
unassigned: "未設定",
actions: {
menu: "{{name}} のアクション",
diff --git a/packages/app/src/i18n/resources/ko.ts b/packages/app/src/i18n/resources/ko.ts
index afdb8974e9..a6d8c44cbf 100644
--- a/packages/app/src/i18n/resources/ko.ts
+++ b/packages/app/src/i18n/resources/ko.ts
@@ -2343,6 +2343,7 @@ export const ko: TranslationResources = {
searchPlaceholder: "검색 단축키",
unavailableOnMobile: "키보드 단축키는 데스크톱에서만 사용할 수 있습니다",
capturePrompt: "단축키를 누르세요...",
+ captureIndexPrompt: "마지막에 1부터 9까지의 숫자를 입력하세요.",
unassigned: "설정되지 않음",
actions: {
menu: "{{name}} 작업",
diff --git a/packages/app/src/i18n/resources/pt-BR.ts b/packages/app/src/i18n/resources/pt-BR.ts
index f89fb97bf0..ecda7818af 100644
--- a/packages/app/src/i18n/resources/pt-BR.ts
+++ b/packages/app/src/i18n/resources/pt-BR.ts
@@ -2368,6 +2368,7 @@ export const ptBR: TranslationResources = {
searchPlaceholder: "Pesquisar atalhos",
unavailableOnMobile: "Atalhos de teclado estão disponíveis apenas no desktop",
capturePrompt: "Pressione o atalho...",
+ captureIndexPrompt: "Termine com um número de 1 a 9.",
unassigned: "Não definido",
actions: {
menu: "Ações para {{name}}",
diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts
index a1a70822b4..e110866c52 100644
--- a/packages/app/src/i18n/resources/ru.ts
+++ b/packages/app/src/i18n/resources/ru.ts
@@ -2373,6 +2373,7 @@ export const ru: TranslationResources = {
searchPlaceholder: "Поиск сочетаний клавиш",
unavailableOnMobile: "Сочетания клавиш доступны только на рабочем столе.",
capturePrompt: "Нажмите сочетание клавиш...",
+ captureIndexPrompt: "Завершите цифрой от 1 до 9.",
unassigned: "Не задано",
actions: {
menu: "Действия для {{name}}",
diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts
index ca221d7d3a..f60f1a385b 100644
--- a/packages/app/src/i18n/resources/zh-CN.ts
+++ b/packages/app/src/i18n/resources/zh-CN.ts
@@ -2305,6 +2305,7 @@ export const zhCN: TranslationResources = {
searchPlaceholder: "搜索快捷键",
unavailableOnMobile: "键盘快捷键仅在桌面端可用",
capturePrompt: "按下快捷键...",
+ captureIndexPrompt: "请以 1 到 9 的数字结尾。",
unassigned: "未设置",
actions: {
menu: "{{name}} 的操作",
diff --git a/packages/app/src/keyboard/keyboard-shortcuts.test.ts b/packages/app/src/keyboard/keyboard-shortcuts.test.ts
index 9e56ef66cd..770d7e710c 100644
--- a/packages/app/src/keyboard/keyboard-shortcuts.test.ts
+++ b/packages/app/src/keyboard/keyboard-shortcuts.test.ts
@@ -4,7 +4,9 @@ import { keyboardEventToComboString } from "./shortcut-string";
import {
buildKeyboardShortcutHelpSections,
buildEffectiveBindings,
+ DEFAULT_BINDINGS,
normalizeCapturedShortcutCombo,
+ isShortcutCaptureComplete,
getBindingIdForAction,
getDefaultKeysForAction,
getWorkspaceIndexJumpModifierKey,
@@ -1390,3 +1392,22 @@ describe("number-group shortcut capture", () => {
expect(normalizeCapturedShortcutCombo(tabBindingId, "Ctrl+9")).toBe("Ctrl+Digit");
});
});
+
+describe("shortcut capture completion", () => {
+ const indexBindings = DEFAULT_BINDINGS.filter((binding) => binding.payload?.type === "index");
+ it.each(indexBindings)("requires a final 1–9 number for $id", ({ id }) => {
+ for (const combos of [[], ["Ctrl+K"], ["Ctrl+0"], ["Ctrl+Digit", "Ctrl+K"]]) {
+ expect(isShortcutCaptureComplete(id, combos)).toBe(false);
+ }
+ for (let index = 1; index <= 9; index++) {
+ const combo = normalizeCapturedShortcutCombo(id, `Ctrl+${index}`);
+ expect(isShortcutCaptureComplete(id, [combo])).toBe(true);
+ expect(isShortcutCaptureComplete(id, ["Ctrl+K", combo])).toBe(true);
+ }
+ });
+ it("keeps ordinary shortcuts available without a number", () => {
+ expect(isShortcutCaptureComplete("show-shortcuts", [])).toBe(false);
+ expect(isShortcutCaptureComplete("show-shortcuts", ["Ctrl+K"])).toBe(true);
+ expect(isShortcutCaptureComplete("show-shortcuts", ["Ctrl+0"])).toBe(true);
+ });
+});
diff --git a/packages/app/src/keyboard/keyboard-shortcuts.ts b/packages/app/src/keyboard/keyboard-shortcuts.ts
index a009008b85..9f6329c013 100644
--- a/packages/app/src/keyboard/keyboard-shortcuts.ts
+++ b/packages/app/src/keyboard/keyboard-shortcuts.ts
@@ -1217,6 +1217,14 @@ export function normalizeCapturedShortcutCombo(bindingId: string, combo: string)
return combo.replace(/(^|\+)[1-9]$/, "$1Digit");
}
+/** Index navigation needs a final number; earlier combos may be any chord prefix. */
+export function isShortcutCaptureComplete(bindingId: string, combos: readonly string[]): boolean {
+ const lastCombo = combos.at(-1);
+ if (!lastCombo) return false;
+ const binding = DEFAULT_BINDINGS.find((candidate) => candidate.id === bindingId);
+ return binding?.payload?.type !== "index" || /(^|\+)(Digit|[1-9])$/.test(lastCombo);
+}
+
export function buildEffectiveBindings(overrides: ShortcutOverrides): ParsedShortcutBinding[] {
return DEFAULT_BINDINGS.map(function (binding) {
const override = overrides[binding.id];
diff --git a/packages/app/src/screens/settings/keyboard-shortcuts-section.tsx b/packages/app/src/screens/settings/keyboard-shortcuts-section.tsx
index 8a8c4b42b9..6a8459fc3f 100644
--- a/packages/app/src/screens/settings/keyboard-shortcuts-section.tsx
+++ b/packages/app/src/screens/settings/keyboard-shortcuts-section.tsx
@@ -20,6 +20,7 @@ import {
buildKeyboardShortcutHelpSections,
getBindingIdForAction,
normalizeCapturedShortcutCombo,
+ isShortcutCaptureComplete,
getDefaultKeysForAction,
resolveShortcutKeysForAction,
type KeyboardShortcutHelpRow,
@@ -269,6 +270,8 @@ function ShortcutRow({
const isBindable = bindingId !== null;
const showDone = isCapturing && capturedCombos.length > 0;
+ const captureComplete =
+ bindingId !== null && isShortcutCaptureComplete(bindingId, capturedCombos);
const showClear = displayChord !== null;
// Reset restores the default, so it is only meaningful when there is a
// default to restore. A binding that ships without one would otherwise show a
@@ -288,11 +291,14 @@ function ShortcutRow({
capturedCombos={capturedCombos}
heldModifiers={heldModifiers}
/>
+ {showDone && !captureComplete && (
+ {t("settings.shortcuts.captureIndexPrompt")}
+ )}
{isCapturing ? (
<>
{showDone && (
-