ui

package
v1.11.0 Latest Latest
Warning

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

Go to latest
Published: Jul 11, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package ui defines the contract between evva's core agent and any UI implementation that drives it. The agent layer never imports a concrete UI; swapping a bubbletea TUI for a web frontend or a JSON-over-stdout bridge means changing one line in cmd/evva/main.go.

Wiring sequence (host responsibility):

  1. ui := bubbletea.New() // construct UI first
  2. ag := agent.New(profile, agent.WithSink(ui), ...) // agent emits to UI
  3. ui.Attach(ag) // UI gets controller
  4. ui.Run(ctx) // blocks until exit

The UI receives events as an event.Sink (agent → UI) and drives the agent through a Controller (UI → agent). Both interfaces are deliberately narrow; UIs that want richer access can type-assert the Controller back to *agent.Agent at their own risk.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Names added in v1.4.3

func Names() []string

Names returns the registered UI names in sorted order — for the -tui help text and "unknown UI" error messages.

func Register added in v1.4.3

func Register(name string, f Factory)

Register adds (or replaces) a named UI factory. Intended to be called from a UI package's init() so a blank import wires it in:

import _ "github.com/johnny1110/evva/pkg/ui/bubbletea" // registers "bubbletea"

Safe for concurrent use.

Types

type CheckpointInfo added in v1.8.2

type CheckpointInfo struct {
	ID            string // opaque id passed back to RestoreCheckpoint
	PromptPreview string // first ~200 chars of the turn's user prompt
	CreatedAt     int64  // unix nano; picker renders a relative age
	FileCount     int    // files whose before-image this checkpoint captured
	// ChatRestoreOK is false when a full compaction since this checkpoint
	// invalidated its conversation cut-point — the picker then offers code
	// restore only (rewind PRD §5.2).
	ChatRestoreOK bool
}

CheckpointInfo is one row in the /rewind picker — a per-user-turn checkpoint. Like SessionInfo it carries only what the picker renders, so listing many is cheap.

type Controller

