diff --git a/apps/electron/src/renderer/components/CommandPalette.tsx b/apps/electron/src/renderer/components/CommandPalette.tsx index fdad360f8..ce8e4ba3f 100644 --- a/apps/electron/src/renderer/components/CommandPalette.tsx +++ b/apps/electron/src/renderer/components/CommandPalette.tsx @@ -34,11 +34,13 @@ import { useRegisterModal } from '@/context/ModalContext' import { useAction, useActionRegistry, + actions as actionDefinitions, actionsByCategory, ACTION_LABEL_KEYS, categoryLabelKey, type ActionId, } from '@/actions' +import { readRecents, recordRecent } from './command-palette-recents' // Actions that should not appear as palette entries: // - the palette's own open action (running it from inside the palette is a no-op) @@ -54,14 +56,35 @@ export function CommandPalette() { const { t } = useTranslation() const { execute, canExecute, getHotkeyDisplay } = useActionRegistry() const [open, setOpen] = useState(false) + const [search, setSearch] = useState('') // ⌘K / Ctrl+K toggles the palette. useAction('app.commandPalette', () => setOpen(prev => !prev)) + // Reset the query each time the palette opens so it always starts fresh + // (and the "Recently used" group — shown only for an empty query — is visible). + const handleOpenChange = useCallback((next: boolean) => { + if (next) setSearch('') + setOpen(next) + }, []) + // Participate in the layered modal stack (Cmd+W / X close the topmost modal). const handleClose = useCallback(() => setOpen(false), []) useRegisterModal(open, handleClose) + // Turn a runnable action id into a display-ready palette entry. + const toItem = useCallback( + (id: ActionId) => { + const labelKey = ACTION_LABEL_KEYS[id] + return { + id, + label: labelKey ? t(labelKey) : actionDefinitions[id].label, + hotkey: getHotkeyDisplay(id), + } + }, + [t, getHotkeyDisplay], + ) + // Build the grouped, display-ready action list once. const groups = useMemo(() => { return Object.entries(actionsByCategory) @@ -75,23 +98,31 @@ export function CommandPalette() { // is disabled, so the palette never shows a dead entry. Evaluated as // the palette opens, i.e. against the focus you're returning to. .filter(action => canExecute(action.id as ActionId)) - .map(action => { - const id = action.id as ActionId - const labelKey = ACTION_LABEL_KEYS[id] - return { - id, - label: labelKey ? t(labelKey) : action.label, - hotkey: getHotkeyDisplay(id), - } - }), + .map(action => toItem(action.id as ActionId)), })) .filter(group => group.items.length > 0) // getHotkeyDisplay / t are stable enough for a menu; recompute on open. // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]) + // "Recently used": the most-recently-run actions, newest first. Kept to + // entries that still exist, aren't excluded from the palette, and can run + // right now — so the group never surfaces a stale or dead command. Only + // shown for an empty query (a search should rank by relevance, not recency). + const recentItems = useMemo(() => { + return readRecents() + .filter((id): id is ActionId => id in actionDefinitions) + .filter(id => !EXCLUDED_ACTIONS.has(id)) + .filter(id => canExecute(id)) + .map(id => toItem(id)) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open]) + const showRecent = search.trim() === '' && recentItems.length > 0 + const runAction = useCallback( (id: ActionId) => { + // Remember this action so it surfaces in "Recently used" next time. + recordRecent(id) // Close first, then run on the next tick. Closing the dialog restores // focus to the element that was active before the palette opened, so the // action runs in the app's real focus context — actions that open a panel @@ -104,7 +135,7 @@ export function CommandPalette() { ) return ( - + {t('common.noResultsFound')} + {showRecent && ( + + {recentItems.map(item => ( + runAction(item.id)} + > + {item.label} + {item.hotkey && ( + {item.hotkey} + )} + + ))} + + )} {groups.map(group => ( {group.items.map(item => ( diff --git a/apps/electron/src/renderer/components/__tests__/command-palette-recents.test.ts b/apps/electron/src/renderer/components/__tests__/command-palette-recents.test.ts new file mode 100644 index 000000000..1e66a17f4 --- /dev/null +++ b/apps/electron/src/renderer/components/__tests__/command-palette-recents.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'bun:test' +import { pushRecent, MAX_RECENTS } from '../command-palette-recents' + +describe('pushRecent', () => { + it('prepends a new id as most recent', () => { + expect(pushRecent(['a', 'b'], 'c')).toEqual(['c', 'a', 'b']) + }) + + it('moves an existing id to the front without duplicating it', () => { + expect(pushRecent(['a', 'b', 'c'], 'c')).toEqual(['c', 'a', 'b']) + expect(pushRecent(['a', 'b', 'c'], 'b')).toEqual(['b', 'a', 'c']) + }) + + it('is idempotent when re-running the current top action', () => { + expect(pushRecent(['a', 'b'], 'a')).toEqual(['a', 'b']) + }) + + it('caps the list at the requested size, dropping the oldest', () => { + expect(pushRecent(['a', 'b', 'c'], 'd', 3)).toEqual(['d', 'a', 'b']) + }) + + it('defaults the cap to MAX_RECENTS', () => { + const many = Array.from({ length: MAX_RECENTS + 3 }, (_, i) => `id-${i}`) + const result = pushRecent(many, 'fresh') + expect(result).toHaveLength(MAX_RECENTS) + expect(result[0]).toBe('fresh') + }) + + it('does not mutate the input list', () => { + const input = ['a', 'b'] + const copy = [...input] + pushRecent(input, 'c') + expect(input).toEqual(copy) + }) +}) diff --git a/apps/electron/src/renderer/components/command-palette-recents.ts b/apps/electron/src/renderer/components/command-palette-recents.ts new file mode 100644 index 000000000..79076d7f3 --- /dev/null +++ b/apps/electron/src/renderer/components/command-palette-recents.ts @@ -0,0 +1,40 @@ +/** + * Command-palette "recently used" history. + * + * A small persisted list of the action IDs most recently run from the command + * palette, newest first. The palette surfaces these in a dedicated "Recently + * used" group at the top so the commands you actually use are one keystroke + * away — the standard command-palette convention (VS Code, Raycast, Linear). + * + * The ordering logic (`pushRecent`) is a pure function so it can be unit-tested + * without a DOM; the read/record wrappers persist through the shared + * localStorage helper. + */ + +import { get, set, KEYS } from '@/lib/local-storage' + +/** How many recent actions to remember. */ +export const MAX_RECENTS = 6 + +/** + * Return a new recents list with `id` moved to the front (most recent). + * + * Pure: no I/O. Removes any earlier occurrence of `id` (so an action never + * appears twice), prepends it, and caps the list at `cap` entries. + */ +export function pushRecent(list: readonly string[], id: string, cap = MAX_RECENTS): string[] { + const withoutId = list.filter((entry) => entry !== id) + return [id, ...withoutId].slice(0, Math.max(0, cap)) +} + +/** Read the persisted recent action IDs (newest first). Never throws. */ +export function readRecents(): string[] { + const value = get(KEYS.commandPaletteRecents, []) + if (!Array.isArray(value)) return [] + return value.filter((entry): entry is string => typeof entry === 'string') +} + +/** Record `id` as the most recently run action and persist the trimmed list. */ +export function recordRecent(id: string): void { + set(KEYS.commandPaletteRecents, pushRecent(readRecents(), id)) +} diff --git a/apps/electron/src/renderer/lib/local-storage.ts b/apps/electron/src/renderer/lib/local-storage.ts index 4d6446afd..a10bf1a6c 100644 --- a/apps/electron/src/renderer/lib/local-storage.ts +++ b/apps/electron/src/renderer/lib/local-storage.ts @@ -50,6 +50,9 @@ export const KEYS = { // Settings navigation lastSettingsSubpage: 'last-settings-subpage', + // Command palette (most-recently-run action IDs, newest first) + commandPaletteRecents: 'command-palette-recents', + // Appearance showConnectionIcons: 'show-connection-icons', reduceMotion: 'reduce-motion', // Minimize animations/transitions app-wide diff --git a/docs/loop/feature-ledger.md b/docs/loop/feature-ledger.md index 51235562a..0f39ee1cc 100644 --- a/docs/loop/feature-ledger.md +++ b/docs/loop/feature-ledger.md @@ -32,9 +32,13 @@ log, not the system of record. | slug | title | source | feasibility | status | issue | pr | branch | updated | notes | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| reduce-motion | "Reduce motion" accessibility setting in Appearance | Claude desktop / macOS / Windows reduce-motion + `prefers-reduced-motion` | frontend-only | pr-open | [#50](https://github.com/modelstudioai/openwork/issues/50) | [#51](https://github.com/modelstudioai/openwork/pull/51) | loop/reduce-motion | 2026-07-03 | Renderer-only pref (localStorage) applied app-wide via `` + `data-reduce-motion` on `` + global CSS guard. Off ⇒ `reducedMotion="user"` (still honors OS). New `ReduceMotionProvider` in `main.tsx`; toggle in Appearance→Interface; 2 new i18n keys ×7 locales. typecheck/`bun test` zero-delta vs main (56-failure set byte-identical); renderer build ✅; i18n parity ✅. CDP assertion included; **could not run locally** (egress 403s Electron binary download). | -| composer-expand | Expand / collapse (maximize) toggle for the chat composer | Claude/ChatGPT/Codex desktop composer maximize | frontend-only | pr-open | [#48](https://github.com/modelstudioai/openwork/issues/48) | [#49](https://github.com/modelstudioai/openwork/pull/49) | loop/composer-expand | 2026-07-03 | Opened by a prior run. Adds `isComposerExpanded` toggle in `FreeFormInput`; 2 new i18n keys. Awaiting review. | -| scroll-to-bottom | "Jump to latest" (scroll-to-bottom) button in the chat transcript | Claude Code / ChatGPT / Codex desktop | frontend-only | pr-open | [#46](https://github.com/modelstudioai/openwork/issues/46) | [#47](https://github.com/modelstudioai/openwork/pull/47) | loop/scroll-to-bottom | 2026-07-02 | Opened by a prior run. Floating jump button in `ChatDisplay` + `seed()` harness hook. Awaiting review. | -| thinking-level-picker | Thinking-level (reasoning effort) picker in the chat composer | Claude Code Desktop effort menu (⌘⇧E) + OpenWork's own model picker | frontend-only | merged | [#44](https://github.com/modelstudioai/openwork/issues/44) | [#45](https://github.com/modelstudioai/openwork/pull/45) | loop/thinking-level-picker | 2026-07-03 | **Merged** into `main` (2026-07-02). `thinkingLevel`/`onThinkingLevelChange` already plumbed to `FreeFormInput`; only the UI trigger was missing. Reuses `thinking.*` + `settings.ai.thinking` i18n keys (zero new keys). | +| command-palette-recents | "Recently used" section in the Command Palette (⌘K) | VS Code ⌘⇧P / Raycast / Linear ⌘K / Claude Code Desktop command menu | frontend-only | pr-open | [#56](https://github.com/modelstudioai/openwork/issues/56) | [#57](https://github.com/modelstudioai/openwork/pull/57) | loop/command-palette-recents | 2026-07-05 | Palette (#42) had no memory of what you run. Adds a persisted recents list (`localStorage` `command-palette-recents`, dedupe/cap-6, newest-first) surfaced as a top "Recently used" group, hidden while searching. Pure `pushRecent` module + 6 unit tests (pass). One new i18n key `commands.recent` × 7 locales. typecheck/`bun test` zero-delta vs main (56 baseline fails on both, 0 new); renderer build ✅. **CDP could not run locally** (same env block as prior rounds: egress 403s Electron binary + `libsignal`/WhatsApp-worker dep) — assertion included for CI/reviewer. | +| thinking-menu-shortcut | Keyboard shortcut (⌘⇧E) to open the composer's thinking menu | Claude Code Desktop effort-menu shortcut (⌘⇧E) | frontend-only | pr-open | [#54](https://github.com/modelstudioai/openwork/issues/54) | [#55](https://github.com/modelstudioai/openwork/pull/55) | loop/thinking-menu-shortcut | 2026-07-05 | Reconciled from GitHub. Awaiting review. | +| composer-prompt-history | Recall previously-sent prompts with ↑/↓ in the composer | Codex desktop up-arrow recall / Claude CLI / shell history | frontend-only | pr-open | [#52](https://github.com/modelstudioai/openwork/issues/52) | [#53](https://github.com/modelstudioai/openwork/pull/53) | loop/composer-prompt-history | 2026-07-05 | Reconciled from GitHub. Awaiting review. | +| reduce-motion | "Reduce motion" accessibility setting in Appearance | Claude desktop / OS `prefers-reduced-motion` | frontend-only | pr-open | [#50](https://github.com/modelstudioai/openwork/issues/50) | [#51](https://github.com/modelstudioai/openwork/pull/51) | loop/reduce-motion | 2026-07-05 | Reconciled from GitHub. Awaiting review. Touches Appearance → Interface + renderer root `MotionConfig`. | +| composer-expand | Expand / collapse (maximize) toggle for the chat composer | Claude / ChatGPT / Codex desktop composer maximize | frontend-only | pr-open | [#48](https://github.com/modelstudioai/openwork/issues/48) | [#49](https://github.com/modelstudioai/openwork/pull/49) | loop/composer-expand | 2026-07-05 | Reconciled from GitHub. Awaiting review. | +| scroll-to-bottom | "Jump to latest" (scroll-to-bottom) button for the transcript | Claude Code Desktop / ChatGPT / Codex desktop | frontend-only | pr-open | [#46](https://github.com/modelstudioai/openwork/issues/46) | [#47](https://github.com/modelstudioai/openwork/pull/47) | loop/scroll-to-bottom | 2026-07-05 | Reconciled from GitHub. Awaiting review. Adds an `e2e/app.ts` `seed(profileDirs)` hook (on-disk session seeding) — not yet on `main`, so transcript-dependent assertions must wait for this to land. | +| app-search-cmdf-bug | `Cmd+F` / `app.search` shortcut doesn't activate session search | Pre-existing OpenWork bug (not a desktop-feature gap) | frontend-only | proposed | [#43](https://github.com/modelstudioai/openwork/issues/43) | – | – | 2026-07-05 | Open bug issue, no PR/branch yet. Recorded for tracking; not a Claude/Codex feature gap. | +| thinking-level-picker | Thinking-level (reasoning effort) picker in the chat composer | Claude Code Desktop effort menu (⌘⇧E) + OpenWork's own model picker | frontend-only | merged | [#44](https://github.com/modelstudioai/openwork/issues/44) | [#45](https://github.com/modelstudioai/openwork/pull/45) | loop/thinking-level-picker | 2026-07-05 | **Merged into `main`.** `thinkingLevel`/`onThinkingLevelChange` already plumbed to `FreeFormInput`; only the UI trigger was missing. Reuses `thinking.*` + `settings.ai.thinking` i18n keys (zero new keys). | | command-palette | Global command palette (⌘K/Ctrl+K) to search & run any action | Claude Code Desktop ⌘K / VS Code & Codex ⌘⇧P / Linear ⌘K | frontend-only | merged | [#41](https://github.com/modelstudioai/openwork/issues/41) | [#42](https://github.com/modelstudioai/openwork/pull/42) | loop/command-palette | 2026-07-02 | Merged into `main`. Reuses action registry `execute()` + cmdk primitives; zero new i18n keys. CDP e2e 2/2 pass. typecheck/test +0 vs main. | | settings-search | Searchable/filterable settings navigation | Claude Code Desktop / VS Code / Codex desktop settings search | frontend-only | merged | [#39](https://github.com/modelstudioai/openwork/issues/39) | [#40](https://github.com/modelstudioai/openwork/pull/40) | loop/settings-search | 2026-07-01 | Merged into `main`. Filters `SettingsNavigator` by title+description; reuses `common.search`/`common.noResultsFound` (no new locale keys). Also hardened `e2e/app.ts` teardown (per-launch profile dir + setsid process-group kill) so multiple CDP assertions run under headless xvfb. | diff --git a/e2e/assertions/command-palette-recents.assert.ts b/e2e/assertions/command-palette-recents.assert.ts new file mode 100644 index 000000000..dae0fe32b --- /dev/null +++ b/e2e/assertions/command-palette-recents.assert.ts @@ -0,0 +1,161 @@ +/** + * Feature assertion: the command palette's "Recently used" group. + * + * Drives the real built app over CDP in the draft (no-session) state — no + * seeded conversation and no backend. It proves the full recents lifecycle: + * seed one recent → it renders in a dedicated "Recently used" group → + * a search query hides the group → clearing restores it → running a *new* + * action from the palette records it and, on reopen, it is the top recent. + * + * The reopen step is the load-bearing one: reading the palette after running + * an action proves the palette actually *persists* what you run and surfaces + * it newest-first — not merely that a seeded list can render. + */ + +import type { Assertion } from '../runner'; + +const PALETTE = '[data-testid="command-palette"]'; +const INPUT = '[data-testid="command-palette-input"]'; +const ITEM = '[data-testid="command-palette-item"]'; +const RECENT = '[data-testid="command-palette-recent-item"]'; + +/** localStorage key the palette reads recents from (shared helper's `craft-` prefix). */ +const RECENTS_KEY = 'craft-command-palette-recents'; + +/** Elements actually visible (cmdk keeps filtered rows in the DOM but hidden). */ +function visibleExpr(selector: string): string { + return `[...document.querySelectorAll(${JSON.stringify(selector)})].filter(el => el.offsetParent !== null)`; +} + +/** Visible text of each matching row, in DOM order. */ +function textsExpr(selector: string): string { + return `${visibleExpr(selector)}.map(el => (el.textContent || '').trim())`; +} + +/** Set a controlled input's value the way React/cmdk expect (native setter + input event). */ +function setInputExpr(value: string): string { + return `(() => { + const input = document.querySelector(${JSON.stringify(INPUT)}); + if (!input) return false; + const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; + setter.call(input, ${JSON.stringify(value)}); + input.dispatchEvent(new Event('input', { bubbles: true })); + return true; + })()`; +} + +/** Dispatch the Cmd/Ctrl+K palette hotkey at the window level (both modifiers for cross-platform). */ +const OPEN_PALETTE_EXPR = `(() => { + window.dispatchEvent(new KeyboardEvent('keydown', { + key: 'k', code: 'KeyK', ctrlKey: true, metaKey: true, bubbles: true, cancelable: true, + })); + return true; +})()`; + +const assertion: Assertion = { + name: 'command palette surfaces and records recently-used actions', + async run(app) { + const { session } = app; + + // App must be mounted and at the ready AppShell state. + await session.waitForFunction( + '!document.getElementById("_loader") && (document.getElementById("root")?.childElementCount ?? 0) > 0', + { timeoutMs: 30000, message: 'React UI did not mount' }, + ); + await session.waitForSelector('[aria-label="Craft menu"]', { + timeoutMs: 30000, + message: 'app did not reach the ready AppShell state', + }); + + // 1. Seed a single recent action. The palette reads recents from + // localStorage each time it opens, so no reload is needed. + const seeded = await session.evaluate(`(() => { + localStorage.setItem(${JSON.stringify(RECENTS_KEY)}, JSON.stringify(['app.toggleTheme'])); + return true; + })()`); + if (!seeded) throw new Error('failed to seed command-palette recents'); + + // 2. Open the palette; the "Recently used" group shows exactly the seeded action. + if (!(await session.evaluate(OPEN_PALETTE_EXPR))) { + throw new Error('failed to dispatch command-palette hotkey'); + } + await session.waitForSelector(PALETTE, { + timeoutMs: 8000, + message: 'command palette did not open on Ctrl/Cmd+K', + }); + await session.waitForFunction(`${visibleExpr(RECENT)}.length === 1`, { + timeoutMs: 5000, + message: 'seeded "Recently used" group did not render exactly one row', + }); + const firstRecent = await session.evaluate(textsExpr(RECENT)); + if (!/toggle theme/i.test(firstRecent[0] || '')) { + throw new Error(`expected seeded recent to be "Toggle Theme", got ${JSON.stringify(firstRecent)}`); + } + + // 2b. The recent row renders ABOVE the first category row. + const recentIsFirst = await session.evaluate(`(() => { + const recent = ${visibleExpr(RECENT)}[0]; + const item = ${visibleExpr(ITEM)}[0]; + if (!recent || !item) return false; + return !!(recent.compareDocumentPosition(item) & Node.DOCUMENT_POSITION_FOLLOWING); + })()`); + if (!recentIsFirst) { + throw new Error('"Recently used" group did not render above the first category'); + } + + // 3. Typing a query hides the recents group (recency yields to relevance), + // while normal filtering still works (a matching category row survives). + await session.evaluate(setInputExpr('sidebar')); + await session.waitForFunction(`${visibleExpr(RECENT)}.length === 0`, { + timeoutMs: 5000, + message: 'recents group did not hide while searching', + }); + await session.waitForFunction( + `${visibleExpr(ITEM)}.some(el => /toggle sidebar/i.test(el.textContent || ''))`, + { timeoutMs: 5000, message: 'expected a "Toggle Sidebar" match while filtering by "sidebar"' }, + ); + + // 4. Clearing the query brings the recents group back. + await session.evaluate(setInputExpr('')); + await session.waitForFunction(`${visibleExpr(RECENT)}.length === 1`, { + timeoutMs: 5000, + message: 'recents group did not reappear after clearing the query', + }); + + // 5. Run a *new* (not-yet-recent) action from the palette: Toggle Sidebar. + const clicked = await session.evaluate(`(() => { + const el = ${visibleExpr(ITEM)}.find(el => /toggle sidebar/i.test(el.textContent || '')); + if (!el) return false; + el.click(); + return true; + })()`); + if (!clicked) throw new Error('could not click the "Toggle Sidebar" row'); + await session.waitForFunction(`!document.querySelector(${JSON.stringify(PALETTE)})`, { + timeoutMs: 5000, + message: 'palette did not close after running an action', + }); + + // 6. Reopen: the just-run action is now the top recent, ahead of the seed — + // proving the palette recorded it and orders recents newest-first. + if (!(await session.evaluate(OPEN_PALETTE_EXPR))) { + throw new Error('failed to reopen the command palette'); + } + await session.waitForSelector(PALETTE, { + timeoutMs: 8000, + message: 'command palette did not reopen', + }); + await session.waitForFunction(`${visibleExpr(RECENT)}.length === 2`, { + timeoutMs: 5000, + message: 'recents group did not grow to two rows after running a new action', + }); + const recents = await session.evaluate(textsExpr(RECENT)); + if (!/toggle sidebar/i.test(recents[0] || '')) { + throw new Error(`expected "Toggle Sidebar" as the top recent, got ${JSON.stringify(recents)}`); + } + if (!/toggle theme/i.test(recents[1] || '')) { + throw new Error(`expected "Toggle Theme" as the second recent, got ${JSON.stringify(recents)}`); + } + }, +}; + +export default assertion; diff --git a/packages/shared/src/i18n/locales/de.json b/packages/shared/src/i18n/locales/de.json index d407e5dbb..54e5db900 100644 --- a/packages/shared/src/i18n/locales/de.json +++ b/packages/shared/src/i18n/locales/de.json @@ -252,6 +252,7 @@ "chatInput.placeholder.typeMessage": "Nachricht eingeben...", "chatInput.placeholder.workOn": "Woran möchten Sie arbeiten?", "commands.searchCommands": "Befehle suchen...", + "commands.recent": "Zuletzt verwendet", "commands.title": "Befehle", "common.back": "Zurück", "common.backToList": "Zurück zur Liste", diff --git a/packages/shared/src/i18n/locales/en.json b/packages/shared/src/i18n/locales/en.json index 6a06e456e..44e9c48d2 100644 --- a/packages/shared/src/i18n/locales/en.json +++ b/packages/shared/src/i18n/locales/en.json @@ -252,6 +252,7 @@ "chatInput.placeholder.typeMessage": "Type a message...", "chatInput.placeholder.workOn": "What would you like to work on?", "commands.searchCommands": "Search commands...", + "commands.recent": "Recently used", "commands.title": "Commands", "common.back": "Back", "common.backToList": "Back to list", diff --git a/packages/shared/src/i18n/locales/es.json b/packages/shared/src/i18n/locales/es.json index 6461320a6..c1da2f911 100644 --- a/packages/shared/src/i18n/locales/es.json +++ b/packages/shared/src/i18n/locales/es.json @@ -252,6 +252,7 @@ "chatInput.placeholder.typeMessage": "Escribe un mensaje...", "chatInput.placeholder.workOn": "¿En qué te gustaría trabajar?", "commands.searchCommands": "Buscar comandos...", + "commands.recent": "Usados recientemente", "commands.title": "Comandos", "common.back": "Atrás", "common.backToList": "Volver a la lista", diff --git a/packages/shared/src/i18n/locales/hu.json b/packages/shared/src/i18n/locales/hu.json index 641c6d16f..9b18a8149 100644 --- a/packages/shared/src/i18n/locales/hu.json +++ b/packages/shared/src/i18n/locales/hu.json @@ -252,6 +252,7 @@ "chatInput.placeholder.typeMessage": "Írj egy üzenetet...", "chatInput.placeholder.workOn": "Min szeretnél dolgozni?", "commands.searchCommands": "Parancsok keresése...", + "commands.recent": "Legutóbb használt", "commands.title": "Parancsok", "common.back": "Vissza", "common.backToList": "Vissza a listához", diff --git a/packages/shared/src/i18n/locales/ja.json b/packages/shared/src/i18n/locales/ja.json index 5e2191990..da7c14111 100644 --- a/packages/shared/src/i18n/locales/ja.json +++ b/packages/shared/src/i18n/locales/ja.json @@ -252,6 +252,7 @@ "chatInput.placeholder.typeMessage": "メッセージを入力...", "chatInput.placeholder.workOn": "何に取り組みますか?", "commands.searchCommands": "コマンドを検索...", + "commands.recent": "最近使用した項目", "commands.title": "コマンド", "common.back": "戻る", "common.backToList": "リストに戻る", diff --git a/packages/shared/src/i18n/locales/pl.json b/packages/shared/src/i18n/locales/pl.json index f85aa9c9f..987728b86 100644 --- a/packages/shared/src/i18n/locales/pl.json +++ b/packages/shared/src/i18n/locales/pl.json @@ -252,6 +252,7 @@ "chatInput.placeholder.typeMessage": "Napisz wiadomość...", "chatInput.placeholder.workOn": "Nad czym chcesz pracować?", "commands.searchCommands": "Szukaj poleceń...", + "commands.recent": "Ostatnio używane", "commands.title": "Polecenia", "common.back": "Wstecz", "common.backToList": "Wróć do listy", diff --git a/packages/shared/src/i18n/locales/zh-Hans.json b/packages/shared/src/i18n/locales/zh-Hans.json index 845231913..b21ac0a6b 100644 --- a/packages/shared/src/i18n/locales/zh-Hans.json +++ b/packages/shared/src/i18n/locales/zh-Hans.json @@ -252,6 +252,7 @@ "chatInput.placeholder.typeMessage": "输入消息...", "chatInput.placeholder.workOn": "想做什么?", "commands.searchCommands": "搜索命令...", + "commands.recent": "最近使用", "commands.title": "命令", "common.back": "返回", "common.backToList": "返回列表",