diff --git a/mobile/src/session/QuickCommandsList.test.ts b/mobile/src/session/QuickCommandsList.test.ts index aae0bf7bbe3..47aa3b654a4 100644 --- a/mobile/src/session/QuickCommandsList.test.ts +++ b/mobile/src/session/QuickCommandsList.test.ts @@ -57,7 +57,6 @@ describe('QuickCommandsList search', () => { query: 'missing', loading: false, disabled: false, - canAdd: true, error: null, onQueryChange: vi.fn(), onLaunch: vi.fn(), @@ -83,7 +82,6 @@ describe('QuickCommandsList search', () => { query: '', loading: false, disabled: false, - canAdd: true, error: null, onQueryChange: vi.fn(), onLaunch: vi.fn(), diff --git a/mobile/src/session/QuickCommandsList.tsx b/mobile/src/session/QuickCommandsList.tsx index 2dae27e3a06..c0e3ac39b9e 100644 --- a/mobile/src/session/QuickCommandsList.tsx +++ b/mobile/src/session/QuickCommandsList.tsx @@ -22,7 +22,6 @@ type ListProps = { query: string loading: boolean disabled: boolean - canAdd: boolean error: string | null onQueryChange: (value: string) => void onLaunch: (command: TerminalQuickCommand) => void @@ -38,7 +37,6 @@ export function QuickCommandsList({ query, loading, disabled, - canAdd, error, onQueryChange, onLaunch, @@ -47,7 +45,6 @@ export function QuickCommandsList({ onAdd }: ListProps) { const hasVisible = repoCommands.length + globalCommands.length > 0 - const addDisabled = disabled || !canAdd // Why: keep an active filter clearable if a delete or paired desktop edit // leaves only one command while the sheet is open. const showSearch = totalCount > 1 || query.length > 0 @@ -110,17 +107,15 @@ export function QuickCommandsList({ [ styles.addRow, - addDisabled && styles.disabled, - pressed && !addDisabled && styles.pressed + disabled && styles.disabled, + pressed && !disabled && styles.pressed ]} - disabled={addDisabled} + disabled={disabled} onPress={onAdd} accessibilityRole="button" > - - {canAdd ? 'New quick command' : 'Quick command limit reached'} - + New quick command ) diff --git a/mobile/src/session/QuickCommandsSheet.test.ts b/mobile/src/session/QuickCommandsSheet.test.ts index bc03965365b..b54b2cdd74d 100644 --- a/mobile/src/session/QuickCommandsSheet.test.ts +++ b/mobile/src/session/QuickCommandsSheet.test.ts @@ -3,7 +3,6 @@ import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { TerminalQuickCommand } from '../../../src/shared/terminal-quick-command-types' import type { RpcClient } from '../transport/rpc-client' -import { MAX_QUICK_COMMANDS } from '../terminal/quick-commands' import { QuickCommandsSheet } from './QuickCommandsSheet' import { createStableLogicalRpcClient } from '../transport/stable-logical-rpc-client' import { FakeSession } from '../transport/mobile-endpoint-supervisor-test-fakes' @@ -342,8 +341,8 @@ describe('QuickCommandsSheet', () => { client.close() }) - it('keeps creation closed when the host command limit is reached', async () => { - mocks.commands = Array.from({ length: MAX_QUICK_COMMANDS }, (_, index) => ({ + it('allows saving another command when forty commands already exist', async () => { + mocks.commands = Array.from({ length: 40 }, (_, index) => ({ ...command, id: `command-${index}` })) @@ -362,8 +361,15 @@ describe('QuickCommandsSheet', () => { }) const list = renderer!.root.findByType(quickCommandsList) - expect(list.props.canAdd).toBe(false) act(() => list.props.onAdd()) + const editor = () => renderer!.root.findByType(quickCommandEditorForm) + act(() => editor().props.onChange({ label: 'Another command', command: 'echo saved' })) + mocks.persist.mockResolvedValue(true) + await act(async () => editor().props.onSave()) + expect(mocks.persist).toHaveBeenCalledWith({ + type: 'upsert', + command: expect.objectContaining({ label: 'Another command', command: 'echo saved' }) + }) expect(renderer!.root.findAllByType(quickCommandEditorForm)).toHaveLength(0) }) diff --git a/mobile/src/session/QuickCommandsSheet.tsx b/mobile/src/session/QuickCommandsSheet.tsx index 33bf538711c..9680f4fac09 100644 --- a/mobile/src/session/QuickCommandsSheet.tsx +++ b/mobile/src/session/QuickCommandsSheet.tsx @@ -5,11 +5,7 @@ import { colors, spacing } from '../theme/mobile-theme' import { BottomDrawer } from '../components/BottomDrawer' import type { RpcClient } from '../transport/rpc-client' import type { TerminalQuickCommand } from '../../../src/shared/terminal-quick-command-types' -import { - getQuickCommandPreview, - MAX_QUICK_COMMANDS, - quickCommandMatchesRepo -} from '../terminal/quick-commands' +import { getQuickCommandPreview, quickCommandMatchesRepo } from '../terminal/quick-commands' import { useQuickCommands } from './use-quick-commands' import { QuickCommandEditorForm } from './QuickCommandEditorForm' import { QuickCommandAgentPicker, QuickCommandsList } from './QuickCommandsList' @@ -96,11 +92,6 @@ export function QuickCommandsSheet({ const globalCommands = visibleCommands.filter((command) => command.scope?.type !== 'repo') const openEditor = (command?: TerminalQuickCommand) => { - // Why: the host rejects full-list updates above this cap; existing rows - // must remain editable/deletable when creation is no longer possible. - if (!command && commands.length >= MAX_QUICK_COMMANDS) { - return - } draftRevisionRef.current += 1 editorGenerationRef.current += 1 setDraft( @@ -213,7 +204,6 @@ export function QuickCommandsSheet({ query={query} loading={loading} disabled={!ready} - canAdd={commands.length < MAX_QUICK_COMMANDS} error={error} onQueryChange={setQuery} onLaunch={handleLaunch} diff --git a/mobile/src/terminal/quick-commands.ts b/mobile/src/terminal/quick-commands.ts index 4f48e093a4b..bd73fa9d4bb 100644 --- a/mobile/src/terminal/quick-commands.ts +++ b/mobile/src/terminal/quick-commands.ts @@ -5,7 +5,6 @@ import { flattenTerminalQuickCommand, getTerminalQuickCommandBody, isTerminalAgentQuickCommand, - MAX_QUICK_COMMANDS, MAX_QUICK_COMMAND_LABEL_LENGTH, MAX_QUICK_COMMAND_TERMINAL_TEXT_LENGTH, parseNormalizedTerminalQuickCommands, @@ -31,7 +30,6 @@ export { supportsTerminalAgentQuickCommand, getTerminalQuickCommandBodySize, formatKilobytes, - MAX_QUICK_COMMANDS, MAX_QUICK_COMMAND_LABEL_LENGTH, MAX_QUICK_COMMAND_TERMINAL_TEXT_LENGTH, parseNormalizedTerminalQuickCommands, diff --git a/src/cli/runtime/websocket-transport.test.ts b/src/cli/runtime/websocket-transport.test.ts index bc5181f15c5..aa027240a8b 100644 --- a/src/cli/runtime/websocket-transport.test.ts +++ b/src/cli/runtime/websocket-transport.test.ts @@ -34,7 +34,10 @@ import { WORKTREE_VISIBILITY_DEFAULTS_RUNTIME_CAPABILITY, WORKTREE_VISIBILITY_SOURCE_DEFAULTS_RUNTIME_CAPABILITY } from '../../shared/protocol-version' -import { TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY } from '../../shared/terminal-quick-command-capabilities' +import { + TERMINAL_QUICK_COMMAND_LARGE_LISTS_RUNTIME_CAPABILITY, + TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY +} from '../../shared/terminal-quick-command-capabilities' import { AGENT_SESSION_BACKGROUND_TASK_CHILD_VIEWS_CAPABILITY } from '../../shared/agent-session-background-task-child-views-capability' vi.mock('./launch', () => ({ @@ -91,6 +94,7 @@ describe('CLI remote WebSocket transport', () => { AUTOMATION_OWNER_FENCING_RUNTIME_CAPABILITY, REPO_SEARCH_QUALIFIED_REFS_RUNTIME_CAPABILITY, TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY, + TERMINAL_QUICK_COMMAND_LARGE_LISTS_RUNTIME_CAPABILITY, AGENT_LAUNCH_RUNTIME_CAPABILITY ] }) diff --git a/src/main/ipc/desktop-renderer-runtime-capabilities.ts b/src/main/ipc/desktop-renderer-runtime-capabilities.ts index 16485f10417..1861d0b3834 100644 --- a/src/main/ipc/desktop-renderer-runtime-capabilities.ts +++ b/src/main/ipc/desktop-renderer-runtime-capabilities.ts @@ -8,7 +8,10 @@ import { AGENT_LAUNCH_RUNTIME_CAPABILITY, AGENT_LAUNCH_TAB_CLOSED_CLIENT_CAPABILITY } from '../../shared/agent-launch-runtime-capability' -import { TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY } from '../../shared/terminal-quick-command-capabilities' +import { + TERMINAL_QUICK_COMMAND_LARGE_LISTS_RUNTIME_CAPABILITY, + TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY +} from '../../shared/terminal-quick-command-capabilities' /** Curate one capability set for unary/streaming desktop calls; remote-only gates can alter local semantics even on the same build. */ export const DESKTOP_RENDERER_RUNTIME_CLIENT_CAPABILITIES: readonly RuntimeCapability[] = [ @@ -20,5 +23,6 @@ export const DESKTOP_RENDERER_RUNTIME_CLIENT_CAPABILITIES: readonly RuntimeCapab AGENT_LAUNCH_PROMPT_UNCONFIRMED_RUNTIME_CAPABILITY, // Treat closed launch tabs as silent cancellation instead of unknown failure. AGENT_LAUNCH_TAB_CLOSED_CLIENT_CAPABILITY, - TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY + TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY, + TERMINAL_QUICK_COMMAND_LARGE_LISTS_RUNTIME_CAPABILITY ] as const diff --git a/src/main/persistence/applying-settings/settings-update-terminal-quick-commands.test.ts b/src/main/persistence/applying-settings/settings-update-terminal-quick-commands.test.ts index a0651f4d5af..e7924f0a983 100644 --- a/src/main/persistence/applying-settings/settings-update-terminal-quick-commands.test.ts +++ b/src/main/persistence/applying-settings/settings-update-terminal-quick-commands.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import type { PersistedState } from '../../../shared/persisted-state-types' import type { TerminalQuickCommand } from '../../../shared/terminal-quick-command-types' -import { MAX_QUICK_COMMANDS } from '../../../shared/terminal-quick-commands' import { getTerminalQuickCommandPromptMessageBytes, MAX_QUICK_COMMAND_AGENT_PROMPT_MESSAGE_BYTES, @@ -49,6 +48,19 @@ function agentCommand(id: string, prompt: string): TerminalQuickCommand { // Local saves and paired saves both reach the store here, so this is the one place the bound holds. describe('updateSettings terminalQuickCommands', () => { + it('stores every command in a list larger than forty, including missing projects', () => { + const commands = Array.from({ length: 100 }, (_, index) => ({ + ...agentCommand(`command-${index}`, 'Review this project'), + scope: { type: 'repo' as const, repoId: `missing-project-${index}` } + })) + const operations = makeOperations() + + const saved = updateSettings(operations, { terminalQuickCommands: commands }) + + expect(saved.terminalQuickCommands).toEqual(commands) + expect(operations.state.settings.terminalQuickCommands).toEqual(commands) + }) + it('stores a prompt at the limit whole', () => { const prompt = promptAtLimit('x') const saved = updateSettings(makeOperations(), { @@ -74,7 +86,7 @@ describe('updateSettings terminalQuickCommands', () => { it('refuses a list too large for one paired reply', () => { // Forty prompts at the limit total about 10 MB of JSON. - const commands = Array.from({ length: MAX_QUICK_COMMANDS }, (_, index) => + const commands = Array.from({ length: 40 }, (_, index) => agentCommand(`c${index}`, promptAtLimit('界')) ) diff --git a/src/main/runtime/orca-runtime-test-mocks.spec.ts b/src/main/runtime/orca-runtime-test-mocks.spec.ts index b9bceaa18ea..c5e2945112c 100644 --- a/src/main/runtime/orca-runtime-test-mocks.spec.ts +++ b/src/main/runtime/orca-runtime-test-mocks.spec.ts @@ -22,7 +22,6 @@ export const FOLDER_WORKSPACE_INSTANCE_SEPARATOR = export const HEADLESS_RUNTIME_WINDOW_ID = importedValues.exportedHEADLESS_RUNTIME_WINDOW_ID export const HeadlessEmulator = importedValues.exportedHeadlessEmulator export const MAX_OSC_TITLE_CHARS = importedValues.exportedMAX_OSC_TITLE_CHARS -export const MAX_QUICK_COMMANDS = importedValues.exportedMAX_QUICK_COMMANDS export const OrcaRuntimeService = importedValues.exportedOrcaRuntimeService export const OrchestrationDb = importedValues.exportedOrchestrationDb export const REVIEW_HEAD_FETCH_TIMEOUT_MS = importedValues.exportedREVIEW_HEAD_FETCH_TIMEOUT_MS diff --git a/src/main/runtime/orca-runtime-test-mocks/imported-values.spec.ts b/src/main/runtime/orca-runtime-test-mocks/imported-values.spec.ts index fc170aa6f79..8327e2d395e 100644 --- a/src/main/runtime/orca-runtime-test-mocks/imported-values.spec.ts +++ b/src/main/runtime/orca-runtime-test-mocks/imported-values.spec.ts @@ -81,7 +81,6 @@ import { TERMINAL_INPUT_MAX_BYTES, TERMINAL_INPUT_TOO_LARGE_ERROR } from '../../../shared/terminal-input' -import { MAX_QUICK_COMMANDS } from '../../../shared/terminal-quick-commands' import { AGENT_PROMPT_BRACKETED_PASTE_END, AGENT_PROMPT_BRACKETED_PASTE_START, @@ -136,7 +135,6 @@ export const exportedFOLDER_WORKSPACE_INSTANCE_SEPARATOR = FOLDER_WORKSPACE_INST export const exportedHEADLESS_RUNTIME_WINDOW_ID = HEADLESS_RUNTIME_WINDOW_ID export const exportedHeadlessEmulator = HeadlessEmulator export const exportedMAX_OSC_TITLE_CHARS = MAX_OSC_TITLE_CHARS -export const exportedMAX_QUICK_COMMANDS = MAX_QUICK_COMMANDS export const exportedOrcaRuntimeService = OrcaRuntimeServiceConstructor export const exportedOrchestrationDb = RuntimeOrchestrationDb export const exportedREVIEW_HEAD_FETCH_TIMEOUT_MS = REVIEW_HEAD_FETCH_TIMEOUT_MS diff --git a/src/main/runtime/orca-runtime-tests/paired-settings.spec.ts b/src/main/runtime/orca-runtime-tests/paired-settings.spec.ts index 9f9909c7ec4..2505d72457a 100644 --- a/src/main/runtime/orca-runtime-tests/paired-settings.spec.ts +++ b/src/main/runtime/orca-runtime-tests/paired-settings.spec.ts @@ -1,6 +1,5 @@ import { describe, expect, it, vi } from 'vitest' import { - MAX_QUICK_COMMANDS, OrcaRuntimeService, applyAgentStatusHooksEnabledMock, electronMocks @@ -172,8 +171,8 @@ describe('OrcaRuntimeService', () => { expect(settings.nativeChatSessionOptions).toEqual({ grok: {} }) }) - it('rejects a concurrent add after the quick command limit is reached', () => { - const terminalQuickCommands = Array.from({ length: MAX_QUICK_COMMANDS }, (_, index) => ({ + it('preserves concurrent additions after forty quick commands', () => { + const terminalQuickCommands = Array.from({ length: 40 }, (_, index) => ({ id: `command-${index}`, label: `Command ${index}`, action: 'terminal-command' as const, @@ -181,27 +180,25 @@ describe('OrcaRuntimeService', () => { appendEnter: true, scope: { type: 'global' as const } })) - const updateSettings = vi.fn() + let settings = { ...store.getSettings(), terminalQuickCommands } + const updateSettings = vi.fn((updates: Partial) => { + settings = { ...settings, ...updates } + }) const runtime = new OrcaRuntimeService({ ...store, - getSettings: () => ({ ...store.getSettings(), terminalQuickCommands }), + getSettings: () => settings, updateSettings } as never) - expect(() => - runtime.updateClientTerminalQuickCommands({ - type: 'upsert', - command: { - id: 'one-too-many', - label: 'One too many', - action: 'terminal-command', - command: 'true', - appendEnter: true, - scope: { type: 'global' } - } - }) - ).toThrow('Quick command limit reached') - expect(updateSettings).not.toHaveBeenCalled() + const first = { ...terminalQuickCommands[0], id: 'desktop-addition' } + const second = { ...terminalQuickCommands[0], id: 'mobile-addition' } + runtime.updateClientTerminalQuickCommands({ type: 'upsert', command: first }) + expect(runtime.updateClientTerminalQuickCommands({ type: 'upsert', command: second })).toEqual([ + ...terminalQuickCommands, + first, + second + ]) + expect(settings.terminalQuickCommands).toEqual([...terminalQuickCommands, first, second]) }) it('accepts runtime-backed setting updates from paired clients', async () => { diff --git a/src/main/runtime/rpc/methods/client-ui.ts b/src/main/runtime/rpc/methods/client-ui.ts index 8b8026d9f98..8792a3f6e2b 100644 --- a/src/main/runtime/rpc/methods/client-ui.ts +++ b/src/main/runtime/rpc/methods/client-ui.ts @@ -26,7 +26,10 @@ import { import { TerminalQuickCommandsUpdate } from '../../../../shared/rpc-contract/terminal-quick-command-params' import { AgentLaunchSettingsMutation } from '../../../../shared/rpc-contract/agent-launch-settings-params' -import { projectTerminalQuickCommandsForClient } from './terminal-quick-command-long-prompt-capability' +import { + assertTerminalQuickCommandMutationReadableForClient, + projectTerminalQuickCommandsForClient +} from './terminal-quick-command-client-capabilities' function uiForCaller( state: PersistedUIState, @@ -120,12 +123,19 @@ export const CLIENT_UI_METHODS = [ name: 'settings.updateTerminalQuickCommands', permission: 'settings-write', params: TerminalQuickCommandsUpdate, - handler: (params, ctx) => ({ - terminalQuickCommands: projectTerminalQuickCommandsForClient( - ctx.runtime.updateClientTerminalQuickCommands(params.mutation), + handler: (params, ctx) => { + assertTerminalQuickCommandMutationReadableForClient( + ctx.runtime.getClientTerminalQuickCommands(), + params.mutation, ctx ) - }) + return { + terminalQuickCommands: projectTerminalQuickCommandsForClient( + ctx.runtime.updateClientTerminalQuickCommands(params.mutation), + ctx + ) + } + } }), defineMethod({ name: 'settings.updatePRBotAuthorOverride', diff --git a/src/main/runtime/rpc/methods/terminal-quick-command-client-capabilities.test.ts b/src/main/runtime/rpc/methods/terminal-quick-command-client-capabilities.test.ts new file mode 100644 index 00000000000..141a20468bb --- /dev/null +++ b/src/main/runtime/rpc/methods/terminal-quick-command-client-capabilities.test.ts @@ -0,0 +1,364 @@ +import { describe, expect, it, vi } from 'vitest' +import { getDefaultPersistedState } from '../../../../shared/constants' +import { ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIES } from '../../../../shared/electron-remote-runtime-client-capabilities' +import { remoteRuntimeClientCapabilities } from '../../../../shared/remote-runtime-client-capabilities' +import { LEGACY_MAX_QUICK_COMMAND_AGENT_PROMPT_LENGTH } from '../../../../shared/terminal-quick-command-prompt-limit' +import type { TerminalQuickCommand } from '../../../../shared/terminal-quick-command-types' +import type { TerminalQuickCommandMutation } from '../../../../shared/terminal-quick-commands' +import { WEB_RUNTIME_CLIENT_CAPABILITIES } from '../../../../shared/web-runtime-client-capabilities' +import { + TERMINAL_QUICK_COMMAND_LARGE_LISTS_RUNTIME_CAPABILITY, + TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY +} from '../../../../shared/terminal-quick-command-capabilities' +import type { OrcaRuntimeService } from '../../orca-runtime' +import { DESKTOP_RENDERER_RUNTIME_CLIENT_CAPABILITIES } from '../../../ipc/desktop-renderer-runtime-capabilities' +import { RuntimeClientSettingsController } from '../../runtime-client-settings' +import { + updateSettings, + type SettingsMutationOperations +} from '../../../persistence/applying-settings/settings-update' +import type { RpcRequest } from '../core' +import { RpcDispatcher } from '../dispatcher' +import { parseRuntimeClientCapabilities } from '../runtime-client-capabilities' +import { CLIENT_UI_METHODS } from './client-ui' + +function makeRequest(method: string, params?: unknown): RpcRequest { + return { id: 'req-1', authToken: 'tok', method, params } +} + +describe('quick-command client compatibility', () => { + const shortCommand: TerminalQuickCommand = { + id: 'status', + label: 'Status', + action: 'terminal-command', + command: 'git status', + appendEnter: true, + scope: { type: 'global' } + } + const longCommand: TerminalQuickCommand = { + id: 'review', + label: 'Review', + action: 'agent-prompt', + agent: 'claude', + prompt: 'x'.repeat(LEGACY_MAX_QUICK_COMMAND_AGENT_PROMPT_LENGTH + 1), + scope: { type: 'global' } + } + + function makeDispatcher(commands = [shortCommand, longCommand]) { + const state = getDefaultPersistedState('/tmp') + state.settings.terminalQuickCommands = commands + const scheduleSave = vi.fn() + const operations: SettingsMutationOperations = { + state, + bumpLocalWorktreeScanGeneration: vi.fn(), + removeRetainedBlob: vi.fn(), + scheduleSave, + notifySettingsChanged: vi.fn() + } + const settings = new RuntimeClientSettingsController({ + getSettings: () => state.settings, + updateSettings: (updates, options) => updateSettings(operations, updates, options) + }) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: these handlers call only the quick-command methods stubbed here. + const runtime = { + getRuntimeId: () => 'test-runtime', + getClientTerminalQuickCommands: () => settings.getTerminalQuickCommands(), + updateClientTerminalQuickCommands: (mutation: TerminalQuickCommandMutation) => + settings.updateTerminalQuickCommands(mutation) + } as unknown as OrcaRuntimeService + return { + dispatcher: new RpcDispatcher({ runtime, methods: CLIENT_UI_METHODS }), + state, + scheduleSave + } + } + + it('refuses a legacy client addition beyond forty before saving anything', async () => { + const commands = Array.from({ length: 40 }, (_, index) => ({ + ...shortCommand, + id: `command-${index}` + })) + const { dispatcher, state, scheduleSave } = makeDispatcher(commands) + + const response = await dispatcher.dispatch( + makeRequest('settings.updateTerminalQuickCommands', { + mutation: { type: 'upsert', command: shortCommand } + }), + { clientKind: 'runtime', clientCapabilities: [] } + ) + + expect(response).toMatchObject({ + ok: false, + error: { + code: 'invalid_argument', + message: expect.stringContaining('Update this Orca client') + } + }) + expect(state.settings.terminalQuickCommands).toEqual(commands) + expect(scheduleSave).not.toHaveBeenCalled() + }) + + it.each([ + { + name: 'local desktop', + caller: { + clientKind: 'runtime' as const, + clientCapabilities: DESKTOP_RENDERER_RUNTIME_CLIENT_CAPABILITIES + } + }, + { + name: 'paired desktop', + caller: { + clientKind: 'runtime' as const, + clientCapabilities: parseRuntimeClientCapabilities( + remoteRuntimeClientCapabilities(ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIES) + ) + } + }, + { + name: 'web', + caller: { + clientKind: 'runtime' as const, + clientCapabilities: parseRuntimeClientCapabilities(WEB_RUNTIME_CLIENT_CAPABILITIES) + } + }, + { + name: 'CLI', + caller: { + clientKind: 'runtime' as const, + clientCapabilities: parseRuntimeClientCapabilities(remoteRuntimeClientCapabilities()) + } + }, + { name: 'in-process', caller: undefined } + ])('saves and acknowledges command forty-one using $name capabilities', async ({ caller }) => { + const commands = Array.from({ length: 40 }, (_, index) => ({ + ...shortCommand, + id: `command-${index}` + })) + const { dispatcher, state, scheduleSave } = makeDispatcher(commands) + + const response = await dispatcher.dispatch( + makeRequest('settings.updateTerminalQuickCommands', { + mutation: { type: 'upsert', command: longCommand } + }), + caller + ) + + expect(response).toMatchObject({ + ok: true, + result: { terminalQuickCommands: [...commands, longCommand] } + }) + expect(state.settings.terminalQuickCommands).toEqual([...commands, longCommand]) + expect(scheduleSave).toHaveBeenCalledOnce() + }) + + it('lets legacy clients edit and delete visible commands while preserving hidden commands', async () => { + const commands = Array.from({ length: 45 }, (_, index) => ({ + ...shortCommand, + id: `command-${index}` + })) + const { dispatcher, state } = makeDispatcher(commands) + const caller = { clientKind: 'runtime' as const, clientCapabilities: [] } + const edited = { ...commands[0], label: 'Edited command' } + + const editResponse = await dispatcher.dispatch( + makeRequest('settings.updateTerminalQuickCommands', { + mutation: { type: 'upsert', command: edited } + }), + caller + ) + expect(editResponse).toMatchObject({ + ok: true, + result: { terminalQuickCommands: [edited, ...commands.slice(1, 40)] } + }) + + const deleteResponse = await dispatcher.dispatch( + makeRequest('settings.updateTerminalQuickCommands', { + mutation: { type: 'delete', id: commands[1].id } + }), + caller + ) + expect(deleteResponse).toMatchObject({ + ok: true, + result: { terminalQuickCommands: [edited, ...commands.slice(2, 41)] } + }) + expect(state.settings.terminalQuickCommands).toEqual([edited, ...commands.slice(2)]) + }) + + it('does not count withheld prompts against a legacy client readable list', async () => { + const commands = Array.from({ length: 39 }, (_, index) => ({ + ...shortCommand, + id: `command-${index}` + })) + const { dispatcher, state } = makeDispatcher([longCommand, ...commands]) + + const response = await dispatcher.dispatch( + makeRequest('settings.updateTerminalQuickCommands', { + mutation: { type: 'upsert', command: shortCommand } + }), + { clientKind: 'runtime', clientCapabilities: [] } + ) + + expect(response).toMatchObject({ + ok: true, + result: { terminalQuickCommands: [...commands, shortCommand] } + }) + expect(state.settings.terminalQuickCommands).toEqual([longCommand, ...commands, shortCommand]) + }) + + it('refuses an edit that would be withheld from its caller before changing the saved command', async () => { + const { dispatcher, state, scheduleSave } = makeDispatcher([shortCommand]) + + const response = await dispatcher.dispatch( + makeRequest('settings.updateTerminalQuickCommands', { + mutation: { type: 'upsert', command: { ...longCommand, id: shortCommand.id } } + }), + { + clientKind: 'runtime', + clientCapabilities: [TERMINAL_QUICK_COMMAND_LARGE_LISTS_RUNTIME_CAPABILITY] + } + ) + + expect(response).toMatchObject({ + ok: false, + error: { + code: 'invalid_argument', + message: expect.stringContaining('Update this Orca client') + } + }) + expect(state.settings.terminalQuickCommands).toEqual([shortCommand]) + expect(scheduleSave).not.toHaveBeenCalled() + }) + + it.each(['runtime', 'mobile'] as const)( + 'refuses a stale %s client edit of a hidden prompt before saving anything', + async (clientKind) => { + const { dispatcher, state, scheduleSave } = makeDispatcher() + + const response = await dispatcher.dispatch( + makeRequest('settings.updateTerminalQuickCommands', { + mutation: { type: 'upsert', command: { ...longCommand, prompt: 'Stale short prompt' } } + }), + { + clientKind, + clientCapabilities: [TERMINAL_QUICK_COMMAND_LARGE_LISTS_RUNTIME_CAPABILITY] + } + ) + + expect(response).toMatchObject({ + ok: false, + error: { + code: 'invalid_argument', + message: expect.stringContaining('Update this Orca client') + } + }) + expect(state.settings.terminalQuickCommands).toEqual([shortCommand, longCommand]) + expect(scheduleSave).not.toHaveBeenCalled() + } + ) + + it('stores a long prompt instead of refusing it', async () => { + const { dispatcher, state } = makeDispatcher([shortCommand]) + const response = await dispatcher.dispatch( + makeRequest('settings.updateTerminalQuickCommands', { + mutation: { type: 'upsert', command: longCommand } + }) + ) + + expect(response).toMatchObject({ + ok: true, + result: { terminalQuickCommands: [shortCommand, longCommand] } + }) + expect(state.settings.terminalQuickCommands).toEqual([shortCommand, longCommand]) + }) + + it('withholds long prompts from a client that cannot read them', async () => { + const { dispatcher } = makeDispatcher() + const olderClient = { clientKind: 'mobile' as const, clientCapabilities: [] } + + for (const request of [ + makeRequest('settings.getTerminalQuickCommands'), + makeRequest('settings.updateTerminalQuickCommands', { + mutation: { type: 'delete', id: 'other' } + }) + ]) { + const response = await dispatcher.dispatch(request, olderClient) + expect(response).toMatchObject({ + ok: true, + result: { terminalQuickCommands: [shortCommand] } + }) + } + }) + + it('publishes long prompts to clients that advertise support and to in-process callers', async () => { + const { dispatcher } = makeDispatcher() + for (const options of [ + { + clientKind: 'mobile' as const, + clientCapabilities: [TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY] + }, + undefined + ]) { + const response = await dispatcher.dispatch( + makeRequest('settings.getTerminalQuickCommands'), + options + ) + expect(response).toMatchObject({ + ok: true, + result: { terminalQuickCommands: [shortCommand, longCommand] } + }) + } + }) + + it.each([false, true])( + 'projects large lists for older and current clients (support: %s)', + async (supported) => { + const commands = Array.from({ length: 100 }, (_, index) => ({ + ...shortCommand, + id: `command-${index}` + })) + const { dispatcher } = makeDispatcher([longCommand, ...commands]) + const olderClient = { + clientKind: 'mobile' as const, + clientCapabilities: supported ? [TERMINAL_QUICK_COMMAND_LARGE_LISTS_RUNTIME_CAPABILITY] : [] + } + + for (const request of [ + makeRequest('settings.getTerminalQuickCommands'), + makeRequest('settings.updateTerminalQuickCommands', { + mutation: { type: 'delete', id: 'other' } + }) + ]) { + const response = await dispatcher.dispatch(request, olderClient) + expect(response).toMatchObject({ + ok: true, + result: { terminalQuickCommands: supported ? commands : commands.slice(0, 40) } + }) + } + } + ) + + it('publishes every command and prompt to current clients and in-process callers', async () => { + const commands = Array.from({ length: 100 }, (_, index) => ({ + ...longCommand, + id: `command-${index}` + })) + const { dispatcher } = makeDispatcher(commands) + for (const options of [ + { + clientKind: 'runtime' as const, + clientCapabilities: [ + TERMINAL_QUICK_COMMAND_LARGE_LISTS_RUNTIME_CAPABILITY, + TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY + ] + }, + undefined + ]) { + const response = await dispatcher.dispatch( + makeRequest('settings.getTerminalQuickCommands'), + options + ) + expect(response).toMatchObject({ ok: true, result: { terminalQuickCommands: commands } }) + } + }) +}) diff --git a/src/main/runtime/rpc/methods/terminal-quick-command-client-capabilities.ts b/src/main/runtime/rpc/methods/terminal-quick-command-client-capabilities.ts new file mode 100644 index 00000000000..903304f6b0f --- /dev/null +++ b/src/main/runtime/rpc/methods/terminal-quick-command-client-capabilities.ts @@ -0,0 +1,56 @@ +import type { TerminalQuickCommand } from '../../../../shared/terminal-quick-command-types' +import { + applyTerminalQuickCommandMutation, + type TerminalQuickCommandMutation +} from '../../../../shared/terminal-quick-commands' +import { isLegacyReadableTerminalQuickCommand } from '../../../../shared/terminal-quick-command-prompt-limit' +import { + LEGACY_MAX_QUICK_COMMANDS, + TERMINAL_QUICK_COMMAND_LARGE_LISTS_RUNTIME_CAPABILITY, + TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY +} from '../../../../shared/terminal-quick-command-capabilities' +import { InvalidArgumentError, type RpcContext } from '../core' + +type QuickCommandReader = Pick + +export function projectTerminalQuickCommandsForClient( + commands: TerminalQuickCommand[], + ctx: QuickCommandReader +): TerminalQuickCommand[] { + if (ctx.clientKind === undefined) { + return commands + } + const readable = ctx.clientCapabilities?.includes( + TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY + ) + ? commands + : commands.filter(isLegacyReadableTerminalQuickCommand) + return ctx.clientCapabilities?.includes(TERMINAL_QUICK_COMMAND_LARGE_LISTS_RUNTIME_CAPABILITY) + ? readable + : readable.slice(0, LEGACY_MAX_QUICK_COMMANDS) +} + +export function assertTerminalQuickCommandMutationReadableForClient( + commands: TerminalQuickCommand[], + mutation: TerminalQuickCommandMutation, + ctx: QuickCommandReader +): void { + if (mutation.type === 'delete') { + return + } + const existing = commands.find((command) => command.id === mutation.command.id) + const next = applyTerminalQuickCommandMutation(commands, mutation) + if ( + (existing && + !projectTerminalQuickCommandsForClient(commands, ctx).some( + (command) => command.id === existing.id + )) || + !projectTerminalQuickCommandsForClient(next, ctx).some( + (command) => command.id === mutation.command.id + ) + ) { + throw new InvalidArgumentError( + 'Update this Orca client to save this quick command. This version cannot display it in the saved list.' + ) + } +} diff --git a/src/main/runtime/rpc/methods/terminal-quick-command-long-prompt-capability.test.ts b/src/main/runtime/rpc/methods/terminal-quick-command-long-prompt-capability.test.ts deleted file mode 100644 index a56de07d420..00000000000 --- a/src/main/runtime/rpc/methods/terminal-quick-command-long-prompt-capability.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { LEGACY_MAX_QUICK_COMMAND_AGENT_PROMPT_LENGTH } from '../../../../shared/terminal-quick-command-prompt-limit' -import type { TerminalQuickCommand } from '../../../../shared/terminal-quick-command-types' -import { TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY } from '../../../../shared/terminal-quick-command-capabilities' -import type { OrcaRuntimeService } from '../../orca-runtime' -import type { RpcRequest } from '../core' -import { RpcDispatcher } from '../dispatcher' -import { CLIENT_UI_METHODS } from './client-ui' - -function makeRequest(method: string, params?: unknown): RpcRequest { - return { id: 'req-1', authToken: 'tok', method, params } -} - -describe('quick-command prompts longer than older clients accept', () => { - const shortCommand: TerminalQuickCommand = { - id: 'status', - label: 'Status', - action: 'terminal-command', - command: 'git status', - appendEnter: true, - scope: { type: 'global' } - } - const longCommand: TerminalQuickCommand = { - id: 'review', - label: 'Review', - action: 'agent-prompt', - agent: 'claude', - prompt: 'x'.repeat(LEGACY_MAX_QUICK_COMMAND_AGENT_PROMPT_LENGTH + 1), - scope: { type: 'global' } - } - - function makeDispatcher(): { - dispatcher: RpcDispatcher - update: ReturnType - } { - const update = vi.fn(() => [shortCommand, longCommand]) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: these handlers call only the quick-command methods stubbed here. - const runtime = { - getRuntimeId: () => 'test-runtime', - getClientTerminalQuickCommands: vi.fn(() => [shortCommand, longCommand]), - updateClientTerminalQuickCommands: update - } as unknown as OrcaRuntimeService - return { dispatcher: new RpcDispatcher({ runtime, methods: CLIENT_UI_METHODS }), update } - } - - it('stores a long prompt instead of refusing it', async () => { - const { dispatcher, update } = makeDispatcher() - const response = await dispatcher.dispatch( - makeRequest('settings.updateTerminalQuickCommands', { - mutation: { type: 'upsert', command: longCommand } - }) - ) - - expect(response).toMatchObject({ ok: true }) - expect(update).toHaveBeenCalledWith({ type: 'upsert', command: longCommand }) - }) - - it('withholds long prompts from a client that cannot read them', async () => { - const { dispatcher } = makeDispatcher() - const olderClient = { clientKind: 'mobile' as const, clientCapabilities: [] } - - for (const request of [ - makeRequest('settings.getTerminalQuickCommands'), - makeRequest('settings.updateTerminalQuickCommands', { - mutation: { type: 'delete', id: 'other' } - }) - ]) { - const response = await dispatcher.dispatch(request, olderClient) - expect(response).toMatchObject({ - ok: true, - result: { terminalQuickCommands: [shortCommand] } - }) - } - }) - - it('publishes long prompts to clients that advertise support and to in-process callers', async () => { - const { dispatcher } = makeDispatcher() - for (const options of [ - { - clientKind: 'mobile' as const, - clientCapabilities: [TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY] - }, - undefined - ]) { - const response = await dispatcher.dispatch( - makeRequest('settings.getTerminalQuickCommands'), - options - ) - expect(response).toMatchObject({ - ok: true, - result: { terminalQuickCommands: [shortCommand, longCommand] } - }) - } - }) -}) diff --git a/src/main/runtime/rpc/methods/terminal-quick-command-long-prompt-capability.ts b/src/main/runtime/rpc/methods/terminal-quick-command-long-prompt-capability.ts deleted file mode 100644 index 12fd07bab78..00000000000 --- a/src/main/runtime/rpc/methods/terminal-quick-command-long-prompt-capability.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Transitional: remove once no supported release lacks TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY. -// -// A client from before long prompts rejects the whole quick-command list if one prompt is longer -// than its old cap, so the host leaves those commands out of what it publishes to that client. -// Omitting beats trimming: a trimmed copy would run the wrong prompt, and saving it would -// overwrite the stored one. - -import type { TerminalQuickCommand } from '../../../../shared/terminal-quick-command-types' -import { isLegacyReadableTerminalQuickCommand } from '../../../../shared/terminal-quick-command-prompt-limit' -import { TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY } from '../../../../shared/terminal-quick-command-capabilities' -import type { RpcContext } from '../core' - -type QuickCommandReader = Pick - -function readsLongPrompts(ctx: QuickCommandReader): boolean { - // An in-process caller is this build; only a negotiated client can predate long prompts. - return ( - ctx.clientKind === undefined || - ctx.clientCapabilities?.includes(TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY) === - true - ) -} - -export function projectTerminalQuickCommandsForClient( - commands: TerminalQuickCommand[], - ctx: QuickCommandReader -): TerminalQuickCommand[] { - if (readsLongPrompts(ctx) || commands.every(isLegacyReadableTerminalQuickCommand)) { - return commands - } - return commands.filter(isLegacyReadableTerminalQuickCommand) -} diff --git a/src/main/runtime/runtime-client-settings.ts b/src/main/runtime/runtime-client-settings.ts index efc0b8eb302..812fcb50dc4 100644 --- a/src/main/runtime/runtime-client-settings.ts +++ b/src/main/runtime/runtime-client-settings.ts @@ -5,7 +5,6 @@ import { applyPRBotAuthorOverride } from '../../shared/pr-bot-author-overrides' import { TASK_PROVIDERS } from '../../shared/task-providers' import { applyTerminalQuickCommandMutation, - MAX_QUICK_COMMANDS, type TerminalQuickCommandMutation } from '../../shared/terminal-quick-commands' import { haveSameDisabledTuiAgents } from '../../shared/tui-agent-selection' @@ -213,13 +212,6 @@ export class RuntimeClientSettingsController { throw new Error('runtime_unavailable') } const current = this.getTerminalQuickCommands() - if ( - mutation.type === 'upsert' && - !current.some((command) => command.id === mutation.command.id) && - current.length >= MAX_QUICK_COMMANDS - ) { - throw new Error('Quick command limit reached') - } const next = applyTerminalQuickCommandMutation(current, mutation) this.store.updateSettings({ terminalQuickCommands: next }, { notifyListeners: true }) return this.getTerminalQuickCommands() diff --git a/src/renderer/src/store/slices/terminal-quick-command-hosts.test.ts b/src/renderer/src/store/slices/terminal-quick-command-hosts.test.ts index 2ebac13a3ad..0d5ddaa3f89 100644 --- a/src/renderer/src/store/slices/terminal-quick-command-hosts.test.ts +++ b/src/renderer/src/store/slices/terminal-quick-command-hosts.test.ts @@ -178,4 +178,20 @@ describe('terminal quick command host collections', () => { expect(set).toHaveBeenCalledWith({ terminalQuickCommands: [savedCommand] }) expect(store.getState().settings?.terminalQuickCommands).toEqual([savedCommand]) }) + + it('saves a new local command when forty commands already exist', async () => { + const commands = Array.from({ length: 40 }, (_, index) => ({ + ...savedCommand, + id: `command-${index}`, + scope: { type: 'repo' as const, repoId: `missing-project-${index}` } + })) + const set = vi.fn(async (updates: { terminalQuickCommands: TerminalQuickCommand[] }) => updates) + vi.stubGlobal('window', { api: { settings: { set } } }) + const store = createTestStore() + store.setState({ settings: { ...getDefaultSettings('/tmp'), terminalQuickCommands: commands } }) + + expect(await store.getState().upsertTerminalQuickCommand('local', savedCommand)).toBe(true) + expect(set).toHaveBeenCalledWith({ terminalQuickCommands: [...commands, savedCommand] }) + expect(store.getState().settings?.terminalQuickCommands).toEqual([...commands, savedCommand]) + }) }) diff --git a/src/shared/remote-runtime-client-capabilities.ts b/src/shared/remote-runtime-client-capabilities.ts index f7dbfaace91..5e67cc7f6d6 100644 --- a/src/shared/remote-runtime-client-capabilities.ts +++ b/src/shared/remote-runtime-client-capabilities.ts @@ -12,7 +12,10 @@ import { WORKTREE_VISIBILITY_SOURCE_DEFAULTS_RUNTIME_CAPABILITY, type RuntimeCapability } from './protocol-version' -import { TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY } from './terminal-quick-command-capabilities' +import { + TERMINAL_QUICK_COMMAND_LARGE_LISTS_RUNTIME_CAPABILITY, + TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY +} from './terminal-quick-command-capabilities' import { AGENT_SESSION_BACKGROUND_TASK_CHILD_VIEWS_CAPABILITY } from './agent-session-background-task-child-views-capability' import { SKILL_INSTALL_RESULT_V2_CAPABILITY } from './skill-install-capability' import { ORCA_SIDEBAR_INPUTS_IS_MAIN } from './sidebar-inputs-switch' @@ -38,6 +41,7 @@ export function remoteRuntimeClientCapabilities( AUTOMATION_OWNER_FENCING_RUNTIME_CAPABILITY, REPO_SEARCH_QUALIFIED_REFS_RUNTIME_CAPABILITY, TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY, + TERMINAL_QUICK_COMMAND_LARGE_LISTS_RUNTIME_CAPABILITY, // Switch point: every desktop socket, browser hosting's included, leaves the unread flag to // a host that raises it. ...(ORCA_SIDEBAR_INPUTS_IS_MAIN ? [WORKSPACE_UNREAD_HOST_RAISES_CAPABILITY] : []), diff --git a/src/shared/terminal-quick-command-capabilities.ts b/src/shared/terminal-quick-command-capabilities.ts index e212a72c3ba..a52e338ba27 100644 --- a/src/shared/terminal-quick-command-capabilities.ts +++ b/src/shared/terminal-quick-command-capabilities.ts @@ -7,7 +7,13 @@ export const TERMINAL_QUICK_COMMANDS_RUNTIME_CAPABILITY = 'terminal.quick-comman export const TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY = 'terminal.quick-commands.long-prompts.v1' as const +// Older clients reject lists above forty; publish only a readable subset to them. +export const LEGACY_MAX_QUICK_COMMANDS = 40 +export const TERMINAL_QUICK_COMMAND_LARGE_LISTS_RUNTIME_CAPABILITY = + 'terminal.quick-commands.large-lists.v1' as const + export const TERMINAL_QUICK_COMMAND_RUNTIME_CAPABILITIES = [ TERMINAL_QUICK_COMMANDS_RUNTIME_CAPABILITY, - TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY + TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY, + TERMINAL_QUICK_COMMAND_LARGE_LISTS_RUNTIME_CAPABILITY ] as const diff --git a/src/shared/terminal-quick-commands.test.ts b/src/shared/terminal-quick-commands.test.ts index e41482602a1..23e3ed850dd 100644 --- a/src/shared/terminal-quick-commands.test.ts +++ b/src/shared/terminal-quick-commands.test.ts @@ -23,6 +23,20 @@ describe('terminal quick commands', () => { expect(normalizeTerminalQuickCommands([])).toEqual([]) }) + it('preserves lists larger than forty commands across loading and protocol parsing', () => { + const commands = Array.from({ length: 100 }, (_, index) => ({ + id: `command-${index}`, + label: `Command ${index}`, + action: 'terminal-command' as const, + command: 'echo saved', + appendEnter: true, + scope: { type: 'repo' as const, repoId: `project-${index}` } + })) + + expect(normalizeTerminalQuickCommands(commands)).toEqual(commands) + expect(parseNormalizedTerminalQuickCommands(commands)).toEqual(commands) + }) + it('removes quick commands from the abandoned preset rollout', () => { expect( normalizeTerminalQuickCommands([ diff --git a/src/shared/terminal-quick-commands.ts b/src/shared/terminal-quick-commands.ts index ac89d995e82..454e4ea4d90 100644 --- a/src/shared/terminal-quick-commands.ts +++ b/src/shared/terminal-quick-commands.ts @@ -7,7 +7,6 @@ import type { TerminalQuickCommandScope } from './terminal-quick-command-types' -export const MAX_QUICK_COMMANDS = 40 export const MAX_QUICK_COMMAND_ID_LENGTH = 80 export const MAX_QUICK_COMMAND_LABEL_LENGTH = 80 export const MAX_QUICK_COMMAND_REPO_ID_LENGTH = 200 @@ -147,10 +146,6 @@ export function normalizeTerminalQuickCommands(input: unknown): TerminalQuickCom appendEnter: record.appendEnter !== false }) } - - if (normalized.length >= MAX_QUICK_COMMANDS) { - break - } } return normalized @@ -212,7 +207,7 @@ function isNormalizedTerminalQuickCommand(value: unknown, expected: TerminalQuic export function parseNormalizedTerminalQuickCommands( input: unknown ): TerminalQuickCommand[] | null { - if (!Array.isArray(input) || input.length > MAX_QUICK_COMMANDS) { + if (!Array.isArray(input)) { return null } const normalized = normalizeTerminalQuickCommands(input) diff --git a/src/shared/web-runtime-client-capabilities.ts b/src/shared/web-runtime-client-capabilities.ts index 66f3296a87c..76ba45a7fd1 100644 --- a/src/shared/web-runtime-client-capabilities.ts +++ b/src/shared/web-runtime-client-capabilities.ts @@ -11,7 +11,10 @@ import { WORKTREE_VISIBILITY_DEFAULTS_RUNTIME_CAPABILITY, WORKTREE_VISIBILITY_SOURCE_DEFAULTS_RUNTIME_CAPABILITY } from './protocol-version' -import { TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY } from './terminal-quick-command-capabilities' +import { + TERMINAL_QUICK_COMMAND_LARGE_LISTS_RUNTIME_CAPABILITY, + TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY +} from './terminal-quick-command-capabilities' /** What the browser client advertises in its handshake with the host it is paired to. */ export const WEB_RUNTIME_CLIENT_CAPABILITIES = [ @@ -27,6 +30,7 @@ export const WEB_RUNTIME_CLIENT_CAPABILITIES = [ WORKTREE_VISIBILITY_SOURCE_DEFAULTS_RUNTIME_CAPABILITY, WORKTREE_BACKGROUND_REMOVAL_RUNTIME_CAPABILITY, TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY, + TERMINAL_QUICK_COMMAND_LARGE_LISTS_RUNTIME_CAPABILITY, // Same renderer as the desktop: a restored, unconfirmed agent row shows as identity only. AGENT_STATUS_RESTORED_UNCONFIRMED_RUNTIME_CAPABILITY ] as const