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:
@@ -38,6 +38,16 @@ const KIND_MEANING: Record<string, string> = {
|
|||||||
export async function renderPlatformSkill(): Promise<string> {
|
export async function renderPlatformSkill(): Promise<string> {
|
||||||
const { PERMISSIONS } = await import('../src/servers/permissions/registry');
|
const { PERMISSIONS } = await import('../src/servers/permissions/registry');
|
||||||
const { SHORTCUT_SECTIONS } = await import('../src/apps/officer-web/state/shortcuts');
|
const { SHORTCUT_SECTIONS } = await import('../src/apps/officer-web/state/shortcuts');
|
||||||
|
const enCatalog = (await import('../src/apps/officer-web/locales/en-GB.json')).default as Record<string, unknown>;
|
||||||
|
/** Resolve a dot-path against the English catalog. This doc is for agents and stays in one language. */
|
||||||
|
const enString = (key: string): string => {
|
||||||
|
const value = key.split('.').reduce<unknown>((node, part) => {
|
||||||
|
if (node && typeof node === 'object') return (node as Record<string, unknown>)[part];
|
||||||
|
return undefined;
|
||||||
|
}, enCatalog);
|
||||||
|
if (typeof value !== 'string') throw new Error(`gen-platform-skill: no English string for "${key}"`);
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
const byKind = new Map<string, typeof PERMISSIONS>();
|
const byKind = new Map<string, typeof PERMISSIONS>();
|
||||||
for (const p of PERMISSIONS) {
|
for (const p of PERMISSIONS) {
|
||||||
@@ -112,14 +122,16 @@ export async function renderPlatformSkill(): Promise<string> {
|
|||||||
out('Derived from `src/apps/officer-web/state/shortcuts.ts`. These have moved several times; quote this');
|
out('Derived from `src/apps/officer-web/state/shortcuts.ts`. These have moved several times; quote this');
|
||||||
out('list rather than remembering one.');
|
out('list rather than remembering one.');
|
||||||
out();
|
out();
|
||||||
|
// The registry holds catalog keys since the shell was localized; this doc is agent-facing and stays
|
||||||
|
// English, so it resolves against en-GB rather than the reader's locale.
|
||||||
for (const section of SHORTCUT_SECTIONS) {
|
for (const section of SHORTCUT_SECTIONS) {
|
||||||
out(`## ${section.title}`);
|
out(`## ${enString(section.titleKey)}`);
|
||||||
if (section.note) {
|
if (section.noteKey) {
|
||||||
out();
|
out();
|
||||||
out(section.note);
|
out(enString(section.noteKey));
|
||||||
}
|
}
|
||||||
out();
|
out();
|
||||||
for (const s of section.shortcuts) out(`- \`${s.keys}\` — ${s.description}`);
|
for (const s of section.shortcuts) out(`- \`${s.keys}\` — ${enString(s.descriptionKey)}`);
|
||||||
out();
|
out();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,12 +66,16 @@ export const ShortcutsModal = () => {
|
|||||||
{/* Only this scrolls — the header stays, for the same reason the theme dropdown's does. */}
|
{/* 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">
|
<div className="grid min-h-0 flex-1 gap-5 overflow-y-auto p-5 sm:grid-cols-2">
|
||||||
{SHORTCUT_SECTIONS.map((section) => (
|
{SHORTCUT_SECTIONS.map((section) => (
|
||||||
<section key={section.title} className="rounded-xl border border-border bg-muted/20 p-4">
|
<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">{section.title}</h3>
|
<h3 className="text-xs font-semibold uppercase tracking-wide text-foreground/80">
|
||||||
{section.note && <p className="mt-1 text-[11px] leading-snug text-muted-foreground">{section.note}</p>}
|
{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">
|
<div className="mt-2">
|
||||||
{section.shortcuts.map((shortcut) => (
|
{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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -609,5 +609,32 @@
|
|||||||
"camera": "Camera:",
|
"camera": "Camera:",
|
||||||
"model": "Model:"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -609,5 +609,32 @@
|
|||||||
"camera": "Câmara:",
|
"camera": "Câmara:",
|
||||||
"model": "Modelo:"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { AppTranslationKey } from '@/lib/i18n';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Every keyboard shortcut the shell owns, as data.
|
Every keyboard shortcut the shell owns, as data.
|
||||||
|
|
||||||
@@ -22,92 +24,97 @@
|
|||||||
export type Shortcut = {
|
export type Shortcut = {
|
||||||
/** As a person reads it — `Ctrl+Shift+Space`, not a KeyboardEvent spelling. */
|
/** As a person reads it — `Ctrl+Shift+Space`, not a KeyboardEvent spelling. */
|
||||||
keys: string;
|
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. */
|
/** The file whose handler actually implements it, until this registry is authoritative. */
|
||||||
implementedIn: string;
|
implementedIn: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ShortcutSection = {
|
export type ShortcutSection = {
|
||||||
title: string;
|
titleKey: AppTranslationKey;
|
||||||
/** Shown under the title when the section needs a caveat that a per-row description cannot carry. */
|
/** Shown under the title when the section needs a caveat that a per-row description cannot carry. */
|
||||||
note?: string;
|
noteKey?: AppTranslationKey;
|
||||||
shortcuts: Shortcut[];
|
shortcuts: Shortcut[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SHORTCUT_SECTIONS: ShortcutSection[] = [
|
export const SHORTCUT_SECTIONS: ShortcutSection[] = [
|
||||||
{
|
{
|
||||||
title: 'Navigation',
|
titleKey: 'shortcuts.navigation',
|
||||||
shortcuts: [
|
shortcuts: [
|
||||||
{
|
{
|
||||||
keys: 'Shift+Space',
|
keys: 'Shift+Space',
|
||||||
description: 'Spotlight — go to any screen, plugin or setting by name',
|
descriptionKey: 'shortcuts.spotlight',
|
||||||
implementedIn: 'Layout/Spotlight/Spotlight.tsx',
|
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',
|
titleKey: 'shortcuts.themes',
|
||||||
note: 'The browser applies as you move, so you watch the app change rather than reading names.',
|
noteKey: 'shortcuts.themesNote',
|
||||||
shortcuts: [
|
shortcuts: [
|
||||||
{
|
{
|
||||||
keys: 'Ctrl+Shift+Space',
|
keys: 'Ctrl+Shift+Space',
|
||||||
description: 'Open the theme browser',
|
descriptionKey: 'shortcuts.openBrowser',
|
||||||
implementedIn: 'theme/useThemeKeys.ts',
|
implementedIn: 'theme/useThemeKeys.ts',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keys: 'Ctrl+Shift+← / →',
|
keys: 'Ctrl+Shift+← / →',
|
||||||
description: 'Previous / next theme, without opening anything',
|
descriptionKey: 'shortcuts.prevNextTheme',
|
||||||
implementedIn: 'theme/useThemeKeys.ts',
|
implementedIn: 'theme/useThemeKeys.ts',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keys: 'Ctrl+Shift+F',
|
keys: 'Ctrl+Shift+F',
|
||||||
description: 'Star or unstar the current theme, from anywhere',
|
descriptionKey: 'shortcuts.starCurrent',
|
||||||
implementedIn: 'theme/useThemeKeys.ts',
|
implementedIn: 'theme/useThemeKeys.ts',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'In the theme browser',
|
titleKey: 'shortcuts.inBrowser',
|
||||||
shortcuts: [
|
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 →',
|
keys: 'l →',
|
||||||
description: 'Apply the focused theme and keep browsing',
|
descriptionKey: 'shortcuts.applyKeepBrowsing',
|
||||||
implementedIn: 'theme/useThemeKeys.ts',
|
implementedIn: 'theme/useThemeKeys.ts',
|
||||||
},
|
},
|
||||||
{ keys: 'h ←', description: 'Put back the theme you started on', implementedIn: 'theme/useThemeKeys.ts' },
|
{ keys: 'h ←', descriptionKey: 'shortcuts.restoreStart', implementedIn: 'theme/useThemeKeys.ts' },
|
||||||
{ keys: 'f', description: 'Star or unstar the focused theme', implementedIn: 'theme/useThemeKeys.ts' },
|
{ keys: 'f', descriptionKey: 'shortcuts.starFocused', implementedIn: 'theme/useThemeKeys.ts' },
|
||||||
{ keys: 'Enter', description: 'Keep what you are looking at', implementedIn: 'theme/useThemeKeys.ts' },
|
{ keys: 'Enter', descriptionKey: 'shortcuts.keepLooking', implementedIn: 'theme/useThemeKeys.ts' },
|
||||||
{ keys: 'Esc', description: 'Cancel and restore', implementedIn: 'theme/useThemeKeys.ts' },
|
{ keys: 'Esc', descriptionKey: 'shortcuts.cancelRestore', implementedIn: 'theme/useThemeKeys.ts' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'In Spotlight',
|
titleKey: 'shortcuts.inSpotlight',
|
||||||
shortcuts: [
|
shortcuts: [
|
||||||
{ keys: '↑ / ↓', description: 'Move through the results', implementedIn: 'Layout/Spotlight/Spotlight.tsx' },
|
{ keys: '↑ / ↓', descriptionKey: 'shortcuts.moveResults', implementedIn: 'Layout/Spotlight/Spotlight.tsx' },
|
||||||
{ keys: 'Enter', description: 'Go there', implementedIn: 'Layout/Spotlight/Spotlight.tsx' },
|
{ keys: 'Enter', descriptionKey: 'shortcuts.goThere', implementedIn: 'Layout/Spotlight/Spotlight.tsx' },
|
||||||
{ keys: 'Esc', description: 'Close', implementedIn: 'Layout/Spotlight/Spotlight.tsx' },
|
{ keys: 'Esc', descriptionKey: 'shortcuts.close', implementedIn: 'Layout/Spotlight/Spotlight.tsx' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Chat',
|
titleKey: 'shortcuts.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.',
|
noteKey: 'shortcuts.chatNote',
|
||||||
shortcuts: [
|
shortcuts: [
|
||||||
{
|
{
|
||||||
keys: '↑ / ↓',
|
keys: '↑ / ↓',
|
||||||
description: 'Previous / next prompt you sent, shell-style',
|
descriptionKey: 'shortcuts.prevNextPrompt',
|
||||||
implementedIn: 'Chat/EmbeddableChat/useEmbeddableChat.ts',
|
implementedIn: 'Chat/EmbeddableChat/useEmbeddableChat.ts',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keys: 'Enter',
|
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',
|
implementedIn: 'Chat/EmbeddableChat/useEmbeddableChat.ts',
|
||||||
},
|
},
|
||||||
{ keys: 'Shift+Enter', description: 'Newline', implementedIn: 'Chat/EmbeddableChat/useEmbeddableChat.ts' },
|
|
||||||
{
|
{
|
||||||
keys: 'Esc',
|
keys: 'Esc',
|
||||||
description: 'Stop the turn that is generating',
|
descriptionKey: 'shortcuts.stopTurn',
|
||||||
implementedIn: 'Chat/EmbeddableChat/useEmbeddableChat.ts',
|
implementedIn: 'Chat/EmbeddableChat/useEmbeddableChat.ts',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user