type Controller interface {
	// Run drives the agent for a single user turn. The UI typically
	// launches this in a goroutine so its main loop stays responsive,
	// and ctx-cancels to honor user interrupts.
	Run(ctx context.Context, prompt string) (string, error)

	// Continue resumes an iter-limit-paused run without appending a new
	// user message.
	Continue(ctx context.Context) (string, error)

	// Messages returns the live conversation transcript. The UI replays
	// these to rebuild its visible scrollback on resume.
	Messages() []llm.Message

	// Usage returns the cumulative token usage for the session — the
	// status bar's running total.
	Usage() llm.Usage

	// LastTurnInputTokens returns the input-token count of the most recent
	// turn: the "how full is the prompt right now" gauge the context meter
	// reads. Prefer this over Usage().Total for context-pressure display.
	LastTurnInputTokens() int

	// TodoStore exposes the agent's todo backing store so the UI can
	// render the todo panel (List) and clear it on auto-fold (Clear).
	// Never nil.
	TodoStore() *todo.TodoStore

	// DaemonState exposes the unified daemon store (subagents, background
	// bash tasks, monitors) so the UI can render the chip strips via
	// SnapshotByKind. Returns nil until the first daemon is registered —
	// callers must nil-check.
	DaemonState() *daemon.DaemonState

	// WorkflowTasks exposes the dynamic-workflow board so the UI can
	// render the board panel. Returns nil when the feature is off
	// (enable_dynamic_workflow) — callers must nil-check.
	WorkflowTasks() *workflow.Store

	// EnqueueUserPrompt hands the agent a prompt the user typed mid-run.
	// The agent drains the queue at the next iteration boundary instead of
	// starting a second concurrent Run.
	EnqueueUserPrompt(prompt string)

	// Logger exposes the agent's structured logger so the UI can emit
	// records that share its context.
	Logger() *slog.Logger

	// Model returns the model id the agent is currently bound to.
	// Used by the TUI's status header; falls back to "-" when empty.
	Model() string

	// AgentID returns the controller's agent identifier so the UI can
	// surface it in headers / banners. Cheap accessor; safe to call
	// every render.
	AgentID() string

	// MaxIterations / SetMaxIterations exposes the loop cap so the
	// /config form can mutate it mid-session. Reads are cheap (atomic
	// load); writes take effect at the next iteration boundary.
	MaxIterations() int
	SetMaxIterations(int)

	// SwitchLLM rebuilds the agent's llm.Client with a new
	// (provider, model) pair and clears the conversation history.
	// Caller (the TUI's /model form) must ensure no Run is in flight
	// before calling — see Agent.SwitchLLM for the running guard.
	SwitchLLM(provider constant.LLMProvider, model constant.Model) error

	// SwitchProfile reconstructs the agent under a new persona — fresh
	// system prompt, fresh active/deferred tool list, fresh session.
	// Caller (the /profile picker) is responsible for ensuring no Run
	// is in flight. Persists the new persona name to evva-config.yml.
	SwitchProfile(name string) error

	// ProfileName returns the active persona's wire identity ("evva",
	// "nono", ...). Used by the TUI status bar's dynamic agent label.
	ProfileName() string

	// ListMainProfiles enumerates the personas the /profile picker
	// should surface — every agent definition with `as: ["main", ...]`
	// in the agent registry.
	ListMainProfiles() []ProfileChoice

	// ListOutputStyles enumerates the styles the /output-style picker
	// should surface: built-ins plus disk styles (user + project tiers),
	// default first.
	ListOutputStyles() []OutputStyleChoice

	// OutputStyleName returns the style the active profile resolves —
	// the persona-declared pin when the persona carries one, else the
	// configured output_style ("default" when unset).
	OutputStyleName() string

	// SwitchOutputStyle persists the style choice and rebuilds the
	// active persona's profile so the new voice applies immediately.
	// Same contract as SwitchProfile: caller ensures no Run is in
	// flight, and the conversation resets (the system prompt changed).
	SwitchOutputStyle(name string) error

	// Effort returns the current effort level name ("low"|"medium"|"high"|"ultra").
	Effort() string

	// SetEffort updates the effort level at runtime. Validates the name;
	// returns an error for unknown levels. Persists to config and applies
	// to the LLM client on the next completion.
	SetEffort(level string) error

	// LLMTemperature / LLMTopK / LLMTopP return the current session-level
	// sampling parameters. nil means "provider default".
	LLMTemperature() *float64
	LLMTopK() *int
	LLMTopP() *float64

	// SetLLMTemperature / SetLLMTopK / SetLLMTopP update the session-only
	// sampling parameters. nil unsets (reverts to provider default).
	// Applies to the live LLM client immediately. Never persisted to disk.
	SetLLMTemperature(v *float64) error
	SetLLMTopK(v *int) error
	SetLLMTopP(v *float64) error

	// Skills returns the merged catalog of user-installed skills (home
	// and workdir, with workdir overrides applied). The TUI's slash
	// suggestion panel surfaces each entry as `/<name>` with the
	// description; the agent decides if/when to invoke them via the
	// SKILL tool. Returns nil when no skills are installed.
	Skills() []Skill

	// MCPServers returns a snapshot of every configured MCP server and its
	// live connection status, for the read-only /mcp panel. Includes
	// disabled servers. Returns nil when no MCP servers are configured (or
	// no manager is installed) — callers render an empty state.
	MCPServers() []MCPServerInfo

	// Compact forces an immediate compaction of the current session.
	// kind is "micro" (elide older tool results, no LLM call) or
	// "full" (one LLM call producing a summary brief). Returns
	// ErrRunInProgress when a Run is currently driving the loop —
	// the TUI surfaces that as a hint rather than retrying.
	Compact(ctx context.Context, kind string) error

	// PermissionModeName returns the agent's current permission stance
	// as a string ("default", "accept_edits", "plan", "bypass", "auto").
	// Named verbosely to avoid collision with the typed Agent.PermissionMode()
	// accessor that returns the internal Mode enum.
	PermissionModeName() string

	// CyclePermissionMode advances the mode in Shift+Tab order and
	// returns the new mode name. Wraps around at the end of the cycle.
	CyclePermissionMode() string

	// SetPermissionModeName sets the permission stance directly by wire
	// name ("default", "accept_edits", "plan", "bypass") — the random-access
	// complement of CyclePermissionMode for callers that present modes as a
	// picker (the swarm web's per-member switch). Errors on unknown names.
	// Safe to call mid-run: the gate reads the mode per tool call.
	SetPermissionModeName(name string) error

	// RespondPermission delivers the user's approval/denial back to
	// the blocked tool goroutine. id is the RequestID from the
	// KindApprovalNeeded event payload. Returns an error only when
	// the id is unknown (already responded / cancelled).
	RespondPermission(id string, decision PermissionDecision) error

	// RespondQuestion delivers the user's answers back to the blocked
	// AskUserQuestion tool goroutine. id is the RequestID from the
	// KindQuestionNeeded event payload.
	RespondQuestion(id string, resp QuestionResponse) error

	// ClearSession starts a fresh conversation under the same persona,
	// LLM, and tools: empty history, zeroed usage, cleared todos, and a
	// new session id. The old session's snapshot stays on disk and
	// remains loadable via ResumeSession. Returns ErrRunInProgress (via
	// the agent layer) when a Run is in flight — the TUI's /clear
	// surfaces that as a hint rather than retrying.
	ClearSession() error

	// ListSessions enumerates persisted sessions scoped to the agent's
	// current workdir, sorted by last-write time descending. The
	// /resume picker calls this to populate its rows. Warnings carries
	// any corrupt-file messages the store collected — the UI may
	// surface them as hints or ignore them.
	ListSessions() ([]SessionInfo, []string)

	// ResumeSession swaps the live agent's state with the session
	// identified by id. Returns ErrRunInProgress (via the agent layer)
	// when a Run is in flight, or an error when the file is missing or
	// unreadable. Successful resume invalidates the prior transcript;
	// the TUI re-renders from Session().Messages.
	ResumeSession(id string) error

	// Checkpoints returns the current session's rewind checkpoints (per user
	// turn), newest first, for the /rewind picker. Returns nil when
	// checkpointing is disabled or the controller is a subagent.
	Checkpoints() []CheckpointInfo

	// RestoreCheckpoint rewinds to the checkpoint identified by id. mode is one
	// of "code" (rewrite captured before-images, delete since-created files),
	// "chat" (truncate the conversation to the turn boundary), or "both". It
	// returns a human-readable summary. Errors with ErrRunInProgress when a Run
	// is in flight, or when id/mode is invalid, or when a chat rewind would
	// cross a compaction boundary (CheckpointInfo.ChatRestoreOK was false). The
	// UI re-renders from Messages() after a chat/both restore.
	RestoreCheckpoint(id, mode string) (string, error)
}

