Help us shape the future of Go! Take the 2026 Go Developer Survey

tui

package
v0.4.5 Latest Latest
Warning

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

Go to latest
Published: Oct 16, 2026 License: MIT Imports: 42 Imported by: 0

Documentation

Overview

Package tui implements the terminal user interface shell: the tview application, the transcript pane, and the input area.

OS-clipboard image source for the prompt: a small reader interface and its default platform-helper implementation that shells out to wl-paste / xclip / xsel / pngpaste to read an image off the system clipboard, feeding the pending-attachment pipeline via the CTRL-V key. A terminal cannot deliver image bytes through bracketed paste (it carries only text) and reading the OS image clipboard portably in pure Go is impractical under the no-CGO / single-binary posture, so this uses an optional local subprocess — the same shell-out posture codemeld already uses for VCS tools, within the no-extra-egress constraint.

/clear (alias /new) command: end the current session, flush it, swap to a fresh same-folder session, replay the in-memory AGENTS.md load record, and carry the active model selection forward.

/color <name> command: recolor the input area's two rules and prompt glyph to one of the six fixed palette colors for the current run only, validating the name against the closed palette and rejecting anything else with the typed input retained.

/compact command: replace the session history with one provider-generated summary carried into a fresh child session.

/connect <provider> command: validate the supplied provider id, reject the reserved browser-login provider and unknown ids with a one-line notice, and on a supported id open the masked API-key prompt and record slash_command{connect,success} only after the submitted key's persistence write is durable — all kept behind ConnectDeps so internal/tui imports neither internal/config nor internal/provider. A cancelled prompt records nothing; a failed write records a durable, secret-free slash_command{connect,error} instead, so no success record can outlive a write that never happened, and the entered key reaches only deps.Connect — never an event, notice, or error string.

/context command: builds a single in-memory snapshot of how much of the model's context window the current session has used — active provider/model, session token totals, whether those totals are incomplete, and the context-window size and percentage used when codemeld knows the window — and renders it as a modal panel.

exit command (alias /quit, and the bare exit/quit synonym): the clean-shutdown sequence — append the slash_command event, run the clean-shutdown flush, then print the resume hint and exit 0, or take the second-failure exit-1 path.

/mode <plan|build|auto-edits> command and the shift+tab mode-cycle key share one injected mode-switch seam (ModeDeps): both validate-then-switch the run-scoped interaction mode and update the same persistent "<mode> mode on (shift+tab to cycle)" hint row below the status line. The command additionally records a slash_command; the key path does not, mirroring the CTRL-E effort picker.

/model <id> command: validate the supplied model id against the active provider's models allowlist, recompute the effort per the fallback rule, then apply the change through the same persistence/event flow as /effort — refusing when no model is selected and recording a command-level error (with the typed input retained) on an unsupported id

/rename <name> command: persists the active session's user-assigned name into the metadata.title field (one Title field, not a separate session_name), writing through the active session's owning store without advancing updated_at (a rename is metadata housekeeping, not activity).

