transcript

package
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Sep 24, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

Documentation

Overview

Package transcript renders the scrolling chat history shared by every aichat product: plain user/assistant messages, streamed assistant text and rich Block entries (e.g. a tui/grid result). It generalises DataTug chat's entries/history viewport (rebuildHistory / ensureBlockVisible in datatug-cli/pkg/chat/ui.go) behind a product-neutral Model.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Block

type Block interface {
	View(width int, focused bool) string
	Update(msg tea.Msg) (Block, tea.Cmd)
	Focusable() bool
}

Block is a rich transcript entry that owns its own rendering and key handling, e.g. a tui/grid result. Update returns the (possibly new) Block value, matching the Bubble Tea value-model convention.

type EntityBlock

type EntityBlock interface {
	Block
	Current() *session.EntityRef
}

EntityBlock is a Block that can report the entity currently under the cursor, e.g. a grid's highlighted row. tui/chatshell uses it for "Add to sidebar" and for FocusedRef().

type Entry

type Entry struct {
	// ID identifies a streaming assistant entry for AppendDelta, and is the
	// target of Targeted messages. Products that never stream and never
	// target messages by entry can leave it empty.
	ID       string
	Role     Role
	Text     string
	Markdown bool
	// Block, when set, is rendered instead of Text and receives key events
	// while focused.
	Block Block
	// contains filtered or unexported fields
}

Entry is one transcript item.

type EscCapturer

type EscCapturer interface {
	CapturesEsc() bool
}

EscCapturer is an optional Block capability: when a focused block is in an input-like mode of its own (e.g. a grid's "/" filter box has focus), it returns true so Esc reaches the block (via Update) instead of chatshell's global "return focus to the composer" handling.

type MarkdownRenderer

type MarkdownRenderer func(text string, width int) string

MarkdownRenderer renders markdown text to terminal-safe output at width. Entries with Markdown set use it when the Model was built WithMarkdownRenderer; otherwise Markdown is inert and the entry renders as plain text.

type Model

type Model struct {
	// contains filtered or unexported fields
}

Model is the transcript viewport: an ordered list of Entry plus a bubbles/viewport rendering them, a focus index over focusable entries ("stops"), and the DataTug ensureBlockVisible scrolling behaviour.

func New

func New(opts ...Option) *Model

New returns an empty, unfocused transcript.

func (*Model) Append

func (m *Model) Append(e Entry)

Append adds a new entry to the end of the transcript. It scrolls to the bottom only when the viewport was already at the bottom (r4 review: NOT merely "unfocused" -- an unfocused but manually scrolled-up viewport is left alone too), so focusing an earlier stop to read it, or simply having scrolled up to re-read something, is not disturbed by new content arriving.

func (*Model) AppendDelta

func (m *Model) AppendDelta(id, text string)

AppendDelta appends text to the streaming entry identified by id, creating it (as an assistant entry) on first use. It is the transcript half of tui/stream's channel re-arm pattern: each EventMsg's text delta lands here. Only the streaming entry's cached render is invalidated; the rest of the transcript is reused as-is.

func (*Model) AppendDeltaNoRender added in v0.0.3

func (m *Model) AppendDeltaNoRender(id, text string)

AppendDeltaNoRender is AppendDelta's text-only half: it appends text to the entry identified by id (creating it, as an assistant entry, on first use, same as AppendDelta) but leaves its cached render untouched and does NOT Rebuild the viewport. It exists for a caller (chatshell's StartStreamMarkdown) that wants to throttle an expensive re-render (e.g. re-running a markdown renderer) to less than once per delta while still accumulating every delta's text immediately; pair it with InvalidateAndRebuild once per throttle window, and always at least once more when the stream completes.

func (*Model) Blur

func (m *Model) Blur()

Blur clears transcript focus.

func (*Model) CapturesEsc

func (m *Model) CapturesEsc() bool

CapturesEsc reports whether the focused Block wants Esc routed to it (via Update) instead of chatshell's global "return to composer" handling.

func (*Model) Clear added in v0.0.3

func (m *Model) Clear()

Clear removes every entry and clears focus.

func (*Model) Current

func (m *Model) Current() *session.EntityRef

Current returns the entity ref under focus, when the focused entry's Block implements EntityBlock.

func (*Model) DeliverWheelToFocusedBlock added in v0.1.0

func (m *Model) DeliverWheelToFocusedBlock(msg tea.MouseWheelMsg) (consumed bool, cmd tea.Cmd)

DeliverWheelToFocusedBlock dispatches msg to the FOCUSED entry's Block ONLY (never a broadcast to every entry, unlike Update's non-key path) -- and ONLY when that Block implements WheelConsumer and its ConsumesWheel reports true for msg. It reports whether msg was consumed: false means either no Block is focused, the focused Block doesn't implement WheelConsumer, or it declined this particular event, and the CALLER should handle the wheel event itself instead (e.g. scroll a viewport) -- mirroring tui/chatshell's "never both" rule for its own transcript viewport vs. a focused Block's own wheel handling.

func (*Model) Entries

func (m *Model) Entries() []Entry

Entries returns the current entries (read-only use expected).

func (*Model) Focus

func (m *Model) Focus(stop int)

Focus focuses transcript stop, or clears focus for a negative index.

