Documentation
¶
Overview ¶
Package transcript renders the conversation for the inline-first UI. It handles every uievent.Kind exhaustively, mirroring internal/ui/ stream's plain-text renderer but styled through internal/ui/theme.
Three layers, in decreasing order of power:
- The live window holds the newest blocks whose total height fits the viewport budget. They are values, not strings, so they re-render: they take focus, collapse, and update state in place.
- The retained ring holds what left the live window, bounded by config.MaxTranscriptLines, so a pager can still read it.
- Terminal scrollback holds every evicted block, printed once by the caller. Frozen text, but natively selectable and searchable.
The trigger matters. A block commits when it is EVICTED, not when it is finalized. Conflating the two is what makes a transcript non-interactive: a finalized block is often still on screen, and while it is on screen the user must be able to focus and collapse it.
View() renders the live window plus the streaming tail, and is bounded by the budget by construction. That bound is the point: a View() taller than the terminal does not compose with Bubble Tea's inline redraw (relative cursor movement plus erase), and earlier content is erased before a user - or a test - can see it.
Index ¶
- type Block
- type FlushMsg
- type Header
- type Model
- func (m Model) Blocks() []Block
- func (m Model) Clear() Model
- func (m Model) ClearFocus() Model
- func (m *Model) ClearSelection()
- func (m Model) Dropped() int
- func (m Model) Dump() string
- func (m Model) Empty() bool
- func (m Model) ExpandBlockAtScreenRow(y int) (Model, bool)
- func (m Model) ExpandedRows(width int) (rows []string, blockTops []int)
- func (m Model) FocusIndex() int
- func (m Model) FocusNext() Model
- func (m Model) FocusPrev() Model
- func (m Model) Focused() bool
- func (m Model) FocusedRowVisible(y int) bool
- func (m Model) FocusedText() (string, bool)
- func (m Model) Following() bool
- func (m Model) HandleEvent(ev uievent.Event) (Model, tea.Cmd)
- func (m Model) HasSelection() bool
- func (m Model) Height() int
- func (m Model) NewWhilePaused() int
- func (m Model) Offset() int
- func (m Model) PageBy(pages, fraction int) Model
- func (m Model) ReasoningHidden() bool
- func (m Model) Rows() []string
- func (m Model) ScrollBy(delta int) Model
- func (m Model) ScrollToBottom() Model
- func (m Model) ScrollToFocus() Model
- func (m Model) ScrollToTop() Model
- func (m Model) SelectedText() string
- func (m Model) Selection() sel.Selection
- func (m Model) SelectionRect() sel.Rect
- func (m Model) SetAllCollapsed(collapsed bool) Model
- func (m Model) SetHideReasoning(hide bool) Model
- func (m *Model) SetSelection(s sel.Selection)
- func (m *Model) SetSelectionRect(r sel.Rect)
- func (m *Model) SetSize(width, height int)
- func (m *Model) SetTheme(t theme.Theme, tier theme.Tier)
- func (m Model) ToggleFocused() (Model, bool)
- func (m Model) ToggleReasoning() Model
- func (m Model) TotalRows() int
- func (m Model) Update(msg tea.Msg) (Model, tea.Cmd)
- func (m Model) View() string
- func (m Model) Width() int
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Block ¶
type Block struct {
ID string
Kind uievent.Kind
// CallID ties every event of one tool call to one block, so pending
// -> running -> ok/failed updates the same header in place instead of
// stacking three blocks for one call.
CallID string
Args map[string]any
Header Header
Body []string
// Collapsible marks a block whose body may be hidden. Assistant
// prose is not collapsible: it has no header to collapse into.
Collapsible bool
Collapsed bool
Focused bool
// Prose renders with no header and no indent, at column 1. It is the
// only content that reads as conversation rather than as tooling
// (wireframes-panes.md section 2, last paragraph).
Prose bool
// Input is the raw text of a turn-start (user prompt) or text-end
// (assistant markdown response) block, preserved so changing theme
// or terminal width can re-render the block with accurate styles.
Input string
// Diff and Plan preserve the raw payloads whose rendered form carries
// theme colour, so a theme change can rebuild them. Anything styled at
// push time and not kept here silently keeps the previous theme until
// the block is rebuilt by a new event.
Diff *uievent.Diff
Plan *uievent.PlanBody
// Usage preserves the raw token-and-cost payload behind the usage
// footer line for the same reason: the footer is styled at push time,
// and restyle rebuilds it from this copy when the theme changes.
Usage *uievent.UsageBody
}
Block is one addressable unit of transcript. It is a value, not a rendered string, so it can be re-rendered when it collapses, when it gains focus, or when its state advances from running to ok.
func (Block) Activity ¶ added in v0.1.1
Activity reports whether the block is tool activity rather than conversation: header-carrying blocks whose bodies hang under an activity group at a 2-column indent, with no blank row between consecutive group members (transcript-polish.md R1). Prose - the user turn, assistant text, the usage footer - is the conversation voice.
func (Block) Height ¶
Height is the TERMINAL row count at width, which the eviction budget consumes. It is not the count of logical body lines: a line wider than the terminal draws on two rows or more, and counting it as one would make the budget nominal instead of real.
Height and Render both derive their rows from bodyRows, so they cannot disagree.
The blank rows BETWEEN blocks are not part of Height: the viewport layout owns separators, because their placement depends on the neighbours (one per turn section, none inside an activity run - transcript-polish.md R1), not on the block alone.
func (Block) Render ¶
Render draws the block's own rows, WITHOUT any trailing blank separator (the viewport layout inserts those between blocks - see Height) and WITHOUT the group indent (the layout prefixes activity blocks with two spaces). Toggling collapse never moves any body row: the header changes only in its first cell (the marker) and in the magnitude hint the collapsed state appends to the meta column ("… +N lines", transcript-polish.md R3; wireframes-panes.md section 5 as amended).
type FlushMsg ¶
type FlushMsg struct{}
FlushMsg ticks the repaint clock while a text/reasoning span streams.
type Header ¶
type Header struct {
Label string
Detail string
DiffAdd int
DiffDel int
Meta string
State string
// Role colours the state word. The word is always present, so
// meaning survives with colour removed.
Role theme.Role
}
Header is a block's first row, in four columns. wireframes-panes.md section 2: label at column 1, then the detail, then meta and state placed inline right after it. The four-column renderer itself lands in a later wave; the value is separated now so the block model does not change again to accept it.
type Model ¶
Model holds the whole conversation, the viewport over it, and the in-flight streaming tail. Text and reasoning deltas accumulate in a buffer instead of committing a block per token (build spec section 4.5: "one Msg per token is one render per token even with the cell renderer"); HandleEvent returns a tea.Cmd that starts a repaint clock while a span is streaming.
func (Model) Clear ¶
Clear empties the transcript: every block, the drop count, the focused block, and the in-flight streaming tail. Auto-follow resumes at the empty state, so new output appears immediately. The /clear command uses this.
func (Model) ClearFocus ¶
ClearFocus returns the focus to the composer.
func (*Model) ClearSelection ¶ added in v0.1.2
func (m *Model) ClearSelection()
ClearSelection drops any selection and its highlight.
func (Model) Dropped ¶
Dropped is how many blocks the bound discarded from the start of the conversation. The view states it, so truncation is never silent.
func (Model) Dump ¶
Dump renders the WHOLE conversation, every block expanded, at the current width.
This is the content behind cockpit-research.md rule 6.3: the cockpit takes the terminal's drawing surface, so it must be able to hand the conversation back. Writing this into native scrollback returns the session to grep, tmux copy-mode, and the terminal's own find.
Collapsed blocks are expanded here. A collapse is a view state, and a dump the user asked for should not hide what they cannot see - which is also why leader runs never appear here: with every member expanded there is no run to coalesce, and each read keeps its own header.
func (Model) Empty ¶
Empty reports whether the transcript has no conversation blocks and no active streaming tail.
func (Model) ExpandBlockAtScreenRow ¶
ExpandBlockAtScreenRow expands the collapsed block that draws on the given viewport row, if any. y is relative to the transcript's own top row, the way a mouse event reports it. It reports false when the row holds no collapsed block header, so a click can fall through.
Clicking a coalesced leader row (R2) opens the whole run: the row the user sees stands in for every member, so the click means "show me these". Only header rows are clickable - clicking expanded content must never collapse it by surprise; the keyboard toggle stays the only way back.
func (Model) ExpandedRows ¶ added in v0.1.1
ExpandedRows renders every block expanded and unfocused at the given width, with the same section separators and group indents the live view uses, so the ctrl+o pager and the scrollback dump read exactly like the cockpit transcript. blockTops maps each block index to the first row its content occupies, for the pager's prompt jumps.
func (Model) FocusIndex ¶
FocusIndex returns the focused block's index, or -1 for the composer.
func (Model) FocusNext ¶
FocusNext moves the focus one block DOWN, towards the newest. From the newest block it returns to the composer, which is directly below it. From the composer it does nothing: there is nothing below.
func (Model) FocusPrev ¶
FocusPrev moves the focus one block UP, towards the oldest. From the composer it enters at the NEWEST block, the one directly above it. It stops at the oldest block: above that is scrollback, which is frozen and cannot take the focus.
func (Model) FocusedRowVisible ¶
FocusedRowVisible reports whether y is inside the viewport.
func (Model) FocusedText ¶
FocusedText is the focused block's plain text, for the clipboard. It returns the body whether the block is collapsed or not: the user asked for the block's content, and a collapse marker is a view state, not part of what they meant to copy.
func (Model) HandleEvent ¶
HandleEvent applies one uievent.Event to the model and returns the updated Model plus a Cmd exactly when a new streaming span needs its repaint clock started. Same value-receiver, return-new-Model shape as Update, so a caller has one calling convention for both instead of an in-place pointer mutation for one and a returned copy for the other.
func (Model) HasSelection ¶ added in v0.1.2
HasSelection reports whether a selection is active. The screen uses it to keep a live drag visible across frames.
func (Model) NewWhilePaused ¶
NewWhilePaused is how many finished blocks arrived since auto-follow was paused. It is 0 while following.
func (Model) ReasoningHidden ¶
ReasoningHidden reports the current state of the reasoning toggle.
func (Model) Rows ¶
Rows renders exactly the visible rows, padded to the viewport height.
Geometry comes from layout(): separators between sections, the 2-column group indent on activity blocks, and coalesced leader rows for collapsed read-only runs (R1, R2). Only the blocks that intersect the viewport are styled; a block above or below it costs one Height call and nothing else.
func (Model) ScrollBy ¶
ScrollBy moves the viewport by delta rows. Scrolling up pauses auto-follow, so streaming output does not drag the reader away from what they stopped to read. Reaching the bottom resumes it and clears the missed count: the reader is caught up.
func (Model) ScrollToBottom ¶
ScrollToBottom jumps to the newest output and resumes auto-follow.
func (Model) ScrollToFocus ¶
ScrollToFocus brings the focused block fully into view, scrolling as little as possible. A block taller than the viewport is aligned to its top, because its header carries the identity. Geometry comes from the layout, so separators and group indents are part of the anchor, and a block hidden inside a coalesced run anchors on the run's leader row.
func (Model) ScrollToTop ¶
ScrollToTop jumps to the start of the conversation.
func (Model) SelectedText ¶ added in v0.1.2
SelectedText returns the plain stream text between anchor and focus over the visible rows. It strips the highlight before cutting so the reverse-video spans cannot shift the cell boundaries, then re-applies nothing: the copy carries no styling.
func (Model) Selection ¶ added in v0.1.2
func (m Model) Selection() sel.Selection
Selection reports the current selection, including the armed anchor.
func (Model) SelectionRect ¶ added in v0.1.2
func (m Model) SelectionRect() sel.Rect
SelectionRect returns the region's current absolute screen rect, as injected by SetSelectionRect.
func (Model) SetAllCollapsed ¶
SetAllCollapsed collapses or expands every collapsible block.
The conversation's total height changes by a large factor, so the viewport re-anchors on the focused block when there is one. Expanding everything and then leaving the reader at the same row number would put them somewhere they did not ask to be.
func (Model) SetHideReasoning ¶
SetHideReasoning explicitly sets whether reasoning blocks are hidden.
func (*Model) SetSelection ¶ added in v0.1.2
func (m *Model) SetSelection(s sel.Selection)
SetSelection records the live drag selection in viewport-local cells.
func (*Model) SetSelectionRect ¶ added in v0.1.2
func (m *Model) SetSelectionRect(r sel.Rect)
SetSelectionRect records the region's absolute screen rect. The screen calls it wherever it sizes the transcript.
func (*Model) SetSize ¶
SetSize records the drawing surface. height is the row count the transcript itself may draw, with the composer and status row already subtracted by the caller.
A width change also rebuilds every user-turn block: its rows are width-styled (selection background to the edge, marker, indent), so rows built at the old width either overflow the new one or stop short of it - a broken fill, not a reflow. Plain prose needs nothing here; it wraps at render time.
func (*Model) SetTheme ¶
SetTheme records a theme change and rebuilds every block body that was styled when it was pushed. A body that is not rebuilt here keeps the previous theme's colours on screen until a new event replaces it.
func (Model) ToggleFocused ¶
ToggleFocused collapses or expands the focused block. It reports false when nothing is focused, or when the focused block cannot collapse, so the caller can pass the key on instead of swallowing it.
A focused block that is collapsed inside a coalesced leader run (R2) has no visible row of its own - the row on screen is the run's leader - so the toggle opens the whole run. Expanded members toggle individually, as before.
A toggle changes the block's height, so the viewport is re-anchored on the focused block rather than on a row number. Without that, expanding a block scrolls the thing the user just acted on off the screen.
func (Model) ToggleReasoning ¶
ToggleReasoning hides or shows reasoning blocks in the live window.
Reasoning is collapsed by default in the wireframes, so this toggles the whole class at once rather than block by block. It changes only live blocks: what already reached scrollback is frozen.
func (Model) TotalRows ¶
TotalRows is the height of the whole conversation at the current width: every block span plus the separators the layout places between sections (transcript-polish.md R1 - spacing follows turns, and the blank rows belong to the sequence, not to any block).
func (Model) Update ¶
Update handles FlushMsg only; every other Msg is ignored, so this Model can sit inside a larger Update without a type-switch guard at the call site.