tui

package
v0.1.2 Latest Latest
Warning

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

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

Documentation

Overview

Package tui implements Cogo's interactive Bubble Tea program.

This file defines the in-memory chat history used by the Model. History is intentionally simple: a slice of role-tagged Messages. Persistence across sessions is Slice 5 (FR-11.1 transcript writes).

Index

Constants

View Source
const (
	ExitOK          = 0
	ExitRunError    = 1
	ExitConfigError = 2
)

Exit codes used by the headless package — re-exported here so cmd/cogo can use one set of constants regardless of which mode it dispatched to.

View Source
const MaxPaletteRows = 8

MaxPaletteRows caps how many items render at once. Bubble Tea viewports handle scrolling; we keep this short to stay readable.

Variables

This section is empty.

Functions

func HelpText

func HelpText() string

HelpText returns the multi-line help message printed by /help.

func NewPrompter

func NewPrompter(prog programSender) permissions.Prompter

NewPrompter returns a Prompter wired to send into prog.

func Run

func Run(ctx context.Context, cfg *config.Config, agentsDir string) (int, error)

Run launches the TUI bound to cfg. It blocks until the user quits.

Resolution failures (no auth, bad config) return ExitConfigError so the caller can distinguish them from runtime errors. Runtime errors from Bubble Tea itself return ExitRunError.

agentsDir is the resolved .agents/ directory if one was found, or empty when running with built-in defaults; we use it to persist "Always allow" path-scope choices into config.json and to write session transcripts under sessions/.

Types

type History

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

History is the append-only chat log for one session.

All operations take O(1) amortized time. Snapshot returns a defensive copy; mutators take an index returned by Append (no bounds-checking helpers are exposed since callers always use freshly returned indices).

func (*History) Append

func (h *History) Append(m Message) int

Append adds m and returns its index for later in-place mutation (AppendText, SetRendered).

func (*History) AppendText

func (h *History) AppendText(i int, chunk string)

AppendText appends chunk to the Text of the message at i. Used to accumulate streaming partial events into one in-progress assistant message.

func (*History) Len

func (h *History) Len() int

Len reports the number of messages.

func (*History) Reset

func (h *History) Reset()

Reset empties the history. Used by the /clear slash command.

func (*History) SetRendered

func (h *History) SetRendered(i int, rendered string)

SetRendered replaces the rendered form of the message at i. Called once per assistant turn after TurnComplete.

func (*History) Snapshot

func (h *History) Snapshot() []Message

Snapshot returns a copy of the message slice safe to read without further mutation by the History.

type KeyMap

type KeyMap struct {
	Submit     key.Binding
	Newline    key.Binding
	Cancel     key.Binding // Ctrl+C — interrupts current turn or exits
	ClearView  key.Binding // Ctrl+L — clears viewport (history preserved)
	ClearInput key.Binding // Ctrl+U — clears the textarea (shell convention)
	ScrollUp   key.Binding
	ScrollDown key.Binding
	LineUp     key.Binding // Up arrow — recalls history when input empty
	LineDown   key.Binding // Down arrow — moves forward through recall

	// Permission modal: y allow once, n deny, s allow for the session,
	// a allow always (persisted).
	ConfirmAllowOnce    key.Binding
	ConfirmDeny         key.Binding
	ConfirmAllowSession key.Binding
	ConfirmAllowAlways  key.Binding
}

KeyMap centralizes the keybindings the TUI handles directly. Bubble Tea's textarea consumes most other keys (typing, arrows, etc.).

func DefaultKeyMap

func DefaultKeyMap() KeyMap

DefaultKeyMap returns Cogo's V1 bindings.

type MarkdownRenderer

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

MarkdownRenderer wraps a Glamour TermRenderer to render assistant messages on TurnComplete. Streaming partial chunks bypass this renderer and display as plain text — Glamour can't gracefully render half-formed code fences or tables.

func NewMarkdownRenderer

func NewMarkdownRenderer(width int, styleName string) (*MarkdownRenderer, error)

NewMarkdownRenderer constructs a renderer with a fixed style name and word wrap at width characters. Width <= 0 disables wrap.

styleName must be a recognized Glamour style ("dark", "light", "notty", etc.). We deliberately avoid glamour.WithAutoStyle() here: it issues an OSC-11 background-color query to the terminal every call, and once Bubble Tea is reading stdin, the terminal's response races into the textarea as input. The TUI detects light vs dark once at startup (before tea.NewProgram) and threads the result through this constructor.

Returns a usable (no-op) renderer plus a non-nil error if Glamour initialization fails so the TUI can keep running with raw markdown rather than crashing.

func (*MarkdownRenderer) Render

func (m *MarkdownRenderer) Render(markdown string) string