func (*Model) FocusedBlock

func (m *Model) FocusedBlock() Block

FocusedBlock returns the Block under focus, or nil.

func (*Model) FocusedEntry

func (m *Model) FocusedEntry() *Entry

FocusedEntry returns the currently focused entry, or nil.

func (*Model) InvalidateAndRebuild added in v0.0.3

func (m *Model) InvalidateAndRebuild(id string)

InvalidateAndRebuild forces the entry identified by id to re-render on the next Rebuild (which this also triggers), picking up whatever text AppendDeltaNoRender has accumulated since the last render. A no-op if no entry has that id.

func (*Model) Rebuild

func (m *Model) Rebuild(scrollToBottom bool)

Rebuild re-renders every entry into the viewport's content and, when an entry is focused, scrolls it into view (ensureBlockVisible). It mirrors DataTug's rebuildHistory/ensureBlockVisible in pkg/chat/ui.go, generalised away from DataTug-specific entry kinds. Per-entry views are cached (Entry.renderOut) and only recomputed when the entry's content changed (renderValid cleared) or its width/focused state differs from the cache.

func (*Model) ReplaceBlock added in v0.0.3

func (m *Model) ReplaceBlock(id string, b Block)

ReplaceBlock replaces the Block of the entry identified by id in place (same position, same ID), e.g. to refresh or re-run a grid without disturbing surrounding transcript order or focus. It is a no-op if no entry has that ID.

func (*Model) ScrollDown

func (m *Model) ScrollDown(lines int)

func (*Model) ScrollUp

func (m *Model) ScrollUp(lines int)

ScrollUp/ScrollDown pass through to the viewport for plain scrolling (e.g. PgUp/PgDn on an unfocused transcript).

func (*Model) SetMarkdownRenderer added in v0.0.3

func (m *Model) SetMarkdownRenderer(r MarkdownRenderer)

SetMarkdownRenderer sets the renderer used for entries with Markdown set, after construction (New's caller may not own the Model's own construction call, e.g. tui/chatshell, which builds a *Model itself and exposes this via its own WithMarkdownRenderer Option).

func (*Model) SetSize

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

SetSize resizes the viewport and re-renders. It is a NO-OP when neither dimension actually changed (r3 review, B1): a caller (e.g. chatshell's View(), which re-applies its own resize() on every render so chrome that changes without a WindowSizeMsg -- the slash-command menu opening by keystroke, SetBusy, SetStatus -- stays in sync) may well call SetSize with the SAME width/height on every single frame. Unconditionally re-Rebuilding on every such call, even with nothing to resize, had two user-visible side effects: it silently snapped a manually-scrolled-up viewport back to the bottom every frame, and it re-forced a FOCUSED entry back into view (ensureBlockVisible) every frame too, both on top of the wasted re-render cost of a no-op resize.

When the size DOES change, the viewport's scroll position is preserved UNLESS it was already at the bottom before the resize (checked BEFORE applying the new dimensions, since AtBottom() itself depends on them), in which case it keeps following -- the same "at the bottom" rule shouldAutoFollow applies for Append/ReplaceBlock/AppendDelta (r4 review folded the two rules back into one: shouldAutoFollow no longer treats "unfocused" as its own reason to follow, so a resize with no NEW content and a stream delta that IS new content now agree on exactly when following is appropriate).

func (*Model) StopForID added in v0.0.3

func (m *Model) StopForID(id string) int

StopForID returns the focus-ring stop index of the focusable entry identified by id, or -1 if there is no such entry, or it isn't focusable (e.g. DataTug's Ctrl+G "jump to latest grid").

func (*Model) Stops

func (m *Model) Stops() int

Stops returns the number of focusable entries.

func (*Model) Update

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

Update routes msg. Key presses go only to the focused entry's Block, when any. Every other message (e.g. a window resize, or a product message) is broadcast to every Block, unless it implements Targeted, in which case it is forwarded only to the entry with the matching ID.

func (*Model) View

func (m *Model) View() string

View renders the transcript viewport.

type Option

type Option func(*Model)

Option configures a Model at construction time.

func WithMarkdownRenderer

func WithMarkdownRenderer(r MarkdownRenderer) Option

WithMarkdownRenderer sets the renderer used for entries with Markdown set.

type Role

type Role string

Role of a transcript entry.

const (
	RoleUser      Role = "user"
	RoleAssistant Role = "assistant"
	RoleSystem    Role = "system"
)

type Targeted

type Targeted interface {
	TargetEntryID() string
}

Targeted is an optional message capability: when a non-key message implements it, Model.Update forwards the message only to the entry whose ID matches TargetEntryID, instead of broadcasting it to every Block.

type WheelConsumer added in v0.1.0

type WheelConsumer interface {
	ConsumesWheel(msg tea.MouseWheelMsg) bool
}

WheelConsumer is an optional Block capability: a FOCUSED Block that wants to handle a tea.MouseWheelMsg itself (e.g. scrolling its own internal view, such as a grid's row list) implements it. ConsumesWheel is a pure query -- it reports whether the Block WOULD consume msg without any side effect -- so DeliverWheelToFocusedBlock can decide whether to actually dispatch it before doing so.

Jump to

Keyboard shortcuts

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