ui

package
v0.0.35 Latest Latest
Warning

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

Go to latest
Published: Sep 11, 2026 License: Apache-2.0 Imports: 36 Imported by: 0

Documentation

Overview

Package ui is the Bubble Tea (Elm) layer of mecatui: the root Model state machine, its Update reducer, the View assembly, and the conversation/block renderers. It imports only the client and theme packages (plus charm libs and stdlib) — never contracts/gen or any internal/... package — so it renders purely from the plain msg structs the client layer translates proto Events into. All glamour rendering happens here on the single update goroutine.

Package ui models_catalog.go owns the durable root-owned model catalog and selection reconciliation state, which survives /models picker instances.

Package ui models_surface.go owns the dynamic picker-local /models surface; durable catalog reconciliation and selection effects remain root-owned by Model.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ClickableRegion

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

ClickableRegion is a surface body-relative rectangle tagged with an opaque, frame-scoped handle.

type ConnectAction added in v0.0.22

type ConnectAction uint8

ConnectAction is the closed set of operations main may perform after the TUI exits. The zero value is invalid so an incomplete intent fails loudly.

const (
	ConnectSaved ConnectAction = iota + 1
	Reauthenticate
	RetryAfterCleanup
	AddTarget
)

ConnectAction values are the closed set of operations main may perform after the TUI exits.

type ConnectController added in v0.0.22

type ConnectController interface {
	ListConnectTargets(context.Context) ([]ConnectTarget, error)
}

ConnectController is the narrow composition-owned saved-target listing seam. The UI can only display its public metadata and emit an intent; it cannot authenticate or launch a browser.

type ConnectRestartIntent added in v0.0.22

type ConnectRestartIntent struct {
	Target string
	Action ConnectAction
	// ResumeSessionID is retained only for same-target recovery actions. Main
	// re-checks ownership through GetSession before adopting it.
	ResumeSessionID string
}

ConnectRestartIntent is consumed by main after Bubble Tea exits. Target is public canonical registry metadata only.

type ConnectTarget added in v0.0.22

type ConnectTarget struct {
	Target   string
	Issuer   string
	ClientID string
	Audience string
}

ConnectTarget is public saved-target metadata. It deliberately has no token, credential reference, CA contents, or other secret-shaped fields.

type Converser

type Converser interface {
	OpenConverse(ctx context.Context) (*client.Stream, error)
}

Converser preserves the original extension seam for raw and test clients.

type Deps

