uiadapter

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 27, 2026 License: AGPL-3.0 Imports: 36 Imported by: 0

Documentation

Overview

Package uiadapter: Phase 2 Conversation and TurnHandle over chat.Session.

Conversation wraps an existing *chat.Session into the ports.Conversation contract. The session is owned by the caller; the adapter holds no reference that outlives the caller closing the session and Send is the only mutating call. Concurrent Send calls are serialized by an internal mutex; the second caller blocks until the first finishes.

TurnHandle exposes the buffered uievent.Event stream for one turn. Events() is closed exactly once when the turn ends. Cancel cancels the turn's per-turn context and closes the channel; it is safe to call after the turn has already ended.

The synthetic-event ordering and Cancel/goroutine/tap coordination contract is documented on Send; the rationale moved to docs/ would belong there if it ever grows beyond a paragraph.

Package uiadapter translates domain events (internal/agent.Event) into the canonical UI event stream (internal/uikit/uievent.Event). It is the single seam between the agent runtime and every renderer; no renderer imports internal/agent, and no agent code imports uievent.

Phase 1 ships ONLY the pure translation layer. Session lifecycle (Conversation, TurnHandle, Cancel), approval gating, settings ports, and the build/constructor live in later phases of docs/design/ui-replacement- phases.md.

Empty-TurnID window: the first event on every turn is KindTurnStart with TurnID="" (chat.Session only surfaces the real ID after SendUserWithEvent returns). The terminal KindTurnEnd always carries the real ID, so renderers that read TurnID off the end event are correct; renderers that need it for every intermediate event must accept the leading-empty window. See conversation.go's emitSyntheticTurnStart comment for the tap-stamp mechanism.

The full per-kind mapping table lives next to TranslateEvent.

Index

Constants

This section is empty.

Variables

View Source
var SubagentProgressRegistrar func(fn func(agent.Event)) (cleanup func())

Send starts one user turn on the wrapped session. The second caller blocks until the first finishes. The per-turn context is derived from ctx; cancelling it cancels the turn and closes the channel.

Synthetic-event ordering and Cancel/goroutine/tap coordination contract: the very first event on the channel is KindTurnStart (TurnID="" because chat.Session only surfaces the turnID after SendUserWithEvent returns; the terminal turn.end carries the real TurnID). Tap-installed events stamp the real TurnID once known via a shared atomic.Pointer. An atomic.Bool closed is the single source of truth for "events is closed"; Cancel and the goroutine CAS-claim it; the tap drops on closed. Exactly one close occurs. SubagentProgressRegistrar allows the UI layer to receive live subagent progress events (nested tool calls, steps, heartbeats) from the subagent progress callback.

Functions

func DefaultCommands

func DefaultCommands() []composer.Command

DefaultCommands returns the list of available slash commands for composer auto-completion.

func ErrFromDetailForTest

func ErrFromDetailForTest(detail string, ok bool) string

ErrFromDetailForTest is the test-export wrapper for errFromDetail. Same pattern as ParseArgsForTest: production callers go through the renderer path; tests exercise the bare helper through this shim so diff-coverage keeps the "non-bare detail" branch under cover.

func ParseArgsForTest

func ParseArgsForTest(input string) map[string]any

ParseArgsForTest is the test-export wrapper for parseArgs. The body helper is unexported so production code never reaches for it directly; tests use this shim instead. Returning the same map[string]any as the internal helper keeps the diff-coverage gate satisfied without making the helper public API.

func PopulateFromToolCalls

func PopulateFromToolCalls(threads *SubagentThreads, msgs []ports.Message)

PopulateFromToolCalls scans conversation messages for subagent tool invocations (such as dispatch_tasks, delegate, spawn_agent, invoke_subagent, and agent_* tools) and seeds SubagentTranscriptConversation instances into threads so resumed sessions show full subagent history when opening their threads in the TUI.

func SkillCommands

func SkillCommands(reg *skills.Registry) []composer.Command

SkillCommands returns the slash command candidates for the given skill registry.

func TranslateEvent

func TranslateEvent(ev agent.Event) []uievent.Event