/resume command: open the in-TUI session picker for the current folder and, on selection, switch the running session to the chosen one — load + classify the target first, then for an equal-schema target cancel the in-flight stream, flush and hand off the prior session, lock it, and replay its launch/model/agent- instruction anchors; an older-schema or vanished selection leaves the originally-active session and its in-flight stream undisturbed. The command behaviour lives behind a ResumeDeps dependency-injection seam (mirroring /clear's ClearDeps) so it is driven end-to-end with fakes and no real terminal; the run-scoped production implementations, the BuildRegistry wiring, and the resume-sequence bridge are supplied by production wiring.

/settings command: open the read-only overlay listing the stored permission grants for this run. The command behaviour lives behind a SettingsDeps dependency-injection seam (mirroring /resume's ResumeDeps) so it is driven end-to-end with fakes and no real terminal. This slice ships the handler, its list surface, and the overlay unregistered: the production BuildRegistry wiring and the revoke control ship separately.

/status command: builds a single in-memory snapshot of session state at open time and renders the 11-item overlay.

Central palette of the TUI's intentional fixed-color accents. These are the only deliberate overrides of the terminal-default theme; every other primitive stays tcell.ColorDefault so it inherits the user's terminal palette. Do NOT add ColorDefault values here — a value belongs in this file only because it is a fixed accent that overrides the terminal default on purpose.

Maps registry dispatch onto the app's SubmitFunc accept/reject contract and routes plain (non-slash) prompts to an injected model-turn entrypoint.

/help command: renders the help panel listing the build's available commands and the keyboard shortcuts (TR-TUI-4).

In-memory, session-scoped input history with ↑/↓ navigation.

Pending image-attachment model and the file-reference → data-URI encoder shared by the '@' picker, pasted/typed path recognition, and the clipboard source: it resolves a referenced path under the project root, enforces the size cap, sniffs the image type, and builds the persisted image_url content part.

Input area: the multi-line prompt (a tview.TextArea), its "› " label and borderless top/bottom-rule chrome, and a cursor accessor Tab slash-name completion reads as a rune offset into the buffer.

CTRL-C priority handler: stop an in-flight stream first; else clear a non-empty input buffer; else exit via the /exit clean-shutdown path when a prior empty-buffer CTRL-C was within the 1500ms window; else show the "press Ctrl-C again to exit" hint and arm that window.

CTRL-D exit shortcut: empty input buffer exits via the clean-shutdown path (equivalent to /exit); a non-empty buffer is a no-op in phase 1.

CTRL-L screen repaint: re-renders the current screen from in-memory state to clear visual artifacts, routed through the app-level key dispatcher and a thin draw seam so it is unit-testable without a live terminal.

ESC key handling for the TUI.

Markdown renderer: parses assistant-reply markdown and emits absolute tview style tags.

Mouse-drag diagnostic logging, gated by the CODEMELD_MOUSE_DEBUG env var (a file path). Off by default and zero-cost when off; when set, it records the mouse/drag/ auto-scroll events the SimulationScreen test harness cannot reproduce, so a capture from a real terminal shows which pane owns a drag and why a scroll did or did not fire. See docs/tui-mouse-debugging.md and docs/environment-variables.md.

One-line TUI failure notices with remediation hints. These functions return strings only and never touch the session record stream, so the user-facing hints stay out of the persisted transcript.

pasteScreen normalizes terminal events at the tcell.Screen layer before tview's event loop sees them.

/connect provider picker overlay: lists connectable providers and confirms one into the direct connect flow.

CTRL-M model picker overlay: a single-column table of the provider×model cross-product of the composed config — models grouped under a non-selectable, bold provider header — with an inline ←/→ reasoning-effort control scoped to the highlighted model's supported set, the per-project settings-write path (and the cross-mode marker disclosure when the write would create a launch- directory marker), and a footer hint. A confirmed selection applies the chosen provider/model/effort through the provider-aware non-slash apply path — the same persistence/event flow /model uses, without emitting a slash_command — and the confirmation (plus any marker disclosure) lands as permanent session-transcript lines.

Live '/' slash-command autocomplete overlay: typing '/' as the first non-whitespace character of the prompt opens a picker listing the registry's commands as "/<name> <description>"; typing narrows it by prefix (canonical names and aliases, consistent with Tab completion), and selecting one submits the canonical "/<name>" command immediately through the normal submit path. It clones the '@' file-reference picker shape (picker_file.go): the same live change-hook trigger, overlay chrome, and focus lifecycle. Command metadata comes from the registry App already holds, so new commands appear automatically.

Package tui provides terminal user-interface components.

Folder-option containment and cross-mode marker applicability for the permission prompt.

Package tui provides the terminal user interface.

Blocked-prompt notifier: the bell and OSC 9 alert raised while a permission prompt holds the run.

Built-in command registry: canonical/alias command metadata, startup validation (TR-TUI-3), and slash-input dispatch that routes aliases to the canonical handler and rejects unknown or wrong-arity input without side effects.

CTRL-R history reverse-search overlay: searches the in-session input history backwards for the most recent entry containing the typed substring, renders that match above a search field, and accepts it into the input buffer on Enter. Phase 1 shows the single most-recent match only; cycling to older matches is deferred to phase 2. Empty history renders a one-line hint and opens no overlay.

Slash-command parser: classifies prompt input as a slash invocation, a bare exit/quit synonym, or ordinary (non-slash) text. Phase-1 syntax only — no quoting, escaping, flags, multi-line, or piping.

Tab completion for slash-command names: a pure helper that, given the input buffer text, the cursor position, and the command registry, decides whether Tab extends the typed name to a canonical command, extends it to the longest shared prefix of several candidates, lists those candidates, or does nothing. Argument-value completion is out of scope in phase 1.

Transcript pane: the ordered lines and one-line notice rendered above input.

App-owned transcript mouse selection: plain left-button drag selects transcript text, the range is highlighted with reverse video at draw time, and the selection is copied to the system clipboard on mouse-up — all while tcell's global mouse mode (needed for wheel scrolling) stays enabled.

Index

Constants

View Source
const ExceptionalSandboxNotice = "" /* 147-byte string literal not displayed */
View Source
const ShellBlockedPathsNotice = "⚠ Blocked paths are not enforced for a command you approve: it runs unchecked."

Variables

This section is empty.

Functions

func CapabilityCoveragePhrase

func CapabilityCoveragePhrase(storedSubject string) string

CapabilityCoveragePhrase renders what a capability grant covers — a plain-words verb followed by the capability's members, e.g. "writing (create + edit)" for write. It returns "" for a non-capability stored subject (an ordinary tool grant covers only its one tool), so the caller omits the coverage line. The member list is read from the matcher's exported source-of-truth table, so the wording cannot drift from what the grant actually folds.

func CapabilityCoverageSentence

func CapabilityCoverageSentence(storedSubject string) string

CapabilityCoverageSentence is the wording every prompt surface prints above its grant options when the grant folds a capability, so no two surfaces describe the same grant differently.

func CrossModeMarkerNotice

func CrossModeMarkerNotice(launchDir, projectRoot, runFolder, boundary string, wouldCreateFile bool, omission FolderOmission) (notice string, show bool)

CrossModeMarkerNotice computes the launch-dir-only cross-mode marker disclosure for the folder option. It returns the one-line notice and whether to show it. The notice is shown only when all of:

  • omission reports the folder option offered, since the notice describes that option;
  • boundary == "launch-dir-only", the only mode that can plant a new marker;
  • wouldCreateFile is true (the file did not exist before — an existing file is already a marker, so no new cross-mode surprise); and
  • <projectRoot>/.codemeld/settings.yaml is an absolute path that lands at the launch directory (<launchDir>/.codemeld/settings.yaml), i.e. the project root has collapsed onto the launch dir.

Otherwise it returns ("", false). The wording reuses the shared RenderCrossModeMarkerNotice helper so the prompt, the model picker, and the /effort and /model commands stay byte-identical (no duplicate string). launchDir, projectRoot, and runFolder are already-resolved absolute paths supplied by the caller; this function does not resolve or stat anything.

func FitCells

func FitCells(s string, maxCells int) string

FitCells bounds s to maxCells terminal cells, keeping a head and the distinguishing tail around a single ellipsis, cutting only on grapheme-cluster boundaries. internal/bootstrap shares it so the preview projection cuts the same way.

func LaunchBannerBar

func LaunchBannerBar(version string, width int) string

LaunchBannerBar renders the launch banner's name line as a teal-filled bar: "◆ codemeld" in bold white, then "<version>" immediately after it in a dim, non-bold teal-tinted white (bannerVersionText) so the version is easy to find next to the wordmark instead of being lost at the right edge on a wide terminal; the remainder of the row is padded with spaces so the teal fill still spans the full terminal width. The fills paint a background — the chrome exception scoped to chrome (see colors.go). width is the terminal width at append time; the bar is not re-filled on a later resize (acceptable for launch-only branding, since the banner scrolls away as the conversation grows). The version is tview.Escape'd defensively, though it is a build constant. A width too small to hold the wordmark + version keeps them adjacent with no trailing pad. The caller passes the result as the first AppendBanner line; the bg/fg/bold tags are live markup that AppendBanner's raw styled-append seam renders.

func LaunchBannerMeta

func LaunchBannerMeta(provider, model, effort, dir string) string

LaunchBannerMeta renders the launch banner's metadata subtitle as the single line under the teal name bar: the active provider · model · effort · launch-dir segments joined by " · ", each in a fixed cool blue-grey that lightens left→right (bannerMeta* in colors.go) so the line reads as one dim secondary ramp matching the design sample rather than a flat run. When no model is selected (model == ""), provider and effort are empty, so the line collapses to "no model selected · <dir>". Every segment value is tview.Escape'd because the banner is appended through the raw AppendStyled seam (AppendBanner): only the color tags are intentional markup, so a path or model id carrying "[tag]"-like text must render literally. The caller passes the result as a banner line; the fg tags are live markup that AppendBanner's styled-append seam renders.

func PersistRemediationHint

func PersistRemediationHint(reason string, errCat fserrors.Category) string

PersistRemediationHint returns the literal remediation hint for a persist or session-write failure, chosen per the table. Hints for the re-read/lock reasons key on reason alone; hints for the write/rename/ parent-dir reasons key on the failure's error_category. The hint is literal text with no field interpolation.

func PromptNotifySequence

func PromptNotifySequence() string

func PromptNotifyTTY

func PromptNotifyTTY(w io.Writer) bool

func ProviderHint

func ProviderHint(phase, reason string, httpStatus int) (string, bool)

ProviderHint returns the phase-1 provider-failure remediation hint for a (phase, reason) pair. HTTP-phase rows additionally key on httpStatus: the http_no_body row matches specific 401/403/429 statuses on the event path, and the quota_exceeded row matches any 4xx/5xx status. The boolean reports whether a hint exists; ambiguous phases (empty_response, stream_drop, decode) and unlisted tuples return ("", false) so codemeld stays silent rather than guessing.

The quota_exceeded row is a render-only hint reason: the HTTP-error path derives it live from the 4xx/5xx status and body and passes it straight to the renderer. It is distinct from the durable event, which the turn layer persists as provider_request_failed{http,http_error} carrying the raw body. So quota_exceeded is not a persisted event reason and never arrives here with httpStatus absent (decoded 0). It is keyed on a live 4xx/5xx status per a 2026-05-27 user decision.

func RenderContext

func RenderContext(snap ContextSnapshot) string

RenderContext returns the deterministic /context panel text: active provider/model, session token totals, and the context-window size with the percentage used when the window is known. An unknown window renders an explicit "unknown" fallback and no percentage; incomplete counts add a notice while still showing available totals. It never includes the system message, environment block, or advertised tool list.

func RenderCrossModeMarkerNotice

func RenderCrossModeMarkerNotice(path string) string

RenderCrossModeMarkerNotice returns the one-line cross-mode marker disclosure (reused by /model and the permission-prompt folder option). Under launch-dir-only mode, when a write creates a .codemeld/settings.yaml that did not exist before, this warns that the file will mark the launch directory as a project root if discovery.boundary is later set to project-root. It names the absolute path so callers do not reinvent the wording.

func RenderHelp

func RenderHelp(reg *Registry) string

RenderHelp returns the deterministic help-panel text for reg: every command available in this build listed alphabetically by canonical name followed by the keyboard shortcuts and the environment variables a TUI user may set to change behavior (see the envVars criterion; not every var codemeld reads). Phase-gated commands not yet shipping in this build are omitted (TR-TUI-4). Each command line shows the canonical name, slash-prefixed aliases (if any), and the description.

func RenderLocalSettingsIgnoreNotice

func RenderLocalSettingsIgnoreNotice(localPath, lockPath string) string

RenderLocalSettingsIgnoreNotice names the two paths and nothing else: codemeld never reads or writes an ignore file, so the notice is the whole mechanism.

func RenderPersistFailureNotice

func RenderPersistFailureNotice(targetPath, reason string, errCat fserrors.Category) string

RenderPersistFailureNotice returns the single logical line for a permission_persist_failed degradation: the session-only lead, the target path, the failure reason, and the remediation hint. The terminal is responsible for wrapping a line wider than its width.

func RenderProjectRootMarkerNotice

func RenderProjectRootMarkerNotice(path string) string

func RenderProviderRequestFailedAfterAttemptsNotice

func RenderProviderRequestFailedAfterAttemptsNotice(verbatim, phase, reason string, httpStatus, attempts int) string

RenderProviderRequestFailedAfterAttemptsNotice returns the provider failure notice used after auto-retry exhausts, prefixing line 1 with the number of attempts while preserving the normal phase/reason hint behavior.

func RenderProviderRequestFailedNotice

func RenderProviderRequestFailedNotice(verbatim, phase, reason string, httpStatus int) string

RenderProviderRequestFailedNotice returns the TUI surface for a provider_request_failed event. Line 1 is the caller-supplied failure line, rendered unchanged — for an HTTP error this is the friendly provider message extracted from the body, not the verbatim raw body; for transport/body failures it is the synthetic surface. A second "→ hint: " line is appended only when ProviderHint matches the (phase, reason[, http_status]) tuple; otherwise a single line is returned.

func RenderRevokeFailedNotice

func RenderRevokeFailedNotice(targetPath, reason string) string

RenderRevokeFailedNotice returns the single logical line for a grant revoke whose settings-file write failed. The live grant is deliberately left in place so memory and disk stay consistent; the user can retry once the cause is cleared.

func RenderRevokeSkipNotice

func RenderRevokeSkipNotice(targetPath string, holding PermissionRow) string

RenderRevokeSkipNotice returns the single logical line for a grant revoke that was skipped because the selected writable settings file does not contain the grant. holding is another row with the same grant (zero when none). Unlike RenderPersistFailureNotice it claims no session-only degradation — nothing was written and the live grant is unchanged.

func RenderSessionWriteFailedNotice

func RenderSessionWriteFailedNotice(targetPath, reason string, errCat fserrors.Category) string

RenderSessionWriteFailedNotice returns the single logical line for a session_write_failed event: same shape as the persist notice with the "session log unwritable" lead. Only write_error/rename_error/ parent_dir_error reasons apply to session files.

func RenderShareFailedNotice

func RenderShareFailedNotice(sharedPath, reason string) string

func RenderShareLocalRemovalFailedNotice

func RenderShareLocalRemovalFailedNotice(sharedPath, localPath, reason string, alreadyShared bool) string

func RenderShareRefusedNotice

func RenderShareRefusedNotice() string

func RenderStatus

func RenderStatus(snap StatusSnapshot) string

RenderStatus returns the deterministic, line-broken text of the /status overlay. The fixed 11-item order is preserved but the items are grouped under bold section titles with a blank line between sections for readability: Session (version, id, started), Location (launch dir, project root), Model (provider, model, effort), Context (AGENTS.md files, config layer sources), Usage (token totals + any incomplete notice). The project-root line carries the "(same as launch dir)" suffix when ProjectRoot matches LaunchDir; config layer sources are rendered farthest-first with their [global]/[project]/[local] kind labels. Because the status view has dynamic colors enabled, every runtime/data value — paths, ids, provider/model/effort, version, AGENTS.md paths — is escaped with tview.Escape so a value that happens to contain a [tag]-like sequence renders literally instead of being swallowed as color markup; only the fixed section titles are left as live markup.

func RenderStatusLine

func RenderStatusLine(state StatusState) string

RenderStatusLine returns the segment portion of the persistent bottom bar: provider / model / effort / launch-dir / sandbox state as two-tone filled rectangular segments — a darker leading cap then a lighter body — in a cooler slate ramp. The mode pill and the shift+tab hint now live on a separate second footer row composed by the App (RefreshModeHint), so this renders only the segment group. It reads the same StatusState the /status overlay and the launch banner read, so the bar never drifts from them. When no model is selected it renders an explicit "no model selected" segment followed by the launch dir and sandbox state, rather than blank fields.

The segment fills paint backgrounds — a scoped exception to the terminal-default theme limited to chrome (see colors.go). Every runtime value is tview.Escape'd before it enters the dynamic-color string so a launch dir, model id, or provider containing a "[tag]"-like sequence renders literally instead of being swallowed as color markup; only the fixed fill tags are live markup.

func ResumeHint

func ResumeHint(sessionID string) string

ResumeHint returns the clean-exit line that tells the user how to resume the just-ended session, e.g. "to resume this session run: codemeld --resume <id>".

Types

type AgentInstructionsReplayer

type AgentInstructionsReplayer interface {
	Replay(target *sessions.Store) error
}

AgentInstructionsReplayer replays the in-memory AGENTS.md load record into the new session as fresh agent_instructions_loaded events, so the new session opens with the same instruction discovery outcome as the just-closed one without re-reading from disk.

type App

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

App is the tview application shell: an event loop, a root layout with the transcript pane above the input area, and an injected submit hook.

func New

func New(submit SubmitFunc) *App

New constructs the application shell, stacking the transcript pane above the input area and binding Enter to submit. submit is the injected dispatch hook; a nil hook makes every submission a no-op that retains the input.

func (*App) ActiveStore

func (a *App) ActiveStore() *sessions.Store

ActiveStore returns the session store the app is currently recording into, reflecting any /clear swap. It may be nil before SetActiveStore is called.

func (*App) AddBlockedPathForTest

func (a *App) AddBlockedPathForTest(path string) bool

func (*App) AnswerQuestionCustomForTest

func (a *App) AnswerQuestionCustomForTest(question int, text string) bool

func (*App) AnswerQuestionForTest

func (a *App) AnswerQuestionForTest(question, option int) bool

func (*App) Append

func (a *App) Append(line string)

Append adds one line to the transcript pane, escaping it so any "[...]" in the text renders literally rather than being parsed as a tview style tag now that the transcript has dynamic colors enabled. All ordinary callers (command output, model-picker disclosure) go through here; the launch banner is the one intentional styled producer and uses AppendBanner instead.

func (*App) AppendBanner

func (a *App) AppendBanner(lines ...string)

AppendBanner adds the launch banner as one multi-line transcript block through the raw styled-append seam (AppendStyled): the banner's intentional tags are parsed by dynamic colors — the teal-filled name bar's "[fg:bg:b]…[-:-:-]" fill (LaunchBannerBar) and any styling on the remaining lines. The banner's display lines are joined with a single newline and committed as one entry so they stay adjacent — the transcript's block separator (one blank line) is inserted only between entries, never inside this one, keeping the banner a tight header. Used by the bootstrap banner (StartTUI and /clear's transcript reset).

func (*App) AppendCompletionFooter

func (a *App) AppendCompletionFooter(records []sessions.Record)

AppendCompletionFooter appends the "provider - model - duration" footer for the most recently completed successful turn in records, taking provider/model from the turn's model_turn event and the duration from its final turn_latency event. It is a no-op when the latest turn produced no visible assistant reply or is missing either durable record — interrupted, failed, and tool-call-only turns show no footer. The turn controller calls it off the tview event-loop goroutine after a natural completion, so it marshals through the tview queue like the other stream-surface methods; the FinalizeStream that committed the reply is queued first, so the footer lands directly after it. The footer is a transcript-only metadata block: it is never recorded, so it cannot become model input or a durable user/assistant message.

func (*App) AppendEditPatch

func (a *App) AppendEditPatch(patch string)

func (*App) AppendStreamDelta

func (a *App) AppendStreamDelta(delta string)

AppendStreamDelta accumulates one incremental chunk of the in-progress assistant reply. While Run is active the mutation is marshaled through the tview queue without a draw and a single timer renders the accumulated text; before Run it renders on arrival. It returns once the delta has been applied but, unlike the per-delta render it replaces, not once it has been drawn. Like PromptUser it must not be called from a tview event-loop callback while the loop is running.

func (*App) AppendStyled

func (a *App) AppendStyled(line string)

AppendStyled adds one pre-styled line to the transcript without escaping, so intentional tview style/color tags are parsed by dynamic colors — e.g. the launch banner's "[::b]…[::-]" bold. (Mode switches no longer append here: their colored hint renders into the persistent mode-hint row instead.) It is the single raw append seam: callers must pass only controlled, non-user text (a closed set of literals), since nothing here escapes "[...]" against style-tag injection; every other transcript string is escaped via Append.

func (*App) AppendToolActivity

func (a *App) AppendToolActivity(lines []string)

AppendToolActivity renders one tool-call round's about-to-run calls inline in the transcript, just before the calls are dispatched, so a multi-step turn shows what is about to run (e.g. "Edit path", "Run: go build") instead of going silent. It first finalizes any in-progress assistant reply so the model's pre-tool text commits above the activity block, then appends the escaped lines as one transcript entry. Like the other stream-surface methods it is the cross-package entry point the provider-turn runtime calls off the tview event-loop goroutine, so it marshals through streamUpdate while Run is active and mutates directly before Run. Tool activity is display-only and never recorded.

func (*App) ApplyInputAccent

func (a *App) ApplyInputAccent(color tcell.Color)

ApplyInputAccent recolors the input's prompt glyph and both horizontal rules to a run-scoped accent and requests a redraw so the change is visible immediately. The accent lives only on the input field for the running session — nothing is persisted and it is not carried across --resume. Global tview.Styles and every surface outside the input area are left untouched. The /color command drives this after resolving a palette name to color.

func (*App) AskClarificationForTest

func (a *App) AskClarificationForTest(text string) bool

func (*App) AskQuestions

func (a *App) AskQuestions(ctx context.Context, req QuestionRequest) (QuestionResult, error)

Must not be called from a tview event-loop callback.

func (*App) AwaitQuestionFormForTest

func (a *App) AwaitQuestionFormForTest(timeout time.Duration) bool

func (*App) BeginShutdown

func (a *App) BeginShutdown()

BeginShutdown latches the discard-pending-renders state Stop latches, without ending the event loop. The exit sequence calls it before it cancels and drains the in-flight turn: that drain runs on the event-loop goroutine, so the surface calls a cancelled turn makes on its way out (the held-partial flush and interrupt seal) have to be dropped rather than queued, or the drain and the turn deadlock on each other. The dropped mutations are view-only — the turn's durable records still land on the store, which is what the drain exists to order before the final flush.

func (*App) BlockedOverlayText

func (a *App) BlockedOverlayText() string

func (*App) ChildAgentCalls

func (a *App) ChildAgentCalls(invocation string, lines []string)

func (*App) ChildAgentEnded

func (a *App) ChildAgentEnded(end events.ChildAgentEnd)

func (*App) ChildAgentStarted

func (a *App) ChildAgentStarted(start events.ChildAgentStart)

ChildAgentStarted, ChildAgentCalls, and ChildAgentEnded are the live child-agent surface the delegate tool drives from the child's own goroutine, so — like AppendToolActivity — they marshal through streamUpdate while Run is active and mutate directly before it. The block is display-only: the durable record is the child_agent_start/end pair the delegate already appends.

func (*App) ChooseBlockedDestinationForTest

func (a *App) ChooseBlockedDestinationForTest(dest BlockedDestination) bool

func (*App) ClearIndicator

func (a *App) ClearIndicator()

ClearIndicator hides the in-flight line and stops its elapsed ticker. The turn controller calls it off the event-loop goroutine when the loop returns — covering natural completion, a mid-stream drop, a user interrupt, and a background failure alike — so the teardown marshals through the tview queue. It is idempotent: a second call (or one with no indicator showing) is a no-op, so the stop channel is closed at most once and a stale ticker tick that lost the generation race cannot redraw.

func (*App) ClearNotice

func (a *App) ClearNotice()

ClearNotice clears the one-line transient notice without touching committed transcript history. submitCurrent calls it as a new submission is accepted, and the model-change paths (the CTRL-M/picker confirm and /model) call it on a successful apply, so a prior turn's notice cannot linger pinned below later output. It is for callers on the tview event-loop goroutine (like Notice); it never affects committed lines (t.lines).

func (*App) CommitProviderError

func (a *App) CommitProviderError(text string)

CommitProviderError commits a provider/turn request failure as a permanent in-place transcript line (escaped) tied to the turn that produced it, so the error scrolls with history rather than re-pinning below later turns the way the single bottom notice did. The provider-turn runtime calls it off the tview event-loop goroutine, so it marshals through the tview queue while Run is active and mutates directly before Run — exactly like AppendStreamDelta, FinalizeStream, and NoticeFromBackground. Ordering is preserved automatically: committing appends after whatever finalize/markInterrupted already sealed (a pre-stream failure lands after the submitted prompt; a post-seal failure after the sealed turn output).

func (*App) CommitRetryRecovered

func (a *App) CommitRetryRecovered(retries int)

CommitRetryRecovered commits a faint transcript breadcrumb after an auto-retry succeeds, so the user can see that a transient provider hiccup recovered without reading provider_request_retry events. It is called off the event-loop goroutine.

func (*App) ConfirmModelRowForTest

func (a *App) ConfirmModelRowForTest(row ProviderModel, effort string)

ConfirmModelRowForTest runs Enter-on-a-row for row without the overlay, so a cross-package test can drive the CTRL-M confirm path over production deps the way SelectConnectProviderForTest drives /connect. Production reaches confirmModelSelection through the key path.

func (*App) ConnectPickerOpen

func (a *App) ConnectPickerOpen() bool

ConnectPickerOpen reports whether the connect provider picker is currently mounted. It is a cross-package test seam for production /connect wiring.

func (*App) ConnectPickerRows

func (a *App) ConnectPickerRows() []ConnectProviderOption

ConnectPickerRows returns the currently mounted connect picker rows, or nil when the picker is closed. It is a cross-package test seam for production /connect wiring.

func (*App) DiscardHeldPrompts

func (a *App) DiscardHeldPrompts()

func (*App) EffortPickerOpen

func (a *App) EffortPickerOpen() bool

EffortPickerOpen reports whether the effort picker overlay is currently mounted. It mirrors ModelPickerOpen as the observable bootstrap's StartTUI assembly test reads to prove a bare /effort opened the picker through the wired EffortDeps.OpenPicker; production code drives the picker through the key/command paths and never needs to poll this.

func (*App) EnqueuePrompt

func (a *App) EnqueuePrompt(text string, parts ...sessions.ContentPart)

func (*App) FilePickerOpen

func (a *App) FilePickerOpen() bool

FilePickerOpen reports whether the file-reference picker overlay is currently mounted. It mirrors EffortPickerOpen/ModelPickerOpen as the observable a simulation test reads to prove a typed '@' opened (or a paste did not open) the picker; production drives it through the change hook and key paths and never polls this.

func (*App) FinalizeStream

func (a *App) FinalizeStream()

FinalizeStream closes a naturally completed streamed assistant reply, committing the in-progress line into the transcript (a no-op when no reply is streaming) so the next streamed reply starts a fresh buffer instead of merging onto the completed one. It is the cross-package finalizer the provider-turn runtime calls when a turn ends without cancellation, and like AppendStreamDelta it marshals through the tview queue while Run is active and so must be driven off the event-loop goroutine.

func (*App) FlushActive

func (a *App) FlushActive() error

FlushActive satisfies CommandHost: it runs the injected command-event flush hook so a /status, /help, or placeholder command makes its slash_command record durable. With no hook installed (unit tests) it is a no-op.

func (*App) HandleCtrlC

func (a *App) HandleCtrlC()

HandleCtrlC applies the priority order, top to bottom:

  1. If a stream is in flight, stop it via the ESC handler's source-parameterized path with source "ctrl-c" (retained content marked interrupted, stream_interrupted

appended, flush). It does not clear input or exit.

  1. Else if the input buffer is non-empty, clear it (and any pending exit hint).
  2. Else if a prior empty-buffer CTRL-C was within ctrlCExitWindow, exit via the /exit clean-shutdown path (the bare "exit" synonym through the submit hook),

so the canonical slash_command{name:"exit"} record, the flush, the hint / exit-0, and the second-failure exit-1 all come from

   /exit rather than being duplicated here.
4. Else show the "press Ctrl-C again to exit" hint and arm the window.

"Empty" is the literal buffer (GetText() == ""); a whitespace-only buffer counts as non-empty and takes step 2, so CTRL-C never exits over text the user can see.

func (*App) HandleCtrlD

func (a *App) HandleCtrlD()

HandleCtrlD applies the CTRL-D shortcut. On an empty input buffer it exits via the clean-shutdown path by dispatching the bare `exit` synonym through the installed submit hook, so the canonical slash_command{name:"exit", args:[], outcome:"success"} record and the rest of the /exit sequence (clean-shutdown flush, resume hint on success, second-failure exit-1) come from the /exit handler rather than being duplicated here. On a non-empty buffer it does nothing: the typed text is left untouched and no event, flush, notice, or shutdown request occurs.

"Empty" is the literal buffer (GetText() == ""); a whitespace-only buffer counts as non-empty and is a no-op, so CTRL-D never exits over text the user can still see. With no submit hook installed (New(nil) and unit tests before SetSubmit) the empty-buffer path is inert — there is nothing to dispatch through.

func (*App) HandleEsc

func (a *App) HandleEsc()

An open overlay owns ESC ahead of stream interruption: it is the modal the user is looking at, and it blocks the very turn ESC would otherwise cancel.

func (*App) HasQueuedPrompt

func (a *App) HasQueuedPrompt() bool

func (*App) IndicatorTokens

func (a *App) IndicatorTokens() (input, output int64)

IndicatorTokens returns the running uploaded (input) and downloaded (output) token figures the indicator is tracking. It is a cross-package test seam (like IndicatorVisible): bootstrap's production-path turn test reads it mid-turn to prove the figures advance from each round's exact Usage (not streamed deltas) and settle on the totals /status reports. Production updates them through SetIndicatorTokens off the live event loop; this is never called at runtime.

func (*App) IndicatorVisible

func (a *App) IndicatorVisible() bool

IndicatorVisible reports whether the in-flight indicator is currently showing. The before-draw chrome-row budget (chromeRows) reads it to subtract the indicator row from the transcript height only while a turn streams. It is also a cross-package test seam (like StatusText): bootstrap's production-path turn test asserts the indicator appears during a real turn and is cleared after it, which the durable events alone cannot show. The read is guarded by indicatorMu because the elapsed-refresh ticker goroutine also touches the indicator state.

func (*App) InputText

func (a *App) InputText() string

InputText returns the current input-buffer text. It is a cross-package test seam (like SubmitInput) that lets bootstrap's StartTUI integration test read the buffer back — e.g. to confirm CTRL-C cleared a non-empty buffer (step 2). Production reads the buffer through the focused input field off the live event loop; this is never called at runtime.

func (*App) MarkStreamInterrupted

func (a *App) MarkStreamInterrupted()

MarkStreamInterrupted closes the in-progress assistant reply with the interrupted marker (a no-op when no reply is streaming), so a cancelled stream is sealed as interrupted. It is the cross-package finalizer the provider-turn runtime calls when it observes the cancellation, and like AppendStreamDelta it marshals through the tview queue while Run is active and so must be driven off the event-loop goroutine. It also hides the in-flight indicator on the same off-loop path (idempotent with the turnController.run backstop) so the indicator never outlives the sealed stream.

func (*App) MarkStreamInterruptedFromCommand

func (a *App) MarkStreamInterruptedFromCommand()

MarkStreamInterruptedFromCommand seals a slash-command-cancelled stream from a command handler that is already running on the tview event-loop goroutine, and hides the in-flight indicator on that same loop path so /clear and /resume drop the indicator as soon as they record the interruption.

func (*App) ModeHintText

func (a *App) ModeHintText() string

ModeHintText returns the footer's second-row text (the filled "• <mode>" pill plus the shift+tab hint), with tview color tags stripped (GetText(true)). It is a cross-package read-only test seam (like StatusLineText): bootstrap's StartTUI assembly test reads it back after driving a mode switch through the production Notify path, proving the pill reflects the active mode rather than the transcript carrying the confirmation. It is the second footer row, distinct from StatusLineText's segments. Production renders the row through the view off the live event loop; this is never called at runtime.

func (*App) ModelPickerOpen

func (a *App) ModelPickerOpen() bool

ModelPickerOpen reports whether the model picker overlay is currently mounted. It is the observable bootstrap's StartTUI assembly test reads to prove the launch picker-required outcome opened the picker through OpenModelPicker; production code drives the picker through the key path and never needs to poll this.

func (*App) ModelPickerRows

func (a *App) ModelPickerRows() []ProviderModel

ModelPickerRows returns the provider×model rows the CTRL-M picker is currently displaying. While the overlay is open it reads the live session rows — the same slice rebuildModelPickerFromCatalog refreshes — so a test observes the visible overlay state, not just the underlying cache; a refresh that reloads the cache without rebuilding the session leaves this stale, which is the assertion the StartTUI refresh test relies on. When no picker session is open it falls back to the live selectable catalog (the list openModelPicker would build from). It returns nil when the provider-aware deps are not yet wired. Production drives the picker through the key path and never polls this.

func (*App) ModelPickerStatusText

func (a *App) ModelPickerStatusText() string

ModelPickerStatusText returns the open picker's in-box refresh status row, or "" when no picker is open. Like ModelPickerRows it is a read-only cross-package test seam; production writes the row and never polls it.

func (*App) Notice

func (a *App) Notice(text string)

Notice shows a one-line user-facing notice beneath the transcript. It is for callers already running on the tview event-loop goroutine; off-loop callers must use NoticeFromBackground.

func (*App) NoticeFromBackground

func (a *App) NoticeFromBackground(text string)

NoticeFromBackground shows a one-line notice from an off-event-loop caller, marshaling the transcript mutation through the tview queue while Run is active.

func (*App) NoticeSeq

func (a *App) NoticeSeq() uint64

NoticeSeq returns the count of notices emitted so far. The model-change paths read it around their apply to tell a notice the apply emitted from a stale pre-apply one.

func (*App) NoticeText

func (a *App) NoticeText() string

NoticeText returns the current one-line transient notice, or "" if none. The ESC key path reads it at runtime to tell its own interrupt hint from any other notice, and it is also the cross-package read-only seam (like TranscriptText) bootstrap's StartTUI assembly test reads.

func (*App) OpenConnectPicker

func (a *App) OpenConnectPicker(options []ConnectProviderOption, onSelect func(id string))

OpenConnectPicker opens the provider picker for a bare /connect command. It is a no-op while another overlay is open or when there are no rows to show. Selecting a provider calls onSelect with the provider id after the overlay is dismissed.

func (*App) OpenEffortPicker

func (a *App) OpenEffortPicker()

OpenEffortPicker opens the effort picker overlay from outside the global CTRL-E key path — the narrow seam a bare /effort drives through EffortDeps.OpenPicker so the command and the key share one picker. It delegates to openEffortPicker so the overlay-open / no-deps / no-model guards stay the single implementation, and is therefore inert until SetEffortDeps installs the deps, exactly like CTRL-E.

func (*App) OpenModelPicker

func (a *App) OpenModelPicker()

OpenModelPicker opens the provider-aware model picker overlay from outside the global CTRL-M key path. It is the narrow seam bootstrap drives for the two non-keyboard routes into the picker: the path-4 launch outcome and an in-TUI /resume recovery failure. It delegates to openModelPicker so the overlay/no-deps guards stay the single implementation — it is therefore inert until the model-picker deps are installed, exactly like CTRL-M.

func (*App) OpenSecretPrompt

func (a *App) OpenSecretPrompt(req SecretPromptRequest)

OpenSecretPrompt mounts the masked secret prompt as a modal overlay and moves focus to its field so typing, Enter (submit), and ESC/Ctrl-C (cancel) drive it. It is a no-op while another overlay is open, so a caller cannot stack it over an existing modal. The typed text lands only in the overlay's own masked field; the app's main input buffer, notices, transcript, history, and session events are never touched.

func (*App) PressCtrlE

func (a *App) PressCtrlE() bool

PressCtrlE drives the app-level CTRL-E global-key capture as if the key were pressed and reports whether the effort picker overlay opened. It is a cross-package test seam (like SubmitInput): bootstrap's StartTUI assembly test uses it to prove the production wiring installed the effort picker deps via SetEffortDeps — without them effortDeps.Model is nil and CTRL-E stays inert, so this returns false. Production CTRL-E always flows through handleGlobalKey off the live event loop; this is never called at runtime.

func (*App) PressCtrlM

func (a *App) PressCtrlM() bool

PressCtrlM drives the app-level CTRL-M global-key capture as if the key were pressed and reports whether the model picker overlay opened. It mirrors PressCtrlE: without the provider-aware deps wired, modelPickerDeps.Catalog is nil and CTRL-M stays inert, so this returns false. Production CTRL-M always flows through handleGlobalKey off the live event loop; this is never called at runtime.

func (*App) PressCtrlV

func (a *App) PressCtrlV() bool

PressCtrlV drives the app-level CTRL-V global-key capture as if the key were pressed and reports whether a pending image attachment resulted. It is a cross-package test seam (like PressCtrlE): bootstrap's StartTUI assembly test uses it to prove the production wiring reads the injected clipboard reader and stages an attachment. Production CTRL-V always flows through handleGlobalKey off the live event loop; this is never called at runtime.

func (*App) PressTab

func (a *App) PressTab(input string) (text, notice string)

PressTab sets input as the buffer (cursor parked at the end, as SetText does) and drives a Tab keypress through the input field's real handler — the same SetInputCapture → handleInputKey → completeName path the live event loop runs — then returns the resulting buffer text and the one-line notice (the multi-match candidate list, when Tab listed rather than completed). It is a cross-package test seam (like SubmitInput): bootstrap's StartTUI assembly test uses it to prove the production wiring installed the command registry via SetRegistry — without it App.registry is nil and Tab completion is inert, so the buffer comes back unchanged. Production Tab always flows through handleInputKey off the live event loop; this is never called at runtime.

func (*App) PromptIndependent

func (a *App) PromptIndependent(reqs []PromptRequest) ([]PromptDecision, error)

PromptIndependent walks one authorization group's approval questions and returns one decision per request, index-aligned with reqs. The window opens once and closes once — each question replaces the previous one in place — so the group has one visible prompt owner while every question still gets its own attributable choice. Each screen shows only the question it is deciding. A group of one question is the ordinary single prompt.

func (*App) PromptUser

func (a *App) PromptUser(req PromptRequest) (PromptDecision, error)

PromptUser presents the permission prompt as a modal overlay on the running app shell and blocks until the user picks an option, then dismisses the overlay and restores the prior focus, leaving the transcript and input buffer untouched. It is invoked from permission orchestration off the tview event-loop goroutine, so it drives the UI through QueueUpdateDraw and must not be called from a tview callback.

func (*App) QuestionContextForTest

func (a *App) QuestionContextForTest() string

func (*App) QuestionFormOpen

func (a *App) QuestionFormOpen() bool

func (*App) RefreshModeHint

func (a *App) RefreshModeHint(modeKey string)

RefreshModeHint records modeKey as the active interaction mode and re-renders the footer's second row (statusModeRow) so its filled "• <mode>" pill — followed by the shift+tab hint — reflects it (the pill has its own row, folding in the mode-hint row). It is seeded at launch from the active mode and called on every switch through the shared ModeDeps.Notify seam, so the row always shows the current mode and a switch no longer appends a confirmation line to the transcript. Callers run on the tview event-loop goroutine (the pre-Run seed and the Notify callback the key/command paths drive), so the text is set directly without marshaling through the event queue.

func (*App) RefreshSessionBadge

func (a *App) RefreshSessionBadge(store *sessions.Store)

RefreshSessionBadge shows store's title in the badge only while store is the active store, and is safe to call off the event loop.

func (*App) RefreshStatusLine

func (a *App) RefreshStatusLine()

RefreshStatusLine re-renders the footer's first row (statusBar) from the current active selection: the filled two-tone provider/model/effort/dir segments (RenderStatusLine), so the segments live-update after a model or effort change. The mode pill lives on the second row and is refreshed separately by RefreshModeHint, so this never touches it. With no StatusState installed (unit tests) it renders the row empty. Callers run on the tview event-loop goroutine (the model/effort apply paths and the pre-Run seed), so the text is set directly without marshaling through the event queue.

func (*App) RefreshStatusLineFromBackground

func (a *App) RefreshStatusLineFromBackground()

func (*App) RemoveBlockedPathForTest

func (a *App) RemoveBlockedPathForTest(path string) bool

func (*App) ReplaySessionRecords

func (a *App) ReplaySessionRecords(records []sessions.Record)

ReplaySessionRecords re-renders a resumed session's prior conversation into the transcript so /resume does not land on an empty-looking pane. Both entry points call it on the tview event-loop goroutine (cold-start before Run, or the in-TUI /resume continuation), so it mutates the pane directly rather than marshaling through the event queue; the projection and bounding rules live on replaySessionBlocks.

func (*App) ResetTranscript

func (a *App) ResetTranscript()

ResetTranscript wipes the transcript pane — committed lines, any in-progress streamed line, and the notice — re-renders it empty, and suppresses the single accepted-submit echo submitCurrent would otherwise append for the command that triggered the reset. /clear calls it (through the injected ClearDeps.Transcript capability) after the session swap commits, so the fresh same-folder session opens on a blank pane instead of the prior conversation and without a leftover "> /clear" line (visible-screen reset). It is called from the command handler, which runs on the tview event-loop goroutine, so it mutates the view directly like MarkStreamInterruptedFromCommand rather than marshaling through QueueUpdateDraw (which would deadlock the loop from inside a handler).

func (*App) ResumeRetry

func (a *App) ResumeRetry()

ResumeRetry returns the in-flight indicator to the normal Working state after a retry backoff completes and before the provider round is re-issued. It clears any idle state stored during the failed attempt rather than restoring it, so resume lands on clean Working copy.

func (*App) RevokeHighlightedPermissionForTest

func (a *App) RevokeHighlightedPermissionForTest() bool

RevokeHighlightedPermissionForTest revokes the highlighted grant exactly as the confirm's Revoke button does — a seam for bootstrap tests that drive the real revoke without a terminal. It returns false when the overlay is closed or the highlighted row backs no grant.

func (*App) RollbackPartial

func (a *App) RollbackPartial()

RollbackPartial removes the un-sealed in-progress reply before a discard-and-restart retry of a partial stream_drop, so the regenerated answer does not stack on the discarded partial. It is the cross-package surface seam the provider-turn runtime calls off the tview event-loop goroutine before re-issuing (turndeps.go binds it onto turn.Deps).

func (*App) Run

func (a *App) Run() error

Run starts the tview event loop and blocks until the application stops. It is the bootstrap handoff entrypoint a later task wires into the fresh-launch flow. While it runs, the streaming surface marshals off-loop transcript mutations through the tview queue; running tracks that window so the same methods mutate directly before Run and in unit tests.

func (*App) RunBackgroundAfterDraw

func (a *App) RunBackgroundAfterDraw(work func() error, complete func(error))

func (*App) RunOnEventLoopForTest

func (a *App) RunOnEventLoopForTest(fn func())

RunOnEventLoopForTest runs fn on the tview event-loop goroutine and waits for the redraw, so a test driving a running app can touch shared tview state without racing the draw loop. Test-only; it deadlocks if called from the loop goroutine.

func (*App) SecretPromptOpen

func (a *App) SecretPromptOpen() bool

SecretPromptOpen reports whether the masked secret prompt is currently mounted. It lets an out-of-package test assert a command opened the prompt (rather than completing synchronously) without reaching the unexported overlay state.

func (*App) SecurityHalt

func (a *App) SecurityHalt(records []events.SecurityStateHalted)

SecurityHalt seals any partial reply as interrupted output and pins the persistent protected-settings halt element beneath the transcript. Like MarkStreamInterrupted it must be driven off the event-loop goroutine; unlike it, the seal records nothing, so a security halt never writes a stream_interrupted event.

func (*App) SeedInput

func (a *App) SeedInput(text string)

SeedInput sets the input buffer to text without submitting it, the way typing (without Enter) would leave the field. It is a cross-package test seam (like SubmitInput): bootstrap's StartTUI integration test seeds a non-empty buffer to prove the production CTRL-C path clears it (step 2) — no existing seam can set the buffer without also submitting. Production input is always typed into the field off the live event loop; this is never called at runtime.

func (*App) SeedNextIndicatorStart

func (a *App) SeedNextIndicatorStart(start time.Time)

SeedNextIndicatorStart seeds the elapsed clock the next ShowIndicator uses with a production turn's submit-start — the instant the user's input was accepted — so the indicator's final visible elapsed matches the turn-latency measured from the same instant. A zero start leaves ShowIndicator on its default dispatch-start basis. The seed is consumed by the next ShowIndicator and applies to that turn only. The turn controller calls it on the tview event-loop goroutine at dispatch, so it sets the field directly under indicatorMu like the other indicator mutators.

func (*App) SelectConnectProviderForTest

func (a *App) SelectConnectProviderForTest(id string) bool

SelectConnectProviderForTest confirms the currently mounted provider row with id. It is a cross-package test seam for production /connect wiring.

func (*App) SelectPermissionForTest

func (a *App) SelectPermissionForTest(entry permissions.PermissionEntry) bool

func (*App) SendingPrompt

func (a *App) SendingPrompt(text string, parts ...sessions.ContentPart)

func (*App) SessionBadgeText

func (a *App) SessionBadgeText() string

func (*App) SetActiveStore

func (a *App) SetActiveStore(s *sessions.Store)

SetActiveStore installs s as the session the app records into. The production bootstrap sets it once before Run, and /clear's session swap (SessionSwapper) replaces it with the fresh same-folder store so subsequent commands and the exit hint observe the new session.

func (*App) SetClipboardReader

func (a *App) SetClipboardReader(r clipboardReader)

SetClipboardReader installs the OS-clipboard image reader the CTRL-V action reads through, replacing the default platform-helper reader New installs. It is a cross-package test seam: bootstrap's StartTUI integration test injects a fake that returns fixed image bytes so the real CTRL-V → attach → submit → persist → provider path can run without an OS clipboard or a helper binary. Production installs the default reader in New; this is never called at runtime.

func (*App) SetCommandFlush

func (a *App) SetCommandFlush(fn func() error)

SetCommandFlush installs the command-event flush hook bootstrap builds over the active session + sessionfailures. It is set once before Run, alongside SetActiveStore and SetSubmit.

func (*App) SetEffortDeps

func (a *App) SetEffortDeps(d EffortDeps)

SetEffortDeps installs the effort surface the CTRL-E picker reads and applies through. The production wiring supplies the implementations bound to the running app + bootstrap result; with none set the picker stays inert. It is set once before Run, alongside the other injected hooks.

func (*App) SetFollowupDebugContext

func (a *App) SetFollowupDebugContext(provider, model, mode string)

SetFollowupDebugContext arms the development-only suggest_followup trace for the next turn, stamping provider/model/mode onto the TUI-side debug events. The turn controller calls it at dispatch — only when the dev-debug gate is on, on the event-loop goroutine — so the surface/ghost/accept/clear seams can record which selection produced a suggestion. Until armed the seams emit nothing.

func (*App) SetHome

func (a *App) SetHome(home string)

SetHome installs the bootstrap-resolved home directory the file-reference picker uses to locate the global ~/.codemeld/global/codemeldignore file. Production wiring (StartTUI) passes BootstrapResult.Home; with none installed the '@' trigger stays inert, so App can be constructed in unit tests without bootstrap state. Set once before Run.

func (*App) SetIndicatorTokens

func (a *App) SetIndicatorTokens(input, output int64)

SetIndicatorTokens updates the running uploaded (input) and downloaded (output) token figures and redraws the line. The turn controller calls it off the event-loop goroutine (from the runner's exact per-round Usage callback only; the streamed progress signal is not wired in), so the redraw marshals through the tview queue. It is a no-op once the indicator is cleared, so a late callback cannot resurrect a hidden line.

func (*App) SetMaxPriceForTest

func (a *App) SetMaxPriceForTest(prompt, completion string) string

func (*App) SetModeDeps

func (a *App) SetModeDeps(d ModeDeps)

SetModeDeps installs the mode-switch surface the shift+tab cycle key reads and applies through. The production wiring supplies the implementation bound to the run-scoped mode state + running app — the same ModeDeps BuildRegistry binds to the /mode command — so both surfaces switch through one seam. With none set the key stays inert. It is set once before Run, alongside the other injected hooks.

func (*App) SetModelPickerDeps

func (a *App) SetModelPickerDeps(d ModelPickerDeps)

SetModelPickerDeps installs the provider-aware surface the CTRL-M model picker reads and applies through. The production wiring supplies the implementations bound to the running app + bootstrap result; with none set the picker stays inert. It is set once before Run, alongside the other injected hooks.

func (*App) SetProviderIdle

func (a *App) SetProviderIdle(displayName string, idle time.Duration)

SetProviderIdle switches the in-flight indicator into the idle-waiting state, replacing the generic Working copy with provider-specific waiting text whose timer counts the provider stream's idle duration (time since its last activity), not the turn elapsed. idle is that elapsed time since the provider's last activity; the idle line starts at that value and advances from it on each redraw. Retry outranks idle, so a call during a retry backoff is stored but not rendered until the retry resumes/clears. Safe to call from off the event-loop goroutine; it is a no-op after clear so a late idle callback cannot resurrect a hidden line.

func (*App) SetRegistry

func (a *App) SetRegistry(r *Registry)

SetRegistry installs the command registry Tab name-completion matches against — the same registry the submit adapter dispatches through, so Tab completes exactly the commands /help would list. The production wiring sets it once before Run, alongside SetSubmit; with none installed Tab completion is inert (unit tests, and before injection).

func (*App) SetRetentionForTest

func (a *App) SetRetentionForTest(text string, confirm bool) string

func (*App) SetScreenFactoryForTest

func (a *App) SetScreenFactoryForTest(factory func() (tcell.Screen, error))

SetScreenFactoryForTest replaces the screen constructor Run uses, so a test can start the real app lifecycle against a simulation screen instead of a terminal. Test-only; it must be called before Run.

func (*App) SetStatusLineState

func (a *App) SetStatusLineState(s StatusState)

SetStatusLineState installs the StatusState the bottom bar's segments render from and seeds the bar immediately. The production wiring passes the same statusState object /status and the launch banner read, so the three never drift. With none installed RefreshStatusLine renders nothing, so unit tests can construct App without bootstrap state. It is set once before Run.

func (*App) SetStreamCanceller

func (a *App) SetStreamCanceller(c StreamCanceller)

SetStreamCanceller installs the provider-turn cancellation handle the ESC / CTRL-C handlers reach to abort an in-flight streamed reply. The production handle is injected by the turn wiring; with none installed StreamCanceller returns nil and the surface stays inert (nothing to cancel). It is set once before Run, alongside the other injected hooks.

func (*App) SetSubmit

func (a *App) SetSubmit(s SubmitFunc)

SetSubmit installs the dispatch-backed submit hook after construction. The production submit adapter is the app's own CommandHost-backed dispatcher, so it cannot be built until New has returned the app; bootstrap calls New(nil) then SetSubmit to close that construction cycle without changing New's signature.

func (*App) SettingsOverlayText

func (a *App) SettingsOverlayText() string

SettingsOverlayText is a cross-package test seam for bootstrap's StartTUI assembly tests; it is never called at runtime.

func (*App) ShowIndicator

func (a *App) ShowIndicator(input, output int64)

ShowIndicator reveals the in-flight activity line with input/output as its starting token figures, records the turn start for the elapsed timer, and launches the refresh ticker. It runs on the tview event-loop goroutine, so it renders the line directly — routing through QueueUpdateDraw from that loop would deadlock; ClearIndicator is the teardown.

func (*App) ShowPanel

func (a *App) ShowPanel(text string)

ShowPanel opens the modal text panel over the main layout, moving focus to it so it captures input while open. It satisfies the CommandHost panel surface used by the /help handler.

func (*App) ShowRetryBackoff

func (a *App) ShowRetryBackoff(nextAttempt, totalAttempts int)

ShowRetryBackoff switches the in-flight indicator into the reconnecting state while the retry loop waits before the next provider attempt. It is safe for the turn runtime to call off the event-loop goroutine and is a no-op after clear.

func (*App) ShowSettingsTabForTest

func (a *App) ShowSettingsTabForTest() bool

func (*App) ShowStatus

func (a *App) ShowStatus(text string)

ShowStatus opens the /status overlay with text, moving focus to it so it captures input while open. The overlay is sized to fit the rendered snapshot (one row per text line, no chrome row — the overlay draws no rule of its own) so the temporary panel below the input area is just tall enough for its content. It is the embedding-app surface the /status handler reaches via CommandHost.ShowStatus.

func (*App) SortRowForTest

func (a *App) SortRowForTest() string

func (*App) StatusLineText

func (a *App) StatusLineText() string

StatusLineText returns the footer's first-row text (the segments), with tview tags stripped. It is a cross-package read-only test seam (like StatusText): bootstrap's StartTUI assembly test reads it back after driving a real model/effort apply through the production path, proving the segments reflect the new selection from the installed statusState — not an in-memory-only snapshot. Production renders the row through the view off the live event loop; this is never called at runtime.

func (*App) StatusModeKey

func (a *App) StatusModeKey() string

func (*App) StatusText

func (a *App) StatusText() string

StatusText returns the text the /status overlay is currently displaying, or "" when the overlay is not open. It is a cross-package test seam (like SubmitInput / PressCtrlE): bootstrap's StartTUI assembly test drives /status through the installed submit hook and reads the overlay text the app's own CommandHost.ShowStatus rendered, proving the config cache StartTUI assembled — not a cache the test rebuilt — feeds the overlay. Production /status always renders through ShowStatus off the live event loop; this is never called at runtime.

func (*App) StepSortForTest

func (a *App) StepSortForTest() string

func (*App) Stop

func (a *App) Stop()

Stop ends the tview event loop, returning control from Run. The production exit controller calls it as part of the clean-shutdown sequence before the process exits. Pending stream renders are discarded because that process exit owns cleanup of any callback already waiting for the event loop.

func (*App) Store

func (a *App) Store() *sessions.Store

Store satisfies CommandHost: dispatch appends each command's slash_command event to whichever session is active at invocation time, which is exactly the active-store reference ActiveStore returns.

func (*App) StreamCanceller

func (a *App) StreamCanceller() StreamCanceller

StreamCanceller returns the installed cancellation handle so the ESC / CTRL-C key handlers can call Cancel() on it. It is nil until SetStreamCanceller is called (and in unit tests), where the caller treats the stream surface as inert and cancels nothing.

func (*App) SubmitCurrent

func (a *App) SubmitCurrent() bool

SubmitCurrent drives the Enter-key submit path (submitCurrent) without mutating the input buffer, so a test can submit after staging a pending attachment via CTRL-V — unlike SubmitInput, which replaces the buffer (and clears attachments) first. It is a cross-package test seam; production submits flow through submitCurrent off the Enter key, and this is never called at runtime.

func (*App) SubmitInput

func (a *App) SubmitInput(input string) bool

SubmitInput drives the installed submit hook with input as if it were typed and entered, returning whether the hook accepted it (and clearing the field on accept) — the same accept/clear contract submitCurrent runs off the Enter key, minus the terminal. It is a cross-package test seam: a test outside internal/tui (bootstrap's StartTUI assembly) can exercise the real submit adapter the app installs via SetSubmit, instead of hand-assembling its own NewSubmitFunc. Production input always flows through submitCurrent off Enter; this is never called at runtime.

func (*App) SubmitQuestionFormForTest

func (a *App) SubmitQuestionFormForTest()

func (*App) SubmitSecretPromptForTest

func (a *App) SubmitSecretPromptForTest(secret string)

SubmitSecretPromptForTest sets the open prompt's field to secret and submits it, driving OnSubmit exactly as Enter would. It exists so an out-of-package test (e.g. internal/bootstrap's /connect wiring) can exercise the full submit path; in-package tests set the field and call submitSecretPrompt directly.

func (*App) SuggestFollowup

func (a *App) SuggestFollowup(label, prompt string)

SuggestFollowup receives an intercepted suggest_followup tool call: label is the imperative the agent proposes and prompt is the text to prefill when the user accepts with Tab. It is the cross-package surface seam the provider-turn runtime calls when the tool loop intercepts the call (turndeps.go binds it onto turn.Deps).

It stores prompt as the pending acceptance and shows label as dim ghost text in the empty input. The tool loop runs off the tview event-loop goroutine, so — like the other provider-runtime surface methods — it marshals the widget mutation through streamUpdate: QueueUpdateDraw while the loop is running (so the ghost text is drawn), or a direct mutation before Run / in unit tests where the queue is never drained. The next buffer change clears both the ghost text and the pending prompt (onInputChanged), so the suggestion never survives typing or submit.

func (*App) ToggleAttributionForTest

func (a *App) ToggleAttributionForTest() string

func (*App) ToggleDataCollectionForTest

func (a *App) ToggleDataCollectionForTest() string

func (*App) TranscriptText

func (a *App) TranscriptText() string

TranscriptText returns the full rendered transcript pane text — the committed lines, the in-progress streamed line, and the one-line notice — exactly as the view shows them. It is a cross-package read-only test seam (like StatusText): bootstrap's StartTUI assembly test reads it to confirm the production turn sink rendered the streamed partial reply into the assembled app's transcript and that ESC sealed it with the interrupted marker — an assertion the runner's retained counter and the durable event cannot make, because they observe bytes counted, not the transcript the user sees. Production renders the transcript through the view off the live event loop; this is never called at runtime.

func (*App) TurnEndPending

func (a *App) TurnEndPending() bool

func (*App) TurnEnded

func (a *App) TurnEnded(outcome string)

func (*App) UserMessageAdded

func (a *App) UserMessageAdded(abort <-chan struct{}, store *sessions.Store, text string, parts []sessions.ContentPart)

UserMessageAdded stops waiting for the event loop once abort closes, because the loop may be blocked in a /clear or /resume that is waiting for this turn to drain.

func (*App) UserMessagesUnsent

func (a *App) UserMessagesUnsent(store *sessions.Store, n, recorded int, outcome string) func()

type ApprovalDisclosure

type ApprovalDisclosure struct {
	Approved bool
	Reason   trust.Reason
	Withheld []string
}

ApprovalDisclosure.Withheld carries the launch notice's own wording for each input, so the two surfaces cannot describe the same launch differently.

type ArgSignature

type ArgSignature int

ArgSignature is a command's argument-arity contract: ArgNone accepts zero arguments, ArgOneRequired exactly one, ArgOneOptional zero or one, ArgRestRequired at least one (all words after the command name are joined into a single argument by dispatch).

const (
	ArgNone ArgSignature = iota
	ArgOneRequired
	ArgOneOptional
	ArgRestRequired
	ArgRestOptional
)

type BlockedDeps

type BlockedDeps struct {
	Rows                       func() []BlockedRow
	Picker                     BlockedPicker
	Remove                     func(BlockedRow) string
	Add                        func(BlockedRow) string
	AddAll                     func([]BlockedRow) string
	AddPath                    func(string, BlockedDestination) string
	ProjectDestinationsAllowed bool
	Destinations               BlockedDestinationPresenter
}

type BlockedDestination

type BlockedDestination string
const (
	BlockedGlobal        BlockedDestination = "global"
	BlockedProjectShared BlockedDestination = "project-shared"
	BlockedProjectLocal  BlockedDestination = "project-local"
)

type BlockedDestinationPresenter

type BlockedDestinationPresenter interface {
	PresentDestination(path string, add func(string, BlockedDestination) string)
}

func NewBlockedDestinationPresenter

func NewBlockedDestinationPresenter(app *App) BlockedDestinationPresenter

type BlockedPicker

type BlockedPicker interface {
	Present(rows []BlockedRow, remove func(BlockedRow) string, add func(BlockedRow) string, addAll func([]BlockedRow) string, relist func() []BlockedRow)
}

func NewBlockedPicker

func NewBlockedPicker(app *App) BlockedPicker

type BlockedRow

type BlockedRow struct {
	Kind       BlockedRowKind
	Text       string
	Path       string
	Source     string
	Occurrence int
	Group      string
}

type BlockedRowKind

type BlockedRowKind int
const (
	BlockedRowWarning BlockedRowKind = iota
	BlockedRowGroup
	BlockedRowGlobal
	BlockedRowLocal
	BlockedRowShared
	BlockedRowProject
	BlockedRowFloor
	BlockedRowMissing
)

type ClearDeps

type ClearDeps struct {
	Stream           StreamCanceller
	StreamTranscript StreamInterruptionMarker
	Session          SessionSwapper
	Model            ModelState
	Mode             ModeCarrier
	Agents           AgentInstructionsReplayer
	WriteFailure     WriteFailureSink
	// Transcript wipes the visible transcript and re-renders the post-clear header
	// once the swap has fully committed. Nil-safe: a nil capability skips
	// the reset, which is how the swap-sequence unit tests run with no transcript.
	Transcript TranscriptResetter
	// FlushCtx is the run context used for the prior session's final pre-switch
	// flush. Threading the real run context (rather than a fixed
	// background one) means a genuinely cancelled run makes that flush a no-op
	// that returns a non-write error, which refuses the swap so the prior
	// session's final records are not dropped before being made durable. Nil
	// means context.Background().
	FlushCtx context.Context
}

ClearDeps gathers the small capability interfaces NewClearHandler closes over. Each is independently injectable so the handler can be driven end-to- end with fakes; production wiring supplies the implementations bound to the running app + bootstrap result.

type ColorDeps

type ColorDeps struct {
	Apply func(color tcell.Color)
}

ColorDeps is the injectable seam NewColorHandler closes over: the run-scoped input-accent apply path (production binds App.ApplyInputAccent). Apply may be nil in unit tests and before the production wiring installs it, leaving a successful /color inert past its recorded event, mirroring the other command deps.

type Command

type Command struct {
	Name        string
	Aliases     []string
	Description string
	ArgSig      ArgSignature
	Phase       Phase
	Handler     HandlerFunc
}

Command is one registry entry: the canonical name, optional aliases that route to the same handler, the one-line description shown by /help, the argument signature, the phase it is available in, and the handler.

type CommandContext

type CommandContext interface {
	// Notice renders a one-line user-facing notice.
	Notice(text string)
	// RetainInput requests that the input buffer keep the user's typed text, e.g.
	// after a command-level validation error.
	RetainInput()
	// ShowPanel opens a modal text panel (e.g. the /help listing) that blocks
	// input until the user dismisses it with ESC, leaving the input buffer intact.
	ShowPanel(text string)
	// ShowStatus opens the /status overlay: a
	// borderless panel drawn directly under the input area with a single top rule,
	// rendering the snapshot text. It blocks input until ESC, identically to
	// ShowPanel but kept as a distinct surface so /status's visual treatment can
	// diverge from /help's panel.
	ShowStatus(text string)
	// Registry returns the registry that dispatched this invocation, so a handler
	// (e.g. /help) can render the live command set without a package global.
	Registry() *Registry
	// Store returns the active session store for this invocation. Handlers that
	// must append events on the prior session before yielding to an in-process
	// session swap (clear) use it to call events.Append directly
	// because AppendSlashCommand only covers the slash_command record.
	Store() *sessions.Store
	// AppendSlashCommand appends this invocation's slash_command event with the
	// canonical name and parsed args. errorDetail must be empty when
	// outcome is OutcomeSuccess and non-empty when it is OutcomeError; the
	// payload validator enforces this and AppendSlashCommand surfaces its error.
	AppendSlashCommand(outcome Outcome, errorDetail string) error
	// FlushActive makes the active session durable after a handler appended an
	// event that has no other flush boundary — the /status, /help, and
	// placeholder commands, whose slash_command record is otherwise left in memory
	// until some unrelated later flush (it ties slash_command writes to their durability point).
	// Handlers call it immediately after AppendSlashCommand and before their
	// next non-append step (opening an overlay/panel, or returning). The production
	// host flushes and, on an atomic-write failure, runs the per-turn
	// protocol (session_write_failed + notice) and returns nil so the run carries
	// the records forward; a nil error means the records are durable (or the
	// failure was handled). Commands that own a later flush of the same stream
	// (/clear, /effort, /model, /exit) do not call it.
	FlushActive() error
}

CommandContext is the per-invocation surface a handler uses to render a one-line notice, retain the user's typed input, and append the command's slash_command event. DispatchSlashInput binds the canonical name and parsed args before the handler runs, so AppendSlashCommand records the canonical name (never the alias the user typed).

type CommandHost

type CommandHost interface {
	Notice(text string)
	Store() *sessions.Store
	// ShowPanel opens a modal text panel that blocks input until dismissed;
	// the embedding app provides the panel surface.
	ShowPanel(text string)
	// ShowStatus opens the /status overlay; the
	// embedding app provides a panel surface distinct from ShowPanel's so the status
	// overlay's borderless top-rule treatment and below-input placement stay separate
	// from /help's box.
	ShowStatus(text string)
	// FlushActive flushes the active session for the command-event
	// flush (see CommandContext.FlushActive). The production app (internal/tui)
	// delegates to an injected hook that routes a write failure through
	// sessionfailures.HandleWriteFailure, keeping that delegation off the
	// sessionfailures→tui import cycle; a host with no flush wiring returns nil.
	FlushActive() error
}

CommandHost is the capability surface the embedding app provides to dispatch: a one-line notice sink and the active session store for event appends. The app supplies the production implementation; tests supply a fake.

type CompactDeps

type CompactDeps struct {
	Context context.Context
	// Must not mutate or cancel the active turn.
	TurnActive func() bool
	// Returns the whole summary request plus the provider/model identity it was
	// built from, so the client is resolved from that same identity rather than a
	// second read of the active selection.
	SummaryRequest func(records []sessions.Record) (req provider.Request, providerID, modelID string, ok bool)
	Client         func(providerID, modelID string) (provider.Client, bool)
	// Must return the underlying store flush error.
	FlushParent func(store *sessions.Store) error
	// When ok is false, release is nil; its owner must call it exactly once.
	AcquireExclusive func() (release func(), ok bool)
	// Runs work off the dispatch goroutine and delivers complete back on the
	// event loop, so only complete may touch the terminal session state.
	RunBackground func(work func() error, complete func(error))
	// A nonnil error can leave a child file; callers must not claim no child
	// exists.
	PrepareChild func(parent *sessions.Store, title string, childRecords, finalParentRecords []sessions.Record) (activate func(), err error)
}

type ConnectDeps

type ConnectDeps interface {
	// ConnectableProviders returns the providers a bare /connect can offer, in a
	// stable user-facing order, as id/label pairs. It carries no secret, endpoint, or
	// scaffold detail and performs no network I/O.
	ConnectableProviders() []ConnectProviderOption
	// OpenConnectPicker presents a non-blocking provider picker for a bare /connect.
	// Selecting a row invokes onSelect with that row's id; cancelling invokes no
	// callback and records no command event.
	OpenConnectPicker(options []ConnectProviderOption, onSelect func(id string))
	// IsProvider reports whether id names a known, connectable provider. It is the
	// lookup that separates an unknown id (rejected) from a supported one. It
	// returns only a bool so no deps-owned string can reach a user-facing notice.
	IsProvider(id string) bool
	NeedsManualKey(id string) bool
	// PromptAPIKey opens the masked API-key entry for the validated provider
	// id and invokes onSubmit with the entered key, or onCancel if the user dismisses
	// it. The production seam masks the key in the UI; the key reaches the handler
	// only as the onSubmit argument, so this seam records or echoes it nowhere. The
	// callbacks run on the event loop after the handler has returned to dispatch.
	PromptAPIKey(id string, onSubmit func(apiKey string), onCancel func())
	Connect(id, apiKey string, onComplete func(error))
}

ConnectDeps is the injectable seam /connect closes over so the handler can be driven end-to-end with a fake, keeping internal/tui free of internal/config and internal/provider. The production implementation lives in internal/bootstrap (its PromptAPIKey backed by the masked prompt, its Connect by the provider-entry writer); this slice binds none.

type ConnectProviderOption

type ConnectProviderOption struct {
	ID    string
	Label string
}

ConnectProviderOption is one connectable provider a bare /connect offers: a provider id and a user-facing display label, both secret-free. It carries no endpoint, key, or settings detail, so the provider list cannot leak deps internals into internal/tui. A bare /connect presents these choices in the provider picker.

type ContextSnapshot

type ContextSnapshot struct {
	Provider     string
	ModelID      string
	InputTokens  int64
	OutputTokens int64
	// Incomplete is true when some turn did not report usage (a model_response
	// anomaly), so the token totals are a lower bound.
	Incomplete bool
	// Meaningful only when CachedReported is true; a 0 here is otherwise the absence
	// of any provider-reported figure, not a measured zero.
	CachedInputTokens int64
	CachedReported    bool
	CachedPartial     bool
	// Window is the context-window size in tokens, or 0 when unknown.
	Window int64
}

ContextSnapshot is the resolved, render-ready /context state captured when the panel opens. Once built it is immutable; the panel shows this frozen snapshot until ESC is pressed.

func BuildContextSnapshot

func BuildContextSnapshot(state ContextState) ContextSnapshot

BuildContextSnapshot freezes the ContextState into a ContextSnapshot at open time, summing input/output tokens from the session's model_response records and flagging incomplete counts, matching the /status accounting semantics.

type ContextState

type ContextState interface {
	// Selection is the active provider and model id. Both are empty when no model
	// has been selected yet.
	Selection() (provider, modelID string)
	// Records is a snapshot of the active session's in-memory record stream, used to
	// sum token usage and detect anomalies, mirroring /status.
	Records() []sessions.Record
	// ContextWindow is the active model's context-window size in tokens, or 0 when
	// codemeld does not know it. No provider/catalog source exposes a window size
	// yet, so production reports 0 (unknown) today.
	ContextWindow() int64
}

ContextState is the in-memory state surface the /context snapshot reads at open time. Every method returns without touching disk; the production adapter lives in internal/bootstrap so internal/tui stays free of backend imports.

type DispatchResult

type DispatchResult struct {
	Status      DispatchStatus
	Canonical   string // resolved canonical name (StatusWrongArity, StatusHandled)
	Notice      string // one-line text dispatch rendered (StatusUnknownCommand, StatusWrongArity)
	RetainInput bool   // caller should keep the typed text in the input buffer
	Err         error  // handler error, if any (StatusHandled)
}

DispatchResult reports the outcome of one DispatchSlashInput call so the caller can decide whether to clear or retain the input buffer and whether to forward the line to the model.

type DispatchStatus

type DispatchStatus int

DispatchStatus classifies what DispatchSlashInput did with one line of input.

const (
	// StatusNotASlashCommand is ordinary prompt text; the caller routes it to the
	// model. No notice, no event.
	StatusNotASlashCommand DispatchStatus = iota
	// StatusUnknownCommand is a syntactically valid /<name> with no registry entry
	// a notice was rendered, input is retained, and no event was
	// appended.
	StatusUnknownCommand
	// StatusWrongArity is a recognized command invoked with the wrong argument
	// count: a notice was rendered, input is retained, no handler was
	// reached, and no event was appended.
	StatusWrongArity
	// StatusHandled means a command handler ran; Err carries its result.
	StatusHandled
)

type EffortApplier

type EffortApplier interface {
	// Apply persists level as model.active.effort — append
	// model_changed (only new_effort differing), flush, write
	// model.active via the shared persistence function, then mirror
	// model.last_used only after the active write succeeds — emitting
	// model_persist_failed / model_global_mirror_failed and the
	// persist-failure notice as prescribed, and setting the next model_turn source
	// to picker or picker_degraded (ending any resume carve-out). A
	// non-nil error is reserved for an internal append/flush failure the handler
	// must surface; persistence failures are not reported as errors here.
	Apply(level string) (EffortApplyResult, error)
}

EffortApplier runs the persistence/event transaction for a new effort on the current provider/model and updates the run's model_turn source. The production implementation lives outside internal/tui so it can call model.PersistPickerSelection and model.ActiveState.ApplyUserSelection without the import cycle. /effort calls Apply only after validation passes. This seam is deliberate rather than removable indirection: collapsing the durable settings write, provider lookup, and session-failure events it reaches into internal/tui would breach the package boundary the interface holds.

type EffortApplyResult

type EffortApplyResult struct {
	MarkerCreated bool
	MarkerPath    string
}

EffortApplyResult: MarkerCreated is true only under launch-dir-only mode when the model.active write created the launch directory's .codemeld/settings.yaml that did not exist before; MarkerPath is that file's absolute path, named in the cross-mode marker disclosure. The persist-failure notice and the source-state update are already handled inside Apply.

type EffortDeps

type EffortDeps struct {
	Model   EffortModel
	Applier EffortApplier
	// OpenPicker, when non-nil, opens the effort picker for a bare (zero-argument)
	// /effort — the same overlay CTRL-E opens, wired in production to
	// App.OpenEffortPicker. It carries the picker's own semantics (no-op while
	// another overlay is open, and the no-model-selected notice with no picker), so
	// the no-arg handler path delegates to it without recording a slash_command. nil
	// in unit tests and before the production wiring installs the opener, in which
	// case a bare /effort is inert (no panic, no event, no input retention).
	OpenPicker func()
	// Refresh, when non-nil, is called after a successful effort apply (both /effort
	// and the CTRL-E picker share this EffortDeps in production) so the persistent
	// status-line footer live-updates to the new effort. It is not called on
	// validation failures or apply errors. nil in unit tests and before the production
	// wiring installs App.RefreshStatusLine.
	Refresh func()
}

EffortDeps gathers the injectable capabilities NewEffortHandler closes over, so the handler can be driven end-to-end with fakes (mirroring ClearDeps); the production wiring supplies the implementations bound to the running app + bootstrap result. It stays the single bundle shared by /effort and the CTRL-E effort picker (installed once via App.SetEffortDeps) rather than giving each surface its own apply path, so the command and the picker cannot drift apart.

type EffortModel

type EffortModel interface {
	// ActiveSelection returns the current provider, model id, and effort, and
	// whether a model is selected in memory. /effort refuses when ok is false.
	ActiveSelection() (provider, modelID, effort string, ok bool)
	// SupportedEfforts returns cached efforts, or only effortNoReasoning when
	// capability metadata is absent.
	SupportedEfforts(provider, modelID string) []string
}

EffortModel is the in-memory active-model surface /effort reads. It mirrors the run-scoped active state and provider effort catalog without internal/tui importing internal/model or internal/provider, which would re-introduce the model→tui import cycle. The production implementation wraps *model.ActiveState and the run-scoped provider catalog lookup. This seam is deliberate rather than removable indirection: collapsing it into the concrete model read would pull internal/model and internal/provider into internal/tui, breaching the package boundary the interface exists to hold.

type ExitController

type ExitController interface {
	// SessionID returns the active session's id, read at exit time so an
	// in-process session swap (clear, /resume) is reflected in the hint.
	SessionID() string
	// FlushOnExit runs the clean-shutdown flush via
	// sessionfailures.CleanShutdownFlush: it attempts the atomic write, retries
	// exactly once on failure, and on the second failure writes the OP_FAIL "session-write-on-exit"
	// line to stderr and returns a non-nil error. A nil return means the
	// final session state is durable.
	FlushOnExit() error
	// Finish ends the run, stopping the TUI and propagating exitCode as the process
	// status. resumeHint, when non-empty, is the line printed to stdout
	// before a status-0 exit; it is empty on the status-1 second-failure path,
	// where the clean-shutdown flush has already written the stderr line and the
	// resume hint is suppressed.
	Finish(exitCode int, resumeHint string)
}

ExitController is the process-boundary surface the clean-exit sequence drives. It is injected rather than calling *App and internal/sessionfailures directly: internal/sessionfailures imports internal/tui for the write-failure notice, so a direct call to its CleanShutdownFlush from this package would form an import cycle. The production implementation, assembled in internal/bootstrap, binds FlushOnExit to sessionfailures.CleanShutdownFlush over the active store and Finish to the tview event-loop teardown plus process exit.

type FolderOmission

type FolderOmission int

FolderOmission is why the folder-grant option is hidden, or that it is offered. The prompt's explanation, facts, marker notice and option list all read this one value, so they cannot disagree about whether the option exists or misreport the cause.

const (
	FolderOptionOffered FolderOmission = iota
	FolderOptionOutOfRoot
	FolderOptionUntrusted
)

func OmitFolderOption

func OmitFolderOption(pathScope, projectRoot string, folderGrantEligibleOutOfRoot, projectUntrusted bool) FolderOmission

OmitFolderOption fails closed: an untrusted project hides the folder option outright, and an out-of-root path scope hides it unless folderGrantEligibleOutOfRoot explicitly opts it in, since offering a folder grant outside the project root is the authorization hazard this guards against. Trust is reported ahead of containment because it holds for every scope in the run, so the containment wording would misstate the cause. pathScope and projectRoot must already be cleaned, absolute and symlink-resolved.

func (FolderOmission) Hidden

func (o FolderOmission) Hidden() bool

type ForkDeps

type ForkDeps struct {
	// Must not mutate or cancel the active turn.
	TurnActive func() bool
	// Must return the underlying store flush error.
	FlushParent func(store *sessions.Store) error
	// A nonnil error can leave a child file; callers must not claim no child exists.
	CreateAndActivate func(parent *sessions.Store, title string, records, terminalParentRecords []sessions.Record) (cleanupWarning string, err error)
}

type HandlerFunc

type HandlerFunc func(ctx CommandContext, args []string) error

HandlerFunc runs a recognized command. args are the parsed arguments, already arity-checked by dispatch. The handler owns its command-specific side effects and the sequencing of its slash_command event via ctx; placeholder entries in the static list bind a shared default that records the event, and BuildRegistry replaces selected handlers with their real behavior.

func NewBlockedHandler

func NewBlockedHandler(deps BlockedDeps) HandlerFunc

func NewClearHandler

func NewClearHandler(deps ClearDeps) HandlerFunc

NewClearHandler returns the /clear (alias /new) handler that runs the session-switch sequence: cancel any in-flight stream, record the canonical slash_command event, flush the prior session, create and swap to a fresh same-folder session, seal the prior session, replay the agent instructions, and carry the model selection forward. On a pre-switch flush failure the swap is refused and the prior session stays active: an atomic-write failure routes through deps.WriteFailure, while a non-write error (e.g. a cancelled run) surfaces directly. The fresh session is created (and installed as active) before the prior store is sealed, so a fresh-session failure leaves the prior session active and still appendable rather than closing the active session out from under the run.

func NewColorHandler

func NewColorHandler(deps ColorDeps) HandlerFunc

NewColorHandler returns the /color handler. Dispatch enforces ArgOneRequired, so there is exactly one argument. An unknown color name prints a one-line notice naming the value and the six valid choices, retains the typed input, and records slash_command{outcome:error} (flushed, since the rejection has no later flush boundary). A known name records slash_command{outcome:success} and flushes it before the visible accent change — /color has no later persistence boundary of its own, so success and error are made durable here, mirroring /help and /status.

func NewCompactHandler

func NewCompactHandler(deps CompactDeps) HandlerFunc

func NewConnectHandler

func NewConnectHandler(deps ConnectDeps) HandlerFunc

NewConnectHandler returns the /connect handler. It rejects the reserved browser-login provider and unknown ids synchronously with a one-line static notice (retaining the typed input and recording a flushed slash_command{outcome:error}). On a supported id it opens the masked API-key prompt and returns to dispatch at once; the connect completes in finishConnect on submit, or does nothing on cancel. Nothing is recorded until a key is submitted and its write lands, so a cancelled or unsubmitted prompt claims no connection. The rejection notices/events are fully static, and the post-submit path records only the id IsProvider vouched for plus the static errConnectFailed text — so no secret, key, or deps-returned value can leak into any notice, event, or error string.

func NewContextHandler

func NewContextHandler(state ContextState) HandlerFunc

NewContextHandler returns the /context handler bound to an in-memory state source. Dispatch order mirrors /status: append the canonical slash_command success event, FlushActive to make it durable across the command-event boundary, then snapshot in-memory state and open the panel. Snapshotting only after the append+flush keeps the handler symmetric with /status and /help and opens the panel only once the durability boundary is crossed.

func NewEffortHandler

func NewEffortHandler(deps EffortDeps) HandlerFunc

func NewExitHandler

func NewExitHandler(ctrl ExitController) HandlerFunc

NewExitHandler returns the /exit handler (also reached by /quit and the bare exit/quit synonym through registry dispatch). It runs the four-step clean-shutdown sequence: (1) append slash_command{name:"exit", args:[], outcome:"success"} to the in-memory stream before the flush; (2) run the clean-shutdown flush; (3) on success print the resume hint and exit 0; (4) on the second-failure path exit 1 with no hint — the appended slash_command event is lost with the rest of the unflushed tail.

func NewForkHandler

func NewForkHandler(deps ForkDeps) HandlerFunc

func NewModeHandler

func NewModeHandler(deps ModeDeps) HandlerFunc

NewModeHandler returns the /mode handler. A bare or unrecognized argument prints the usage notice, retains the typed input, and records slash_command{outcome:error} with the same text; a valid value records slash_command{outcome:success}, switches the active mode, and updates the shared footer mode-hint row through Notify (the same row shift+tab updates), appending no transcript confirmation line. The success record is made durable by the switch's own interaction_mode flush (Apply), mirroring how /effort relies on its apply flush.

func NewModelHandler

func NewModelHandler(deps ModelDeps) HandlerFunc

NewModelHandler returns the /model handler. It refuses with the one-line no-model-selected error when no model is selected, rejects an id outside the active provider's models allowlist (retaining the typed input and recording slash_command{outcome:error} with the same text), and on a valid id recomputes the effort per the fallback rule, records slash_command{outcome:success}, and runs the apply transaction. The active provider is never changed; /model validates only against that provider's allowlist.

func NewRenameHandler

func NewRenameHandler(deps RenameDeps) HandlerFunc

NewRenameHandler returns the /rename handler. Dispatch enforces ArgRestRequired and joins all words after the command name into a single argument, so a zero-argument /rename is already rejected with the standard arity notice before the handler runs and exactly one (possibly multi-word) argument is present here. On a valid name the handler records slash_command{outcome:success}, then persists the title through the active-store seam — which also flushes the just-appended event — and only then renders a one-line confirmation notice. A persistence failure returns before the notice so no visible confirmation appears past a failed durability boundary; mirroring /status and /help, no second slash_command error event is appended on that path.

func NewResumeHandler

func NewResumeHandler(deps ResumeDeps) HandlerFunc

NewResumeHandler returns the /resume handler. It lists the current folder's resumable sessions; on a non-ENOENT enumeration failure it records slash_command{outcome:error} and renders the failure notice without opening the picker (step 3). Otherwise it records slash_command{outcome:success} and flushes it durable before opening the picker (the FlushActive contract: a slash_command with no later flush boundary is made durable before the next non-append step — here, opening the overlay and returning to dispatch — since the production picker's Present returns at once). It then opens the picker and returns; the empty-folder dismissal and the ESC dismissal have nothing left to do, while the on-selection session-switch sequence runs inside the picker's continuation callback (Resolution — Option A: a continuation, never a blocking call), so the event-loop submit path is not frozen while the picker is open. Because that post-selection sequence runs after the handler has returned, a residual failure it produces cannot flow back to the dispatch result; surfaceResumeErr renders it as a one-line notice, the only channel left (mirroring the CTRL-M model picker's confirm path). The pre-picker steps (lister/skipped/success-record-and-flush) still return their errors synchronously to dispatch, where a failure retains the input.

func NewSettingsHandler

func NewSettingsHandler(deps SettingsDeps) HandlerFunc

NewSettingsHandler returns the /settings handler bound to deps. It records the picker-open slash_command{success} and flushes it durable before opening the overlay and returning to dispatch — the same crash-safe ordering /resume uses, so an accepted /settings audit event is on disk before control returns to the event loop.

func NewStatusHandler

func NewStatusHandler(state StatusState) HandlerFunc

NewStatusHandler returns the /status handler bound to an in-memory state source. Dispatch order: record the canonical slash_command success event then snapshot in-memory state and open the overlay. Snapshotting after the event append is intentional — the slash_command record is itself a model_response-irrelevant event, so the snapshot order is observable only through its no-disk and ordering behavior, and the order keeps the handler symmetric with /help.

type History

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

History is the per-session record of submitted input lines, in submission order, with a cursor for ↑/↓ navigation and a saved live draft. It is memory only and never persisted: a fresh launch or `--resume` starts empty. Duplicates — including consecutive equal entries — are preserved.

func NewHistory

func NewHistory() *History

NewHistory returns an empty history capped at the phase-1 200-entry limit.

func (*History) Append

func (h *History) Append(entry string)

Append records entry as the newest submission, preserving duplicates and evicting the oldest entry once the cap is exceeded (FIFO). It resets any in-progress navigation back to the live line, so the next ↑ starts from the newest entry and re-saves the current draft.

func (*History) Down

func (h *History) Down() string

Down walks one step toward newer entries and returns the text the input field should show. From the newest entry it restores the saved draft and leaves navigation; once back on the draft it stays there. When not navigating it returns the saved draft and changes nothing.

func (*History) Snapshot

func (h *History) Snapshot() []string

Snapshot returns a copy of the recorded entries in submission order, oldest first. It is the read-only seam reverse-search reads through: the copy means a caller cannot mutate stored history or the navigation state.

func (*History) Up

func (h *History) Up(draft string) string

Up walks one step toward older entries and returns the text the input field should show. The first ↑ of a run saves draft (the current live line) and returns the newest entry; further ↑ walk farther back and stay put at the oldest entry. With no history it is a no-op that returns draft unchanged.

type ModeCarrier

type ModeCarrier interface {
	CarryForward(fresh *sessions.Store) error
}

ModeCarrier carries the active interaction mode into the fresh session and records interaction_mode{trigger:"clear"} on it, so the new session opens in the mode active when /clear was issued — with the source that mode originally came from — rather than resetting to mode.default. It takes the fresh store because, unlike the model carry-forward, it must emit a record into the new session. Wired as a distinct capability rather than folded into ModelState.CarryForward so mode carry-forward is not hidden inside model carry-forward. The production implementation lives in internal/bootstrap so its durable emit can route a write failure through sessionfailures without the internal/tui ↔ internal/sessionfailures import cycle.

type ModeDeps

type ModeDeps struct {
	Switcher ModeSwitcher
	// Notify, when non-nil, renders the current mode into the persistent mode-hint row
	// for the given mode key — shown by both /mode and the shift+tab cycle so a switch
	// through either surface updates the same row identically. nil in unit tests
	// and before the production wiring installs it, leaving the switch silent rather
	// than panicking.
	Notify func(modeKey string)
}

ModeDeps gathers the injectable capabilities the /mode command and the shift+tab key close over, so both entry points share one mode-switch + confirmation seam and can be driven end-to-end with fakes (mirroring EffortDeps). The audit keeps it as the single shared bundle rather than giving each surface its own switch path, so /mode and shift+tab cannot drift apart. Production wiring supplies the implementations bound to the running app + bootstrap result.

type ModeSwitcher

type ModeSwitcher interface {
	// Current returns the active mode key ("plan" | "build" | "auto_edits").
	Current() string
	// Apply records a runtime switch to modeKey and durably emits the
	// interaction_mode event; the switch takes effect on the next turn. A non-nil
	// error is an internal append/flush failure the caller must surface.
	Apply(modeKey string) error
}

ModeSwitcher is the run-scoped interaction-mode surface the /mode command and the shift+tab key drive. The production implementation (wired in internal/bootstrap) wraps mode.ActiveState and emits the interaction_mode{trigger:"user_command"} event; keeping it behind this string-keyed interface keeps internal/tui off an import of internal/mode and internal/events. The dependency-surface audit keeps this seam deliberately rather than as removable indirection: collapsing it into the concrete bootstrap switcher would pull internal/mode, internal/events, and the session-failure write path they reach into internal/tui, breaching the package boundary the interface exists to hold.

type ModelApplier

type ModelApplier interface {
	// Apply persists Selection{active provider, modelID, effort} as model.active
	// — append model_changed (only new_model_id and possibly
	// new_effort differing from the prior selection), flush, write
	// model.active via the shared persistence function, then mirror model.last_used
	// only after the active write succeeds — emitting model_persist_failed /
	// model_global_mirror_failed and the persist-failure notice as
	// prescribed, and setting the next model_turn source to picker or
	// picker_degraded (ending any resume carve-out). A non-nil error is
	// reserved for an internal append/flush failure the handler must surface;
	// persistence failures are not reported as errors here.
	Apply(modelID, effort string) (ModelApplyResult, error)
}

ModelApplier runs the persistence/event transaction for a new model id and effort on the active provider and updates the run's model_turn source. It mirrors EffortApplier; the production implementation lives outside internal/tui so it can call model.PersistPickerSelection and model.ActiveState.ApplyUserSelection without the import cycle. /model calls Apply only after validation passes. This seam is deliberate rather than removable indirection: collapsing the durable settings write, provider lookup, and session-failure events it reaches into internal/tui would breach the package boundary the interface holds.

type ModelApplyResult

type ModelApplyResult struct {
	MarkerCreated bool
	MarkerPath    string
}

ModelApplyResult mirrors EffortApplyResult: MarkerCreated is true only under launch-dir-only mode when the model.active write created a .codemeld/settings.yaml that did not exist before, and MarkerPath is that file's absolute path, named in the cross-mode marker disclosure. The persist-failure notice and the source-state update are already handled inside Apply.

type ModelCatalog

type ModelCatalog interface {
	EffortModel
	// Models returns the active provider's configured model id allowlist
	// in declaration order. It is both the set the supplied id must belong to
	// and the set named in the unsupported-model error.
	Models(provider string) []string
}

ModelCatalog is the in-memory active-model surface /model reads. It extends EffortModel with the active provider's configured model allowlist so /model can validate the supplied id and recompute the effort, without internal/tui importing internal/model or internal/provider (which would re-introduce the model→tui import cycle). The production implementation wraps *model.ActiveState and the composed provider config. This seam is deliberate rather than removable indirection: collapsing it into the concrete model read would pull internal/model and internal/provider into internal/tui, breaching the package boundary the interface exists to hold.

type ModelDeps

type ModelDeps struct {
	Model   ModelCatalog
	Applier ModelApplier
	// OpenPicker, when non-nil, opens the model picker for a bare (zero-argument)
	// /model — the same overlay CTRL-M opens, wired in production to
	// App.OpenModelPicker. Like CTRL-M it opens even when no model is selected (it is
	// how the first model is chosen) and is a no-op while another overlay is open, so
	// the no-arg handler path delegates to it without recording a slash_command. nil
	// in unit tests and before the production wiring installs the opener, in which
	// case a bare /model is inert (no panic, no event, no input retention).
	OpenPicker func()
	// Refresh, when non-nil, is called after a successful /model apply so the
	// persistent status-line footer live-updates to the new model/effort. It
	// is not called on validation failures or apply errors. nil in unit tests and
	// before the production wiring installs App.RefreshStatusLine.
	Refresh func()
	// ClearNotice, when non-nil, clears any pinned transient notice after a successful
	// /model apply, so a stale notice (e.g. a prior validation error or a
	// session-write hint) cannot linger pinned below the model change. It is not called
	// on the bare picker path, validation failures, or apply errors — those must keep
	// their notice. nil in unit tests and before the production wiring installs
	// App.ClearNotice, where the clear is a no-op (the submitCurrent clear still runs
	// in production for the /model command path).
	ClearNotice func()
	// NoticeSeq, when non-nil, reports the count of notices emitted so far so the handler
	// can tell a stale pre-apply notice from one the apply itself emitted (the
	// local-settings ignore notice, or the degraded active-write failure) and clear only
	// the former, even when the apply repeats the previous notice verbatim.
	// nil in unit tests, where the clear stays unconditional.
	NoticeSeq func() uint64
}

ModelDeps gathers the injectable capabilities NewModelHandler closes over, so the handler can be driven end-to-end with fakes (mirroring EffortDeps); the production wiring supplies the implementations bound to the running app + bootstrap result. ModelDeps stays the command-only dependency bundle, deliberately separate from the provider-aware ModelPickerDeps: /model validates against and applies on the active provider only, while the CTRL-M picker spans the provider×model cross-product and may switch providers, so merging the two into one applier would make /model inherit provider switching.

type ModelFavorite

type ModelFavorite struct {
	Provider string
	ModelID  string
}

type ModelPickerDeps

type ModelPickerDeps struct {
	Catalog ProviderModelCatalog
	Applier ProviderModelApplier
	// Refresh, when non-nil, is called after a successful CTRL-M apply so the
	// persistent status-line footer live-updates to the new provider/model/effort.
	// It is not called on apply errors. nil in unit tests and before the
	// production wiring installs App.RefreshStatusLine.
	Refresh func()
	// onComplete runs off the TUI event loop and must be marshaled back before UI access.
	RefreshCatalog func(providerID string, force bool, onComplete func(ModelRefreshResult))
	// FlushRefresh durably records a forced refresh. Unlike the general command flush,
	// it returns an atomic-write failure after routing it through the session-write
	// failure protocol: the picker must not claim the refresh was recorded until it
	// is on disk. Nil in unit tests falls back to the general command flush.
	FlushRefresh   func(*sessions.Store) error
	Favorites      func() []ModelFavorite
	ToggleFavorite func(provider, modelID string) ([]ModelFavorite, error)
}

ModelPickerDeps gathers the provider-aware capabilities the CTRL-M picker closes over, so it can be driven end-to-end with fakes (mirroring EffortDeps/ModelDeps). The production wiring supplies the implementations bound to the running app + bootstrap result; with none set the picker stays inert (CTRL-M is a no-op).

type ModelRecoverer

type ModelRecoverer interface {
	Recover(target *sessions.Store) error
}

ModelRecoverer runs the step-1 model recovery against the loaded session's record stream. On a clean recovery it adopts the recovered selection (overriding any prior in-memory selection, source "resume"); on a recovery failure it appends model_recovery_failed, clears the selection, and routes to the model picker. Those outcomes are handled inside the capability, so it returns a non-nil error only for an internal append/flush failure the handler must surface.

type ModelRefreshOutcome

type ModelRefreshOutcome int
const (
	ModelRefreshNoChange ModelRefreshOutcome = iota
	ModelRefreshUpdated
	ModelRefreshFailed
	ModelRefreshCredentialUnavailable
)

type ModelRefreshResult

type ModelRefreshResult struct {
	Provider string
	Outcome  ModelRefreshOutcome
}

type ModelSettingsTarget

type ModelSettingsTarget struct {
	SettingsPath string
	LaunchDir    string
	ProjectRoot  string
}

type ModelState

type ModelState interface {
	CarryForward()
}

ModelState carries the in-memory model selection forward across the session switch so the first model_turn in the new session reuses the prior session's last source value rather than re-running the run-start on-disk model lookup.

type NoticeRenderer

type NoticeRenderer interface {
	Notice(text string)
}

NoticeRenderer is the one-line notice sink that surfaces persist-failure and session-write-failure notices. *tui.App and the TUI command host satisfy it via their Notice method; abstracting it keeps the failure-handling packages testable and independent of which surface is active.

type OlderSchemaForker

type OlderSchemaForker interface {
	OfferFork(target *sessions.Store) error
}

OlderSchemaForker presents the read-only view and one-shot fork action for an older-schema selection. The originally-active session stays active in the background — it is neither locked away nor sealed — until the fork action itself succeeds, so an ESC out of the fork affordance returns the user to it.

type OpenRouterSettings

type OpenRouterSettings struct {
	Attribution       func() bool
	SetAttribution    func(bool) error
	DataCollection    func() string
	SetDataCollection func(string) error
	Sort              func() string
	SetSort           func(string) error
	MaxPrice          func(model string) (provider.MaxPrice, bool)
	SetMaxPrice       func(model string, p provider.MaxPrice) error
	ActiveModel       func() (providerID, modelID string)
	SortEstimate      func(model, sortMode string, fetched func()) (provider.OpenRouterEstimate, bool)
}

type Outcome

type Outcome string

Outcome is the slash_command event outcome.

const (
	OutcomeSuccess Outcome = "success"
	OutcomeError   Outcome = "error"
)

type ParseResult

type ParseResult struct {
	Kind       SlashParseKind
	Invocation SlashInvocation
}

ParseResult is the tagged result of ParseSlashInput. Invocation is meaningful only when Kind is KindSlashInvocation; it is the zero value otherwise.

func ParseSlashInput

func ParseSlashInput(input string) ParseResult

ParseSlashInput classifies one line of prompt input. It trims surrounding whitespace first, so leading spaces before "/" or around "exit"/"quit" do not change the outcome. Tokenization of slash arguments collapses runs of whitespace; there is no quoting, escaping, flag, or multi-line handling in phase 1.

type PermissionLister

type PermissionLister interface {
	List() []permissions.PermissionEntry
}

PermissionLister returns the stored permission grants to display. The production implementation wraps config.Cache.Permissions(); tests inject a fake.

type PermissionRow

type PermissionRow struct {
	Entry permissions.PermissionEntry
	Layer permissions.LayerKind
	Path  string
}

PermissionRow is one management copy of a grant. Layer and Path are empty for a session grant, whatever Entry.Layer says.

type Phase

type Phase int

Phase is the release phase a command becomes available in. It is a typed integer so the registry's {1, 2, 3} phase domain is enforced at registry construction rather than left as a bare int.

const (
	PhaseOne   Phase = 1
	PhaseTwo   Phase = 2
	PhaseThree Phase = 3
)

type PickerResult

type PickerResult struct {
	Selected bool
	Index    int
}

PickerResult reports what the user did in the picker: Selected is true when the user pressed Enter on a row (Index names it), false when ESC dismissed the picker without switching.

type PromptDecision

type PromptDecision struct {
	// Option is the grant option the user selected.
	Option PromptOption
	// StoredSubject is the final stored subject string for a grant-creating decision
	// (session/folder/global): the direct-command scope sub-choice (Exact/Subcommand/
	// Program) for a direct command, the backend generalized compound subject built by
	// permissions.EncodeCompoundSubject from the per-stage choices for a recognized
	// simple compound, or the capability subject (write/read*) carried
	// in PromptRequest.CapabilityStoredSubject for a structured-tool capability grant. It
	// is empty for allow-once, deny, and any subject whose grant stores the concrete
	// request subject (a non-capability tool, an ordinary literal command, or a compound
	// the backend refuses to generalize, e.g. one with a destructive stage), so no folded
	// subject is stored there.
	StoredSubject string
	// DenyEnvelope is the synthetic deny tool-error envelope, built by
	// permissions.BuildDenyEnvelope with the default deny reason. Deny is
	// a single action, so no user-supplied reason is collected and this is
	// always the default-reason envelope. It is set only for OptionDeny decisions and
	// is empty for every other option. The shell neither delivers nor persists it;
	// the backend routes it back to the model as the denied tool result and persists it as a
	// normal tool-result entry.
	DenyEnvelope string
}

PromptDecision is the outcome the prompt returns to permission orchestration. The subject-scope choice extends this struct along the same return path without changing the shell's option selection.

type PromptEntrypoint

type PromptEntrypoint func(input string, imageParts ...sessions.ContentPart) bool

PromptEntrypoint routes a plain (non-slash) prompt into the model-turn path, reporting whether the input was accepted (the app clears the input field) or must be retained (e.g. a pre-turn durability failure). This is only the routing seam; the production implementation (in internal/bootstrap) drives the pre-turn persistence contract before any provider request, and the provider turn loop itself is owned by later tasks. imageParts carries any pending image attachments so the plain-prompt persist path builds a content-part user message; it is the variadic tail so text-only callers are unchanged. A nil PromptEntrypoint rejects every plain prompt.

type PromptNotifier

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

func StartPromptNotifier

func StartPromptNotifier(ctx context.Context, after func(time.Duration) <-chan time.Time, notify func()) *PromptNotifier

func (*PromptNotifier) Stop

func (n *PromptNotifier) Stop()

Stop waits for an in-flight notify to return, so no write lands after the prompt it belongs to is gone.

type PromptOption

type PromptOption int

PromptOption identifies one grant option. The iota order is the fixed top-to-bottom order of promptOptionsInOrder, so the cursor's initial position and the rendered list order both derive from it; OptionAllowProject is host-only and is listed in hostOptionsInOrder instead.

const (
	// OptionAllowOnce authorizes the single in-flight invocation and creates no
	// grant; it is the default-highlighted option (option 1).
	OptionAllowOnce PromptOption = iota
	// OptionDeny refuses the call (option 2). It is a single action:
	// selecting it finalizes the deny immediately with the default-reason envelope,
	// with no second overlay to collect an optional reason.
	OptionDeny
	// OptionAllowSession creates a session grant (option 3).
	OptionAllowSession
	// OptionAllowFolder creates a folder grant (option 4).
	OptionAllowFolder
	// OptionAllowGlobal creates a global grant (option 5).
	OptionAllowGlobal
	OptionAllowProject
)

type PromptRequest

type PromptRequest struct {
	// Action is the human-readable action header: "Create file" /
	// "Edit file" for the file tools, "Run command" for shell subjects, "Run tool"
	// for any other structured tool. The raw subject kind is not displayed; it
	// lives only in the event log.
	Action string
	// For shell subjects, Subject is effective source text, not the matching subject.
	Subject string
	// Body is the payload shown verbatim with no truncation and nothing added — no
	// injected line numbers, no reformatting. Empty for a shell subject, whose command
	// is the Subject.
	Body string
	// Kind is the subject classification used to decide whether the
	// direct-command sub-choice applies. It is no longer displayed.
	Kind SubjectKind
	// CanonicalProgram is the canonical program token of a direct-command subject.
	// The scope subjects derive from it (including the Program-scope `<program> :*`
	// subject confirmed via the inline confirm step). Empty for non-direct-command
	// subjects.
	CanonicalProgram string
	// CanonicalArgs are the canonical argument tokens that follow the program
	// token, already normalized by the matching backend. They drive the
	// flag/positional classification and the Exact/Subcommand subject strings.
	CanonicalArgs []string
	// Pipeline is the full pipeline source text (all stages joined by " | ")
	// shown as display-only context on a direct pipeline-stage prompt, so the user
	// sees the isolated stage in Subject is one stage of a larger pipeline rather
	// than an unexplained standalone request (a safe-looking `sort`/`wc -l`). Empty
	// for a single direct command, a literal whole-command route, and every
	// structured tool. Display-only: it never affects matching or the stored grant.
	Pipeline string
	// WholeCommand marks a literal-route prompt as a whole-command approval — the
	// Subject is the entire command run verbatim, not one stage of a direct pipeline —
	// so the prompt frames a literal approval distinctly from a per-stage direct one.
	// False for direct commands (single or pipeline stage) and every structured tool.
	WholeCommand bool
	// BatchedStages, when it holds two or more entries, makes this one command-level
	// approval for a direct pipeline whose remaining (non-read-only) stages each still need
	// a decision. It is the ordered list of those stages'
	// canonical commands, shown verbatim under the full Pipeline so the user sees exactly what
	// is being approved — never a merged/summarized command. One option choice then applies to
	// every stage and a deny denies the whole pipeline. Empty (or a single entry, which the
	// batched producer renders as an ordinary single-stage prompt) leaves the prompt exactly as
	// before. Display-only: it never participates in matching or a stored grant.
	BatchedStages []string
	// BatchedEscalations is the per-stage scope-escalation warning for a batched approval,
	// index-aligned with BatchedStages. A non-empty entry holds the narrower already-approved
	// scope that the same stage's request now broadens (an ancestor of that folder), so the
	// batched prompt shows the same broaden-warning a per-stage prompt would; batching
	// must not hide trust-scope information. An empty entry means that stage does not broaden any
	// approved scope. Empty/nil for a non-escalating batch. Display-only, like BatchedStages.
	BatchedEscalations []string
	// ApprovalIndex and ApprovalTotal are this prompt's 1-based position in a walk of separate
	// approval questions and the number of questions in that walk. The overlay shows the position
	// only when ApprovalTotal exceeds 1, and discloses nothing about the questions that follow.
	// Both are zero for a prompt that is not part of such a walk. Display-only.
	ApprovalIndex int
	ApprovalTotal int
	// RunFolder is the resolved path_scope the command will execute in
	// (the cd-shifted path when a `cd <path> && ` prefix is present, not the
	// process cwd).
	RunFolder string
	// ProjectRoot is the resolved project root. When it differs from
	// RunFolder the prompt shows both and marks them different.
	ProjectRoot string
	// FolderSettingsPath is the absolute path the folder grant would be written
	// to (<project_root>/.codemeld/settings.yaml), shown for the folder option.
	// Empty when the caller has no folder option to describe.
	FolderSettingsPath string
	// FolderCoverage is the one-line statement of what the folder grant covers
	// (the resolved path_scope and its descendants), shown for the folder option.
	FolderCoverage     string
	OutOfRootFileWrite bool
	OutOfRootReadScope bool
	// CapabilityStoredSubject is the capability subject a grant should store when this
	// structured-tool prompt is approved with a grant-creating option. Dispatch supplies
	// permissions.CapabilityWriteTree for a file-tool member and CapabilityRead for an
	// out-of-project read, so those are the two values production sends. When set, a
	// session/folder/global selection returns it as PromptDecision.StoredSubject so the
	// stored grant folds the capability's sibling member tools at the scope, and
	// renderPromptFacts states that coverage. Empty for shell,
	// literal, direct commands (which use the scope chooser instead), tools that map to no
	// capability, and in-project reads — leaving the stored subject the concrete request
	// subject. It does not affect matching, the cache, or which options are shown.
	CapabilityStoredSubject string
	// LaunchDir is the resolved absolute directory codemeld was launched in. With
	// Boundary and WouldCreateSettingsFile it gates the launch-dir-only cross-mode
	// marker notice via CrossModeMarkerNotice; the prompt renders
	// no caller-supplied notice string, so the gate is always enforced here.
	LaunchDir string
	// Boundary is the resolved discovery.boundary mode ("launch-dir-only" or
	// "project-root"). Only "launch-dir-only" can plant a new cross-mode marker, so
	// it gates the notice.
	Boundary string
	// WouldCreateSettingsFile reports whether persisting a folder grant would newly
	// create <project_root>/.codemeld/settings.yaml (it did not exist before). An
	// existing file is already a marker, so no new cross-mode surprise and no notice.
	WouldCreateSettingsFile bool
	// ScopeEscalationNarrower, when non-empty, marks that this request's path scope is
	// strictly broader than (an ancestor of) a folder the user already approved for the
	// same subject; it holds that narrower already-approved scope. renderPromptFacts
	// then shows a salience warning naming it, so the broadening is not silent.
	// It changes nothing about matching or persistence — display only. Empty for every
	// non-escalating prompt.
	ScopeEscalationNarrower string
	// Compound, when non-nil, carries the recognized simple-compound decomposition
	// (its ordered direct-command stages and pinned operators) for a literal-command
	// subject the backend recognized as a simple compound. It gates the
	// per-stage subject-scope chooser: a grant-creating selection opens one chooser per
	// stage, each defaulting to the narrowest (Exact) scope, and the chosen per-stage
	// canonical subjects are encoded into the backend generalized compound subject via
	// permissions.EncodeCompoundSubject. Nil for every non-compound subject — including
	// ordinary literal commands, which keep the byte-exact no-chooser path. In production
	// promptgate populates it from permissions.RecognizeSimpleCompound for literal-command
	// requests the backend recognizes as a simple compound.
	Compound *permissions.SimpleCompound
	// Summary is the display-only approval summary the bootstrap seam computes: internal/tui
	// cannot import internal/dispatch, so the classification cannot happen here. Its rows take
	// priority over the remaining fixed facts when the terminal cannot show all of them.
	Summary permissions.PromptSummaryResult

	// ProjectUntrusted withholds every durable project grant for a run the trust gate
	// refused. Its false zero value means trusted, which is a default for direct construction
	// inside internal/tui only: a project with no settings file is trusted by vacuity, so
	// there is no shape here to infer the decision from. Every production bootstrap path must
	// set it explicitly from the run's fixed decision, which is where that decision exists.
	ProjectUntrusted bool

	HostTool           string
	HostRequest        string
	Host               string
	FreshConfirmation  bool
	DirectHTTPFallback bool

	ExceptionalAuthority string
}

func (PromptRequest) IsDirectCommand

func (r PromptRequest) IsDirectCommand() bool

IsDirectCommand reports whether the subject is a direct command. The subject-scope sub-choice attaches to a grant-creating selection when this is true (literal-command and structured-tool subjects are fixed and offer no sub-choice).

func (PromptRequest) IsSimpleCompound

func (r PromptRequest) IsSimpleCompound() bool

IsSimpleCompound reports whether the subject is a recognized simple compound eligible for the per-stage subject-scope chooser. It is a separate gate from IsDirectCommand: simple compounds arrive as literal-command subjects, so an ordinary literal (Compound nil) keeps the byte-exact no-chooser path and only a request carrying the recognized decomposition opens the compound chooser.

type ProviderModel

type ProviderModel struct {
	Provider    string
	DisplayName string
	ModelID     string
}

ProviderModel is one selectable (provider, model id) row in the CTRL-M model picker's provider×model cross-product. The picker spans every selectable provider's models allowlist: the active provider is a chosen, validated field the picker may switch — unlike /model, which never changes the provider.

Provider is the stable provider id persisted in model.active / model.last_used, emitted in events, and used for apply and disambiguation; DisplayName is the presentation-only group-header label (e.g. "OpenAI ChatGPT" for the openai-chatgpt built-in, "OpenAI Platform" for openai). They differ for built-in providers, so the header reads the display name while persistence keeps the id. A row with an empty DisplayName falls back to the provider id for its header.

type ProviderModelApplier

type ProviderModelApplier interface {
	// ApplyProviderModel persists Selection{provider, modelID, effort} as
	// model.active and returns the cross-mode marker outcome the
	// picker must disclose. A non-nil error is reserved for an internal append/flush
	// failure the picker must surface; persistence failures are not reported as
	// errors here.
	ApplyProviderModel(provider, modelID, effort string) (ModelApplyResult, error)
}

ProviderModelApplier runs the persistence/event transaction for a chosen provider+model+effort and updates the run's model_turn source. It mirrors ModelApplier but switches the provider too (the picker spans the provider×model cross-product), so it cannot reuse ModelApplier.Apply, which is model-only. The production implementation reuses the shared picker-apply transaction: append model_changed before WriteActive, flush, mirror model.last_used only after the active write succeeds, emit model_persist_failed / model_global_mirror_failed and the notice as prescribed, and set the next source to picker / picker_degraded. It must not dispatch /model and must not append a slash_command event.

type ProviderModelCatalog

type ProviderModelCatalog interface {
	// ActiveSelection returns the current provider/model/effort and whether a model
	// is selected in memory. Unlike the effort picker, the model picker opens even
	// when ok is false — it is how the first model is chosen — so ok only governs the
	// initial highlight and whether the apply's model_changed carries prior fields.
	ActiveSelection() (provider, modelID, effort string, ok bool)
	// ProviderModels returns the selectable (provider, model id) cross-product in
	// display order (configured provider/model order, or a deterministic sort over
	// map config). A provider with an empty models allowlist contributes no rows.
	ProviderModels() []ProviderModel
	// ConfiguredProviders returns one row per configured (non-builtin) provider,
	// including those whose model allowlist is empty (ModelID == ""). ProviderModels
	// omits empty providers, so the picker uses this to request a non-forced refresh
	// for configured providers whose cache has no rows when it opens.
	ConfiguredProviders() []ProviderModel
	// SupportedEfforts returns cached efforts, or only effortNoReasoning when
	// capability metadata is absent.
	SupportedEfforts(provider, modelID string) []string
	SettingsTarget() ModelSettingsTarget
}

ProviderModelCatalog is the provider-aware surface the CTRL-M model picker reads. It is additive to (not a widening of) ModelCatalog/ModelApplier so the existing production structs keep satisfying those interfaces and internal/bootstrap still compiles; the production implementation lives outside internal/tui and is wired onto the app there. internal/tui still imports neither internal/model nor internal/provider (which would re-introduce the model→tui import cycle).

type QuestionAnswer

type QuestionAnswer struct {
	ID       string   `json:"id"`
	Prompt   string   `json:"prompt"`
	Selected []string `json:"selected"`
	Custom   string   `json:"custom,omitempty"`
}

type QuestionRequest

type QuestionRequest struct {
	Context   string
	Questions []QuestionSpec
}

type QuestionResult

type QuestionResult struct {
	Answers       []QuestionAnswer `json:"answers"`
	Cancelled     bool             `json:"cancelled,omitempty"`
	Clarification string           `json:"clarification,omitempty"`
}

func (QuestionResult) Encode

func (r QuestionResult) Encode() (string, error)

A clarification marshals through its own shape because `answers` has no omitempty: this struct would pair it with an empty answer list the model reads as a question answered with nothing.

type QuestionSpec

type QuestionSpec struct {
	ID          string
	Prompt      string
	Options     []string
	MultiSelect bool
	AllowCustom bool
}

type Registry

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

Registry is the validated set of built-in commands plus canonical-name and alias-to-canonical lookup maps.

func BuildRegistry

func BuildRegistry(replacements map[string]HandlerFunc) *Registry

BuildRegistry assembles a built-in command registry from the source-of-truth builtinCommands metadata, replacing the default handler of any command whose name appears in replacements with the supplied handler. A nil or empty map yields the unbound, metadata-only registry — every command keeps the handler builtinCommands declares (recordHandler for the phase-1 placeholders), which is what the slash-command, help, and dispatch unit tests build; a populated map binds real handlers for production assembly. This is the single place the command list is copied, so the unbound and production registries cannot drift from builtinCommands.

func MustNewRegistry

func MustNewRegistry(commands []Command) *Registry

MustNewRegistry builds a Registry and panics on any validation violation. Registry-shape violations are a build-stopping error; for the in-binary source-of-truth list a startup panic is the runtime equivalent.

func NewRegistry

func NewRegistry(commands []Command) (*Registry, error)

NewRegistry validates commands against the registry rules / TR-TUI-3 and builds the lookup maps. It returns an error (rather than panicking) so tests can assert each violation class individually; MustNewRegistry is the startup wrapper that turns a violation into the build-stopping panic.

func (*Registry) Commands

func (r *Registry) Commands() []Command

Commands returns the registry entries in source order, for /help listing.

func (*Registry) DispatchSlashInput

func (r *Registry) DispatchSlashInput(input string, host CommandHost) DispatchResult

DispatchSlashInput parses one line via ParseSlashInput and dispatches it. A bare exit/quit is treated as /exit. Unknown commands and wrong-arity invocations render a one-line notice, retain the typed input, and append no event; valid invocations run the canonical handler, which appends the slash_command event at its own sequencing point.

type RenameDeps

type RenameDeps struct {
	// SetTitle persists title as the active session's metadata.title through the
	// session's owning store (sessions.Store.FlushTitle), deliberately not via the
	// package-level sessions.SetTitle: the live store already owns the file and, for
	// a resumed session, already holds the sidecar lock SetTitle would try to
	// re-acquire, so a second non-blocking acquire could self-contend. It must not
	// advance updated_at (a rename is not session activity). The
	// production implementation routes an atomic-write failure through the run's
	// session-failure protocol and returns the write error after it is handled, so
	// the command is rejected before its confirmation notice.
	SetTitle func(title string) error
}

RenameDeps is the injectable persistence seam NewRenameHandler closes over — a deliberate package boundary, not removable indirection: its single SetTitle field is what keeps active-store title persistence and atomic-write-failure routing (which live in the bootstrap/session-failure layer) out of internal/tui. SetTitle persists the new title for the active session and makes the handler's just-appended slash_command record durable in the same atomic write.

type ResumeActions

type ResumeActions struct {
	Rename func(entry ResumeEntry, title string) error
	Delete func(entry ResumeEntry) error
	ReList func() ([]ResumeEntry, error)
}

ResumeActions bundles the in-picker mutation callbacks the open picker invokes while it stays mounted: Rename and Delete persist a title / remove a file, and ReList re-enumerates the current folder's survivors so the picker can refresh its rows after a mutation. They are kept separate from the Enter/ESC onResult continuation so a rename or delete never triggers the session-switch sequence (the open picker holds mutable row state; only Enter/ESC resolve the switch). ReList ignores skipped ids on refresh (the skipped-id notices are shown once, at the initial open).

type ResumeDeps

type ResumeDeps struct {
	Lister SessionLister
	Picker ResumePicker
	// Rename and Delete are the in-picker mutation capabilities: ctrl+r
	// persists a typed title via Rename, ctrl+d removes the file via Delete. The
	// production wiring binds them to sessions.SetTitle / sessions.DeleteSession (the
	// deleter additionally guards the active session). They are invoked only while the
	// picker is open and never resolve the Enter/ESC continuation.
	Rename           SessionRenamer
	Delete           SessionDeleter
	Stream           StreamCanceller
	StreamTranscript StreamInterruptionMarker
	Loader           TargetLoader
	Switch           ResumeSwitch
	Refresh          UpdatedAtRefresher
	Model            ModelRecoverer
	Agents           AgentInstructionsReplayer
	Fork             OlderSchemaForker
	WriteFailure     WriteFailureSink
	// Transcript resets and re-renders the visible transcript pane after an equal-schema
	// switch commits, so the resumed session shows its prior conversation. A nil
	// capability skips the visible replay; the session switch itself is unaffected.
	Transcript TranscriptReplayer
	// FlushCtx is the run context used for the prior session's pre-switch flush and
	// the loaded session's launch/refresh/post-replay writes. Threading the
	// real run context means a genuinely cancelled run makes the pre-switch flush a
	// no-op that returns a non-write error, refusing the switch so the prior session's
	// final records are not dropped before being made durable. Nil means
	// context.Background().
	FlushCtx context.Context
}

ResumeDeps gathers the capability interfaces NewResumeHandler closes over, each independently injectable so the handler can be driven end-to-end with fakes (mirroring ClearDeps). StreamCanceller, AgentInstructionsReplayer, and WriteFailureSink are the same interfaces /clear uses (defined in cmd_clear.go). Production wiring supplies the implementations bound to the running app + bootstrap result.

type ResumeEntry

type ResumeEntry struct {
	ID        string
	StartedAt time.Time
	UpdatedAt time.Time
	Preview   string
	// Path is the absolute session-file path, populated by the production lister so the
	// rename/delete actions can target the file directly (sessions.SetTitle /
	// sessions.DeleteSession) without re-resolving the id. ID still drives load/switch.
	Path string
	// Title is the persisted optional session title (sessions.Metadata.Title). When
	// non-empty it overrides Preview as the row's display text (resumeRowTitle).
	Title string
}

ResumeEntry is one row the picker renders for a resumable session: the id, the start/update timestamps, and the truncated first user message preview. It is the TUI-facing projection of a sessions.ListEntry, whose scan already carries the title and the untruncated preview; the production lister only bounds the preview to the picker's cell budget. ID is the handle Load resolves (the cold-start --resume <id> handle).

type ResumePicker

type ResumePicker interface {
	// Present shows entries and returns immediately; when the user selects a row
	// (Enter) or dismisses the picker (ESC) it invokes onResult exactly once with
	// the outcome. The post-selection session-switch sequence runs inside onResult,
	// not after Present returns, so the event-loop submit path is never blocked.
	// actions carries the rename/delete/re-list callbacks the picker invokes while it
	// stays open (ctrl+r / ctrl+d); they are side-effect-only and never resolve
	// onResult, so an in-picker mutation cannot run the session-switch continuation.
	Present(entries []ResumeEntry, onResult func(PickerResult), actions ResumeActions)
	// PresentEmpty shows message and returns immediately; when any keypress
	// dismisses it it invokes onDismiss exactly once. Used only when the
	// folder has no resumable sessions.
	PresentEmpty(message string, onDismiss func())
}

ResumePicker presents the resumable sessions and reports the user's choice through a continuation, or shows the empty-folder message. It never blocks: the production implementation (picker_resume.go) mounts a tview Pages overlay, returns control to the event loop immediately, and invokes the supplied callback from the focused list's Enter/ESC handlers on the single tview goroutine — modeled on the CTRL-M model picker. A blocking call here would freeze the event loop before the picker keys could arrive, since the production submit path runs handlers synchronously off the tview input callback (Resolution — Option A, 2026-06-02). Keeping it behind this seam lets the handler's listing/selection behaviour be asserted with a fake that invokes the callback synchronously.

func NewResumePicker

func NewResumePicker(app *App) ResumePicker

NewResumePicker returns the production ResumePicker bound to app, for the production ResumeDeps. The overlay it presents is continuation-based: it never blocks the event-loop submit path.

type ResumeSwitch

type ResumeSwitch interface {
	Switch(target, prior *sessions.Store) error
	Activate(target *sessions.Store) (cleanupWarning string, err error)
}

type ScopeKind

type ScopeKind int
const (
	ScopeExact ScopeKind = iota
	ScopeSubcommand
	ScopeProgram
	ScopePrefix
	ScopePath
)

type SecretPromptRequest

type SecretPromptRequest struct {
	Label    string
	Help     string
	OnSubmit func(secret string)
	OnCancel func()
}

SecretPromptRequest is the caller-supplied data for one masked secret prompt: the label and help line shown above the field, and the callbacks run once the user submits or cancels. OnSubmit receives the entered string; OnCancel (optional) runs on ESC or Ctrl-C. Both fire on the tview event loop after the overlay is torn down, so the entered text reaches the caller only as the OnSubmit argument.

type SessionDeleter

type SessionDeleter interface {
	Delete(entry ResumeEntry) error
}

SessionDeleter deletes a resumable session file (delete action). The production implementation guards the active session and calls sessions.DeleteSession on the entry's file path; a non-nil error (lock contention, the active-session guard, unlink failure) is rendered as a transient picker notice and leaves the file in place.

type SessionLister

type SessionLister interface {
	List() (entries []ResumeEntry, skipped []sessions.SkippedID, err error)
}

SessionLister lists the resumable sessions for the current canonical folder. It returns the survivors as picker rows, the ids it skipped, and a typed enumeration error. The production implementation wraps sessions.ListResumable for the run's home/cwd; on a non-ENOENT readdir failure it returns the *sessions.ResumeListEnumerateError that names the subdirectory and error category.

type SessionRenamer

type SessionRenamer interface {
	Rename(entry ResumeEntry, title string) error
}

SessionRenamer persists a new title on a resumable session (rename action). The production implementation calls sessions.SetTitle on the entry's file path; a non-nil error (lock contention, version refusal, write failure) is rendered as a transient picker notice and leaves the file unchanged.

type SessionSettings

type SessionSettings struct {
	RetentionDays    func() int
	SetRetentionDays func(int) error
}

type SessionSwapper

type SessionSwapper interface {
	Close(prior *sessions.Store) error
	Fresh() (*sessions.Store, error)
}

SessionSwapper owns the in-process session swap. Close refuses any further appends to prior so background events landing during the switch window are dropped. Fresh creates the new same-folder session — file path, UUIDv7 id, ensured folder dir, initial launch_began{source:"fresh"} — and returns its store; the production implementation also replaces the host's active-session reference as part of the same call.

type SettingsDeps

type SettingsDeps struct {
	Lister   PermissionLister
	Picker   SettingsPicker
	Rows     func() []PermissionRow
	Revoke   func(PermissionRow)
	Share    func(PermissionRow) ShareResult
	Approved bool
	Boundary string
}

SettingsDeps is the /settings handler's dependency surface. A nil Revoke leaves the overlay read-only and a nil Share leaves it without a share action. Approved is the run's explicit root approval, never inferred from the loaded rows, and Boundary is discovery.boundary.

type SettingsPicker

type SettingsPicker interface {
	Present(rows []PermissionRow, deps SettingsDeps)
}

SettingsPicker presents the stored grants in the overlay and returns immediately. It re-reads deps.Rows after a revoke or share so the rows reflect what the seam changed.

func NewSettingsPicker

func NewSettingsPicker(app *App, settings OpenRouterSettings, session SessionSettings) SettingsPicker

type ShareResult

type ShareResult struct {
	Refused       bool
	SharedPath    string
	LocalPath     string
	SharedSkipped bool
	SharedCreated bool
	SharedErr     error
	LocalErr      error
}

type SlashInvocation

type SlashInvocation struct {
	Name string
	Args []string
}

SlashInvocation is the parsed form of a "/<name> [args...]" line. Args are the whitespace-tokenized tokens after the name, with no quoting or escaping; it is nil when no arguments were supplied.

type SlashParseKind

type SlashParseKind int

SlashParseKind is the outcome of parsing one line of prompt input.

const (
	// KindNotASlashCommand is ordinary prompt text: it neither begins with a
	// valid slash invocation nor is a bare exit/quit synonym. A line whose first
	// non-whitespace character is "/" but is not followed by a valid name (e.g.
	// "/", "/ ", "/Foo") also lands here per TR-TUI-12 — there is no separate
	// "invalid slash" outcome in phase 1.
	KindNotASlashCommand SlashParseKind = iota
	// KindSlashInvocation is a well-formed "/<name> [args...]" invocation. Whether
	// the name names a real command is the registry's concern, not the
	// parser's; an unknown-but-valid name still parses as this kind.
	KindSlashInvocation
	// KindBareExit is a bare "exit" or "quit" (case-insensitive, trimmed),
	// treated as equivalent to "/exit". The parser reports it as a
	// distinct kind and leaves the mapping to /exit to the dispatch layer.
	KindBareExit
)

type StatusLayer

type StatusLayer struct {
	Kind string
	Path string
}

type StatusSnapshot

type StatusSnapshot struct {
	Version            string
	SessionID          string
	StartedAt          time.Time
	LaunchDir          string
	ProjectRoot        string
	ProjectMatchLaunch bool
	Provider           string
	ModelID            string
	Effort             string
	AgentEntries       []agentinstr.LoadEntry
	Layers             []StatusLayer
	InputTokens        int64
	OutputTokens       int64
	FullyAttestable    bool
	WebResearchEnabled bool
	Approval           ApprovalDisclosure
	SandboxState       string
	SandboxReason      string
	SandboxMode        string
}

StatusSnapshot is the resolved, render-ready /status state captured at the moment the overlay opens. Once built it is immutable; the overlay shows this frozen snapshot until ESC is pressed ("no auto-refresh").

func BuildStatusSnapshot

func BuildStatusSnapshot(state StatusState) StatusSnapshot

BuildStatusSnapshot resolves state into a StatusSnapshot. Token totals are summed across every model_response event in the record stream, and the attestability flag flips off as soon as any model_response carries a non-empty anomaly list (item 11).

type StatusState

type StatusState interface {
	// Version is the codemeld build version (item 1).
	Version() string
	// LaunchDir is the absolute launch directory (item 4).
	LaunchDir() string
	// ProjectRoot is the resolved project root (item 5).
	ProjectRoot() string
	// Layers is the settings layers that composed the session config, global first
	// and local last (item 10).
	Layers() []StatusLayer
	// Selection is the active provider/model_id/effort triple (items
	// 6-8).
	Selection() (provider, modelID, effort string)
	// AgentInstructions is the in-memory AGENTS.md load record (item 9)
	// already in the load_order produced by agentinstr.LoadAgentInstructions.
	AgentInstructions() []agentinstr.LoadEntry
	// SessionID is the active session's UUIDv7 id (item 2).
	SessionID() string
	// SessionStartedAt is the active session's start timestamp (item 3).
	SessionStartedAt() time.Time
	// Records is a snapshot of the active session's in-memory record stream,
	// used to sum token usage and detect anomalies (item 11).
	Records() []sessions.Record
	WebResearch() bool
	// Approval is the approval gate's decision as it was fixed at launch; it is never
	// recomputed from the files it covers.
	Approval() ApprovalDisclosure
	SandboxStatus() (state, reason, modeKey string)
}

StatusState is the in-memory state surface the /status snapshot reads at open time ("single snapshot... no auto-refresh"). Every method must return without touching disk — the snapshot is built entirely from caller-supplied in-memory state so the overlay's no-disk-at-open invariant is enforced by the boundary rather than by the body.

It is deliberately retained as the one in-memory status source shared by all four readers — the /status overlay (BuildStatusSnapshot), the launch banner, the persistent status line (RenderStatusLine), and the file-reference picker's project-root lookup — so those surfaces cannot drift apart. Keeping it an interface here also keeps this package from importing the bootstrap/model/config packages that assemble the concrete state; collapsing it into those concrete types would pull that dependency edge into internal/tui, so the seam stays.

type StreamCanceller

type StreamCanceller interface {
	Cancel() (retainedChars int, inFlight bool)
}

StreamCanceller cancels any in-flight provider request before a session switch. Cancel returns the character count already retained for the cancelled response and reports whether a stream was actually in flight, so the caller only emits stream_interrupted when there really was one.

type StreamInterruptionMarker

type StreamInterruptionMarker interface {
	MarkStreamInterruptedFromCommand()
}

StreamInterruptionMarker seals the visible in-progress assistant line after a slash-command cancellation. It is called from command handlers on the tview event-loop goroutine, so production marks the transcript directly rather than queuing an off-loop update.

type SubjectKind

type SubjectKind string

SubjectKind is the matching subject's classification shown on the prompt and branched on for the direct-command sub-choice. The string values are the human labels the prompt displays.

const (
	// SubjectDirectCommand is a normalized exec-style command eligible for the
	// subject-scope sub-choice.
	SubjectDirectCommand SubjectKind = "direct-command"
	// SubjectLiteralCommand is a literal command subject matched byte-for-byte
	// against its canonical form; no scope sub-choice is offered.
	SubjectLiteralCommand SubjectKind = "literal-command"
	// SubjectStructuredTool is a structured tool call eligible for grants; no
	// scope sub-choice is offered.
	SubjectStructuredTool SubjectKind = "structured-tool"
	SubjectHost           SubjectKind = "host"
)

type SubmitFunc

type SubmitFunc func(input string, imageParts ...sessions.ContentPart) bool

SubmitFunc receives trimmed prompt input plus any pending image content parts and reports whether it accepted the submission. The app clears the input field only when the hook returns true. imageParts is the variadic tail so existing text-only callers and the many command tests still pass just a string; when present only the plain-prompt path carries them onward — slash command dispatch stays text-only. Slash parsing and command dispatch land behind this seam.

func NewSubmitFunc

func NewSubmitFunc(reg *Registry, host CommandHost, prompt PromptEntrypoint) SubmitFunc

NewSubmitFunc builds the production SubmitFunc the app submits through: it dispatches each line via the registry's DispatchSlashInput and maps the result onto the accept/reject boolean that controls input clearing.

  • StatusNotASlashCommand: ordinary prompt text — forward it to prompt and accept only if prompt accepts; a nil prompt rejects.
  • StatusUnknownCommand / StatusWrongArity: dispatch already rendered the one-line notice and asked to retain input, so reject (no model send, no

event) — the typed text stays for the user to edit.

  • StatusHandled: accept (clear the input) unless the handler requested input

retention, e.g. /effort's unsupported-value path. A non-nil

handler error retains the input rather than discarding the user's text on
an internal failure; the handler owns any user-facing notice it can render.

type TargetLoader

type TargetLoader interface {
	Load(entry ResumeEntry) (target *sessions.Store, older bool, failCat fserrors.Category, err error)
}

TargetLoader resolves and loads the chosen session and classifies its schema version. older reports an older-schema file that routes to the read-only + fork affordance. On a load failure — e.g. the file was pruned between listing and selection — it returns a nil store, failCat carrying the error category for the step-8 notice, and a non-nil error; failCat is the empty category on success.

type TranscriptReplayer

type TranscriptReplayer interface {
	ResetTranscript()
	ReplaySessionRecords(records []sessions.Record)
}

TranscriptReplayer resets the visible transcript pane and replays a session's prior conversation into it. After an equal-schema /resume switch commits, the handler calls Reset then Replay so the chosen session's history replaces the prior session's visible transcript instead of mixing the two or leaving the pane empty. The App satisfies it via ResetTranscript / ReplaySessionRecords; it is injected so the switch sequence can be asserted with a fake, and an unset (nil) capability skips replay entirely (the switch still commits).

type TranscriptResetter

type TranscriptResetter interface {
	Reset()
}

TranscriptResetter wipes the visible transcript pane after the session swap and re-renders the post-clear header for the fresh session, so /clear shows a blank pane (then the launch banner) instead of the prior conversation (visible-screen reset). It is invoked only after the swap and all post-swap replay/flush/carry-forward work have succeeded, so a refused switch leaves the prior session's transcript untouched. The production implementation lives in internal/bootstrap so it can source the launch banner from statusState without importing bootstrap state into internal/tui; a nil capability (the swap-sequence unit tests) skips the reset.

type TurnDrainer

type TurnDrainer interface {
	WaitTurnDrained()
}

type UpdatedAtRefresher

type UpdatedAtRefresher interface {
	Refresh(ctx context.Context, target *sessions.Store) error
}

UpdatedAtRefresher refreshes the loaded session's updated_at and flushes it so a resumed session stays inside the retention window. It absorbs a per-turn atomic-write failure internally and returns a non-nil error only for a fatal condition such as a cancelled run context, which aborts the switch.

type WriteFailureSink

type WriteFailureSink interface {
	Handle(reason sessions.WriteReason, cat fserrors.Category, path string, detail error, store *sessions.Store) error
}

WriteFailureSink handles a pre-switch flush failure: it appends the session_write_failed event to the prior session store and renders the one-line notice. The production implementation lives outside internal/tui (alongside or above sessionfailures) so it can delegate to sessionfailures.HandleWriteFailure without re-introducing the internal/tui ↔ internal/sessionfailures import cycle.

Jump to

Keyboard shortcuts

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