ui

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jul 4, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Overview

Package ui is the root of the terminal client: one tea.Model that coordinates controllers, routes input by scope, and composites every widget onto one cell buffer (doc 02 sections 11 and 18). Behavior lives in the controllers; the root owns size, state, focus, the overlay, and the theme, and nothing else.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ChatController

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

ChatController owns the scrollback: the lazy list, follow mode, and the projection of bus messages into parts (doc 02 sections 4 and 5). It knows nothing about permissions or the editor.

func NewChat

func NewChat(th theme.Theme, now func() time.Time) *ChatController

NewChat builds an empty chat in follow mode.

func (*ChatController) Apply

func (c *ChatController) Apply(msg btea.Msg)

Apply projects one bus message into the part list. Unknown messages are ignored; the projection is the only writer of parts.

func (*ChatController) Draw

func (c *ChatController) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor

Draw paints the scrollback, or the landing wordmark while the chat is empty.

func (*ChatController) Empty

func (c *ChatController) Empty() bool

Empty reports whether nothing has been said yet, which is what keeps the landing screen up.

func (*ChatController) Handle

func (c *ChatController) Handle(a keys.Action, _ btea.KeyPressMsg) btea.Cmd

Handle runs a chat-scope key action.

func (*ChatController) Scroll

func (c *ChatController) Scroll(delta int)

Scroll moves the window by a coalesced wheel delta.

func (*ChatController) SetTheme

func (c *ChatController) SetTheme(th theme.Theme)

SetTheme swaps the palette by rebuilding the list; the memo is keyed by width and version, not theme, so a rebuild is the honest reset.

type Client

type Client interface {
	// NewSession creates a session in the current project.
	NewSession(ctx context.Context, title string) (string, error)

	// Sessions lists the project's sessions, newest first.
	Sessions(ctx context.Context) ([]SessionInfo, error)

	// Submit enqueues one user turn and returns its id.
	Submit(ctx context.Context, session, text string) (string, error)

	// Cancel aborts the running turn on a session.
	Cancel(ctx context.Context, session string) error

	// Respond answers a permission request: allow, allow_session, deny.
	Respond(ctx context.Context, session, requestID, decision string) error

	// MemoryIndex renders the live pinned index the ant carries every turn.
	MemoryIndex(ctx context.Context) (string, error)

	// MemorySearch runs the same ranked recall the loop runs.
	MemorySearch(ctx context.Context, query string) ([]MemoryHit, error)

	// MemoryForget archives a memory through the permission pipeline. The
	// answer arrives as a permission request on the stream, so this blocks
	// until the user resolves it. It reports whether a row was archived.
	MemoryForget(ctx context.Context, session, id string) (bool, error)
}

Client is the slice of the core's session API the shell drives. It is declared here, not imported, because the UI never imports a core package (doc 02 section 1); cmd wires an adapter over the real core. Decisions and modes cross as their wire strings.

type Controller

type Controller interface {
	Handle(a keys.Action, k btea.KeyPressMsg) btea.Cmd
}

Controller handles a key action already matched in its scope. A nil return means the action produced no command; the root never forwards an unmatched key here.

type EditorController

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

EditorController owns the prompt textarea, the external-editor hatch, and nothing else. Submitting goes through the submit callback the root injects, so the editor never sees the client.

func NewEditor

func NewEditor(th theme.Theme, submit func(string) btea.Cmd) *EditorController

NewEditor builds a focused textarea with the prompt styling.

func (*EditorController) Blur

func (e *EditorController) Blur()

func (*EditorController) Draw

func (e *EditorController) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor

Draw sizes the textarea to the area and paints its lines.

func (*EditorController) Focus

func (e *EditorController) Focus() btea.Cmd

Focus and Blur hand the textarea the keyboard.

func (*EditorController) Handle

Handle runs an editor-scope action.

func (*EditorController) Lines

func (e *EditorController) Lines() int

Lines is the content height the layout grows the editor by.

func (*EditorController) SetValue

func (e *EditorController) SetValue(s string)

SetValue replaces the buffer, used when the external editor returns.

func (*EditorController) Type

func (e *EditorController) Type(msg btea.Msg) btea.Cmd

