console

package
v0.16.22 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 6, 2026 License: MIT Imports: 31 Imported by: 0

Documentation

Overview

Mouse tracking and context menu support for sprout IDE

Package console (continued) — tool execution timeline subscriber.

Index

Constants

View Source
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

View Source
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

View Source
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.

View Source
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.

View Source
const MaxPastedImageSize = 10 * 1024 * 1024

MaxPastedImageSize is the maximum size of a pasted image before rejection (10 MB).

View Source
const PastedImageDirName = ".sprout/pasted-images"

PastedImageDirName is the subdirectory (relative to CWD) where pasted images are saved.

View Source
const PastedTextDirName = ".sprout/pastes"

PastedTextDirName is the workspace-relative directory under which large text pastes are auto-saved by SavePastedText. Mirrors PastedImageDirName.

View Source
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 BoldText

func BoldText(text string) string

BoldText wraps text with bold formatting using ANSI codes.

func BoxHint added in v0.16.19

func BoxHint(label string) string

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 Colorize

func Colorize(text, color string) string

Colorize wraps text with a color code and reset

func ColorizeBold

func ColorizeBold(text, color string) string

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

func DetectImageMagic(data []byte) (ext string, mimeType string)

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

func FormatErrorBlock(header string, err error) string

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

func FormatYesNoPrompt(yesDefault bool) string

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

func FormatYesNoPromptStdout(yesDefault bool) string

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

func IsLikelyMarkdown(text string) bool

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 KeymapHintRow added in v0.16.20

func KeymapHintRow() string

KeymapHintRow renders a single-line hint of registered keybindings suitable for embedding in a footer or status bar. Format: "Alt+T label1 · Alt+V label2 · ..." The label is the Description truncated to ~30 display columns. Returns empty string when no bindings are registered.

func LockOutput added in v0.16.7

func LockOutput()

LockOutput acquires the console output mutex.

func MoveCursorDownSeq

func MoveCursorDownSeq(n int) string

MoveCursorDownSeq returns the escape sequence to move cursor down by n lines.

func MoveCursorLeftSeq

func MoveCursorLeftSeq(n int) string

MoveCursorLeftSeq returns the escape sequence to move cursor left by n columns.

func MoveCursorSeq

func MoveCursorSeq(x, y int) string

MoveCursorSeq returns the escape sequence to move the cursor to (x,y) Note: ANSI uses row (y) first, then column (x).

func MoveCursorToColumnSeq

func MoveCursorToColumnSeq(n int) string

MoveCursorToColumnSeq returns the escape sequence to move cursor to column n (1-based).

func MoveCursorUpSeq

func MoveCursorUpSeq(n int) string

MoveCursorUpSeq returns the escape sequence to move cursor up by n lines.

func PersonaBadge

func PersonaBadge(depth int, personaID string) string

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

func PersonaColor(personaID string) string

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

func PersonaIndent(depth int) string

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

func PromptShellApprovalParts(ctx context.Context, parts []ShellPartInfo) (map[string]bool, error)

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, cfg *configuration.Manager)

RegisterKeymapForFooter wires Alt+T → footer-tooltip toggle and Alt+V → output-verbosity toggle into the global keymap. Call from your REPL bootstrap (or wherever the agent shell starts).

The cfg parameter is optional (nil falls through to a no-op handler for the verbosity toggle). 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

func SavePastedImage(data []byte, baseDir string) (string, error)

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

func SavePastedText(content, baseDir string) (string, error)

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

func SetColorBlind(enabled bool) bool

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

func SetScrollRegionSeq(top, bottom int) string

SetScrollRegionSeq returns the escape sequence to set the scrolling region (1-based, inclusive).

func ShouldSmartSavePaste

func ShouldSmartSavePaste(content string) bool

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

func WrapHardLine(s string, cols int) []string

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

func WriteWrappedLines(w io.Writer, lines []string, cols, withCursorRow, cursorCol int) error

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

type CompletionProvider func(line string, cursorPos int) []string

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 NewContextMenu

func NewContextMenu() *ContextMenu

NewContextMenu creates a new 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) Hide

func (cm *ContextMenu) Hide()

Hide hides the context menu

func (*ContextMenu) NavigateDown

func (cm *ContextMenu) NavigateDown()

NavigateDown moves selection down

func (*ContextMenu) NavigateUp

func (cm *ContextMenu) NavigateUp()

NavigateUp moves selection up

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

func (*ContextMenu) Show

func (cm *ContextMenu) Show()

Show displays the context menu

func (*ContextMenu) Toggle

func (cm *ContextMenu) Toggle()

Toggle shows/hides the context 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

func (*EscapeParser) Reset

func (ep *EscapeParser) Reset()

Reset the parser state

type FooterTooltip added in v0.16.19

type FooterTooltip struct {

	// Timeout controls how long the tooltip stays visible if no
	// keystroke dismisses it. Zero means "no auto-dismiss"; the
	// InputReader's HandleEvent hook is the primary dismiss path.
	Timeout time.Duration

	// Source supplies the per-tool breakdown. Defaults to the
	// global MetricsRecorder if nil. Set via SetSource.
	Source func() []ToolInvocation

	// Cancel is called on Hide (dismiss by keystroke) so the timeout
	// 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) Fprintf

func (g Glyph) Fprintf(w io.Writer, format string, args ...any)

Fprintf writes a formatted glyph-prefixed line to an explicit writer.

func (Glyph) Fprintln

func (g Glyph) Fprintln(w io.Writer, msg string)

Fprintln writes the glyph-prefixed message to an explicit writer. Tests use this to capture output to a buffer.

func (Glyph) Prefix

func (g Glyph) Prefix() string

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

func (g Glyph) Print(msg string)

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).

func (Glyph) Printf

func (g Glyph) Printf(format string, args ...any)

Printf writes a formatted line with the glyph prefix to stderr.

func (Glyph) Rune

func (g Glyph) Rune() string

glyphRune is the visible character for the glyph. UTF-8; widely supported in terminal fonts.

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

type MouseModifier struct {
	Shift bool
	Alt   bool
	Ctrl  bool
}

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.

func (Panel) Lines added in v0.16.19

func (p Panel) Lines() []string

Lines returns the panel rendered as individual terminal rows. The output includes the top border, title (if any), content rows, and bottom border. Each row is a complete terminal line.

func (Panel) Render added in v0.16.19

func (p Panel) Render() string

Render returns the panel as a single string with embedded newlines. Each content line is wrapped to the panel's max width when set.

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

type ReviewDiffLine struct {
	Type    string // "context", "add", "remove"
	Content string
}

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

type SelectItem struct {
	Label  string
	Detail string
	Value  string
}

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.

func (*SelectList) Run

func (s *SelectList) Run(ctx context.Context) (string, bool, error)

Run blocks until the user picks an item or cancels. Returns the selected item's Value and ok=true on confirm, or ("", false) on cancel (Esc / Ctrl+C). On non-TTY input, falls back to numbered-list + numeric stdin entry so the picker remains scriptable.

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
	// Footer is the hint line shown beneath the list (dim). When empty,
	// 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.
	WarnCost  float64
	AlertCost float64
	// 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) SetShowKeymapHint added in v0.16.20

func (f *StatusFooter) SetShowKeymapHint(show bool)

SetShowKeymapHint enables/disables the keyboard shortcut hint row above the rule. When true, drawLocked reserves an extra row. SP-115.

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

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL