File size: 11,858 Bytes
064bfd6 f36b9c7 346a09e 96f34e3 f36b9c7 064bfd6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 | 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
/**
* When set, completely replaces the default system prompt for this query.
* Used by companion mode to use ONLY the persona prompt instead of the full
* Codev CLI system prompt.
*/
overrideSystemPrompt?: string
}
/**
* 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
}
|