Files
orca/mobile/src/session/quick-command-draft.ts
T
Brennan BensonandOrcaWin 7c23f79622 fix(quick-commands): save agent prompts whole, bounded by what a chat message carries (#24488)
* fix(quick-commands): save agent prompts whole, bounded by what a chat message carries

Agent quick-command prompts were silently cut to 6,000 characters on save:
the editor accepted a long paste, settings normalization trimmed it, and
reopening showed the shortened copy. The 6,000 was a Windows launch speed
threshold, not a limit on prompts.

- Storage no longer trims. A save is refused, never trimmed, when a prompt's
  chat text block is over 256 KiB of JSON (measured on its real escaped size,
  the measure the chat send schema applies) or the list is too large for one
  paired reply. Host and desktop store both check before writing.
- The editor shows the size against the limit and blocks Save past it, and
  stays open with the text when the host refuses the save.
- terminal.quick-commands.long-prompts.v1: hosts withhold prompts over 6,000
  characters from clients that do not advertise it (older clients reject a
  whole list holding one); editors keep an older host's 6,000 cap.
- The phone editor no longer drops pasted text past 6,000 via maxLength.

Launch delivery of long prompts is left to the launch-prompt series
(#24257, #23962, #24080); this branch's earlier paste-after-ready delivery
duplicated it and is removed.

* fix(quick-commands): drop a stale duplicate doc comment

* fix(quick-commands): advertise long prompts to this build's own host too

* fix(quick-commands): keep submitted editor immutable until save settles

* test(quick-commands): keep storage and save lifecycle coverage focused

* fix(quick-commands): keep portalled choices within pending form ownership

* Preserve retained credentials when settings validation refuses a save

---------

Co-authored-by: OrcaWin <alpha-eng@stably.ai>
2026-10-15 18:37:41 -07:00

136 lines
3.9 KiB
TypeScript

import type {
TerminalQuickCommand,
TerminalQuickCommandAction,
TerminalQuickCommandScope
} from '../../../src/shared/terminal-quick-command-types'
import type { TuiAgent } from '../../../src/shared/tui-agent'
import {
getTerminalQuickCommandBodySize,
isAgentQuickCommand,
MAX_QUICK_COMMAND_LABEL_LENGTH,
MAX_QUICK_COMMAND_TERMINAL_TEXT_LENGTH,
supportsTerminalAgentQuickCommand
} from '../terminal/quick-commands'
// A single mutable draft covering both quick-command actions. Both the command
// text and the agent/prompt are kept while editing so toggling Action back and
// forth doesn't discard what the user already typed (mirrors desktop's draft).
export type QuickCommandDraft = {
id: string | null
label: string
action: TerminalQuickCommandAction
command: string
appendEnter: boolean
agent: TuiAgent | null
prompt: string
scope: TerminalQuickCommandScope
}
export function createEmptyQuickCommandDraft(scope: TerminalQuickCommandScope): QuickCommandDraft {
return {
id: null,
label: '',
action: 'terminal-command',
command: '',
appendEnter: true,
agent: null,
prompt: '',
scope
}
}
export function quickCommandToDraft(command: TerminalQuickCommand): QuickCommandDraft {
const scope: TerminalQuickCommandScope =
command.scope?.type === 'repo' && command.scope.repoId
? { type: 'repo', repoId: command.scope.repoId }
: { type: 'global' }
if (isAgentQuickCommand(command)) {
return {
id: command.id,
label: command.label,
action: 'agent-prompt',
command: '',
appendEnter: true,
agent: command.agent,
prompt: command.prompt,
scope
}
}
return {
id: command.id,
label: command.label,
action: 'terminal-command',
command: command.command,
appendEnter: command.appendEnter !== false,
agent: null,
prompt: '',
scope
}
}
/** Whether the draft's prompt is over its limit; `agentPromptMaxLength` is an older host's cap. */
export function isQuickCommandDraftPromptTooLong(
draft: QuickCommandDraft,
agentPromptMaxLength: number | null
): boolean {
if (draft.action !== 'agent-prompt') {
return false
}
const size = getTerminalQuickCommandBodySize('agent-prompt', draft.prompt, agentPromptMaxLength)
return size.used > size.max
}
export function isQuickCommandDraftValid(
draft: QuickCommandDraft,
agentPromptMaxLength: number | null = null
): boolean {
if (!draft.label.trim()) {
return false
}
if (draft.action === 'agent-prompt') {
return Boolean(
draft.agent &&
supportsTerminalAgentQuickCommand(draft.agent) &&
draft.prompt.trim().length > 0 &&
!isQuickCommandDraftPromptTooLong(draft, agentPromptMaxLength)
)
}
return draft.command.trim().length > 0
}
// Build the persisted command from a draft. Returns null when incomplete so the
// caller can keep the editor open. The server re-normalizes, but we trim/cap
// here so optimistic local state matches what will be saved.
export function draftToQuickCommand(
draft: QuickCommandDraft,
agentPromptMaxLength: number | null = null
): TerminalQuickCommand | null {
if (!isQuickCommandDraftValid(draft, agentPromptMaxLength)) {
return null
}
// Why: timestamps alone collide when desktop and mobile add commands in the
// same millisecond, which would turn an atomic upsert into an overwrite.
const id =
draft.id ??
`quick-command-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`
const label = draft.label.trim().slice(0, MAX_QUICK_COMMAND_LABEL_LENGTH)
if (draft.action === 'agent-prompt' && draft.agent) {
return {
id,
label,
action: 'agent-prompt',
agent: draft.agent,
prompt: draft.prompt.trimEnd(),
scope: draft.scope
}
}
return {
id,
label,
action: 'terminal-command',
command: draft.command.trimEnd().slice(0, MAX_QUICK_COMMAND_TERMINAL_TEXT_LENGTH),
appendEnter: draft.appendEnter,
scope: draft.scope
}
}