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):
- ui := bubbletea.New() // construct UI first
- ag := agent.New(profile, agent.WithSink(ui), ...) // agent emits to UI
- ui.Attach(ag) // UI gets controller
- 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 ¶
- func Names() []string
- func Register(name string, f Factory)
- type CheckpointInfo
- type Controller
- type EmitQueue
- type Factory
- type MCPServerInfo
- type OutputStyleChoice
- type PermissionDecision
- type PermissionRuleSeed
- type ProfileChoice
- type QuestionAnnotation
- type QuestionResponse
- type SessionInfo
- type Skill
- type UI
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.
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
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
Len reports how many events are queued and not yet handed to the sink.
type Factory ¶ added in v1.4.3
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.
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
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 ¶
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 ¶
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 ¶
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 ¶
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. |
|
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. |