type Deps struct {
	Session SessionCreator
	Conv    Converser
	MCP     client.MCP       // MCP/ToolHive inventory + resources/prompts; nil disables the overlay
	Cmds    client.Commander // slash-command discovery for the input palette; nil disables it
	// ServerInfo reads the safe build and composition identities when /diagnostics is invoked against a remote server.
	ServerInfo ServerInfoGetter
	// ServerImpl is the locally-known embedded server family. It is used without
	// an RPC when Embedded is true.
	ServerImpl  string
	Skills      client.SkillLister      // skills-inventory discovery for the /skills panel; nil disables it
	Agents      client.AgentLister      // agent-definition discovery for the /agents panel; nil disables it
	Soul        client.SoulFetcher      // soul (persona) inspection for the /soul panel; nil disables it
	UserModel   client.UserModelLister  // user-model inspection for the /usermodel panel; nil disables it
	Reflections client.ReflectionClient // proposal review and explicit reflection; nil disables it
	Dream       client.DreamClient      // manual memory consolidation review; nil disables /dream
	Compactor   client.SessionCompactor // out-of-band session compaction; nil disables /compact
	Models      client.ModelLister      // selectable-model discovery for the /models picker; nil disables it
	Worktrees   client.WorktreeLister   // worktree discovery for the /worktrees overlay (issue #102); nil disables it
	// Sched is the schedule discovery + management surface for the /schedule overlay
	// (issue #234); nil disables it (the overlay is honestly absent). The overlay can
	// create/inspect/pause/resume/fire-now on any store-backed server; auto-firing on
	// a cadence is the server's tick loop (ON by default on a schedule-capable store,
	// ADR 0073 — `mecated --no-scheduler` opts out).
	Sched client.ScheduleLister
	// Sessions is the stored-session inventory surface for the /sessions picker
	// (issue #245 Phase 2); nil disables it (the overlay is honestly absent). It is
	// the lister the picker calls to enumerate stored sessions. Unlike the
	// caps-gated overlays it is NOT gated on a ServerCapabilities bit — the picker
	// is available whenever a lister + authoritative transcript loader are wired
	// (a no-FS/cloud server with a durable SessionStore still has stored sessions).
	Sessions client.SessionPager
	// StorageHealth is the authenticated aggregate health surface. The capability
	// bit controls whether the Sessions panel advertises its maintenance tab.
	StorageHealth client.StorageHealthFetcher
	// Migration and Cleanup are deliberately distinct management seams. Their
	// server capability bits independently gate the semantics-preserving and
	// destructive workflows.
	Migration client.SessionMigrator
	Cleanup   client.SessionCleaner
	// SessionManagement mutates stored main-chat metadata. nil leaves rename/delete
	// undiscoverable even if a custom lister advertises those capabilities.
	SessionManagement client.SessionManager
	// Transcript is the authoritative snapshot-derived conversation surface used
	// by /sessions for both continuation and read-only inspection. Event replay is
	// optional activity and never substitutes for this seam.
	Transcript client.SessionTranscripter
	// Replayer is the optional durable-event-log activity surface used by live
	// delivery catch-up. It never attests conversation completeness.
	Replayer client.SessionReplayer
	// LiveStream is the LIVE per-session event feed (ADR 0075 Scenario 5): the server
	// pushes fire-result delivery notes for the active session as they occur. The ui
	// holds the interface (not a *Client) so it is injectable with a fake for offline
	// tests. nil disables the live bridge (the ui still renders deliveries via the
	// replay on a session switch/reload, just not live). *Client satisfies it.
	LiveStream client.LiveStreamer
	// MCPAuthorization is the distinct browser authorization surface. It never
	// shares the permission-approval stream or controls.
	MCPAuthorization client.MCPAuthorizationController
	// WorkspaceEnrollment is the distinct pre-prompt whole-bundle control.
	WorkspaceEnrollment client.WorkspaceEnrollmentController
	// OpenURL opens a presentation URL obtained only through MCPAuthorization.
	// Composition owns the OS integration; nil leaves the action unavailable.
	OpenURL func(context.Context, string) error
	// SelectionStore persists the picked model (last-used). nil disables persistence
	// (the pick still applies to the next create this run, just isn't remembered).
	SelectionStore SelectionStore
	Learning       LearningSettings // operator mecatl settings.yaml; nil disables /learning
	// Connect lists public saved remote-target metadata. It cannot authenticate,
	// launch a browser, or expose credentials; selection exits via ConnectRestartIntent.
	Connect ConnectController
	// ConnectOpen reopens /connect after a host-side login/dial failure. ConnectError
	// is host-sanitized display text only.
	ConnectOpen  bool
	ConnectError string
	// ConnectReason controls the auth-recovery affordance without exposing a
	// transport error or credential to the renderer.
	ConnectReason          client.AuthReason
	ConnectTarget          string
	ConnectResumeSessionID string
	// BearerBacked records credential provenance for classifying server auth
	// responses. It contains no credential material.
	BearerBacked bool
	// InitialModel is the persisted selection loaded at launch (composition-side,
	// from the state file). The picker seeds its active selection from it (the ●
	// marker) and the startup CreateSession carries it — AFTER the connect-time
	// ListModels reconcile clears it if its PROVIDER is no longer available. The
	// reconcile is provider-level only (issue #41): a model absent from the (possibly
	// embedded-floor) snapshot is still carried verbatim — the server validates it,
	// and a rejection falls back to the default loudly (connectFallbackMsg).
	InitialModel client.ModelSelection
	// WorkspaceDefault / GlobalDefault are the SEPARATE raw state-file values loaded at
	// launch (composition-side): the per-workspace entry (zero when none — see
	// WorkspaceDefaultSet) and the global `default:` block. They are display-only
	// provenance inputs for the /models picker's "current: <model> (<provenance>)"
	// line and the ★ global-default row marker — the ui derives a best-effort label
	// from client-held state, never a server round-trip. (InitialModel is the RESOLVED
	// Load result = workspace-or-global; these are the un-collapsed pieces.)
	WorkspaceDefault    client.ModelSelection
	WorkspaceDefaultSet bool
	GlobalDefault       client.ModelSelection
	// Clipboard reads the OS clipboard for ctrl+v paste (image-first, text-fallback).
	// nil cleanly disables ctrl+v image paste (same convention as nil MCP/Cmds);
	// main.go populates it with client.NewClipboard().
	Clipboard client.Clipboard
	Theme     theme.Theme
	// ThemeAutoDetect enables the terminal-background light/dark auto-detect
	// (ADR 0280): composition sets it true only when no explicit --theme/
	// MECATUI_THEME was supplied AND stdout is a real TTY (never on redirected
	// output, which must never see the OSC background-colour query). When true,
	// Init requests the terminal's background color (tea.RequestBackgroundColor)
	// and a light response switches the active theme to the built-in "solar"
	// theme; a dark or absent response keeps Theme as given. Explicit theme
	// selection always wins — this field is simply never set true then.
	ThemeAutoDetect bool

	// StatusSource is composed outside ui. The UI only submits display facts and
	// consumes semantic snapshots through one Bubble Tea listener.
	StatusSource statusline.Source
	// LocalSessionContext optionally resolves ADR 0296's privileged local root.
	// The root is used only as a direct status-command CWD, never UI state.
	LocalSessionContext client.LocalSessionContextGetter

	// ClientBuild is the local mecatui build identity and Embedded selects the
	// local server identity path for /diagnostics.
	ClientBuild string
	Embedded    bool

	// Display-only context for the header bar.
	Server         string
	ConnectionMode string
	Workspace      string
	Mode           string
	Model          string
	// Resume is a statically validated existing chat selected before Bubble Tea
	// starts. Its authoritative transcript is adopted without CreateSession; nil
	// preserves the new-session default.
	Resume *client.ResumeSelection
	// BrowseSessions launches into the same inventory used by /sessions without
	// creating a throwaway session. New-chat creation remains gated on the startup
	// model-list reconcile.
	BrowseSessions bool
	// InitialPrompt is a CLI-supplied seed prompt auto-submitted once the first
	// session is ready (the equivalent of typing the prompt and pressing enter).
	// Empty = today's behavior (no seed). Cleared after the first use so a
	// /models restart or /clear never re-submits it. Populated by main.go from
	// -p/--prompt + --prompt-file.
	InitialPrompt string
	// DebugTarget is immutable launch metadata for a dedicated analysis session.
	// It is presentation/control state only; the client adapter owns wire projection.
	DebugTarget string

	// Version is the mecatui build identity, shown on the first-run welcome splash
	// (e.g. "v0.3.1", "dev+0123456789ab", or "dev"). Threaded from the shared
	// internal/buildinfo.BuildID; "" omits the version line.
	// Display-only.
	Version string

	// NoBanner suppresses the rich first-run welcome splash (mascot + gradient
	// wordmark): the zero-state then renders the LEGACY plain card (title + prompt
	// hint + affordances). Set by --no-banner, --quiet, or a non-interactive stdin
	// (composed in main). Default false (full splash).
	NoBanner bool

	// Ctx is the program-level context; per-run stream contexts derive from it.
	Ctx context.Context //nolint:containedctx // stored to parent per-run stream cancels

	// NoAltScreen disables the alternate screen buffer, rendering inline in the
	// terminal's normal buffer. Default false (full-screen TUI on the alt screen).
	// Set true by the --no-alt-screen/--inline flag — a first-class user opt-out
	// for streaming the session into native scrollback (so it stays
	// searchable/scrollable after exit) — and by golden tests so the final frame
	// persists in the captured output instead of being cleared on exit.
	NoAltScreen bool

	// NoMouse disables mouse capture on the alt screen (View leaves MouseMode at
	// MouseModeNone), so the terminal's OWN click-drag selection works again — at
	// the cost of in-app mouse-wheel scroll and the in-app drag-select/copy layer
	// (selectable() returns false). Keyboard scroll is unaffected. Default false
	// (mouse captured). Set true by --no-mouse / MECATUI_NO_MOUSE=1. Inert under
	// NoAltScreen (mouse is already off inline). The escape hatch for terminals
	// that strip OSC52 or users who prefer native selection.
	NoMouse bool

	// NoWindowTitle suppresses the dynamic terminal window/tab title, leaving the
	// title at the bare "mecatui" (no phase word, no session title). Default
	// false (the title is dynamic: "<title> — <status word> mecatui"). Set true by
	// --terminal-title=off / MECATUI_NO_TERMINAL_TITLE=1 — the escape hatch for
	// terminals/multiplexers where a set title does more harm than good (or where
	// the per-phase churn is unwanted).
	NoWindowTitle bool

	// Debug enables every client-side diagnostic surface. DebugMouse, DebugSteer,
	// and DebugAsk remain narrow compatibility aliases for their original surfaces.
	Debug bool

	// DebugMouse turns on a footer diagnostic overlay when Debug is false: on every
	// mouse press/motion the footer-left is overridden with raw coordinates and
	// their content mapping.
	DebugMouse bool

	// DebugSteer traces steer correlation in the status line when Debug is false.
	DebugSteer bool

	// DebugAsk registers /debug-ask when Debug is false. The command injects a fake
	// long-args permission ask through the real reducer.
	DebugAsk bool

	// KeyOverrides maps a keyMap field name (e.g. "Agents") to its replacement chord(s).
	// nil = no overrides, byte-identical to today. Resolved+validated in composition.
	KeyOverrides map[string][]string
	// contains filtered or unexported fields
}