Render applies Glamour to markdown. If the renderer failed to initialize, it returns markdown unchanged so the user still sees something usable.

type Message

type Message struct {
	Role     Role
	Text     string
	Rendered string // optional pre-rendered (Glamour) form for assistants
}

Message is one entry in the chat history.

Text holds the original payload — for assistant messages this is raw markdown streamed token-by-token; for everything else it is plain text. Rendered is optional and populated for assistant messages once the turn completes (see internal/tui/markdown.go).

func (Message) Display

func (m Message) Display() string

Display returns the form of the message that should be shown in the viewport: the rendered form when populated, otherwise the raw Text.

type Model

type Model struct {

	// AlwaysAllow is invoked when the user picks "always allow" in the
	// permission modal. The host (TUI launcher) plugs in a function
	// that persists the pattern to .agents/config.json. May be nil in
	// tests.
	AlwaysAllow func(req permissions.PromptRequest) error
	// contains filtered or unexported fields
}

Model is Cogo's Bubble Tea model. Mutated through *Model receivers so goroutine-driven Sends can update the same instance.

func NewModel

func NewModel(cfg *config.Config, a *agent.Agent, mdStyle string) *Model

NewModel constructs a fresh chat session bound to a configured agent. The program reference is set later via SetProgram.

mdStyle picks the Glamour style ("dark" or "light") for assistant markdown rendering. Detect this once before tea.NewProgram (see program.Run); resolving it during the program's lifetime causes Glamour's background-color query response to leak into the textarea.

func (*Model) Init

func (m *Model) Init() tea.Cmd

Init returns the initial commands. The spinner is started so its Tick loop can animate when transitioning into the streaming state; the textarea blink keeps the cursor visible.

func (*Model) SetProgram

func (m *Model) SetProgram(p programSender)

SetProgram wires the running tea.Program in so background goroutines (the agent runner) can Send messages back to the loop.

func (*Model) Update

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

Update is Cogo's central message dispatch.

func (*Model) View

func (m *Model) View() string

View renders the model as a single string. Layout (top to bottom):

Header
Viewport (scrollable history)
Palette (slash / file picker — only when active)
Input area (textarea inside a rounded border) — replaced by the
  permission modal when a request is pending
Footer (status hint or spinner)
bottomPad blank rows (breathing room)

type Role

type Role int

Role tags a chat message.

const (
	RoleUser      Role = iota // input typed by the human
	RoleAssistant             // model output (raw markdown)
	RoleSystem                // notices like "/clear", "/help" output
	RoleError                 // unrecoverable error from a turn
	RoleTool                  // a tool invocation made by the agent (display-only)
)

type SlashAction

type SlashAction string

SlashAction identifies the slash command typed by the user.

const (
	SlashHelp    SlashAction = "help"
	SlashClear   SlashAction = "clear"
	SlashQuit    SlashAction = "quit"
	SlashMemory  SlashAction = "memory"
	SlashStats   SlashAction = "stats"
	SlashModel   SlashAction = "model"
	SlashMCP     SlashAction = "mcp"
	SlashSkills  SlashAction = "skills"
	SlashReload  SlashAction = "reload"
	SlashMouse   SlashAction = "mouse"
	SlashUnknown SlashAction = "unknown"
)

func ParseSlash

func ParseSlash(input string) (action SlashAction, command, args string, isSlash bool)

ParseSlash inspects input. If it looks like a slash command (leading `/` after trimming whitespace), returns the recognized action, the raw command name (without leading `/`, as the user typed it for error messages), the args (everything after the command token, trimmed), and isSlash=true. Otherwise returns ("", "", "", false).

Unrecognized slash commands return SlashUnknown so callers can show a friendly "unknown command" message in the chat without leaking input to the model.

type State

type State int

State tracks the agent's current activity for input gating and View rendering.

const (
	StateIdle      State = iota // accepting input, no turn in flight
	StateStreaming              // a turn is running, input disabled
)

type Styles

type Styles struct {
	Header       lipgloss.Style
	HeaderAccent lipgloss.Style
	UserPrefix   lipgloss.Style
	UserText     lipgloss.Style
	Assistant    lipgloss.Style
	System       lipgloss.Style
	Error        lipgloss.Style
	InputBorder  lipgloss.Style
	Spinner      lipgloss.Style
	Footer       lipgloss.Style
	Confirm      lipgloss.Style
}

Styles centralizes Lip Gloss styling so the View has a single source of truth for colors, padding, and borders. AdaptiveColor pairs let each style render correctly on light and dark terminals.

func DefaultStyles

func DefaultStyles() Styles

DefaultStyles returns the Slice 2 visual identity. Explicit theme switching (light/dark forcing) is deferred to a later slice; for now every color is adaptive.

Jump to

Keyboard shortcuts

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