Controller is the narrow API a UI uses to send commands back to the agent. Implemented by *agent.Agent.

The interface is intentionally minimal. Render state lives behind public typed accessors — Messages / Usage for the transcript and status bar, TodoStore / DaemonState for the panels — so a UI in any module can read it without importing evva internals.

type EmitQueue added in v1.11.0

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

EmitQueue decouples event producers from a UI's render loop: Push never blocks, and one pump goroutine delivers queued events to the sink in FIFO order once Start is called.

It exists because the natural bubbletea sink — tea.Program.Send — is a handoff to an unbuffered channel: it blocks while the receive loop is busy (or not yet running), and producers routinely emit from inside critical sections the render path reads back. The dynamic-workflow engine is the canonical case: its dispatch sweep emits daemon/board changes while holding the engine mutex that the TUI's per-frame daemon snapshot needs — with an inline Send, producer and loop wait on each other and the whole TUI freezes. Routing Emit through an EmitQueue makes the producer side wait-free; only the pump ever blocks.

The zero value is not usable; construct with NewEmitQueue.

func NewEmitQueue added in v1.11.0

func NewEmitQueue(sink func(event.Event)) *EmitQueue

NewEmitQueue returns a queue that delivers into sink. The sink runs on the pump goroutine and may block freely.

func (*EmitQueue) Close added in v1.11.0

func (q *EmitQueue) Close()

Close stops the pump and drops undelivered events — the UI they were destined for is gone. A batch already handed to the sink still finishes delivering (a dead tea.Program returns from Send immediately, so this does not wedge shutdown). Push after Close is a silent no-op.

func (*EmitQueue) Len added in v1.11.0

func (q *EmitQueue) Len() int

Len reports how many events are queued and not yet handed to the sink.

func (*EmitQueue) Push added in v1.11.0

func (q *EmitQueue) Push(e event.Event)

Push enqueues e and returns immediately. Safe from any goroutine, while holding any lock, before Start (events buffer until the pump runs), and after Close (dropped).

func (*EmitQueue) Start added in v1.11.0

func (q *EmitQueue) Start()

Start launches the pump. Events pushed before Start flush first, in order — the documented wiring constructs (and lets emit) the agent before the UI runs. Idempotent; a closed queue stays closed.

type Factory added in v1.4.3

type Factory func(evvaHome string) UI

Factory builds a UI given the user's config home (EVVA_HOME). One is registered per UI implementation so the host can pick one at startup by name (e.g. `evva -tui bubbletea`). The signature is deliberately uniform — every UI takes the config dir and nothing else, so the selection flag can construct any of them the same way; a UI that needs more reads it from its own config under evvaHome.

func Lookup added in v1.4.3

func Lookup(name string) (Factory, bool)

