Documentation
¶
Overview ¶
Mouse tracking and context menu support for sprout IDE
Package console (continued) — tool execution timeline subscriber.
Index ¶
- Constants
- func ApplyColorBlindFromEnv()
- func BoldText(text string) string
- func BoxHint(label string) string
- func ClearLineSeq() string
- func ClearScreenSeq() string
- func ClearToEndOfLineSeq() string
- func ClearToEndOfScreenSeq() string
- func ClearToStartOfLineSeq() string
- func ColorBlindEnabled() bool
- func Colorize(text, color string) string
- func ColorizeBold(text, color string) string
- func CycleCompletion(cycle *CompletionCycle, line string, cursorPos int, ...) (newLine string, newCursorPos int, ok bool)
- func DetectImageMagic(data []byte) (ext string, mimeType string)
- func DisableMouseTracking()
- func EnableMouseTracking()
- func FormatErrorBlock(header string, err error) string
- func FormatHunkSummary(hunk ReviewHunk) string
- func FormatYesNoPrompt(yesDefault bool) string
- func FormatYesNoPromptStdout(yesDefault bool) string
- func HideCursorSeq() string
- func HomeCursorSeq() string
- func IsLikelyMarkdown(text string) bool
- func KeymapHelpTable() string
- func LockOutput()
- func MoveCursorDownSeq(n int) string
- func MoveCursorLeftSeq(n int) string
- func MoveCursorSeq(x, y int) string
- func MoveCursorToColumnSeq(n int) string
- func MoveCursorUpSeq(n int) string
- func PersonaBadge(depth int, personaID string) string
- func PersonaColor(personaID string) string
- func PersonaIndent(depth int) string
- func PrintExternal(msg string)
- func PromptShellApprovalParts(ctx context.Context, parts []ShellPartInfo) (map[string]bool, error)
- func RegisterGlobalIndicator(ind *ActivityIndicator)
- func RegisterGlobalStatusFooter(f *StatusFooter)
- func RegisterKeymapForFooter(footer *StatusFooter)
- func RenderColoredDiff(w io.Writer, hunk ReviewHunk)
- func ResetScrollRegionSeq() string
- func SavePastedImage(data []byte, baseDir string) (string, error)
- func SavePastedText(content, baseDir string) (string, error)
- func SetColorBlind(enabled bool) bool
- func SetGlobalMetricsRecorderForTest(r *MetricsRecorder) func()
- func SetScrollRegionSeq(top, bottom int) string
- func ShouldSmartSavePaste(content string) bool
- func ShowCursorSeq() string
- func SinkFromPrintf() func(string)
- func StderrIsTerminal() bool
- func StdoutIsTerminal() bool
- func StopGlobalStatusFooter()
- func SuspendIndicator()
- func TryLockOutput() bool
- func UnlockOutput()
- func WithOutput(fn func())
- func WrapHardLine(s string, cols int) []string
- func WrapSteerLayout(text string, cursorByte, cols, maxRows int) (lines []string, cursorRow, cursorCol int)
- func WriteWrappedLines(w io.Writer, lines []string, cols, withCursorRow, cursorCol int) error
- type ActivityIndicator
- func (a *ActivityIndicator) ClearStatic()
- func (a *ActivityIndicator) Elapsed() time.Duration
- func (a *ActivityIndicator) IsActive() bool
- func (a *ActivityIndicator) IsTTY() bool
- func (a *ActivityIndicator) Replace(line string)
- func (a *ActivityIndicator) ReplaceLast(line string)
- func (a *ActivityIndicator) ReplaceLastN(line string, n int)
- func (a *ActivityIndicator) SetStatic(line string)
- func (a *ActivityIndicator) Start(msg string)
- func (a *ActivityIndicator) Stop()
- func (a *ActivityIndicator) Update(msg string)
- type AssistantTurnRenderer
- func (r *AssistantTurnRenderer) CursorOnFreshRow() bool
- func (r *AssistantTurnRenderer) EndReasoning()
- func (r *AssistantTurnRenderer) FinalizeAtTurnEnd()
- func (r *AssistantTurnRenderer) OnExternalWrite()
- func (r *AssistantTurnRenderer) OnExternalWriteRows(n int)
- func (r *AssistantTurnRenderer) ReasoningActive() bool
- func (r *AssistantTurnRenderer) SetFooter(f *StatusFooter)
- func (r *AssistantTurnRenderer) WriteChunk(chunk string)
- func (r *AssistantTurnRenderer) WriteReasoningChunk(chunk string)
- type CIOutputHandler
- func (h *CIOutputHandler) IsCI() bool
- func (h *CIOutputHandler) IsInteractive() bool
- func (h *CIOutputHandler) PrintProgress()
- func (h *CIOutputHandler) PrintSummary()
- func (h *CIOutputHandler) Printf(format string, args ...interface{})
- func (h *CIOutputHandler) ShouldShowProgress() bool
- func (h *CIOutputHandler) UpdateMetrics(totalTokens, contextTokens, maxContextTokens, iteration int, totalCost float64)
- func (h *CIOutputHandler) Write(p []byte) (n int, err error)
- func (h *CIOutputHandler) WriteString(s string) error
- type CompletionCycle
- type CompletionProvider
- type ContentSource
- type ContextMenu
- func (cm *ContextMenu) AddItem(id, label, description, shortcut string, enabled bool, ...)
- func (cm *ContextMenu) ClearItems()
- func (cm *ContextMenu) Hide()
- func (cm *ContextMenu) NavigateDown()
- func (cm *ContextMenu) NavigateUp()
- func (cm *ContextMenu) Render()
- func (cm *ContextMenu) SelectCurrent() *ContextMenuItem
- func (cm *ContextMenu) SetPosition(row, col int)
- func (cm *ContextMenu) Show()
- func (cm *ContextMenu) Toggle()
- type ContextMenuItem
- type EditReviewResult
- type EscapeParser
- type FooterTooltip
- type Glyph
- type GroundTruthTermios
- type InputEvent
- type InputEventType
- type InputReader
- func (ir *InputReader) AddToHistory(command string)
- func (ir *InputReader) Backspace()
- func (ir *InputReader) Delete()
- func (ir *InputReader) DeleteWordBackward()
- func (ir *InputReader) GetHistory() []string
- func (ir *InputReader) HandleEvent(event *InputEvent)
- func (ir *InputReader) InsertChar(char string)
- func (ir *InputReader) KillToEndOfLine()
- func (ir *InputReader) KillToStartOfLine()
- func (ir *InputReader) MoveCursor(delta int)
- func (ir *InputReader) MoveWord(direction int)
- func (ir *InputReader) NavigateHistory(direction int)
- func (ir *InputReader) NavigateVertically(direction int)
- func (ir *InputReader) ReadLine() (string, error)
- func (ir *InputReader) Refresh()
- func (ir *InputReader) SetCompleter(c CompletionProvider)
- func (ir *InputReader) SetCursor(pos int)
- func (ir *InputReader) SetFooterTooltip(t *FooterTooltip)
- func (ir *InputReader) SetGroundTruth(gt *GroundTruthTermios)
- func (ir *InputReader) SetHistory(history []string)
- func (ir *InputReader) SetInitialContent(content string)
- func (ir *InputReader) SetPrompt(p string)
- type KeymapEntry
- type KeymapRegistry
- type LineCapWriter
- type MarkdownFormatter
- type MetricsRecorder
- type MouseButton
- type MouseEvent
- type MouseEventKind
- type MouseModifier
- type Panel
- type PanelStyle
- type ReasoningFold
- type ReviewDiffLine
- type ReviewHunk
- type SelectItem
- type SelectList
- type SelectListOptions
- type ShellPartInfo
- type StatusFooter
- func (f *StatusFooter) ClearSteerLine()
- func (f *StatusFooter) Refresh()
- func (f *StatusFooter) Resize()
- func (f *StatusFooter) SetProseStreaming(active bool)
- func (f *StatusFooter) SetSteerLine(text string)
- func (f *StatusFooter) SetSteerLineWithCursor(text string, cursorByteOffset int)
- func (f *StatusFooter) SetSteerLineWrapped(text string, cursorRow, cursorCol int)
- func (f *StatusFooter) Start()
- func (f *StatusFooter) Stop()
- func (f *StatusFooter) TerminalSize() (cols, rows int)
- type SteerInputReader
- func (r *SteerInputReader) DrainUnsentBuffer() string
- func (r *SteerInputReader) IsActive() bool
- func (r *SteerInputReader) ResetBuffer()
- func (r *SteerInputReader) SetCompleter(c CompletionProvider)
- func (r *SteerInputReader) SetGroundTruth(gt *GroundTruthTermios)
- func (r *SteerInputReader) Start()
- func (r *SteerInputReader) Stop()
- func (r *SteerInputReader) SubmitMode() SteerSubmitMode
- type SteerSubmitMode
- type StreamingMarkdownFormatter
- type TerminalManager
- type ToolInvocation
- type ToolTimeline
Constants ¶
const ( ColorReset = "\033[0m" ColorBold = "\033[1m" ColorDim = "\033[2m" ColorItalic = "\033[3m" ColorUnderline = "\033[4m" // Colors ColorRed = "\033[31m" ColorGreen = "\033[32m" ColorYellow = "\033[33m" ColorBlue = "\033[34m" ColorMagenta = "\033[35m" ColorCyan = "\033[36m" ColorWhite = "\033[37m" ColorGray = "\033[90m" // Bright colors ColorBrightRed = "\033[91m" ColorBrightGreen = "\033[92m" ColorBrightYellow = "\033[93m" ColorBrightBlue = "\033[94m" ColorBrightMagenta = "\033[95m" ColorBrightCyan = "\033[96m" ColorBrightWhite = "\033[97m" // Background colors BgRed = "\033[41m" BgGreen = "\033[42m" BgYellow = "\033[43m" BgBlue = "\033[44m" BgMagenta = "\033[45m" BgCyan = "\033[46m" BgGray = "\033[100m" )
ANSI color codes
const ( // Enable mouse tracking (X10 mode - button press/release only) MouseTrackingX10 = "\x1b[?9h" // Enable mouse tracking (VT200 mode - all mouse events) MouseTrackingVT200 = "\x1b[?1000h" // Enable mouse tracking with SGR extended coordinates MouseTrackingSGR = "\x1b[?1006h" // Disable all mouse tracking MouseTrackingDisable = "\x1b[?1006l\x1b[?1000l\x1b[?9l" )
Mouse tracking escape sequences
const ( SteerPromptPrefix = "⇄ steer › " QueuePromptPrefix = "⏸ queue › " )
SteerPromptPrefix is the visible glyph + space rendered at the start of the pinned input line in STEER mode. QueuePromptPrefix is the alternative shown after the user toggles via Tab. Both exposed for testing / theming.
const ( SmartPasteLineThreshold = 100 SmartPasteByteThreshold = 5 * 1024 )
SmartPasteLineThreshold and SmartPasteByteThreshold trigger the smart- paste flow when a bracketed paste's content exceeds either bound. The content is written to PastedTextDirName and a `@<relpath>` reference is inserted into the input buffer instead of the raw blob, mirroring the image-paste pattern. SP-048-4d.
const MaxPastedImageSize = 10 * 1024 * 1024
MaxPastedImageSize is the maximum size of a pasted image before rejection (10 MB).
const PastedImageDirName = ".sprout/pasted-images"
PastedImageDirName is the subdirectory (relative to CWD) where pasted images are saved.
const PastedTextDirName = ".sprout/pastes"
PastedTextDirName is the workspace-relative directory under which large text pastes are auto-saved by SavePastedText. Mirrors PastedImageDirName.
const SteerHistoryCap = 50
SteerHistoryCap bounds the in-memory steer history to a sensible session-level value. Exposed for testing / config.
Variables ¶
This section is empty.
Functions ¶
func ApplyColorBlindFromEnv ¶ added in v0.16.19
func ApplyColorBlindFromEnv()
ApplyColorBlindFromEnv reads SPROUT_COLOR_BLIND and enables the palette swap if set to a truthy value ("1", "true", "yes"). Called from cmd/root.go after flag parsing so the CLI flag wins over the env var (the flag sets the atomic directly via SetColorBlind).
func BoxHint ¶ added in v0.16.19
BoxHint returns a brief one-line rendering of a box-drawing example, useful for inline UI hints (footer, tooltips) without rendering a full multi-row panel.
func ClearLineSeq ¶
func ClearLineSeq() string
ClearLineSeq returns the escape sequence to clear the entire current line.
func ClearScreenSeq ¶
func ClearScreenSeq() string
ClearScreenSeq returns the escape sequence to clear the entire screen.
func ClearToEndOfLineSeq ¶
func ClearToEndOfLineSeq() string
ClearToEndOfLineSeq returns the escape sequence to clear from cursor to end of line.
func ClearToEndOfScreenSeq ¶
func ClearToEndOfScreenSeq() string
ClearToEndOfScreenSeq returns the escape sequence to clear from cursor to end of screen.
func ClearToStartOfLineSeq ¶
func ClearToStartOfLineSeq() string
ClearToStartOfLineSeq returns the escape sequence to clear from start of line to cursor.
func ColorBlindEnabled ¶ added in v0.16.19
func ColorBlindEnabled() bool
ColorBlindEnabled reports the current palette state. Mostly useful in tests and /help output.
func ColorizeBold ¶
ColorizeBold wraps text with bold and a color code
func CycleCompletion ¶ added in v0.16.18
func CycleCompletion(cycle *CompletionCycle, line string, cursorPos int, completer CompletionProvider) (newLine string, newCursorPos int, ok bool)
CycleCompletion either advances the existing completion cycle or starts a fresh one. Returns the new (line, cursorPos) pair the caller should apply, plus an `ok` flag — false when there is no completer installed OR the completer returned zero candidates (silent no-op, matches the InputReader's existing behavior at input_completion.go:42-46).
`cycle` must be a non-nil pointer to a CompletionCycle owned by the caller; CycleCompletion initializes it on the first call and reads from it on subsequent calls. Both InputReader.handleTabCompletion and SteerInputReader.handleSteerCompletion allocate the cycle lazily on first apply. The caller must call cycle.Advance(newLine) after a successful apply, and cycle.Reset() whenever the user edits the buffer so the next press starts fresh.
func DetectImageMagic ¶
DetectImageMagic checks if data starts with a known image format signature. Returns the file extension (e.g., ".png") and MIME type (e.g., "image/png"), or empty strings if no known image format is detected.
func DisableMouseTracking ¶
func DisableMouseTracking()
DisableMouseTracking disables mouse tracking
func EnableMouseTracking ¶
func EnableMouseTracking()
EnableMouseTracking enables mouse tracking in the terminal
func FormatErrorBlock ¶
FormatErrorBlock turns an error into a CLI-renderable block.
- nil error → empty string. Callers can append it directly to a header line without an extra branch.
- Single-line error → "<header>: <err>\n", matching today's `fmt.Fprintf(os.Stderr, "[FAIL] Error: %v\n", err)` output exactly so existing logs aren't disturbed.
- Multi-line error (contains \n after trimming the trailing newline) → "<header>:\n <line1>\n <line2>\n…", indented by two spaces, with red coloring when ANSI colors are enabled. Preserves the full stderr a tool produced instead of collapsing it to its first line.
The two-space indent matches the rest of sprout's CLI conventions (tool timeline, footer padding) so a multi-line error reads as part of the same chrome rather than a foreign block.
func FormatHunkSummary ¶ added in v0.16.12
func FormatHunkSummary(hunk ReviewHunk) string
FormatHunkSummary returns a compact one-line description of a hunk for use in list views. Includes FilePath when set so multi-file batches stay readable.
func FormatYesNoPrompt ¶
FormatYesNoPrompt returns a y/N or Y/n prompt string with the default letter bolded. When yesDefault is true, the Y is bolded (Y/n). When yesDefault is false, the N is bolded (y/N).
ANSI codes are only applied when the stderr is a TTY. When stderr is not a terminal (e.g., piped to a file), the plain text is returned without any escape codes.
func FormatYesNoPromptStdout ¶
FormatYesNoPromptStdout returns a y/N or Y/n prompt string with the default letter bolded, checking stdout for terminal status (same logic as FormatYesNoPrompt but uses os.Stdout instead of os.Stderr for the TTY check).
func HideCursorSeq ¶
func HideCursorSeq() string
HideCursorSeq returns the escape sequence to hide the cursor.
func HomeCursorSeq ¶
func HomeCursorSeq() string
HomeCursorSeq returns the escape sequence to move cursor to home position (1,1).
func IsLikelyMarkdown ¶
IsLikelyMarkdown checks if text contains markdown patterns More selective to avoid formatting code blocks, shell output, or other non-summary text
func KeymapHelpTable ¶ added in v0.16.19
func KeymapHelpTable() string
KeymapHelpTable renders the registered entries as a fixed-column table suitable for embedding in /help output. Column widths are computed from the data so the table stays aligned regardless of how many entries are registered.
func MoveCursorDownSeq ¶
MoveCursorDownSeq returns the escape sequence to move cursor down by n lines.
func MoveCursorLeftSeq ¶
MoveCursorLeftSeq returns the escape sequence to move cursor left by n columns.
func MoveCursorSeq ¶
MoveCursorSeq returns the escape sequence to move the cursor to (x,y) Note: ANSI uses row (y) first, then column (x).
func MoveCursorToColumnSeq ¶
MoveCursorToColumnSeq returns the escape sequence to move cursor to column n (1-based).
func MoveCursorUpSeq ¶
MoveCursorUpSeq returns the escape sequence to move cursor up by n lines.
func PersonaBadge ¶
PersonaBadge renders a colored "[persona]" prefix for a tool-timeline line. Returns the empty string for depth 0 or empty persona — the primary agent's own tool lines get no badge so the existing UX is preserved.
Example: PersonaBadge(1, "coder") → "\033[36m[coder]\033[0m "
PersonaBadge(0, "orchestrator") → ""
func PersonaColor ¶
PersonaColor returns the ANSI color escape for the given persona ID, or the empty string when colors are disabled (NO_COLOR / non-TTY default).
Unknown personas get a dim-gray fallback so they're still visually grouped without colliding with any of the well-known IDs.
func PersonaIndent ¶
PersonaIndent returns the leading whitespace for a depth-N tool line. Two spaces per depth level keeps the timeline scannable without taking over the line. Depth 0 returns "" so primary-agent output is unchanged.
func PrintExternal ¶ added in v0.16.17
func PrintExternal(msg string)
PrintExternal prints a message to the terminal without corrupting an active input line. When a ReadLine loop is active (activeInputReader is set), the message is printed by clearing the current input line, emitting the message (which scrolls within the terminal's scroll region), and then redrawing the input prompt + buffer below it. When no ReadLine is active, the message is printed directly.
This is the correct path for background messages (security cautions, tool-log lines, async output) that arrive while the REPL is waiting for user input between turns. The previous code routed these through the streaming callback's fallback (fmt.Print), which wrote to stdout without cursor management — displacing the cursor from the input line and making subsequent keystrokes appear at the wrong position.
func PromptShellApprovalParts ¶ added in v0.16.19
PromptShellApprovalParts shows the user one line per part of the shell proposal and prompts y/n per part. Returns a decisions map keyed by part ID. Supports bulk 'a' (accept all remaining) and 'r' (reject all remaining) for fast triage.
The picker is intentionally line-based (not SelectList) so it stays testable via io.Reader injection and so it works in non-TTY contexts like CI / piped stdin. The arrow-key picker in security_prompt.go is reserved for the single 4-option gate.
func RegisterGlobalIndicator ¶
func RegisterGlobalIndicator(ind *ActivityIndicator)
RegisterGlobalIndicator installs ind as the process-wide indicator that SuspendIndicator and clihooks.SuspendIndicator both target. Pass nil to clear. Safe to call multiple times.
func RegisterGlobalStatusFooter ¶
func RegisterGlobalStatusFooter(f *StatusFooter)
RegisterGlobalStatusFooter installs f as the process-wide footer that StopGlobalStatusFooter targets. Pass nil to clear. Safe to call multiple times. Mirrors RegisterGlobalIndicator.
func RegisterKeymapForFooter ¶ added in v0.16.19
func RegisterKeymapForFooter(footer *StatusFooter)
RegisterKeymapForFooter wires Alt+T → footer-tooltip toggle into the global keymap. Call from your REPL bootstrap (or wherever the agent shell starts). Idempotent — calling twice doesn't double-register because the keymap replaces by Action name.
func RenderColoredDiff ¶ added in v0.16.12
func RenderColoredDiff(w io.Writer, hunk ReviewHunk)
RenderColoredDiff renders a hunk's diff lines with ANSI colors: green for additions, red for removals, dim for context.
When the hunk has a FilePath, the header renders "<path> · hunk-ID ── proposed (lines X-Y) ────" so multi-file edit reviews are scannable; an empty FilePath falls back to the legacy "hunk-ID ── proposed ────" form.
func ResetScrollRegionSeq ¶
func ResetScrollRegionSeq() string
ResetScrollRegionSeq resets the scrolling region to the full screen.
func SavePastedImage ¶
SavePastedImage saves raw image data to .sprout/pasted-images/ under the provided base directory (typically the workspace root). It returns a relative path like "./.sprout/pasted-images/paste_20260320_145959_abc123.png". If baseDir is empty, it falls back to os.Getwd().
func SavePastedText ¶
SavePastedText writes content to .sprout/pastes/ with a timestamped random-suffixed filename and returns the workspace-relative path (e.g. "./.sprout/pastes/paste_20260520_145959_abc123.txt"). The path is suitable for insertion as a `@path` reference the agent can read. SP-048-4d. Mirrors SavePastedImage.
func SetColorBlind ¶ added in v0.16.19
SetColorBlind enables or disables the color-blind palette swap. Returns the previous value so callers (tests) can restore it.
func SetGlobalMetricsRecorderForTest ¶ added in v0.16.19
func SetGlobalMetricsRecorderForTest(r *MetricsRecorder) func()
SetGlobalMetricsRecorderForTest installs a recorder for the duration of a test. Returns a cleanup func that restores the previous recorder.
func SetScrollRegionSeq ¶
SetScrollRegionSeq returns the escape sequence to set the scrolling region (1-based, inclusive).
func ShouldSmartSavePaste ¶
ShouldSmartSavePaste reports whether content is large enough to merit auto-saving via SavePastedText. Threshold-only helper so the caller can short-circuit without computing both counters at every call site.
func ShowCursorSeq ¶
func ShowCursorSeq() string
ShowCursorSeq returns the escape sequence to show the cursor.
func SinkFromPrintf ¶
func SinkFromPrintf() func(string)
SinkFromPrintf is a convenience that writes to stdout via fmt.Print. It exists so callsites don't have to spell out the closure.
func StderrIsTerminal ¶
func StderrIsTerminal() bool
StderrIsTerminal returns true if os.Stderr is connected to a terminal.
func StdoutIsTerminal ¶
func StdoutIsTerminal() bool
StdoutIsTerminal returns true if os.Stdout is connected to a terminal.
func StopGlobalStatusFooter ¶
func StopGlobalStatusFooter()
StopGlobalStatusFooter resets the registered global footer's scroll region and clears its row. Safe to call when no footer is registered or when it's already stopped (no-op). Use from signal handlers immediately before os.Exit so the user's terminal isn't left in a weird state.
func SuspendIndicator ¶
func SuspendIndicator()
SuspendIndicator stops the registered global activity indicator if one is active. Safe to call when no indicator is registered (no-op) or when the indicator is already stopped (idempotent). Use this immediately before rendering an interactive CLI prompt to keep the spinner from overwriting the prompt text. Mirrored by clihooks.SuspendIndicator for callers that can't import pkg/console.
func TryLockOutput ¶ added in v0.16.17
func TryLockOutput() bool
TryLockOutput attempts to acquire the console output mutex without blocking. Returns true if the lock was acquired, false if it is held by another goroutine. Callers MUST check the return value and only call UnlockOutput on true.
func UnlockOutput ¶ added in v0.16.7
func UnlockOutput()
UnlockOutput releases the console output mutex.
func WithOutput ¶ added in v0.16.7
func WithOutput(fn func())
WithOutput runs fn while holding the console output mutex. Use this wrapper for short, self-contained ANSI render sequences.
func WrapHardLine ¶ added in v0.16.18
WrapHardLine splits a single line of text into visual rows that fit within `cols` terminal columns, breaking on rune boundaries and accounting for wide (CJK) runes via runewidth. An empty input yields one empty row so hard-line counts map cleanly to visual-line counts.
Unlike strings.Split, this is width-aware: a rune that won't fit in the remaining columns starts a new row, matching how the terminal renders it. Combining runes (width 0) are accumulated without breaking.
func WrapSteerLayout ¶ added in v0.16.18
func WrapSteerLayout(text string, cursorByte, cols, maxRows int) (lines []string, cursorRow, cursorCol int)
WrapSteerLayout is the width-aware layout helper for the steer panel. It takes a steer text (already includes any prefix) and a byte cursor position within it, and returns:
- lines: visual rows after hard-break (\n) split + soft wrap (cols)
- cursorRow, cursorCol: 0-based (row, col) within `lines` for the given byte cursor position
When the total visual row count exceeds maxRows, the topmost rows are dropped and the first visible row is prefixed with "… " so the caret row stays visible. cursorRow is clamped into the visible range and cursorCol is clamped into [0, len(lines[cursorRow])].
The mapping walks `text` once with the same wrap semantics as WrapHardLine + \n-as-hard-break, so byte→(row,col) is exact for any byte index within the string. This is the same model WrappedGeometry in input_render.go uses (with explicit columns), but bounded by maxRows.
func WriteWrappedLines ¶ added in v0.16.18
WriteWrappedLines writes `lines` to `w`, each padded to `cols` columns with spaces (or truncated with "…" if over cols). No trailing newline — the caller controls layout.
When withCursorRow >= 0, a visible caret (▏) is inserted into lines[withCursorRow] at byte column cursorCol. The caret counts against the cols budget, so the surrounding text is truncated if needed.
Types ¶
type ActivityIndicator ¶
type ActivityIndicator struct {
// contains filtered or unexported fields
}
ActivityIndicator renders a transient single-line spinner that updates on a timer. It's designed for the "agent is doing something" gap between user submit and visible output, and for showing per-tool progress during tool execution.
All output goes to a Writer (default os.Stderr). When the writer is not a TTY, all methods are no-ops so piped or redirected output stays clean.
The zero value is unusable — construct via NewActivityIndicator.
func NewActivityIndicator ¶
func NewActivityIndicator(w io.Writer) *ActivityIndicator
NewActivityIndicator constructs an indicator that writes to w. If w is nil, os.Stderr is used. TTY detection runs against the underlying file descriptor; when w is not an *os.File it is treated as not-a-TTY.
func (*ActivityIndicator) ClearStatic ¶ added in v0.16.18
func (a *ActivityIndicator) ClearStatic()
ClearStatic removes the static text from the indicator row, clearing the line. No-op when not a TTY or when no static text is set.
func (*ActivityIndicator) Elapsed ¶
func (a *ActivityIndicator) Elapsed() time.Duration
Elapsed returns how long the current spinner has been running. Returns zero if the indicator is not active.
func (*ActivityIndicator) IsActive ¶
func (a *ActivityIndicator) IsActive() bool
IsActive reports whether the spinner is currently rendering.
func (*ActivityIndicator) IsTTY ¶ added in v0.16.18
func (a *ActivityIndicator) IsTTY() bool
IsTTY reports whether the indicator's writer is a TTY.
func (*ActivityIndicator) Replace ¶
func (a *ActivityIndicator) Replace(line string)
Replace atomically stops the spinner and prints line in its place, terminated with a newline. Use this when a transient spinner should resolve into a permanent result line (e.g. ✓ tool · 0.3s).
On a non-TTY writer, line is still printed (so non-interactive logs still see the resolved result).
func (*ActivityIndicator) ReplaceLast ¶
func (a *ActivityIndicator) ReplaceLast(line string)
ReplaceLast is shorthand for ReplaceLastN(line, 1).
func (*ActivityIndicator) ReplaceLastN ¶
func (a *ActivityIndicator) ReplaceLastN(line string, n int)
ReplaceLastN stops the spinner and OVERWRITES the previous N rows before printing line. Used by the tool-collapse subscriber to merge a series of identical tool-end lines (separated by spinner-frame blank rows) into a single "✓ read_file × N (foo.go, bar.go, …)" line updated in place.
Caller is responsible for knowing N matches the actual row layout:
- n=1: overwrites the immediately preceding row (e.g. a spinner)
- n=2: overwrites the prev row + the blank line above it (the pattern emitted between consecutive tool spinners in the CLI's ToolStart path)
Only safe to call when the caller knows those rows belong to this indicator — no streaming text or unrelated chrome has written there since. On a non-TTY writer this degenerates to a regular Fprintln so logs still show each iteration (slightly noisier but never corrupted).
func (*ActivityIndicator) SetStatic ¶ added in v0.16.18
func (a *ActivityIndicator) SetStatic(line string)
SetStatic pins a non-animated line to the indicator row. It stops any running spinner and replaces it with static text. The text updates in place on subsequent calls until ClearStatic is used. No-op when not a TTY.
func (*ActivityIndicator) Start ¶
func (a *ActivityIndicator) Start(msg string)
Start begins rendering the spinner with the given message. If the indicator is already active, the message is updated in place and the spinner continues from its current frame.
msg should be a single line; embedded newlines and carriage returns are stripped to keep the render loop on one row.
func (*ActivityIndicator) Stop ¶
func (a *ActivityIndicator) Stop()
Stop halts the ticker and erases the spinner line. Idempotent — safe to call when the indicator is already stopped. When the indicator is already fully idle (no spinner, no static text), Stop is a true no-op and writes nothing to the terminal, so redundant calls from hot loops (e.g. the streaming callback calling Stop on every prose chunk) never clobber the current row. Never blocks for more than 500ms — if the render goroutine is stuck (e.g., outputMu held by a blocked write on a saturated PTY), Stop returns without waiting for it, avoiding a cascade deadlock that freezes the entire terminal.
func (*ActivityIndicator) Update ¶
func (a *ActivityIndicator) Update(msg string)
Update changes the spinner's message without restarting it. No-op if the indicator is not currently active.
type AssistantTurnRenderer ¶
type AssistantTurnRenderer struct {
// contains filtered or unexported fields
}
AssistantTurnRenderer wraps the streaming-callback path for one assistant turn. It formats each line of assistant prose with markdown (ANSI colors, syntax highlighting, tables, code blocks) AS IT STREAMS, before the text reaches the terminal. This replaces the old clear-and-reprint approach (which was disabled because the formatted output's row count never matched the streamed text, causing cursor clobbering).
Lines are buffered until a newline arrives, then passed through the StreamingMarkdownFormatter and emitted with the configured indent. This gives ~1 line of latency (imperceptible) but eliminates all cursor manipulation — clobbering is structurally impossible.
A "segment" is a contiguous run of stream chunks with no interleaved non-prose terminal output between them. Tool logs and any other writeTerminalMessage call must notify the renderer via OnExternalWrite to finalize the current segment and start a fresh one.
func NewAssistantTurnRenderer ¶
func NewAssistantTurnRenderer(width int, formatter *MarkdownFormatter) *AssistantTurnRenderer
NewAssistantTurnRenderer constructs a renderer with the given terminal width snapshot and markdown formatter. When the formatter has colors enabled, a StreamingMarkdownFormatter is created for per-line formatting. width <= 0 disables soft-wrap accounting; the indent still works.
func (*AssistantTurnRenderer) CursorOnFreshRow ¶ added in v0.16.18
func (r *AssistantTurnRenderer) CursorOnFreshRow() bool
CursorOnFreshRow reports whether the renderer is currently sitting at the start of an untouched row (column 0, no in-progress text). True after endReasoningLocked (which advances past the summary's \n) and after each completed newline in WriteChunk. Used by the CLI's streaming callback to decide whether to inject a separator \n before the first prose chunk: when false, the cursor is mid-line (the indicator's cleared residue) and the \n is required to escape it; when true, the cursor is already on a fresh row and the \n would add a spurious blank line — notably when reasoning ran first.
func (*AssistantTurnRenderer) EndReasoning ¶ added in v0.16.2
func (r *AssistantTurnRenderer) EndReasoning()
EndReasoning is the exported counterpart of endReasoningLocked for callers that drive the lifecycle directly (e.g. an explicit "end of thinking" event). The CLI today doesn't need it — WriteChunk and FinalizeAtTurnEnd both call the locked form — but it's exposed for completeness and tests.
func (*AssistantTurnRenderer) FinalizeAtTurnEnd ¶
func (r *AssistantTurnRenderer) FinalizeAtTurnEnd()
FinalizeAtTurnEnd is called once the assistant's turn has completed (after the spinner stops, after any post-turn book-keeping). It flushes any remaining partial line through the formatter and ensures a trailing newline so the cursor lands on a fresh row for the next turn's output.
func (*AssistantTurnRenderer) OnExternalWrite ¶
func (r *AssistantTurnRenderer) OnExternalWrite()
OnExternalWrite finalizes the current segment without re-rendering it. Wire this into the OutputRouter's writeTerminalMessage so that tool-log lines, agent messages, and any other non-prose terminal output break the prose segment cleanly. A fresh segment begins on the next WriteChunk.
func (*AssistantTurnRenderer) OnExternalWriteRows ¶ added in v0.16.18
func (r *AssistantTurnRenderer) OnExternalWriteRows(n int)
OnExternalWriteRows finalizes the current segment and advances physicalLines by `n` rows to account for external writes that consumed terminal rows (e.g. a blank-line separator or a multi-line todo block). This keeps the renderer's state in sync at segment boundaries.
When n == 0 the segment is still reset (same as OnExternalWrite). When n > 0 the renderer treats the external write as if it had emitted n newline-terminated rows: physicalLines advances, the cursor is considered at the start of a fresh row, and the segment buffer resets.
func (*AssistantTurnRenderer) ReasoningActive ¶ added in v0.16.19
func (r *AssistantTurnRenderer) ReasoningActive() bool
ReasoningActive reports whether a reasoning header ("▽ Thinking…") is currently printed on the renderer's row waiting to be finalized in place by endReasoningLocked. The streaming callback uses this to suppress the separator \n on the first prose chunk: when reasoning is active, the cursor is mid-line on the header row, and endReasoningLocked will rewrite that exact row via \r\033[K. Injecting a \n first would advance past the header row, leaving "▽ Thinking…" orphaned and placing the summary on the wrong row.
func (*AssistantTurnRenderer) SetFooter ¶ added in v0.16.18
func (r *AssistantTurnRenderer) SetFooter(f *StatusFooter)
SetFooter wires the status footer so the renderer can suppress its refresh during active prose streaming — the root cause of the "scattered characters" clobbering symptom (DEC save/restore cursor races with scroll-region content).
func (*AssistantTurnRenderer) WriteChunk ¶
func (r *AssistantTurnRenderer) WriteChunk(chunk string)
WriteChunk emits a chunk of assistant text to stdout, formatting each complete line with markdown before it reaches the terminal. Text is buffered until a newline arrives, then passed through the StreamingMarkdownFormatter and emitted with the configured indent.
When colors are disabled (streamFmt == nil), falls back to raw emit: each line gets the indent but no formatting.
func (*AssistantTurnRenderer) WriteReasoningChunk ¶ added in v0.16.2
func (r *AssistantTurnRenderer) WriteReasoningChunk(chunk string)
WriteReasoningChunk consumes one chunk of reasoning/thinking output from the streaming pipeline and renders the collapsed form. On the FIRST chunk of a reasoning segment it prints a single dim "▽ Thinking…" header; on subsequent chunks it only accumulates the byte count so the terminal stays clean even when the model emits tens of KiB of internal monologue. The header is finalized into "▽ Thinking · N kB (~N tokens)" by the next prose chunk (via WriteChunk) or by FinalizeAtTurnEnd.
The header is printed WITHOUT a trailing newline so that endReasoningLocked can rewrite it in-place on the same row using `\r\033[K` + summary + `\n`. This avoids DEC save/restore (`\0337`/`\0338`) entirely — those sequences collide with concurrent writers (activity indicator, status footer, InputReader) that use `\r\033[K` and can corrupt the cursor position on many terminals.
No-op when the chunk is empty. Safe to call concurrently with other renderer methods — internal mutex guards the state.
type CIOutputHandler ¶
type CIOutputHandler struct {
// contains filtered or unexported fields
}
CIOutputHandler manages output formatting for CI/non-interactive environments
func NewCIOutputHandler ¶
func NewCIOutputHandler(writer io.Writer) *CIOutputHandler
NewCIOutputHandler creates a new CI output handler
func (*CIOutputHandler) IsCI ¶
func (h *CIOutputHandler) IsCI() bool
IsCI returns true if running in CI environment
func (*CIOutputHandler) IsInteractive ¶
func (h *CIOutputHandler) IsInteractive() bool
IsInteractive returns true if running in an interactive terminal
func (*CIOutputHandler) PrintProgress ¶
func (h *CIOutputHandler) PrintProgress()
PrintProgress prints a progress update in CI-friendly format
func (*CIOutputHandler) PrintSummary ¶
func (h *CIOutputHandler) PrintSummary()
PrintSummary prints a final summary
func (*CIOutputHandler) Printf ¶
func (h *CIOutputHandler) Printf(format string, args ...interface{})
Printf writes formatted output
func (*CIOutputHandler) ShouldShowProgress ¶
func (h *CIOutputHandler) ShouldShowProgress() bool
ShouldShowProgress returns true if progress should be shown
func (*CIOutputHandler) UpdateMetrics ¶
func (h *CIOutputHandler) UpdateMetrics(totalTokens, contextTokens, maxContextTokens, iteration int, totalCost float64)
UpdateMetrics updates the tracked metrics
func (*CIOutputHandler) Write ¶
func (h *CIOutputHandler) Write(p []byte) (n int, err error)
Write implements io.Writer interface
func (*CIOutputHandler) WriteString ¶
func (h *CIOutputHandler) WriteString(s string) error
WriteString writes a string with appropriate formatting
type CompletionCycle ¶ added in v0.16.18
type CompletionCycle struct {
// contains filtered or unexported fields
}
CompletionCycle tracks an in-progress completion cycle. When the user presses the completion binding repeatedly without typing other characters, we step through candidates in order. When the buffer changes (typing, arrow keys, etc.), the next binding press starts a fresh cycle automatically because lastApplied no longer matches the current line.
This struct is intentionally reader-agnostic; both InputReader and SteerInputReader embed it via a pointer field. CycleCompletion drives the state machine.
func (*CompletionCycle) Advance ¶ added in v0.16.18
func (c *CompletionCycle) Advance(applied string)
Advance records that `applied` was just applied to the buffer, so the next CycleCompletion call with the same `line` knows to advance through candidates instead of starting over.
func (*CompletionCycle) Reset ¶ added in v0.16.18
func (c *CompletionCycle) Reset()
Reset clears cycle state. Call this whenever the user edits the buffer (typing, arrow keys, paste, history recall) so the next completion press starts fresh from the new buffer content.
type CompletionProvider ¶
CompletionProvider returns candidate completions for the current input state. It receives the current line and cursor position and should return a list of full-line replacements ordered by likelihood. An empty result means "no completion available."
Shared by InputReader and SteerInputReader (SP-078 Phase 2). The implementation in pkg/console/input_completion.go predates this split; this file keeps the type definition so the same provider can be installed on either reader.
type ContentSource ¶
type ContentSource interface {
Model() string
ContextTokens() (used, limit int)
TotalCost() float64
WorkingDir() string
}
ContentSource supplies the current values rendered in the footer. The footer reads from it on every Refresh; the source must be safe for concurrent calls.
type ContextMenu ¶
type ContextMenu struct {
Items []*ContextMenuItem
Selected int
Visible bool
Row int // Screen row where menu appears
Col int // Screen column where menu appears
Width int // Menu width in characters
Height int // Menu height in lines
OnSelect func(item *ContextMenuItem)
OnEscape func()
}
ContextMenu represents the right-click context menu
func (*ContextMenu) AddItem ¶
func (cm *ContextMenu) AddItem(id, label, description, shortcut string, enabled bool, subMenu ...[]*ContextMenuItem)
AddItem adds a menu item to the context menu
func (*ContextMenu) ClearItems ¶
func (cm *ContextMenu) ClearItems()
ClearItems removes all items from the menu
func (*ContextMenu) NavigateDown ¶
func (cm *ContextMenu) NavigateDown()
NavigateDown moves selection down
func (*ContextMenu) Render ¶
func (cm *ContextMenu) Render()
Render draws the context menu to the terminal
func (*ContextMenu) SelectCurrent ¶
func (cm *ContextMenu) SelectCurrent() *ContextMenuItem
SelectCurrent selects the currently highlighted item
func (*ContextMenu) SetPosition ¶
func (cm *ContextMenu) SetPosition(row, col int)
SetPosition sets the screen position for the menu
type ContextMenuItem ¶
type ContextMenuItem struct {
ID string
Label string
Description string
Shortcut string
Enabled bool
SubMenu []*ContextMenuItem
}
ContextMenuItem represents a single item in the context menu
type EditReviewResult ¶ added in v0.16.12
type EditReviewResult struct {
AcceptedHunks []string // hunk IDs the user accepted
Rejected bool // true if user chose reject-all
Edited string // non-empty if user edited content via $EDITOR
}
EditReviewResult captures the user's decisions from the CLI diff review.
func RenderEditReview ¶ added in v0.16.12
func RenderEditReview(w io.Writer, hunks []ReviewHunk) EditReviewResult
RenderEditReview displays all hunks with colored diffs and returns the user's per-hunk decisions. Non-interactive callers (no TTY) get approve-all automatically.
The prompt for each hunk is:
[a]ccept / [r]eject / [s]kip (default: accept)
At the end, a summary shows accepted vs rejected counts.
type EscapeParser ¶
type EscapeParser struct {
// contains filtered or unexported fields
}
EscapeParser handles escape sequences using a simple state machine
func NewEscapeParser ¶
func NewEscapeParser() *EscapeParser
NewEscapeParser creates a new escape sequence parser
func (*EscapeParser) Parse ¶
func (ep *EscapeParser) Parse(b byte) *InputEvent
Parse processes a byte and returns an event if complete
type FooterTooltip ¶ added in v0.16.19
type FooterTooltip struct {
// keystroke dismisses it. Zero means "no auto-dismiss"; the
// InputReader's HandleEvent hook is the primary dismiss path.
Timeout time.Duration
// global MetricsRecorder if nil. Set via SetSource.
Source func() []ToolInvocation
// goroutine stops cleanly. Wired by Show.
Cancel func()
// contains filtered or unexported fields
}
FooterTooltip renders a transient multi-line breakdown above the status footer when toggled via Alt+T. It is the CLI-D surface: a power-user shortcut that shows the same data /stats renders, but without leaving the REPL or losing in-progress input.
Lifecycle:
- Show: clear the footer row, render a multi-row block above it, suppress until either 5 s elapses (Timeout) or any keystroke fires (the InputReader auto-dismisses via its HandleEvent hook).
- Hide: erase the rendered rows, redraw the footer at its canonical row.
The tooltip is rendered relative to the footer's terminal size; on non-TTY writers it is a no-op (same as the footer itself).
func NewFooterTooltip ¶ added in v0.16.19
func NewFooterTooltip(w io.Writer) *FooterTooltip
NewFooterTooltip constructs a tooltip that writes to w.
func (*FooterTooltip) Hide ¶ added in v0.16.19
func (t *FooterTooltip) Hide()
Hide erases the rendered tooltip block and restores the footer row. Idempotent — safe to call when not visible.
func (*FooterTooltip) Show ¶ added in v0.16.19
func (t *FooterTooltip) Show(cols, rows int)
Show renders the tooltip above the status footer. cols and rows are the terminal size; the tooltip occupies the rows immediately above row N (the footer rule).
func (*FooterTooltip) Toggle ¶ added in v0.16.19
func (t *FooterTooltip) Toggle(cols, rows int)
Toggle is the Alt+T handler: shows if hidden, hides if visible.
func (*FooterTooltip) Visible ¶ added in v0.16.19
func (t *FooterTooltip) Visible() bool
Visible reports whether the tooltip is currently rendered.
type Glyph ¶
type Glyph int
Glyph encodes a single semantic category for CLI status lines. Every output line that announces a status (success, error, warning, progress, …) should pick exactly one of these. The rendered prefix is `<glyph> ` in the canonical color; consistency makes the scroll region scannable at a glance — green ticks = good, red marks = problems, amber = needs attention.
Honors NO_COLOR / FORCE_COLOR via envutil.ResolveColorPreference. In no-color mode the glyph still renders (it's UTF-8, not ANSI) so the semantic is preserved; only the color escape is suppressed.
const ( // GlyphSuccess marks a completed action / success state. // Replaces: [OK], [clean], [done] GlyphSuccess Glyph = iota // GlyphError marks a failure / error state. // Replaces: [FAIL] GlyphError // GlyphWarning marks something that needs attention but is non-fatal. // Replaces: [WARN], [skip] (some) GlyphWarning // GlyphInfo marks a system / informational message. // Replaces: [bot], [web], [skills], [chart] (welcome banner uses) GlyphInfo // GlyphAction marks an action in flight / submitted. // Replaces: [tool], [chart] (progress uses), [RELOAD] GlyphAction // GlyphPaused marks paused / queued state — waiting for something. // Replaces: [||] (interrupting), [queued] GlyphPaused // GlyphStopped marks a stopped / interrupted / aborted state. // Replaces: [STOP], [!] GlyphStopped // GlyphShell marks a shell command being executed. Replaces the // bare "$ " prompt prefix in agent_workflow_runner and similar // sites. Distinct from GlyphAction (which uses →) so power users // can grep / scroll for shell output specifically. // // CLI-F-3: explicit constant for the shell-prompt glyph. GlyphShell // GlyphDim marks secondary / continuation / metric lines that // shouldn't draw the eye. Replaces: [skip] (some), [debug] GlyphDim )
func (Glyph) Fprintln ¶
Fprintln writes the glyph-prefixed message to an explicit writer. Tests use this to capture output to a buffer.
func (Glyph) Prefix ¶
Prefix returns the colored glyph plus a single trailing space, ready to lead a line:
fmt.Fprintf(os.Stderr, "%sresumed: %s\n", console.GlyphSuccess.Prefix(), label) → ✓ resumed: foo
In no-color mode the glyph still appears (the color escape is just empty). The reset escape only emits when a color was emitted.
func (Glyph) Print ¶
Print writes "<glyph> <msg>\n" to stderr. Convenience for the most common call shape. Use Printf for format-string callers, or Fprintln/Fprintf if you need to target a specific writer (tests).
type GroundTruthTermios ¶ added in v0.16.1
type GroundTruthTermios struct {
// contains filtered or unexported fields
}
GroundTruthTermios holds a pristine snapshot of the terminal's termios state taken once at REPL startup — before any raw-mode / steer-mode manipulation. All subsequent mode transitions should restore toward this snapshot (not toward whatever state the previous mode happened to save), preventing termios-state descent across PauseSteer / ResumeSteer cycles.
func CaptureGroundTruth ¶ added in v0.16.1
func CaptureGroundTruth() *GroundTruthTermios
CaptureGroundTruth snapshots the current termios of stdin. Call once at REPL startup when the terminal is in its default cooked state. Returns nil on non-TTY or error (callers degrade gracefully).
If the captured state is not actually cooked (some bootstrap path left the terminal in raw mode before we got here), the snapshot is normalized so the cooked flags we depend on are forced on. Without this, every later Restore() would set ICANON-off and the recovery mechanism would be self-defeating.
func (*GroundTruthTermios) EnsureCooked ¶ added in v0.16.2
func (g *GroundTruthTermios) EnsureCooked()
EnsureCooked unconditionally writes a known-cooked termios derived from the ground-truth snapshot. Unlike EnsureSane, it does not first check whether the terminal "looks" sane — ICANON alone doesn't catch every way input can be broken (VMIN=0 leftover from steer mode, IXOFF stop, missing OPOST). Call at the top of every ReadLine to guarantee a clean baseline before MakeRaw saves the to-be-restored state.
func (*GroundTruthTermios) EnsureSane ¶ added in v0.16.1
func (g *GroundTruthTermios) EnsureSane() bool
EnsureSane restores the ground-truth state if the terminal appears stuck in raw mode (ICANON off when we expect cooked). Returns true if a restore was performed.
func (*GroundTruthTermios) Fd ¶ added in v0.16.1
func (g *GroundTruthTermios) Fd() int
Fd returns the file descriptor the ground truth was captured from.
func (*GroundTruthTermios) IsTerminalSane ¶ added in v0.16.1
func (g *GroundTruthTermios) IsTerminalSane() bool
IsTerminalSane checks whether the terminal is currently in an ICANON-on state (i.e. cooked mode). Returns false when the terminal appears stuck in raw / steer mode. Returns true on non-TTY (nothing to check) or when the terminal is healthy.
func (*GroundTruthTermios) Restore ¶ added in v0.16.1
func (g *GroundTruthTermios) Restore() error
Restore resets the terminal to the ground-truth state. Returns an error if the ioctl fails (caller can log and continue — a failed restore is not fatal, just means the terminal might be in a weird mode).
type InputEvent ¶
type InputEvent struct {
Type InputEventType
Data string
}
InputEvent represents a key press or input event
type InputEventType ¶
type InputEventType int
const ( EventChar InputEventType = iota EventUp EventDown EventLeft EventRight EventHome EventEnd EventBackspace EventDelete EventEnter EventTab EventInterrupt EventSuspend EventEscape EventPasteStart EventPasteEnd // Mouse events EventMouse EventWordLeft EventWordRight EventDeleteWordBackward // EventAltLetter is fired for Alt-modified letters not already // claimed by a more specific event (Alt+B / Alt+F / Alt+Backspace). // The letter is in .Data as a single byte (e.g. "T" for Alt+T). // CLI-D uses this to drive the status-footer tooltip toggle. EventAltLetter )
type InputReader ¶
type InputReader struct {
// contains filtered or unexported fields
}
InputReader handles interactive input with proper escape sequence handling
func NewInputReader ¶
func NewInputReader(prompt string) *InputReader
NewInputReader creates a new input reader
func (*InputReader) AddToHistory ¶
func (ir *InputReader) AddToHistory(command string)
AddToHistory adds a command to history
func (*InputReader) Backspace ¶
func (ir *InputReader) Backspace()
Backspace deletes the character before the cursor
func (*InputReader) Delete ¶
func (ir *InputReader) Delete()
Delete deletes the character at the cursor position
func (*InputReader) DeleteWordBackward ¶ added in v0.16.12
func (ir *InputReader) DeleteWordBackward()
DeleteWordBackward deletes the word before the cursor (Ctrl-W / Meta-Backspace).
func (*InputReader) GetHistory ¶
func (ir *InputReader) GetHistory() []string
GetHistory returns the command history
func (*InputReader) HandleEvent ¶
func (ir *InputReader) HandleEvent(event *InputEvent)
HandleEvent processes an input event
func (*InputReader) InsertChar ¶
func (ir *InputReader) InsertChar(char string)
InsertChar inserts a character string at the current cursor position.
func (*InputReader) KillToEndOfLine ¶ added in v0.16.12
func (ir *InputReader) KillToEndOfLine()
KillToEndOfLine deletes from the cursor to the end of the line (Ctrl-K).
func (*InputReader) KillToStartOfLine ¶ added in v0.16.12
func (ir *InputReader) KillToStartOfLine()
KillToStartOfLine deletes from the start of the line to the cursor (Ctrl-U).
func (*InputReader) MoveCursor ¶
func (ir *InputReader) MoveCursor(delta int)
MoveCursor moves the cursor left or right
func (*InputReader) MoveWord ¶ added in v0.16.12
func (ir *InputReader) MoveWord(direction int)
MoveWord moves the cursor by one word in the given direction (-1 backward / Alt-B / Ctrl-Left, +1 forward / Alt-F / Ctrl-Right). A word is a maximal run of non-whitespace (unicode.IsSpace).
func (*InputReader) NavigateHistory ¶
func (ir *InputReader) NavigateHistory(direction int)
NavigateHistory navigates through command history
func (*InputReader) NavigateVertically ¶
func (ir *InputReader) NavigateVertically(direction int)
NavigateVertically handles both history navigation and multi-line text navigation direction: -1 for up, 1 for down
func (*InputReader) ReadLine ¶
func (ir *InputReader) ReadLine() (string, error)
ReadLine reads a line of input with proper escape sequence handling
func (*InputReader) Refresh ¶
func (ir *InputReader) Refresh()
Refresh redraws the current input line
func (*InputReader) SetCompleter ¶
func (ir *InputReader) SetCompleter(c CompletionProvider)
SetCompleter installs a completion provider that is invoked when the user presses Tab. Pass nil to disable completion. The provider receives the current buffer + cursor position and returns ordered candidate replacements.
func (*InputReader) SetCursor ¶
func (ir *InputReader) SetCursor(pos int)
SetCursor sets the cursor to an absolute position
func (*InputReader) SetFooterTooltip ¶ added in v0.16.19
func (ir *InputReader) SetFooterTooltip(t *FooterTooltip)
SetFooterTooltip installs the tooltip controller invoked by Alt+T. Pass nil to disable the keybinding entirely. The default controller writes to os.Stderr.
func (*InputReader) SetGroundTruth ¶ added in v0.16.1
func (ir *InputReader) SetGroundTruth(gt *GroundTruthTermios)
SetGroundTruth installs the terminal's pristine cooked-mode termios snapshot for pre-flight sanity checks. Call once at REPL startup, before the first ReadLine.
func (*InputReader) SetHistory ¶
func (ir *InputReader) SetHistory(history []string)
SetHistory sets the command history
func (*InputReader) SetInitialContent ¶ added in v0.16.1
func (ir *InputReader) SetInitialContent(content string)
SetInitialContent pre-fills the input buffer with text that should appear as if the user typed it. Used by the REPL loop to carry over unsent steer-panel text into the main prompt after a turn ends. The content is consumed on the next ReadLine call and then cleared.
func (*InputReader) SetPrompt ¶
func (ir *InputReader) SetPrompt(p string)
SetPrompt updates the input reader's prompt prefix. Call this between ReadLine invocations (not during) to reflect state changes such as the active model name after a `/model` switch. SP-048-5d follow-up.
type KeymapEntry ¶ added in v0.16.19
type KeymapEntry struct {
// Key is the user-facing combo, e.g. "Alt+T". Used for /help
// documentation; the dispatch path uses Action instead.
Key string
// Action is the internal name, e.g. "footer.tooltip.toggle".
Action string
// Description is the /help blurb.
Description string
// Handler runs on each match. Called synchronously in the REPL
// goroutine; long-running work should be dispatched elsewhere.
Handler func()
}
KeymapEntry is one row in the keymap table.
type KeymapRegistry ¶ added in v0.16.19
type KeymapRegistry struct {
// contains filtered or unexported fields
}
KeymapRegistry is a process-wide table that maps an "Alt+<letter>" keypress to a named action plus a callback. The InputReader consults the registry on every EventAltLetter; the callback runs synchronously in the read-loop goroutine.
This is the canonical place to wire stateless, non-prefix keybindings. For bindings that conflict with text input (e.g. anything that shouldn't fire while the user is typing inside the steer panel) register an Action with a Guard that returns false.
CLI-D-3: this is the keymap table the TODO references. Built as a thread-safe registry because both the REPL goroutine and any configuration / setup code may register handlers.
func GlobalKeymap ¶ added in v0.16.19
func GlobalKeymap() *KeymapRegistry
GlobalKeymap returns the process-wide registry, creating it on first use. Returns the same pointer for the rest of the process lifetime.
func (*KeymapRegistry) Dispatch ¶ added in v0.16.19
func (r *KeymapRegistry) Dispatch(action string) bool
Dispatch invokes the handler for the given action if registered. Returns true if a handler fired. Safe to call from any goroutine — it takes only a read lock during lookup and releases before invoking the handler.
func (*KeymapRegistry) Entries ¶ added in v0.16.19
func (r *KeymapRegistry) Entries() []KeymapEntry
Entries returns a snapshot of all entries in registration order. Used by /help to render the binding table.
func (*KeymapRegistry) Lookup ¶ added in v0.16.19
func (r *KeymapRegistry) Lookup(action string) (KeymapEntry, bool)
Lookup returns the entry for action, or false if not registered.
func (*KeymapRegistry) MatchAltLetter ¶ added in v0.16.19
func (r *KeymapRegistry) MatchAltLetter(letter string) (KeymapEntry, bool)
MatchAltLetter looks up an action by Alt+<letter> binding. The keymap is small so we just scan Entries; the alternative (a second map) would double the registration bookkeeping for no measurable win.
func (*KeymapRegistry) Register ¶ added in v0.16.19
func (r *KeymapRegistry) Register(entry KeymapEntry)
Register adds (or replaces) an entry keyed by Action. Action is the idempotent identifier so multiple Register calls with the same Action don't pile up; the most recent wins, and its Handler replaces the previous one. Key + Description are overwritten similarly.
type LineCapWriter ¶
type LineCapWriter struct {
// CharLimit is the maximum characters allowed on one line before
// truncation kicks in. Zero or negative disables capping.
CharLimit int
// Sink receives all bytes that are not clipped.
Sink func(string)
// contains filtered or unexported fields
}
LineCapWriter wraps a sink (typically the streaming-output `fmt.Print`) and clamps each *visual* line to a maximum character count. When a line exceeds the cap, the writer emits the head, then a single `… [+N chars]` truncation marker, then swallows further bytes on that line until the next newline.
This exists because the LLM sometimes streams a tool result that contains a single very long line (minified JS/JSON, a base64 blob, an unbroken log line) and terminals soft-wrap that into hundreds of visual rows, blowing up the user's scrollback. The LLM's view of the content is unchanged — only what the terminal renders is clipped.
Not goroutine-safe; callers are expected to serialize writes (the streaming callback path naturally does this — one chunk at a time from a single producer).
func NewLineCapWriter ¶
func NewLineCapWriter(charLimit int, sink func(string)) *LineCapWriter
NewLineCapWriter constructs a writer with the given char limit and downstream sink. Use SinkFromPrintf for the typical stdout case.
func (*LineCapWriter) Flush ¶
func (w *LineCapWriter) Flush()
Flush is intended for end-of-stream cleanup. If the stream ends without a trailing newline on a suppressed line, the marker still needs to land so the user sees what was dropped.
func (*LineCapWriter) Write ¶
func (w *LineCapWriter) Write(chunk string)
Write processes one streaming chunk. The chunk can contain any mix of newlines and long runs; the writer tracks position across calls.
type MarkdownFormatter ¶
type MarkdownFormatter struct {
// contains filtered or unexported fields
}
MarkdownFormatter converts markdown to ANSI-colored terminal output
func NewMarkdownFormatter ¶
func NewMarkdownFormatter(enableColors, enableInline bool) *MarkdownFormatter
NewMarkdownFormatter creates a new markdown formatter.
The caller's enableColors preference is overridden by the environment per the no-color.org convention (SP-048-4a):
- NO_COLOR set to any non-empty value → colors OFF (always wins)
- FORCE_COLOR set to any non-empty value → colors ON (unless NO_COLOR)
This lets users opt out of ANSI escapes globally (`NO_COLOR=1 sprout`) and CI pipelines opt in (`FORCE_COLOR=1 sprout > log.txt`) without individual call sites needing to know. The resolver lives in pkg/envutil (a zero-dep leaf) to avoid the import cycle pkg/utils → pkg/console.
func (*MarkdownFormatter) Format ¶
func (f *MarkdownFormatter) Format(text string) string
Format formats markdown text to colored terminal output
func (*MarkdownFormatter) SetWidth ¶ added in v0.16.8
func (f *MarkdownFormatter) SetWidth(w int) *MarkdownFormatter
SetWidth sets the content width used for width-aware rendering (e.g. the horizontal rule spans this many columns). Returns the receiver for chaining.
type MetricsRecorder ¶ added in v0.16.19
type MetricsRecorder struct {
// contains filtered or unexported fields
}
MetricsRecorder aggregates per-tool invocation stats. Process-wide instance is exposed via GlobalMetricsRecorder; tests can build their own with NewMetricsRecorder for isolation.
func GlobalMetricsRecorder ¶ added in v0.16.19
func GlobalMetricsRecorder() *MetricsRecorder
GlobalMetricsRecorder returns the process-wide recorder, creating one if init hasn't run yet (defensive — init above normally sets it).
func NewMetricsRecorder ¶ added in v0.16.19
func NewMetricsRecorder() *MetricsRecorder
NewMetricsRecorder constructs an empty recorder with started=now.
func (*MetricsRecorder) RecordToolInvocation ¶ added in v0.16.19
func (m *MetricsRecorder) RecordToolInvocation(name string, tokens int64, costUSD float64, latencyMicros int64)
RecordToolInvocation accumulates one observation. tokens and costUSD are per-invocation (not cumulative); the recorder adds them. latency is the per-invocation latency in microseconds.
func (*MetricsRecorder) Snapshot ¶ added in v0.16.19
func (m *MetricsRecorder) Snapshot() []ToolInvocation
Snapshot returns a copy of all rows, sorted by Name. Safe for concurrent calls.
func (*MetricsRecorder) StartedAt ¶ added in v0.16.19
func (m *MetricsRecorder) StartedAt() time.Time
StartedAt returns the time the recorder was created.
func (*MetricsRecorder) Totals ¶ added in v0.16.19
func (m *MetricsRecorder) Totals() ToolInvocation
Totals returns the aggregate row.
type MouseButton ¶
type MouseButton int
MouseButton represents which mouse button was pressed
const ( MouseButtonLeft MouseButton = iota MouseButtonMiddle MouseButtonRight MouseButtonExtra1 MouseButtonExtra2 )
type MouseEvent ¶
type MouseEvent struct {
Kind MouseEventKind
Button MouseButton
Modifiers MouseModifier
Row int // 1-based row
Col int // 1-based column
Flags int // Additional flags (e.g., motion flags)
}
MouseEvent represents a complete mouse event from the terminal
func ParseMouseEvent ¶
func ParseMouseEvent(data string) (*MouseEvent, error)
ParseMouseEvent parses a mouse escape sequence into a MouseEvent Format: ESC [ M Cb Cx Cy (X10 mode)
ESC [ < Cb;Cx;Cy M (SGR mode)
type MouseEventKind ¶
type MouseEventKind int
Mouse event types
const ( MouseEventPress MouseEventKind = iota MouseEventRelease MouseEventMotion MouseEventWheelUp MouseEventWheelDown MouseEventWheelLeft MouseEventWheelRight )
type MouseModifier ¶
MouseModifier represents modifier keys pressed with mouse event
type Panel ¶ added in v0.16.19
type Panel struct {
Title string // optional header text (rendered in the top border)
Content []string // body lines (each rendered on its own row)
Style PanelStyle
}
Panel is a bordered text block with an optional title. Content lines are passed individually — use Panel.Render or Panel.Lines to output.
type PanelStyle ¶ added in v0.16.19
type PanelStyle struct {
BorderColor string // ANSI escape for the border (e.g. "\033[36m" for cyan)
TitleColor string // ANSI escape for the title text
Padding int // spaces between border and content (default 1)
MinWidth int // minimum panel width including borders (default 40)
MaxWidth int // maximum panel width including borders (0 = unbounded)
}
PanelStyle controls the visual treatment of a Panel. Zero value is a valid default: brand-colored top/bottom borders, no title.
func DefaultPanelStyle ¶ added in v0.16.19
func DefaultPanelStyle() PanelStyle
DefaultPanelStyle returns a brand-colored panel style suitable for most CLI output — cyan borders with a white title.
type ReasoningFold ¶ added in v0.16.18
type ReasoningFold struct {
// contains filtered or unexported fields
}
ReasoningFold tracks reasoning output and renders a live-updating "⋯ thinking · N tokens · T elapsed" line on the ActivityIndicator row. When resolved, it prints a permanent "⋯ thought for N tokens · T elapsed" line into the scrollback.
func NewReasoningFold ¶ added in v0.16.18
func NewReasoningFold(indicator *ActivityIndicator) *ReasoningFold
NewReasoningFold creates a fold instance. If indicator is nil or not a TTY, it operates in degraded mode (single Fprintln per burst + summary).
func (*ReasoningFold) Chunk ¶ added in v0.16.18
func (f *ReasoningFold) Chunk(text string)
Chunk receives a reasoning text chunk. Updates token estimate (len(text)/4) and refreshes the display. On TTY: updates SetStatic every ~100ms via the ticker goroutine. On non-TTY: no-op (already printed at Start).
func (*ReasoningFold) Interrupt ¶ added in v0.16.18
func (f *ReasoningFold) Interrupt()
Interrupt handles Ctrl+C mid-reasoning. Prints "⋯ thinking interrupted (N tokens)" and clears pinned state. Idempotent.
func (*ReasoningFold) IsActive ¶ added in v0.16.18
func (f *ReasoningFold) IsActive() bool
IsActive reports whether the fold is currently tracking an active reasoning burst (started but not yet resolved or interrupted).
func (*ReasoningFold) Resolve ¶ added in v0.16.18
func (f *ReasoningFold) Resolve()
Resolve finalizes the current reasoning burst. On TTY: clears the indicator row (ClearStatic), prints permanent summary into scrollback. On non-TTY: prints summary line. Idempotent — second call is no-op.
func (*ReasoningFold) Start ¶ added in v0.16.18
func (f *ReasoningFold) Start()
Start begins a new reasoning burst. Resets token estimate and elapsed time. On TTY: pins the indicator row with SetStatic showing the initial state. On non-TTY: prints one line immediately. Multiple Start() calls in one session produce independent resolved lines.
type ReviewDiffLine ¶ added in v0.16.12
ReviewDiffLine is a single line in a review hunk.
type ReviewHunk ¶ added in v0.16.12
type ReviewHunk struct {
ID string
FilePath string // optional; "" → don't render a path prefix
OldStart int
OldLines int
Lines []ReviewDiffLine
}
ReviewHunk is a console-local view of a diff hunk, avoiding an import cycle with pkg/agent. The agent's Hunk type is adapted to this struct at the call site.
FilePath is the path the hunk belongs to (relative to the repo root or absolute, caller's convention). When set, RenderColoredDiff prefixes the hunk header with a dim path so multi-file edit reviews are scannable; when empty, the header falls back to the legacy format.
type SelectItem ¶
SelectItem is a single entry in a SelectList.
Label — primary text shown to the user
Detail — optional dim-rendered suffix, right-aligned ("anthropic · 200k")
Value — payload returned when the item is chosen
type SelectList ¶
type SelectList struct {
// contains filtered or unexported fields
}
SelectList drives a single-column picker UI. The zero value is unusable — construct via NewSelectList.
func NewSelectList ¶
func NewSelectList(opts SelectListOptions) *SelectList
NewSelectList constructs a picker with the given options. Items shorter than PageSize render compactly without scroll; longer lists page with arrow keys.
func (*SelectList) DismissKey ¶ added in v0.16.15
func (s *SelectList) DismissKey() string
DismissKey returns the printable key that dismissed the picker under DismissOnAnyKey (empty for Enter/Esc/Ctrl+C exits or when the feature is off). Callers can forward it into their own input reader so the user's keystroke isn't lost.
type SelectListOptions ¶
type SelectListOptions struct {
// Title is rendered above the list, glyph-prefixed (GlyphInfo).
Title string
// Items is the full set to choose from. Filter narrows in place.
Items []SelectItem
// Searchable enables type-to-filter mode. Printable characters
// append to the filter buffer and the list reranks against the
// filter via the shared fuzzy matcher.
Searchable bool
// PageSize is how many rows of items are rendered at once. 0 picks
// a sensible default (10).
PageSize int
// SelectList renders a default hint matching the current mode.
Footer string
// DismissOnAnyKey makes any printable key (that isn't navigation or
// Enter) dismiss the picker with ("", false, nil). Useful for
// "press any key to continue"-style dismissal so the user doesn't
// have to reach for Esc or Enter. Ignored when Searchable is true.
DismissOnAnyKey bool
}
SelectListOptions configures a SelectList run.
type ShellPartInfo ¶ added in v0.16.19
type ShellPartInfo struct {
ID string // stable ID (e.g. "part-0")
Text string // raw text of this part
Kind string // CommandKind string value
Semantic string // human-readable description
RiskLabel string // short risk-tier label: CRITICAL, HIGH, MEDIUM, LOW
}
ShellPartInfo is a projection of agent.ShellPart that carries only the fields the CLI picker needs. It lives in pkg/console to avoid a cyclic import (pkg/agent imports pkg/console for the picker; pkg/console cannot import pkg/agent).
type StatusFooter ¶
type StatusFooter struct {
// Cost-warn thresholds (USD). Costs above warn render yellow; above
// alert render red. Sane defaults; future config wiring possible.
// contains filtered or unexported fields
}
StatusFooter renders a single pinned line at the bottom of the terminal showing live session state: model, context-window usage, cumulative cost, and working directory.
Mechanism: when started, the footer sets a terminal scroll region of rows 1..(N-1) where N is the terminal height. Subsequent output scrolls within that region; row N stays put for the footer. On Stop (and on signal-driven shutdown) the scroll region is reset so the user's terminal isn't left in a broken state.
Suppressed entirely on non-TTY writers — Render is a no-op, scroll region is never touched.
func GetGlobalStatusFooter ¶ added in v0.16.18
func GetGlobalStatusFooter() *StatusFooter
GetGlobalStatusFooter returns the process-wide footer, or nil if none is registered. Used by the AssistantTurnRenderer to suppress footer refresh during active prose streaming.
func NewStatusFooter ¶
func NewStatusFooter(w io.Writer, source ContentSource) *StatusFooter
NewStatusFooter constructs a footer that writes to w. If w is nil os.Stderr is used (the same channel the spinner uses). Non-TTY writers produce a no-op footer.
func (*StatusFooter) ClearSteerLine ¶
func (f *StatusFooter) ClearSteerLine()
ClearSteerLine drops the steer panel, blanks the rows it occupied, and contracts the scroll region back to 2 reserved rows. Called when the SteerInputReader stops (e.g. ProcessQuery returned). SP-055.
func (*StatusFooter) Refresh ¶
func (f *StatusFooter) Refresh()
Refresh re-reads the source and redraws the footer. Idempotent and cheap; safe to call from event subscribers on each ToolEnd.
Skipped while prose is actively streaming (proseStreaming flag set by the AssistantTurnRenderer) to avoid the DEC save/restore cursor sequences racing with scroll-region content — the root cause of the "scattered characters" clobbering symptom.
func (*StatusFooter) Resize ¶
func (f *StatusFooter) Resize()
Resize handles a terminal-size change (SIGWINCH). The OLD footer rows (tracked via lastRows) are cleared first so a grow doesn't leave the previous footer stranded mid-screen, then the scroll region is re-applied for the new height and the footer is redrawn at the new bottom.
func (*StatusFooter) SetProseStreaming ¶ added in v0.16.18
func (f *StatusFooter) SetProseStreaming(active bool)
SetProseStreaming toggles the prose-streaming gate. When true, Refresh() is a no-op so the footer's cursor save/restore can't race with prose being written to the scroll region.
This method MUST NOT take outputMu. It is called from the AssistantTurnRenderer's WriteChunk / resetSegment paths, both of which already hold LockOutput — and resetSegment fires from FinalizeAtTurnEnd, also under LockOutput. Calling Refresh() (which calls draw → LockOutput) here would be a re-entrant lock on a non-reentrant sync.Mutex, self-deadlocking the REPL goroutine at every turn end. That hang left the steer panel on screen and blocked the next ReadLine, reproducing the "can't submit follow-ups, must hard-close" symptom. Callers that need a catch-up draw call Refresh() themselves once the lock is released.
func (*StatusFooter) SetSteerLine ¶
func (f *StatusFooter) SetSteerLine(text string)
SetSteerLine reserves one or more pinned rows above the rule and renders the supplied text there. Newlines (`\n`) in `text` produce additional rows up to maxSteerRows. Called by SteerInputReader as the user types — each keystroke replaces the prior content. Safe to call repeatedly; the scroll region is re-applied only when the row count changes. SP-055.
SP-078: also clears steerWrappedActive so a subsequent legacy SetSteerLine after SetSteerLineWrapped reverts to the byte-offset render path.
func (*StatusFooter) SetSteerLineWithCursor ¶ added in v0.16.12
func (f *StatusFooter) SetSteerLineWithCursor(text string, cursorByteOffset int)
SetSteerLineWithCursor is like SetSteerLine but also specifies the byte offset within text where the input caret (▏) should appear. Used by SteerInputReader to render a mid-buffer cursor for readline cursor movement (Ctrl-A/E/B/F, Alt-B/F, etc.). An offset of -1 falls back to caret-at-end (legacy) behavior.
func (*StatusFooter) SetSteerLineWrapped ¶ added in v0.16.18
func (f *StatusFooter) SetSteerLineWrapped(text string, cursorRow, cursorCol int)
SetSteerLineWrapped is the SP-078 width-aware variant. text is the full steer buffer (already prefixed). cursorRow and cursorCol are 0-based indices into the VISUAL row array the footer will render after hard-break (\n) split + soft wrap to the terminal width.
Use this when the buffer can exceed the panel width; the legacy SetSteerLineWithCursor path splits on \n only and overflows horizontally on over-wide lines. cursorRow < 0 is treated as "caret at end of last visible row."
The footer reserves enough scroll-region rows for the visual row count (capped at maxSteerRows) and shifts the caret row back into the visible window when truncation occurs.
func (*StatusFooter) Start ¶
func (f *StatusFooter) Start()
Start declares the scroll region, spawns a SIGWINCH watcher, and renders the initial footer line. Safe to call multiple times; redundant calls just re-render (idempotent on the watcher).
func (*StatusFooter) Stop ¶
func (f *StatusFooter) Stop()
Stop resets the scroll region to full-screen, clears the footer row, and halts the SIGWINCH watcher. MUST be called on every exit path (including signal-driven shutdown) or the user's terminal is left with a broken scroll region. Idempotent — safe to call when already stopped.
func (*StatusFooter) TerminalSize ¶ added in v0.16.18
func (f *StatusFooter) TerminalSize() (cols, rows int)
TerminalSize is the exported alias of terminalSize, for callers outside the console package (e.g. SteerInputReader's width-aware render path). Returns (cols, rows). Both are 0 when the footer is not attached to a real TTY (fd < 0 or GetSize errored).
type SteerInputReader ¶
type SteerInputReader struct {
// contains filtered or unexported fields
}
SteerInputReader captures keystrokes during an active model turn and renders them into a pinned line above the status footer (SP-055). It is distinct from InputReader, which owns stdin only during the REPL prompt; SteerInputReader takes over stdin between ReadLine() calls, while the model is processing.
Lifecycle:
r := NewSteerInputReader(footer, submitFn, interruptFn) r.Start() // puts terminal in raw mode, starts read loop ... ProcessQuery runs ... r.Stop() // restores cooked mode, clears the pinned line
Key handling (raw mode — ICANON / ISIG disabled, so signals must be re-implemented by reading the byte directly). Control keys below are dispatched directly in readLoop; arrow keys, function keys, escape sequences, Alt+key combos, bracketed-paste markers, and multi-byte UTF-8 runes go through the shared EscapeParser (the same parser InputReader uses in input_escape_parser.go) and are routed via handleEvent. Tab toggles the submit mode (STEER ↔ QUEUE).
Enter (CR/LF) → submitFn(buffer); buffer cleared Tab → toggle STEER/QUEUE submit mode Backspace (DEL) → remove rune before cursor Escape (alone) → clear buffer (does not exit steer mode) Ctrl+C (0x03) → interruptFn() — caller routes to TriggerInterrupt Ctrl+A/E → move cursor to start / end Ctrl+B/F → move cursor back / forward one rune Ctrl+D → forward-delete rune at cursor Ctrl+K/U → kill from cursor to end / start of buffer Ctrl+W → delete word before cursor Alt+B/F → move cursor back / forward one word Ctrl+Left/Right → move cursor back / forward one word Left/Right → move cursor back / forward one rune Up/Down → recall steer history Alt+Enter/Shift+Enter → insert a literal newline (multi-line compose)
Submission UX: when Enter is pressed, submitFn receives the buffer. Caller is expected to forward to Agent.InjectInputContext (or equivalent). The buffer is then cleared and the pinned line shows the prompt prefix again, ready for the next steer.
Suppressed entirely on non-TTY stdin (Start is a no-op), matching the behavior of StatusFooter / ActivityIndicator. Callers can construct the reader unconditionally; the gating happens here.
func NewSteerInputReader ¶
func NewSteerInputReader(footer *StatusFooter, submitFn, queueFn, interruptFn func(string)) *SteerInputReader
NewSteerInputReader builds a reader that draws into the given footer and reports submitted/interrupt events via the callbacks. The callbacks fire on the reader's read goroutine — keep them quick or dispatch to another goroutine to avoid blocking the input loop.
queueFn is optional: when nil the user has no way to switch into queue mode (Tab becomes a no-op). When non-nil, Tab toggles between STEER and QUEUE submit modes; pressing Enter in QUEUE mode calls queueFn(text) instead of submitFn(text).
func (*SteerInputReader) DrainUnsentBuffer ¶ added in v0.16.1
func (r *SteerInputReader) DrainUnsentBuffer() string
DrainUnsentBuffer returns any text the user typed into the steer panel but did not submit (no Enter pressed). The caller (typically the REPL loop via SteerCoordinator) can carry this into the next ReadLine call so the text is not silently discarded when a turn ends. The buffer is left intact; call ResetBuffer afterwards to clear it.
func (*SteerInputReader) IsActive ¶
func (r *SteerInputReader) IsActive() bool
IsActive reports whether the reader is currently capturing input. Used by callers that need to coordinate (e.g. signal handlers).
func (*SteerInputReader) ResetBuffer ¶ added in v0.16.1
func (r *SteerInputReader) ResetBuffer()
ResetBuffer clears the in-progress steer buffer. Called by the coordinator after draining the unsent text into the InputReader, so the next Start() begins with a clean slate.
func (*SteerInputReader) SetCompleter ¶ added in v0.16.18
func (r *SteerInputReader) SetCompleter(c CompletionProvider)
SetCompleter installs a completion provider for the steer panel (SP-078 Phase 2). Bound to Ctrl-] (the only free completion binding — Tab is reserved for STEER ↔ QUEUE mode toggle). The provider receives the current buffer + cursor position and returns ordered candidate replacements. Pass nil to disable completion.
Mirrors (*InputReader).SetCompleter in pkg/console/input_completion.go; the underlying cycle state machine is shared via completion.go.
func (*SteerInputReader) SetGroundTruth ¶ added in v0.16.1
func (r *SteerInputReader) SetGroundTruth(gt *GroundTruthTermios)
SetGroundTruth installs the REPL's pristine cooked-mode termios snapshot. Stop() uses this instead of per-enter oldState for restoration, preventing termios descent across PauseSteer / ResumeSteer cycles.
func (*SteerInputReader) Start ¶
func (r *SteerInputReader) Start()
Start puts the terminal in raw mode and spawns the read goroutine. Idempotent. No-op on non-TTY. The pinned line is rendered immediately so the user sees the empty prompt as soon as a turn begins.
func (*SteerInputReader) Stop ¶
func (r *SteerInputReader) Stop()
Stop restores cooked mode, clears the pinned line, and waits for the read goroutine to exit. Idempotent. MUST be called on every exit path (including signal-driven shutdown) or the terminal will be left in steer mode.
Ordering matters: we wait for the goroutine to exit BEFORE calling exitSteerMode. In steer mode VMIN=0/VTIME=0 makes Read return immediately with 0 bytes, so the goroutine's poll loop observes stopCh within one tick (5ms). If we restored cooked mode first, the goroutine's next Read would block forever (cooked VMIN=1) and leak.
func (*SteerInputReader) SubmitMode ¶
func (r *SteerInputReader) SubmitMode() SteerSubmitMode
SubmitMode reports the current Enter-binding. Exposed for tests.
type SteerSubmitMode ¶
type SteerSubmitMode int
SteerSubmitMode controls what happens on Enter (SP-055 Phase 3b). STEER (default) injects mid-turn via the submit callback (typically Agent.InjectInputContext → seed.InjectInput). QUEUE buffers the message into the agent's deferred queue, which the REPL drains and prepends to the next user-typed prompt.
const ( SteerSubmitModeNow SteerSubmitMode = iota // mid-turn injection (default) SteerSubmitModeQueue // hold until next turn )
type StreamingMarkdownFormatter ¶ added in v0.16.19
type StreamingMarkdownFormatter struct {
// contains filtered or unexported fields
}
StreamingMarkdownFormatter applies markdown formatting line-by-line as text arrives, maintaining cross-line state (code blocks, tables) so that multi-line constructs render correctly without any cursor manipulation.
This replaces the old clear-and-reprint approach in FinalizeAtTurnEnd, which was disabled because the formatter's output row count never matched the streamed row count, causing cursor-clobbering. By formatting each line before it reaches the terminal, clobbering is structurally impossible.
Usage: feed complete lines (without trailing \n) via ProcessLine. Each call returns the formatted output for that line (may be empty for consumed-only lines like code-fence boundaries, or multiple lines for a table flush). Call Flush at segment/turn end to emit any pending buffered table rows.
func NewStreamingMarkdownFormatter ¶ added in v0.16.19
func NewStreamingMarkdownFormatter(f *MarkdownFormatter) *StreamingMarkdownFormatter
NewStreamingMarkdownFormatter wraps an existing MarkdownFormatter with streaming state.
func (*StreamingMarkdownFormatter) Flush ¶ added in v0.16.19
func (s *StreamingMarkdownFormatter) Flush() string
Flush emits any pending buffered state (incomplete table rows). Call at segment/turn end so buffered table content is not lost.
func (*StreamingMarkdownFormatter) InTable ¶ added in v0.16.19
func (s *StreamingMarkdownFormatter) InTable() bool
InTable reports whether table rows are currently buffered (used by callers to decide whether to emit a partial line raw vs. through the formatter on segment boundaries).
func (*StreamingMarkdownFormatter) ProcessLine ¶ added in v0.16.19
func (s *StreamingMarkdownFormatter) ProcessLine(line string) string
ProcessLine formats one complete line (without trailing newline). Returns formatted output ending with \n. May return:
- "" — line was consumed without output (code fence open/close, table row buffered)
- single line + \n — regular markdown line or code-block content
- multiple lines — table flush triggered by a non-table line after buffered rows
func (*StreamingMarkdownFormatter) Reset ¶ added in v0.16.19
func (s *StreamingMarkdownFormatter) Reset()
Reset clears all streaming state for a new segment.
type TerminalManager ¶
type TerminalManager interface {
// SaveCursor saves the current cursor position
SaveCursor() error
// RestoreCursor restores the cursor to the previously saved position
RestoreCursor() error
// MoveCursor moves the cursor to the specified position (x, y)
MoveCursor(x, y int) error
// WriteText writes text at the current cursor position
WriteText(text string) error
// Flush ensures all output is written to the terminal
Flush() error
// GetSize returns the terminal dimensions
GetSize() (width, height int, err error)
}
TerminalManager defines the interface for terminal operations used by the UI rendering system.
type ToolInvocation ¶ added in v0.16.19
type ToolInvocation struct {
Name string
Count int64
TotalTokens int64
TotalCost int64 // store cents to avoid float drift; convert at render
TotalLatency int64 // microseconds
}
ToolInvocation is one row in the per-tool metrics breakdown that the CLI-D status-footer tooltip renders on Alt+T.
The recorder is intentionally minimal — it tracks the same four numbers the TODO asks for (invocation count, total tokens, total cost, average latency) and is safe for concurrent use from any goroutine. Tools publish via RecordToolInvocation; the tooltip pulls via Snapshot / Sorted.
func (ToolInvocation) AvgLatency ¶ added in v0.16.19
func (t ToolInvocation) AvgLatency() float64
AvgLatency returns the average latency per invocation in milliseconds, rounded to two decimals. Zero if Count == 0.
type ToolTimeline ¶ added in v0.16.19
type ToolTimeline struct {
// contains filtered or unexported fields
}
ToolTimeline subscribes to the event bus and prints a live, glyph-prefixed timeline of tool executions to the console. Each tool start emits an action arrow; each tool end emits a success/error glyph with elapsed time.
Format:
→ read_file /foo/bar.go · Started ✓ read_file /foo/bar.go · 0.32s ✗ shell_cmd "rm -rf /" · 1.20s: Permission denied
The zero value is unusable — construct via NewToolTimeline.
func NewToolTimeline ¶ added in v0.16.19
func NewToolTimeline(bus *events.EventBus, w io.Writer) *ToolTimeline
NewToolTimeline creates a ToolTimeline that writes to w and subscribes to bus. If w is nil, os.Stderr is used. Call Stop() when the timeline is no longer needed (e.g., at session teardown).
func (*ToolTimeline) Flush ¶ added in v0.16.19
func (tl *ToolTimeline) Flush() <-chan struct{}
Flush returns a channel that is closed after the next event is fully processed (written to the output). Call Flush() before publishing an event, then block on the returned channel to wait for processing to complete. Safe to call concurrently.
func (*ToolTimeline) Stop ¶ added in v0.16.19
func (tl *ToolTimeline) Stop()
Stop unsubscribes from the event bus and waits for the event loop to exit. Safe to call multiple times; subsequent calls are no-ops.
Source Files
¶
- activity_indicator.go
- ansi.go
- assistant_turn_renderer.go
- ci_output_handler.go
- completion.go
- console_lock.go
- display_width.go
- edit_review.go
- error_block.go
- footer_tooltip_pos.go
- glyphs.go
- image_paste.go
- input_completion.go
- input_context_menu.go
- input_core.go
- input_editing.go
- input_editor_escape.go
- input_escape_parser.go
- input_history.go
- input_keymap.go
- input_mouse.go
- input_paste.go
- input_render.go
- input_search.go
- input_terminal.go
- keymap_registration.go
- line_cap.go
- markdown_formatter.go
- metrics_recorder.go
- mouse_menu.go
- panel.go
- persona_style.go
- reasoning_fold.go
- security_prompt.go
- select_list.go
- shell_approval_picker.go
- signal_compat_unix.go
- status_footer.go
- status_footer_badges.go
- status_footer_format.go
- status_footer_scroll.go
- status_footer_steer.go
- status_footer_tooltip.go
- steer_editor.go
- steer_input.go
- steer_input_completion.go
- steer_input_editing.go
- steer_input_events.go
- steer_input_history.go
- steer_input_paste.go
- steer_input_readloop.go
- steer_input_render.go
- steer_search.go
- steer_termios_unix.go
- streaming_markdown.go
- terminal_health_unix.go
- terminal_manager.go
- text_paste.go
- tool_timeline.go
- wrap.go