TranslateEvent converts one agent.Event into zero or more uievent.Events using default translation options (iteration and cache notices disabled).

func TranslateEventWithOptions

func TranslateEventWithOptions(ev agent.Event, opts TranslateOptions) []uievent.Event

TranslateEventWithOptions converts one agent.Event into zero or more uievent.Events with custom notice visibility options.

Types

type Adapter

type Adapter struct {
	*Conversation
	// contains filtered or unexported fields
}

Adapter is the production handle cmd/mivia-ui (or a future CLI surface) drives a real chat.Session through the ports.Conversation seam. The embedded *Conversation exposes every Conversation method (Send, History, Model, ContextUsage, Title). The store and mcp fields are kept so the cleanup closure the constructor returns can close both, and so a future phase that surfaces checkpoint or MCP state in the UI does not need to plumb new fields through New's signature.

func New

func New(ctx context.Context, in Input) (*Adapter, func(), error)

New wires every Input field into a real chat.Session and returns it wrapped in an Adapter. The returned cleanup closes the checkpoint store and the MCP manager (the latter only when MCP was enabled). On any error the function returns (nil, nil, err): no partial resources leak, because BuildSession closes its own store on error and AttachMCPServers closes its manager on error.

The principal returned by BuildSession is captured but not stored on the Adapter; future phases that surface checkpoint state will add a field rather than re-running the build to surface it.

func (*Adapter) SubagentThreads

func (a *Adapter) SubagentThreads() ports.SubagentThreads

SubagentThreads returns the active ports.SubagentThreads registry.

func (*Adapter) Thread

func (a *Adapter) Thread(callID string) (ports.Conversation, bool)

Thread satisfies ports.SubagentThreads.

type Approver

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

Approver implements ports.Approver and bridges tool approval requests between the chat.Session's ApprovalGate and the UI's Pending / Resolve surface.

func NewApprover

func NewApprover(sess *chat.Session) *Approver

NewApprover creates an Approver and hooks it into sess.ApprovalGate. If sess.ApprovalStanding is nil, it initializes a new standing cache so session-level always decisions persist.

func (*Approver) Pending

func (a *Approver) Pending() <-chan ports.ApprovalRequest

Pending returns the read-only channel delivering approval requests to the UI.

func (*Approver) Resolve

func (a *Approver) Resolve(id string, decision ports.Decision)

Resolve answers a pending approval request by ID with the user's decision. Resolving an unknown or already resolved ID is a safe no-op.

type CommandRunner

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

CommandRunner bridges the UI slash-command loop with the backend session, config, and agent state.

func NewCommandRunner

func NewCommandRunner(sess *chat.Session, res *config.Resolved, state *cliagents.AgentSessionState) *CommandRunner

NewCommandRunner constructs a CommandRunner for the given session and configuration.

func NewCommandRunnerWithPool

func NewCommandRunnerWithPool(sess *chat.Session, pool *SessionPool, res *config.Resolved, state *cliagents.AgentSessionState) *CommandRunner

NewCommandRunnerWithPool constructs a CommandRunner with an explicit SessionPool.

func (*CommandRunner) Commands

func (r *CommandRunner) Commands() []composer.Command

Commands returns all available slash commands, merging builtins with active skills.

func (*CommandRunner) Pool

func (r *CommandRunner) Pool() *SessionPool

Pool returns the SessionPool backing this runner's session switches (/resume, /new). Callers building the UI use it to source the initial Conversation and its SubagentThreads registry from the SAME pool that SelectSession and handleNew hand out on later switches, so the activity panel's thread dialog is wired to whichever session is actually active rather than a separately-constructed twin.

func (*CommandRunner) Run

func (r *CommandRunner) Run(ctx context.Context, name, args string) ports.CommandOutcome

Run executes one slash command by name with arguments.

func (*CommandRunner) SelectAgent

func (r *CommandRunner) SelectAgent(_ context.Context, name string) ports.CommandOutcome

SelectAgent switches the session's active agent.

func (*CommandRunner) SelectEffort

func (r *CommandRunner) SelectEffort(_ context.Context, levelStr string) ports.CommandOutcome