Deps are the ui's injected collaborators and presentation config. The ui imports client + theme only — never contracts/gen or any internal/... package; all proto contact happens behind Converser/SessionCreator.

type HitID

type HitID uint64

HitID is an opaque, frame-scoped handle for a clickable surface region. It has no model-wide meaning: only the surface that minted it during the current View frame may interpret it.

type LearningSettings

type LearningSettings interface {
	Advance() (fromLabel, toLabel, restart string, err error)
	AdvanceSensitivity() (fromLabel, toLabel, restart string, err error)
}

LearningSettings atomically advances the operator's completed-trajectory learning mode and returns display-ready labels plus restart guidance. Policy ordering stays outside ui.

type Model

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

Model is the root Elm model. It owns the conversation, the bubbles widgets, the renderer (glamour cache), the active run stream, and the per-run cancel func.

func New

func New(deps Deps) Model

New builds the root model from deps. It wires the widgets but does not connect; Init kicks off CreateSession.

func (Model) ActiveSessionID

func (m Model) ActiveSessionID() string

ActiveSessionID returns the currently bound opaque session ID. The process entry point reads it only after Bubble Tea has restored the terminal.

func (Model) ConnectRestartIntent added in v0.0.22

func (m Model) ConnectRestartIntent() (ConnectRestartIntent, bool)

