mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
Allow saving quick command lists beyond forty (#32972)
* Allow saving quick command lists beyond forty Preserve compatibility by limiting older clients to readable commands * Cover quick command persistence across client capabilities * Reject edits to hidden quick commands from stale clients
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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({
|
||||
<Pressable
|
||||
style={({ pressed }) => [
|
||||
styles.addRow,
|
||||
addDisabled && styles.disabled,
|
||||
pressed && !addDisabled && styles.pressed
|
||||
disabled && styles.disabled,
|
||||
pressed && !disabled && styles.pressed
|
||||
]}
|
||||
disabled={addDisabled}
|
||||
disabled={disabled}
|
||||
onPress={onAdd}
|
||||
accessibilityRole="button"
|
||||
>
|
||||
<Plus size={18} color={colors.textSecondary} />
|
||||
<Text style={styles.addText}>
|
||||
{canAdd ? 'New quick command' : 'Quick command limit reached'}
|
||||
</Text>
|
||||
<Text style={styles.addText}>New quick command</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
]
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
+14
-2
@@ -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('界'))
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<typeof settings>) => {
|
||||
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 () => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 } })
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -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<RpcContext, 'clientKind' | 'clientCapabilities'>
|
||||
|
||||
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.'
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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<typeof vi.fn>
|
||||
} {
|
||||
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] }
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -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<RpcContext, 'clientKind' | 'clientCapabilities'>
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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] : []),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user