Type forwards a raw message to the textarea: typing, paste, blink.

func (*EditorController) Value

func (e *EditorController) Value() string

Value reads the buffer, for tests and the cancel path.

type Focus

type Focus int

Focus is which pane owns non-dialog input.

const (
	FocusEditor Focus = iota
	FocusChat
	FocusNone
)

type Layout

type Layout struct {
	Area    uv.Rectangle
	Header  uv.Rectangle // compact mode's one-line stand-in for the sidebar
	Sidebar uv.Rectangle
	Main    uv.Rectangle // the chat list
	Pills   uv.Rectangle // todos/queue/subagent tabs; zero height until M3
	Editor  uv.Rectangle
	Status  uv.Rectangle // help, status, and notifications share this line
	Compact bool
}

Layout is the full set of screen regions for a frame. It is comparable, so the draw path recomputes it every frame and resizes children only when the new layout differs from the last.

func ComputeLayout

func ComputeLayout(w, h int, editorLines int, forceCompact bool) Layout

ComputeLayout is pure: same inputs, same Layout. editorLines is the editor's content height; it grows the editor region between the bounds, and past the cap the textarea scrolls internally. forceCompact lets the user pin compact mode regardless of size.

type MemoryController added in v0.3.0

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

MemoryController owns the memory panel: it opens it, feeds it the live index and search results off the client, tails fold events onto its log, and routes a forget through the client, which runs it past the permission pipeline. It holds a reference to the pushed panel so an async result lands on the dialog the user is looking at; the reference clears when the dialog closes.

func NewMemory added in v0.3.0

func NewMemory(client Client, th theme.Theme, ns string) *MemoryController

NewMemory builds the controller.

func (*MemoryController) Apply added in v0.3.0

func (mc *MemoryController) Apply(msg btea.Msg)

Apply folds a bus message into the panel. Only fold events matter here; the panel ignores the rest of the stream.

func (*MemoryController) Closed added in v0.3.0

func (mc *MemoryController) Closed()

Closed clears the panel reference when the overlay pops it on escape.

func (*MemoryController) Forget added in v0.3.0

func (mc *MemoryController) Forget(id, session string) btea.Cmd

Forget routes an archive through the client. The call blocks until the user resolves the permission request the pipeline raises, so it runs off the loop; the perm dialog stacks over the panel while it waits.

func (*MemoryController) OnForgot added in v0.3.0

func (mc *MemoryController) OnForgot(m memForgot) btea.Cmd

OnForgot refreshes the index after an archive so the row that left drops out of the pinned view. A denied forget archived nothing, so nothing changed.

func (*MemoryController) OnIndex added in v0.3.0

func (mc *MemoryController) OnIndex(m memIndexLoaded)

OnIndex lands a refreshed index on the panel.

func (*MemoryController) OnResults added in v0.3.0

func (mc *MemoryController) OnResults(m memResults)

OnResults lands a completed search on the panel.

func (*MemoryController) Open added in v0.3.0

func (mc *MemoryController) Open(overlay *dialog.Overlay, now time.Time) btea.Cmd

Open pushes the panel and loads its index. An open dialog owns input, so a second memory keypress never reaches here; the panel closes on escape like every other dialog, which is what Closed clears up after.

func (*MemoryController) Search added in v0.3.0

func (mc *MemoryController) Search(query string) btea.Cmd

Search runs the archival recall off the update loop.

func (*MemoryController) SetTheme added in v0.3.0

func (mc *MemoryController) SetTheme(th theme.Theme)

SetTheme swaps the palette for a panel opened after the change.

type MemoryHit added in v0.3.0

type MemoryHit struct {
	ID    string
	Label string
	Body  string
	Stale bool
}

MemoryHit is one row a memory search returned, shaped for the panel.

type Model

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

Model is the root program: the only tea.Model in the tree. It owns size, state, focus, the overlay, and the theme, and fans everything else out to controllers (doc 02 section 18.1).

func New

func New(o Options) *Model

New assembles the root and its controllers.

func (*Model) Init

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

Init starts the editor cursor and, on first run, the onboarding flow.

func (*Model) SetKeymap