SelectEffort applies a reasoning effort level override.

func (*CommandRunner) SelectModel

func (r *CommandRunner) SelectModel(_ context.Context, name string) ports.CommandOutcome

SelectModel switches the session's active model.

func (*CommandRunner) SelectSession

func (r *CommandRunner) SelectSession(ctx context.Context, id string) ports.CommandOutcome

SelectSession loads and resumes a saved session.

func (*CommandRunner) SessionActive

func (r *CommandRunner) SessionActive(id string) bool

SessionActive reports whether a pooled session currently has a turn in flight: a map lookup and an atomic load, no I/O. It is safe to call on every /resume picker refresh tick, unlike listSessionSummaries which re-queries the session store.

func (*CommandRunner) SetActiveSession

func (r *CommandRunner) SetActiveSession(sess *chat.Session)

SetActiveSession updates the active session for subsequent commands.

func (*CommandRunner) SetSettingsStore

func (r *CommandRunner) SetSettingsStore(s *SettingsStore)

SetSettingsStore links a SettingsStore to synchronize active sessions during session switches.

type Conversation

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

Conversation wraps a *chat.Session and satisfies ports.Conversation. Fields are unexported; NewConversation is the only construction path.

func NewConversation

func NewConversation(sess *chat.Session) *Conversation

NewConversation wraps an existing chat.Session. The caller owns the session and is responsible for its lifecycle; NewConversation stores the pointer verbatim and does not retain any other reference.

func (*Conversation) ActiveTurn

func (c *Conversation) ActiveTurn() (ports.TurnHandle, bool)

ActiveTurn returns the current active turn handle, if any.

func (*Conversation) ContextUsage

func (c *Conversation) ContextUsage() ports.Usage

ContextUsage reports the session's live prompt-cost estimate. Field mapping: InputTokens <- chat.ContextUsage.UsedTokens, OutputTokens = 0 (chat.ContextUsage.OutputReserveTokens is max output capacity, not consumed tokens), CachedTokens = 0 (chat has no cache field; honest zero), CostUSD = 0 (chat has no cost field; honest zero). Percent from chat.ContextUsage is discarded.

func (*Conversation) History

func (c *Conversation) History() []ports.Message

History returns a snapshot of the session's user/assistant turns at the moment of the call. Empty input returns nil (NOT an empty slice) so callers can distinguish "no history" from "history is empty".

func (*Conversation) ID

func (c *Conversation) ID() string

ID returns the active session's ID.

func (*Conversation) IsActive

func (c *Conversation) IsActive() bool

IsActive reports whether a turn is currently in flight on this conversation. It is the liveness signal session pickers use to show which background sessions are actually doing something.

func (*Conversation) Model

func (c *Conversation) Model() ports.ModelInfo

Model reports the bound provider/model and its usable prompt budget.

ContextWindow carries the PROMPT BUDGET, not the model's raw context window: its only consumer is the top bar's context percentage, and the agent compacts against the budget (the window minus the output reserve). Reporting the raw window made that gauge read about two thirds at the exact moment compaction fired, and left it unable to reach 100% at all - the surface users watch to decide whether to intervene showed slack that did not exist. Falls back to the window when no budget is derived, so a binding without a profile still shows something rather than nothing.

func (*Conversation) NoticeOptions

func (c *Conversation) NoticeOptions() TranslateOptions

NoticeOptions returns the current notice visibility options.

func (*Conversation) ScrollLines

func (c *Conversation) ScrollLines() int

ScrollLines returns the configured viewport scroll step.

func (*Conversation) Send

func (*Conversation) Session

func (c *Conversation) Session() *chat.Session

Session returns the wrapped *chat.Session.

func (*Conversation) SetNoticeOptions

func (c *Conversation) SetNoticeOptions(opts TranslateOptions)

SetNoticeOptions configures notice visibility options for this conversation.

func (*Conversation) SetScrollLines

func (c *Conversation) SetScrollLines(n int)

SetScrollLines updates the viewport scroll step for this conversation.

func (*Conversation) SetShowReasoning

