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