func (m *Model) SetKeymap(km keys.Map)

SetKeymap swaps the live bindings. The next keypress matches against the new map and the help line reads it, no restart involved.

func (*Model) Update

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

Update is small on purpose: route input, fan events, apply actions.

func (*Model) View

func (m *Model) View() btea.View

View composites every widget onto one cell buffer and hands bubbletea the rendered frame; ultraviolet damage-diffs it against the terminal.

type Options

type Options struct {
	Client        Client
	Theme         theme.Theme
	Keys          keys.Map
	FirstRun      bool                       // no config and no nest: run onboarding
	Cwd           string                     // shown in the sidebar
	Model         string                     // the configured model, pre-ledger
	Provider      string                     // the configured provider
	Effort        string                     // reasoning effort, empty when unset
	Models        []string                   // pickable models for the picker
	ContextWindow int64                      // tokens, for the context fill figure
	Session       string                     // resume: next turns go here, "" starts fresh
	Namespace     string                     // the worker ant's memory namespace, for the panel
	Drops         func() uint64              // the broker's lossy-lane drop counter
	Onboarded     func(splash.Outcome) error // persists first-run choices
	Now           func() time.Time           // tests pin this
}

Options wires the root to everything it must not construct itself.

type PermController

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

PermController turns permission events into dialogs and decisions into client calls. It is the only place a perm.Choice becomes a wire word. The session a request arrived on rides with the request id, so the answer lands on the right session even if the user switched away.

func NewPerm

func NewPerm(client Client, th theme.Theme) *PermController

NewPerm builds the controller.

func (*PermController) Request

func (p *PermController) Request(m bus.PermissionRequestedMsg, overlay *dialog.Overlay, now time.Time)

Request pushes a dialog for an incoming permission request.

func (*PermController) Resolve

func (p *PermController) Resolve(d perm.Decision) btea.Cmd

Resolve sends the user's decision to the core.

func (*PermController) Resolved

func (p *PermController) Resolved(m bus.PermissionResolvedMsg, overlay *dialog.Overlay, now time.Time)

Resolved pops the dialog when the core resolved the request some other way, a rule or a timeout, so a stale prompt never lingers.

func (*PermController) SetTheme

func (p *PermController) SetTheme(th theme.Theme)

SetTheme swaps the palette for dialogs opened after the change.

type Router

type Router struct{}

Router decides which scope owns input this frame. It is a table, not a switch: precedence is dialog > focused pane > global, resolved before any key is matched, which is the fix for the giant root-model key switch (doc 02 section 18.2).

func (Router) Route

func (Router) Route(m *Model) (keys.Scope, Controller)

Route resolves the scope and the controller that owns it. A dialog owns input entirely; its keys never reach a pane. The nil controller for the dialog case is deliberate: the overlay handles the raw message itself, grace period and all.

type SessionInfo

type SessionInfo struct {
	ID    string
	Title string
}

SessionInfo is one row of the session switcher.

type SidebarController

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

SidebarController projects bus messages into the sidebar's state. The numbers it shows come from the ledger events, never from client-side recounting (D5, D14).

func NewSidebar

func NewSidebar(th theme.Theme, o Options) *SidebarController

NewSidebar seeds the panel with what the shell knows before any event arrives.

func (*SidebarController) Apply

func (s *SidebarController) Apply(msg btea.Msg)

Apply folds one bus message into the state.

func (*SidebarController) Draw

func (s *SidebarController) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor

Draw refreshes the drop counter and paints the panel.

func (*SidebarController) SetModel

func (s *SidebarController) SetModel(name string)

SetModel records a picked model name.

func (*SidebarController) SetTheme

func (s *SidebarController) SetTheme(th theme.Theme)

SetTheme swaps the palette.

func (*SidebarController) ToggleDebug

func (s *SidebarController) ToggleDebug()

ToggleDebug flips the drop-counter surface.

type State

type State int

State is where the shell is in its life: first-run onboarding, the empty landing screen, or a live chat (doc 02 section 18.3).

const (
	StateOnboarding State = iota
	StateLanding
	StateChat
)

type StatusController

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