func (c *Conversation) SetShowReasoning(show bool)

SetShowReasoning updates the default reasoning visibility for this conversation.

func (*Conversation) SetSubagents

func (c *Conversation) SetSubagents(subagents *SubagentThreads)

SetSubagents connects the SubagentThreads registry for isolating subagent events from the main chat transcript.

func (*Conversation) ShowReasoning

func (c *Conversation) ShowReasoning() bool

ShowReasoning returns the default reasoning visibility.

func (*Conversation) Title

func (c *Conversation) Title() string

Title returns the session's display title, derived from the first user message. Memoised on first call for the current session ID.

type Input

type Input struct {
	// Resolved is the workspace-resolved configuration. Required; a
	// nil Resolved returns an error naming "resolved" so a CLI caller
	// can surface a precise diagnostic.
	Resolved *config.Resolved
	// WorkspaceRoot is the directory hooks and config tools anchor at.
	// Required iff HooksConfigured is true: with no hooks, the empty
	// string is allowed. Returning an error here keeps a silent
	// mis-wiring impossible.
	WorkspaceRoot string
	// Workspace is the workspace.Root the tool registry uses for
	// filesystem scoping. Optional; nil yields a registry with no
	// filesystem tools.
	Workspace *workspace.Root
	// MCPConfig selects the MCP servers attached after the registry is
	// built. Disabled (Enabled=false) is a no-op, not an error.
	MCPConfig config.MCPConfig
	// SessionID is the checkpoint principal's subject scope. Empty
	// defaults to chat.Session.SessionID inside BuildSession.
	SessionID string
	// StorePath is the SQLite checkpoint store BuildSession opens.
	// Required; an empty path returns an error naming "store path".
	StorePath string

	// Dispatcher tunables. BuildDispatcher passes them through; zero
	// means "use the runtime defaults".
	MaxDepth       int
	MaxRetries     int
	MaxInputBytes  int
	MaxOutputBytes int
	MaxBudget      int

	// Completer is the provider completer the chat session's initial
	// binding runs on. May be nil (chat.NewSession accepts a nil
	// completer for construction); a nil completer cannot run a turn.
	Completer provider.Completer
	// RedactionPolicy is the privacy redaction policy forwarded to MCP
	// server output handling. Nil means the workspace configured none
	// and MCP redaction is skipped (matching internal/cli's default).
	RedactionPolicy *redact.Policy

	// HooksConfigured reports whether the dispatcher should install
	// lifecycle hook closures. False (or an empty WorkspaceRoot) means
	// nil compare per invocation, no hook overhead at all - the same
	// contract the historical cli path held.
	HooksConfigured bool
	// HooksGroups returns the runnable hook groups for the current
	// session. Required iff HooksConfigured is true; nil with
	// HooksConfigured true is treated as "no groups".
	HooksGroups func() []hooks.Group
	// NoteHookWarnings receives runtime diagnostics from hooks that
	// actually executed, for the caller to surface (e.g. a /hooks
	// listing). Nil is safe: warnings are simply dropped.
	NoteHookWarnings func([]string)
}

Input carries every value New needs to wire a real chat.Session behind the ports.Conversation seam. Most fields are direct copies of what the CLI chat command already supplies; the seam exists so cmd/mivia-ui can do the same wiring without importing internal/cli or any of its split sub-packages (cliworktree, clichat, etc.).

type SessionPool

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

SessionPool manages active and resumed sessions in memory. It allows background sessions to keep running while the user switches freely between them.

func NewSessionPool

func NewSessionPool(initialSess *chat.Session, res *config.Resolved, agentState *cliagents.AgentSessionState, toolsOn bool) *SessionPool

NewSessionPool constructs a SessionPool seeded with the initial session.

func (*SessionPool) CreateFresh

func (p *SessionPool) CreateFresh() (ports.Conversation, error)

CreateFresh creates a brand-new session, inheriting runtime state (tools, store, context manager, event bus, session directory) from the first existing pool member. It does NOT call Load — the session starts empty. The new conversation is registered in the pool and returned.

func (*SessionPool) GetOrCreate

