textInputTypes.ts 11 KB

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