Lookup returns the factory registered under name and whether it existed.

type MCPServerInfo added in v1.4.3

type MCPServerInfo struct {
	Name          string // server name (the mcpServers key)
	Transport     string // "stdio" | "http"
	Status        string // "connected" | "pending" | "failed" | "needs-auth" | "disabled"
	Scope         string // "user" | "project" — which settings.json it came from
	Detail        string // one-line summary: the launch command (stdio) or url (http)
	ToolCount     int    // tools discovered (0 unless connected)
	ResourceCount int    // 1 when the server advertises resources, else 0
	Error         string // connection error, populated for failed / needs-auth
}

MCPServerInfo is the UI-facing view of one configured MCP server — the flattened snapshot the /mcp panel renders. Like Skill / ProfileChoice it deliberately exposes only plain types so the ui package never imports pkg/mcp; the agent layer maps mcp.ServerState into this shape.

type OutputStyleChoice added in v1.11.0

type OutputStyleChoice struct {
	Name        string
	Description string
	Source      string
}

OutputStyleChoice is one row in the /output-style picker: the style name, its description blurb, and where it came from ("built-in", "user", or "project").

type PermissionDecision

type PermissionDecision struct {
	Behavior string // "allow" | "deny"
	Reason   string
	// AddRule is non-nil when the user picked "Allow for this session" —
	// the agent's gate adds it to the in-memory store before falling
	// through to tool.Execute.
	AddRule *PermissionRuleSeed
}

PermissionDecision is the UI-side payload returned through Controller.RespondPermission. It mirrors permission.Decision but uses strings so the ui package doesn't depend on internal/permission.

type PermissionRuleSeed

type PermissionRuleSeed struct {
	ToolName string
	Content  string // empty means tool-wide
}

PermissionRuleSeed is the minimum info the agent needs to construct a session-scope allow rule. Source is fixed to session by the agent.

type ProfileChoice

type ProfileChoice struct {
	Name      string
	WhenToUse string
}

ProfileChoice is one row in the /profile picker. Surfaces the persona name plus the optional when_to_use blurb the user authored in meta.yml so the picker can hint at what each persona is for.

type QuestionAnnotation

type QuestionAnnotation struct {
	Notes   string
	Preview string
}

QuestionAnnotation captures the preview content (if any) of the option the user selected, plus any free-text notes they added.

type QuestionResponse

type QuestionResponse struct {
	// Answers maps question text → answer string. For single-select the value
	// is the chosen option label; for multi-select it is comma-separated
	// labels; for "Other" it is the user-typed free text. Kept for back-compat;
	// prefer MultiAnswers for multi-select fidelity.
	Answers map[string]string
	// MultiAnswers maps question text → the chosen option labels (the native
	// multi-select form: single-select is a one-element slice, "Other" the typed
	// text). Additive in v2.x — when set it is authoritative; Answers stays
	// populated (comma-joined) for callers that still read it. Optional; nil is fine.
	MultiAnswers map[string][]string
	// Annotations is keyed by question text. Present only when the user
	// selected an option that had a preview, or typed "Other" notes.
	Annotations map[string]QuestionAnnotation
}

QuestionResponse is the UI-side payload returned through Controller.RespondQuestion. It mirrors question.Response but uses plain Go types so the ui package doesn't import internal/question.

type SessionInfo added in v1.4.3

type SessionInfo struct {
	ID              string // session-id (matches the JSON file basename)
	FirstUserPrompt string // up to 200 chars; picker truncates to 150 for display
	UpdatedAt       int64  // unix nano of last save (file mtime); resume picker sorts desc
	CreatedAt       int64  // unix nano of first save
	Profile         string // persona name at save time
	Provider        string // LLM provider name at save time
	Model           string // LLM model id at save time
	MessageCount    int    // length of Session.Messages — picker shows "<n> msgs"
}

SessionInfo is one row in the /resume picker. Lightweight by design — only what the picker renders, so the UI can show many entries without loading full message bodies.

type Skill

type Skill struct {
	Name        string
	Description string
}

Skill is the UI-facing view of a user-installed skill — just the name and a one-line description for the slash-command suggestion panel. The ui package deliberately does not expose Path or Source: the UI never needs to read the SKILL.md file itself, the agent (via the SKILL tool) does that.

type UI

type UI interface {
	event.Sink

	// Attach hands the UI the controller it uses to drive the agent.
	// Called by the host once, between agent construction and Run.
	Attach(Controller)

	// Run starts the UI's input/render loop and blocks until exit.
	Run(ctx context.Context) error
}