func (p *SessionPool) GetOrCreate(sessionID string) (ports.Conversation, error)

GetOrCreate retrieves an active conversation or instantiates a new session loaded from the persisted session store.

func (*SessionPool) IsActive

func (p *SessionPool) IsActive(id string) bool

IsActive reports whether the session with the given ID has a turn currently in flight. A session this process has never loaded into the pool cannot be active from here, so it reports false.

func (*SessionPool) Session

func (p *SessionPool) Session(id string) *chat.Session

Session returns the underlying chat.Session for a session ID, or nil if not present.

func (*SessionPool) Threads

func (p *SessionPool) Threads() *SubagentThreads

Threads returns the SubagentThreads registry every pooled Conversation is wired to. Callers building the UI (internal/newtui) pass this same instance to Screen.SetSubagentThreads so the dialog resolves whichever session is currently active, including one reached by /resume or /new.

type SettingsStore

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

SettingsStore holds the active configuration and state for settings management.

func NewSettingsStore

func NewSettingsStore(sess *chat.Session, res *config.Resolved, state *cliagents.AgentSessionState) *SettingsStore

NewSettingsStore builds a SettingsStore populated from the resolved configuration and agent state.

func (*SettingsStore) SetActiveSession

func (s *SettingsStore) SetActiveSession(sess *chat.Session)

SetActiveSession updates the active session pointer for SettingsStore.

func (*SettingsStore) SetConversation

func (s *SettingsStore) SetConversation(conv *Conversation)

SetConversation attaches the active Conversation to receive live notice option updates.

func (*SettingsStore) Settings

func (s *SettingsStore) Settings() ports.Settings

Settings returns the ports.Settings bundle with all section adapters.

type SubagentThreads

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

SubagentThreads implements ports.SubagentThreads by dynamically resolving threads registered during runtime subagent executions.

func NewSubagentThreads

func NewSubagentThreads() *SubagentThreads

NewSubagentThreads creates a new SubagentThreads registry.

func (*SubagentThreads) HandleEvent

func (s *SubagentThreads) HandleEvent(ev agent.Event, opts TranslateOptions)

HandleEvent receives subagent-originated agent.Events, translates them, records their history, and routes them to the matching SubagentTranscriptConversation.

func (*SubagentThreads) RegisterThread

func (s *SubagentThreads) RegisterThread(callID string, conv ports.Conversation)

RegisterThread adds or replaces an active conversation thread for a tool call ID.

func (*SubagentThreads) Thread

func (s *SubagentThreads) Thread(callID string) (ports.Conversation, bool)

Thread retrieves the conversation thread for a given tool call ID.

type SubagentTranscriptConversation

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

SubagentTranscriptConversation represents a subagent transcript thread.

func NewSubagentTranscriptConversation

func NewSubagentTranscriptConversation(title string, model ports.ModelInfo, history []ports.Message) *SubagentTranscriptConversation

NewSubagentTranscriptConversation creates a new thread conversation.

func (*SubagentTranscriptConversation) ActiveTurn

ActiveTurn returns a live event subscription for the active subagent.

func (*SubagentTranscriptConversation) ContextUsage

func (c *SubagentTranscriptConversation) ContextUsage() ports.Usage

ContextUsage returns token usage for the subagent thread.

func (*SubagentTranscriptConversation) History

History returns a copy of the thread history.

func (*SubagentTranscriptConversation) ID

ID returns the subagent thread ID.

func (*SubagentTranscriptConversation) Model

Model returns the subagent model information.

func (*SubagentTranscriptConversation) RecordEvent

func (c *SubagentTranscriptConversation) RecordEvent(e uievent.Event)

RecordEvent records one translated uievent into message history and notifies listeners.

func (*SubagentTranscriptConversation) Send

Send records user messages in the thread and emits transcript stream events.

func (*SubagentTranscriptConversation) Title

Title returns the title of the subagent thread.

type TranslateOptions

type TranslateOptions struct {
	ShowIterationNotices   bool
	ShowPromptCacheNotices bool
}

TranslateOptions configures notice visibility and filtering during event translation.

Jump to

Keyboard shortcuts

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