/ src / types / textInputTypes.ts
textInputTypes.ts
  1  import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs'
  2  import type { UUID } from 'crypto'
  3  import type React from 'react'
  4  import type { PermissionResult } from '../entrypoints/agentSdkTypes.js'
  5  import type { Key } from '../ink.js'
  6  import type { PastedContent } from '../utils/config.js'
  7  import type { ImageDimensions } from '../utils/imageResizer.js'
  8  import type { TextHighlight } from '../utils/textHighlighting.js'
  9  import type { AgentId } from './ids.js'
 10  import type { AssistantMessage, MessageOrigin } from './message.js'
 11  
 12  /**
 13   * Inline ghost text for mid-input command autocomplete
 14   */
 15  export type InlineGhostText = {
 16    /** The ghost text to display (e.g., "mit" for /commit) */
 17    readonly text: string
 18    /** The full command name (e.g., "commit") */
 19    readonly fullCommand: string
 20    /** Position in the input where the ghost text should appear */
 21    readonly insertPosition: number
 22  }
 23  
 24  /**
 25   * Base props for text input components
 26   */
 27  export type BaseTextInputProps = {
 28    /**
 29     * Optional callback for handling history navigation on up arrow at start of input
 30     */
 31    readonly onHistoryUp?: () => void
 32  
 33    /**
 34     * Optional callback for handling history navigation on down arrow at end of input
 35     */
 36    readonly onHistoryDown?: () => void
 37  
 38    /**
 39     * Text to display when `value` is empty.
 40     */
 41    readonly placeholder?: string
 42  
 43    /**
 44     * Allow multi-line input via line ending with backslash (default: `true`)
 45     */
 46    readonly multiline?: boolean
 47  
 48    /**
 49     * Listen to user's input. Useful in case there are multiple input components
 50     * at the same time and input must be "routed" to a specific component.
 51     */
 52    readonly focus?: boolean
 53  
 54    /**
 55     * Replace all chars and mask the value. Useful for password inputs.
 56     */
 57    readonly mask?: string
 58  
 59    /**
 60     * Whether to show cursor and allow navigation inside text input with arrow keys.
 61     */
 62    readonly showCursor?: boolean
 63  
 64    /**
 65     * Highlight pasted text
 66     */
 67    readonly highlightPastedText?: boolean
 68  
 69    /**
 70     * Value to display in a text input.
 71     */
 72    readonly value: string
 73  
 74    /**
 75     * Function to call when value updates.
 76     */
 77    readonly onChange: (value: string) => void
 78  
 79    /**
 80     * Function to call when `Enter` is pressed, where first argument is a value of the input.
 81     */
 82    readonly onSubmit?: (value: string) => void
 83  
 84    /**
 85     * Function to call when Ctrl+C is pressed to exit.
 86     */
 87    readonly onExit?: () => void
 88  
 89    /**
 90     * Optional callback to show exit message
 91     */
 92    readonly onExitMessage?: (show: boolean, key?: string) => void
 93  
 94    /**
 95     * Optional callback to show custom message
 96     */
 97    // readonly onMessage?: (show: boolean, message?: string) => void
 98  
 99    /**
100     * Optional callback to reset history position
101     */
102    readonly onHistoryReset?: () => void
103  
104    /**
105     * Optional callback when input is cleared (e.g., double-escape)
106     */
107    readonly onClearInput?: () => void
108  
109    /**
110     * Number of columns to wrap text at
111     */
112    readonly columns: number
113  
114    /**
115     * Maximum visible lines for the input viewport. When the wrapped input
116     * exceeds this many lines, only lines around the cursor are rendered.
117     */
118    readonly maxVisibleLines?: number
119  
120    /**
121     * Optional callback when an image is pasted
122     */
123    readonly onImagePaste?: (
124      base64Image: string,
125      mediaType?: string,
126      filename?: string,
127      dimensions?: ImageDimensions,
128      sourcePath?: string,
129    ) => void
130  
131    /**
132     * Optional callback when a large text (over 800 chars) is pasted
133     */
134    readonly onPaste?: (text: string) => void
135  
136    /**
137     * Callback when the pasting state changes
138     */
139    readonly onIsPastingChange?: (isPasting: boolean) => void
140  
141    /**
142     * Whether to disable cursor movement for up/down arrow keys
143     */
144    readonly disableCursorMovementForUpDownKeys?: boolean
145  
146    /**
147     * Skip the text-level double-press escape handler. Set this when a
148     * keybinding context (e.g. Autocomplete) owns escape — the keybinding's
149     * stopImmediatePropagation can't shield the text input because child
150     * effects register useInput listeners before parent effects.
151     */
152    readonly disableEscapeDoublePress?: boolean
153  
154    /**
155     * The offset of the cursor within the text
156     */
157    readonly cursorOffset: number
158  
159    /**
160     * Callback to set the offset of the cursor
161     */
162    onChangeCursorOffset: (offset: number) => void
163  
164    /**
165     * Optional hint text to display after command input
166     * Used for showing available arguments for commands
167     */
168    readonly argumentHint?: string
169  
170    /**
171     * Optional callback for undo functionality
172     */
173    readonly onUndo?: () => void
174  
175    /**
176     * Whether to render the text with dim color
177     */
178    readonly dimColor?: boolean
179  
180    /**
181     * Optional text highlights for search results or other highlighting
182     */
183    readonly highlights?: TextHighlight[]
184  
185    /**
186     * Optional custom React element to render as placeholder.
187     * When provided, overrides the standard `placeholder` string rendering.
188     */
189    readonly placeholderElement?: React.ReactNode
190  
191    /**
192     * Optional inline ghost text for mid-input command autocomplete
193     */
194    readonly inlineGhostText?: InlineGhostText
195  
196    /**
197     * Optional filter applied to raw input before key routing. Return the
198     * (possibly transformed) input string; returning '' for a non-empty
199     * input drops the event.
200     */
201    readonly inputFilter?: (input: string, key: Key) => string
202  }
203  
204  /**
205   * Extended props for VimTextInput
206   */
207  export type VimTextInputProps = BaseTextInputProps & {
208    /**
209     * Initial vim mode to use
210     */
211    readonly initialMode?: VimMode
212  
213    /**
214     * Optional callback for mode changes
215     */
216    readonly onModeChange?: (mode: VimMode) => void
217  }
218  
219  /**
220   * Vim editor modes
221   */
222  export type VimMode = 'INSERT' | 'NORMAL'
223  
224  /**
225   * Common properties for input hook results
226   */
227  export type BaseInputState = {
228    onInput: (input: string, key: Key) => void
229    renderedValue: string
230    offset: number
231    setOffset: (offset: number) => void
232    /** Cursor line (0-indexed) within the rendered text, accounting for wrapping. */
233    cursorLine: number
234    /** Cursor column (display-width) within the current line. */
235    cursorColumn: number
236    /** Character offset in the full text where the viewport starts (0 when no windowing). */
237    viewportCharOffset: number
238    /** Character offset in the full text where the viewport ends (text.length when no windowing). */
239    viewportCharEnd: number
240  
241    // For paste handling
242    isPasting?: boolean
243    pasteState?: {
244      chunks: string[]
245      timeoutId: ReturnType<typeof setTimeout> | null
246    }
247  }
248  
249  /**
250   * State for text input
251   */
252  export type TextInputState = BaseInputState
253  
254  /**
255   * State for vim input with mode
256   */
257  export type VimInputState = BaseInputState & {
258    mode: VimMode
259    setMode: (mode: VimMode) => void
260  }
261  
262  /**
263   * Input modes for the prompt
264   */
265  export type PromptInputMode =
266    | 'bash'
267    | 'prompt'
268    | 'orphaned-permission'
269    | 'task-notification'
270  
271  export type EditablePromptInputMode = Exclude<
272    PromptInputMode,
273    `${string}-notification`
274  >
275  
276  /**
277   * Queue priority levels. Same semantics in both normal and proactive mode.
278   *
279   *  - `now`   — Interrupt and send immediately. Aborts any in-flight tool
280   *              call (equivalent to Esc + send). Consumers (print.ts,
281   *              REPL.tsx) subscribe to queue changes and abort when they
282   *              see a 'now' command.
283   *  - `next`  — Mid-turn drain. Let the current tool call finish, then
284   *              send this message between the tool result and the next API
285   *              round-trip. Wakes an in-progress SleepTool call.
286   *  - `later` — End-of-turn drain. Wait for the current turn to finish,
287   *              then process as a new query. Wakes an in-progress SleepTool
288   *              call (query.ts upgrades the drain threshold after sleep so
289   *              the message is attached to the same turn).
290   *
291   * The SleepTool is only available in proactive mode, so "wakes SleepTool"
292   * is a no-op in normal mode.
293   */
294  export type QueuePriority = 'now' | 'next' | 'later'
295  
296  /**
297   * Queued command type
298   */
299  export type QueuedCommand = {
300    value: string | Array<ContentBlockParam>
301    mode: PromptInputMode
302    /** Defaults to the priority implied by `mode` when enqueued. */
303    priority?: QueuePriority
304    uuid?: UUID
305    orphanedPermission?: OrphanedPermission
306    /** Raw pasted contents including images. Images are resized at execution time. */
307    pastedContents?: Record<number, PastedContent>
308    /**
309     * The input string before [Pasted text #N] placeholders were expanded.
310     * Used for ultraplan keyword detection so pasted content containing the
311     * keyword does not trigger a CCR session. Falls back to `value` when
312     * unset (bridge/UDS/MCP sources have no paste expansion).
313     */
314    preExpansionValue?: string
315    /**
316     * When true, the input is treated as plain text even if it starts with `/`.
317     * Used for remotely-received messages (e.g. bridge/CCR) that should not
318     * trigger local slash commands or skills.
319     */
320    skipSlashCommands?: boolean
321    /**
322     * When true, slash commands are dispatched but filtered through
323     * isBridgeSafeCommand() — 'local-jsx' and terminal-only commands return
324     * a helpful error instead of executing. Set by the Remote Control bridge
325     * inbound path so mobile/web clients can run skills and benign commands
326     * without re-exposing the PR #19134 bug (/model popping the local picker).
327     */
328    bridgeOrigin?: boolean
329    /**
330     * When true, the resulting UserMessage gets `isMeta: true` — hidden in the
331     * transcript UI but visible to the model. Used by system-generated prompts
332     * (proactive ticks, teammate messages, resource updates) that route through
333     * the queue instead of calling `onQuery` directly.
334     */
335    isMeta?: boolean
336    /**
337     * Provenance of this command. Stamped onto the resulting UserMessage so the
338     * transcript records origin structurally (not just via XML tags in content).
339     * undefined = human (keyboard).
340     */
341    origin?: MessageOrigin
342    /**
343     * Workload tag threaded through to cc_workload= in the billing-header
344     * attribution block. The queue is the async boundary between the cron
345     * scheduler firing and the turn actually running — a user prompt can slip
346     * in between — so the tag rides on the QueuedCommand itself and is only
347     * hoisted into bootstrap state when THIS command is dequeued.
348     */
349    workload?: string
350    /**
351     * Agent that should receive this notification. Undefined = main thread.
352     * Subagents run in-process and share the module-level command queue; the
353     * drain gate in query.ts filters by this field so a subagent's background
354     * task notifications don't leak into the coordinator's context (PR #18453
355     * unified the queue but lost the isolation the dual-queue accidentally had).
356     */
357    agentId?: AgentId
358  }
359  
360  /**
361   * Type guard for image PastedContent with non-empty data. Empty-content
362   * images (e.g. from a 0-byte file drag) yield empty base64 strings that
363   * the API rejects with `image cannot be empty`. Use this at every site
364   * that converts PastedContent → ImageBlockParam so the filter and the
365   * ID list stay in sync.
366   */
367  export function isValidImagePaste(c: PastedContent): boolean {
368    return c.type === 'image' && c.content.length > 0
369  }
370  
371  /** Extract image paste IDs from a QueuedCommand's pastedContents. */
372  export function getImagePasteIds(
373    pastedContents: Record<number, PastedContent> | undefined,
374  ): number[] | undefined {
375    if (!pastedContents) {
376      return undefined
377    }
378    const ids = Object.values(pastedContents)
379      .filter(isValidImagePaste)
380      .map(c => c.id)
381    return ids.length > 0 ? ids : undefined
382  }
383  
384  export type OrphanedPermission = {
385    permissionResult: PermissionResult
386    assistantMessage: AssistantMessage
387  }