| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387 |
- 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
- }
|