shortcut registry keyed (96 -> 73)

shortcuts.ts is data with no component around it, so it carries catalog keys and
ShortcutsModal resolves them -- same shape as the settings sections.

gen-platform-skill.ts also reads this registry to generate the agent-facing SKILL.md,
which docs:check validates. That doc stays in one language, so the generator resolves
against en-GB directly and throws if a key is missing rather than emitting a dot-path
into a file an agent will quote at somebody. The regenerated skill is byte-identical,
which is the check that keying the registry changed nothing an agent reads.
This commit is contained in:
2026-08-23 05:26:43 +00:00
parent 5e032898f8
commit 2e846933e9
5 changed files with 113 additions and 36 deletions
@@ -66,12 +66,16 @@ export const ShortcutsModal = () => {
{/* Only this scrolls — the header stays, for the same reason the theme dropdown's does. */}
<div className="grid min-h-0 flex-1 gap-5 overflow-y-auto p-5 sm:grid-cols-2">
{SHORTCUT_SECTIONS.map((section) => (
<section key={section.title} className="rounded-xl border border-border bg-muted/20 p-4">
<h3 className="text-xs font-semibold uppercase tracking-wide text-foreground/80">{section.title}</h3>
{section.note && <p className="mt-1 text-[11px] leading-snug text-muted-foreground">{section.note}</p>}
<section key={section.titleKey} className="rounded-xl border border-border bg-muted/20 p-4">
<h3 className="text-xs font-semibold uppercase tracking-wide text-foreground/80">
{t(section.titleKey)}
</h3>
{section.noteKey && (
<p className="mt-1 text-[11px] leading-snug text-muted-foreground">{t(section.noteKey)}</p>
)}
<div className="mt-2">
{section.shortcuts.map((shortcut) => (
<Row key={shortcut.keys} keys={shortcut.keys} description={shortcut.description} />
<Row key={shortcut.keys} keys={shortcut.keys} description={t(shortcut.descriptionKey)} />
))}
</div>
</section>
+27
View File
@@ -609,5 +609,32 @@
"camera": "Camera:",
"model": "Model:"
}
},
"shortcuts": {
"navigation": "Navigation",
"spotlight": "Spotlight — go to any screen, plugin or setting by name",
"thisList": "This list",
"themes": "Themes",
"themesNote": "The browser applies as you move, so you watch the app change rather than reading names.",
"openBrowser": "Open the theme browser",
"prevNextTheme": "Previous / next theme, without opening anything",
"starCurrent": "Star or unstar the current theme, from anywhere",
"inBrowser": "In the theme browser",
"moveList": "Move through the list",
"applyKeepBrowsing": "Apply the focused theme and keep browsing",
"restoreStart": "Put back the theme you started on",
"starFocused": "Star or unstar the focused theme",
"keepLooking": "Keep what you are looking at",
"cancelRestore": "Cancel and restore",
"inSpotlight": "In Spotlight",
"moveResults": "Move through the results",
"goThere": "Go there",
"close": "Close",
"chat": "Chat",
"chatNote": "Recall only takes the arrow key from an EMPTY composer, and any other key ends a recall — so a draft is never overwritten.",
"prevNextPrompt": "Previous / next prompt you sent, shell-style",
"send": "Send · on an empty composer with something queued, send that now",
"newline": "Newline",
"stopTurn": "Stop the turn that is generating"
}
}
+27
View File
@@ -609,5 +609,32 @@
"camera": "Câmara:",
"model": "Modelo:"
}
},
"shortcuts": {
"navigation": "Navegação",
"spotlight": "Spotlight — ir para qualquer ecrã, plugin ou definição pelo nome",
"thisList": "Esta lista",
"themes": "Temas",
"themesNote": "O explorador aplica à medida que se move, para que veja a aplicação mudar em vez de ler nomes.",
"openBrowser": "Abrir o explorador de temas",
"prevNextTheme": "Tema anterior / seguinte, sem abrir nada",
"starCurrent": "Marcar ou desmarcar o tema atual, a partir de qualquer lugar",
"inBrowser": "No explorador de temas",
"moveList": "Percorrer a lista",
"applyKeepBrowsing": "Aplicar o tema focado e continuar a explorar",
"restoreStart": "Voltar ao tema em que começou",
"starFocused": "Marcar ou desmarcar o tema focado",
"keepLooking": "Manter o que está a ver",
"cancelRestore": "Cancelar e restaurar",
"inSpotlight": "No Spotlight",
"moveResults": "Percorrer os resultados",
"goThere": "Ir para lá",
"close": "Fechar",
"chat": "Chat",
"chatNote": "A recuperação só usa a tecla de seta num compositor VAZIO, e qualquer outra tecla termina a recuperação — pelo que um rascunho nunca é substituído.",
"prevNextPrompt": "Prompt anterior / seguinte que enviou, à maneira da shell",
"send": "Enviar · num compositor vazio com algo em fila, enviar isso agora",
"newline": "Nova linha",
"stopTurn": "Parar o turno que está a gerar"
}
}
+35 -28
View File
@@ -1,3 +1,5 @@
import type { AppTranslationKey } from '@/lib/i18n';
/*
Every keyboard shortcut the shell owns, as data.
@@ -22,92 +24,97 @@
export type Shortcut = {
/** As a person reads it — `Ctrl+Shift+Space`, not a KeyboardEvent spelling. */
keys: string;
description: string;
/** A catalog key. This is data with no component around it, so ShortcutsModal resolves it. */
descriptionKey: AppTranslationKey;
/** The file whose handler actually implements it, until this registry is authoritative. */
implementedIn: string;
};
export type ShortcutSection = {
title: string;
titleKey: AppTranslationKey;
/** Shown under the title when the section needs a caveat that a per-row description cannot carry. */
note?: string;
noteKey?: AppTranslationKey;
shortcuts: Shortcut[];
};
export const SHORTCUT_SECTIONS: ShortcutSection[] = [
{
title: 'Navigation',
titleKey: 'shortcuts.navigation',
shortcuts: [
{
keys: 'Shift+Space',
description: 'Spotlight — go to any screen, plugin or setting by name',
descriptionKey: 'shortcuts.spotlight',
implementedIn: 'Layout/Spotlight/Spotlight.tsx',
},
{ keys: 'Ctrl+?', description: 'This list', implementedIn: 'Layout/ShortcutsModal.tsx' },
{ keys: 'Ctrl+?', descriptionKey: 'shortcuts.thisList', implementedIn: 'Layout/ShortcutsModal.tsx' },
],
},
{
title: 'Themes',
note: 'The browser applies as you move, so you watch the app change rather than reading names.',
titleKey: 'shortcuts.themes',
noteKey: 'shortcuts.themesNote',
shortcuts: [
{
keys: 'Ctrl+Shift+Space',
description: 'Open the theme browser',
descriptionKey: 'shortcuts.openBrowser',
implementedIn: 'theme/useThemeKeys.ts',
},
{
keys: 'Ctrl+Shift+← / →',
description: 'Previous / next theme, without opening anything',
descriptionKey: 'shortcuts.prevNextTheme',
implementedIn: 'theme/useThemeKeys.ts',
},
{
keys: 'Ctrl+Shift+F',
description: 'Star or unstar the current theme, from anywhere',
descriptionKey: 'shortcuts.starCurrent',
implementedIn: 'theme/useThemeKeys.ts',
},
],
},
{
title: 'In the theme browser',
titleKey: 'shortcuts.inBrowser',
shortcuts: [
{ keys: 'j / k ↑ / ↓', description: 'Move through the list', implementedIn: 'theme/useThemeKeys.ts' },
{ keys: 'j / k ↑ / ↓', descriptionKey: 'shortcuts.moveList', implementedIn: 'theme/useThemeKeys.ts' },
{
keys: 'l →',
description: 'Apply the focused theme and keep browsing',
descriptionKey: 'shortcuts.applyKeepBrowsing',
implementedIn: 'theme/useThemeKeys.ts',
},
{ keys: 'h ←', description: 'Put back the theme you started on', implementedIn: 'theme/useThemeKeys.ts' },
{ keys: 'f', description: 'Star or unstar the focused theme', implementedIn: 'theme/useThemeKeys.ts' },
{ keys: 'Enter', description: 'Keep what you are looking at', implementedIn: 'theme/useThemeKeys.ts' },
{ keys: 'Esc', description: 'Cancel and restore', implementedIn: 'theme/useThemeKeys.ts' },
{ keys: 'h ←', descriptionKey: 'shortcuts.restoreStart', implementedIn: 'theme/useThemeKeys.ts' },
{ keys: 'f', descriptionKey: 'shortcuts.starFocused', implementedIn: 'theme/useThemeKeys.ts' },
{ keys: 'Enter', descriptionKey: 'shortcuts.keepLooking', implementedIn: 'theme/useThemeKeys.ts' },
{ keys: 'Esc', descriptionKey: 'shortcuts.cancelRestore', implementedIn: 'theme/useThemeKeys.ts' },
],
},
{
title: 'In Spotlight',
titleKey: 'shortcuts.inSpotlight',
shortcuts: [
{ keys: '↑ / ↓', description: 'Move through the results', implementedIn: 'Layout/Spotlight/Spotlight.tsx' },
{ keys: 'Enter', description: 'Go there', implementedIn: 'Layout/Spotlight/Spotlight.tsx' },
{ keys: 'Esc', description: 'Close', implementedIn: 'Layout/Spotlight/Spotlight.tsx' },
{ keys: '↑ / ↓', descriptionKey: 'shortcuts.moveResults', implementedIn: 'Layout/Spotlight/Spotlight.tsx' },
{ keys: 'Enter', descriptionKey: 'shortcuts.goThere', implementedIn: 'Layout/Spotlight/Spotlight.tsx' },
{ keys: 'Esc', descriptionKey: 'shortcuts.close', implementedIn: 'Layout/Spotlight/Spotlight.tsx' },
],
},
{
title: 'Chat',
note: 'Recall only takes the arrow key from an EMPTY composer, and any other key ends a recall — so a draft is never overwritten.',
titleKey: 'shortcuts.chat',
noteKey: 'shortcuts.chatNote',
shortcuts: [
{
keys: '↑ / ↓',
description: 'Previous / next prompt you sent, shell-style',
descriptionKey: 'shortcuts.prevNextPrompt',
implementedIn: 'Chat/EmbeddableChat/useEmbeddableChat.ts',
},
{
keys: 'Enter',
description: 'Send · on an empty composer with something queued, send that now',
descriptionKey: 'shortcuts.send',
implementedIn: 'Chat/EmbeddableChat/useEmbeddableChat.ts',
},
{
keys: 'Shift+Enter',
descriptionKey: 'shortcuts.newline',
implementedIn: 'Chat/EmbeddableChat/useEmbeddableChat.ts',
},
{ keys: 'Shift+Enter', description: 'Newline', implementedIn: 'Chat/EmbeddableChat/useEmbeddableChat.ts' },
{
keys: 'Esc',
description: 'Stop the turn that is generating',
descriptionKey: 'shortcuts.stopTurn',
implementedIn: 'Chat/EmbeddableChat/useEmbeddableChat.ts',
},
],