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) Dropped() int
- func (m Model) Dump() string
- func (m Model) Empty() bool
- func (m Model) ExpandBlockAtScreenRow(y int) (Model, bool)
- 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) 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) SetAllCollapsed(collapsed bool) Model
- func (m Model) SetHideReasoning(hide bool) Model
- 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, Plan and Progress 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
Progress *uievent.Progress
}
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) 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 trailing +1 is the blank separator row after the block (docs/design/wireframes.md variant A, mivia-ui-mock.html): every top-level block in the transcript is followed by one blank row, or adjacent blocks read as one dense, cramped run of text instead of distinct entries. It applies uniformly, collapsed or not, so spacing never depends on collapse state.
func (Block) Render ¶
Render draws the block. The header row is byte-identical whether the block is collapsed or expanded, apart from the collapse marker, so toggling never moves any other row (wireframes-panes.md section 5).
The last row is always the blank separator Height accounts for; see its doc comment.
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) 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.
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.
Only the header row of a block is clickable: the body of a collapsed block is not on screen, and clicking expanded content must not collapse it by surprise - the keyboard toggle stays the only way back.
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) 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.
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.
func (Model) ScrollToTop ¶
ScrollToTop jumps to the start of the conversation.
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) 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 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) 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.