transcript

package
v0.1.3 Latest Latest
Warning

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

Go to latest
Published: Aug 30, 2026 License: AGPL-3.0 Imports: 14 Imported by: 0

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

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

func (b Block) Activity() bool

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

func (b Block) Height(width int) int

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

func (b Block) Render(t theme.Theme, tier theme.Tier, width int) string

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

type Model struct {
	Theme theme.Theme
	Tier  theme.Tier
	// contains filtered or unexported fields
}

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 New

func New(t theme.Theme, tier theme.Tier) Model

New returns an empty Model with no block focused, following the tail.

func (Model) Blocks

func (m Model) Blocks() []Block

Blocks returns the whole conversation, oldest first.

func (Model) Clear

func (m Model) Clear() Model

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

func (m Model) ClearFocus() Model

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

func (m Model) Dropped() int

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

func (m Model) Dump() string

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

func (m Model) Empty() bool

Empty reports whether the transcript has no conversation blocks and no active streaming tail.

func (Model) ExpandBlockAtScreenRow

func (m Model) ExpandBlockAtScreenRow(y int) (Model, bool)

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

func (m Model) ExpandedRows(width int) (rows []string, blockTops []int)

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

func (m Model) FocusIndex() int

FocusIndex returns the focused block's index, or -1 for the composer.

func (Model) FocusNext

func (m Model) FocusNext() Model

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

func (m Model) FocusPrev() Model

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

func (m Model) Focused() bool

Focused reports whether a block currently holds the focus.

func (Model) FocusedRowVisible

func (m Model) FocusedRowVisible(y int) bool

FocusedRowVisible reports whether y is inside the viewport.

func (Model) FocusedText

func (m Model) FocusedText() (string, bool)

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

func (m Model) Following() bool

Following reports whether new output pulls the view to the bottom.

func (Model) HandleEvent

func (m Model) HandleEvent(ev uievent.Event) (Model, tea.Cmd)

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

func (m Model) HasSelection() bool

HasSelection reports whether a selection is active. The screen uses it to keep a live drag visible across frames.

func (Model) Height

func (m Model) Height() int

func (Model) NewWhilePaused

func (m Model) NewWhilePaused() int

NewWhilePaused is how many finished blocks arrived since auto-follow was paused. It is 0 while following.

func (Model) Offset

func (m Model) Offset() int

Offset is the first visible row of the conversation.

func (Model) PageBy

func (m Model) PageBy(pages, fraction int) Model

PageBy moves by whole screens. fraction 2 is a half page.

func (Model) ReasoningHidden

func (m Model) ReasoningHidden() bool

ReasoningHidden reports the current state of the reasoning toggle.

func (Model) Rows

func (m Model) Rows() []string

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

func (m Model) ScrollBy(delta int) Model

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

func (m Model) ScrollToBottom() Model

ScrollToBottom jumps to the newest output and resumes auto-follow.

func (Model) ScrollToFocus

func (m Model) ScrollToFocus() Model

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

func (m Model) ScrollToTop() Model

ScrollToTop jumps to the start of the conversation.

func (Model) SelectedText added in v0.1.2

func (m Model) SelectedText() string

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

func (m Model) SetAllCollapsed(collapsed bool) Model

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

func (m Model) SetHideReasoning(hide bool) Model

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

func (m *Model) SetSize(width, height int)

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

func (m *Model) SetTheme(t theme.Theme, tier theme.Tier)

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

func (m Model) ToggleFocused() (Model, bool)

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

func (m Model) ToggleReasoning() Model

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

func (m Model) TotalRows() int

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

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd)

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.

func (Model) View

func (m Model) View() string

View is the visible rows joined, which is what the screen draws.

func (Model) Width

func (m Model) Width() int

Width and Height report the current viewport size.

Jump to

Keyboard shortcuts

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