init
This commit is contained in:
@@ -0,0 +1,387 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user