388 lines
11 KiB
TypeScript
388 lines
11 KiB
TypeScript
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs'
|
|
import type { UUID } from 'crypto'
|
|
import type React from 'react'
|
|
import type { PermissionResult } from '../entrypoints/agentSdkTypes.js'
|
|
import type { Key } from '../ink.js'
|
|
import type { PastedContent } from '../utils/config.js'
|
|
import type { ImageDimensions } from '../utils/imageResizer.js'
|
|
import type { TextHighlight } from '../utils/textHighlighting.js'
|
|
import type { AgentId } from './ids.js'
|
|
import type { AssistantMessage, MessageOrigin } from './message.js'
|
|
|
|
/**
|
|
* Inline ghost text for mid-input command autocomplete
|
|
*/
|
|
export type InlineGhostText = {
|
|
/** The ghost text to display (e.g., "mit" for /commit) */
|
|
readonly text: string
|
|
/** The full command name (e.g., "commit") */
|
|
readonly fullCommand: string
|
|
/** Position in the input where the ghost text should appear */
|
|
readonly insertPosition: number
|
|
}
|
|
|
|
/**
|
|
* Base props for text input components
|
|
*/
|
|
export type BaseTextInputProps = {
|
|
/**
|
|
* Optional callback for handling history navigation on up arrow at start of input
|
|
*/
|
|
readonly onHistoryUp?: () => void
|
|
|
|
/**
|
|
* Optional callback for handling history navigation on down arrow at end of input
|
|
*/
|
|
readonly onHistoryDown?: () => void
|
|
|
|
/**
|
|
* Text to display when `value` is empty.
|
|
*/
|
|
readonly placeholder?: string
|
|
|
|
/**
|
|
* Allow multi-line input via line ending with backslash (default: `true`)
|
|
*/
|
|
readonly multiline?: boolean
|
|
|
|
/**
|
|
* Listen to user's input. Useful in case there are multiple input components
|
|
* at the same time and input must be "routed" to a specific component.
|
|
*/
|
|
readonly focus?: boolean
|
|
|
|
/**
|
|
* Replace all chars and mask the value. Useful for password inputs.
|
|
*/
|
|
readonly mask?: string
|
|
|
|
/**
|
|
* Whether to show cursor and allow navigation inside text input with arrow keys.
|
|
*/
|
|
readonly showCursor?: boolean
|
|
|
|
/**
|
|
* Highlight pasted text
|
|
*/
|
|
readonly highlightPastedText?: boolean
|
|
|
|
/**
|
|
* Value to display in a text input.
|
|
*/
|
|
readonly value: string
|
|
|
|
/**
|
|
* Function to call when value updates.
|
|
*/
|
|
readonly onChange: (value: string) => void
|
|
|
|
/**
|
|
* Function to call when `Enter` is pressed, where first argument is a value of the input.
|
|
*/
|
|
readonly onSubmit?: (value: string) => void
|
|
|
|
/**
|
|
* Function to call when Ctrl+C is pressed to exit.
|
|
*/
|
|
readonly onExit?: () => void
|
|
|
|
/**
|
|
* Optional callback to show exit message
|
|
*/
|
|
readonly onExitMessage?: (show: boolean, key?: string) => void
|
|
|
|
/**
|
|
* Optional callback to show custom message
|
|
*/
|
|
// readonly onMessage?: (show: boolean, message?: string) => void
|
|
|
|
/**
|
|
* Optional callback to reset history position
|
|
*/
|
|
readonly onHistoryReset?: () => void
|
|
|
|
/**
|
|
* Optional callback when input is cleared (e.g., double-escape)
|
|
*/
|
|
readonly onClearInput?: () => void
|
|
|
|
/**
|
|
* Number of columns to wrap text at
|
|
*/
|
|
readonly columns: number
|
|
|
|
/**
|
|
* Maximum visible lines for the input viewport. When the wrapped input
|
|
* exceeds this many lines, only lines around the cursor are rendered.
|
|
*/
|
|
readonly maxVisibleLines?: number
|
|
|
|
/**
|
|
* Optional callback when an image is pasted
|
|
*/
|
|
readonly onImagePaste?: (
|
|
base64Image: string,
|
|
mediaType?: string,
|
|
filename?: string,
|
|
dimensions?: ImageDimensions,
|
|
sourcePath?: string,
|
|
) => void
|
|
|
|
/**
|
|
* Optional callback when a large text (over 800 chars) is pasted
|
|
*/
|
|
readonly onPaste?: (text: string) => void
|
|
|
|
/**
|
|
* Callback when the pasting state changes
|
|
*/
|
|
readonly onIsPastingChange?: (isPasting: boolean) => void
|
|
|
|
/**
|
|
* Whether to disable cursor movement for up/down arrow keys
|
|
*/
|
|
readonly disableCursorMovementForUpDownKeys?: boolean
|
|
|
|
/**
|
|
* Skip the text-level double-press escape handler. Set this when a
|
|
* keybinding context (e.g. Autocomplete) owns escape — the keybinding's
|
|
* stopImmediatePropagation can't shield the text input because child
|
|
* effects register useInput listeners before parent effects.
|
|
*/
|
|
readonly disableEscapeDoublePress?: boolean
|
|
|
|
/**
|
|
* The offset of the cursor within the text
|
|
*/
|
|
readonly cursorOffset: number
|
|
|
|
/**
|
|
* Callback to set the offset of the cursor
|
|
*/
|
|
onChangeCursorOffset: (offset: number) => void
|
|
|
|
/**
|
|
* Optional hint text to display after command input
|
|
* Used for showing available arguments for commands
|
|
*/
|
|
readonly argumentHint?: string
|
|
|
|
/**
|
|
* Optional callback for undo functionality
|
|
*/
|
|
readonly onUndo?: () => void
|
|
|
|
/**
|
|
* Whether to render the text with dim color
|
|
*/
|
|
readonly dimColor?: boolean
|
|
|
|
/**
|
|
* Optional text highlights for search results or other highlighting
|
|
*/
|
|
readonly highlights?: TextHighlight[]
|
|
|
|
/**
|
|
* Optional custom React element to render as placeholder.
|
|
* When provided, overrides the standard `placeholder` string rendering.
|
|
*/
|
|
readonly placeholderElement?: React.ReactNode
|
|
|
|
/**
|
|
* Optional inline ghost text for mid-input command autocomplete
|
|
*/
|
|
readonly inlineGhostText?: InlineGhostText
|
|
|
|
/**
|
|
* Optional filter applied to raw input before key routing. Return the
|
|
* (possibly transformed) input string; returning '' for a non-empty
|
|
* input drops the event.
|
|
*/
|
|
readonly inputFilter?: (input: string, key: Key) => string
|
|
}
|
|
|
|
/**
|
|
* Extended props for VimTextInput
|
|
*/
|
|
export type VimTextInputProps = BaseTextInputProps & {
|
|
/**
|
|
* Initial vim mode to use
|
|
*/
|
|
readonly initialMode?: VimMode
|
|
|
|
/**
|
|
* Optional callback for mode changes
|
|
*/
|
|
readonly onModeChange?: (mode: VimMode) => void
|
|
}
|
|
|
|
/**
|
|
* Vim editor modes
|
|
*/
|
|
export type VimMode = 'INSERT' | 'NORMAL'
|
|
|
|
/**
|
|
* Common properties for input hook results
|
|
*/
|
|
export type BaseInputState = {
|
|
onInput: (input: string, key: Key) => void
|
|
renderedValue: string
|
|
offset: number
|
|
setOffset: (offset: number) => void
|
|
/** Cursor line (0-indexed) within the rendered text, accounting for wrapping. */
|
|
cursorLine: number
|
|
/** Cursor column (display-width) within the current line. */
|
|
cursorColumn: number
|
|
/** Character offset in the full text where the viewport starts (0 when no windowing). */
|
|
viewportCharOffset: number
|
|
/** Character offset in the full text where the viewport ends (text.length when no windowing). */
|
|
viewportCharEnd: number
|
|
|
|
// For paste handling
|
|
isPasting?: boolean
|
|
pasteState?: {
|
|
chunks: string[]
|
|
timeoutId: ReturnType<typeof setTimeout> | null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* State for text input
|
|
*/
|
|
export type TextInputState = BaseInputState
|
|
|
|
/**
|
|
* State for vim input with mode
|
|
*/
|
|
export type VimInputState = BaseInputState & {
|
|
mode: VimMode
|
|
setMode: (mode: VimMode) => void
|
|
}
|
|
|
|
/**
|
|
* Input modes for the prompt
|
|
*/
|
|
export type PromptInputMode =
|
|
| 'bash'
|
|
| 'prompt'
|
|
| 'orphaned-permission'
|
|
| 'task-notification'
|
|
|
|
export type EditablePromptInputMode = Exclude<
|
|
PromptInputMode,
|
|
`${string}-notification`
|
|
>
|
|
|
|
/**
|
|
* Queue priority levels. Same semantics in both normal and proactive mode.
|
|
*
|
|
* - `now` — Interrupt and send immediately. Aborts any in-flight tool
|
|
* call (equivalent to Esc + send). Consumers (print.ts,
|
|
* REPL.tsx) subscribe to queue changes and abort when they
|
|
* see a 'now' command.
|
|
* - `next` — Mid-turn drain. Let the current tool call finish, then
|
|
* send this message between the tool result and the next API
|
|
* round-trip. Wakes an in-progress SleepTool call.
|
|
* - `later` — End-of-turn drain. Wait for the current turn to finish,
|
|
* then process as a new query. Wakes an in-progress SleepTool
|
|
* call (query.ts upgrades the drain threshold after sleep so
|
|
* the message is attached to the same turn).
|
|
*
|
|
* The SleepTool is only available in proactive mode, so "wakes SleepTool"
|
|
* is a no-op in normal mode.
|
|
*/
|
|
export type QueuePriority = 'now' | 'next' | 'later'
|
|
|
|
/**
|
|
* Queued command type
|
|
*/
|
|
export type QueuedCommand = {
|
|
value: string | Array<ContentBlockParam>
|
|
mode: PromptInputMode
|
|
/** Defaults to the priority implied by `mode` when enqueued. */
|
|
priority?: QueuePriority
|
|
uuid?: UUID
|
|
orphanedPermission?: OrphanedPermission
|
|
/** Raw pasted contents including images. Images are resized at execution time. */
|
|
pastedContents?: Record<number, PastedContent>
|
|
/**
|
|
* The input string before [Pasted text #N] placeholders were expanded.
|
|
* Used for ultraplan keyword detection so pasted content containing the
|
|
* keyword does not trigger a CCR session. Falls back to `value` when
|
|
* unset (bridge/UDS/MCP sources have no paste expansion).
|
|
*/
|
|
preExpansionValue?: string
|
|
/**
|
|
* When true, the input is treated as plain text even if it starts with `/`.
|
|
* Used for remotely-received messages (e.g. bridge/CCR) that should not
|
|
* trigger local slash commands or skills.
|
|
*/
|
|
skipSlashCommands?: boolean
|
|
/**
|
|
* When true, slash commands are dispatched but filtered through
|
|
* isBridgeSafeCommand() — 'local-jsx' and terminal-only commands return
|
|
* a helpful error instead of executing. Set by the Remote Control bridge
|
|
* inbound path so mobile/web clients can run skills and benign commands
|
|
* without re-exposing the PR #19134 bug (/model popping the local picker).
|
|
*/
|
|
bridgeOrigin?: boolean
|
|
/**
|
|
* When true, the resulting UserMessage gets `isMeta: true` — hidden in the
|
|
* transcript UI but visible to the model. Used by system-generated prompts
|
|
* (proactive ticks, teammate messages, resource updates) that route through
|
|
* the queue instead of calling `onQuery` directly.
|
|
*/
|
|
isMeta?: boolean
|
|
/**
|
|
* Provenance of this command. Stamped onto the resulting UserMessage so the
|
|
* transcript records origin structurally (not just via XML tags in content).
|
|
* undefined = human (keyboard).
|
|
*/
|
|
origin?: MessageOrigin
|
|
/**
|
|
* Workload tag threaded through to cc_workload= in the billing-header
|
|
* attribution block. The queue is the async boundary between the cron
|
|
* scheduler firing and the turn actually running — a user prompt can slip
|
|
* in between — so the tag rides on the QueuedCommand itself and is only
|
|
* hoisted into bootstrap state when THIS command is dequeued.
|
|
*/
|
|
workload?: string
|
|
/**
|
|
* Agent that should receive this notification. Undefined = main thread.
|
|
* Subagents run in-process and share the module-level command queue; the
|
|
* drain gate in query.ts filters by this field so a subagent's background
|
|
* task notifications don't leak into the coordinator's context (PR #18453
|
|
* unified the queue but lost the isolation the dual-queue accidentally had).
|
|
*/
|
|
agentId?: AgentId
|
|
}
|
|
|
|
/**
|
|
* Type guard for image PastedContent with non-empty data. Empty-content
|
|
* images (e.g. from a 0-byte file drag) yield empty base64 strings that
|
|
* the API rejects with `image cannot be empty`. Use this at every site
|
|
* that converts PastedContent → ImageBlockParam so the filter and the
|
|
* ID list stay in sync.
|
|
*/
|
|
export function isValidImagePaste(c: PastedContent): boolean {
|
|
return c.type === 'image' && c.content.length > 0
|
|
}
|
|
|
|
/** Extract image paste IDs from a QueuedCommand's pastedContents. */
|
|
export function getImagePasteIds(
|
|
pastedContents: Record<number, PastedContent> | undefined,
|
|
): number[] | undefined {
|
|
if (!pastedContents) {
|
|
return undefined
|
|
}
|
|
const ids = Object.values(pastedContents)
|
|
.filter(isValidImagePaste)
|
|
.map(c => c.id)
|
|
return ids.length > 0 ? ids : undefined
|
|
}
|
|
|
|
export type OrphanedPermission = {
|
|
permissionResult: PermissionResult
|
|
assistantMessage: AssistantMessage
|
|
}
|