UI is the contract a TUI / GUI / web frontend implementation satisfies.

Emit is called from the agent loop's goroutine (per Sink's contract, the agent serializes per-agent emits internally). The UI must hand the event off to its own render loop WITHOUT BLOCKING — route it through an EmitQueue rather than calling tea.Program.Send inline: Send blocks while the loop is busy, and emitters may hold locks the render path reads back (see EmitQueue's doc for the TUI-freezing deadlock that inline Sends caused).

Run blocks the calling goroutine until the UI exits (user quit, ctx cancelled, fatal error). It is the host's main blocking call.

Directories

Path Synopsis
Package bubbletea is evva's reference terminal UI: it satisfies the public ui.UI contract (pkg/ui) and drives any agent through ui.Controller, so it depends only on pkg/* — a downstream host embeds it exactly the way cmd/evva does.
Package bubbletea is evva's reference terminal UI: it satisfies the public ui.UI contract (pkg/ui) and drives any agent through ui.Controller, so it depends only on pkg/* — a downstream host embeds it exactly the way cmd/evva does.
app
Package app is the v2 TUI's top-level tea.Model.
Package app is the v2 TUI's top-level tea.Model.
components/agents
Package agents renders the horizontal subagent chip strip that sits just above the input.
Package agents renders the horizontal subagent chip strip that sits just above the input.
components/bgtasks
Package bgtasks renders the horizontal background-task chip strip.
Package bgtasks renders the horizontal background-task chip strip.
components/diff
Package diff renders a *fs.FileDiff (the structured metadata write_file / edit_file attach to tools.Result) as a multi-line git-style string with a gutter / content split.
Package diff renders a *fs.FileDiff (the structured metadata write_file / edit_file attach to tools.Result) as a multi-line git-style string with a gutter / content split.
components/input
Package input owns the v2 TUI's bottom textarea: prompt composition, paste compaction, and history navigation.
Package input owns the v2 TUI's bottom textarea: prompt composition, paste compaction, and history navigation.
components/monitors
Package monitors renders the horizontal monitor-task chip strip.
Package monitors renders the horizontal monitor-task chip strip.
components/overlays
Package overlays implements the modal panels the App pushes onto its focus stack: /config (form), /model (picker), /compact (chooser).
Package overlays implements the modal panels the App pushes onto its focus stack: /config (form), /model (picker), /compact (chooser).
components/slash
Package slash renders the autocomplete suggestion panel that pops up when the user types "/" at the start of the input.
Package slash renders the autocomplete suggestion panel that pops up when the user types "/" at the start of the input.
components/status
Package status owns the v2 TUI's bottom HUD: the run-state pill, model + token cells, context-utilization meter, and the contextual hint line that sits above it.
Package status owns the v2 TUI's bottom HUD: the run-state pill, model + token cells, context-utilization meter, and the contextual hint line that sits above it.
components/todos
Package todos renders the bottom todo panel and the green "TODOS COMPLETE" snapshot folded into the transcript when every todo in the store finishes.
Package todos renders the bottom todo panel and the green "TODOS COMPLETE" snapshot folded into the transcript when every todo in the store finishes.
components/transcript
Package transcript owns the scrollback model of the v2 TUI.
Package transcript owns the scrollback model of the v2 TUI.
components/workflow
Package workflow renders the dynamic-workflow board panel — the solo counterpart of the swarm web board: one row per task with lifecycle glyph, dependency badges, verify-policy chip, and a live spinner for tasks whose worker daemon is currently running.
Package workflow renders the dynamic-workflow board panel — the solo counterpart of the swarm web board: one row per task with lifecycle glyph, dependency badges, verify-policy chip, and a live spinner for tasks whose worker daemon is currently running.
events
Package events declares the tea.Msg types the v2 TUI passes through its Update loop.
Package events declares the tea.Msg types the v2 TUI passes through its Update loop.
mouse
Package mouse owns mouse-capture wiring + clipboard integration.
Package mouse owns mouse-capture wiring + clipboard integration.
theme
Package theme owns every styled surface in the v2 TUI.
Package theme owns every styled surface in the v2 TUI.
lp
Package lp is evva's "low profile" terminal UI: a quiet, professional black + gold alternative to the bundled NEON TOKYO TUI.
Package lp is evva's "low profile" terminal UI: a quiet, professional black + gold alternative to the bundled NEON TOKYO TUI.
app
Package app is lp's top-level tea.Model — the low-profile root.
Package app is lp's top-level tea.Model — the low-profile root.

Jump to

Keyboard shortcuts

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