StatusController owns the bottom line: the current scope's short help, covered by transient notifications with a time-to-live (doc 02 section 12.4). One line serves as help, status, and notification.

func NewStatus

func NewStatus(th theme.Theme, km keys.Map, now func() time.Time) *StatusController

NewStatus builds the line over the live keymap.

func (*StatusController) Draw

func (s *StatusController) Draw(scr uv.Screen, area uv.Rectangle, scope keys.Scope) *tea.Cursor

Draw paints one line: the note while one is live, else scope help.

func (*StatusController) Expire

func (s *StatusController) Expire()

Expire clears the note once its deadline passed.

func (*StatusController) Notify

func (s *StatusController) Notify(level, text string) btea.Cmd

Notify covers the help line with a message and schedules its expiry.

func (*StatusController) SetKeymap

func (s *StatusController) SetKeymap(km keys.Map)

SetKeymap swaps the live bindings, so a rebind shows on the next draw.

func (*StatusController) SetTheme

func (s *StatusController) SetTheme(th theme.Theme)

SetTheme swaps the palette.

Directories

Path Synopsis
Package bus bridges the core's event stream to the UI program (doc 02 section 3).
Package bus bridges the core's event stream to the UI program (doc 02 section 3).
Package chatlist is the lazy chat scrollback (doc 02 section 5).
Package chatlist is the lazy chat scrollback (doc 02 section 5).
Package dialog is the modal layer (doc 02 section 8).
Package dialog is the modal layer (doc 02 section 8).
Package diff renders unified diff text into styled lines, shared by the edit-result chat renderer and the permission prompt (doc 02 section 9).
Package diff renders unified diff text into styled lines, shared by the edit-result chat renderer and the permission prompt (doc 02 section 9).
Package input holds the pieces that sit between the terminal and the update loop (doc 02 section 14): a filter that coalesces high-rate mouse traffic so a wheel flick cannot starve the keyboard, and the external editor escape hatch for prompts that outgrow a textarea.
Package input holds the pieces that sit between the terminal and the update loop (doc 02 section 14): a filter that coalesces high-rate mouse traffic so a wheel flick cannot starve the keyboard, and the external editor escape hatch for prompts that outgrow a textarea.
Package keys is the binding registry (doc 02 section 12).
Package keys is the binding registry (doc 02 section 12).
Package markdown renders assistant prose.
Package markdown renders assistant prose.
Package memory is the memory panel (doc 07, plan 03 slice 11): a modal dialog that shows what the colony remembers.
Package memory is the memory panel (doc 07, plan 03 slice 11): a modal dialog that shows what the colony remembers.
Package parts is the render model: the UI draws exactly one data structure, the ordered list of typed parts that make up a session's messages, projected from the core's events with no second schema (doc 02 section 4).
Package parts is the render model: the UI draws exactly one data structure, the ordered list of typed parts that make up a session's messages, projected from the core's events with no second schema (doc 02 section 4).
Package perm is the permission dialog (doc 02 section 9).
Package perm is the permission dialog (doc 02 section 9).
Package picker is the one filterable list dialog: the model picker, the session switcher, and the command palette are all a Dialog with different items and a different ID (doc 02 section 7.1).
Package picker is the one filterable list dialog: the model picker, the session switcher, and the command palette are all a Dialog with different items and a different ID (doc 02 section 7.1).
Package sidebar is the live ops panel (doc 02 section 13.1): a fixed width column of stacked sections reading from state the root projects out of core events.
Package sidebar is the live ops panel (doc 02 section 13.1): a fixed width column of stacked sections reading from state the root projects out of core events.
Onboarding: a short sequence of dialogs on the overlay stack (doc 02 section 19.2).
Onboarding: a short sequence of dialogs on the overlay stack (doc 02 section 19.2).
Package tea holds the one rendering contract every widget in the UI implements: paint yourself onto cells within a rectangle (doc 02 section 2.5).
Package tea holds the one rendering contract every widget in the UI implements: paint yourself onto cells within a rectangle (doc 02 section 2.5).
Package theme is the semantic palette layer (doc 02 section 10, D18).
Package theme is the semantic palette layer (doc 02 section 10, D18).

Jump to

Keyboard shortcuts

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