ConnectRestartIntent reports a deliberate internal restart request. It is intentionally a value with only public metadata.

func (Model) Init

func (m Model) Init() tea.Cmd

Init starts the spinner and kicks off connect.

Connect SEQUENCING (§4 key-removed safety): when a model lister is wired, it fetches ListModels FIRST and lets the connecting-phase ModelsMsg reconcile the persisted selection against PROVIDER availability BEFORE firing CreateSession — so a removed provider key can never hard-fail the connect with InvalidArgument. The reconcile is provider-level only (issue #41): the model string rides through verbatim (the boot snapshot may be the embedded floor), the server validates it, and a server rejection degrades to the default loudly via createSessionCmd's fallback leg. With no lister wired (old server / persistence off) it fires CreateSession directly (the historical path, with an empty selection).

func (Model) Update

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

Update is the Elm reducer. It is split by message type; all model mutation and all glamour rendering happen here on the single update goroutine (the stream reader never touches the model). After most state changes it calls refreshView to re-render the conversation into the viewport.

Streamed deltas (AssistantDeltaMsg/ReasoningDeltaMsg) do NOT refreshView per token: they append to the conversation and mark the view dirty (markDirty), which arms a single one-shot renderTickMsg (~16ms ≈ one 60fps frame) that flushes the dirty view at most once per frame and disarms. The invariant is "ONLY the delta cases defer; every other transition force-flushes" — turn/tool/result/error AND the permission.ask gate all flush (via afterEvent/endRun), so the pending tail is always rendered before any boundary (the final frame and event ordering are unchanged; only the per-token re-render churn is coalesced). refreshView clears viewDirty, making "rendered ⟺ not dirty" an invariant.

Within each 16ms flush, the per-BLOCK render cache (renderer.blockCache, keyed on block.rev/width/expand) means only blocks whose rev, the wrap width, or the expand toggle changed actually re-render — in practice just the live tail block; every settled block joins the conversation string from cache. The selection splice and vp.SetContent still see the full joined string, so selection/scroll behaviour is unchanged.

func (Model) View

func (m Model) View() tea.View

View assembles the three-region layout (header / viewport / input / footer) into a tea.View. While a permission modal is open it overlays the modal, centred, over the conversation region. Bubble Tea v2 returns a tea.View struct (not a string); we set Content and request the alt screen.

type SelectionStore

type SelectionStore interface {
	Save(workspace string, sel client.ModelSelection) error
	SaveGlobalDefault(sel client.ModelSelection) error
}

SelectionStore persists + loads the client-side model selection (last-used). It is satisfied by a main-owned concrete type backed by an XDG state file; nil cleanly disables persistence (the active selection then lives only for the run). The ui touches no os/xdg itself — persistence is composition-side, like SessionCreator. Save is given the workspace so the store can key per-workspace; SaveGlobalDefault writes the workspace-agnostic global `default:` block (the model new/unseen workspaces inherit) — the picker's ctrl+g affordance.

type ServerInfoGetter added in v0.0.22

type ServerInfoGetter interface {
	GetServerInfo(ctx context.Context, providerID string) (client.ServerInfo, error)
}

ServerInfoGetter reads safe identity and sanitized diagnostic display data for the caller's already-known active provider from a remote server. It never returns connection instructions.

type SessionBoundConverser added in v0.0.24

type SessionBoundConverser interface {
	OpenConverseForSession(ctx context.Context, sessionID string) (*client.Stream, error)
}

SessionBoundConverser is an optional additive capability implemented by the official client to bind affinity metadata before the first frame.

type SessionCreator

type SessionCreator interface {
	CreateSession(ctx context.Context, sel client.ModelSelection, mode string) (string, client.Capabilities, client.ResolvedModel, error)
	// CreateSessionWithCarryover forks the current transcript with optional model
	// overrides; placement and omitted settings are inherited by the server.
	CreateSessionWithCarryover(ctx context.Context, sourceSessionID string, sel client.ModelSelection) (string, client.Capabilities, client.ResolvedModel, error)
	// ClearSession creates an empty-history successor. selector is nil for ordinary
	// /clear and points only to an opaque ListWorktrees result for a worktree switch.
	ClearSession(ctx context.Context, sourceSessionID string, selector *client.WorktreeSelector) (string, client.SessionSnapshot, error)
	// CloseSession ends a server-side session by id. The /models restart-now handoff
	// closes the OLD session before creating the new one so a model switch leaves no
	// orphaned server-side session. Best-effort: the caller proceeds with the new
	// create even if the close errors.
	CloseSession(ctx context.Context, id string) error
	// GetSession re-reads the EFFECTIVE resolved model for an existing session. The
	// footer context-meter heal (issue #66) fires it on a turn boundary while the
	// meter's denominator is still unknown — the create-time echo can carry a 0 /
	// curated-floor window for a session on a LIVE-ONLY model whose async live
	// model-list swap had not yet landed, and the server's ResolvedModel resolves to
	// the real live window once it has. SessionCreator is the canonical ui-injection
	// seam for it (the method also satisfies the narrower client.SessionGetter that
	// RefreshResolvedModelCmd consumes); *client.Client (via the sessionAdapter) and
	// the test fakes satisfy both.
	GetSession(ctx context.Context, id string) (client.SessionSnapshot, error)
	// SetMode asks the server to change the current session's permission mode and
	// returns the server-confirmed mode. Mid-turn attempts may be rejected; the ui
	// defers and retries at the next prompt boundary.
	SetMode(ctx context.Context, id, mode string) (string, error)
	// ForkSession creates a peer session from srcID's conversation-history snapshot
	// (ADR 0065) with an OPTIONAL reasoning-effort override (ADR 0068; empty
	// inherits the source's) and returns the new session id. The /effort fork-resume
	// handoff uses it: the transcript SURVIVES the effort switch because the fork
	// carries it (title is omitted — the fork inherits the source's title; provider
	// and model ALWAYS inherit). The caller owns closing the source session and the
	// GetSession refetch for the fork's resolved-model echo.
	ForkSession(ctx context.Context, srcID, reasoningEffort string) (string, error)
}

SessionCreator creates a server-side session and returns its id together with the server's advertised capabilities. *client.Client satisfies it (via the sessionAdapter); tests supply a fake. Keeping it an interface lets the ui be driven entirely offline. Capabilities is the proto-free relayed truth the ui stores for its honest discoverability affordances (Phase B); an older server yields the all-false zero value. ResolvedModel is the EFFECTIVE provider+model the server resolved the session to (echoed verbatim); the ui shows it in the header from turn zero, and an older server yields the zero value (no model segment).

Directories

Path Synopsis
Package platform reports host-platform facts the TUI adapts its presentation to.
Package platform reports host-platform facts the TUI adapts its presentation to.
Package prompttextarea owns the upstream textarea behind Mecatl's prompt-editor policy.
Package prompttextarea owns the upstream textarea behind Mecatl's prompt-editor policy.
Package welcome renders the first-run splash for the mecatui zero-state: a faithful mascot (half-block on any truecolor terminal, a zero-dependency kitty Unicode-placeholder high-res path on capable terminals), a gradient "mecatl" wordmark, and an info block.
Package welcome renders the first-run splash for the mecatui zero-state: a faithful mascot (half-block on any truecolor terminal, a zero-dependency kitty Unicode-placeholder high-res path on capable terminals), a gradient "mecatl" wordmark, and an info block.

Jump to

Keyboard shortcuts

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