Skip to content

sase's TUI User Guide

Overview

sase's TUI is the primary TUI for the SASE toolkit. It provides an interactive interface for navigating, managing, and operating on Patches, agents, and machine services.

Launching

sase tui [QUERY] [options]

Run sase tui from an interactive terminal. It opens on the Agents tab and starts the service host (pass -x to skip that). Press ? on any tab for the keymap and a short guide, : for the Command Line, ; for the Command Palette, and q to quit. The command was previously sase ace, which no longer exists; the TUI's settings still live under the ace: section of sase.yml (for example ace.keymaps and ace.page_size).

If no Patches query is provided, sase's TUI loads the last used Patches query, then the first saved Patches query, then falls back to !!! for error suffixes. The top-level Agents tab restores its own last submitted Agents query after startup.

CLI Options

Option Description
QUERY (positional) Query string for filtering Patches
-m, --model-tier Override model tier for all LLM providers (large or small)
-M, --model-size Deprecated alias for --model-tier (big or little)
-p, --profile [PATH] Profile the TUI session with pyinstrument; optional output path
-r, --refresh-interval Auto-refresh interval in seconds (default: 10, 0 to disable)
-s, --sanity-refresh-interval Full sanity-refresh interval in seconds (default: 300); see Auto-Refresh
-x, --no-service, --no-axe Disable auto-starting the service host on startup
-v, --vcs-provider Override VCS provider (git, hg, or auto)
-R, --restart-service, --restart-axe Restart an already-running service host on startup (no-op if it is not running)
-t, --tab Tab to focus on startup (agents by default, artifacts, or services; axe is a compatibility alias, and changespecs and patches alias artifacts)
-T, --tmux Launch sase's TUI in a new tmux window and print the target for external control

When profiling is enabled, sase's TUI writes text output to PATH. If PATH is omitted, it uses the managed temp tree: $SASE_TMPDIR/ace-profiles/ace_profile_<timestamp>.txt when SASE_TMPDIR is set, or $SASE_HOME/tmp/ace-profiles/ace_profile_<timestamp>.txt otherwise (SASE_HOME defaults to ~/.sase). On exit, sase's TUI prints the shortened path and copies it when a clipboard tool is available.

Examples

sase tui                              # Last query, first saved query, or "!!!"
sase tui '"feature" AND "Drafted"'    # Filter by name and status
sase tui '+myproject'                 # Filter by project
sase tui -m small -r 30 '!!! OR @@@' # Small model, 30s refresh

When --profile is enabled, sase's TUI prints a shortened profile-output path after the TUI exits and tries to copy that shortened path to the system clipboard (pbcopy, wl-copy, xclip, or xsel when available).

Agent Screenshots

Use sase screenshot when you need a PNG of the real TUI state an agent would see. The command starts sase tui in the detached sase_ace_agents tmux session at 120x40, waits for the screen to paint, sends optional tmux keys, asks the live app to export SVG, then rasterizes that SVG with the same bundled-font renderer used by the golden PNG suite.

sase screenshot -o /tmp/sase.png
sase screenshot -p j -p j -w 'Ready' -o /tmp/sase.png
sase screenshot -p / -w INSERT --type 'machine:apollo' -w 'machine:apollo' -p enter -o /tmp/sase.png
sase screenshot --host apollo -o /tmp/sase-remote.png
sase screenshot --keep -- -t services
sase screenshot --window sase_ace_agents:sase_tmux_1 -o /tmp/sase-again.png

Repeatable -p/--press, -T/--type, and -w/--wait-for form one argv-ordered input script, so a query filter can be typed and submitted without a second --window capture. --type sends literal TUI text (tmux send-keys -l). Send the query key as a literal -p /: tmux has no slash key name, so -p slash types the letters slash. Wait for the input to open (-w INSERT) before typing, and wait for the typed text before -p enter.

By default, the tmux window is killed after capture. Pass --keep to leave it running, drive it manually with tmux send-keys, and recapture it later with --window. The command pins TUI animations off and uses truecolor terminal defaults, but it still shows live machine state and timestamps by design. If capture times out, the error includes the final tmux capture-pane text so you can see what the TUI was showing.

Pass --host to run the live TUI and SVG export on an enrolled machine alias or any raw SSH destination, then fetch the SVG and rasterize it locally. Enrollment only supplies a convenient ssh_target; SSH itself is full shell access and uses your normal SSH trust boundary, not gateway-scoped credentials. Remote screenshots show that machine's installed sase and state, not your local working tree. The remote sase commands run through the account's noninteractive login shell so tools installed under login-profile paths such as ~/.local/bin can be found without allocating a terminal. Check the printed remote_sase_version=sase <version> line when comparing results; it is read from sase version --json on the remote host. A remote host needs sase, tmux, and SSH access, but it does not need the local visual rasterizer dependencies.

Clipboard Transports

Every copy inside sase's TUI runs in the background and tries both a verifiable system transport and OSC 52 for the client terminal. Inside tmux, sase's TUI tries tmux load-buffer -w - first; otherwise the system candidates are pbcopy, wl-copy, xclip, and xsel as appropriate for the platform and display environment. A plain Copied … toast means a subprocess transport confirmed success, while Copied … (OSC 52) means sase's TUI emitted the terminal escape sequence without a verifiable subprocess result. OSC 52 payloads above the terminal-safe size limit are skipped. If neither transport works, sase's TUI opens the generated text in a read-only fallback view so it can still be selected and recovered.

Tab System

sase's TUI has three tabs, cycled with Tab and Shift+Tab:

While a prompt input bar is open, Tab/Shift+Tab belong to the prompt (snippet expansion, tabstops, list indent) and never switch tabs.

Tab Description
Agents View running and completed agents, their files and prompts
Artifacts Browse the durable Agent catalog, Stitches, Patches, Beads, configured document providers, and Files. See the Artifacts pane contract and visual grammar.
Services Monitor the service host, configured service procs, scheduler work, and background commands

Agents is the first tab and the startup default. Each tab has contextual help: press ? to open the Help modal on its Keymaps view, then ] to switch to the tab's Guide view. While Help is open, the configured tab-switch keys still switch sase's TUI tabs and refresh both views in place. By default those keys are Tab and Shift+Tab; if you remap them, the modal follows the configured keys.

Press / in the Keymaps view to open a live filter bar. Typing splits the query into whitespace-separated tokens that must all match — each token is checked against a row's section name, key display, or description, so a token that matches a section name (e.g. beads) pulls in every keymap in that section. Matched text is highlighted and a counter shows how many keymaps and sections matched. The filter follows you across sase's TUI tab switches while Help stays open, but resets whenever the panel is closed and reopened. Esc clears an active filter before it closes the Help modal.

On first use, empty tabs render onboarding states instead of blank panels: the Patches view shows a getting-started card when no Patches or saved queries exist yet, and the Agents tab walks through launching a first agent — the project/Patch launch hint appears only when a launchable target exists — and can recommend installing plugins from the Admin Center when no third-party plugins are installed. Onboarding cards carry "learn more" links into the published docs. An empty Beads pane points agents to /sase_new_task, calls out sized draft tasks, and explains how ready tasks enter TaskTriage.

Within Artifacts, number keys follow the visible left-to-right order of the strip: Agent, Stitch, Patch, and Bead are always 1, 2, 3, 4; configured document-provider tabs such as Plan and Research take the next digits; and File, which always renders last, carries the highest digit — 5 with no provider tabs configured, 7 with two. Digits stop at 9, and File keeps 9 when the runtime strip grows past nine panes; middle overflow panes may then have no digit. Use [ / ] to cycle through the complete runtime strip. As horizontal space tightens, the strip chooses the widest tier that fits from a full → compact → micro ladder and re-renders only when the tier changes. Compact removes outer padding; micro also tightens the separators and hides inactive labels, leaving their digits and icons visible. If even micro is wider than the available space, micro remains selected. Every Artifacts tab has an icon, including provider tabs whose missing or invalid ref.icon falls back to the generic ◆, so the micro tier never leaves an inactive tab unidentified. Hovering a tab shows that pane's one-sentence description. These keys act only while Artifacts is visible. Press p on any Artifacts pane to change the shared project scope — first-open seeds from the current project — or use the command palette to jump directly to a top-level view. Patches remains query-scoped: choosing a project there rewrites the Patches query's project: token. The strip order does not change the entry point: opening Artifacts still selects Stitch by default.

sase's TUI owns one link rail across the three top-level tabs. It appears when the current selection has artifact links: an Artifacts-pane entry, a named agent or session turn, or an AXE job. Synthetic clan containers, routines, and background-command rows do not provide a link subject. The rail includes a breadcrumb while a link-follow trail is active.

Press $ to arm the rail. Then press $ again to follow its first entry, 1-9 for a numbered entry, or 0 to open the complete links panel. The rail always advertises $0 all or $0 +N more, so links beyond the first nine remain discoverable. Several projected links produced by the same rule may collapse into one entry; following that entry opens the panel already scoped to the group.

In the panel, a-z follow the first 26 entries directly, Enter follows the highlighted entry, arrows or Ctrl+N/Ctrl+P move, and Esc closes it. - removes a writable durable link and + creates a link from marked rows where that action is available. Projected links are read-only. Rows show relation direction, rationale, provenance, and missing targets; the panel shows a separate notice when the aggregate is stale. Two glyphs pre-flag a row before you follow it, using the same resolution the follow path itself uses: ⊘ means the ref is dangling — no pane resolves it at all — and ↻ means the target exists but is not selectable in place, so following it will rewrite the destination pane's query. A destination pane that is still loading is not flagged, because "not in the result set yet" there is a loading artifact rather than a real reveal need. The ? help modal lists both glyphs beside the link-follow keys.

Link follows can cross tabs and keep a bounded 32-hop trail. Ctrl+O walks backward and Ctrl+Shift+O walks forward, restoring the tab, pane, project scope, query, selection, and supported AXE fold state. If no link-trail hop is available, those keys retain the current pane's normal jump-stack behavior. Ordinary navigation clears the link trail. See Artifact Links for relation and projection details.

Following a link whose target is outside the destination pane's current result set does not fail. Every jump — the $ link rail, the $0 links panel, and relation jumps outside Patches — runs one engine: it plans a single verified rewrite, then commits it once. The query bar always truthfully describes what is shown, and query history records exactly one ^ entry, which restores the original query and selection. While the destination pane is still loading, the jump waits for it rather than reporting a miss. Starting a new jump cancels one still in flight.

Step When What happens
Resolve Always Find the target through the destination pane's own row identity, re-resolved once the pane has loaded.
Scope The current project scope excludes the target Switch to the target's project, or to All projects if its project is unknown and the current scope cannot resolve it. Never narrows from All.
Select The target is visible Select it. No toast.
Fold A collapsed fold hides it Expand the minimum fold, then select. No query change, no toast.
Acquire The row is not in the pane's loaded inventory Fetch it directly from its source in the background, then continue.
Context The query or limit hides it Rewrite to the context query below, verified against the target's row before committing.
Identity No context, or the context still misses Rewrite to the tightest query that names the row (id:, sha:, …), keeping the limit.
Neutral Still missing, where allowed (never Stitches) Replace the query with a blunt limit:all.
Honest report Everything missed Say so: a dangling ref, a load failure, an unconfigured provider pane, or "not in <Pane>".

The rewrite lands the target inside its natural session — never an isolated row — and keeps the current limit:, raising it only when the session would not fit:

Target The query becomes Lens label
Bead phase sase-16n.7 id:sase-16n.* epic sase-16n
Bead epic sase-16n id:sase-16n id:sase-16n.*, epic fold expands epic sase-16n
Bead task or flag sase-abc id:sase-abc bead sase-abc
Agent hood member sase-16n.7 name:sase-16n.*, or (name:sase-16n OR name:sase-16n.*) when the root exists sase-16n hood
Lone agent x name:x agent x
Agent session turn x--y session:x session x
File agent:<creating agent>, else id:<file> files from <agent>, else file <id>
Plan or provider doc path:<doc path> plan <name> or <provider> <name>
Stitch repo@sha repo:<repo> since:<day> until:<day>, plus project:, sidecar:true, or merges:show as needed <repo> · <day>
Patch in a stack ancestor:<stack root> stack <root>
Submitted, reverted, archived patch name:<patch> patch <name>

The * above is the wildcard filter: id:sase-16n.* matches every phase of epic sase-16n but not the epic itself.

An agent: link lands on the Agents tab when the agent is loaded there, opening any collapsed fold, group banner, or tribe panel around it. The Agents-tab main filter is never rewritten: an agent it hides lands in Artifacts ▸ Agent instead. A job: link lands on the Services tab and expands the job's routine fold.

Each rewrite explains itself in one toast naming the new query, what hid the target, any scope change, and the real keys that restore (^) and walk back (Ctrl+O):

↪ Bead sase-16n.7
query  id:sase-16n.* limit:100
was    -status:closed limit:100 · hidden by -status:closed
epic sase-16n · ^ restore · Ctrl+O back

Extra lines appear only when they apply: scope <old> → <new> (project display names, or All projects), fetched outside the loaded rows after an Acquire, and Agents tab filter hides it — showing Artifacts ▸ Agent. The was line names the terms that hid the target (hidden by your query in a Boolean-dialect pane, or hidden by the project scope), or past limit:N when only the limit did. The key names follow the live keymap, and the toast stays for about 8 seconds. Selecting, fold-expanding, switching scope, or fetching without a rewrite shows no toast.

While a rewrite is live, the pane's info header shows a reversible lens chip — ↩ sase-16n.7 · epic sase-16n in the pane's accent color, followed by a dim ^ to return naming the configured prev_query key rather than introducing a new binding. The lens is derived from the live query rather than stored as a flag: it is active only while the pane's canonical query is still exactly the query the reveal wrote and the pane's dialect has not changed. Editing the query yourself, walking query history with ^ / _, loading a saved query, or triggering a fresh reveal all end it with no separate clear step.

Split Modes in Artifacts Panes

Every Artifacts pane starts with an even left-list/right-detail split. Press } to grow the list panel or { to shrink it. The mode is shared by every Artifacts sub-tab and cycles with wraparound in either direction:

Mode Left panel Right panel
narrow 30% 70%
even 50% 50%
wide 70% 30%

The {████} badge at the right of the sub-tab strip shows the current mode in the active pane's accent color: one filled cell is narrow, two is even, and three is wide. Clicking the badge cycles forward, like }.

The Patch pane remains content-sized instead of reserving empty list space. Its mode sets the maximum list width for the available terminal width, while the existing 43-cell readability floor and 80-cell upper bound still apply.

Pane Description Brief

Every Artifacts pane — the five built-ins, the Plan pane, and every pane a sidecar ref creates — carries a resolved, never-empty description, shown as a host-owned brief directly under the sub-tab strip. Press D to cycle it through three modes:

Mode What you see
off Nothing; the row disappears entirely.
summary One line: an icon and a one-sentence summary, ellipsized to fit.
full The summary plus a longer body, capped at 6 rows.

ace.artifacts.description_mode seeds which mode a session starts in (default summary); D changes it in memory for the current session only. Clicking the brief cycles it the same way. An unconfigured document-provider pane shows, in full mode only, a dim italic hint naming both places its description can be configured: ref.pane.description in the sidecar's ref config, or ace.artifacts.panes."<pane_id>".description in sase.yml. See ace.artifacts for the config surface and Artifacts pane visual grammar for the full rendering rules.

The non-Patches panes share fast navigation over their selectable left-panel entries. Stitches and Files skip day headings; Agent, Beads, and provider document panes skip group, section, and empty-state rows as applicable. Movement clamps at the first or last entry and silently does nothing when a list is empty.

Key Action
g / G Select the first / last agent, commit, issue, bead, document, or file
Enter Open the selected commit, document, bead, or file in its full-screen reader
Ctrl+F / Ctrl+B Move down / up 10 selectable entries
Ctrl+D / Ctrl+U Scroll the active right-hand detail pane down / up (half page)
' Show adaptive entry hints; press ' again for the first entry or the last jump origin
Ctrl+O / Ctrl+Shift+O Walk the link trail first, then the pane jump stack; back falls through to first hint
$$ / $1-$9 / $0 Follow the first / numbered artifact link, or open the complete links panel
Ctrl+J / Ctrl+K Load ace.page_size more rows / unload that page (rewrites the host limit: cap)

Hint keys select an entry without activating it. Jump-back history is kept separately for each non-Patches pane, and stale origins disappear automatically after filtering, changing project scope, refreshing data, or collapsing an expanded bead tree. Escape or an invalid hint exits jump mode. These actions use the configured keymap values; the keys above are the defaults.

The Agent pane is the deliberate exception to Enter: its detail is already inline, so selecting a row updates the right-hand identity, lifecycle, lineage, provenance, prompt, chat, and published-page view without opening a reader.

When the selected entry has declared relationships, a relation panel appears at the bottom of the list column, starting collapsed into a one-line rail that names its own expand key (. by default) so it is never hidden knowledge. Press . (toggle_relation_panel) or click the rail to expand it; the expanded panel's border carries the reverse affordance ({key} collapse, bottom-right) so it can be collapsed the same way. Its section names come from the pane contract, so examples include parents and children, document lifecycle stages, dependencies, linked beads or plans, and file-version families. Navigation is two keystrokes: first a relation mode, then the key printed in square brackets beside the target row. The modes are < for ancestors, > for descendants, and ~ for session or siblings, and the footer lists only the ones the current entry actually has; these keys stay live even while the panel is collapsed. A section header ending in (N hidden) means the query is filtering out that many targets, a row ending in (missing) points at an entry that no longer exists, and a row ending in → <pane> crosses to another Artifacts pane.

On Patches, choosing a hidden same-pane target rewrites the query to reveal it rather than failing. Patches saves the query and selection you started from first and pushes the old query onto the same history stack ^ walks, so ^ returns to the exact view you came from. Other panes run relation jumps through the same engine as $ link follows (see Link Jumps): a filtered-out target is unfolded, fetched, or revealed by one reversible query rewrite, and each jump adds a link-trail hop that Ctrl+O walks back.

Shared entry-jump surfaces allocate hints from the zero-based alphabet 0–9, a–z, A–Z. A session with at most 62 targets uses one character (0 through Z). A larger session uses two characters for every target, beginning 00, 01, …, 0Z, 10 and ending at the fixed ZZ capacity. The first character of a two-character hint keeps jump mode open; the second completes the jump. Hints remain case-sensitive.

Outside the Artifacts panes above, a single shared implementation backs ' everywhere it appears: each Admin Center working section (see Global Keybindings) and four modals — the notification options modal, the model picker, the saved-group revival modal, and Launch Control. In all of them, pressing ' a second time while the hints are painted is the jump back key: it pops the most recent origin off a bounded stack of the last ten pre-jump positions, rather than toggling between one saved target and the current row. With an empty stack it falls through to the first hinted row instead. The footer shows which of the two the next ' will do — JUMP ' back while the stack holds an origin, JUMP ' first when it does not. Changes that shift which row is where — refiltering the model picker, paging or deleting in the revival modal, drilling into or out of a Launch Control bucket, or an async provider-snapshot reload — discard the stored origins instead of leaving them pointing at whatever row inherited the index.

Copy Mode in Agent, Stitches, Beads, Provider Documents, and Files

Press % on any non-Patches Artifacts pane to open the context-aware Copy as… palette for the visible entry. Rows are grouped by representation, show their configured accelerator and a warm preview, and can be selected with the mouse, arrow keys or j/k plus Enter, or the accelerator directly. q/Esc cancels. If an accelerator is configured as j, k, or q, the configured copy target wins over navigation or cancellation.

Pane Keys
Agent %@ artifact ref · %! ref in agent prompt · %n name · %l Markdown link · %p artifacts path · %c chat path · %P prompt · %j metadata JSON
Stitches %@ artifact ref · %l Markdown link · %J metadata JSON · %! ref in agent prompt · %% full SHA · %m message · %r repo@sha · %p plan
Beads %@ artifact ref · %l Markdown link · %J metadata JSON · %! ref in agent prompt · %% id · %t title · %b description and notes · %d design · %u linked issue ref
Plans %@ artifact ref · %d bead design ref · %l Markdown link · %J metadata JSON · %! ref in agent prompt · %% bead id · %p path · %t title · %b body
Other providers %@ artifact ref · %l Markdown link · %J metadata JSON · %! ref in agent prompt · %p path · %t title · %b body
Files %% contents · %@ artifact ref · %L Markdown link · %p stored path · %o source path · %l label · %j metadata JSON · %! ref in agent prompt

%s captures the current sase tui tmux pane on every view.

The palette and copied value follow the active pane. After selection or cancellation, the footer returns to the active pane's normal bindings. An unknown printable key warns but leaves the palette open so another choice can be made.

When entries are marked, the reference choice copies newline-separated prompt references, the Markdown-link choice copies a bullet list, the metadata-JSON choice copies one array, and the handoff choice seeds one prompt with the marked set. Entries that cannot produce the selected representation are skipped; the completion toast reports both the copied and skipped counts. The palette title reports the visible marked count and applicable rows use plural labels such as commit SHAs.

The same % prefix opens the palette above copy-forwarding readers and modals, including the preview panel. Dismissing the palette returns to the underlying modal. Snapshot choices dismiss the palette before capture, so the palette itself is not included in the copied pane.

Marks in Agent, Stitches, Beads, Provider Documents, and Files

Press m to mark or unmark the selected entry and u to clear the active pane's marks. Each non-Patches pane keeps an independent stable-target mark set, so marks survive refreshes and switching panes without affecting marked PRs. Changing the shared Artifacts project scope clears the non-Patches marks.

When marks exist, the pane's % copy targets operate on marked entries in visible order instead of only the selected entry. Identity, location, and data representations use paste-ready forms; content dumps retain labeled fenced sections. Content-shaped targets are capped at 512,000 bytes per item and per assembled payload, with an explicit truncation banner and toast. The footer shows the active pane's mark count only while that count is nonzero.

Filtering Agent, Patches, Stitches, Beads, and Plans

Patches keeps its canonical query visible in a persistent, read-only filter row at the top of the detail column, so the active query is legible without opening anything. Press / (or the local f) to start editing it. Typing re-filters the already-loaded Patch snapshot on every keystroke, so previews are instant and never re-query the store. Enter commits: it reloads Patches from the committed query, pushes the query you replaced onto the ^ history stack, and returns focus to the list. Escape abandons the edit, restoring the committed query, its result, and the row you had selected. A parse error stays inline in the row and leaves the visible Patch list on the last valid query. Tab accepts completions for keys, values, shorthand sigils, state predicates, status shorthands, and — while the row is empty — saved-slot commands. Saved-query commands such as #3 status:Draft, # status:Ready, and #3 save, allocate, or delete a slot without changing the active query or closing the editor.

Stitches keeps its effective canonical query visible above the timeline. Press / or the local f shortcut to focus that row for live editing; Enter commits the query and returns focus to the timeline, while Escape restores the last committed query and result. The row remains visible in either case. Beads, Plans, Files, and every document provider pane keep the same persistent idle row, so the committed query stays visible without opening an editor. The Beads default is -status:closed limit:<page_size>; that query lives in the idle bar, not on the counts line. Tokens from different facets combine with AND semantics; comma-separated and repeated values within one facet combine with OR semantics. Free-text terms must all match. Press Tab to accept the highlighted key or value completion.

Agent uses the same persistent query row and the Boolean profile dialect shared with sase agent search. It accepts AND, OR, NOT, and parentheses, plus free text over agent names and metadata. Its fields cover identity and lineage (name, kind, session, clan, tribe, role, workflow, parent, project), execution (state, status, provider, model, attempt), lifecycle booleans (hidden, dismissed, revivable, attention, retry), archive links (linked, relation, artifact), start/finish bounds (since, until, after, before), and runtime bounds (min, max). Date bounds accept Nh / Nd / Nw / Nm, today, and YYYY-MM-DD, where Nm means months; runtime bounds accept seconds or Ns / Nm / Nh / Nd, where Nm means minutes. There are no sigils or macros. For example, state:dismissed AND revivable:true, provider:codex AND status:FAILED AND since:7d, and linked:true AND relation:read are valid queries.

The top-level Agents tab uses the same Boolean grammar through the live agents-live profile, but keeps zero idle screen-space cost: when no query is active, no filter row is visible; when a query is active, the detail column shows the canonical highlighted query and match count. Press / or f to open the auto-hiding filter bar. Live Agent fields include the shared identity and runtime fields (name, session, clan, project, kind, role, workflow, model, provider, status, attempt, hidden, attention, retry, since, until, after, before, min, max, text) plus operational fields (cl, machine, tribe, pinned, unread, needs, source). Archive-only catalog fields such as state, dismissed, revivable, linked, relation, artifact, and label are not live-tab filters, and the live tab does not accept an Artifacts limit: token.

The Agent catalog also derives three archive capabilities that describe what a dismissed row's persisted archive can actually support, rather than whether a bundle file merely exists:

Field True when
historically_viewable The archive holds enough data to inspect the run after the fact.
durably_revivable The archive holds enough data to restore the run.
restartable The archive holds the prompt and model parameters needed to run it again.

The query schema currently advertises those three field names, but the Agent query index does not populate their values. Consequently, both historically_viewable:true and historically_viewable:false (and the equivalent queries for the other two fields) return no rows. Inspect the revive modal for these per-row capabilities; use revivable:true when the goal is to find rows sase's TUI can restore.

revivable:true now means all of "dismissed", "backed by a readable top-level bundle", and durably_revivable, so the saved query the w action seeds (state:dismissed AND revivable:true) no longer lists rows whose archive cannot actually be restored. The revive modal shows the three capabilities as Viewable, Revivable, and Restart lines, plus a Missing line naming any unmet requirement, and reviving a row that lacks one is refused up front with This archive record is not revivable: missing … instead of failing at the last step.

Stitches accepts singular project: plus repo:, author:, origin:, type:, sha:, since:, until:, sidecar:, merges:, and limit: and free text matched against the commit subject. sha: is the pane's identity field: it is repeatable, comma-listable, and negatable, and it matches a prefix of the full commit id, so sha:a1b2c3 selects that commit the way a git short SHA does. origin: accepts stitch, auto, and manual, and is repeatable, comma-listable, and negatable like repo: and author:. type: is also repeatable, comma-listable, and negatable; it accepts manual, automatic (auto alias), stitch, merge, patch, and observed SASE_TYPE footer values. merges:hide/show/only controls merge-commit visibility exactly like sase stitch list's --merges flag (see VCS Provider Reference). project: is not repeatable, comma-listable, or negatable because it selects the repository constellation before commits are collected. With no project: token, collection spans all projects. It accepts a configured project name, ProjectSpec directory key, or alias; committed known values are rewritten to the configured project name. On first open, when ace.current_project.seed_filters is on, the shared Artifacts project scope seeds from the current project unless an explicit project: / +name term already selected one. The project picker replaces that token while preserving every other committed token; its All projects choice removes it. The compatibility a action removes an active project token and restores the last automatic or picked project on the next press.

The bundled initial query is sidecar:false merges:hide since:24h; sase's TUI injects limit:<ace.page_size> (default limit:100) when that string has no limit: token. It is configurable with ace.artifacts.stitches.default_query (ace.artifacts.commits.default_query is a deprecated alias), and changes take effect the next time sase's TUI starts. An explicit project: in sase's TUI query or in the configured default query wins over the current-project seed. An empty parsed query includes sidecar repositories; at sase's TUI startup the async Artifacts seed can add a visible project token. Canonical rendering always includes either sidecar:true or sidecar:false, and the configured d action rewrites that same visible token. Selecting a sidecar with repo: therefore requires sidecar:true. For example, project:sase repo:sase author:Ada origin:stitch since:7d sidecar:false fix shows recent tracked SASE commits by Ada whose subjects contain fix, repo:plans sidecar:true shows that sidecar across all projects, and limit:40 caps a deliberately broad search. Day-granular until: values (today, yesterday, and YYYY-MM-DD) include the full named day; relative and minute-precise values remain instant bounds. Relative windows such as since:24h re-anchor whenever the pane refreshes.

The repository legend starts with [P/N], where P is the selected commit's one-based position and N is the number of matched entries currently displayed. Day headings do not count as entries. A + on the denominator, as in [1/40+], means the displayed total is only a lower bound. The persistent filter row reports the corresponding coverage state (exact, preview, or capped) without repeating the match count. Each timeline row also has a fixed origin glyph immediately before the subject: ✦ stitch for commits created through sase stitch create, ↻ auto for other SASE-created commits, and ✎ manual when the commit has no SASE provenance footer. The legend lists only the origins present in the displayed commits, so a result containing only tracked work shows only ✦ stitch.

Every Artifacts pane accepts a host-owned limit:N token that caps how many matched rows the list shows. It is not a row property: sase's TUI extracts it before dialect parse / Rust eval, matches against the remainder, then slices. Startup writes limit:<ace.page_size> into each pane's default query when no limit: is present; a user-authored limit:40 or limit:all is left alone, and deleting the token leaves that pane uncapped. Ctrl+J raises the cap by ace.page_size and Ctrl+K lowers it, never dropping below one page; unload of an unlimited query (limit:all or a deleted token) introduces limit:<ace.page_size>. When the cap clips the result, the filter row says capped and Stitches uses a lower-bound total such as [1/40+]. limit:all is the unlimited synonym on every pane; limit:0 is also accepted by the shared parser (Stitches used that spelling first). Canonical query text omits both. Provider or aggregate truncation metadata can still mark a count as capped without inventing an active query limit.

Plans accepts kind:, status:, tier:, project:, path:, since:, until:, and limit: plus free text matched across plan-document metadata and content. path: is the pane's identity field — the document path or provider identity, matched by substring so path:<fragment> finds a document by any part of its path, and still searched by free text — and every other document-provider pane gains the same substring path: key when its declaration does not already define one. kind: accepts proposal, active, archive, and the document-sidecar roles present in the current scope, such as plans, research, or designs. kind:archive matches committed documents that are not linked from a live bead, while kind:designs narrows documents to that sidecar.

Beads accepts repeatable id:, type:, task_type:, tier:, status:, size:, due:, project:, assignee:, owner:, model:, has:, bug:, label:, since:, and until: terms, plus host-owned limit:. id: is the pane's identity field: it matches a bead id exactly, and bead ids remain free-text searchable as well, so id:sase-w3.2 pins one row while a bare sase-w3 still matches by substring. task_type: accepts catalog slugs plus untyped for legacy beads. Status values include the five stored states plus the derived blocked, launched, and triage states. due: accepts live, soon, or due and matches flag beads — the dedicated feature-flag removal tasks — by how close they are to their removal thresholds. Other bead types have no due state, so a positive due: term hides them. has: accepts +1, reopened, plan, bug, deps, notes, and triage. bug: matches issue state, reference, relation, or project, with completion for none, open, closed, stale, drift, mirrored, and referenced; label: matches cached provider labels. Free text also searches cached external issue title, body, URL, and labels alongside the bead id, title, description, notes, design, references, and ownership metadata.

A leading unquoted - excludes a match. Stitches can exclude repositories, authors, origins, types, commit SHAs, and subject text. Plans can exclude kind:, status:, tier:, project:, path:, and free text. Files and Beads can exclude every declared filter facet and free text, including since: and until:. Exclusion wins when positive and negative constraints overlap: repo:sase,plans -repo:plans, author:Ada -author:bot, and status:open -status:blocked are all valid. A comma list negates the whole token, so -repo:plans,research excludes either repository. Stitches and Plans do not allow negated date bounds; Files does, but permits each date key only once. Beads permits repeated and negated date bounds. limit: cannot be negated in any pane. Stitches' project:, sidecar:, and merges: fields are singular and cannot be negated; sidecar: accepts only true or false, and canonical queries always render its explicit value. Quote the whole token to search for a literal leading minus ("-repo:plans"); quote only the excluded value to keep negation active (-"generated rollout"). Matching remains case-insensitive, and repository/project aliases work for both inclusion and exclusion.

In the flat-token query grammar, a declared Boolean field also takes a bare shorthand: an unquoted key token with no colon expands to key:true, and -key to -key:true. Quoting opts out, so "key" stays free text. Because the shorthand is purely lexical (it always canonicalizes to the long spelling), it costs no schema field and moves no compiled profile's digest, so no saved query is invalidated. Stitches' sidecar field is Boolean, so a bare sidecar filters to sidecar:true instead of searching for the literal word in a commit subject; quote it ("sidecar") to search for the word. The Agent pane uses the Boolean-expression grammar instead: write revivable:true there, because bare revivable remains a free-text term.

Bead Pane

The top-level Beads view (4) is the work-item home for standalone tasks, epic plan beads, and their phase beads. Every bead appears once: tasks occupy their own section, while epics expand with l and collapse with h to reveal phases. Rows show stored status and ownership metadata, ✦ when a task has a pending TaskTriage decision, and ▤ when the bead links a plan document. Linked issue chips use ○ for open, ● for closed, and ? for a stale issue absent from the complete cached listing; stale or drifted links are highlighted, and +N summarizes additional links. Closed beads are loaded but hidden by the visible -status:closed limit:<page_size> default; press f to edit or clear that query. Section headings report matched and total counts while a filter is active.

n collects a standalone task for the selected project. Title, filing reason, size, and task type are required. The reason is one or two sentences, at most 2000 characters, and is stored separately from the title and description. Ctrl+S creates the bead and Esc cancels. See Creation Reason.

The pane supports the full bead workflow:

Key Action
j / k Select the next / previous bead
Enter Open the complete bead detail in the preview reader
f Edit the bead filter query
Ctrl+J / Ctrl+K Load more matching beads / unload one page (rewrites limit:)
l / h Expand / collapse the selected epic
s Cycle the selected bead's status using the type-aware sequence below
z Snooze the selected task bead (or edit/cancel an existing snooze)
e Edit the bead's valid fields
N Append a note; @path attaches a snapshot and Ctrl+T cycles its audience
n Create a task bead in the selected project (filing reason required)
c Close with a required reason and optional note, or reopen a closed bead
w Launch an epic or launchable task; phase work launches with its epic
E Open a linked external issue
y Copy the bead's @bead: reference
% u Copy a linked issue reference (copy mode)
b Enter issue-action prefix mode
$ Arm the link rail, e.g. to follow the linked plan
R Open the Refresh panel

N opens a modal for appending a note and leaves earlier notes in place. Add @<path> to capture a file snapshot when you save with Ctrl+S: Tab completes paths, pasting a single existing file path inserts an attachment reference, and @@ inserts a literal @. Ctrl+T cycles the attachment audience through automatic, private, and public. Public asks you to confirm on save; policy can still reject the note and return you to the editor. The bead detail lists each attachment's 🌐 or 🔒 chip from the stored descriptor and leaves availability badges and thumbnails to the attachment viewer. See From the Beads pane.

When a bead has several issue links, E, % u, and the b-mode v, e, s, and u actions first open a selector. In b prefix mode, press v to view the cached body, e to edit supported title/body/label fields, s to close or reopen after confirmation, u to copy the provider URL, a to attach an existing numeric issue, or c to create and attach a new issue. Attach and create operate on the bead itself and therefore do not select an existing link. Availability follows the active VCS provider's capabilities. The detail reader shows each link's relation, state, labels, assignees, author, update/comment metadata, and cached body. The info line reports linked, remote-only, stale, and drifted counts, plus provider unavailability and per-project errors, so tracker health remains visible without a separate Bugs pane.

Closing offers done, canceled, and superseded resolutions. A bead with unfinished descendants is rejected unless the close modal's force option is enabled with a non-done resolution; the modal previews those descendants first. Closing or launching a task with a pending TaskTriage request settles that gate so the notification does not outlive the decision. Mutation work runs as tracked procs and refreshes the pane after completion.

The default s status action is type-aware:

task: open → ready → in_progress → closed → open
      claimed → ready
      snoozed → ready
other beads: open → in_progress → closed → open

The s action changes persisted status only; moving a task from ready to in_progress does not itself launch a worker. Use w, the matching TaskTriage notification, or sase bead work <task-id> to launch the work. The editor shows only fields valid for the selected bead type, while dependencies remain read-only in the detail panel. s only cycles a task out of snoozed (back to ready, canceling the snooze); entering snoozed needs z, since snoozing takes arguments (a wake time, and optionally a +1 target and a reason) that a blind status cycle cannot supply.

Press z on a task bead to open the snooze picker: presets for 4 hours, Tomorrow morning, 3 days, and 1 week, plus a custom duration field accepting the same "<duration> [+<N>]" vocabulary as sase bead snooze (see Snoozing a Task Bead) and an optional reason field. Pressing z on an already-snoozed task opens the same modal in re-snooze mode, showing the current wake time and offering a cancel-snooze choice. A snoozed row in the list shows the snoozed status glyph and a dim relative wake time.

When that worker appears on the Agents tab, its bead badge points directly to the task, and its SASE CONTEXT / BEAD lane shows the task title and description, plus size when one is stored, without trying to resolve an epic plan.

Agent Pane

Artifacts → Agent is the durable historical catalog. It is intentionally different from the top-level Agents tab: the live tab is the operational view for running work, unread completions, and current clan/session panels, while this pane keeps queryable rows for prior runs, dismissed agents, sessions, clans, workflows, and workflow children. Rows are newest-first. The pane paints its newest 500 rows first, then extends to the complete catalog and builds the full query index off-thread.

The left side defaults to Session grouping; o / O cycle forward or backward through Session, State, and Project. The shared fold keys (h, l, H, L) collapse or expand group banners. Selecting a row updates the right-hand detail with its durable agent: reference, identity, lifecycle and timing, model/provider, session and retry lineage, provenance, a bounded prompt preview, chat path, and hosted agent page when those files resolve.

The collapsed relation rail combines catalog lineage—session, clan, parent, and retry chain—with typed artifact links such as cites, read, implements, and their inverse labels. Expand it with ., then use the printed <, >, or ~ mode and row hint to navigate. linked:true, relation:<slug>, and artifact:<canonical-ref> expose the same graph as query facets.

Key Action
j / k Select the next / previous catalog row
/ Edit the Boolean Agent query
o / O Cycle grouping forward / backward
w Revive the selected dismissed row, or the marked revivable rows
m / u Mark / unmark the selected row · clear this pane's marks
Ctrl+J / Ctrl+K Load more matching agents / unload one page (rewrites limit:)
p Change the shared Artifacts project scope
% Open the Agent Copy as… palette
R Refresh the catalog (through the Refresh panel)

w skips marked rows that are not revivable. On an aggregate session or clan with one revivable member it revives that member directly; with several, it narrows the pane to the matching state:dismissed AND revivable:true session or clan query so the choice is explicit. Use sase agent search for the same catalog dialect outside sase's TUI.

Document Provider Panes

Artifacts includes one document pane per configured artifact-reference provider. The fixed tabs are Agent, Stitch, Patch, Bead, and File; provider-backed document tabs such as Plan and Research appear between Bead and File when an enabled project configures the matching sidecar ref: policy. Persisted selections use stable ids such as ref:plan and ref:research, so a missing provider falls back to Stitch instead of crashing startup. Each pane renders an icon before its label — the five fixed marks (⬡ Agent, ◉ Stitch, ⎇ Patch, ◈ Bead, ▤ File) are built in, and a provider pane's mark comes from its sidecar ref.icon. The active tab's icon takes that pane's accent color; inactive icons render dim. A missing ref.icon, one that fails validation, or one wider than two terminal cells uses the generic ◆ provider icon instead.

Plans is the built-in provider-backed document pane for the plans sidecar. It keeps the existing plan actions: A and X approve or reject pending proposals. A plan's owning bead (and a bead's linked plan) is an artifact link, so follow it from the link rail with $. Plans groups its list by Kind, Status, or Project (o / O). Other document providers reuse the same list, filter, detail, preview, copy, and refresh behavior from their declared properties and detail fields, and group only when they declare ref.grouping (or ref.pane.group_by); see Grouping declarations.

Commit Detail and Linked Plans

Press Enter on a Stitches entry to open its full message and syntax-highlighted diff. The modal uses j / k for line scrolling, Ctrl+D / Ctrl+U for half pages, g / G for the ends, y to copy the full SHA, and Esc or q to close. When the current result contains multiple commits, Ctrl+N / Ctrl+P move through them with wraparound.

The modal's header block carries a compact commit-time chip, right-aligned on the line that also shows the diff path — an absolute stamp plus a relative age, such as Today 07:05:54 · 2h ago. Today and yesterday show seconds; older commits show HH:MM after their day label. Times are the commit's author time in the configured local timezone. The chip is free on the Artifacts timeline, where the commit time is already loaded; on the Agents tab, a commit whose stored metadata records no time has it looked up from the VCS alongside the diff, so the chip appears once that load lands. Commits made by SASE persist their author time in commit_result.json / commit_results.json, so later views need no lookup at all. A commit whose time cannot be resolved simply shows no chip.

Press p in the commit modal to load the last structured SASE_PLAN footer tag and render its referenced local UTF-8 file as Markdown; press p again to return to the cached diff. This is a local-only lookup: an absolute path is expanded and checked directly, while a relative path is checked first in the commit repository, then in each known project workspace and its plans store. sase's TUI does not clone or materialize a missing store. A missing tag, invalid reference, unavailable path, non-file path, or unreadable file produces a specific toast and leaves the commit visible. Moving to another commit always returns the modal to commit mode.

Preview Reader

Press Enter on a Beads entry, provider document, or Files row to open its full contents in the preview reader. Prompt-normal-mode K opens the same reader for a previewable macro, skill, or file. When sase's TUI knows a canonical artifact reference, the title shows that logical reference beside the resolved local path.

The reader opens at its normal size when the content fits and grows toward a near-full-screen maximum (a thin backdrop margin remains) for long or wide content. Width follows the 95th-percentile line width, so one very long line does not force full width. Switching views with R or p only ever grows the panel; resizing the terminal recomputes it.

Key Action
j / k Scroll down / up one line
Ctrl+D / Ctrl+U Scroll down / up half a page
g / G Jump to the top / bottom
y Copy the complete preview contents
Y Copy the local source path, when available
% Open the active Artifacts sub-tab's Copy as… palette
R Toggle Markdown previews between rendered and source views
p Toggle the full macro properties view
/ Open source search (smartcase substring matching)
n / N Jump to the next / previous match with wraparound
o Open the source path in $EDITOR (falling back to nvim)
Z Hand the source path to the rich terminal artifact viewer
Esc Cancel input, clear active search, then close the reader
q Close the reader

Path-only actions are omitted from the footer when the preview has no local source path; invoking one still produces a specific warning instead of failing. Clipboard operations run in the background and report when no clipboard tool is available. Plans open in rendered Markdown by default when they fit the reader's bounded render budget; macros, skills, files, and oversized documents open as source.

For a macro or skill preview with declared properties (inputs, tags, skill/snippet/ memory flags, local macros, or steps), a compact band appears above the source pane showing its description, an inputs table, and a dim chips summary of everything else it declares. p opens a full, scrollable properties view — the same projection sase macro show renders — with the band hidden while that view is active; p again restores whichever mode was showing before. A preview with no declared properties (a bare-body macro, a plain file, a bead, and so on) shows no band, no p row in the footer, and p emits a warning toast instead of switching views.

Search is commit-on-enter: / opens a one-line input prefilled with the last committed query, and Enter highlights every matching source line before jumping to the first match at or below the current viewport. Queries containing an uppercase character are case-sensitive; other queries are case-insensitive. Starting search from rendered Markdown switches to source view. Esc in the input cancels the edit, while Esc after a committed search first clears the matches and only closes the reader on the next press.

Prompt K on a supported image path (.png, .jpg, .jpeg, .webp, .gif) opens the reader with an inline cell preview sized to the near-full-screen panel. It re-renders on terminal resize. The footer shows Y path | % copy | Z viewer | esc close, and Z opens the full-fidelity artifact viewer. y, /, and o warn instead of acting on image previews.

File Pane

Files browses the artifact-file index that backs sase artifact list. The pane is one of the five fixed Artifacts views: Agent, Stitch, Patch, Bead, and File. Configured document-provider panes appear between Bead and File in the runtime tab strip. Rows are grouped by logical file identity, so repeated captures of @file:~/bob/gtd.md or repeated sase artifact create rows for the same logical artifact appear as one selectable row with versions.

Each row shows a view-mode glyph, the project, the producing agents, an origin badge, the logical label, the latest selected version's timestamp, and the indexed size.

Glyph View mode Opens with
▨ image The rich terminal viewer (kitten icat)
▶ video The rich terminal viewer (mpv)
▤ pdf The rich terminal viewer (PDF pages rendered to PNG)
▤ markdown The preview reader, rendered by default
• text The preview reader, as source

The glyph comes from the same classifier that chooses the viewer, so it can never disagree with what Enter actually opens. Origin badges distinguish files cited in a prompt (ref), files registered explicitly with sase artifact create (created), and automatic captures (capture).

The info line above the list summarizes the loaded snapshot as kind chips and origin chips, where documents totals PDFs and Markdown. The index loads off the message pump in two pages: a first bounded page paints immediately, then the full index replaces it, and the status line reports both the loaded row count and any in-flight extension.

Selecting a row loads its detail panel off-thread. The detail header shows version i/n, the durable file:<id> or logical @file reference, digest, capture time, project, agent, origin, MIME type, and size. Markdown and text rows include a bounded preview. (/) cycle the selected row's versions without moving to another logical file; repeated captures with the same SHA-256 share one version and accumulate provenance.

Key Action
j / k Select the next / previous file, skipping day headings
Enter View the file: preview reader for Markdown and text, rich viewer for media
Z Hand the marked visible rows — or the selection — to the rich terminal viewer
E Open text in $EDITOR (falling back to nvim); open media with xdg-open
a Jump to the producing agent on the Agents tab, reviving it first when dismissed
f Edit the pane's filter query
Ctrl+J / Ctrl+K Load more matching files / unload one page (rewrites limit:)
z Cycle the kind filter through All and the stored kinds present in the snapshot
o / O Cycle grouping (Source, Kind, Project) forward / backward
( / ) Select previous / next version for the current logical file
y Copy the row's @file:<id> reference
Y Copy the row's anchored stored path
m / u Mark / unmark the selected file · clear this pane's marks
% Open the Files Copy as… palette
R Refresh the index (through the Refresh panel)
p Change the shared Artifacts project scope (first-open seeds current project)

These are the default keymap values; the Files-pane actions retain their files_* configuration names and are remappable under ace.keymaps.app as files_next, files_prev, files_view_selected, files_open_viewer, files_open_external, files_open_agent, files_filters, files_cycle_kind, and files_copy_path. y/R are the shared artifacts_copy_reference/refresh actions every Artifacts pane binds. Ctrl+J / Ctrl+K are the shared artifacts_load_more/artifacts_unload actions. Version cycling uses the old nested-files sub-tab keys. The pane also shares the navigation and jump keys described in Navigation in Agent, Stitches, Beads, Provider Documents, and Files.

Y copies the anchored stored path, except that PDF rows deliberately yield the live Markdown source they were rendered from when the index recorded one. Relative index paths are anchored to the producing workspace, so a copied path is always usable outside the workspace that created it, and the completion toast says when the copied path no longer exists. The palette previews that same preferred path, so what a PDF row shows is what %p copies. % adds the rest of the Files-pane copy targets: contents, Markdown link, source path, label, and metadata JSON, each of which also operates on the marked set.

a resolves the producing agent from already-loaded live and dismissed agents by artifact directory, then by raw name suffix, then by recorded agent name, always within the row's own project. A file whose source workspace was recycled still opens and still copies — only its Source path reports missing.

Filtering Files

Files keeps its committed query visible in a persistent filter row, the same idle chrome Agent, Patches, Stitches, Beads, and Plans use. Press f or / to edit it. Filtering is purely in-memory over the loaded snapshot, so a query narrows thousands of rows without a re-query. Files accepts id:, kind:, project:, agent:, workflow:, origin:, since:, until:, and host-owned limit:, plus free text matched against the label, logical path, stored path, source path, digest, and artifact id. id: is the pane's identity field: it matches a logical file id exactly and stays free-text searchable too. Tokens from different facets combine with AND semantics, while comma-separated or repeated values within id:, kind:, project:, agent:, workflow:, and origin: combine with OR semantics. kind: accepts the stored kinds chat, plan, image, markdown, pdf, and file; origin: accepts ref, created, and capture; since: and until: accept YYYY-MM-DD, YYYY-MM, YYYYMM, or a relative Nd / Nw / Nm offset and may each appear once.

z and the kind: token drive the same filter state: cycling with z closes an open edit session and sets kind: to the next stored kind actually present in the snapshot, wrapping back to All. A query listing several kinds is treated like All, so the next press selects the first present kind. When a filter hides every row, the pane says so and names the key that focuses the query row.

Epic phase sizes across plan surfaces

sase's TUI uses the literal scope labels xsmall, small, medium, large, and xlarge, with mint, sky, gold, rose, and violet chips whose text remains the primary signal. Valid older plans and phase beads with an omitted size use the stable small fallback, while an invalid authored value never produces a confident chip or count. The Plans pane also uses that shared display fallback for a legacy standalone task with no stored size; launch routing uses the same @small fallback.

Surface Size contract
Agents author and lander Shows every normalized authored size in roadmap order.
Agents phase worker Shows only that worker's normalized authored size, preserving phase isolation.
Artifacts / Beads epic, phase, and task beads Shows current persisted bead sizes in rows and details; the epic detail summarizes direct children in xsmall, small, medium, large, xlarge order. Standalone tasks appear in their own section. A legacy task with no stored size displays and launches through the shared small fallback.
Artifacts proposals, linked plans, and archives Retains the authored phases property exactly once instead of adding a competing roadmap.
Telegram epic review Adds a validated textual size breakdown while retaining the detailed Properties card and source/PDF attachment.
Epic clan summary, sase bead show, and epic work preview Continues using persisted bead sizes, which these execution surfaces already exposed.
Raw approval, validation/schema, source/PDF, and mobile attachment paths Remains a lossless generic/source view; authored phase metadata is preserved without a second summary.

Keybindings: Artifacts / Patches

Key Action
j / k Move to next / previous visible row (banner at fold < L2, PR at the leaf level)
< / > / ~ Navigate to ancestor / child / sibling PR
' Jump by adaptive hint (current tab); hints land on collapsed banners too
Ctrl+O / Ctrl+Shift+O Walk the link trail first, then the current-tab jump stack; back falls through to first hint
$$ / $1-$9 / $0 Follow the first / numbered artifact link, or open the complete links panel
` Jump to entry across all tabs (see Jump All Modal)
o / O Cycle PR grouping mode forward / reverse (BY_PROJECT ↔ BY_DATE ↔ BY_STATUS)
g / G Scroll detail panel to top / bottom
Ctrl+D / Ctrl+U Scroll detail panel down / up (half page)
{ / } Narrow / widen the shared Artifacts list panel (with wraparound)
Ctrl+J / Ctrl+K Load ace.page_size more rows / unload that page (rewrites the host limit: cap)

Note: o opens a direct grouping picker on the Agents tab. o/O cycle the L0 grouping bucket forward / reverse on Artifacts panes that have a grouping mode (each surface keeps its own in-session mode). Beads has no grouping modes, and a provider document pane has them only when it declares ref.grouping, so the keys are a silent no-op on panes without modes; the same is true on the Services tab. The Artifacts open-externally verb moved to E; bang-mode !o still marks PR origin. See PR Grouping and Folding and the Agents-tab Grouping Modes below.

PR Actions

Key Action
A Accept proposal (! = spec only, @ = mark ready to mail)
b Rebase PR onto parent
C / c1-c9 Checkout PR (primary / workspace 1-9)
d Show diff (Patches sub-tab only; d toggles sidecars on Stitches and the Services description)
e Edit spec file
f Edit the Patches filter query
F Edit hooks (re-run / delete via hint input; type . to pick from hook history)
M Mail PR
m Mark / unmark current PR (auto-advances to next)
n Rename PR (non-Sub/Rev PRs only)
!o Mark PR origin (sase/external/unknown)
!R Rewind to previous commit (! suffix skips VCS operations)
R Open the Refresh panel (refreshes immediately when it is disabled)
y Copy the PR's @patch: reference
s Change status (opens status modal)
S Bulk status change for all marked PRs
T / t Checkout + tmux (primary workspace / prompt for a workspace number)
u Clear all marks
v View files (hint mode)
V Open the Agent Run Log modal for the current PR
w Reword PR description
W Add tag to PR description
x Show/hide submitted PRs
X Show/hide reverted PRs
Y Sync workspace

PR Grouping and Folding

The Patches sub-tab is always grouped — the renderer walks one of BY_PROJECT, BY_DATE, or BY_STATUS and emits a banner row above each bucket. BY_PROJECT is the startup default; o cycles BY_PROJECT → BY_DATE → BY_STATUS for the current session.

Mode L0 buckets Notes
BY_PROJECT Project name Adds an L1 sibling-root sub-banner shared by foobar_1 / foobar_2 style suffixed siblings. Singletons suppress their L1 banner.
BY_DATE Today / Yesterday / This Week / Earlier Bucket from the latest TIMESTAMPS entry. Today/Yesterday add 4-hour L1 windows; hourly L2 headings appear only inside 4-hour windows with 2+ PRs. This Week adds day headings; Earlier adds week headings plus (no timestamp).
BY_STATUS Mailed / Ready / WIP / Draft / Submitted / Reverted / Archived Bucket from the literal status field; actionable buckets first (Mailed = awaiting response, Ready = next to mail), terminal states last. Adds an L1 sibling-root sub-banner shared by foobar_1 / foobar_2 style suffixed siblings inside each status bucket. Singletons suppress their L1 banner.

In BY_DATE mode, PRs sort newest-first within each date bucket. Today and Yesterday are grouped first by compact 4-hour windows (8AM-12PM); one-hour headings (09:00) appear only when that 4-hour window contains at least two PRs. This Week uses calendar-day subgroups; Earlier uses Monday-start week ranges. PRs without a parseable TIMESTAMPS entry fall into (no timestamp) under Earlier.

The active grouping mode is shown in the Patches sub-tab's info-panel header as a [group: <label>] badge.

Key Action
l Expand the focused banner one level (or peel one layer of the visible tree)
h Collapse the focused banner; on a collapsed L1 banner, escalate to its parent. With agent focus, collapse the deepest enclosing group.
zL Snap to fully expanded — all banners and Patch rows visible (z fold-mode prefix; bare L does nothing on Patches)
H Snap to fully collapsed — collapse every visible banner

Collapsed banner rows are first-class navigation stops: j/k step through them just like Patch rows, and ' jump-hints land on them too. After a fold change that hides the focused PR, focus snaps to the deepest collapsed ancestor banner so the cursor always sits on a row the user can see.

Fold Mode (z prefix)

Key Action
z c Cycle stitches section (expand → collapse)
z d Cycle deltas section (summary → files → line counts)
z h Cycle hooks section (expand → collapse)
z m Cycle mentors section (expand → collapse)
z t Cycle timestamps section (expand → collapse)
z C Toggle stitches section (collapsed ↔ fully expanded)
z D Toggle deltas section (summary ↔ line counts)
z H Toggle hooks section (collapsed ↔ fully expanded)
z M Toggle mentors section (collapsed ↔ fully expanded)
z T Toggle timestamps section (collapsed ↔ fully expanded)
z z Cycle all sections
z Z Toggle all sections (expand ↔ collapse)
z 1 Set every section to collapsed (level 1)
z 2 Set every section to expanded (level 2)
z 3 Set every section to fully expanded (level 3)
z L Expand every grouping banner

STITCHES, HOOKS, MENTORS, and TIMESTAMPS sections each cycle through three fold levels:

Level Behavior
Collapsed Notes truncated to fit; multi-line body shown as [+N lines]; only latest drawers
Expanded Full notes; body shown in dimmed text; all CHAT/DIFF/PLAN drawers visible
Fully Expanded Everything visible including rejected proposals

The lowercase cycle keys (z c, z h, z m, z t) step through all three levels in order. The uppercase toggle keys (z C, z H, z M, z T) skip the intermediate Expanded state, jumping directly between Collapsed and Fully Expanded.

When collapsed, a [folded: CHAT + DIFF + PLAN + N proposals] indicator appears on STITCHES entries with hidden content. The indicator width is pre-calculated so that note truncation accounts for it. TIMESTAMPS shows a [folded: N] indicator inline with the header and displays the most recent timestamp entry when collapsed, giving a quick view of the last lifecycle event.

The DELTAS section uses the same three levels with its own meaning. When collapsed, the section renders a one-line file and line-count summary such as DELTAS: +3 (+428) ~6 (+91 ~37 -14) -1 (-22) (10 files). When expanded, the alphabetical entry list is shown with colored glyphs (green +, gold ~, red -); fully expanded adds inline line-count tokens to each entry. Binary files display binary; zero-count entries display 0 lines. z D jumps between the summary and the full list. The section is omitted entirely when the Patch has no deltas.

Workflows and Agents

Key Action
r Run workflow on current PR
+ Run a custom agent (opens project/Patch selection)
Space Prefill the prompt with the most recently launched VCS macro (blank home prompt if none; Space then Ctrl+U for a blank prompt)
,<space> Run an agent from the current PR (skips selection)
Ctrl+G Open that most recent VCS macro in $EDITOR first
@ Restore a stashed prompt

If sase's TUI cannot detect a workspace provider for the selected Patch or agent, the quick-launch actions show an error toast instead of opening a prompt with a broken VCS prefix.

Bang Mode (! prefix)

Key Action
!! Run a background command (choose a project, a workspace, then a command)
!x Start / stop service host (or select process)
!o Mark PR origin (sase/external/unknown)
!R Rewind to previous commit (! suffix skips VCS operations)

!! ends in a Run Command picker over previously run background commands: type to filter the history or enter a new command, move with Ctrl+N / Ctrl+P or the arrow keys, press Enter to run it, and Esc to cancel. ,! skips the project and uses the current PR's project.

A background command is a oneshot service proc: a durable proc-store row with a recorded exit code, a store-owned log, and a stable #1–#9 index, shown in the Services tab's ── oneshots ── section. It runs outside the TUI and the service host, so quitting or restarting either does not kill it, and a rerun (r) works after a TUI restart. At most nine oneshots can be running at once; finished commands never block a new one (the oldest finished index is reused once all nine are taken). sase service proc run submits through the same path.

Hook History Modal

Type . into the F edit-hooks input to open the hook history modal, which lists previously added hook commands:

Key Action
j / k Navigate through hook history
Enter Add the highlighted hook to the current Patch
Ctrl+D Delete highlighted hook from history
Ctrl+G Edit first — select hook and open in input
Esc / q Cancel and close modal

The modal supports live filtering as you type in the search box and displays last-used timestamps for each hook.

Leader Mode (, prefix)

Help is not a leader command: press the app-level ? on any tab to open the Help modal.

Key Action
,, Repeat the last leader command
,! Run command using current PR context
,A Open the Agent Run Log modal for the current PR
,c Clear COMMENTS field (kills CRS agents, deletes CRS proposals)
,C Review mentors (opens Mentor Review modal)
,h Run agent from home prompt context; bare prompts default to #git:home
,m Open Launch Control (aliases, providers, tmux Agent; see Launch Control)
,U Open Update panel (SASE, providers)
,E Plan Everything from cached update snapshots and skip confirmation if runnable
,L Jump to the log entry for the most recent error toast
,M Kill running mentors
,R Show runners info
,<space> Run agent from current PR (skips project selection)
,. Open the Prompts overlay on History
,Ctrl+G Edit the newest prompt-history entry in $EDITOR (no overlay)
,> Open the Prompts overlay on History with cancelled prompts visible
,@ Open the Prompts overlay on Stash without auto-restoring a lone entry

The ,h shortcut opens a home-context prompt directly. Project and PR launch pickers use lifecycle-aware discovery: project entries, including home when it appears in picker lists, must have enabled and launchable ProjectSpecs; PR choices come from enabled ProjectSpecs. Disabled projects do not appear in normal launch pickers until they are enabled with sase project enable <project>. You can also type a known-project VCS ref explicitly; launch preparation treats that as intent to resume work and re-enables the project before claiming a workspace. When the current project appears in the list, the + picker highlights that row on mount; typing in the filter box still resets the highlight to the first match.

Project launch pickers also support Ctrl+D for cleanup of empty project entries. This deletes only the highlighted project's active/archive ProjectSpec files, refuses entries whose ProjectSpec files still contain Patches, and does not delete workspace checkouts or other SASE state. For lifecycle changes, bulk operations, ProjectSpec editing, or deleting the whole SASE project directory, use the Projects tab of the SASE Admin Center (press #).

The repeat binding is the leader prefix followed by the configured repeat_last key. With the defaults both are comma, so the sequence is ,,; if the leader prefix is changed but repeat_last is not, the second key remains comma. Repeat re-dispatches the last recognized leader subkey against the current tab and selection. If no leader command has been run yet, sase's TUI shows a toast and does nothing.

Note: ,x (kill & edit) and ,X (kill & edit last launched agent) are only available on the Agents tab — see Agents Tab Leader Mode.

Mentor Review Modal

Press ,C to open the Mentor Review modal, which lets you navigate mentor comments, accept or reject suggestions, and apply accepted changes. See docs/mentors.md for the full mentor system reference.

Key Action
j / k Navigate between mentors
n / p Navigate between comments within a mentor
N / P Navigate between accepted comments only
Ctrl+D / Ctrl+U Scroll comment details down / up
Space Toggle acceptance of the current comment
a Apply accepted comments and propose (amend with propose)
A Apply accepted comments and commit
r Run a mentor profile (opens profile picker)
y Copy the current comment to the clipboard
Shift+K Kill a running mentor
Esc / q Close modal

Copy Mode (% prefix)

Press % to open the Copy as… palette for the selected Patch. Select a row with the mouse, arrows or j/k and Enter, or complete any configured two-key accelerator directly. q/Esc cancels; configured target keys take precedence if rebound to j, k, or q.

Key Action
%% Copy Patch
%! Copy Patch + snapshot
%b Copy bug number
%c Copy PR number
%n Copy PR name
%@ Copy @patch: reference
%l Copy Markdown link
%p Copy project spec file
%s Copy sase tui snapshot

Keybindings: Agents Tab

Machines

When at least one remote machine is enrolled, the Agents tab shows local and remote rows in one list. Remote agent, session, and clan nodes carry a short host-alias chip such as apollo or mac; local rows never carry a here chip, including under a by-machine group header. A fleet header row appears above the list only when there is something to act on. A fleet configuration or follow-store error is shown on its own; otherwise the row joins machine diagnostics and machine feed errors with ·. Diagnostics naming one or two machines read apollo unknown (or apollo, mac unknown); anything else reads as a diagnostic count such as 3 machine issues. One machine feed error reads <alias>: feed <status>: <detail>, with (cached 5m ago) when a cache backs it; two or more read 2 machines with feed errors. Otherwise the row stays hidden. An entirely local setup keeps the compact local view and exposes the machine connection route.

Remote rows use the same visible operations where the owner advertises support: machine status, retry, bounded remote content, pending question/gate handling, stop, and fork. The normal x stop and F fork actions work on capable remote rows; actions that require local files, tmux, or local Patch state stay unavailable. Remote mutations are durable requests, optimistically annotate the row while in flight, and refresh the Agents list after they settle. Setup and recovery commands are covered by the Remote Dispatch Runbook.

Remote agents are grouped into the same session and clan nodes as local ones, with the same queue badges (cN, wN) and the same session and clan status rules. A remote row stays free of connection chrome while its machine's feed is online and fresh; anything else is shown on the row. A stale feed reads like stale · cached 5h ago, and an invalid feed reads feed invalid, is named in the Agents header, and adds a Feed error line to the detail panel, so cached data never looks silently healthy. A remote row the owner reports as WAS RUNNING shows how long ago it was last seen.

Agent Tabs

The Agents tab shows one agent tab at a time instead of the whole roster. A tab is a presentation-only placement: it never changes where an agent runs, its identity, clan, session, or tribe. Tabs come from each root's recorded tab (see Tab Directive): %tab:<name> authors a named tab, and roots without one land on the default tab. The default tab is labeled main, or ⌂ <machine name> in machine mode (ace.agent_tabs.machine_tabs: on, or auto when dispatch machines are configured), where <machine name> is the viewer's id.machine_name (⌂ local when unset). In machine mode each remote machine gets its own ⌨ <alias> tab; %tab:apollo is still an ordinary named tab, distinct from ⌨ apollo. A remote alias literally named local, or equal to the local machine name (case-insensitive), renders as ⌨ <alias>·remote.

A tab exists while at least one visible root resolves to it, and the strip beside the fleet status appears once two or more tabs exist (with a single tab the Agents tab looks exactly as before). The active tab shows as a pill; inactive tabs show their label, count, and only the non-zero attention tokens (S/F/U). Machine tabs carry a ⌨ glyph whose label turns amber when its host is stale and red when it is invalid or offline. The local machine tab carries a teal ⌂ home glyph instead. An arrival dot (•) marks an inactive tab that gained a new root since you last visited it. At narrow widths the strip compacts and then windows around the active tab with ‹N / N› overflow chips that open the tab picker.

] / [ cycle tabs (wrapping, including overflow tabs). The palette entry Agents: go to tab…, the ' jump hints on strip chips, and clicking a chip or overflow chip all switch tabs too. Every cross-tab jump switches to the target's tab first and then reveals it: Node Finder searches all tabs and chips off-tab rows, ,j / ,J consider every tab, and notification, link, monitor, run-log, and Files jumps all land cross-tab. Choosing a tab while the All-tabs layout level is active drills into that tab. Selection, folds, and sticky panels are remembered per tab, and the active tab persists across restarts. If the active tab's last agent leaves, the tab stays selected with an empty state until you navigate away; other empty states name a hiding query (with a clear-filter hint) or an unavailable feed.

Bulk confirmations name their scope while the strip is visible: panel-wide actions and the custom selector read on <tab> (for example on sase), across all tabs only at the all-tabs scope, and a marked cleanup that spans tabs adds an N of M marked agents are on other tabs line. Marks themselves stay global. Tab styling, ordering, and machine-mode behavior live under ace.agent_tabs (see docs/configuration.md).

Launching from a named tab inherits it: the prompt bar shows a tab: <name> chip and the submitted prompt gains %tab:<name> (opt out with %tab:main or ace.agent_tabs.launch_from_view: false). gb opens the Launch Tab picker to insert or replace %tab. A launch that lands on another tab toasts its destination (blog-fix → blog) and lights that tab's arrival dot. Move agents between tabs with sase agent tab list|set|unset (see Moving agents between tabs) or the N Tribe & Tab modal, which completes known tabs and applies optimistically with rollback; rows owned by a remote machine cannot be moved. Filter the roster with the tab: query field (exact match, or main for the default tab).

Key Action
j / k Move to the next / previous visible row; while a whole panel is selected, or when the focused panel has no other selectable row, cycle whole panels instead
J / K Cycle focus across expanded tribe side panels (forward / reverse)
' Jump to a row, collapsed grouping banner, or split-panel title by adaptive hint
Ctrl+O / Ctrl+Shift+O Walk the link trail first, then the current-tab jump stack; back falls through to first hint
$$ / $1-$9 / $0 Follow the first / numbered artifact link, or open the complete links panel
Ctrl+J / Ctrl+K Next / previous card in the focused deck panel (wraps; sets the panel's preferred card)
( / ) Older / newer card block in the focused deck panel (wraps; only when the shown card has 2+ blocks)
] / [ Next / previous agent tab (wraps; only while the strip shows two or more tabs; see Agent Tabs)
(palette) Pick an agent tab from the command palette (Agents: go to tab…; same strip gating as ]/[)
P Cycle the focused panel's deck view wider, pinning it fixed (wraps; Main and Files only, only when there is more than one distinct layout)
` Jump to entry across all tabs (see Jump All Modal)
" Find and jump to any node, including hidden ones (see Node Finder)
0–9 Jump from a selected clan, agent node, session member, or whole-panel roster to its numbered member or neighbor (live in the jump panel below the detail panels)
o, then p/d/s/m Choose grouping mode: Project, Date, Status, or Machine
oo Step the Agents panel layout ladder forward, then close the grouping picker (see Grouping Modes)
g Scroll to top (focused deck panel)
G Scroll to bottom (focused deck panel)
Ctrl+D / Ctrl+U Scroll the expanded jump panel when its content overflows, else the expanded header panel when its content overflows, else the focused deck panel (half page)
Ctrl+N / Ctrl+P Focused panel to the next / previous deck (Main → Files → Tools → FINAL, wraps)
p Pick the focused panel's deck: m Main, f Files, t Tools, n FINAL; M/F/T/N show it in the other panel (opening one below if needed); pp/Esc close
\ / \| Split deck panels stacked / side by side; the same key erases its divider, the other key nests a third panel or turns three panels
} / { Grow / shrink the focused deck panel (split layouts only)
Ctrl+F / Ctrl+B Focus the next / previous deck panel in reading order, wrapping (split layouts only)
Ctrl+Shift+F / > Swap the focused panel's session with the next panel in reading order, wrapping; geometry and ratios stay with the slots and focus follows the content (split only)
Ctrl+Shift+B / < Swap the focused panel's session with the previous panel in reading order, wrapping (split layouts only)
Ctrl+Shift+D / Ctrl+X Close the focused deck panel; focus goes to the most recently focused survivor (split layouts only)
Ctrl+T Turn the deck layout: transpose a two-pane split, or a three-pane T layout (split layouts only)
Ctrl+S Toggle the node rail (persisted preference); while zoomed, restore the snapshot like Z

Note: o opens a direct grouping picker on the Agents tab. o/O still cycle the L0 grouping bucket forward / reverse on Artifacts panes that have a grouping mode (each surface keeps its own in-session mode). Beads has no grouping modes, and a provider document pane has them only when it declares ref.grouping, so the keys are a silent no-op on panes without modes; the same is true on the Services tab. The Artifacts open-externally verb moved to E; bang-mode !o still marks PR origin. g/G keep their conventional vim-style scroll-to-top/bottom meaning on every tab. See Grouping Modes below.

The numbered NEIGHBORS rows use dotted agent-name relationships rather than Patch sibling families. Relations are keyed on the name a row presents as its sase agent name, so a session participates under its bare session name rather than its root member's -- name. The section shows visible ancestors and descendants plus neighbors from every dotted hood that contains the selected sase-agent name — including the hood that matches that name exactly. For example, foo.bar.worker lists peers under foo.bar and cousins elsewhere under foo, grouped deepest hood first, and a session fam lists fam.helper as a descendant while fam.helper lists fam back as its ancestor. Dotless names can still have descendants such as foo.child. A digit jump target is resolved by stable identity and revealed through any clan, session, workflow, or grouping folds before focus moves.

When a clan or a sase agent is selected, its jump panel assigns a fixed number to each numbered row, up to 100 targets. A sase agent is a multi-member session container or a single agent; sase-agent panels number their SESSION TURNS roster (when present) and then their NEIGHBORS section from one continuous ladder. Clan panels number their CLAN MEMBERS roster and then their CLAN NEIGHBORS section from one continuous ladder. A selected session turn row numbers its enclosing session's SESSION TURNS roster the same way, listing every sibling except itself from the same ladder; a turn row owns no sase agent, so it has no NEIGHBORS rows to follow the roster. Documents with at most ten numbered rows use 0–9; larger documents number the first 100 rows with two-key values 00–99 and show any remaining entries as an unnumbered count. Every live numbered target lives in the sticky jump panel below the deck panels, so the digit answers stay on screen while the deck body scrolls. The deck body no longer contains these roster sections. After the first digit of a two-key jump, the panel narrows to the matching candidates; press Esc to cancel or any non-digit key to cancel and continue with that key's normal action. A successful jump expands only the target's ancestor chain, switches tribe panels when needed, and participates in the normal Ctrl+O jump-back history. A digit on a dismissed neighbor revives that agent instead of jumping. If the roster or the neighbor relationship changed since the panel was drawn, the jump is cancelled with a warning rather than landing somewhere stale.

In-flight targets, whose glyph is ▶ (running) or ◐ (starting), are lit: their number chip runs into a softly tinted pill holding the bold name and glyph. This appears in the collapsed legend, the narrowed candidates, and the expanded roster alike. When the collapsed legend cannot show every target, it fills its slots with in-flight targets first (still in number order), then the lowest remaining numbers. The +N overflow count gains a lit ▶M when M in-flight targets remain hidden behind ..

Agent Actions

Key Action
!R Revive a previously dismissed agent
a Open completion artifacts for the focused agent; in tmux, press again to close the viewer pane
+ Run custom agent
A Toggle bare %auto plan auto-approval / auto-answer questions
F Prepare a fork of the selected agent/session, named proc, monitor, clan container, or focused named tribe panel
n Name agent
r Refresh the Agents tab, or open the Refresh panel when that panel is enabled
R Edit prompt and relaunch the selected local agent, or retry a remote row on its owner
v View files (hint mode; annotates clan/session containers in place; adds ⚒ run log targets on the Runs card that open the retained run log in the pager)
D Toggle prior-attempt view (only shown when the agent has retried)
d Expand / collapse the agent header panel (sticky identity header above the detail panels)
. Expand / collapse the jump panel (sticky jump targets below the detail panels)
I Show/hide non-run agents
V Open the focused agent's metadata as a sectioned document in the pager (see below)
w Wait/unwait agent (opens WaitModal — see below)
W Prepare a prompt that waits for the selected agent/session, clan, or named tribe; marks produce %w:a,b,c
m Mark / unmark current agent, or all top-level agents in focused collapsed group (auto-advances to next)
s Save and dismiss marked agents as a revivable group (opens optional group-name modal)
U Toggle the focused agent's unread marker
u Clear all agent marks
x Kill / dismiss the agent, clan, or focused panel or group (or every marked agent); stop a monitor or proc
X Open the cleanup panel for panel, all-panel, tribe, marked, group, or custom cleanup
Enter Act on agent: review pending gate, go to Patch, or choose when several apply
e Edit chat in editor; with marks, open all editable marked transcripts in one editor invocation
E Edit focused deck content in editor (Files opens the real path; Main/Tools open the active card text as a temporary .md)
t Open the focused agent's tmux target, or a workspace chooser (m marks many; a selector opens one)
T Open tmux window in the agent's primary project workspace
N Open the Tribe & Tab modal (tribe input is pre-seeded with pinned for agents without a tribe; empty keeps it, Ctrl+D clears it; on a clan row or clan member it sets the clan's recorded tribe; the second input moves the agent's whole presentation root between tabs — empty keeps, main or Ctrl+T clears; remote rows cannot move)
z Start metadata fold mode for clan, agent node (session or single agent), or selected whole-tribe detail panels
Z Zoom the focused deck panel in place (node column hidden); press again to restore
= Isolate the focused tribe panel, or restore the remembered pre-isolation layout
- Collapse every open agent-node/clan fold in the focused tribe panel, or restore the last sweep's folds
_ Collapse every open agent-node/clan fold in every eligible tribe panel, or restore the last sweep's folds
Ctrl+N / Ctrl+P Focused panel to the next / previous deck (Main → Files → Tools, wraps)
p Pick the focused panel's deck: m Main, f Files, t Tools; M/F/T show it in the other panel (opening one below if needed); pp/Esc close

When t opens the Tmux Workspace chooser, a displayed selector key opens that one target immediately, even if other rows are already marked. m marks or unmarks the highlighted row and advances to the next (wrapping around). Enter opens every marked target in the chooser's displayed order, or only the highlighted row when nothing is marked. q / Esc cancel without opening anything.

On Artifacts and Services, r still runs a Patch workflow or an Axe job/bgcmd, and R still opens the Refresh panel (or refreshes immediately when that panel is disabled). Only the Agents tab swaps those keys.

x: how a kill is carried out

x on a running agent hides its row at once and sends one SIGTERM from the TUI. The rest runs in a durable sase agent persist-cleanup proc, which keeps going if you quit the TUI. It publishes the dismissal first, then finds the agent's whole process set: the runner's process group and session, its ppid descendants, and every process that inherited the agent's launch scratch key (recorded as launch_scratch_key in agent_meta.json), including ones that started their own session. It sends SIGTERM, waits at least six seconds so a sase tool run wrapper can finish its own five-second escalation, sends SIGKILL to whatever remains, and verifies every process is gone. Only then does it release the workspace claim and delete workflow artifacts. A registered proc or monitor supervisor found in the set is stopped through its own stop, so its record settles. A dismissed row that is not success-terminal (for example a FAILED row in retry backoff) is terminated the same way when its recorded runner is still alive.

If a process survives SIGKILL, the row stays dismissed, the workspace claim and artifacts are kept, and an error toast names the surviving PIDs. sase agent kill NAME runs the same termination in the foreground.

x on a running monitor, an active named proc, or a pending gate removes its row in the same step. A running monitor goes through the ordinary kill confirmation and the durable proc stops it before the other side effects. An active named proc or pending gate asks for confirmation, then its row is hidden at once while the same durable bulk transaction stops the named proc (through the native proc service) or cancels the gate. Clan, panel, group, and marked cleanups include active named procs and pending gates the same way instead of skipping them; any selected member the cleanup cannot cover is named in the confirmation modal. A gate whose decision started executing under the cancel comes back with an error toast. A gate that is already settling keeps the "waiting for a decision" warning.

Enter: act on an agent

Enter on an Agents-tab row opens that agent node's pending gate (every gate kind, including plan, question, sudo, launch, HITL, and custom gates), jumps to its Patch, or — when more than one target applies — opens a one-keypress chooser. One target runs directly; two or more open the chooser. Gates come first, newest first, with the Patch last; the first row is the primary action, so Enter then Enter always opens the newest pending gate. The footer names what Enter will do: the gate's action (for example review plan), go to PR for a lone Patch, or choose action when several targets apply.

What counts as a target depends on the selected row:

  • A gate row targets its own gate while it is pending. A settled gate toasts This gate already settled (<state>).
  • A session container collects every pending gate across its members, plus the session's Patch (or, when the container has none, the Patch of its most recently started member). A gate reachable through both a gate turn row and its inbox notification counts once, so a session waiting on a single tale plan, with no Patch, runs Enter directly (footer review tale plan); a session that also has a Patch shows the choose action footer instead.
  • A session member or standalone agent targets gates it created and gate notifications matched to it. An agent stopped on a question, or a workflow step waiting for input, falls back to the answer flow when no gate notification matches.
  • A workflow step child uses its parent workflow's Patch; a child of a project-level workflow uses that workflow's meta Patch, if it recorded one.
  • A remote row answers its pending remote attention request.
  • A clan container toasts Select an agent inside this clan. Monitors and named procs have no targets, and a grouping banner ignores Enter.

With no target, Enter toasts No pending gate or Patch for this agent. Enter works from the first frame: if the notification inbox has not been polled yet, sase's TUI reads it once off the UI thread and then acts. It searches every undismissed notification rather than a bounded page, so older pending decisions stay reachable regardless of backlog size. Before a gate opens, the target is revalidated; a gate that settled in the meantime toasts Gate <id> is no longer pending.

The chooser is titled Act on <agent> and groups rows under GATE/GATES and PATCH headings, each row showing its key, label, and a status badge (with age for gates). A guidance line (⏎ again → <primary action> · or press a key) names the primary. Chooser keys: one gate uses g; several gates use 1–9 in display order with g as a hidden alias for the first; the Patch uses p. Enter selects the highlighted row (starting on the primary), j/k or Up/Down move, Esc/q cancel, mouse click selects, and other printable keys are swallowed. A choice whose target disappeared while the chooser was open toasts That action is no longer available instead of acting.

The retired ,n leader chord is replaced by Enter. The direct Patch jump survives as the jump_to_agent_patch command, unbound by default; see ace.keymaps.

Forking Agents and Groups

With a named agent selected, press F to open a prompt prefilled with #fork:<agent>. Selecting a session root uses the session name instead. Failed named rows are valid fork targets; the child receives the failed parent's transcript plus the recorded failure message when available. The same action works on the synthetic container row for a clan (#fork:<clan>) and while an expanded or collapsed named tribe panel has whole-panel focus (#fork:@<tribe>). The reserved @default panel and grouping banners are not fork targets.

Finished planner rows (EPIC CREATED, PLAN COMMITTED) are also fork targets; a session root forks its whole session, including the planner transcript and its gate/monitor turns. PLAN REJECTED and STOPPED rows are not fork targets.

F also works on a stand-alone named-proc row and on a monitor session member, active or settled. The prefilled reference is the proc's exact durable proc ID — not its reusable friendly name — so the eventual fork can never drift onto a different proc if that name is reused later; the footer and prompt label still show the friendly proc name. A proc or monitor row has no chat, so it never advertises retry, edit-chat, or name; x kills an active proc or stops a running monitor, and dismisses a settled one.

Press W on the same selections to prepare %w:<agent-or-session>, %w:<clan>, or %w:@<tribe>. A non-empty marked set takes precedence and produces one comma-separated wait over the named marked rows instead of the focused group. The reserved @default panel and grouping banners are not wait targets either.

Group references are dynamic; pressing F does not snapshot the selected transcripts. A session reference contributes every known concrete turn in the session's sequential chain, oldest first, agent turns and monitor turns alike, and includes turns that ended unsuccessfully with their failure context rather than dropping them. Only turns that are still running, or whose transcript/log is missing or unreadable, are listed as not shown. Agent-turn members are transcripts of prior agents' conversations; named-proc and monitor members are command execution records whose output is untrusted evidence, never an instruction. An explicit --<suffix> reference contributes only that member. A clan reference resolves the newest clan generation when the deferred launch proceeds and requires every member of that generation to have succeeded. A tribe reference follows the next-entity rule: the new run waits for the earliest successful entity in that tribe launched after its own artifact was created. It does not fork the agents currently visible in the selected tribe panel. See Tribe wait and fork targets for the full ordering rules.

sase's TUI also tries to carry VCS context into either prefilled prompt. For one selected agent or session row, it uses that row's launch ref when it can resolve it. For a selected clan or tribe, it adds a VCS tag only when every real agent in the current scope resolves to the same workflow and ref. Mixed or missing context produces only the #fork or %w reference, leaving you to add the desired #git, #gh, or other VCS tag. A marked wait is different: one mark uses that row's context; multiple marks use the selected marked row, or the first named mark when the selection is elsewhere. The VCS lookup runs off the UI thread. Before opening the prompt, sase's TUI verifies that the selected scope and its members did not change; marked waits instead verify the marked target set. A stale selection cancels with a warning rather than opening a prompt for the wrong target.

Clan and Session Detail Panels

Selecting a clan container shows a CLAN summary. Selecting a real multi-member session root titles the sticky header panel SESSION (cyan, matching the name), then shows the session's normal Main deck cards, with its SESSION TURNS roster in the jump panel. A selected agent turn — standalone or session member — titles the header panel AGENT TURN (gold, matching the name). Both rosters use the numbered member jumps described above. Clan direct members in the Agents list sort by status priority — Failed, Stopped, Running/Starting, Queued, Waiting, Done — with launch recency breaking ties. The clan's CLAN MEMBERS jump-panel roster instead keeps chronological launch order so its numbers do not change as statuses change; a nested session remains one direct entry with its chain indented beneath it. Session rosters retain sequential chain order.

Selecting a session turn row (not the container) also shows its SESSION TURNS roster in the jump panel: the same enclosing session's members, in the same chain order, minus the selected member itself. The heading carries a dim · <session name> suffix naming the session, since the count shown is one less than the session's full size. Unlike a container panel, a member panel folds this roster (and the rest of its own sections) using the selected member's own three-level agent scale rather than the session's two-level scale, so no Fold: N/M header line appears.

Clan metadata has three session-only detail levels. Session metadata uses the last two effective states as its two-level scale for fold-aware metadata: session level 1 is expanded and level 2 is fully expanded.

Level Content
1 Core member rows plus headings and counts; expensive disk-backed bodies remain deferred
2 Bounded triage detail such as activity, wait/retry state, context summaries, and compact member metadata
3 Full available sections and the richest member annotations, including workspace, timestamps, and attempt count

The compact clan roster and its fixed numeric member jumps remain available at all three levels. Clan sections appear only when their content is known to exist: known-empty sections are omitted, while unknown required disk-backed content produces one dim ⋯ scanning member data… tail for the document instead of a placeholder for each section. Session rosters and their numeric jumps likewise remain available at both effective levels. Session macro and prompt sections are omitted when absent, while an unfinished reply remains visible as pending rather than disappearing as empty. AGENT PROMPT and the consolidated AGENT REPLY are plain navigation anchors whose available conversation bodies stay fully visible at both session levels.

v annotates a clan document in place rather than replacing it: the panel keeps its current sections and fold level and gains inline [N] markers. Clan hints come from the clan summary, member-attributed ERRORS, variable, REPLIES, and PROMPTS bodies, per-entry SASE CONTEXT rows, SLOW TOOL CALLS rows, and the COMMITS lane; each path resolves against the workspace of the member that produced it, and summary paths that name a plan, artifact, or delta resolve through an index computed during clan enrichment rather than a blind workspace join. A logical plan: reference is marked and resolved as one token including the prefix, and an archived prompts/<YYYYMM>/<name>.md reference resolves into the project's agents sidecar checkout rather than the agent workspace. Hints exist only where text is actually visible, so availability follows the active fold level — level 1 hints the clan summary only, level 2 adds the bounded triage lines, and level 3 adds full bodies and per-entry context, tool-call, and commit rows. Markers are numbered in document order. The jump panel is hidden while hint mode is active and returns with the same fixed member numbers when it ends. While clan enrichment is still in flight the hint bar stays open and the document is re-annotated when the deferred sections land.

The default fold chords are:

Key Action
zz Cycle the whole Main deck document forward through its active scale
zZ Open every fold to the active maximum; at that maximum, close every fold to the minimum
za Cycle the foldable section, tribe CLAN SUMMARIES/PROMPTS entry, or session SASE CONTEXT lane at the top of the focused Main view
zA Toggle that foldable section or entry between collapsed and fully expanded
z1-z2 Set a session directly to level 1 or 2
z1-z3 Set a clan or single-agent scope directly to level 1-3
z1-z4 Set a selected whole tribe panel directly to level 1-4

The Fold: N/M header field reports the position within the active scale, while glyphs on foldable headings show their effective per-section levels. Only session panels print that header line; a single sase agent relies on the SLOW TOOL CALLS heading glyph (and, with the jump panel expanded, the NEIGHBORS heading glyph) instead. On a session conversation heading, za and zA refresh normally but do not create or change a section override. A valid panel-level cycle, extreme toggle, or direct selection clears real per-section overrides. Fold state is shared by the Agents detail documents: an ordinary agent's own three-level scale shapes its NEIGHBORS and SLOW TOOL CALLS sections, so z* chords have a visible effect on a regular sase agent, and the same clan/agent scope carries over to the next selected clan or session container. Most other sections on a regular-agent panel stay fold-inert, except the SASE CONTEXT / BEAD lane's multi-line values: at scale position 1 (z1, Collapsed), a task or phase worker's Notes, and a task worker's +1 Evidence, collapse to a one-line digest, N lines (zz to show); single-line values never fold, and at positions 2-3 the full value renders. A selected whole tribe panel adds level 4 for exhaustive detail. These keys are configurable; see Agent Clans, Sessions, and Tribes for the grouping model.

When sase's TUI knows a planner/author or epic lander's associated plan, the metadata panel adds a PLAN lane in SASE CONTEXT. A task worker that authored a plan in the same run also shows a PLAN lane beside its task BEAD lane. The lane order is PLAN, BEAD, ARTIFACTS, the audited MEMORY, GLOSSARY, SKILLS, and WORKSPACES, with absent lanes omitted once they have resolved. A plan or any recorded output is enough to show the context section. An epic phase worker never shows its parent epic as a PLAN lane. Instead, its launch metadata identifies the epic plan and exact phase bead, and sase's TUI derives one phase-local BEAD lane from that phase's validated, frontmatter-ordered entry. The lane shows Phase Title, Description, Size, Epic Plan, and Epic Title; Size uses the literal xsmall, small, medium, large, or xlarge label and the same accessible chip palette as epic summaries. The phase title comes from the same validated entry, is normalized to one line, and wraps losslessly like the other values. Authored descriptions are also normalized to one line; a missing description uses the same stable plan-and-phase pointer generated during deterministic bead creation. This modern path does not read the bead store, and missing, unreadable, damaged, explicitly invalid, or out-of-range metadata keeps the known identity/path fallbacks while rendering optional fields as unavailable, without exposing the epic goal, dependencies, or any peer phase.

SASE CONTEXT streams: its lanes are resolved cheapest-first in batches and each batch is published as it lands, so the section appears almost immediately instead of waiting for the slowest lane. A lane that has not resolved yet is not the same as a lane that resolved to nothing — while a lane is still in flight it renders a dim, non-interactive resolving… row that holds its position in the order above, so the section fills in rather than reshuffling as the remaining lanes arrive. PLAN and BEAD share one backing lane, so both show their own resolving… row until it lands and the panel can tell which of the two actually has content. Repaints coalesce through the same debouncer that drives the rest of the header, leaving hint mode and scroll position undisturbed.

Within SASE CONTEXT / ARTIFACTS / Beads, a selected agent's newest current structured bead note appears beneath its bead row with its original author and append time. The card shows at most three wrapped note-text lines, then points to the existing numbered bead detail for the full note and any earlier notes. Edits retain the original author, retracted notes disappear, and an audited read reason remains as a separate read: continuation. The cached touch index supplies these previews off the interactive render path; it never opens bead streams or resolves bead detail while navigating.

For planner/author and lander rows, the lane body contains the complete normalized Title, Goal, and canonical Path; a tale additionally gets a Size row between Goal and Path, showing the authored xsmall/small/medium chip, or the medium chip plus a (default) marker when the tale's size was missing or an over-sized legacy large/xlarge normalized at launch (see Plan Frontmatter Schema and Validation); epics never show a Size row here. The lane header shows the effective user-facing tier (plan, tale, or epic) and, for epics, the phase count. The tier records how the user approved the plan: approve means a plan approved without an SDD commit, tale (and the legacy commit-only action) means a committed tale, and epic means a committed or launched epic. That displayed choice survives a later commit or launch failure. When action metadata is absent, sase's TUI falls back to a valid authored tier: tale or tier: epic; a legacy committed record without a readable authored tier falls back to tale, while a genuinely unresolved tier renders tier unavailable. Path selection is independent: committed paths are relative to the agent workspace (including SDD sidecars such as sase/repos/plans/...), while pending and explicitly uncommitted archives use ~/.sase/plans/....

Validated authored epics add a phase roadmap beneath those three rows. sase's TUI validates this display as a launch consumer: modern phases retain their authored xsmall, small, medium, large, or xlarge size, while historical phases with an omitted size normalize to small; an explicit invalid size or other schema damage makes all phase metadata unavailable. Each entry shows its one-based authored order and diamond, title, fixed-width literal size chip, canonical ID, no dependencies or after <id>, ..., plus an authored phase model when present. Optional descriptions get their own hanging-indented line. The order and diamond glyph describe static plan structure, not execution state or live bead progress. Tales retain the compact four-row form (Title, Goal, Size, Path). The chip remains visible while the title and other long ASCII or wide-Unicode values fold completely without ellipses; the lane caps content at 80 terminal cells on wide panels and reflows to the normal deck panel or zoomed deck width. Logical header text contains the same size labels for search, copy, and style inspection. In hint mode only Path receives a numbered file hint, allocated in the plan's visual reading order. Missing or damaged plans keep their known lane and path visible; when epic context is known, validation failure renders one quiet phases unavailable header state rather than partial phase data.

sase's TUI separates fast visible-inbox loads from full-history scans. The visible inbox is the normal Agents-tab working set: every active row plus every non-hidden completed row, subject only to a first-paint safety cap (about 6x the measured inbox). The first load for a committed query reads that whole inbox as a baseline; refreshes after a baseline stay windowed patches over it. Startup, manual This-tab refresh (r then r on Agents), and active agent search use that path through the persistent artifact index when it is available.

If the index is missing or unhealthy, sase's TUI falls back to a bounded source-artifact scan for the first paint and shows a repair warning with the reason. That repair state can arm a deferred full-history reconcile after input has been quiet, but normal Agents This-tab refreshes still stay on the visible-inbox path. Use sase agent index status --json for a lightweight check that does not scan source artifacts, sase agent index verify to compare the index with source artifacts, and sase agent index gc to rebuild the index and dismissed projection. Use r then f (or ,y) when you want an immediate full-history refresh: it revalidates and reads the whole archive through the artifact index, and falls back to a full source-artifact scan only when the index is missing, busy, or fails. Normal SQLite deletes never reclaim disk space; sase agent index vacuum reports freelist pages and dismissed row counts, and -a/--apply compacts the index file with VACUUM (dry run by default; this never removes or alters a row).

Refresh Panel

Press R on Artifacts or Services, or r on Agents, to open the Refresh panel: a centered single-key chooser that names every refresh sase's TUI can perform, shows how fresh each target already is, and runs exactly one of them.

Key Aliases Option What it does
r R, 1 This tab Reload the current tab's visible surface (Agents inbox, Artifacts, or Services).
f 2 Full history Reload Agents from the complete archive. Works from any tab.
u 3 Usage windows Re-probe provider subscription limits.
a 4 Everything Forced sanity sweep plus full history and usage.

R is an alias for This tab, so a double-tapped R R on Artifacts or Services (or r r on Agents) reproduces the old immediate refresh. j/k (or arrows / Ctrl+N/Ctrl+P) move the cursor; Enter activates the highlighted row (This tab when the panel opens); Esc or q cancels. The ,y leader chord opens the same panel from any tab with Full history highlighted and a ,y lives here now — press f reminder, so ,y then f (or Enter) runs the full-history reload.

Each row shows a freshness chip from a reload requested in this session (12s ago, 2h ago, just now). A surface that has not been reloaded yet shows —, never a guessed age. The usage row shows checking… while it looks up how many providers it can refresh. When auto-refresh is enabled, a header line reports its cadence and the countdown to the next tick.

If usage metrics are disabled or no providers are eligible, the usage row stays visible but unavailable: pressing u explains why and leaves the panel open so you can pick something else.

The default-on refresh_panel sunset flag is the escape hatch back to the old gestures. Disable it (sase flag disable refresh_panel) to restore immediate current-tab refresh (R on Artifacts and Services, r on Agents) and ,y (Agents full-history) without the chooser. See feature flags.

The dismissed projection that hides agents from the visible inbox is rebuilt from the in-memory dismissed set unioned with every dismissed-bundle summary. Reviving an agent now purges its dismissed bundle, so a revived agent stays visible. For archives that accumulated stale bundles before that fix, plain sase agent index gc is not a repair on its own -- it rebuilds the projection from those lingering bundles and re-hides the revived agents. Run sase agent index gc --purge-revived-bundles (-r) to first delete dismissed-bundle files and summary rows for suffixes that are no longer present in dismissed_agents.json, then rebuild the corrected projection.

When one or more agents are marked, e edits the marked set instead of only the focused row. sase's TUI opens editable completed transcripts in visible row order, deduplicates repeated paths, skips live marked rows that are still running or have no chat file, and reports that live skip count. Stale marks are ignored for this action, and marks remain in place after the editor exits.

Sase Agent Neighbors Section

Every sase agent panel carries a numbered NEIGHBORS roster in the jump panel. The section appears on session container panels after their SESSION TURNS roster when both exist, and on ordinary agent panels. Tribe panel summaries, session member child rows, and workflow aggregate rows have no NEIGHBORS section. Clan containers carry a distinct CLAN NEIGHBORS roster instead (see below). A selected session turn row owns no sase agent, so its panel carries only the SESSION TURNS roster (siblings, minus itself) and never a NEIGHBORS section.

A selected clan container shows CLAN NEIGHBORS after its CLAN MEMBERS roster when another clan shares its dotted hood. Two clans are neighbors when their presented clan names share the same root hood: foo, foo.bar, foo.baz, and foo.bar.deep all share the foo hood. Names compare case-insensitively at dot boundaries; the selected clan itself, ordinary agent rows, malformed names, and unrelated prefixes such as foobar are excluded. Each target keeps its full stable clan identity including its generation, and rows render under their full clan names. The section appears only when another eligible clan is visible under the current query and fold state. Numbering draws from one continuous ladder shared with CLAN MEMBERS, so eleven combined entries use two digits and entries beyond the shared 100 slots appear as an unnumbered … +N more clan neighbors (not numbered) count. A digit jump reveals the target clan through folds and tribe panels and participates in Ctrl+O jump-back history; a stale relation or missing target cancels without moving selection.

The rows for that sase agent are ancestors, descendants including same-session dismissed descendants, then hood neighbors grouped by hood, nearest hood first — under dim ancestors, descendants, and <hood> hood group labels. A sase agent joins the hood that matches its own name, and a session uses its bare session name for that match, so a session visual.worker and a single agent visual.worker.notes relate as ancestor and descendant exactly as two single agents with those names would. Row labels are shortened relative to their group, so a myclan hood neighbor reads .code and a descendant reads --impl.helper. A ⊘ glyph and a dismissed annotation mark dismissed rows, and folded marks a prospective row that currently lives inside a collapsed clan. In the jump panel the section sits after SESSION TURNS when both exist, so a sase agent's numbered neighbors stay reachable without scrolling the Main deck body.

Every neighbor always renders and gets a digit whatever the fold level. The fold level only changes the heading glyph and each row's annotation detail, both visible once the jump panel is expanded with .. Numbering is stable across fold levels and JUMP-panel toggles: the collapsed legend, the expanded roster, and the published jump map all share one continuous ladder. The heading count is always the sase agent's total neighbor count. The only hidden-row tail is the shared 100-slot numbering capacity, which reports … +N more neighbors (not numbered). On a session, siblings that already appear under SESSION TURNS are not repeated; they are reported by a dim … +N also listed under SESSION TURNS tail instead. The heading count still includes the suppressed rows.

Opened Repository Context

Configured linked_repos are recorded in agent metadata at launch time, while linked and external repos opened during a run are recorded in opened-repository markers. For non-terminal agents, sase's TUI can include dirty opened repos in the agent detail SASE CONTEXT ARTIFACTS lane under Deltas. The field counts primary and opened-repo changes together, groups linked and external entries under distinct glyphs and canonical repo names, and resolves file hints relative to the opened repo directory. Missing workspace directories, clean repos, and completed/failed agents are not part of this live delta display.

When a SASE-launched agent uses /sase_repo, the run records an opened-repository marker. The underlying command infers the host project and workspace from cwd; configured linked repos remain backed by hidden PROJECT_STATE: sibling project records, while external repos remain workspace-local and create no project record. sase's TUI shows the markers in the prompt/detail SASE CONTEXT section with the repo name, kind, resolved path, open time, and reason. Live deltas, commit diffs, and revert all retain the canonical external name (for example, gh:pallets/click); reverting an external repo discards local clone changes without re-cloning from the network.

Wait Modal

Press w on the Agents tab to open the WaitModal. It has five editable fields — Agents, Beads, Time, Capacity, and Priority — each prefilled from the agent's current wait, plus a focusable Follow epics toggle row directly under Agents. Time, Capacity, Priority, and Beads render a live preview of how the typed value will be interpreted; an invalid Time, Capacity, or Priority value blocks apply and focuses the offending field.

The Follow epics row is a tri-state toggle: on (teal ↪), off (dim), or mixed (each listed target keeps its own policy; newly added agents get the default). It joins the Ctrl+J / Ctrl+K field cycle and Space toggles it when focused. When every listed target is a --plan row the toggle is disabled with a reason, because --plan rows release when the plan is submitted and never follow epics. Beads prefills with authored beads only — beads a follow promotion derived are excluded. Applying the modal writes the follow selection into the wait directive; removing a target, or turning its follow off, drops its follow stage and its derived bead IDs while keeping the already-pinned epic beads. Turning follow back on lets the next evaluation re-promote.

Beads completes against every non-closed bead in the agent's project, read from the same canonical store the wait resolver consults, so a bead offered by the picker is always one the resolver can see. The agent's own epic/phase bead is excluded from candidates — an agent can never wait on the bead it exists to close. Candidates are ordered by status (in_progress, claimed, ready, open, snoozed), then by most-recently-updated, then by ID; the filter fragment matches bead ID or title. At most 100 rows render per keystroke, with a trailing …N more — keep typing row when more match. A bead already present in the field renders with a dim · selected suffix.

The Agents and Beads fields each have their own completion list directly beneath them, but only one is ever visible at a time — whichever of the two fields was focused most recently (Agents by default). Focusing Time, Capacity, or Priority never changes which list is shown, and the hidden list is not part of keyboard focus traversal.

The beads preview reports one of: an empty-field neutral message, a loading message while the bead catalog loads in the background, a neutral "bead store unavailable" state when the project's store can't be read (IDs are not verified in that case), a valid summary of up to three typed beads with their status glyphs plus an aggregate count, or an error when a typed ID isn't in the project's bead store or is the agent's own bead. Applying an error-state bead wait is a soft, two-step guard rather than a hard block: the first Enter focuses the Beads field and changes the footer instead of dismissing; a second Enter applies the wait as typed. Any edit to the Beads field disarms the guard. A store that can't be read never arms the guard, since bead stores sync through git and a locally-missing ID can still be valid upstream.

Behavior depends on the agent's status:

  • WAITING or QUEUED agent: Edit dependency names, bead gates, a time floor, the per-launch capacity budget, or the runner-slot priority. A runner-slot-parked agent applies a capacity- or priority-only edit live on its next poll; changing earlier wait stages restarts the agent. Clearing an explicit capacity budget returns that launch to the current global max_running_agents budget.
  • STARTING or RUNNING agent: Enter a dependency, bead gate, time floor, capacity budget, or priority to kill and restart the current agent with canonical %wait(...) / %queue(...) directives.

The Capacity field is this launch's capacity budget, replacing the current global max_running_agents budget for its own admission decision. It is not a count of individual turns. A serial session — including its monitor and --next follow-up — still occupies one claim of its session weight, so that weight still counts against this budget. Only a root or a live parallel clan member waits here; a serial session member rides the session's slot and never parks.

Priority must be a non-negative integer and defaults to 10; lower values are admitted first. See Runner slot waits for how priority interacts with FIFO order and the bounded deference window applied to deprioritized waiters.

Enter applies, Tab accepts a highlighted agent- or bead-name completion and otherwise moves focus to the next field, Ctrl+R runs the agent now by clearing every wait condition, and Escape cancels. The modal supports readline-style keybindings (Ctrl+F/Ctrl+B/Ctrl+A/Ctrl+E) for cursor movement.

Ctrl+J and Ctrl+K walk forward and backward through the six fields directly, in the displayed order (Agents, Follow epics, Beads, Time, Capacity, Priority), wrapping around at either end and placing the cursor at the end of the field's current value. Unlike Tab, they never consume a highlighted completion, so they move focus even while a completion list is open; when focus is on the Agents or Beads completion list itself, the step is taken from that list's own field. Up / Down and Ctrl+P / Ctrl+N move within the visible completion list rather than between fields.

VCS Tag Resolution in Fork/Wait

When forking or waiting on an agent, VCS tags in the prompt (e.g., #git(ref), #gh:ref) are automatically updated to point to the correct branch. For non-project agents, the ref is replaced with the agent's PR name (branch). For project agents using #pr, the ref is replaced with @<name> which resolves to the agent's branch. HITL suffixes (!!, ??) are stripped during replacement since fork scenarios should not carry over HITL overrides.

Workflow Visibility

Workflows launched via sase run are visible in the Agents tab alongside sase's TUI-launched workflows. The TUI scans artifacts/run/* directories in addition to workflow-* and ace-run directories, and writes an initial workflow_state.json before execution so that step data appears immediately rather than showing a bare RUNNING entry. Anonymous tmp_* workflows are included in the normal visible-inbox index when their workflow state has appears_as_agent: true and does not set hidden: true; explicitly hidden workflow rows are omitted from the default view. Specialized review runners launched by axe (mentor, CRS, fix-hook, and summarize-hook review agents) are also visible and are automatically grouped into tribe @review, matching the behavior of a %id(..., tribe=review) prompt launch.

Agent Artifacts

Press a on a focused agent to open the artifact panel whenever artifacts are associated with that agent. The list can include chat transcripts, plan files, generated Markdown PDFs, generated images, generated videos, prompt-referenced media from saved prompt artifacts, and explicit files saved with sase artifact create -p <path> [-l <label>] [-k <kind>]. sase's TUI always opens the panel, even for a single artifact, so the label, kind, and path are visible before launching the terminal viewer. With agents marked, a opens one combined panel listing every marked agent's artifacts, each row labeled with its agent's name.

The prompt/detail header includes those non-chat entries in the plan-adjacent SASE CONTEXT ARTIFACTS lane. Within that lane, Beads, Reads, Commits, Deltas, and Files stay in that order when present. Beads lists the beads the agent touched, read, or viewed, plus its assigned beads (see the Main deck description below); audited bead: reads appear there rather than under Reads. Reads lists audited sase artifact read invocations (newest first, with reasons); prompt citations and silent show / path / open inspection do not appear. Paths are made workspace-relative when possible, and hint mode assigns numbers to filesystem-backed reads and output paths so they can be opened with the normal file-hint flow.

Artifact panel controls:

Key Action
selector Open the artifact with that one-key selector (1-0, then letters)
j / k Move through artifact rows
m Mark / unmark the highlighted artifact and advance to the next row
% Open the file-kind Copy as… palette
y Copy Markdown contents (an accelerator for the palette's c row)
Y Copy the preferred anchored stored/source path
Enter Open marked artifacts in list order, or the highlighted row if unmarked
A Open all artifacts in list order, ignoring marks
z Open marked artifacts (or the highlighted row) in a zoomed tmux pane; outside tmux it opens them normally with a warning
q / Esc Close the panel

The modal-local file palette offers @ prompt-form references, l Markdown links, c Markdown contents, p stored paths, P source paths, J metadata JSON, and s snapshots. Stored and source paths are separate, anchored answers; an absent source is labeled “not recorded,” and copying a stale source keeps the “no longer exists” warning. With marks, the palette copies rows in visible order: references and paths are newline-separated, links form a Markdown list, metadata is a JSON array, and Markdown contents use bounded fenced sections. Unavailable rows are skipped with an explicit count. These modal-local l/J accelerators are distinct from the Artifacts Files pane's compatibility-preserving %L/%j keys. The legacy y and Y accelerators remain available and apply to the same marked set.

Y shares one helper with the Files pane, so both copy the same anchored path: the stored path, except that PDF rows yield the live Markdown source they were rendered from when the index recorded one. Relative index paths are anchored to the producing workspace — including legacy rows whose workspace is discoverable only through the agent's artifact metadata — and the completion toast says when the copied path no longer exists.

When sase's TUI is running inside tmux, artifact viewing opens in a right-side tmux pane so the TUI remains visible. The Agents list collapses while the tracked pane is live, row-changing navigation shows a warning instead of moving to a different agent, l focuses the tracked pane, and lowercase a closes it. If the pane was already closed, lowercase a opens the artifact panel normally. Outside tmux, sase's TUI suspends while the terminal viewer runs in the current pane. The viewer supports image, video, Markdown, PDF, and text artifacts: images are displayed directly with kitten icat, videos play with mpv, Markdown is first rendered to PDF, PDFs are converted to PNG pages for paging, and unknown file artifacts fall back to a text viewer. The viewer needs kitten for image/PDF/Markdown display, mpv for videos, pdftoppm for PDF/Markdown paging, and pandoc plus a supported PDF engine for Markdown rendering. Missing tools produce a warning instead of failing the TUI.

Viewer controls:

Key Action
j Next page when the artifact has multiple pages; wraps around
k Previous page when the artifact has multiple pages
n Next artifact when viewing an artifact sequence
p Previous artifact when viewing an artifact sequence
r Refresh the current page
z Toggle tmux zoom when available for the viewer pane
Tab Focus the SASE TUI from a tmux artifact pane
q Close the viewer

Only one plan artifact is shown for an agent. When both an archived plan and an SDD tale path are present, sase's TUI prefers the committed SDD plan; otherwise it keeps the path that best matches the run metadata.

During successful-agent finalization, Markdown-to-PDF rendering updates workflow_state.json.pdf_status and a compact activity label. sase's TUI renders that label only in the prompt/detail header's labeled Activity: field, so long conversions show progress such as PDF 2/4 <path> or PDFs done 3/4 (1 skipped) instead of looking idle.

Tribe Side Panels

The Agents tab is laid out as a series of vertically-stacked side panels, one per effective agent tribe. Agents without a stored tribe live in the reserved @default panel; an explicit default assignment converges on the same panel, so the UI never creates a duplicate default bucket. @default is derived for presentation and is not backfilled into agent_meta.json or agent_tribes.json; clearing a user-managed tribe returns the agent to this panel. Pressing N on a clan row or on any clan member instead sets the whole clan generation's recorded tribe, and clearing it writes an explicit unset that sticks even when members still carry an epic tribe (see clan records). Every tribe renders as @<tribe> with a sase-agent count in the panel title. One standalone agent or one sequential session is one sase agent, and a rootless clan contributes one sase agent per direct member rather than one for its synthetic container. Per-tribe icons, identity colors, and initial expansion are configurable through ace.tribes; the special default entry styles the reserved panel. Agents launched by AXE jobs land in the built-in @job panel. SASE still stores that tribe under its legacy name chop, but @job wait and fork targets, tribe:job filters, and an ace.tribes.job entry all address it (an explicit ace.tribes.chop entry still wins for styling). A manual panel fold lasts for that panel's current lifetime, and the configured initial state is applied again when the panel appears after a restart or after the tribe disappears and returns. Once a panel has shown rows under the current Agents query, it stays mounted for the session even if a bounded or incomplete load briefly omits them; it retires only when every node it showed is authoritatively gone — dismissed, killed, a dismissed named proc, moved to another tribe, or absent from a complete history load — including a clan whose members are all gone. Changing the Agents query starts that bookkeeping over. Across structured sase's TUI surfaces, identity colors apply only to an existing configured icon and the @tribe name; they do not recolor free-form @... text or selection, fold, count, heading, and status chrome. Configured icons remain limited to surfaces that already show an icon. Each panel title can also show compact scoped metrics in the form [S1 R2 W1 F1 U1 D3]: S is stopped for human input, R is running, W is waiting to start, F is failed, U is unread terminal work, and D is done/read terminal work. Zero-count metrics are omitted. The status metrics use the same sase-agent projection as the adjacent total and classify a sequential session once from its normalized owner status. The selected whole-panel TRIBE header uses that same projection, while its nested count and per-session/per-clan member summaries preserve the concrete-member distinction. On the selected whole panel, the title marker, total, brackets, and metric letters use the focus accent; each numeric metric count retains its semantic status color. The title can end with an amber ⚙N badge for running monitors followed by a grey ⚙N badge for finished ones, in that order, after the metric chip (or after the total when the chip is empty); the two counts partition the tribe's monitors exactly. Each badge is fold- and collapse-independent — it still reports on a fully collapsed panel — and is omitted entirely when its own count is zero. Both badges keep their semantic hue on a selected panel while the brackets and metric letters take the focus accent. Panel heights are sized to their content and separated by a one-row gap. When the panels fit, the first panel grows to absorb leftover vertical space while later panels stay pinned to their natural height; when the panels overflow, space is weighted by each panel's rendered row count.

A selected tribe panel's TRIBE identity fields (Name, Status, Composition, Runtime, Fold) render in the sticky header panel above the Main deck; collapsed, it shows the tribe name, status, and counts on one row and the composition, runtime, and fold on the next. The scrolling Main deck opens with an unlabeled description row only when the tribe has a configured description. That row is set off by a blank line and wrapped at a fixed 80-cell measure (no hanging indent — there is no label to indent past). A tribe with no configured description renders no row there at all, including unconfigured ad-hoc tribes with no ace.tribes entry. To find configured tribes that are missing a description, run sase doctor -C config.tribes.

Use J / K to move across expanded panels (forward / reverse) and enter the first or last selectable row in the destination; collapsed panels are skipped entirely, and the keymaps do nothing when no other panel is expanded. Collapsed grouping banners count as rows. When the focused panel's only selectable row is already selected — or it renders none — lowercase j / k select the adjacent whole panel, wrapping across every panel including collapsed ones; l or Esc then descends into the newly selected panel's remembered row. This differs from J / K, which skip collapsed panels and land directly on a row. Whole-panel focus is available only in the split layout. Lowercase h walks from any agent or workflow-step row to its validated immediate workflow, session, clan, and finally tribe parent without changing structural or grouping folds. It also selects a lone split panel after the structural chain is exhausted. A selected panel has a ❖ title and shows a fold-aware TRIBE summary in the Main deck. While it is selected, j / k cycle whole panels without descending; l or Esc returns to the remembered row. A second h collapses the selected panel when another panel remains visible. On a collapsed panel, the first l expands it while keeping whole-panel focus and the second returns to the remembered row; L on a collapsed panel does not expand it — it warns Selected tribe panel is collapsed instead (see uppercase H/L below). Lowercase h on a collapsed panel selects the visually bottom-most expanded panel without changing any panel folds, and Ctrl+O returns to the collapsed origin. When every live panel is collapsed, h remains a no-op and shows the existing Panel is already collapsed warning. Apostrophe jump hints include every split-panel title, even a lone expanded panel, as well as collapsed titles, and support the normal Ctrl+O jump back.

Selection is the source of truth across refreshes: when a status change or roster reload moves the selected agent into another panel, panel focus moves with it. Focus stays put only when you parked it deliberately — whole-panel focus, a selected grouping banner, or a collapsed focused panel — or when the agent's new panel is collapsed, in which case the cursor returns to the focused panel instead. If the selected agent disappears, the cursor lands on the neighboring visible row.

Lowercase l only advances a real fold owned by the selected row or its immediate workflow/session owner, so a visible hidden leaf under an already fully expanded workflow is a no-op. Uppercase H is the structural mutation key. When the selected row owns an open workflow or sequential-session agent node, the first press retreats that agent node one fold level. From a visible hidden step that hides the selected row, selection re-anchors to the agent-node owner. After the selected agent node is collapsed, later presses fully collapse every remaining open agent node in the next grouping scope, then only the open canonical clan enclosing the selected row. With that now-collapsed clan container still selected, another press collapses every remaining open canonical clan in the group; only a later press collapses the grouping banner. A banner, already-collapsed agent node, or already-collapsed clan selection proceeds directly to that remaining-agent-node or group-wide clan sweep. LLM Calls detail still takes priority. On a selected expanded whole panel, H hints every currently expanded agent node, clan, and top-level grouping banner in that panel — the same L hint affordance restricted to collapsible targets — and fully collapses whichever one you pick; it never expands and never touches the panel itself, which stays lowercase h's job. A panel with nothing expanded warns without arming hint mode; an already collapsed panel keeps the usual already-collapsed notification. The merged layout has no whole-panel focus and keeps the row-focused group scope across the merged roster.

Press Z with a whole tribe panel selected to zoom that tribe's Summary card in place. Press = to isolate the focused tribe panel: it keeps that panel expanded and collapses every sibling panel. If that changes the layout, sase's TUI remembers the prior collapsed-panel set for one session-local restore. Panels whose state would change back show ↺ in their titles, the footer changes to = restore panels, and the next = restores the remembered layout. A separate sibling-panel or layout mutation invalidates the pending restore. An already isolated panel is an idempotent no-op and does not arm a restore. = works from whole-panel focus and from a row selection inside a panel alike — from a row, it isolates the panel that holds the cursor without changing the selected row. This action preserves the selected panel's remembered row and is available only in the split layout.

Press - to sweep every open structural fold — agent nodes and clans, never grouping banners such as Done or Running — closed in the tribe panel that holds focus, in one press. It resolves scope the same way = does: from whole-panel focus, from a row or banner selection inside a panel, and in the merged layout, where it treats the merged roster as one scope. - never collapses the panel itself; that stays lowercase h's job, and it never touches an open grouping banner either — use H for that. When the focused panel has nothing left to collapse, - reverses itself: it re-expands exactly the folds its own last sweep in that panel closed, restoring each structural fold to the level it held before (a fully expanded agent node comes back fully expanded, not merely expanded). The restore is filtered at press time to folds that are still live in that panel and still collapsed, so it is forgiving of folds the user re-expanded by hand or owners that disappeared, and it never resurrects a fold that no longer exists. Each panel remembers at most one sweep; a fresh sweep replaces that panel's record, and a panel that stops being live drops it. While a restore is armed, the panel marks every fold - would re-expand with a gold ▿ on the owner row and ▿N in the panel title; those markers clear as soon as the next - press would sweep instead of restore. The footer shows - collapse folds when the focused panel has an open agent-node or clan fold to sweep, or - restore folds when nothing is left to collapse but a prior sweep's reverse is still armed. A panel with only open grouping banners and no open agent node or clan reports nothing to collapse or restore.

Press _ for the all-panels sibling of -: in one press it sweeps every open agent-node and clan fold in every tribe panel, not just the focused one, and shares -'s per-panel sweep records, so the two compose — sweeping one panel with - and the rest with _ still restores everything in the reverse order. A panel that is effectively collapsed is skipped in both directions, exactly like - refuses to sweep a collapsed panel, though its record stays live and reachable once it is expanded again. _ works identically from any selection — a row, a group banner, whole-panel focus on an expanded panel, or whole-panel focus on a collapsed panel — sweeping or restoring the other panels either way. When nothing is left to collapse anywhere, it re-expands every fold every panel's last sweep closed, restoring each to its exact prior level. The footer shows _ collapse all folds / _ restore all folds, but only once at least two tribe panels are eligible; with a single panel it would be a redundant duplicate of -.

Per-panel actions (kill, dismiss, expand, etc.) operate on whichever panel currently holds focus. Press X to open the cleanup panel: d dismisses completed agents in the focused panel, D dismisses completed agents across loaded panels, k cleans the focused panel, K cleans all loaded panels, m cleans marked agents, g cleans the focused group, t opens the tribe chooser, and c opens the custom selector. Enter runs the highlighted row, and Esc or q closes the panel. Each row is disabled when the loaded roster contributes no candidates. Its detail line is a candidate count, not a promise that every candidate can complete that action; the confirmation or planner may narrow the final set.

There is no separate clan chooser. A synthetic clan container row is never a cleanup target itself: every scope above expands it into the live members of its generation, so members folded away behind a collapsed clan are still reached by a panel, tribe, group, or marked cleanup. Loaded workflow children are pulled in when their parent is a candidate, and identities are de-duplicated in Agents-tab order. Named-proc handling depends on the chosen scope: panel/global and marked/group bulk actions can dismiss a terminal named proc but skip an active one with a warning, while tribe-planner and custom-selector cleanup omit named procs. To stop an active stand-alone named proc, select its row in the Agents tab and press x. Every scope then continues through its normal bulk-cleanup confirmation or planner flow.

The TRIBE summary has four metadata detail levels, controlled by the same zz, zZ, za, and zA chords used for clan and session detail. From levels 1-3, zZ opens every fold to level 4; at level 4, it closes every fold to level 1:

Level Name Tribe summary content
1 Glance Header, compact numbered top-level roster, attention previews, clan summary headline index (up to 8 clans), prompt headline digest (up to 8 distinct prompts), and headings/counts for non-empty sections
2 Triage Bounded previews for every represented section; every clan summary (up to 24) with styled ledes, and every distinct prompt (up to 24) with labels, macro chips, and sizes
3 Inspect Nested roster detail and grouped full section bodies, still with protective bounds; 16-line clan summary previews and 10-line prompt previews
4 Forensics Unbounded bodies, tracebacks, the richest member annotations, full clan summary bodies (500-line safety cap per clan), full prompt bodies (500-line safety cap per prompt) and launch directives, and all-time runtime statistics and percentiles

The compact roster and its fixed numeric jump targets exist at all four levels. Number keys jump to a top-level clan, session, workflow, or agent, expanding the required panel and ancestor folds first. These metadata-member numbers are separate from ordinary apostrophe entry hints, whose adaptive target keys may use two characters in a large list.

PROMPTS sits after CLAN SUMMARIES in the Main deck body (the TRIBE MEMBERS roster itself lives in the jump panel): it maps what each agent in the tribe was asked to do. Its number chips are the same digits as the roster jump targets. Identical prompt bodies are listed once with a ×N badge and a shared-by list. za/zA on a prompt entry opens just that prompt. It is the level-1 exception: unlike most sections, it shows prompt headlines at Glance instead of only a heading. Entries also carry green macro chips and, for multi-line prompts, a dim line count, separated by dim · dividers. When the tribe's prompts target more than one known project, each entry ends with that project's accent-colored +<project> chip (or its #<workflow>:<name> spelling when the name is not tag-shaped); unknown targets and Patch refs get no chip.

CLAN SUMMARIES sits before PROMPTS in the Main deck body (the TRIBE MEMBERS roster itself lives in the jump panel): it maps the curated summary of every clan in the tribe, because clan intent reads before raw prompts. Each entry line carries the clan's roster digit as a number chip, the clan label, a banner-kind kicker (EPIC, …) when present, a headline truncated at 120 characters, and a line count for multi-line summaries. za/zA on an entry opens just that clan's summary. Like PROMPTS, it is a level-1 exception: Glance shows the headline index, Triage adds a styled lede of up to four lines, Inspect shows 16-line previews, and Forensics shows full bodies behind a 500-line per-clan safety cap.

Reply, slow-call, prompt, and clan-summary presence enrichment is requested off-thread at every tribe level so known-empty sections can remain absent. Full bodies still follow the level-specific bounds above, and all-time runtime statistics remain level-4-only. When required disk-backed content is not known yet, the document ends with one dim ⋯ scanning member data… tail; known-empty content produces no section or placeholder. Section-level overrides inherit from the panel level and are cleared by a valid panel-level cycle, zZ extreme toggle, or direct z1-z4 selection.

Tribes are set or cleared with N (see Agent Actions). When opening the modal on an agent without a tribe the input is pre-seeded with pinned so a single Enter promotes the agent into the standard "pinned" panel; that default makes tribe removal discoverable too — opening the modal on an assigned agent and submitting an empty string clears the tribe. The tribe=<name> keyword on %id assigns the tribe at launch, and #tribe:<name> combines it with an automatic id; sase agent tribe manages it from the CLI.

Group Banners and Folding

In STANDARD mode, agents within each tribe side panel use either a two-tier or three-tier banner hierarchy depending on whether any agent in the panel targets a Patch:

  • 3-level layout (panel contains at least one Patch-scoped agent): project → Patch → name-root. Project-scoped agents and agents with no cl_name fall into a synthetic (no Patch) bucket that sorts last.
  • 2-level layout (no Patch anywhere in the panel): project → name-root.

Banners are rendered between agent rows and carry a summary chip (N agents · K running · M failed). Workflow children inherit grouping identity from their parent agent so banners never appear between a parent and its workflow steps. Optional name-root and dotted-prefix banners appear only when they group at least two rows.

Labels such as L0, L1, and L2 describe a banner's nesting depth, not a shared fold setting. Every emitted grouping banner has its own binary expanded/collapsed state, kept separately for each tribe panel and grouping mode. Three independent folding layers can therefore be visible at once:

Layer What it controls Default keys
Grouping banner Project, Patch, date, status, and name buckets Repeated H collapses after scoped agent nodes/clans; l expands; - never sweeps banners
Structural row Clan members, session members, and workflow descendants H retreats a selected workflow/session one level, then remaining group agent nodes, then group clans; l expands; - sweeps every open agent node and clan at once
Split-panel title A whole tribe panel; collapsing requires multiple panels h or ' selects; h collapses; l expands; L hints an agent-node/clan/banner fold to toggle in the focused expanded panel
Key Action
l Expand the selected collapsed grouping banner or structural row; on whole-panel focus, expand or enter the panel
h Navigate outward; collapse selected expanded panel; from collapsed panel, select the last expanded panel if one exists
L From a row or the selected panel, hint every visible agent-node/clan/banner fold in the focused tribe to toggle; on a collapsed panel, show the already-collapsed warning
H Collapse selected workflow/session one level, then remaining group agent nodes/clans/group, or hint a fold to collapse in the selected panel; compact expanded LLM Calls detail
= Isolate the focused tribe panel, or restore the pre-isolation layout; works from whole-panel focus or a row selection
- Sweep every open agent node and clan in the focused panel closed in one press, or restore the last sweep; never touches grouping banners or the panel itself
_ Like -, but across every eligible tribe panel at once
,H Collapse one fold by hint: the selected tribe's expanded folds from a row; every tribe's expanded folds and panel titles from a selected panel

Collapsed grouping banners at any depth are selectable rows; expanded banners remain visible headings but are skipped by row navigation. When a collapsed banner is focused, l expands only that banner and moves focus to the next visible child banner or first agent row. When a banner is focused, m toggles marks for all top-level agents in that group; workflow child rows are not marked independently by the banner shortcut. x performs a bulk kill/dismiss on every top-level agent in that group (single confirmation modal). Marked collapsed banners show [✓] when all covered top-level agents are marked and [~] when only some are marked. Marks take priority over the group for bulk actions, so a non-empty mark set always drives the bulk action regardless of banner focus. When a fold change hides the previously focused agent, focus snaps to the nearest visible ancestor banner so navigation context is never lost.

Clan and session rows add an agent-tree hierarchy inside those grouping banners. Their trailing names are color-coded by kind without an additional icon. A clan is a selectable synthetic container, never an agent, and ends in an orchid <name> after its rolled-up status and member counts. A real multi-member session root remains a teal agent row and ends in an azure <name>; ordinary agent annotations and lone plan proposers with only their display-only planner child remain gold. Clan @tribe labels follow the orchid name. A clan row starts with the teal project label of its direct members in the title slot, for example bob-cli (RUNNING) [R5 W3] research.35. A clan spanning several projects shows the dominant project first, at most two labels, then +N. The CLAN header repeats the label as a Project:/Projects: field and at the start of the compact second line. A clan's outer fold is binary: from a collapsed clan row, press l once to reveal its direct agents, session rows, and visible workflow rows. The clan row's fold count and status chrome count those direct clan agent nodes once; nested session or workflow members do not inflate them. To reveal descendants within a session or workflow, move to that row and press l there; pressing l again on the clan row itself has no effect. Lowercase h moves to the validated parent without changing fold state. Sequential session members use --<suffix> names and run one after another. Killing or dismissing a clan row cascades to the clan's live members; acting on one member leaves its siblings alone. Direct clan members always sort by the clan-local status priority Failed, Stopped, Running, Queued, Waiting, Done in every grouping mode; Starting shares Running's rank. Launch recency orders only members in the same status bucket. A session row moves as one unit with its follow-ups and workflow steps, preserving their adjacency and internal order.

Clan rows aggregate member status using the same operational precedence: human-input questions, pending plan review, failure, and running/starting states outrank queued work; QUEUED then outranks WAITING, followed by an all-done result. Consequently, a clan with queued work and ordinary waiters displays QUEUED unless a higher-priority member state is present. When a clan has exactly one running member (STARTING included) and no asking, reviewing, or failed member, the clan shows that member's exact status: its label and styling, including the FINALIZING word and the RETRYING (Ns) countdown, on both the clan row and the CLAN Status: line. For example, a session running a TESTING monitor makes the clan read TESTING, while a failed monitor with an authored stop label can read TESTED [W1 F1 D9] and still remain in the Failed group. The status bucket, precedence, and count chip are unchanged; TESTED is only the authored turn label, not proof that verification passed. When the aggregate is QUEUED and exactly one direct member is queued, the clan row and CLAN Status: line also show that member's admission rank (QUEUED #3/4), plus the same pN / held by extras the member row shows. Two queued members stay generic QUEUED with no rank. The count chip remains concrete and independent, so QUEUED #3/4 [Q1 D4] keeps the rank next to the chip, and QUEUED [Q3 W6] reports three runner-slot waiters and six dependency, bead, or time waiters without merging the two categories.

A clan row whose waiting members name unknown targets attaches an orange ?N immediately after the waiting count inside its count chip, as in QUEUED [Q1 W3?2]. N counts distinct unknown agents, clan members, and beads across the clan's direct WAITING members, so a dependency shared by two members counts once. Expand the clan to see which member's WAITING ?N token names the stale target.

The uppercase H ladder starts with the selected workflow or sequential-session agent node when that agent node is still open. The first press retreats that agent node by exactly one fold level. From a visible hidden step, that press hides hidden steps while leaving ordinary descendants visible and re-anchors selection to the agent-node owner; the next press then collapses that still-selected agent node. Clans stay binary and do not take this two-level path. After the selected agent node is collapsed, later presses continue through the existing group-scoped remaining-agent-node, selected-clan, remaining-clans, structural-fallback, and grouping-banner ladder. If the grouping banner that H would collapse next contains any open standalone workflow, agent, or sequential-session sase agent, and the selection does not own an open workflow or session fold, the next press drives every such remaining agent node directly to fully collapsed while leaving the banner open. Once remaining agent nodes are saturated, a selection inside an open canonical clan makes the next press collapse only that clan. A selected descendant re-anchors to its visible clan container; selecting the container itself preserves selection without writing new selection memory. With the collapsed container still selected, the following press drives every remaining open canonical clan in the group directly to collapsed. A grouping banner, already-collapsed agent node, already-collapsed clan, or invalid clan owner falls through to that group-wide sweep immediately. The footer advertises H collapse workflow or H collapse session while the selected agent node is open, then H collapse sase agents, then H collapse clan, then H collapse clans, and only then H collapse group. Equal group names in other tribe panels are never affected; merged layout intentionally treats the merged panel as one scope. Ambiguous or malformed clan owners are skipped without blocking valid siblings.

Whole-panel focus gives H a hinted collapse instead of the group-scoped ladder, because it has no selected row or grouping scope to walk. It enumerates every currently visible expanded agent node, clan, and top-level grouping banner in the selected panel — never an owner hidden behind a still-collapsed parent banner, since that owner isn't emitted as a row at all until its parent is expanded — assigns each one an adaptive hint key, and shows the chips in place of jump hints. Typing a hint fully collapses that one fold; an already collapsed fold is never offered, so every hint does something. H never expands and never collapses the panel itself, which stays lowercase h's job. A panel with no expanded folds warns without arming hint mode; an already collapsed panel keeps the existing Panel is already collapsed warning. The footer shows the configured hooks_or_collapse_all key as collapse fold whenever the selected panel has an expanded agent node, clan, or top-level banner to hint. L's hint mode uses the same enumeration but is not restricted to collapsible targets, so it also offers currently collapsed agent nodes, clans, and banners and toggles whichever one you pick.

The ,H leader chord is always one scope wider than the selection. From a row or banner it opens the same collapse-only hints as whole-panel H for the focused tribe. From a selected tribe panel — expanded or collapsed — it hints every expanded fold in every expanded tribe panel and adds a title chip on each of those panels, so one keystroke can collapse a different panel without navigating to it. Whole-panel focus stays on the panel you started from, so ,, repeats the picker against the current selection. The footer reads COLLAPSE · ALL TRIBES in that wider scope. ,H never expands and never routes through LLM Calls compaction.

Visual treatment: every row carries a fixed-width tier-guide gutter built from one │ segment per ancestor L0/L1 banner (in the parent tier's dim accent — project blue or Patch cooler accent), so nesting reads as a tree at a glance. L0 project / bucket banners use a sky-blue ▌ left bar and a heavy ━ rule. Patch banners, BY_DATE subgroups, and banners that own another dotted-prefix subgroup use a cooler ▎ bar and lighter ─ rule. Leaf name-root and dotted-prefix banners use a ▸ branch glyph with a teal label. Singleton name-root groups suppress their banner entirely to reduce visual noise.

The currently-focused side-panel row is marked with a thick accent-colored left bar, bold text, and a translucent accent tint applied to the row background. The tint is intentionally light so per-token status colors (running cyan, failed red, waiting yellow, etc.) remain readable through the highlight — the bar and bold weight do most of the work of marking the selection.

After a kill or dismiss, focus re-anchors on the visually-next row (rather than the next row in input order) so the selection always lands somewhere meaningful in the rendered tree.

Grouping Modes

Press o on the Agents tab to open the grouping picker, then choose p Project, d Date, s Status, or m Machine. The picker's layout row is a three-level ladder — Split by tribe, Merged, All tabs — walked with o (next) and O (previous, wrapping); h/l and the arrow keys move the highlight while the row is focused, and the picker closes after a step. oo steps forward one rung, oO steps back. With fewer than two agent tabs only Split by tribe / Merged are offered. Split shows one panel per tribe titled @tribe · N; Merged shows one panel titled with the tab's label and count (All agents while the tab strip is hidden); All tabs shows every tab in one panel titled All agents · every tab · N, with a tab chip on named-tab rows and panels: all tabs in the info row. Merged panels annotate named tribes inline on agent nodes; the default tribe is implied there, so default nodes do not add @default labels. Transitions keep the selected node: zooming in from All tabs lands on that node's tab, and choosing a tab at the All-tabs level drills into it. Panel layout and grouping mode are independent: stepping the ladder preserves the active grouping mode and does not persist beyond the current session. The Agents tab shows a brief toast (Grouping: by project / by date / by status / by machine) when the chosen mode changes:

Mode L0 buckets Notes
STANDARD Project (with optional Patch sub-level) The "by project" default. Uses the 2-/3-level layout described above.
BY_DATE Today / Yesterday / This Week / Earlier Date bucket at L0, then a date-aware L1 subgroup. Sorted newest-first within each bucket.
BY_STATUS Stopped / Failed / Running / Queued / Waiting / Done / Starting Bucketed by shared status semantics; status priority fixes bucket position. Standalone agent nodes precede name subgroups, with launch recency sorting units inside each partition.
BY_MACHINE here, then remote aliases alphabetically Machine at L0, status at L1, then name-root/name-prefix groups. Project and Patch levels are omitted.

In BY_DATE mode, sase's TUI chooses one L1 subgroup style from the L0 date bucket: one-hour windows (09:00) for Today and Yesterday, calendar-day labels for This Week, and Monday-start week ranges for Earlier. The time anchor is stop_time for terminal agents and start_time otherwise. The same anchor selects the L0 date bucket, so an agent that started Friday evening and finished Saturday morning renders under Saturday's bucket, matching the finish timestamp on its row. Buckets and their subgroups sort newest-first. Workflow children inherit the parent's anchor so they stay adjacent regardless of their own start time, and agents with no usable timestamp fall into a (no time) subgroup that sorts last.

In BY_STATUS mode the L0 banner is the status bucket and L1 is the name-root, with the same singleton-suppression rule as STANDARD. Status priority fixes the bucket order: Stopped, Failed, Running, Queued, Waiting, Done, Starting. Within each bucket, standalone sase agents render before every visible name-root subgroup; start_time sorts agent nodes newest-first inside the standalone partition and subgroup units newest-first inside the subgroup partition. The same partitioning rule applies under a name-root, where directly contained agent nodes precede visible dotted-prefix subgroups. Units with no launch timestamp sort after timestamped units within their partition, with structural names and input order providing deterministic tie-breakers. A session, clan, or workflow subtree uses its outer/root agent's launch time and remains contiguous. Inside a clan, direct members still use the clan-local Failed, Stopped, Running/Starting, Queued, Waiting, Done priority described above, with launch recency breaking same-status ties; that order intentionally differs from this L0 bucket order. Session follow-ups and workflow steps remain adjacent to their direct-member anchor in their established internal preorder, including any name-prefix banners. The Starting bucket remains last and its transient rows remain hidden, so startup-only work does not displace active rows during daemon or launch refreshes. Each mode keeps its own per-group fold registry, so collapsing buckets in BY_STATUS doesn't affect the project layout you had in STANDARD. BY_STATUS banners are prefixed with semantic glyphs (▲, ✗, ▶, …, ⏳, ✓, ◐) so the bucket title still leads visually.

In BY_MACHINE mode, local rows use the here L0 bucket and remote rows use their enrolled alias. Every machine is divided into the same priority-ordered status groups as BY_STATUS, and every status banner remains visible even for a singleton because it communicates lifecycle state. Within a status group, standalone sase agents precede name-root/name-prefix groups and launch recency supplies the same deterministic ordering. A status change can therefore move a row between subgroups while keeping it under the same machine. As in the other nonstandard modes, project and Patch grouping levels disappear and this mode keeps an independent fold registry.

The active grouping strategy is also surfaced in the Agents tab header via an unbracketed group: <label> (o) element so the current session mode is always visible after the cycle toast fades. Top-level header elements are joined by a dim · separator: the leading counts group, an optional filter: element, an optional view: element, the always-visible group: element, and an optional refresh: <N>s (r) countdown. Only the agent status counts and the filter's match count keep square brackets: an active filter reads filter: <query> [matched/loaded] (/), where the dim trailing (/) names the configured edit_query key. After the first scan, the header starts with the visible sase-agent total N. One standalone agent or one sequential session is one sase agent, regardless of whether the session is folded. A rootless clan container contributes no sase agent itself; each direct clan member contributes one, and a direct member that is a sequential session still contributes only one. A hidden top-level STARTING agent contributes one sase agent even though it is not selectable yet. Grouping mode, tribe ownership, and fold state do not change this projection.

Runner load lives at the right of the row as a labeled load: <load>/<capacity> gauge, just before the model/project cluster, followed by a · separator. <load> is occupied runner capacity units and <capacity> is the current effective max_running_agents budget (temporary override first, configured value second). Normal agents claim 1.0; non-default %queue(weight=...) / %q(w=...) launches claim their authored capacity units, and a %q(w=0) launch claims none. Both numbers render as integers when possible (7/10, not 7.0/10.0), trimming to 2 decimals otherwise. If the capacity snapshot is unavailable, sase's TUI renders —/— instead of deriving a fake value from visible rows; when only the occupied value is unknown it renders —/<cap>. Capacity belongs to the machine running this sase's TUI session and does not change when the Agents list is searched, folded, filtered by tribe/project, or focused on remote rows. Rows from other enrolled machines never add to this machine's load, although they still show their own machine's capacity and weight badges (described under Queued below). The visible running count and the global queued count remain in the status strip, for example 8 [8 running · 1 queued] with load: 8/10 at the right. The gauge shares the usage-window ten-step color gradient, keyed on free-capacity percent, so a given color means the same headroom in both places; at or over capacity it becomes the inverted red chip, exactly as an exhausted 0% usage window. In compact density the load: label drops, leaving 8/10. Hovering the gauge spells out the units in use and free, or that new agents queue until capacity frees up when it is full. The tooltip also lists each claim holder and its weight, and a session's running monitor holds its session's claim even when the session row is under Done (see Runner slots). The running count keeps its stable green count style. A nonzero queue count is cornflower blue.

An optional status strip follows in the form [S stopped · T starting · R running · W waiting · F failed · U unread · D done], with numeric counts in place of the letters and zero-count metrics omitted. These buckets classify the same sase agents as the leading total, using a sequential session's normalized owner status instead of counting historical members separately. stopped counts agents paused for plan approval, questions, or workflow human-input steps; starting counts just-launched agents that have not yet surfaced as visible rows; running excludes queued, waiting, failed, and stopped agents; waiting contains genuinely blocked dependency, bead, and time waits, while the status strip's queued count contains every live runner-capacity waiter; failed is terminal failed work; unread counts terminal sase agents that still need acknowledgement; and done is completed visible work that has already been acknowledged. Nested session/clan member summaries remain concrete. The position/navigation denominator is a separate count: rendered selectable roots, where a clan container is one row and a hidden STARTING agent is excluded. During startup the header renders Agents: … until the first agent scan has loaded, avoiding a misleading zero-agent count. Each TUI launch starts in by-project grouping; cycling only changes the current session.

Queued holds QUEUED agents that have cleared every dependency, bead, and time wait and need only runner capacity under their current admission budget. A queued row renders as QUEUED #3/12, followed by pN for an explicit queue priority and by held by <armer> while an agent hold keeps it from starting. A sequential session whose next member is queued shows that member's QUEUED status and queue position on the session row. A clan with exactly one queued direct member does the same on the clan row and CLAN Status: line; two queued members stay generic QUEUED with no rank. Authored capacity renders as a quiet cN badge beside the existing wN weight badge, and turns gold when the launch's budget exceeds the current effective global limit. Non-default queue weights render as the same quiet wN badge used on running rows and queue-ladder entries; an explicit zero renders w0. The detail pane repeats these values as Weight: and Capacity: lines. Remote rows show the same badges and detail lines using the values reported by the machine that owns the agent. New prompts reject capacity=0, but a persisted legacy record with an explicit zero capacity still renders c0 and Capacity: legacy 0 (exact-weight drain budget). Waiting holds genuinely blocked but self-progressing agents — WAITING with a time wait (%wait(time=5m), %wait(time=1430)), a non-empty waiting_for dependency, or a bead wait. A compact WAITING row summarizes named waits as one sequence of independent tokens: agent counts keep the established status glyphs (✗1 ▶1 ✓1 ?1), while bead counts keep the canonical Beads-tab status glyph (○ open, ◐ in progress, ● closed). When a bead status matches a present agent bucket, the bead token follows that agent token, for example WAITING ▶1 ◐2 or WAITING ✓1 ●1; unmatched bead tokens trail in canonical bead order. Zero entries are omitted. When a row waits on exactly one bead and has no agent, name, or group wait dependency, sase's TUI shows that bead's ID instead of a count. The ID has no prefix until status resolution; afterward it is prefixed by the bead's status glyph (or ? for an unknown status). Multiple or mixed waits keep the counts. Unknown agents and unknown beads both render as ?N; when both are present they appear as adjacent independent counts, as in WAITING ?1 ?2. These tokens sit directly after WAITING and before a reserved-tribe !, duration, or countdown annotation. A %wait(for_epic=) follow appends teal ↪ tokens after the counts: WAITING ↪ epic… while the epic launch is in flight, WAITING ↪ ◐ sase-7k while following one epic, WAITING ↪ ◐2 while following several epics, and WAITING ↪ ! when the follow is blocked. An armed target with no recorded follow stage adds no row noise. They are not the trailing gold ◆ linked-bead badge that marks an agent launched by sase bead work. Stopped keeps the strict "you need to act" semantics for plan approval, questions, and workflow input.

Agent Row Glyphs

To keep rows compact, agent statuses and types are rendered as one- or two-character badges instead of verbose text:

Glyph Meaning
▶ RUNNING
✓ DONE
✓P PLAN DONE
▶P PLAN APPROVED
★E EPIC CREATED
✎ PLAN
✗ FAILED
… QUEUED
⏳ WAITING
? QUESTION
↻ RETRYING (followed by attempt count, e.g. ↻2)
⊛ Finalizer chip: the most severe finalizer state since the newest successful run (failed > refused > interrupted > deferred > running), with k of n runs when several runs are in play; the row word reads FINALIZING while finalizers run (see Finalizers on the Agents tab)
≡ Workflow row (top-level)
❑ Patch / Patch row (top-level)
⚡ Autonomous (%auto) agent
◌ Hidden agent (visible only when . toggles them in)
⚙ Monitor turn (row label)
⚙N N running monitors in a session/clan subtree, or in a tribe panel title for its whole tribe (amber)
⚙N N finished monitors in a session/clan subtree, or in a tribe panel title for its whole tribe (grey)
⋔ Gate turn; its gate accent while pending/running, grey when settled, red on failure
⋔N N gates in a session/clan subtree or tribe panel title, colored by lifecycle bucket
▣ Stand-alone %proc named proc (row label; beta, typed_launch_units)
▣N N stand-alone named procs in a panel title's separate proc chip

A monitor turn (a session member whose work is a supervised command, started with sase monitor start) renders an amber ⚙ glyph and omits a left-side title — identity is the right-hand %id (<session>--mon), not the configured monitor label. A live elapsed suffix or exit-code/timeout badge replaces the ordinary agent statuses. The status token — the start label while running, the stop label once settled — is colored by a deterministic accent derived from that pair, so every TESTING/TESTED monitor is the same hue; failed, timed-out, and lost monitors stay red. Two extra badges mark a stalled monitor handoff: a red ⚠ replaces the exit-code badge when a terminal monitor's supervisor never reported a real exit code, and an amber ⚑ follows the row when its --next follow-up was dropped or launched degraded — a monitor can finish cleanly and still strand its follow-up. See Monitors.

Gate turns use ⋔ and their authored pending/settled labels, such as GATE/GATED, QUESTION/ANSWERED, or plan-tier statuses. A pending gate has no LLM process and does not occupy a runner slot, but it may retain the session's workspace claim. Its state is one of pending, settling, answered, completed, failed, timeout, stopped, or lost. Session and tribe chips count pending/running, settled, and failed gates separately by color. After a decision, the gate member owns the displayed outcome: handoff outcomes PLAN APPROVED, TALE APPROVED, EPIC APPROVED, and ANSWERED use the Running bucket even though the gate itself has settled, so the row represents the successor handoff. Rejection and cancellation use Done; timeout, failure, and lost gates use Failed.

A monitor row nests under the agent that started it, not under a synthetic aggregate — one gear-glyph row at the starter's depth plus one. It is revealed by its agent session's fold rather than its starter's own: a collapsed session shows an amber ⚙N badge for its running monitors and a grey ⚙N badge for its finished ones — the two counts partition the subtree's monitors, with a monitor that has not reported a terminal state counting as running — and counts every monitor in its collapsed ×N, but renders no monitor row, even when the session root itself is the starter. The tribe panel title aggregates both lanes across the whole tribe, so a fully collapsed panel still reports both what is still running and how much has already completed inside it. A single l on the session container row reveals every member and monitor in that session in one step; monitors are not deferred to a further "fully expanded" press the way hidden workflow steps are. Selecting a monitor row and pressing l or H acts on that governing session fold — H collapses the session and reanchors the cursor there — while h still walks up to the monitor's starter.

A monitor has no LLM process to kill, so x on a selected running monitor row is routed off the ordinary kill/dismiss path: it opens a Stop Monitor confirmation (defaulting to Keep running) and, once confirmed, terminates the supervised command through the same code path as sase monitor stop. As with the CLI, stopping never launches the recorded --next follow-up agent. The stop itself runs as a tracked proc, so a slow teardown does not block the TUI. x on a monitor row that has already settled falls through to the normal dismiss behavior, and bulk scopes still win over the single row: marks, a focused panel, and a focused group are all handled before sase's TUI looks at whether the selected row is a monitor.

Agents launched by sase bead work also show a gold ◆ <bead_id> badge between the status glyph and the tribe/name. That trailing gold ◆ linked-bead badge is a fixed-style identity marker, not a wait-count token; waited-on beads in a WAITING summary use status-colored ○ / ◐ / ● tokens instead. A phase agent named <epic_id>.<N> displays that phase bead ID; the final <epic_id>.land agent displays the parent epic bead ID; a standalone task worker named <task_id> displays its task bead ID. Legacy plain <epic_id> land agents keep the same badge. Legacy dismissed names keep the badge after their historical date prefix is stripped. Modern phase and task rows use their explicit launch metadata immediately; legacy bead-shaped names retain the deferred bead-store confirmation fallback.

Each agent row also carries a per-provider emoji badge before the display name so the LLM provider behind a row is readable at a glance without scanning the right-hand model suffix:

Badge Provider
🎭 Claude
🪐 Antigravity (agy)
🤖 Codex
🐼 Qwen
🐙 OpenCode
🦋 Muse Code (Meta)
🚀 Grok Build (xAI)

The same provider palette also colors the <PROVIDER>(<model>) suffix on the right edge of the row — the provider name, the parentheses, and the model name each render in a distinct shade from that provider's palette so multi-model fan-outs are easy to scan. Providers without a dedicated palette (anything outside the table above) fall back to a neutral purple palette and render no emoji badge.

An agent that was materialized on this machine by the retired agents-sync import leg carries an owner badge — [<label>] after its identity name — recording where it came from. A foreign machine belonging to you renders as just the machine name; another user's run renders as username@machine. The badge appears wherever an imported identity is named: agent rows, the revive modal, and neighbor pickers. Imported sequential sessions are also folded under one synthetic session-container row so members imported without their root still group as one session instead of scattering across the panel. Both are presentation over leftover local state; nothing imports new rows any more, and sase agent names purge-local-state removes the state that produces them (see Agent Hood Synchronization).

Workflow child rows for python and bash steps render a leading ❯ glyph plus the step name, styled with the matching step-type accent — bash amber, python green — because that name is those rows' identity. The glyph's presence is a stronger signal than the step-type color alone for colorblind users and for rapid scanning; color still carries the bash/python distinction. Sase turns (session-member agent turns, monitor named procs, and workflow agent steps) omit a left-side title; their identity is the right-hand %id / session-member name. Parallel rows fan out into structural children, and prompt_part rows are invisible by default.

The right-hand edge of each row carries a runtime suffix (<start-timestamp> · <elapsed>) right-aligned within the panel. Active rows that have actually started include a 🏃‍♂️ marker before the ticking elapsed duration; unread completed rows use a ✅ marker in the same suffix slot, or ❌ when the agent finished in a FAILED state; and user-paused rows (PLAN, QUESTION, WAITING INPUT) use a ✋ marker while waiting for a human response. Pre-run WAITING and QUEUED rows with no BEGIN time hide the suffix so admission waits do not look like live runtime. On an active sequential-session container, the live suffix is 🏃‍♂️ <current-turn-runtime> / <session-total-runtime>: the left value is the concrete agent or monitor turn currently executing, and the right value is the aggregate session interval. Gate-turn windows are excluded from that session total — they measure the human decision, not agent runtime — and the same exclusion applies to clan totals. On an active clan container the live suffix is <lowest-running-lane-runtime> / <clan-total-runtime> (same marker), where the left value is the smallest current runtime among the clan's running lanes (a sequential session lane contributes its total runtime, the same value its own row shows on the right of its suffix) and the right value is the aggregate clan interval. For finished agents, the start-timestamp half is rendered as a humanized (date_prefix, time) pair sized to fit the existing 15-cell slot:

  • Same day: HH:MM:SS
  • Prior day, same year: Mon DD HH:MM (drops seconds — they're noise once a row finished hours ago)
  • Different year: Mon DD 'YY (date only)

The elapsed duration starts at BEGIN when a row recorded wait-before-run metadata, otherwise at the row start time. For slot-participating user agents, BEGIN is runner admission and includes primary and linked-workspace preparation in the active runtime. Completed DONE / PLAN DONE / TALE DONE workflow rows use the terminal agent stop time when one exists; plan-step rows that finish without a subprocess stop time anchor to the latest recorded plan submission time so completed planning rows do not keep ticking. PLAN APPROVED rows with a running follow-up show active elapsed time for the planner segment plus the coder segment, excluding the idle approval gap between plan submission and code launch. The date prefix uses a softer dim #8787AF while the time half keeps the standard #8787AF, giving the column internal hierarchy without inflating the palette. Statuses not in the table fall back to (STATUS) text for forwards compatibility.

A stand-alone %proc unit (beta, behind typed_launch_units) is backed only by the proc store — it is never an agent, never nested under an agent session, and never counted in agent runner, unread, clan, or session totals. It renders as its own top-level ▣ row with a Bash/Python language badge, current phase/status, elapsed time, and project, and a panel title reports it in a separate ▣<count> chip alongside the ordinary agent metrics. Selecting one opens a NAMED PROC detail (status/phase timeline, project/ workspace/cwd, language, code digest and safe preview, waits, condition result, timeouts, and a bounded live-log tail). x on a running stand-alone named proc asks for confirmation and then kills it and removes its row in one step — the durable cleanup proc stops it through the native proc service — and x dismisses a finished one with no confirmation. Dismissal only clears the Agents-tab row — the proc stays visible in the Procs pane and in sase proc show, and a dismissed Agents-tab row does not come back. See Experimental typed launch units for the directives that create it.

Press / or f on the Agents tab to open the auto-hiding filter bar. ,/ (leader mode) starts forward inline deck search over the focused deck panel — the text of all Main cards (with card titles as separators), the Files view content, or the Tools LLM Calls text: type to jump to matches, Ctrl+R reverses the search direction, Enter keeps the matches (Esc or Ctrl+C cancels), n / N move between them, y / Y yank the current match or selection / its whole line, and Esc or q closes a committed search. The filter bar uses the same structured Boolean Agent dialect as Artifacts -> Agent and sase agent search, evaluated against the live agents-live profile. Bare words match an agent's cl_name, display_name, agent_name, and status, plus its macro, live reply/response, chat transcript, and prior attempt replies through the text corpus. When ace.current_project.seed_agents_query is on, sase's TUI seeds this query with the current project's exact project: term on first load and marks it seeded until you edit it. That setting defaults off because the same query also drives unread jumps and prospective clans. While a filter is active, the Agents header shows it as filter: <query> with any seeded marker, a [matched/loaded] count, and the edit-key hint; clicking the query opens the filter bar.

The bar previews each valid edit against the loaded snapshot, Enter commits and adds the previous query to history, Escape restores the pre-edit query and result, Tab accepts completions, and ^ / _ walk query history while the bar is open. Each successful Enter commit is remembered in machine-local SASE state and restored in the next sase's TUI session; submitting an empty or whitespace-only query remembers the unfiltered view. Typing, Escape, invalid submits, history preview, saved-slot commands, and ,/ deck search do not replace the remembered query.

Restored Agents queries are applied before provider filtering and before current-project seeding. A restored query does not push history, change saved-query slots, or write back to disk until you submit again. Non-empty remembered records from the inactive query dialect, or from an older agents-live profile, are ignored with a warning; explicit empty records restore across dialects.

A leading # saves or deletes an Agents-tab saved-query slot in the agents-live namespace: #3 status:FAILED, # status:queued, and #3 save, allocate, or delete a slot without changing the committed query.

Property keys (closed allowlist):

Key Form Matching behavior
name, session, clan, project key:value Exact, case-insensitive identity fields; completions include observed values.
kind kind:agent, kind:workflow, etc. Enum for agent/member/session/clan/workflow rows.
status, provider, source, needs key:value Enums; source accepts axe / manual, and needs accepts input.
role, workflow, model, cl, text key:value Case-insensitive substring fields; text: searches the full text corpus.
machine, tribe, tab key:value Exact live operational fields; machine accepts here, aliases, and hostnames; tab is exact and main means no stored tab.
pinned, unread, hidden, attention, retry key:true / key:false Boolean properties.
since, until, after, before key:2h, key:today, key:YYYY-MM-DD Start and finish bounds; Nh / Nd / Nw / Nm, today, and ISO dates.
min, max key:5m, key:1h Runtime duration bounds.
attempt attempt:2 Retry attempt number.

Boolean operators: juxtaposition is implicit AND; explicit AND, OR, and NOT (or !) are honored, and parentheses group expressions. Bare and quoted strings are case-insensitive; prefix a quoted string with c for case-sensitive matching, as in c"FAILED".

Legacy token migration:

Legacy (agent_query) Unified (agents-live)
type:workflow / type:run / type:running kind:workflow / kind:agent
age>2h, age>=2h until:2h (started at or before 2h ago)
age<5m, age:5m since:5m (started at or after 5m ago)
status:foo (substring) status:FOO (enum, completion-assisted)
project:foo (substring) project:foo (exact; completion-assisted)
tribe: (bare, "any tribe") no direct equivalent; OR explicit tribes
everything else (cl:, machine:, pinned:, needs:input, source:axe, text:, booleans, AND/OR/NOT, parens, quoting) unchanged spelling

Parse failures are non-fatal: the filter bar renders the error inline and keeps the last good result. When the bad token is a known legacy spelling such as age>2h or type:run, the error appends the unified replacement hint.

A committed query also decides how much agent history is loaded. When the persistent artifact index can answer every term exactly — cl:, model:, provider:, project:, kind:agent / kind:workflow, and machine: (including a negated machine: filter), combined with AND / OR — the loader selects matching agents from the whole archive. Any other term (for example status:, name:, tribe:, tab:, or free text) is first evaluated against the bounded recent-history window, and the header adds filtered on partial history; loading full history... until a quiet-window full-history reconcile finishes and older matches can appear. Whenever the applied roster is knowingly less than the visible inbox for the current query — a fallback scan, a truncated inbox, a query-incomplete read, or a committed-query change still loading — the header shows a loading indicator instead (⟳ loading agents... with no filter committed), which clears on the completing apply. Pushdown-miss queries are only partial when the read was viewport-bounded or truncated; a non-truncated baseline read saw the whole visible inbox.

Transcript files are read only by the background content-index worker while a query is active and are cached by (path, mtime_ns) so auto-refresh stays cheap. Per-file reads are capped at 512 KB; missing or unreadable files are skipped silently. Keystrokes do string-level canonicalization only; row matching runs through the Rust-backed agents-live query index.

Leader Mode (, prefix)

Leader mode is available on every tab. In the Agents tab it also exposes notification shortcuts for the currently loaded agent list; global entries such as ,m and ,U behave the same from other tabs. Unread-completed actions operate on terminal rows that are loaded in the Agents tab; ,j can reveal a direct member hidden by a collapsed clan. Help is not a leader command: press the app-level ? to open the Help modal.

Key Action
,, Repeat the last leader command
,/ Search the selected agent's metadata
,h Run agent from home prompt context; bare prompts default to #git:home
,H Collapse one fold by hint: the selected tribe's expanded folds from a row; every tribe's expanded folds and panel titles from a selected panel
,j Jump to the next unread completed agent, revealing a collapsed clan when needed, and mark it read
,J Jump to the next visible stopped/terminal agent, newest first, without changing unread state
,u Mark all loaded unread completed agents read, across tabs and collapsed clans; with none newly unread, press again within 10 seconds to undo
,m Open Launch Control (aliases, providers, tmux Agent; see Launch Control)
,U Open Update panel (SASE, providers)
,E Plan Everything from cached update snapshots and skip confirmation if runnable
,y Open the Refresh panel with Full history highlighted (refresh Agents from full history when the panel is disabled)
,R Show runners info
,L Jump to the log entry for the most recent error toast
,B Capture an Agents-tab reproduction bundle for debugging row disappearance or duplication
,T Toggle continuous Agents-tab repro invariant checks and auto-capture on violation
,r Revert focused or marked agent commits, including recorded linked repos
,x Kill focused or marked agent(s) and edit their prompt(s)
,X Kill & edit this session's last launched agent, ignoring marks and focus
,<space> Run agent from current agent's PR (skips selection)
,. Open the Prompts overlay on History
,Ctrl+G Edit the newest prompt-history entry in $EDITOR (no overlay)
,> Open the Prompts overlay on History with cancelled prompts visible
,@ Open the Prompts overlay on Stash without auto-restoring a lone entry

Here, "stopped" means a dismissable terminal row such as DONE, FAILED, PLAN DONE, TALE DONE, PLAN REJECTED, PLAN COMMITTED, or EPIC CREATED; it is separate from the Agents header's "stopped" attention bucket for rows paused on user action.

The CLI equivalent of ,x without the edit pause is sase agent restart NAME: it stops the named agent and immediately relaunches the stored prompt under the same name.

If any agents are marked, ,x acts on that marked set instead of the focused row. Stale marks are ignored; if any remaining marked agent has no recoverable prompt, sase's TUI warns and leaves the set untouched. After confirmation, sase's TUI kills or dismisses the marked agents and opens a prompt stack with one editable pane per original prompt in mark order. Embedded --- inside an individual agent prompt stays inside that agent's pane.

Each recovered prompt is marked for forced name reuse so the relaunch keeps the original agent's name instead of claiming <name>1. The marker is a ! on the %id directive, and its exact shape follows the prompt:

Original prompt Rewritten as
%id:foo %id:!foo
%id(foo) %id(!foo)
a clan member's prompt %id(!<suffix>, clan=<clan>)
a session member's prompt %id(!<suffix>, session=<session>)
no %id directive unchanged — a fresh name is used

Note that the clan and session forms carry the member's trailing <suffix> — the part after the <clan>. prefix, or the session role segment — not the full agent name; the membership keyword supplies the rest. An existing bead= value is carried across, and a standalone clan: declaration is dropped in favor of the clan= keyword. The last row is not a failure: a prompt that never named its agent has nothing to reuse, so it simply relaunches under a newly allocated name. A serial session member is the one case where a prompt with no %id is still rewritten, because its session= attachment comes from the row rather than the prompt; session roots are not treated that way. Forced reuse of a session member replaces only that member and its own descendants; the session root and the sibling members are left untouched, so the session= attachment still finds its parent.

sase's TUI is the surface that confirms that reuse, and it carries the authorization through to the launch, so no second confirmation is asked for. Forced reuse cannot be combined with alt/fan-out directives in one segment; such a prompt is rejected with an explanatory error and preserved in prompt history, so you can reopen it from ,. and split the launch.

,X targets the most recently launched agent this sase's TUI session accepted, instead of the focused or marked row(s) — marks are ignored entirely, even when agents are marked. Repeating ,X walks back through this session's launch history one accepted launch at a time; a launch already killed or dismissed by hand is skipped. A launch leaves that history only once its kill or dismissal actually starts, so canceling the confirmation, losing the row mid-action, or failing to resolve the prompt leaves the same launch as the next ,X target instead of walking back to an older one — and pressing ,X again while that confirmation is still up neither opens a second one nor advances. Once its target row exists, ,X reuses the exact ,x machinery above (the same confirmation rule and forced-name-reuse rewrite), so a single-agent launch opens one editable pane and a bulk-Patch or multi-prompt --- launch opens one pane per launched agent in launch order. Nothing is recorded across sase's TUI restarts: the targetable history is this session's in-memory launch stack, not disk state.

,X is meant to undo a premature <enter> press, including the moment right after submit before the launched agent's row exists yet. In that in-flight window sase's TUI restores the submitted prompt immediately (no confirmation — the ,X press is the confirmation) and kills the launched agent(s) once the launch proc finishes. A replacement submit from the restored prompt waits until that kill settles so the original name is not resurrected underneath it. Repeat ,X while the kill is still pending re-focuses the restored prompt and does not walk back to an earlier launch. Restarting sase's TUI mid-flight drops the pending kill: the launch completes, the row appears, and ordinary ,x applies.

Submitting a prompt returns immediately: <enter> removes the prompt bar on the next repaint, even when the launch still has to wait, and the launch carries on as a pending launch. The launch shows as a launch <name> row in the proc indicator and the Procs tab from the moment you press <enter>; its message names what it is waiting for (for example "waiting for kill/dismiss cleanup" while a ,x or ,X cleanup settles), and the durable sase run row takes over when it is submitted. A launch that has not been submitted yet has nothing to kill, so ,X on it simply cancels the launch and reopens its prompt in a bar (still marked for forced name reuse, so a re-submit stays behind the same cleanup). A cancelled bulk-Patch launch restores its shared prompt; the Patch marks are not restored. Quitting sase's TUI while a launch is still pending stashes its prompt so @ can restore it.

Press ,r on a DONE or FAILED agent to preview commits attributed to that agent before creating git revert commits. For plan/follow-up sessions, sase's TUI reverts the session scope when the row carries session metadata; otherwise it reverts the focused agent name. The preview includes the primary workspace plus recorded linked_repos metadata entries that still point at an existing workspace directory; never-opened linked workspaces are not part of this action. Each repository is checked before execution, and a dirty or non-git linked repo is reported and skipped while clean repositories can still be reverted. Successful execution creates one revert commit per repository, pushes when a remote tracking branch is available, and writes revert_result.json beside the agent artifacts.

When agents are marked, ,r previews the combined commit set for the marked DONE / FAILED rows. Marked agents must come from the same primary workspace. The bulk path still groups work by repository, deduplicates overlapping session matches, skips marked rows with no matching commits, and reports partial linked-repo failures instead of hiding them.

Agents Tab Reproduction Bundles

Agents-tab reproduction bundles capture the loader/apply sequence that determines which rows are visible. Use them when the Agents tab briefly drops historical rows, re-adds them, or shows duplicate workflow parents.

When you see one of these bugs in a live sase's TUI session, switch to the Agents tab and press ,B before refreshing again. sase's TUI writes a commit-safe bundle to ~/.sase/repros/<timestamp>-manual-.../agents_tab_repro.json and shows a toast with the path. "Commit-safe" means local names and paths are redacted, and prompt, response, chat, and diff bodies are omitted. The bundle keeps the row identities, loader state, app projection state, screen text, and an SVG screenshot needed to replay the row-list behavior.

Replay a bundle from a checkout of this repository:

sase repro replay tests/ace/tui/repro/fixtures/agents_tab_disappear_reappear_v1.json --assert-stable --json

The current expected verdict for the checked-in fixture is:

{
  "result": "passed",
  "failed_invariants": [],
  "verdict": "current code fixed for the captured Agents-tab bug class"
}

Add --write-artifacts /tmp/sase-agents-tab-repro-artifacts to write one .txt screen dump and one .svg screenshot per replay step. The replay JSON lists those paths in screen_paths and screenshot_paths.

Use out-of-band capture only when you need a filesystem baseline and did not have the live TUI capture running:

sase repro capture agents-tab --output /tmp/sase-agents-tab-capture --commit-safe --json

Out-of-band capture is labeled capture_mode=out_of_band because it loads the current filesystem state and cannot reconstruct transient refreshes that already passed through the running TUI. The replay harness is scoped to the known Agents-tab disappearance/reappearance and duplicate-parent bug class; it is not a general proof for arbitrary rendering races.

For continuous diagnosis, press ,T on the Agents tab to enable invariant checks after each load/apply cycle. On the first violation in a burst, sase's TUI auto-captures one bundle under ~/.sase/repros/<timestamp>-auto-.../ and shows a warning toast. It does not write a new bundle every refresh while the same violation remains active.

Bang Mode (! prefix)

Key Action
!! Run background command
!x Start / stop service host (or select process)
!R Revive a previously dismissed agent

Copy Mode (% prefix)

Press % to open the Copy as… palette for the focused agent. It supports mouse selection, arrows or j/k plus Enter, and every configured direct accelerator below. q/Esc cancels unless that key is itself configured for a copy target, in which case the target wins.

Key Action
%c Copy chat file path
%E Copy file path
%@ Copy the focused concrete agent's durable global @agent: reference
%n Copy the focused agent's agent_name (falls back to display_name; toast indicates which)
%p Copy agent prompt
%r Copy the selected node's tool run id (when it has tool runs)
%s Copy sase tui snapshot

Keybindings: Services Tab

The visible tab is Services. Its sidebar is four stacked panels. The top Service Procs panel holds every service proc assembled from built-in, plugin, user, and machine-overlay configuration — daemon rows plus background commands (oneshot service procs) in a separate ── oneshots ── section below the daemon procs. Below it, one panel per routine declaring source holds that source's routines with their job rows nested under each parent: User Routines, then Plugin Routines, then Builtin Routines. Source means the first config layer that declared the routine, not the layer that last changed one of its fields — a builtin routine with a user interval override stays Builtin, and jobs always render in their parent routine's panel. Each panel border title carries at-a-glance metadata (counts, status chips, and badges such as hidden oneshots or a non-running scheduler). J jumps to the first row of the next panel and K to the last row of the previous panel, wrapping around and skipping empty panels; Ctrl+O returns to where you jumped from. j / k cross panel boundaries on the underlying global list without selecting chrome. Service proc rows show their lifecycle, enablement provenance, restart state, and bounded output. The dashboard status line reports the service host state (Host: <state>). Project-local sase.yml service entries are intentionally ignored.

Builtin routine rows start folded on first sight so the dense builtin list stays calm; a folded row still carries its jobs' health as a red !N badge (failed, timed-out, or missing-script jobs, counted from the same cached snapshots as the panel title). Folding applies only once those snapshots have arrived, so a header-only pre-load never hides rows behind a misleading fold. h / l fold and expand the focused routine, and an explicit fold persists for the session. ace.services.fold_builtin_routines (default true) controls the first-sight behavior permanently.

Once service status has loaded, the info panel leads with a host clause: Services · host ● running 4d · sase.service while the host runs (uptime in its largest unit when known, then the installed native service unit or detached), Services · host ○ starting while the host is still starting (not a failure), Services · host ○ stale · press !x to start when the heartbeat is stale, or Services · host ○ stopped · press !x to start when the host is down. A host config error renders inline (! <error>), and an unreadable snapshot renders its short reason in the header. Disabled rows carry an inline provenance chip (disabled here for a local override, disabled by <layer> otherwise), unavailable rows show unavailable: <reason>, and other non-running rows carry the core's human summary (for example crash-looping (3)) plus the restart count (· 5r) whatever the state.

On a selected top-level service proc, x starts or stops it, r restarts it, and !e enables or disables it on this machine. Those actions are no-ops when no service proc is selected; in particular, x does not toggle the host from a routine or job row in any of the routine panels. Use !x to start or stop the service host itself. The SVC footer pill shows the running/desired service-proc count (N/M), or a red ! when the host or a service proc is unhealthy (see Service Health Pill). The right-hand panel shows the selected proc's effective command, current and desired state, restart policy, dependencies, last exit, restart decision reason, stop provenance, any pending start/restart request, and output tail, plus the host config error when one is set. The description sits in the sticky description panel above it.

The canonical tab id is services (axe is still accepted as a legacy alias, for example sase tui -t axe). Configuration keys such as ace.axe_description_expanded and many internal row names keep the historical axe spelling, which is why the detailed scheduler reference below still uses “Axe.”

The Services sidebar renders four row types across its four panels so the operational tree reads at a glance:

  • Service proc rows live in the Service Procs panel with a solid left accent bar (▌) in the service teal, a [*] / [!] / [~] / [?] / [-] / [·] status marker, the proc name, and an optional state chip (restart count, disablement provenance, or the core's human summary).
  • Routine rows live in their declaring-source panel (User, Plugin, or Builtin Routines) as top-level sections with a solid left accent bar (▌) in the routine hue, a [*] / [!] / [·] running/error/idle marker, the routine name, an optional overrun roll-up (⚠N), a red health badge (!N) counting failed, timed-out, or missing-script jobs even when the row is folded, and an optional compact Nc / Ne cycles/errors chip at the end.
  • Job rows are child rows indented under their parent with a └─ tree connector, a per-run status icon (✓ success, ! failure/timeout, ? missing script, ● running, * agent-launched, · no runs), and the job name in a dim-gold child hue. Disabled jobs remain visible with a quiet disabled chip but cannot be run manually.
  • Oneshot rows (background commands run via !!) live in the Service Procs panel below the daemon procs under a dim ── oneshots ── divider when both groups are present. Each row leads with a state glyph (▷ running, ✓ exit 0, ✗ failed or killed), a muted #N index, and the command, and ends with a chip carrying the recorded exit code and age (for example exit 0 · 4m ago), so they cannot be mistaken for scheduled AXE work. Commands started before oneshots existed remain readable from their old slot directories (no exit code) while the bgcmd_legacy_slots sunset flag is on.

Description Panel

The right-hand dashboard keeps the selected service proc, routine, or job description in a dedicated panel between the status line and scrolling output. Every row of the panel carries a solid left accent gutter (▌) in the row's own hue — service teal for service procs, routine gold and job copper otherwise — so the block reads as a blockquote and stays visually distinct from the output pane below. Generated for_each job instances also show their target key on the summary row. The panel stays fixed while output scrolls and disappears for background-command and empty Services views. It shares one session state across the Services tab: d on a service proc row also collapses routine and job panels, and the reverse is true too. A service proc with no configured description shows the dim-italic No description configured fallback, while a proc missing from the snapshot hides the panel entirely. Oneshot rows get no panel.

The panel has two states, and d toggles between them for the rest of the session. The summary row ends with a ▸ d / ▾ d disclosure hint whenever there is a body to reveal and the row has room for it:

Collapsed — one row, ellipsized at the pane width:

▌ Complete finished hooks and start stale ones, with zombie detection            ▸ d

Expanded — the summary, a blank gutter row, and the reflowed body:

▌ Complete finished hooks and start stale ones, with zombie detection            ▾ d
▌
▌ Scans every Patch matching the axe query, completes hooks whose runner exited, and
▌ starts the next stale hook when a runner slot is free.
▌
▌ • Honors max_hook_runners; a full slot table defers work to the next tick rather than
▌   queueing.
▌ • Hooks still running past zombie_timeout_seconds are marked ZOMBIE and stop holding a slot.

The body is reflowed rather than replayed: blank lines separate blocks, a block whose first line starts with -, *, or • renders as a hanging-indent bullet list, and every other block is joined into one paragraph and re-wrapped to the current pane width. See Description Grammar for the authored form.

ace.axe_description_expanded (default true) sets the state each sase tui session starts in. d flips an in-memory session state and repaints from cached snapshot data — it never reloads config, reads disk, or writes the toggle back.

An expanded panel never crowds out the job output it exists to explain. The dashboard budgets max(3, min(16, floor(pane_height * 0.45))) rows for the panel, falling back to 10 rows before its height is known. If the rendered block exceeds that budget, the last row becomes a dim … +N more · e marker on routine and job rows — service proc rows read … +N more with no · e, because e does nothing there: nothing is silently dropped, and e opens the AXE entry editor, whose first field is the full description in a multi-line text area.

Because d belongs to the Services tab, show_diff is scoped to the Patches sub-tab. Pressing d outside Patches no longer opens a diff for an unrelated Patch.

Dynamic Sidebar Width and No-Wrap Rows

Every sidebar row is rendered as single-line Rich Text with no_wrap=True and overflow="ellipsis". After each refresh each panel computes its widest formatted row and its border title, and the widest of the visible panels decides the sidebar width: the AXE container resizes between a 35-cell minimum and an 80-cell maximum, clamped further so the right-hand dashboard always keeps at least 40 cells. On terminals too narrow to fit a label even at the clamped width, the row ellipsizes rather than wrapping onto a second line, and a long panel title truncates with Textual's border-title ellipsis.

Controlled-Output Highlighting and ANSI Fallback

Output in the dashboard right panel uses a semantic highlighter for sources whose shape is controlled by sase, and falls back to ANSI rendering for everything else:

  • Routine aggregate logs ([YYYY-MM-DD HH:MM:SS] [routine] message) get timestamp, routine name, status words (success, failure, timeout, running, error, …), PIDs, durations, exit codes, and counts colored by severity and consistent with the sidebar taxonomy.
  • Controlled job output — runner lifecycle lines such as Launched proposal 1 as <name> (PID <pid>) use the same status-word, PID, duration, and count highlighting as other routine messages.
  • External job scripts and background command output are arbitrary text and stay on the ANSI fallback (Text.from_ansi) with the existing capping and tail-biased caching behavior.

Render cache slots are keyed on (source_id, source_type) so the semantic and ANSI paths cannot collide for the same numerical identity.

Job Result Documents

Selecting a recorded job run composes three sections inside the existing scroll region:

  1. RESULT is always present and is derived entirely from the cached run entry. It includes the status, structured summary and reason, counters, proposal and launch rosters, evidence, dry-run/source markers, and any error or traceback.
  2. A job-authored structured report follows when the result document supplies one. Semantic tones map to the AXE palette, tables elide cells at wide widths and stack at widths below 60 cells, and all job strings are rendered literally rather than parsed as Rich markup or ANSI.
  3. OUTPUT contains the existing ANSI-rendered log tail and retains the waiting, failure, reason, and no-output fallbacks for runs with an empty log.

The card and report are cached by run identity, lifecycle state, completion timestamp, and rendered width. They paint only from the in-memory job snapshot; navigation does not read, stat, or glob the run files. Auto-scroll continues to follow active running and launched output, but terminal runs open at the RESULT card so the report is not scrolled off screen on selection.

Key Action
j / k Cycle the next / previous nav item in the focused panel (service proc, oneshot, routine, or job) and wrap inside that panel
J / K Jump into the first / last row of the next / previous panel
Ctrl+N / Ctrl+P Page through the focused job's run history (older / newer)
' Jump to a current-tab entry by adaptive hint
Ctrl+O / Ctrl+Shift+O Walk the link trail first, then the current-tab jump stack
$$ / $1-$9 / $0 Follow the first / numbered job link, or open the complete links panel
` Jump to an entry across all tabs
g Scroll to top
G Scroll to bottom (pins auto-scroll)
Ctrl+D / Ctrl+U Scroll output down / up by half a page
Ctrl+F / Ctrl+B Scroll output down / up by a full page

Commands

Key Action
a Add a routine, or add a job under the selected routine
d Expand / collapse the description panel for the selected service proc, routine, or job
e Edit the selected routine or job configuration
E Open the selected recorded job output in $EDITOR
+ Run agent
r Restart the selected service proc, run an enabled selected job manually, or re-run the focused completed background command (!!) row
x Start / stop the selected service proc; on a oneshot row, kill it (after confirmation) or clear it once finished
X Clear output
. Show / hide oneshot rows (the Service Procs title counts hidden ones as +N hidden)

The a flow discovers installed sase_job_* executables and also accepts a custom executable. Both add and edit open a single-page property sheet showing every schema field, including unset and inherited fields. The active row's detail dock shows its schema help plus effective, target-layer, and inherited values. Edits remain sparse: an inherited field is not copied into the selected writable scope unless you touch it. Compound and advanced fields expand in place as raw YAML, with inherit/reset available for removing a target-layer override.

Editing a generated job row edits its immutable base job and warns that every generated instance is affected. Before writing, the panel shows an exact effective before/after preview plus a source-file diff. When the scheduler is running, the preview makes restart explicit: save and restart the scheduler to reconcile it immediately, or save only and leave the current scheduler configuration active until the next restart. E remains reserved for opening recorded job output.

AXE Property Sheet

The panel opens existing entries in browse mode, with no editor focused, so property navigation works immediately. A new entry opens in cell mode on its first required property.

Browse key Action
j / k / ↑ / ↓ Move to the next / previous property
g / G Move to the first / last property
Enter / i Edit the active value in place; toggle a boolean
Space Toggle a boolean or cycle an enum forward
h / l / ← / → Cycle an enum backward / forward
Ctrl+R Mark inherit/reset; press again to restore the original value
1…9 / Ctrl+T Select a numbered writable scope / cycle scopes
Ctrl+S Build the validation and source-diff preview
Ctrl+L Reload after a stale-write conflict while preserving the draft
q Close the panel directly and discard unsaved edits
Esc Close the panel
Cell key Action
Enter Commit a single-line value; insert a newline in a multi-line value
Esc Leave INSERT for NORMAL, then commit and return to browse mode
q Type q in INSERT; close the panel directly from NORMAL mode
Tab / Shift+Tab Commit and edit the next / previous property
Vim keys Edit through the standard VimTextArea layer
Ctrl+S / Ctrl+R / Ctrl+T Preview, inherit/reset, or cycle scope while the editor is focused
Preview key Action
↑ / ↓ / Ctrl+D / Ctrl+U Scroll by line or page
g / G Scroll to the top / bottom
Enter Save, restarting the scheduler when it is running
Ctrl+O Save without restarting the scheduler
q Close the panel directly
Esc Return to the property sheet

Leader Mode (, prefix)

Help is not a leader command: press the app-level ? on any tab to open the Help modal.

Key Action
,, Repeat the last leader command
,h Run agent from home prompt context; bare prompts default to #git:home
,m Open Launch Control (aliases, providers, tmux Agent; see Launch Control)
,U Open Update panel (SASE, providers)
,E Plan Everything from cached update snapshots and skip confirmation if runnable
,L Jump to the log entry for the most recent error toast
,R Show runners info
,. Open the Prompts overlay on History
,Ctrl+G Edit the newest prompt-history entry in $EDITOR (no overlay)
,> Open the Prompts overlay on History with cancelled prompts visible
,@ Open the Prompts overlay on Stash without auto-restoring a lone entry

Bang Mode (! prefix)

Key Action
!! Run background command
!x Start / stop service host (or select process)
!e Enable / disable the selected service proc on this machine

Copy Mode (% prefix)

Press % to open the Copy as… palette for the selected AXE row. Choose with the mouse, arrows or j/k plus Enter, or use a configured direct accelerator. q/Esc cancels, with configured target keys taking precedence.

Key Action
%o Copy visible output
%O Copy full output
%s Copy sase tui snapshot

Services Control

Key Action
Q Open the quit / restart menu

Query System

Editing Queries

/ is the app-level query key on every Artifacts pane and on the top-level Agents tab. Every pane with a filter session keeps a persistent idle filter row; / (or the local f) focuses it for editing. On Agents, / (or f) opens the auto-hiding filter bar, and ,/ starts forward inline deck search. Help is the app-level ? on every tab.

Context Default query key
Patches / (or local f)
Stitches / (or local f)
Beads / (or local f)
Provider documents / (or local f)
Files / (or local f)
Artifacts → Agent /
Agents tab query / or f

The Services tab has no query editor. Its ? help modal and the command palette both still offer "Edit search query" there, but the action currently does nothing on Services; use the tab's own filtering and navigation keys instead.

To save a query, prefix with #:

  • #3 "myproject" -- save to slot 3
  • # "myproject" -- save to next available slot
  • #3 (no query) -- delete slot 3

On Patches, Artifacts → Agent, and the top-level Agents tab, these commands run inside the inline filter and leave both the active query and editor session in place.

On first open, when ace.current_project.seed_filters is on and the Patches query carries no project: / +name term of any polarity or depth, sase's TUI appends a visible project:<name> token for the current project (the configured project name, never the ProjectSpec key). An explicit term wins, including NOT project:… and a term nested inside parentheses. The seeded token is session-only: it is not written to last_query.txt. Press p and choose All projects to remove it. Submitting the query from the filter bar keeps the token as yours and persists it.

Saved Queries

Saved-query slots are kept separately for each Artifacts pane. On the Artifacts tab, press 0 followed by a slot digit (1-9, then 0 again for slot 0) to load that slot from the active pane's saved queries -- e.g. 02 loads slot 2. The active pane is not switched; a slot saved for a different query dialect is refused with an error instead of being applied. Esc or any other non-digit key after 0 cancels without changing the query. Bare digits still select the corresponding visible Artifacts sub-tab; the saved-query slot keys live behind the 0 prefix so the two never collide.

Press * on an Artifacts pane that supports saved queries to open the saved-query chooser instead. Press a populated slot (1–9, then 0), move with j/k or the arrow keys and press Enter, or click a row. q/Esc closes the chooser without changing the query. The chooser shows the saved query text and marks the active query; an empty chooser also repeats the save syntax. The chooser is unavailable from the Agents and Services tabs.

Query History

Key Action
^ Navigate to previous query in history
_ Navigate to next query in history

Query history is available on every Artifacts sub-tab whose pane contract enables query_history, including provider-backed document panes, and inside the top-level Agents tab filter bar. Each pane has its own durable previous/next stack, and Help shows the active pane's stack with the configured previous/next key labels.

See docs/query_language.md for the full query syntax reference, including boolean expressions, status shorthands, property filters, and searchable fields.

Config Flags pane

Open SASE Admin Center with #, then Config > 02 Flags. The pane is the keyboard-first control surface for every code-owned SASE feature flag. It shows effective state, kind, default, provenance, saved machine preference, description, removal bead, and removal horizon; it does not edit portable configuration files.

With the default-on admin_center_flags sunset flag, Config's nested catalog is:

01 All · 02 Flags · 03 Holds · 04 Launch · 05 Memory · 06 Snippets · 07 Macros

Disable admin_center_flags to drop Flags, leaving the six-child catalog numbered 01 All through 06 Macros. sase flag enable and sase flag disable remain available either way; they are the recovery and automation surface when the pane is off.

The pane uses the Admin Center list/detail layout: a header with registered/on/saved counts, a flag rail, a scrollable detail card, a hidden inline filter, and a one-line footer. Effective on/off and source are shown separately from saved on/off. When environment or root CLI -f/-F still wins, a yellow “forced for this process” warning explains that saving restarts sase's TUI and the service host but will not take effect until that override is removed.

Key Action
/ Open an inline filter over key, description, kind, effective state, and provenance
Esc Close/clear the filter before closing Admin Center
j / k Move the rail; arrows, Home/End, and mouse clicks work the same way
q Close Admin Center
Enter/Space Open a cancel-first confirmation (OFF -> ON or ON -> OFF)
r Reload the catalog
0 then 1–7 Jump to a numbered Config child while Flags is visible (1–6 when the rollout flag is off)

Confirmation is cancel-first. It names the flag, the current-to-target state, the saved state path, any shadowing source, and that sase's TUI and service host restart after TUI tasks and installation changes finish. Confirming writes only the machine-state file, waits up to 60 seconds for TUI-local work and installation mutations, then performs one controlled sase's TUI+service-host restart. Independent commands keep running. Durable operations started from this TUI still finish their result handling first, within the same 60-second wait. A restart failure does not roll back the saved preference.

Disabling admin_center_flags from its own row is supported: the confirmation says the Flags pane will disappear after restart and gives sase flag enable admin_center_flags as the recovery command.

See feature flags for precedence, the state file, and the CLI contract.

Global Keybindings

These work on all tabs:

Key Action
Tab / Shift+Tab Switch between Agents, Artifacts, and Services tabs
# Open SASE Admin Center home (repeat on home to resume the last section); inside a working section, jump to the alternate section (repeat to toggle back)
. Artifacts: collapse/expand the relations panel; Services: show/hide oneshot rows; Agents: expand/collapse the jump panel
: Open the Command Line: run sase commands without leaving the TUI
; Open the context-aware Command Palette
i Show notifications inbox
+ Run a custom agent (opens project/Patch selection)
Space Prefill the prompt with the most recently launched VCS macro (blank home prompt if none; Space then Ctrl+U for a blank prompt)
Ctrl+G Open the agent editor pre-filled with the most recent VCS macro prefix
Ctrl+L Dismiss all currently-visible toast notifications
@ Restore a stashed prompt: a lone entry restores directly; several open the Prompts overlay on Stash (@@ pops the newest draft when several are stashed)
$$ / $1-$9 / $0 Follow the first / numbered contextual artifact link, or open the links panel
Q Open the quit / restart menu
R Open the Refresh panel on Artifacts and Services (this tab, full history, usage, or everything). On Agents, retry the selected local or remote agent
r On Agents, refresh (or open the Refresh panel). Artifacts and Services keep r for Patch workflow / Services run or re-run
q Quit (first closes an open artifact viewer pane)
? Show help modal

The generic Open SASE Admin Center action and the first # always open a lightweight landing page without mounting a working pane. Press # again while home is visible to resume the last section that was successfully active. With no prior visit, the repeated key stays on home and loads nothing. Inside a working section, the same key takes on a second meaning: it jumps to the section you were in immediately before the current one, and pressing it again toggles back — exactly two sections remembered, like a two-slot alternate. A color-coded, clickable footer along the bottom of the working section names the jump target (or explains that none exists yet). The numbered strip remains clickable: 1 Config, 2 Logs, 3 Machines, 4 Procs, 5 Projects, 6 Statistics, 7 Tools, and 8 Updates. Tab enters Config, and Shift+Tab enters Updates. Each working pane and its data are loaded only on first entry, then cached while the modal remains open. Command-palette actions such as Open logs panel, Open procs panel, and Open statistics, plus update shortcuts and indicators, enter their requested pane directly and make a successful entry the next resume target. Closing from home does not clear an older target.

Both the top-level resume target and the alternate are persisted machine-locally and survive across sase's TUI process restarts. Within one running sase's TUI process, closing and reopening Admin Center also remembers each selectable pane's last logical entry by stable identity, plus the minimal scope or sub-tab needed to show it again. Filters, marks, scroll position, loaded data, pane instances, Statistics controls, and other pane-local state still end with the modal. If ace.keymaps.app.open_config_center is rebound, repeat that configured key instead; the footer and landing page display the effective binding and destination.

Inside every working section, ' is an Admin Center-wide entry-jump key: it paints adaptive hints over that section's selectable rows using the same hint alphabet described under Navigation in Agent, Stitches, Beads, Provider Documents, and Files, a hint character moves the selection there, ' again returns to the previous position (or the first hint with an empty back stack), and Esc cancels. Each working section's own keybindings table names its jump targets; two are deliberate exceptions. The Statistics tab has no row cursor, so ' there arms the same numbered-view selection the 0 prefix already arms, using the visible strip numbers as hints. Config's nested catalog is ordered 01 All · 02 Flags · 03 Holds · 04 Launch · 05 Memory · 06 Snippets · 07 Macros when admin_center_flags is on, or 01 All through 06 Macros (without Flags) when it is off; 0 then the matching digits selects those children, while bare digits continue to belong to the active child or the Admin Center's top-level tabs. The Updates tab's single merged list jumps normally across every section — SASE, Plugins, and Agent CLIs alike.

Quit / Restart Menu

Pressing Q opens the quit / restart menu. When this TUI instance has in-process work, the menu warns inline (N TUI tasks will be interrupted) and it offers three actions:

  • 1 / s — quit sase's TUI and stop the scheduler service proc (best effort: the TUI still quits if the stop fails); the service host and its other service procs keep running
  • 2 / r — restart the TUI, leaving the service host running
  • 3 / a — restart the TUI and restart the service host

Press esc (or q) to cancel and return to the TUI.

Durable procs, ! background commands, monitor turns, and service procs survive quitting and restarting; the menu says nothing about them because they keep running.

A plain q quits sase's TUI directly when nothing would be interrupted. Otherwise q shows a y/n confirmation (default No) listing the in-process TUI work that would be lost; confirming quits, declining (or esc/q) stays in the TUI.

An unsent agent prompt does not, by itself, ask you to confirm. After any required confirmation — and immediately when none is required — plain q and menu quit (1 / s) write that prompt to Stash before exiting. The save is one Stash row: every non-empty pane, in order, or the frontmatter alone when every pane is empty but frontmatter is present. Plan Feedback and Coder Prompt bars are not saved. If the write fails, the exit stops, the TUI stays open, and the error is Failed to stash prompt draft — staying so no text is lost. Choosing 1 / s also leaves the scheduler running when that save fails.

Restart (2 / r, or 3 / a) tries the same save. When it succeeds, the TUI says Prompt draft stashed; press @ to restore after restart. When it fails, the restart still exits and the draft is not written.

A submitted prompt that is still waiting to launch does not, by itself, ask you to confirm. The scheduler batches in the next paragraph are separate: they are chop launches that have not all been started. When a quit or restart actually exits, each waiting prompt is recorded in prompt history as cancelled and copied to Stash so @ can bring the Stash copy back. The exit still completes if that copy fails or does not finish within 3 seconds. When a confirmation is shown for other in-process work, its summary adds Your unsent prompt draft will be stashed when a draft is open, and a count such as 1 pending launch will be stashed for @ when prompts are waiting. Declining the confirmation writes nothing.

Options 1 and 3 also check for in-flight scheduler launch batches (a chop run that has proposed launches but has not launched them all yet). When quitting would drop the remaining launches — or when in-process TUI work would be lost — a y/n confirmation (default No) lists exactly what would be lost. Option 2 confirms only for in-process TUI work. Declining returns to the TUI without exiting or stopping anything; when nothing would be lost, q and all three Q options behave exactly as before with no extra prompt.

Command Palette

Press ; from any tab to open the Command Palette — a context-aware modal listing every keymapped action that is currently runnable. The palette is the discovery surface for the TUI: rather than memorizing every chord, you can search by command label, key sequence (e.g. %n, ,A, zc), category, or alias.

Behavior:

  • Only commands applicable to the current tab and selected entry are shown by default. For example, PR diff appears only when a PR is selected; service start/stop appears only on the Services tab; agent-specific actions appear only when an agent row (not a group banner) is focused.
  • Each row shows the keybinding, the command label, and a category badge such as Navigation, PR Actions, Agent Actions, Copy, or Leader.
  • A title-bar badge (Agents, Artifacts, or Services) reflects the current tab.
  • Typing : into an empty filter hops to the Command Line.
  • When the filter has no match, a fallback row offers Runsase in Command Line, which opens the Command Line pre-filled with the filter text (without running it).

Keybindings inside the palette:

Key Action
Type Filter commands (case-insensitive substring)
: on empty filter Hop to the Command Line
↑ / ↓ Move highlight
Ctrl+P / Ctrl+N Move highlight
Enter Run the highlighted command
Esc Close without running anything

The palette delegates execution to the same handlers that the keybindings use, so behavior matches pressing the chord directly. Selecting a built-in mode subcommand (e.g. %n to copy an agent name) runs the action without forcing you through the transient prefix mode. Custom modes defined in sase.yml are also represented per-command.

The ; binding follows your configured keymap. To rebind it, set ace.keymaps.app.open_command_palette in ~/.config/sase/sase.yml; comma-separated keys in that setting are treated as alternate bindings for the same action.

Command Line

Press : from any tab to open the Command Line — a centered panel where you type sase commands (the sase prefix is implicit) and run them without leaving the TUI.

Behavior:

  • Selection-aware, fuzzy completion with a live signature line: the panel knows the current selection, ranks the selected entity first, and shows advisory grammar diagnostics. Tab completes, Ctrl+F accepts an active completion, Ctrl+R searches history, and ; on an empty line hops back to the Command Palette.
  • The frame's borders carry the chrome: ❯ Command Line and the working-context chip (⌂ +<project> · <path>) on the top border, the key hints for the current context and the N running count on the bottom one. Both borders recompose when the terminal resizes, so their ends stay aligned: the chip is middle-truncated first, and the hints give way to the running count. The frame is 96% of the terminal wide (at most 200 columns) and 80% of its height, centered; Ctrl+T toggles full height.
  • The completion popup (and, on terminals at least 140 columns wide, the doc peek) sits in a completion tray beneath the input and signature line. It never covers command output. The tray keeps a fixed height (up to 13 rows), so the input never moves as candidates change. The candidate text still lines up under the slot being completed.
  • The transcript follows new output. Ctrl+D / Ctrl+U scroll it by half a page from the input in INSERT or NORMAL mode, taking over the input's delete-char / delete-to-line-start keys in this panel. Scrolling back to the bottom (or running a command) resumes following.
  • The completion popup shows an 8-row window that scrolls through every candidate; its footer counts the highlighted row (N of M) and shows the one key that fits the menu state. An empty line offers RECENT history and FOR <selection> suggestions under section headings. A slot whose provider failed reads ⚠ <kind> unavailable, and one with nothing to offer reads no <kind>; the note never carries over to another slot. Provider results are cached briefly (pending plans for 5 seconds, everything else 15) and forgotten when a command finishes; the next fetch then skips the providers' own disk cache, so a plan you just approved is not offered again. A result that arrives after you edited the line or moved the cursor is dropped. Project slots always merge the provider behind the projects the TUI already knows, and a path slot lists dotfiles once the name you type starts with ..
  • Every command runs as an ordinary durable proc (tagged command-line, visible by default in Admin Center → Procs), so hiding the panel never interrupts anything. Finished command-line procs keep their own retention bucket of 50.
  • Run policies: most commands run as procs, some (editors, pagers, interactive tools) run in the real terminal with the TUI suspended, and a few refuse with an alternative. Declined confirmation commands render an explicit R rerun with -y; R does nothing for other blocks.
  • Built-ins: cd (pin a working directory), clear, help, and history. They run instantly with no proc. cd takes a path, +<project> (the project's label as completion shows it, or +home), or - to unpin.
  • Block keys (NORMAL mode): o expand, v pager, K kill, r/R rerun, e edit, y/Y copy output/command, p open in Procs, x remove, i back to input.

Panel keys are configurable under ace.keymaps.command_line in ~/.config/sase/sase.yml: history (up/down, Ctrl+R), the ; palette hop, scroll_transcript_down / scroll_transcript_up (Ctrl+D / Ctrl+U), and every Block key above. Set an action to unbound to disable it; it also leaves the key hints. The completion-menu keys follow the zsh menu-select contract and stay fixed: Tab / Shift+Tab / Ctrl+N / Ctrl+P / ↑ / ↓ to move, Enter or Ctrl+F to accept, Esc to leave the menu, → to accept ghost text at the end of the line.

The : binding follows your configured keymap. To rebind it, set ace.keymaps.app.open_command_line in ~/.config/sase/sase.yml.

Machines Tab

Open the SASE Admin Center with #, then press 3 or select Machines. This is a local inventory and administration surface for the controller plus every enrolled remote. It does not contact all gateways when opened: remote rows begin with state not checked and health unknown until you request an explicit status check. Remote capacity remains not reported even after that check because the status response does not carry runner capacity. The local row reports the controller's configured runner capacity immediately.

The list shows Alias, State, Health, Capacity, Last observed, and Endpoint. The local Alias is the controller's configured machine name even though its Agents query token is here. The detail card adds capabilities, provider, pinned installation identity, quarantine state, and the status message from the most recent check while this Admin Center remains open.

Key Action
j / k Move selection
/ Filter aliases, endpoints, providers, installation identity, and quarantine
s Run one bounded authenticated hello for the selected remote
c Show the persistent connect/enrollment flow
r Show repair guidance for the selected remote
R Show the rename command for the selected remote
x Show removal guidance for the selected remote
y Copy the commands shown in the action card
Enter Close Admin Center and open this machine's agent tab (or filter Agents when the tab strip is hidden)
f Close Admin Center and open Agents filtered to this machine
U Reload local machine inventory without probing gateways

Connect, repair, rename, and remove are guidance actions: they display canonical CLI commands and safety notes but do not mutate machine state themselves. Enter selects the machine's agent tab when the tab strip is visible, and otherwise applies an Agents query of machine:local for the local controller or machine:<alias> for a remote (f always filters). See the Remote Dispatch Runbook for the credentialed enrollment and recovery procedures.

Projects Tab

Open the SASE Admin Center with # and switch to the Projects tab with 5, Tab / Shift+Tab, or the main tab strip. The tab contains a second clickable strip: Projects · Repos · Workspaces. [ / ] cycle these sub-tabs while Tab / Shift+Tab continue switching the main Admin Center tabs.

The Projects sub-tab lists true, non-system projects only, with enabled projects first and disabled projects still visible. Here, "true project" means a project backed by its own main ProjectSpec, rather than an internal linked-repo backing record; a true project can be enabled or disabled. Rows show a CUR marker, the display/canonical name, VCS kind (git or gh), lifecycle state, active claims, workspace/repo counts, and warnings. Telemetry-only directories and linked-repo backing records cannot appear.

Key Action
j / k Move selection
' Jump to a row via adaptive hints, within the active sub-tab only
/ Filter the current sub-tab
[ / ] Cycle Projects, Repos, and Workspaces sub-tabs
r / w Show repos or workspaces pre-filtered to the highlighted project
Enter Run the highlighted project's default lifecycle action
i Initialize the marked set, or the highlighted project if unmarked
I Initialize every enabled project (sase init --all)
m / u Toggle one mark / clear all marks
e / A Edit the ProjectSpec / aliases
a / d Enable / disable the highlighted project or marked set
c Make the highlighted project current
Ctrl+D Delete the highlighted SASE project directory or marked directories
F Force the last blocked disable after confirming live-work checks
R Reload records or the current inventory
p Open the shared project picker on the Repos or Workspaces sub-tab
Esc Clear an inventory project filter; otherwise close the Admin Center
q Close the SASE Admin Center

c requires the highlighted project to be enabled and launchable; pressing it on a project that is already current, disabled, or not launchable reports why in the status line instead of starting a write. All Projects-tab keys, including c, i, and I, are configurable under ace.keymaps.projects.

Initialize from the Projects tab

i initializes the marked set, or the highlighted project when nothing is marked. Disabled and system-managed rows in the mark set are dropped with a status message; if nothing remains, sase's TUI warns and submits nothing. I always means the canonical sase init --all inventory: marks, filter, and highlight are ignored.

Either key sets a status line immediately, then plans off-thread with sase init … --check --json. When every target is already current, sase's TUI toasts that and does not open a modal. Otherwise an initialization-plan preview shows the exact apply argv, per-planner rows with the CLI's glyph vocabulary, and warnings and blockers verbatim. The memory step may commit and push generated project memory; that warning is shown with the same prominence as the CLI prompt. Confirm runs exactly one sase init … --yes proc into the Procs tab and refreshes the pane in place. If the plan has TTY-only blockers, t suspends sase's TUI into interactive sase init for the blocked subset. Enter still means enable; initialization is never implicit.

The preview's own keys are listed on its border: y runs the plan, d toggles full file diffs on and off, t hands the run to a real terminal when the plan has TTY-only blockers (the row and key appear only then), and Esc, q, or n cancels. Ctrl+D / Ctrl+U scroll a long preview. The run button is disabled outright when no project in scope has a changed, runnable planner, and a plan whose runnable planners would overwrite or delete a file is styled as a danger confirmation rather than a neutral one.

The current project — the same one the status-row project: +<name> chip names — is marked on three surfaces at once, all in that project's accent color: a + in the row table's CUR column plus its name rendered in that accent, a current:+<name> segment appended to the summary line, and a +CURRENT badge on the detail panel's header line. The detail panel also carries a dedicated Current project: line for the highlighted row, whether or not it is current — stating the fact for a current row (and, when it arrived via a Patch, which one) or the exact reason and fix for one that is not (enable it first, it has no launchable ProjectSpec, or press c). This display always resolves live and ignores ace.current_project.indicator, which only hides the status-row cluster's project group; see Current project.

When one or more projects are marked, a, d, and Ctrl+D target the marked set instead of only the highlighted row. Successful lifecycle changes clear the affected marks; blocked or failed rows stay marked so you can inspect or retry them. Disabling uses the same locked mutation path as sase project disable; live RUNNING claims or artifact markers block it unless the F force retry is intentional.

The Repos sub-tab inventories every known primary, sidecar, linked, and opened external repo for enabled projects by default. Rows show owning project, checkout presence, and path; details include source, description, auto_clone, environment name, and SDD storage mode. The Workspaces sub-tab joins every registry entry with its claim, PID liveness, pin, last-used time, TTL staleness, and checkout presence. Missing checkouts point to sase workspace repair, and dead claims are warning-styled. Both sub-tabs load off-thread and show cached rows during refresh.

On first open, Repos and Workspaces seed their project filter from the current project when ace.current_project.seed_filters is on. Press p on either inventory to choose all projects, an enabled project (●), or a disabled project (○). Explicitly selecting a disabled project is how its repos/workspaces become visible. / then filters within that project scope; Esc clears the scope and the cleared filter sticks for the rest of the session. The picker is filterable by display name, canonical key, or state and shows repo/workspace counts for each project.

e suspends sase's TUI, opens the selected ProjectSpec in $EDITOR (falling back to nvim), holds the ProjectSpec edit lock for the editor session, then reloads project records. In this panel, Ctrl+D asks for confirmation before deleting the entire SASE project directory: ProjectSpecs, project-local config, artifacts, and related state under ~/.sase/projects/<project>/. Deletion is refused while the project still has RUNNING claims or live artifact markers. It does not delete workspace checkouts, and system-managed projects such as home are excluded from the panel.

Statistics Tab

Open the SASE Admin Center with #, then press 6 or switch to Statistics. Its eight sub-tabs summarize overview, runners, projects, providers, agent activity, macro usage, plan/question activity, and performance for the selected time range. The strip is numbered 01 Overview · 02 Runners · 03 Projects · 04 Providers · 05 Activity · 06 Macros · 07 Plans & Questions · 08 Perf; press 0 and then the second digit to jump straight to a view. The Admin Center-wide ' entry-jump key arms this same numbered-view selection instead of painting row hints — Statistics has no row cursor, so the already visible strip numbers act as its jump hints; Esc or any non-digit cancels. Use [ / ] to move between views, t / T to cycle time ranges, c to enter a custom range, p / P to cycle project scope, Ctrl+D / Ctrl+U to scroll, r to refresh, and ? for the pane's key help. First open seeds the project filter from the current project when ace.current_project.seed_filters is on; p / P can always cycle away from that seed, including back to All projects. On Overview, Agents Run, Success Rate, and Commits open Projects; Plans Proposed and Questions open Plans & Questions.

The Perf sub-tab combines five headline measures—Startup (median visible-ready), Stalls (stall count, with hitches named separately in the tile's detail line), Launch (p95 total launch time), Agent p95, and LLM p95—with startup stages, stall/hitch events, grouped latency and reliability, and source-coverage diagnostics. Press g to group latency by subsystem, provider, or workflow; the grouping also decides what the count column counts (LLM invocations, agent runs, or an ungrouped count) and whether a Share column applies. Perf counts come from telemetry and TUI logs, not the artifact index, so they are not comparable with the run counts on the other sub-tabs. Perf is global: the project chip remains visible but is marked not applied. See Reading the Admin Center Perf view for data sources, retention, and probe details.

The Statistics Macros sub-tab reports macros referenced by agent launch prompts:

  • By Usage ranks macros by runs and shows references, share, agents, success, runtime, and recency.
  • By Model breaks each macro down by model.
  • By Project breaks it down by project.
  • Used With shows macros referenced together in the same run.

Press g to cycle those four groupings without reloading the underlying statistics. Press x to choose one macro and replace the ranking with its full time, model, project, provider, tribe, and co-usage breakdown; press X to clear that focus. The range and project filters apply before all macro aggregation.

These counts come from each run's launch-boundary macros.json, recorded before prompt expansion. A run counts once per macro name, while Refs counts distinct argument variants of the same name separately. References introduced inside workflow step templates are intentionally excluded. Historical runs appear after the agent-artifact index rebuilds at the current schema.

Each record carries a kind — workflow, part, or swarm. A macro swarm is consumed by the dispatcher before any agent starts, so it never appears as a lexical reference in a child's prompt; instead the swarm is attributed to every agent it launched, and a nested swarm records every link of its chain. Because swarm records carry no arguments, Refs equals Runs for a swarm row. Attribution is forward-only: runs launched before this feature shipped are not backfilled.

This Statistics sub-tab is distinct from Config's Macros child described in Macro Browser: that child browses and edits macro definitions, while the Statistics sub-tab measures how launch prompts used them.

Tools Tab

Open the SASE Admin Center with #, then press 7 or switch to Tools. Its three sub-views cover Runs (silent runs pinned first in red, then live, then settled newest first), Failures (failure-signature groups with run and agent witness counts), and Catalog (the current project's named tools with LAST and TYPICAL from the run ledger). The pane is filtered to the current project by default; press A to widen it to all projects. Use [ / ] to move between views, / to filter Runs with tool:, state:, agent:, and verdict: tokens, enter to focus detail (Failures lists the group's affected runs newest first with their agents), a to jump to the run's ⚒ Runs block on the owning Agents row, v to open the retained log in the pager, y to copy the run id, s to stop the focused live run (same DANGER confirm as the Agents tab), r to run the focused catalog tool at the current project root, and R to reload. The Runs detail reuses the Agents tab Runs block renderer. The pane never settles or reconciles runs; the only writes it offers are the explicit stop and catalog-run flows. The top-bar tools: group opens this pane with all projects, so the pane never shows fewer runs than the chip claims.

Launch Control

Press ,m from any tab to open Launch Control — one keyboard-driven surface for launch configuration, model aliases, temporary provider routing, and launching an interactive agent CLI in a new tmux window. The top level has three visible sections: Launch settings, Built-in size aliases, and Your aliases. Consecutive visible sections are separated by exactly one non-selectable blank row; there is no leading, trailing, or doubled spacer.

Data rows use the grid ownership gutter | name | value/model | state. The former row kind column is gone: labels such as launch, setting, role, user, and bucket do not appear as their own column. User-owned aliases and buckets still have the tan ▌ ownership gutter, misplaced built-in aliases keep a gold ! marker in the name cell, and collapsed buckets put ▸ directly before the bucket name.

Launch settings contains six rows: default model, epic lander, big epic lander, big epic starts at, default effort, and max runners. The three model rows show raw alias/config value → effective provider/model. big epic starts at shows the effective bead.big_epic_phase_threshold as <N> phase or <N> phases: epics with N or more authored phases use the big epic lander, while smaller epics use the regular epic lander. default effort and max runners show their launch-effective scalar values and any active temporary override state.

Alias rows show the alias name, effective provider/model as a provider-themed badge, and a state tag — configured, implicit / implicit → @<fallback> / implicit → @<fallback> @ <effort>, or an override · <time> left / override · until cleared chip when a temporary override is active. Configured references use the same configured → @<target> @ <effort> form. A model-specific effort carried by an override appears beside the effective provider/model badge. When one or more providers have temporary routing state, the title adds a compact line for active priority and/or disables. Priority renders as priority: CODEX ★ <time>, hard disables as disabled providers: CODEX <time>, and soft disables as CLAUDE soft <time>.

The alias area is split into Built-in size aliases and Your aliases. Each header reports the aliases represented by its rows (including members of collapsed custom buckets) and its bucket count. Built-in size aliases always lists exactly five rows in size order — @xsmall, @small, @medium, @large, @xlarge — with no bucket to drill into; each row is edited, overridden, reset, or cleared directly like any other alias row. Your aliases holds only user-owned aliases and custom buckets, in alphabetical order. If there are no custom aliases or buckets, Your aliases remains visible with a non-selectable hint naming llm_provider.model_aliases.custom.

Custom buckets group your own aliases under a shared display name, either through model_aliases.buckets.<name> or a custom alias's bucket: tag; there is no built-in bucket for a custom alias to join, so every bucket that appears is entirely user-owned. Each collapsed bucket row reports the member count and active overrides, while the description strip summarizes distinct effective models. Open a bucket with l, Right, or Enter; return with h or Left. Inside the bucket, each alias keeps its own configured/implicit state and can be edited, reset, overridden, or cleared independently. A configured description under model_aliases.buckets.<name> replaces the default. A custom bucket renders its bucket state in the ownership accent, and the drilled-in title ends with · custom bucket plus the ownership glyph.

The two-line strip below the list explains the highlighted row. Launch settings show their config path, effective value, and boundary/override context. Builtin aliases use fixed descriptions. User aliases use llm_provider.model_aliases.custom.<name>.description; a malformed user alias without one shows that config path as the fix. A non-pool alias with an explicit effort uses the second line to say whether it matches or overrides the configured default. For a selector-valued alias, the strip lists every parsed member with an available/unavailable marker. A round-robin pool's row state includes an availability count such as pool 2/2 that counts only pool members (a last-resort tail does not inflate the denominator), and → marks the exact next peeked selection without advancing its cursor — including when the tail is selected. After the last pool member, a fallback: separator introduces last-resort rows. An ordered fallback labels candidates in priority order, marks the current winner, and never reads rotation state. The row's provider/model/effort badge is derived from that same selected member. Temporarily hard-disabled providers count as unavailable for this display and render as a red ×. Soft-disabled members stay selectable and still count in pool <available>/<total>: a selected one renders as amber → ✓ provider/model@effort, and a spared one as amber × provider/model@effort, with no trailing soft chip. Temporary provider priority uses distinct wording: the preferred member is labeled priority, and other usable members that remain behind it are labeled backup. If a temporary alias override targets a hard-disabled provider, the override is preserved but paused: the row shows the live fallback/pool target, the state tag says the override is paused, and the description names the disabled provider that must expire or be re-enabled before the override resumes. An active override whose provider remains available, including a soft-disabled one, still bypasses selector choice for the override's lifetime.

If a builtin size alias is mistakenly configured under llm_provider.model_aliases.custom, opening the panel emits one warning toast listing every affected @alias. A gold warning glyph remains on the affected alias row even while a temporary override is active, and highlighting it replaces the normal description with the same actionable advice. Move the entry's model value from llm_provider.model_aliases.custom to llm_provider.model_aliases.builtin; sase's TUI identifies the misplaced entry but does not rewrite the configuration automatically. Because ownership follows the alias kind rather than where it is configured, the misplaced alias stays in the Built-in size aliases section — never inside a custom bucket — and does not receive the ownership gutter.

Navigate with j/k (or arrows / Ctrl+N / Ctrl+P) and act on the highlighted row. Navigation, and jump hints, skip headers, spacer rows, and the empty-custom hint.

Key Action
l / Right / Enter Open the highlighted bucket
h / Left Back to the top level from an open bucket
' Jump — paint adaptive hints; a hint moves the highlight without activating the row, and a second ' jumps back
o Override — set/change a time-bound temporary alias/default-effort/runner-limit override
x Clear — remove the active temporary override on the highlighted override-capable row
e Edit — change the persistent configured value
r Reset — unset an alias/model setting or the big-epic threshold
p Providers — disable, prioritize, or re-enable registered providers for future routing
u Usage — inspect cached provider subscription usage and request a bounded update
t tmux Agent — launch an interactive agent CLI in a new tmux window
H History — view recorded prior runs for the highlighted alias, alias-backed launch setting, or bucket
Ctrl+E Effort — persistently edit, temporarily override, or clear the global default effort
Ctrl+R Limit — persistently edit, temporarily override, or clear the global runner limit
Esc / q Close the panel

Providers · Usage

Press u in Launch Control to open the read-only Providers · Usage view. The same view is available from the global command palette as Open Providers · Usage, with search aliases including usage, quota, limits, capacity, subscription, and providers.

The first paint reads only the local usage cache. Each provider row shows remaining capacity or collection status, applicable account scope, freshness, and an Updating… marker when a durable refresh is live. Select a provider for its plan, account mode, collection status, last observation, and per-window remaining percentage, reset, age, state, and source. The layout drops the meter and combines detail columns as the terminal narrows.

Rows reserve the orange ⚠ failing badge for collectors with three or more consecutive probe failures. The selected provider detail shows collector health for both degraded and failing states, including the failure streak, failing-since age when known, and last successful probe age when known. This health describes the collector pipeline only: stale-but-numeric allowance data can still render, passive stream observations do not hide a dead probe pipeline, and the indicator never changes launch routing.

The application header groups independently selected usage windows by provider on the right of the existing title row, for example:

🎭 62% 3d4h · fable 0% 1d8h  🤖 81% 5d2h

The title is centered on the header line. Usage occupies a right-docked reserve sized so the complete title still fits centered; changing usage text never moves the title, and the navigation and routing controls on the row below stay put. The complete title has priority: sase's TUI never truncates it merely to show more usage, and a clipped title keeps the full string in its tooltip.

The rendered segment owns one quiet space before the first provider icon and one after the block. Each provider icon appears once, followed by one space, and providers are separated by two quiet spaces with no inter-provider punctuation. Windows of the same provider are joined with a middle dot (·). A single visible window has no extra separator even when hidden windows remain.

The default weekly all-model window renders as <remaining-percent> <reset-countdown>; additional selected windows include compact names such as fable, 5h, mo, 5h/fable, or 5h/gemini. Compact names omit redundant weekly and all-model components and drop the family: prefix of family-scoped windows (5h/3p, not 5h/family:3p) while retaining model/family distinctions; tooltips (scope: family: 3p) and sase usage list (scope=family:3p) still show the session scope; scope? means the provider did not expose exact applicability. The name, percentage, and reset countdown share the window's ten-step remaining-capacity color, from red (nearly exhausted) to blue (nearly full), except an exact 0% highlights the window's whole value run — its name, percentage, and reset countdown together — with the inverted red style. Provider icons, middle dots, provider gaps, and outer padding keep their normal surfaces; a non-zero window's name and rejected marker do too. Structural punctuation is neutral, normal weight in both themes. Which windows appear, and at what remaining percentage, is fully configurable through llm_provider.usage_metrics.indicator.

Stale or unknown-age numeric observations render with neutral text and disclose their freshness in the tooltip instead of adding a visible marker. <1% remains a low but nonzero reading on the normal badge surface. ?% 0h0m↻ means the window's reset has passed and sase's TUI is awaiting a new observation, retaining the last known percentage only in the tooltip. A bare ? in the countdown position means the provider never reported a reset time. ! marks a vendor-rejected window that still shows remaining capacity. An exact 0% window never shows it, because the inverted red run already signals exhaustion, so every provider's exhausted window looks the same whether or not its vendor reports a rejection. Collector failures no longer add a header warning glyph; selected failing windows keep collector-failure prose in the tooltip, and model picker hints keep their own separate ⚠ usage failing identity.

Provider groups always render in provider-name order. Within a provider, the default weekly all-model window is first, followed by additional windows by window key. Attention is shown only through color and markers; it never changes order. sase's TUI shows the longest prefix of complete windows that fits the measured header remainder after the icon and unclipped title, allowing the final provider group to be partially visible, before adding a +N overflow count. If no complete window prefix plus disclosure fits, sase's TUI falls back through usage N, N, and …, relaxing the text-only padding at one-cell boundaries. Every selected and overflowed window's full identity, exact key, precise percentage, scope, effective display policy, and reset timestamp are in the tooltip. Clicking the usage cluster — including a window, +N, usage N, a bare count, or … — opens Providers · Usage at the first displayed provider and does not expand the header. Clicking the routing pill still opens Config > Launch. The Usage command is also reachable from Launch Control's u and the command palette when there is no display space at all.

Press u inside the view to submit or join bounded refresh work for eligible providers. The modal stays responsive, reattaches to an in-flight refresh when reopened, reloads the cache as operations settle, and reports partial failures by provider. Enter focuses the detail table, Tab moves focus, j/k navigate, and Esc/q closes. Collection requires llm_provider.usage_metrics.enabled; see Subscription Usage.

On big epic starts at, Enter and e open a focused positive-integer editor, and r previews a reset. The input accepts an unsigned base-10 whole number with minimum 1 and no spaces, signs, floats, or booleans; the inline constraint reads minimum 1 · package default 5. The preview targets the writable user-base sase.yml or its chezmoi source at bead.big_epic_phase_threshold. Reset uses an unset operation so lower-precedence or package defaults resume; it does not write a literal 5. There is no temporary threshold override. Pressing o or x on that row warns and points back to Edit/Reset.

After a successful threshold write, Launch Control reloads the effective value and the epic-lander row descriptions from the same provider/launch snapshot. If a higher-precedence layer keeps the requested user value from winning, the notification reports the actual effective value and the requested value. Dirty Git-backed targets receive the standard tracked commit/pull/push offer with chore: update big epic phase threshold.

Default effort controls

Ctrl+E works from every alias and bucket row because default effort is global. The Default Effort card shows the exact value used for new launches and, while a temporary override is active, the configured value beneath it. Press e to edit permanently, o to override temporarily, or x to clear the active temporary override; x is shown only when there is something to clear. Explicit prompt effort and effort carried by an alias or selected pool member still win, and already-running agents are unaffected.

Both Edit and Override open the same ordered, single-key effort ladder: 1 none, 2 minimal, 3 low, 4 medium, 5 high, 6 xhigh, and 7 max. Edit also offers 0 Provider default. That option writes the schema's empty sentinel into the user-base sase.yml, deliberately masking a lower-precedence package/plugin value; Override does not offer a pseudo-level, because cancelling or clearing honestly resumes configured/provider behavior. Config-derived values are best-effort: a provider that cannot honor a level retains its provider behavior.

Temporary effort Override reuses the model-alias duration workflow unchanged: 15m, 30m, 1h, 2h, 4h, Until cleared, a combined custom duration, or t for an exact local time/date. It is machine-wide state in ~/.sase/llm_effort_override.json; setting replaces the prior effort override, expiry is enforced on the next launch, and Clear is idempotent. This state is independent of ~/.sase/llm_override.json, which stores concrete model-alias overrides.

Permanent Edit always targets the writable user base config rather than a project-local layer. Its preview shows llm_provider.default_effort, configured before/after values, the actual target, validation, and the source-preserving YAML diff. With use_chezmoi: true, the actual write goes to the chezmoi source and the target is applied before sase's TUI reports success. A dirty Git-backed target receives the usual tracked commit/pull/push offer with chore: update default model effort. An active temporary override remains launch-effective after this write until it expires or is cleared; the preview and success notification both make that explicit.

Runner capacity controls

Ctrl+R is a fixed Launch Control binding and works from every alias, collapsed bucket, and open bucket. It is not a leader-keymap setting. The Runner Capacity card shows the current effective capacity budget and, while a temporary override is active, its remaining time plus the configured value. Press e to edit the user-base configuration, o to set a temporary machine-wide override, or x to clear an active override.

Edit and Override open a focused positive-integer card. Edit is prefilled with the configured value; Override is prefilled with the current effective value. The input accepts an unsigned base-10 whole number with minimum 1 and no product maximum. A persistent edit previews the exact max_running_agents path, actual user or chezmoi source target, configured before/after values, validation diagnostics, and source-preserving YAML diff. Its tracked commit offer uses chore: update max running agents. A higher-precedence overlay can keep the configured effective value different from the requested user-layer value, which the reload notification reports truthfully.

Temporary Limit overrides reuse the same relative/custom/exact-time duration cards as model and effort overrides. The versioned machine-wide record is ~/.sase/max_running_agents_override.json; setting a value replaces the previous runner-limit override, now >= expires_at expires it, and Clear is idempotent. A persistent edit does not clear a live temporary override. Lowering the effective cap never stops an already-running agent: occupied capacity may temporarily exceed the cap, and new work waits until enough capacity drains. Raising the cap lets eligible parked agents advance through the existing priority/FIFO gate on their next poll. Launches with an explicit %queue(capacity=N) use that positive-integer budget for their own admission decision, so they can intentionally start above the global budget while still holding an ordinary weighted claim once admitted. Question continuations reacquire against the current effective global cap after their gate turn has released capacity.

Provider routing controls

Press p from Launch Control to open Provider Routing. The modal lists every user-facing registered LLM provider in stable order with its model count and one of these states:

State Meaning
available The provider is registered and its declared CLI is present.
CLI missing Automatic alias routing already skips it because its CLI is missing.
disabled · manual · <time> left Launch Control manually hard-disabled it until expiry or clearing.
disabled · usage-limit automatic · <time> left Usage-limit detection automatically hard-disabled it.
soft · manual · <time> left Launch Control manually soft-disabled it until expiry or clearing.
soft · usage-limit automatic · <time> left A usage-limit automatic disable that was flipped to soft.
★ priority · <time> left This provider is preferred in pools that include it.
backup · CODEX priority This provider remains usable behind the active priority provider.
CLI missing · ★ priority unavailable Priority intent remains, but routing uses backups until it can run.

Hidden testing providers stay out of this human-facing modal. Disabling a provider does not unregister it, change sase.yml, change model aliases, or stop provider processes that are already running. A hard disable (d / Enter) keeps today's fail-closed behavior: new launches, follow-ups, later retry/fallback resolution, model pickers, and %model completion drop that provider. A soft disable (s) spares the provider in | pools while another member can cover, never diverts a || fallback, and still accepts explicit %model / picker / completion choices for that provider. Provider priority (p inside the modal) is temporary preference state, not a disable: pools prefer that provider when it is a usable member, while explicit choices and || fallback order continue to do what they say.

On any row, press d or Enter for a hard disable or s for a soft disable, then choose how long. The duration picker is the same set of choices as alias overrides: 15m, 30m, 1h, 2h, 4h, Until cleared, a custom duration, or t for an exact local time/date. When the highlighted row already has an active disable in the other mode, the picker's first row is x Keep current window (<time> left) — one keypress to flip hard ↔ soft without re-choosing a window. That row is omitted when the mode already matches, so it is never a no-op. On a disabled row, x enables the provider immediately, including an automatic usage-limit disable. Pressing x on an enabled row warns without mutating state. Successful changes refresh the provider rows, Launch Control title, alias routing rows, and the top-bar indicators without closing the modal, so several providers can be managed in one pass. Unknown disable sources are shown as readable labels instead of being folded into the manual state.

On an enabled, installed, user-facing row, press p to set or change provider priority, then choose the same relative, until-cleared, custom, or exact-time duration used by provider disables. Press c to clear the active priority, even when no selectable provider rows remain. The modal keeps the current selection after writes and refreshes the rows in place. If another session changed priority between the modal's snapshot and the attempted write, the modal reloads the current state and asks you to press p or c again against the fresh snapshot. If a write commits but the follow-up refresh fails, the toast says the routing write succeeded and leaves the modal open for retry.

A sparing (soft) pool member still counts toward pool <available>/<total> and remains selectable. In the alias description it renders as amber → ✓ provider/model@effort when selected or amber × provider/model@effort when skipped, with no soft chip. Priority members render with priority and backup members with backup, including in the guided selector builder, model picker, and %model completion. Soft-disabled providers stay in the model picker (header labelled soft, rows dimmed one step) and in %model completion (annotated soft in the provenance column). Hard-disabled providers are still omitted from both.

sase's TUI also shows active provider routing state as pills inside the violet overrides: top-bar group (the launch-default and current-project chips moved one row down, to each tab's status-row launch-context cluster). An active priority renders like overrides: CODEX ★ 42m (with a state word only for soft-disabled or unavailable, such as overrides: CODEX ★ soft-disabled 42m). One hard-disabled provider renders like overrides: CLAUDE off 42m; one soft-disabled provider renders like overrides: CLAUDE soft 42m. Several disables render the most severe (hard first) provider plus a count, such as overrides: CLAUDE +2, and use the soft palette only when every active disable is soft. When alias, priority, and disable facts are active their pills sit side by side under the one overrides: label. Each pill keeps its color and its tooltip section, and clicking the group opens Launch Control.

Disabled-provider launch panel

When sase's TUI is about to launch an agent that can only run on a hard-disabled provider, it opens a one-keypress panel instead of submitting a launch that would fail at invoke time. Soft disables never open this panel. After the instant input and project-tag checks pass, the prompt bar is released and the accepted launch appears as a preparing proc row while the provider check runs. Aborting restores the prompt when the bar and screen are free; if the user is already typing or another modal owns focus, it saves the prompt to the stash instead and never steals focus.

The panel is one blocked agent at a time. A four-agent swarm with two blocked units shows the panel twice in sequence. Enabling a provider while resolving one agent can unblock a later one, so sase's TUI re-checks after every write.

Key When it appears Action
e Always Enable every provider blocking this agent, then re-check and continue the launch.
s Always Soft-enable those providers (same remaining window) so this agent can run while pools still spare them.
1…9 Only when two or more providers block this agent Enable that one provider and re-check. The panel stays up if another blocker remains.
m Only when every slot of this agent agrees on one model Open the model picker and rewrite this agent's %model. A fan-out unit shows a dim edit-the-prompt line instead.
a Always Abort this agent. The other agents in the launch still start.
A Only when the launch has more than one agent Abort the whole launch. The label states the real agent total.
esc/q Always Same as a.

A single-agent launch that only enables or soft-enables submits the original prompt. Picking a different model on a one-agent launch rewrites that prompt. Dropping or re-modelling a unit in a multi-agent launch submits only the agents that remain.

Automatic provider drain

After a manual hard disable in Provider Routing (p from Launch Control, above) newly hard-disables a provider, sase's TUI automatically relaunches the agents that disable just stranded. No prompt is shown. The drain is submitted only when all of these hold:

  • The write was a hard disable (a soft disable never drains — it strands nothing).
  • The write transitioned the provider into hard-disabled (no disable → hard, soft → hard, or an expired disable → hard). Re-hard-disabling or extending an already hard-disabled provider never re-drains; use sase agent drain <provider> for that case.
  • The provider_drain beta flag is enabled.

The disable write returns immediately with the usual "disabled" toast, then a Draining <PROVIDER> start toast follows. The drain runs as the durable agent.drain proc (sase agent drain <provider> --yes --json) with the shared provider-drain:<provider> concurrency key the automatic usage-limit path uses (see Draining a Disabled Provider), so it appears in the Procs tab and dedups against an automatic drain already in flight for that provider rather than running twice. A completion toast summarizes the durable result envelope, and the Procs row holds the full JSON envelope. If the TUI exits first, the proc still finishes and its result stays in Procs / sase proc show.

An empty or no-longer-disabled automatic drain records a successful no-op: when the drain finds nothing to relaunch (nothing_to_drain) or the provider is no longer hard-disabled by the time the proc plans (not_disabled, soft_disabled), the proc succeeds with exit 0 and the completion toast says nothing was drained.

Draining onto a chosen model is still available as sase agent drain <provider> -m <model>.

tmux Agent

Press t from Launch Control to open tmux Agent. The panel lists every registered provider that declares an interactive CLI, in shortcut-key order, and launches the chosen one in a new tmux window in the current pane's directory. The same catalog and launch engine back sase tmux-agent on the command line; only the presentation differs. These windows are unmanaged agent CLIs, not SASE agents — they do not appear in sase agent list.

If sase's TUI is not running inside tmux, t warns sase's TUI is not running inside tmux; start sase's TUI in a tmux window to launch agent CLIs. and does not open the panel.

Each row is <key> ● <display name> <vendor> <state>: the key in the selector accent, the bullet and display name in the provider's accent color, the vendor dim, and the state one of:

State Meaning
ready The CLI is installed and launchable.
not installed The row is dim and unselectable; the description strip shows the install hint.
routing disabled · <time> left Hard-disabled for SASE automatic routing, but still launchable from here.
soft · <time> left Soft-disabled for SASE routing; still launchable from here.

A disabled-for-routing provider stays selectable because a human choosing an agent CLI is not automatic routing. The annotation is visible, not a wall.

The two-line description strip shows the exact command that will run, then the preview window name (ai, ai2, ai3, …) plus effort and bypass state. A skipped config-default effort is named rather than swallowed. Bypass is on by default (matching the shell script this feature replaces) and is always visible in the strip; it is never applied invisibly.

Key Action
Enter / the row's assigned key Launch the highlighted (or keyed) agent CLI
s Launch once without the provider's approval-bypass args
j/k (arrows, Ctrl+N/Ctrl+P) Navigate; not-installed rows are skipped
Esc Close and return to Launch Control
q Close, but only when no provider claimed q (Qwen keeps q; the title and footer say so)

A successful launch notifies Opened tmux window: <name> · <display name> and dismisses back to Launch Control. A failure notifies with an error and leaves the panel open.

See sase tmux-agent and the tmux_agent config block for the CLI surface, the drop-in tmux binding, and the documented parity recipe against the script this feature replaces.

Alias History

Press H on an alias row, an alias-backed launch setting (default model, epic lander, big epic lander when configured as a raw @alias reference), or a collapsed bucket to open Alias History — bounded prior runs for that alias or, for a bucket, every member alias. A concrete (non-alias) launch setting and the default effort, max runners, and big epic starts at scalar settings are not aliases; pressing H on one of those rows only shows a warning toast. The panel loads off-thread and never changes Launch Control's own state.

The title names the alias or bucket, keeps the tan ownership accent for a user-owned source, shows the effective provider/model/effort badge when a single alias supplied it, and reports the total recorded, returned (currently visible), and done/failed/running counts. A bucket's runs are grouped under a disabled header per member alias, separated by the same single-spacer convention used elsewhere in Launch Control; headers, spacers, and per-group empty hints are never jump targets. Rows render newest first exactly as recorded, with a status marker, relative time, agent/workflow identity, the configured project display name, a provider-themed model badge with effort, and one of four provenance chips:

Chip Meaning
direct An explicit %model directive named this alias.
default The configured default model resolved to this alias.
via @<...> This alias was reached indirectly, through an earlier alias in the chain.
unrecorded No alias origin was captured for this run — recording predates it.

The fixed detail strip below the list explains the highlighted run: the recorded alias trail resolved to its concrete provider/model/effort, the same origin explanation as the chip (an honest, non-speculative note for unrecorded rather than a guessed reason), and the prompt snippet plus whichever of project, workspace, bead, Patch, start time and duration, retry attempt, hidden state, and macro context are actually present — nothing is invented for a field the query did not return. The returned window is a display limit, not the full retention history; the title's recorded/shown counts and the footer's "more available" hint make the difference visible.

Below the detail strip, a compact model-usage region ranks the models the currently shown runs actually used. Counts cover the loaded window only, after deduplicating the same artifact directory when a bucket lists one run under more than one member alias; a dim (deduped) marker appears when that filter removed rows. Bar color follows the same provider palette as the PROVIDER(model) badges above. Configured selector members with no runs in the window render as unused; a model that ran but is not in the current pool is tagged off-pool; runs with no recorded model collapse to one unrecorded row. When every counted run for a model shares one effort the row shows @ <effort>; mixed efforts show a dim @ mixed. At most four rows render; a +N more overflow row carries the leftover count and percent so the shown shares still total 100%. Ctrl+J, Ctrl+K, r, and . recompute the strip with the new window; j/k never do.

Key Action
j/k (arrows, Ctrl+N/Ctrl+P) Navigate
' Jump — adaptive hints over selectable runs only
Enter Open the highlighted run's full prompt in the preview panel
y Copy the highlighted run's durable @agent:... reference
Ctrl+J Load more — add 10 runs to the per-alias limit and reload, keeping the highlighted run
Ctrl+K Unload — subtract 10 runs, never dropping below the initial model_alias_history_limit window
r Refresh — revalidate this load, bypassing the cache once
. Toggle hidden runs in or out of the results
Esc / q Close and return to Launch Control, unchanged

A run without a durable agent name warns instead of copying a guessed reference, and a missing or unreadable raw_macros.md warns instead of closing the panel.

Temporary overrides

Edit and Override open the shared model picker with an ALIASES group before the provider-grouped concrete models. Alias rows show the exact @name token and its current effective provider/model; filter by either @medium or medium, an alias kind or description, or the displayed target. For persistent edits, the current alias and any alias that would introduce a direct or transitive cycle remain visible but unavailable with a concise reason. Custom... accepts a concrete model string, provider/model path, or bare @alias reference in both flows and applies the same safety check to free-form @alias values. Concrete model rows for hard-disabled providers are omitted; soft-disabled providers stay in the picker (header labelled soft, rows dimmed one step), priority providers are labeled priority, and priority backups are labeled backup. Alias rows remain visible and show their current live fallback target. Free-form explicit input is validated before submission and reports the same disabled-provider diagnostic as a launch. In Edit, Custom... additionally accepts a typed | pool or || fallback expression and opens prefilled with the alias's current value, so changing one member of an existing selector no longer means retyping the whole expression; Edit also offers a guided Pool / fallback... row next to Custom... that builds a selector from the picker without typing | by hand (see Persistent edits below). In Override, a typed pool or fallback is refused outright with a message pointing at e — selectors are config-only and overrides take a single target, so Override's Custom... never shows the Pool / fallback... row.

Override continues from the picker to the duration picker (15m, 30m, 1h, 2h, 4h, Until cleared, or a custom duration like 45m, 1h30m, 90m). Press t in the duration picker to choose Until a specific time. The focused time popup accepts local forms such as 5pm, 5:30 PM, 17:30, 1730, today 5pm, tomorrow 9am, and 2026-07-12 09:00. An undated clock means its next occurrence (later today or tomorrow); an explicit day/date must still be in the future.

The popup previews the resolved weekday/date, local time and abbreviation, configured IANA timezone, and remaining duration before Enter writes anything. Daylight-saving gaps are rejected; repeated fall-back times require an offset-qualified ISO value such as 2026-11-01T01:30-04:00. Invalid input stays focused with an inline explanation. Esc goes back to the duration picker, where a second Esc cancels the override flow. Overrides are per-alias and per-launch-setting, and independent:

  • An override on default model drives the no-%model launch default. It renders in a gold status-row pill as PROVIDER(model)[@<effort>] <time-left> — the model half of the launch-context cluster at the far right of each tab's status row.
  • An override on any built-in size alias or custom alias takes effect wherever that alias is resolved. A size-specific phase or task override affects only that alias. An override on a selector-valued alias — a | load-balanced pool, || ordered fallback, or parenthesized (A | B) || C last-resort, such as the shipped size-alias selectors (see the generated shipped size-alias defaults) — suspends that alias's own rotation/fallback for a single concrete target until the override expires or is cleared.
  • An override on epic lander or big epic lander affects only epic land agents below, or at/above, bead.big_epic_phase_threshold, independently of default model and of each other.

Every non-default override (an alias, epic lander, or big epic lander) is surfaced by the alias pill inside the labeled overrides: violet top-bar group: a single active override renders as overrides: @<alias>[@<effort>] <time-left> or as overrides: epic lander <time-left> / overrides: big epic lander <time-left>, and several render as overrides: <first> +N, naming the alphabetically first overridden alias or launch-setting label and counting the rest. That alias pill sits beside the priority and disable pills when those are active. In both pills, lane color carries the "override" meaning while the effort suffix and time use a recessive tone; ∞ means until cleared. Hover the group for full target and expiry details, or click it to open Launch Control.

When no override is active, the same status-row chip instead names the current launch default — the smallest %model value that would pin this exact target (a bare model name such as grok-4.7 when the model unambiguously names its provider, otherwise the explicit codex/o3 form) plus the optional [@<effort>] suffix, with no background accent — and stays live for the whole sase's TUI session. The label is toned with the launch default's provider: the model name takes the provider's model hue (the same one the model picker uses, for example Grok cyan, Claude amber, or Codex mint) and the @<effort> suffix takes a recessive tone from the same hue family, so the two-tone shape matches the override pills. Provider hue is the calm lane's identity axis, while the override lanes keep their gold and violet accents for the override meaning. While the default is still resolving (...) or unavailable, the pill keeps a neutral dim-cyan tone rather than guessing a provider. A model alias is never used as the pill's subject, since an alias may be a rotating pool rather than one concrete model. The optional @<effort> suffix is the launch-effective default a no-%model / no-%effort prompt will actually receive (alias-borne effort, a temporary default-effort override, or llm_provider.default_effort); it is omitted when that value is unset. If llm_provider.default_model (directly or through a referenced alias, such as a shipped size-alias pool) resolves to a load-balanced | pool, the pill follows the pool's round-robin cursor as it advances: a launch consumes one member, and within a few seconds the pill flips to name whichever member runs next. For a cross-provider | pool the pill's hue flips with it, so the color tells you which provider the next launch will actually hit. It never resolves on the UI thread and never advances the cursor itself — it only reflects state that a real launch already changed. Hover the pill for a <alias> rotates across N models; PROVIDER(model) is next line whenever the default routes through such a pool. The tooltip deliberately keeps the provider-qualified PROVIDER(model) form: the pill is the width-constrained surface and stays compact, and color is never the only carrier of the provider, so hovering is the authoritative confirmation of which provider the launch default resolves to.

Overrides do not displace explicit launch intent: explicit prompt directives (%model:codex/o3, %model:opencode/anthropic/claude-sonnet-4-5) and an explicit provider_name argument always win, already-running agents keep their current provider/model. A temporary hard provider disable is different: an explicit request for a hard-disabled provider fails with a provider-and-expiry diagnostic rather than silently switching providers. A soft disable never fails that explicit request. Override state is persisted to ~/.sase/llm_override.json — shared across all sase processes on the machine — and is best-effort self-cleaning: expired or malformed entries are pruned on next read. Until cleared is a no-expiry mode — convenient, but still a temporary state, not a permanent config edit. The temporary override is independent of SASE_MODEL_TIER_OVERRIDE; a concrete override takes the full provider/model path, while the tier override only applies when no concrete override is active.

Delegated launches (plan coder follow-ups and sase bead work phase, task, and land agents) route directly through the built-in size aliases configured under llm_provider.model_aliases.builtin, plus the epic lander/big epic lander launch settings — there is no more role-alias indirection through a separate default/smart/cheap/etc. lane. A phase or task selects @xsmall, @small, @medium, @large, or @xlarge by its normalized size directly; a phase or task without size metadata uses the @small fallback. Epic land agents without an explicit land model use epic_lander_model (shipped @large) below bead.big_epic_phase_threshold, or big_epic_lander_model (shipped @xlarge) at or above it. Because epic lander and big epic lander are configured as raw alias references, a temporary override on the alias they reference (@large and @xlarge by default) cascades into their effective resolution; overriding epic lander or big epic lander directly takes precedence over that nested reference. A temporary override on a selector-valued built-in size alias — any shipped size-alias pool or fallback (see the generated shipped size-alias defaults) — suspends only that alias's own rotation/fallback and does not cascade to any other alias or launch setting.

Persistent edits

Edit and Reset change the alias's value in sase.yml itself, written through the Rust-backed, source-preserving config-edit path (comments and key order are preserved). The change is shown in a preview/confirm step before it is written, and after a successful write the panel offers to commit and push it (y/n). With use_chezmoi: true the edit targets the chezmoi source and the commit/push runs against the chezmoi repo followed by chezmoi apply; when the target file is not in a git repo the commit offer is skipped and the file is simply written. An active temporary override visually "wins" the effective-target column even after a persistent edit; the state tag distinguishes the configured value from the currently effective (overridden) one.

Selecting an alias during Edit stores the raw reference (for example, editing epic lander to @xlarge), so it remains a dynamic link and follows future changes to @xlarge. Selecting an alias during Override instead resolves it when the override is written and stores that concrete provider/model snapshot together with the raw token; later changes to the referenced alias do not change the active override. A canonical trailing effort is snapshotted with the target and shown in the row, success notification, and single-override pill (the gold status-row chip for a default model override, the violet top-bar pill otherwise). A known suffix on an alias reference is ignored for dependency/cycle checks but retained for the written value; unknown trailing @token text is not treated as effort.

Builtin aliases edit under llm_provider.model_aliases.builtin.<name>. User aliases under llm_provider.model_aliases.custom.<name> edit their model field and reset by deleting the whole custom alias entry. The custom input also accepts a |-separated load-balanced pool, a ||-separated ordered fallback, or a parenthesized (A | B) || C last-resort. The editor rejects empty or unparenthesized mixed selectors and alias references that would reach any nested selector before opening the write preview.

Choosing Pool / fallback... from the e picker opens a guided builder instead of typing an expression by hand. It seeds its state from the alias's current value — an existing | or || expression expands into its members and mode, a parenthesized (A | B) || C value populates the pool plus last-resort tail, a single target becomes a one-member list, and an empty value starts blank — and shows the live normalized expression as members change. a adds a pool member through the same model picker and effort ladder used elsewhere in the panel (its own Custom... accepts a bare model, provider/model path, or @alias, with an optional trailing @effort); f adds a last-resort candidate the same way; d removes the highlighted member; J / K reorder it down and up without crossing the pool/tail boundary; E sets or clears that member's effort; w / W raise and lower a pool member's weight and ignore last-resort rows; t toggles between round-robin pool and ordered fallback, and refuses while a last-resort tail is present; enter confirms and routes the composed expression to the same preview/write path as a typed value; esc cancels back to the picker. As in the top-level Edit picker, an alias reference that would reach another pool or fallback is unselectable here. Confirm is blocked, with an inline reason, while the selector has fewer than two pool members or the live validation line reports an error.

Examples

  • Highlight default model, o, pick codex/o3, duration 1h — launches with no %model directive use Codex o3 for the next hour, then revert to the configured default.
  • Highlight @medium, o, pick a model, then t, enter 5pm — the preview resolves the next 5:00 PM in the configured timezone and the override expires at that exact instant.
  • Highlight @small, o, pick claude/opus, and choose Until cleared — small phases and tasks without an explicit model use CLAUDE(opus) until you clear it; the violet non-default pill appears in the top bar.
  • Highlight @large, e, pick claude/opus, and confirm — only large phases and tasks without an explicit model use that target, replacing the shipped pool (see the generated shipped size-alias defaults); other-sized phase/task routing is unchanged.
  • Highlight @xlarge, e, pick claude/opus, and confirm — xlarge phases and tasks use that target directly, and big epic lander (left at its shipped @xlarge reference) inherits the same change.
  • Leave @xlarge implicit — xlarge phases, tasks, and threshold-selected epic landers (which reference @xlarge by default) select the first available member of its fallback chain (see the generated shipped size-alias defaults).
  • Highlight @xsmall, e, choose Custom..., enter claude/haiku@minimal | codex/gpt-4.1-mini@low, and confirm — xsmall phases and tasks round-robin across installed providers while the panel continues to show the next selection without consuming it.
  • Highlight @small, e, choose Custom..., enter claude/haiku | codex/gpt-4.1-mini, and confirm — small phases and tasks round-robin across this independent pool without consuming the @xsmall cursor.
  • Highlight @medium, e, choose Custom..., enter claude/haiku@minimal | codex/gpt-4o-mini, and confirm — medium phases and tasks round-robin across this independent pool without consuming the @xsmall or @small cursor.
  • Press t — open tmux Agent, then c or Enter on Claude Code to launch it in a new ai window; s launches once without approval-bypass flags.
  • Press p, highlight claude, d, choose 1h — new alias-backed launches route around Claude for the next hour, direct %model:claude/opus launches fail explicitly, and already-running Claude processes continue.
  • With claude disabled, an override on @medium that targets claude/opus pauses; the row shows the live fallback target until Claude is re-enabled or the disable expires.
  • Highlight big epic lander, e, pick a model, and confirm — only threshold-selected epic landers use that persistent target; leaving it implicit inherits through its shipped @xlarge reference, independently of epic lander.
  • Highlight big epic lander, e, filter for @large, select it, and confirm — the persistent value is the dynamic @large reference, not a copied concrete model.
  • Highlight @medium, press o, select @xlarge, then choose 1h — the override records the concrete provider/model to which @xlarge resolves at write time while retaining @xlarge as its raw input.
  • Highlight an alias or launch setting, x — clear its temporary override; r — unset its configured value back to its implicit fallback.

See docs/llms.md for the resolution order and state-file format.

Notifications Modal

Press i to open the notifications modal. On the Agents tab, Enter opens the selected agent's pending gate or Patch directly, without the modal. See docs/notifications.md for the full keybinding reference, modal tabs, priority/error/muted classification, and the per-notification snooze and mute affordances.

Rows and the detail header begin with the notification's single-glyph icon when one is present, with a per-action fallback icon otherwise. The text action badge remains visible as the secondary label.

Press d on the highlighted inbox row to open Gate Debug, even when the row is not gate-backed or its gate modal can no longer load. The same d binding is available inside plan/epic approval, user-question, launch-approval, custom-gate, and workflow HITL panels. Gate Debug presents Overview, Request, Response, Errors, and raw Row tabs; [ / ] switch tabs, y copies the current tab, Y copies the bundle path, e opens the backing file, and d, q, or Esc closes the overlay without losing state in the underlying panel.

The inbox: top-bar group renders one colored <icon><count> chip per notification-panel tab, in the panel's own order and colors, or a dim inbox: 0 when nothing is pending. See Top-Bar Indicator for the snoozed-only and overflow forms.

Snooze Reminder Scheduling

Snooze expiry does not depend on the general refresh cadence. sase's TUI keeps at most one timer for the nearest deadline reported by the current notification snapshot, so reminders fire on time even with clean inotify state or --refresh-interval 0 (which disables ordinary auto-refresh). The timer callback stays thin and synchronous: it compares cached wall-clock values on Textual's message pump and hands the store read to a coalesced proc, so no disk or worker I/O runs on the pump and an expired snooze never triggers a full Agents-list rebuild.

While any snooze is pending, sase's TUI rechecks the wall clock at most one second apart, so a suspended host, a resumed session, or a forward/backward system-clock change re-evaluates the authoritative UTC deadline promptly instead of waiting out a monotonic timer. Startup reconciliation, notification-file watcher events, ordinary polling, and modal snooze/resnooze/unmute/dismiss completions all route through the same coalescing guard, so an external mutation can replace or cancel the cached nearest deadline immediately. The coordinator starts after first paint and its timer and task are cancelled during normal and controlled teardown.

Once due, sase's TUI performs one current-state snapshot read, applies counts, toasts, and status projections, then schedules the next future deadline. Each observed resurface batch produces toasts (one per row, or grouped per severity when more than three rows arrive together) and at most one tmux bell, unless a delivery rule overrides it — including rows that were marked read while snoozed — and no repeat on later polls. Cancelled, dismissed, permanently muted, and not-yet-due rows never ring. If another process wins the expiry, the persisted unread state and resurfaced_at still make the transition observable here. Resurfaced rows sort as recent activity in the modal while continuing to display their original sent time. See docs/notifications.md for the full state and timing contract.

Notification Actions

Some notifications carry an action field that triggers a handler when the notification is selected. The following notification action types are supported:

Action Source Behavior
CustomGate Agent/tool Opens the generic choices, add-ons, and feedback modal
GateExecutionFailed Gate executor Opens the resume / restart / cancel dialog, or the error report when none applies
HITL Workflow Opens the workflow human-in-the-loop response modal
JumpToAgent Agent/workflow Jumps to the matching Agents-tab row, revealing it first like the Node Finder: expands collapsed folds, grouping banners, and panels; shows I-hidden rows; clears a hiding Agents query with a toast
JumpToPatch Sync/workflow Jumps to the referenced Patch on the Patches sub-tab
JumpToMentorReview Mentors Jumps to the Patch and opens mentor review output when available
LaunchApproval Agent Opens the launch approval modal for an agent-requested launch
OpenToolRun Tool run Selects the settled run's Agents-tab node with its ⚒ Runs card active and the run's block selected, or opens Admin Center → Tools focused on the run
PlanApproval Agent Opens the plan approval modal
RemoteAttention Remote machine Opens the remote question or gate modal and submits to the owning machine
SudoRequest Agent Opens the sudo review modal; approving runs in a terminal
Tmux External bridge Runs tm <workspace-name> for the notification's action_data.workspace_dir
UserQuestion Agent Opens the structured user-question response modal
ViewErrorReport Axe/agent Opens action_data.error_report_path, or the first attached file, in $EDITOR

The axe error_digest job creates ViewErrorReport notifications whose digest files live under ~/.sase/axe/error_digests/digest_<timestamp>.txt; user-agent failures can use the same action for their own attached error reports.

The custom-gate modal shows the sender and notes or verified preview, one icon-led button per terminal choice, and checkboxes for that choice's independently selectable add-on commands. The Decision column holds those controls only; an option that declares typed input wears a dim ✎ n inputs badge and collects those values in the dedicated input panel instead (see Remapping Gate Modal Keys). Required feedback also opens that panel, whose submit stays blocked until non-empty text is present; optional feedback still answers in one keystroke and is attached by pressing i first, and disabled feedback shows no note field at all. Unsupported future actions produce a warning instead of silently doing nothing.

Custom gates and neutral HITL gates execute through the shared hash-verifying gate executor. sase's TUI schedules the terminal command and each selected add-on through the tracked proc queue, streams live stdout/stderr to the proc, shows each command as a reporter phase, and refreshes the inbox when the proc completes. Legacy HITL bundles retain the direct response-file fallback.

Toast Notifications

Each newly-arrived notification produces a short toast in the TUI. The toast text is derived per-action type (plan, question, HITL, axe error, Patch sync, agent update) so the message previews the actual event rather than a generic "N new notification(s)" line. Severity is also picked per type: plans, questions, and HITL render as warnings; axe errors (and sync failures) render as errors; everything else renders as information.

A genuinely new tale or epic plan review rings the terminal bell on arrival (one bell announcement, sent as three short tmux beeps) and remains visually prominent as a warning toast and priority inbox row. Already-answered plan reviews discovered during polling and the post-approval coder or epic handoff stay silent. Questions, other audible notification classes, and explicit snooze-expiry reminders retain their existing bell behavior; snoozing a plan review therefore still produces the requested reminder bell when it expires.

These are defaults. ace.notification_rules can suppress a toast or replace the bell with a sound file or silence for any notification, including snooze-expiry reminders; rows whose toast is suppressed are left out of the grouped-toast count.

When more than 3 notifications arrive in the same poll tick, per-notification toasts are consolidated into one grouped toast per severity bucket (e.g., 2 warnings: 1 plan, 1 question). Ordering is urgency-first: errors, then warnings, then information. Silent notifications are excluded from this pipeline entirely.

Agent completion and failure toasts include the %id-set agent name with an @ prefix when present (e.g., CLAUDE(opus) @sase-q.land completed: ace(run)-...); anonymous agents (no agent_name) keep the prior format.

Macro Browser

Press # on any tab to open SASE Admin Center, press 1 for Config, then choose the Macros child (0 then 7, or [ / ]; within a session, Config reopens on the child you used last). It displays all discovered macros in a two-panel layout: a filterable list on the left and a syntax-highlighted preview on the right. Markdown macros with leading YAML frontmatter render the frontmatter and body with their respective syntax styles.

Macros are grouped by source (project sase/macros/, home ~/sase/macros/, project-specific home, config sase.yml, plugins, built-in, plus labeled legacy compatibility sources). Workflow macros (multi-step YAML) are marked with a gear icon; standalone workflows are displayed with the #!name insertion syntax. Project-local macros defined in each project's sase.yml file are also included, even though the TUI's normal config loading does not read project-local config files.

The list rows and preview metadata show the same insertion form and visible input metadata used by Ctrl+T completion. Step-only inputs are hidden from this user-facing surface because they are supplied by workflow execution rather than typed by the user.

Keybindings

Key Action
j / ↓ Navigate to next macro
k / ↑ Navigate to previous macro
Ctrl+N Navigate to next macro
Ctrl+P Navigate to previous macro
' Jump to a non-header row via adaptive hints
/ Show and focus the filter input
[ / ] Switch to the previous / next Config child
Ctrl+D Scroll preview panel down
Ctrl+U Scroll preview panel up / clear input
Enter Target the highlighted macro: load it into the home prompt bar for editing
E Open the highlighted definition in $EDITOR
Ctrl+O Add a new macro
Ctrl+I Inline-expand the highlighted macro into the home prompt bar
Esc Close SASE Admin Center

The filter input starts hidden. Press / to reveal it, then type to narrow the list in real time; Enter or Esc closes the input and returns to the list. While the input is focused, every printable key — including digits and ' — is ordinary filter text, so values such as bug2 can be typed normally; Ctrl+N / Ctrl+P, Ctrl+D / Ctrl+U, Ctrl+O, and Ctrl+I still reach the list. From the list itself, ' arms entry-jump over the non-header rows.

Editing Macros

Press Enter on any macro to load its definition into the home prompt bar and target it for editing — see Editing an Existing Macro from the TUI for the full targeting loop, including the visual chip states, the target-aware Enter save menu, and the chezmoi-aware write path. Project, home, and config sources are editable and bind the bar to their source file. Read-only sources (legacy, plugin, and built-in) load without a target: the bar shows a persistent read-only marker instead, and gw falls through to the save-as flow so your edits land in a new, editable copy rather than being silently discarded. Press E to open an editable definition directly in $EDITOR instead; after saving, the browser offers the applicable follow-up actions (commit/push, a scoped chezmoi apply, or sase memory init / sase skill init).

Creating Macros

Press Ctrl+O to start the guided creation flow:

  1. Location modal — Choose where to save the new macro (project sase/macros/, home ~/sase/macros/, project sase/sase.yml, or a global config file). Legacy sources remain browseable but are never new-write destinations. Press Ctrl+G to open the selected config file in $EDITOR instead of proceeding with creation.
  2. Filename modal — Enter a filename (.md for prompt parts, .yml for workflows). Workflow files are pre-filled with a YAML template containing the workflow scaffold.
  3. Editor — The file opens in $EDITOR for editing.
  4. Follow-up actions — After saving, the browser offers the actions that apply to the new file: commit/push, and — when use_chezmoi redirected the write to the chezmoi source — a scoped chezmoi apply of just that file.

Jump All Modal

Press ` (backtick) on any tab to open the Jump All Modal. It displays all entries across Agents, Artifacts, and Services tabs with the same adaptive one- or two-character hints used by current-tab entry jump. Completing an entry's hint switches to the appropriate tab and focuses it.

Up to 62 entries use 0–9, a–z, A–Z. Larger result sets use fixed-width pairs from 00 through ZZ; a first character is consumed without closing the modal, and uppercase characters remain case-sensitive.

Key Action
Hint Jump to the corresponding entry
` Jump back to the previous position (see below)
Ctrl+D / Ctrl+U Scroll the entry list down / up
Esc Close modal (any other key that is not a hint also does)

The modal groups entries by tab (Agents, Artifacts, Services) and shows contextual information for each: PR names and statuses, agent names with running indicators, and Axe routine/command labels.

Jump Back

Both jump modals support a jump-back feature for toggling between two entries:

  • Backtick jump-back: Pressing ` inside the Jump All Modal returns to the previous position, enabling quick toggling between two entries across tabs.
  • Apostrophe jump-back: Pressing ' twice ('') in the single-tab entry jump mode jumps back to the previously jumped-from entry. The footer shows a "JUMP" mode indicator with ' back when a target exists.
  • Fast jump: Ctrl+O runs the same current-tab jump-back path without painting hints first; when no jump-back target exists, it selects the first current-tab hint.
  • Forward jump: After walking backward, Ctrl+Shift+O walks forward through that current tab's jump stack. Agents, Artifacts/Patches, and Services keep independent back and forward positions.

The single-tab variant (' apostrophe) shows entries only from the current tab with the same hint-character navigation.

Node Finder

Press " (quotation mark) on the Agents tab to open the Node Finder modal. It lists every reachable sase node as a tree — clans, agent nodes, session turns (including monitors and gates), workflow roots, workflow agent steps, and stand-alone named procs — including rows hidden by collapsed folds, collapsed grouping banners, collapsed or isolated tribe panels, and the Agents query. Every jumpable row carries a jump hint.

Key HINTS mode (default) SEARCH mode (query focused)
0-9a-zA-Z Complete a hint and jump, or set a pending prefix Edit the query; live refilter
Tab / Shift+Tab Focus the query Back to HINTS, query kept
/ Focus the query Types /
Enter Jump to the highlighted node Jump to the highlighted node
Ctrl+N / Ctrl+P, ↓/↑ Next / previous jumpable row (wraps) Same
" Jump back (same as Ctrl+O) Types "
Backspace Cancel a pending prefix; otherwise swallow Delete a character
Esc Cancel a pending prefix; otherwise close Back to HINTS, query kept
PgUp / PgDn Scroll the preview pane Scroll the preview pane
Any other key Swallow; the footer flashes no hint ‹x› Types

The why-hidden glyph legend: blank means visible, ◆ you are here, ⊘ hidden by the Agents query, ◌ hidden by I (until listed, shown as an I hides K chip), ▭ inside a collapsed or isolated-away tribe panel, ≡ inside a collapsed grouping banner, ▸ inside a collapsed fold.

"Hidden" covers folds, banners, panels, and rows the Agents query hides. Jumping to a query-hidden node clears the Agents query (recorded in query history, announced with a toast); restore it with the query-history keys. The one-line jobs of the sibling surfaces: ' jumps by hint on the current tab, ` jumps across all tabs, digits jump roster members, and " in the finder jumps back.

The preview sits beside the list. On a terminal at least 140 columns wide the list also keeps a status column. From 100 columns up to that width the two panes split evenly and the status column is hidden. Below 100 columns the preview stacks under the list. PgUp and PgDn scroll the preview in both modes.

The preview paints from the snapshot taken when the finder opened. The highlighted row shows its kind and name, a compact identity block, and a breadcrumb from the tribe panel through ancestor names to the row (@default ▸ parent ▸ name). It also says whether the row is visible, and what Enter will do: ⏎ selects it when the row is already visible, or the reveals it will perform first (clearing the Agents query, opening a hidden panel, opening a collapsed group, expanding folds) and then selecting it. A session row lists its turns. A clan lists a status tally and up to twelve members, then … N more members when the clan is larger. A monitor or named proc shows its command and an output tail. A gate turn shows its label and state. A workflow root shows a step-status tally. A non-node row says Select a node to inspect it.

Agent turns then fill two more sections from that turn's artifacts: a PROMPT head and a REPLY · tail. A session container uses its newest agent turn for those sections and labels the prompt with that turn's name. Clans, monitors, gates, and named procs stop after the in-memory preview. The prompt and reply slots read ⋯ loading until that read finishes, then (no prompt recorded) or (no reply recorded) when the artifact is empty. The reply tail names how many earlier lines were left off.

Mentor Comment Stats in PR List

When a Patch has completed mentor reviews with comments, its Patches sub-tab list entry shows inline stats:

  • checkmark + count (e.g., ✓3) — number of accepted comments
  • dot + count (e.g., ●2) — number of unread comments

These stats are computed from the latest stitch's finished mentors. They update as you accept or read comments in the Mentor Review modal.

PR Origin Chip

A Patch with a pr_url shows a PR_ORIGIN chip next to its PR badge in the Patches sub-tab list and in the detail panel: nothing for the default sase origin (a PR SASE created through the tracked PR workflow), external for a PR SASE adopted but did not create, and origin? for unknown (no evidence either way). The detail panel adds a one-line note for external Patches, since AXE excludes external-origin Patches from its candidate selection entirely (see AXE). Press !o on a PR row (see PR Actions above) to open the Mark PR Origin modal and set it explicitly, or run sase patch set-origin <name> <sase|external|unknown> (see CLI Reference). See PR_ORIGIN and Origin Matching for the underlying Patch field and the origin: query property.

Current project

sase's TUI has one current project: the head of the VCS macro MRU store. Launching an agent on a project — or on a Patch owned by that project — promotes it to that head. sase project set-current <project> and the Projects tab's c key (see Projects Tab) move it the same way, by promoting the project to the MRU head, without a launch. Click the status-row project: +<name> chip to open the + launch picker, which is the surface that actually records a launch.

Because setting the current project and launching an agent both promote the same MRU entry, making a project current also moves it to the head of the prompt bar's <ctrl+p> VCS-prefix cycle — the same coupling a launch already produces.

The chip is the project half of the launch-context cluster at the far right of every tab's status row, which reads model: <launch default> · project: +<name>. The dim model: label reads override while a default model override is active, and when the row is too narrow both labels drop, leaving <launch default> · +<name>. The · project: group disappears when no current project resolves or the indicator is disabled. On the Agents tab, the runner load gauge sits just before the cluster. The chip's color is unique among currently enabled projects. Hovering names the project, the MRU ref it came from, and (when the head was a Patch) the Patch; the tooltip also names the two ways to make a project current: launch an agent on it, or press c on the Projects tab. Hide the chip with ace.current_project.indicator: false.

When ace.current_project.seed_filters is on (the default), first-open surfaces that can filter by project seed from the current project instead of starting at all projects:

  • the shared Artifacts project scope (Agent, Stitches, Beads, Plans, Files, Patches)
  • the Statistics project filter
  • the Repos / Workspaces inventory filters
  • the Memory panel's scope ring
  • the highlight in the + launch picker

The seed never overrides an explicit project: / +name term, a pick you already made this session, or a surface that is already open. A mid-session launch or set moves the chip live but does not re-scope those surfaces. Turn seeding off with ace.current_project.seed_filters: false.

The Agents-tab search query is not seeded by default (ace.current_project.seed_agents_query: false) because that query also drives unread jumps and prospective clans, not just the visible list. When enabled, the seed is used only if there is no remembered Agents query; a remembered empty query restores the unfiltered view and suppresses the seed. One line of config turns seeding on.

Inspect the resolved project from the CLI with sase project current. See ace.current_project for the three fields.

Tab Bar Display

The tab bar renders plain tab labels (Agents, Artifacts, Services). Per-bucket counts live inside each tab's body — for example the per-panel count summaries on the Agents tab — rather than as suffixes on the tab title itself.

Top-Bar Indicators

The right-aligned indicator cluster speaks the same visual language as the status-row cluster beneath it (load: 5/8 · model: opus@high · project: +sase): every group renders as a <type>: <body> group where only the <type>: label is dim, so a group's value looks the same in full and compact modes, and visible groups are joined by a dim ·. The left-to-right order runs from activity to system state to launch routing to personal queues: tools, bg, updates, overrides, priority, disabled, stash, inbox. The always-visible inbox anchors the right edge directly above project:. Labels are fixed strings that never pluralize. Count chips carry their identity glyph inside the fill — ⚒ for tool runs (sky blue, red with ⚠ while silent), ⚙ for background procs (blue) and bare monitors (orange), ⬆ for updates, ≡ for the stash, ★ for priority — so compact mode (labels dropped together when the full cluster does not fit in the cells left over after the tab strip and a 2-cell minimum gap; separators kept) still identifies each group; widening restores full labels without oscillation. Every group is clickable: tools opens Admin Center › Tools › Runs with all projects, bg opens the Admin Center Procs tab, updates opens the Updates tab (or the Procs tab on the running update while SASE is updating, when the badge shows its green gear inset, or the Procs tab on the first restart blocker while a restart is queued, when the badge shows its yellow gear inset), overrides, priority, and disabled open Launch settings, stash opens the Prompts overlay on Stash, and inbox opens the notification modal. While SASE is updating itself, the updates group shows a green ⚙ gear inset at its left edge. While installed code waits for TUI-local tasks, submissions, or installation changes before restarting ACE and the SASE service, it shows a yellow ⚙ gear inset instead (green outranks yellow). Independent commands keep running. Durable operations started from this TUI still finish their result handling first, within the same 60-second wait. The yellow tooltip names the actual blockers and the time ACE restarts anyway; clicking it opens the Procs tab on the first blocker. Feature-flag restarts never show the yellow gear.

Tools and Background Indicators

The tools: group shows every live sase tool run on the machine: a sky-blue ⚒ N chip for healthy runs, a red ⚒⚠ M chip for silent runs (no activity for 60 s or more), and an orange ⚙ K chip for monitor turns not currently carrying a run. The ⚒ count comes from the ToolRun ledger, so it covers every mode (escalated, detached, catalog, monitor adopt/join, foreground) on every tab, ignoring the project filter, tribe selection, and session; a live child folds into its live parent. An execution is drawn exactly once: a proc that carries a live run is never drawn as a gear. Each chip hides at zero and the group hides only when all chips are zero. Before the first glance load the ⚒ chips stay hidden rather than showing a false zero; if loads keep failing the last snapshot renders dim with a trailing ?. Hover lists up to five runs (silent first, then oldest) plus bare monitors; click to open Admin Center › Tools › Runs with all projects.

The bg: group shows a filled blue ⚙ N chip while sase's TUI own background procs are running (e.g., sync, mail, accept, and notification-gate operations) — the same count the Procs tab header shows in blue. Tool-run carriers never count here; they live under tools:. Procs that are updating SASE move to the green ⚙ gear in updates: and no longer count in the blue chip. The chip hides at zero. The group excludes service-host rows — service procs and oneshots — which the Services tab reports instead. Hover for the proc labels; click to open the Procs tab.

Current Project Indicator

The uniquely colored +<project> chip is not in the tab bar: it is the project half of the launch-context cluster at the right of each tab's status row (model: <launch default> · project: +<name>). Clicking it opens the + launch picker. See Current project for what the chip means, what it seeds, and how to turn each part off.

Runners Modal

Press ,R (leader + R) to open the runners modal. It shows concurrency information including hook runners, agent runners, and a Procs section listing active and recently completed TUI procs from the current sase's TUI session. These include Patch actions, agent launch and cleanup work, monitor-stop, and notification updates. Each row shows the target, proc type and status, and elapsed or total duration; a failed row also shows its error message. This modal does not show proc output. Use the Admin Center's Procs tab or sase proc show ID for durable records and captured output. Press j to paint hint keys over the rows and jump to the matching agent or Patch; Ctrl+D / Ctrl+U scroll, and r, q, or Esc closes the modal.

File Panel Rendering

Agent files render in full and scroll natively in the file panel. Syntax highlighting falls back to plain text for large content. Pathological outputs above the file-panel safety limit show the first 5,000 lines and an explicit editor notice; press E to open the complete content.

Agents Zoom and Node Rail

The left column has three presentations, derived from deck-area state: EXPANDED by default, RAIL when the persisted Ctrl+S preference is set, and HIDDEN while a deck is zoomed (HIDDEN wins over RAIL). The persisted preference only ever means rail; zoom never writes it.

Ctrl+S toggles the node rail: the same tribe panels and the same visible rows at a fixed 22-cell width, row for row, so a row never moves vertically. Each agent row carries its glyph plus its name: the glyph cell (? needs you, ✗ failed, ▶ running, ◐ starting, ○ queued, ◷ waiting, ✓ done, Ø user-stopped, plus kind glyphs for monitors, gates, workflows, steps, and Patches) followed by the expanded row's identity token. Nested names render relative to their parent (.cld, --plan), long names use a middle ellipsis, and the width never fits to content, so the deck edge stays stable. Color means urgency; the rail shows no runtimes, badges, or emoji. Settled-and-read names render dim. Tribe titles are left-aligned (up to 18 cells), and the bottom border counts off-screen rows (▴N ▾M).

Position is identity: rail row k is expanded row k. Hovering a rail row shows that row's full expanded text as a tooltip, the identity header always names the selection (so j / k is never blind), and ' jump hints plus the " node finder reach every node. Focus stays on the list in rail mode, so every key works unchanged; runtime ticks pause and catch up on expand. The footer shows Ctrl+S expand nodes while railed, and the info row's nodes i/N · Ctrl+S chip is clickable. The ? help modal's Node Rail box lists the full glyph legend.

Each rail agent row ends with a right-aligned ×N count in the shared count column and a pip at the edge. ×N means "folded, N inside": it shows only when the expanded row shows ×N, capped at ×99. The trailing pip cell shows the marked ▪ pip when marked, the unread • pip when unread and unmarked, and blank otherwise — marked always wins over unread. Tree guides (│ continuing, └ terminal) prefix child rows and clamp at depth 3. Banners keep the expanded prefix vocabulary with the label and a heavy (L0) or light rule that is always at least 1 cell; folded banners add the ×N count and a 1-cell urgency roll-up (? needs-you beats red ✗ failed beats gold • unread, else blank). Collapsed tribe titles add their ×N lane roll-up the same way.

Z zooms the focused deck panel in place: it snapshots the deck-area state (layout, panels, focus, ratio), then shows only the focused panel with the node column hidden entirely — no rail. The zoomed panel gets a heavy border in its own deck accent, a reverse-gold ZOOM chip leading its title, and a restore hint (◧ 1 of 2 · Z restore from a split, Z restore from a single deck) leading its subtitle. The info row shows the same clickable ZOOM chip, then Z restore, then node i/N (j / k still moves the selection); the footer shows Z restore while zoomed. The panel keeps its widget, card, and scroll position, and search, E, cards, and decks all work normally while zoomed.

A second Z restores the snapshot exactly, as does Ctrl+S while zoomed ("in zoom, any sidebar key gives your layout back"). Using a layout key (\, |) while zoomed only restores the snapshot, keeping deck and card edits made while zoomed, so no split is ever silently lost.

Image Preview Foundation

sase's TUI renders PNG, JPEG, WebP, and GIF attachments with a Pillow-backed Rich cell preview. The renderer decodes the first image frame, preserves aspect ratio within the visible panel bounds, composites transparency, and paints colored half-block cells using truecolor when the terminal advertises it and 256-color approximations otherwise.

Generated images are already attached to successful agent completion notifications and recorded in done.json as image_paths. The Agents tab file panel and notification modal route supported raster image attachments through this preview layer before attempting text decoding. See agent_images.md for supported image extensions, guardrails, and current preview behavior.

Agent Auto-Naming

Prompts with no %id directive, or with a bare %id, use the plain auto-name template @. SASE reserves the lowest available token from the sequence 0, 1, ..., 9, a, ..., z, 00, 01, ...; with no reserved names, plain auto-naming yields concrete names such as 0, then 1.

An explicit %id value containing exactly one marker is an agent-name template. The legacy marker is bare @, so the first allocation for %id:@.cld becomes 0.cld, %id:build-@ becomes build-0, and %id:research.@.final becomes research.0.final. Keyed markers such as %id:research.{@1}.final are preferred for macro swarms: SASE resolves every matching key in %id, %clan, clan=, waits, fork/resume references, and prose before any spawned member can start. Bare @ still works, but template references use latest-wins lookup and can be unsafe when a swarm member starts after a newer overlapping launch. See Macro template directives for {@<id>} and {@<id>!} qualification rules.

Names are permanent IDs: a name used by any existing agent state remains reserved until that agent is explicitly wiped or deleted. This enables the fork-by-name workflow: press F on a running named agent to queue a follow-up that waits for it to finish and then loads its conversation history.

Provider/Model Suffixes

When the same base name is shared by multiple co-launched agents (e.g. multi-model fan-out via the %model: directive), the rendered display name carries a short .<provider> or .<provider>(<model>) suffix so each row is distinguishable. Provider suffixes are supplied by the LLM provider plugins via the llm_provider_short_name hook (built-in defaults: cld for Claude, cdx for Codex, agy for Antigravity). Additional provider plugins can contribute their own short names. Model-name shorthands come from the llm_model_short_aliases hook (e.g. fable for claude-fable-5, gpt61sol for gpt-6.1-sol; see Model Short Aliases) and are resolved against the configured model so the suffix stays compact regardless of how the model was spelled in the prompt or config. Single-runtime spawns omit the suffix.

An explicit %id:<name> launch fails before spawning if <name> is already reserved. The prompt is saved as a cancelled history entry and the error suggests the lowest free numeric suffix, such as <name>1. To deliberately reuse a reserved name from the TUI, launch with %id:!<name>; the ! form confirms that SASE should wipe the previous owner and then claim the name for the new agent. Reviving and dismissing agents preserve their stored names.

The durable registry lives at ~/.sase/agent_name_registry.json and is rebuilt from visible artifacts plus dismissed bundles when missing or stale. Use sase agent names migrate-auto to run the historical auto-name migration that moves older generated names into the permanent namespace; pass --force to rerun after the migration marker is present or --json for machine-readable output.

Per-Step Naming for Multi-Agent Workflows

Sequential plan-session workflows have a stable session container plus member suffixes. When the first follow-up attaches, the original agent is renamed and the bare session name becomes a pure container. Generated follow-up rows and phase metadata use canonical double-dash suffixes. For example, if the initial agent was named a:

  1. The first attachment creates session container a and gives the original its persisted role suffix (a--plan for a plan proposer or a--0 for a generic agent).
  2. The planner phase uses a canonical --plan role suffix.
  3. Feedback and question-continuation rounds become a--2, a--3, etc.
  4. Terminal follow-ups use the phase suffix, such as a--code, a--epic, or a--commit.

The base name (a) is reserved for the session as a whole, so %wait:a or @a references resolve through the session container. In sase's TUI, the aggregate session row displays that bare container name, while expanded concrete member rows keep their exact suffixed names (a--0, a--plan, a--code, and so on). New plan-session metadata stores double-dash role_suffix values (--plan, --2, --code, ...). sase's TUI still canonicalizes older dotted suffixes (.plan, .2, .code, etc.) and legacy single-dash suffixes (-plan, -2, -code, etc.) when reading legacy artifacts.

Agent Statuses

Each row in the Agents tab displays a status label. The sections below separate labels that represent execution, waiting, review, or a successor handoff from labels that represent completed work. This is a display-oriented grouping: a gate turn can be processless or already settled while its label still uses the Running bucket to represent an in-progress handoff.

Execution, Waiting, Review, and Handoff Statuses

Gate-turn rows use lifecycle presentation rather than a separate hard-coded color for every plan or question label: pending/settling labels use the gate's configured or deterministic accent, answered/completed/stopped labels turn grey, and failed/timeout/lost labels turn red. Legacy non-turn rows retain their older status presentation where noted, with specialized lifecycle labels falling back to dim text. Presentation and bucket are separate: a grey settled gate can still appear in the Running bucket when it handed off to a successor.

Status Presentation Description
RUNNING Gold Agent subprocess is executing
QUEUED Cornflower blue Cleared dependency, bead, and time waits; parked for runner capacity
WAITING Amethyst/purple Paused on a dependency, bead, or time wait; ?N marks unknown targets; matching bead tokens follow agent tokens
WAITING INPUT Gate accent; legacy dim Workflow is paused at a human-in-the-loop (HITL) step
TALE Gate accent; legacy dim An authored tale is waiting for user review
EPIC Gate accent; legacy dim An authored epic is waiting for user review
PLAN Gate accent; legacy dim A legacy or unreadable-tier plan is waiting for user review
PLAN APPROVED Grey on the settled gate turn Plan approval was durably accepted; follow-up execution is being attempted
TALE APPROVED Grey on the settled gate turn Tale approval and commit were durably accepted; follow-up execution is being attempted
EPIC APPROVED Grey on the settled gate turn Epic approval was durably accepted; creation is being attempted and no epic ID has been back-filled yet
QUESTION Gate accent; legacy dim Agent is asking the user a question (via /sase_questions)
ANSWERED Grey on the settled gate turn The answer was accepted and a successor is being launched
SUDO Gate accent An agent's sudo request is waiting for review; it settles as SUDOED or DENIED
RETRYING Orange Agent hit a retryable error and is in a countdown before retrying

Modern /sase_questions calls hand the session to a processless QUESTION gate turn and end the asking LLM turn. The turn owns the durable question until it is answered, cancelled, or times out, so dismissing its notification does not dismiss the session state. An answer settles it as ANSWERED and launches the next ordinary session member with the accumulated Q&A. That successor starts under the serial-session admission exemption and becomes the session's occupied slot; it does not enter the runner queue. The Enter shortcut can reopen the live question even when no unread notification remains.

Approval labels are receipt-derived: they appear as soon as the accepted gate decision is durable, without waiting for the selected command or successor launch to finish. If that execution fails, the label changes to PLAN FAILED or EPIC FAILED, so an accepted decision cannot hide a failed action. Commit-only plans are different: PLAN COMMITTED appears only after the archive succeeds.

Older in-flight runs may still use a pending_question.json marker. Those compatibility runs yield their slot while unanswered, then reacquire capacity in the same process and can become QUEUED after an answer. sase's TUI continues to project that marker as QUESTION until the legacy continuation resumes or terminates.

The keybinding footer renders available conditional actions as non-breaking key/label chips. When the chips do not fit on one line, the footer switches to a deterministic grid so narrow terminals and leader-mode action sets do not wrap in the middle of a binding. Mode labels such as LEADER are pinned on the left, and the service status indicator remains pinned on the right. The status is a segmented badge with a neutral SVC label chip before the colored state chip, so the indicator always identifies the service host it describes.

Service Health Pill

Once the first service-status snapshot loads, the SVC pill summarizes service health. Proc states use the wire vocabulary the core emits (running, unavailable, disabled, stopped, crash_loop, backoff, exited): crash_loop and backoff are failures, exited is a failure while desired state is running (a clean give-up reads as a warning), unavailable is a warning, and disabled or an operator-stopped proc is muted rather than a failure.

Pill Color Description
N/M Teal Healthy: N of the M counted service procs are running (enabled procs whose desired state is running or that are unavailable)
! Red Unhealthy: the service host is stopped or stale, or a counted proc is unavailable, crash-looping, backing off, or exited
? Yellow Unknown: no service-status snapshot could be read (service status unavailable)

When health is unhealthy, sase's TUI also raises a Services unhealthy: <reason> warning toast (for example host stopped, host stale, or gateway unavailable), and raises it again whenever the underlying service status changes while health stays unhealthy. A host that is still starting is not a failure; a host whose heartbeat is stale is a failure distinct from stopped.

The health pill replaces the host-state labels below as soon as the first status load finishes. When no snapshot could be read the pill shows a yellow ? (unknown) instead of a healthy count. The labels are therefore only visible briefly at startup, between the startup stopwatch retiring and the first status load; -R / --restart-service restarts the host after that load, so the pill keeps showing health rather than RESTARTING:

Status Color Description
RUNNING Green Service host is running normally
STOPPED Red Service host is not running
STARTING Yellow Service host is starting up
STOPPING Yellow Service host is shutting down
RESTARTING Deep sky blue Service host is restarting

During TUI startup the footer slot shows a live starting stopwatch with a rotating glyph in place of the host status, ticking at ~10 Hz until the TUI finishes mounting and the real host status resolves. The background color turns from its normal tone to a slow-startup tone once the elapsed time crosses the slow threshold, giving immediate visual feedback on cold-start latency. A safety timeout forcibly retires the stopwatch if the mount signal never fires.

Completed Statuses

The ordinary-row renderer reserves green for plain DONE; specialized terminal labels fall back to dim text unless a gate or monitor supplies lifecycle presentation.

Status Presentation Description
DONE Green Agent completed successfully
PLAN DONE Dim Plan workflow fully completed (all steps)
TALE DONE Dim Tale plan workflow fully completed (all follow-ups)
PLAN COMMITTED Grey on a settled gate; otherwise dim Commit-only plan was archived successfully
EPIC CREATED Dim A created epic ID is known, or a legacy epic follow-up completed
FAILED Red Agent exited with an error
PLAN FAILED Red Accepted plan action or archive failed
EPIC FAILED Red Accepted epic-creation action failed

Monitor turns are the exception to this status table's success-oriented labels: a gate-approved epic monitor uses EPIC APPROVED as its start label and EPIC CREATED as its stop label for every terminal state. A failed, timed-out, stopped, or lost monitor can therefore display EPIC CREATED in its state-dependent color. Inspect the monitor state, bucket, exit code, and output; on the planner row itself, EPIC CREATED means the created epic ID was successfully back-filled.

Completed agents can be dismissed with x on a single row, or through the X cleanup panel for focused-panel, global, tribe, clan, marked, group, and custom planner-backed selections. DONE, PLAN DONE, and TALE DONE rows with a saved response path are resumable from the Agents tab.

When a terminal agent becomes unread, sase's TUI marks it with the completed-agent indicator and includes it in the Agents header unread count. Selecting that row, jumping to it with ,j, or toggling it back to read with U acknowledges the row and dismisses the matching user-agent completion notification. Manually marking a row unread with U arms it for normal acknowledgement after you move away and return, so the marker can be used as a short-lived reminder without leaving stale inbox entries.

If the currently focused row finishes while you are already on the Agents tab, sase's TUI still marks it unread and keeps the completion notification active until a real navigation or selection event acknowledges it. A refresh that merely preserves focus does not silently consume the unread marker.

The unread count in the Agents header is drawn as black text on a gold pill so the "you still have unseen completed work" signal stands out from the rest of the colored metrics. It uses the same gold as the inbox's ✉ general-notification chip in the top bar, giving you a single color to scan for.

Switching to the Agents tab does not bulk-dismiss completion notifications. sase's TUI projects active completion notifications onto unread rows, then acknowledges rows one at a time when you select or navigate into a terminal unread row. Bulk acknowledgement is explicit through ,u. That command marks every loaded unread completed agent read: rows on every Agents tab, members of collapsed clans and tribes, and rows that belong to a query which is not the visible tab. Clan containers, session members, workflow children, monitors, gates, and named procs stay out of the set, and the gold unread pill counts the same rows, so the toast count matches the header. The toast reads Marked N completed agents read · press ,u within 10s to undo. With no new unread completions, press ,u again within those 10 seconds to restore the marked agents that are still loaded and terminal (Restored N completed agents unread). A newly unread completion cancels the undo; the next ,u marks the current unread set read instead. When nothing is unread and no valid undo remains, ,u reports No unread completed agents. This undo belongs to the current TUI session. Plan approvals and user questions are never auto-dismissed by this flow; they always require explicit y / n confirmation from their respective modals.

Agent Revival

Press !R on the Agents tab to revive previously dismissed work. sase's TUI opens the saved-group revival modal first, showing newest saved groups with a right-hand preview of included agents, projects, PRs, statuses, provider/model labels, and revival count. Select a group and press Enter to revive it, choose Load more saved groups... to page older groups, or choose Custom revival search... to open the older dismissed-agent search where you choose all, home, project, or PR scope manually. In that dismissed-agent search, Ctrl+J loads ace.page_size more archive rows (default 100) and Ctrl+K unloads the last page, never dropping below the first page.

Use m to mark related Agents-tab rows and then s to save and dismiss them as a group. The save modal accepts an optional human name. Leaving it blank keeps the generated display title, such as "3 agents from @review" or "2 agents in auth_retry". Saving a marked group hides the selected rows from the normal Agents tab without killing running processes. When a marked top-level workflow row has child rows, sase's TUI also includes the children in the saved group so revival can restore the original tree.

Dismissed agents are saved as individual bundle files under month shards in ~/.sase/dismissed_bundles/YYYYMM/ and can be restored later. Saved group metadata lives under ~/.sase/dismissed_agent_groups/ and stores stable references to those bundle files plus the optional group name, status counts, projects, PRs, model/provider metadata, and tribes. There is no limit on the number of dismissed agents or saved groups that can be stored.

Dismiss operations are O(1) per agent: each agent is saved to its own JSON file rather than a monolithic store. Parent workflow rows use <raw_suffix>.json; workflow children use <raw_suffix>__c<step_index>.json. sase's TUI keeps a SQLite summary index in the dismissed-bundle directory so the revive modal and internal lookups can list dismissed agents without opening every bundle. Use sase agent archive verify to check that maintenance index, or sase agent archive rebuild-index to rebuild it from bundle files. The index stores metadata such as status, name, project, model, provider, workflow, and Patch metadata; it is not a full-text copy of agent chat contents.

Revival removes the agent identity from the dismissed set, restores enough artifact files for sase's TUI to rediscover the agent, and preserves the dismissed bundle as historical recovery data. Saved-group revival skips missing bundle references with a warning and restores the remaining agents. Group metadata is not deleted after revival; sase's TUI marks the group with revived_at and increments times_revived so the modal can show previous use. The reload path forces a full-history scan and can hydrate the just-revived row directly from the bundle, so agents still appear after revive even if the persistent artifact index was empty or stale.

Every revival also writes structured events to ~/.sase/logs/events.jsonl (start, per-agent success, per-agent failure). Read them back with sase revive-log — see Agent revival audit log for the record schema and CLI flags.

Legacy Dismissed-Name Prefix

Current dismiss and revive operations preserve stored agent names, per-agent tribes, and top-level/workflow-child identity. Older dismissed bundles may still contain YYmmdd.<base> names from the previous dismissal model, and sase's TUI keeps compatibility helpers for reading those bundles. Bare %wait (no target) intentionally skips legacy dismissal-prefixed candidates so it anchors on a live, visible agent.

Agents Deck Picker

On the Agents tab, p opens a small centered deck picker for the focused deck panel. One more keypress picks the deck directly: m Main, f Files, t Tools, n FINAL. Pressing the opener again (pp with the default binding) switches the focused panel to the last deck that panel showed, or to the cycle-previous deck when that panel has no history yet, and closes the picker. The return row names the destination and badges it last deck or previous. Esc and q close without changing anything. j/k move the highlight (wrapping across the return row and the four deck rows) and Enter picks the highlighted row; any other printable key is swallowed so nothing leaks through to the tab. History is one deck deep per panel, updated by every real deck change (picker letters, palette jumps, Ctrl+N / Ctrl+P, and a capital letter that changes the other panel), and restored with the deck layout. The picker heading names the focused panel (deck panel single, top / bottom or left / right in a split, zoomed while zoomed), each deck row shows its letter, glyph, count, and whether it is already showing (or shown in the other split panel), and the palette offers the same jumps as Show <Main|Files|Tools|FINAL> deck in focused panel plus Show last deck in focused panel. The old detail-view picker is retired: the Agents tab now always shows agent data decks and cards. The in-picker deck letters are fixed and not configurable. p keeps its unrelated Artifacts project-scope meaning on the Artifacts tab.

The capital deck letters (M / F / T / N) show that deck in the most recently focused other panel instead of the focused one. When the opener is a single letter, its capital (P by default) shows the same resolved return deck in the other panel, with the existing split and zoom rules. From a single panel they open a new panel below (top-bottom) showing the picked deck; in an existing top-bottom or left-right split they target the MRU other panel and keep the layout as is (a left-right split is never rotated). While zoomed, the zoom ends first, the way Z ends it, and the split comes back in its original orientation (or a new bottom panel opens when the zoom came from a single panel). Focus always stays on the panel the picker was opened from, unlike \, which moves focus into the new panel; Ctrl+F moves it if you want. A muted hint line at the bottom of the picker names the MRU target with its position glyph (for example show in the ◲ bottom-right panel) and says where the capital letter will go. Picking a capital letter for the deck the other panel already shows changes nothing. Enter and mouse clicks keep their focused-panel meaning. The palette offers the same jumps as Show <Main|Files|Tools|FINAL> deck in other panel plus Show last deck in other panel.

Agent Data Decks and Cards

The Agents tab detail column shows one to three deck panels between the sticky header panel and the jump panel. Each deck panel shows one agent data deck, a named ordered set of agent data cards about the selected node. Ctrl+N / Ctrl+P cycle the focused panel through the Main, Files, Tools, and FINAL decks (wrapping, nothing skipped), and p opens the deck picker for a two-key jump to any deck (pp returns to the last deck, or to the cycle-previous deck when the focused panel has no history); Ctrl+J / Ctrl+K move to the next / previous card in the focused panel.

  • Main deck. Context (details and prompt; the default card) and Reply — titled Output for named procs, monitors, gates, and workflow steps — plus a leading TRACEBACK section at the top of Reply when the agent failed. Clan rows and whole-panel tribe focus show a single Summary card instead. A paged deck keeps the card last chosen with Ctrl+J / Ctrl+K (for example Reply) as the selection moves between nodes.
  • Files deck. One card per file page (commit diffs, the live diff, linked-repo diffs, then extra files), titled by file. The default card is the same default page the file view always chose.
  • Tools deck. Two cards: ⚒ Runs first, then LLM Calls (see Agents Tab Tool Runs and Agents Tab LLM Calls Panel). The Runs card exists only when the selected node has at least one tool run. The default card is the panel's sticky Tools preference when it names a card that exists, otherwise Runs, otherwise LLM Calls — so a reader who prefers LLM Calls stays there. Tools is paged-only: it never spreads, P stays a no-op, and there is no deck-view badge. The border subtitle's Tools segment reads tools ⚒N M (N runs, M LLM calls; tools ⚒N when there are no calls), bold accent while a run is live and red while one is silent.
  • FINAL deck. The ⊛ FINAL deck shows how the selected node's turns landed: an Overview card plus one card per finalizer instance (see Finalizers on the Agents tab).

A multi-card deck renders spread — every card on one scrollable page separated by titled rules — when its cards fit within ace.agent_decks.spread_max_screens panel viewport heights (default 1.5; 0 means always paged), and paged — one card at a time — otherwise. In spread mode Ctrl+J / Ctrl+K scroll the next / previous card's header to the top. The deck panel's border title names the deck, its deck view badge (Main and Files decks only), and its cards with the active card highlighted (and an N/M position when there is more than one card); the border subtitle is a main · files · tools · final switcher showing each deck's card, file, run/call, or finalizer count when known, dimming decks with no content. A deck with no content for the selection shows an empty-state card instead, so the layout never jumps.

Deck Views

Each Main and Files deck panel has a deck view: spread (every card on one scrollable page), page cards (one whole card per page), or page blocks (one card block per page). The view is normally automatic — resolved from the content through the ace.agent_decks spread thresholds — or fixed to one layout by you. The Tools and FINAL decks have no views: they always lay out automatically and show no badge. Deck views stay a Main-and-Files feature; FINAL's spread/paged and block paging resolve automatically from the ace.agent_decks thresholds like every other automatic deck.

The badge. The top border title shows the effective view right after the deck name, for example ◆ MAIN page blocks · auto or ◆ MAIN spread · fixed. The layout word uses the deck accent; fixed is bold accent while auto stays muted, so a fixed panel stands out at a glance. On narrow panels the badge shortens (cards, blocks, B·A) but never depends on color alone. No badge appears for the Tools or FINAL deck, an empty deck, or a panel that has never painted a full Main document.

P cycles the view. P steps the focused panel's Main or Files deck wider (page blocks → page cards → spread, wrapping) and pins the result as a fixed view. The first press from automatic starts after the effective layout, so it always changes what is on screen; layouts that would render identically for the current content are skipped. P is unavailable when there is nothing to choose (a one-card deck, the Tools or FINAL deck, an empty deck, or while a Main document is still partial). The footer shows a P view entry and the help modal an Agents › Navigation row only while the cycle is available. The first press that fixes a view posts one teaching toast naming the palette reset; routine presses stay silent because the badge changes in place. The badge and panel chrome repaint on the first frame after P; the Main body for the new layout is built off the event loop and swapped in when ready, so on a very long Reply the badge can lead the body by a moment.

Palette. The command palette offers Cycle deck view plus four direct choices: Deck view: automatic, Deck view: spread (fixed), Deck view: page cards (fixed), and Deck view: page blocks (fixed) (Main only — Files never pages blocks). Only automatic returns a panel to automatic; P cycles just the three fixed layouts. The choice equal to the current policy is hidden. A direct choice applies to the focused panel's deck even when it changes nothing for the current card, since it also covers future cards.

Persistence. Each panel keeps one view per deck (Main and Files), surviving card and deck switches, splits, zoom, and restarts in ~/.sase/ace_agents_deck_state.json. New panels start automatic. A view change keeps the reader's place: the same card, block, scroll offset, and pin survive in every direction.

Files specifics. A fixed spread on Files reads every page fully (capped per page, with truncation hints, so nothing is silently dropped). While that read runs the deck stays paged and the badge reads page cards · spreading…. Files with an image or video can never spread: the deck stays paged with page cards · spread unavailable, keeps the fixed preference for the next compatible selection, and toasts once when you caused the change.

Card Blocks

Cards gain a third level — deck → card → card block — for the session Reply card. An agent session's Reply card holds one block per concrete session turn (AGENT (role), ⚙ MONITOR, ⋔ GATE phases, matching the SESSION TURNS jump-panel roster); the still-reachable legacy non-session followup_agents Reply path gets one block per followup the same way. Every other card renders exactly as before.

Newest-block landing and the triage loop. Selecting a node lands on its newest block, so the first thing you see is the latest turn's output. The working loop is: read the newest block, press ( to step one turn older, repeat, and press ) to walk back toward the newest. Block state is ephemeral and panel-local (kept per deck panel for the current selection); changing nodes, toggling attempts, or losing a vanished block id re-lands on the newest. While you sit on an older block and a new turn starts, the view stays put and the newcomer only gains an arrival dot — a following view (sitting on the newest block) advances to it automatically.

Block-spread vs block-paged. Only a card shown alone pages its blocks; a spread deck always shows every block inline. A lone card renders block-spread (all blocks on one page) when it fits within ace.agent_decks.block_spread_max_screens panel viewport heights (default 1.5; 0 means always one block per page), and block-paged (one block per page) otherwise. Block navigation never flips the deck's own spread/paged mode.

The block rail. Whenever a paged deck's active card has 2+ blocks, a one-row rail sits docked under the Main deck panel's top border showing the session timeline: roster-numbered entries with status colors, an accent pill on the active block, arrival dots on unseen newcomers, and a ( ) blocks key hint at wide widths. At its widest tier the rail names the block mode just before the key hint: page N/M when blocks are paged, all N when they are inline. Clicking an entry selects that block; in a spread deck the rail stays hidden because the phase dividers already mark each turn.

Keys. ( steps to the older block and ) to the newer block (both wrap); in a spread deck they top-align the target turn's header, in a block-paged card they swap the page. The footer shows a (/) blocks entry and the help modal a matching Older / newer card block row only while the focused card has 2+ navigable blocks. Ctrl+Shift+J / Ctrl+Shift+K are not bound by default: in common terminal chains (tmux + kitty included) they arrive as plain Ctrl+J / Ctrl+K and would cycle cards, not blocks. If your terminal delivers them distinctly, bind them yourself as a personal override of prev_card_block / next_card_block (see configuration and the keymap settings).

\ opens a second panel below the first (stacked) and | opens one to its right (side by side); the new panel takes focus and shows the next deck with content, or a duplicate deck when every other deck is empty or already shown. One rule governs the split keys: \ draws a stacked divider and | a side-by-side divider. If that kind of divider already spans the whole area, the key erases it and the side you are on grows to fill the space. Otherwise, with fewer than three panels, the key draws the divider through the focused panel — the unfocused panel becomes the full-span main panel without moving or resizing. With three panels, the key turns the layout instead. There are seven geometries: single, two two-pane splits, and four three-pane T shapes with a full-span main panel:

 single     R2 stacked    C2 side by side
┌──────┐    ┌──────┐       ┌───┬───┐
│  A   │    │  A   │       │ A │ B │
│      │    ├──────┤       │   │   │
└──────┘    │  B   │       └───┴───┘
            └──────┘
 R3 main-top  R3 main-bottom  C3 main-left   C3 main-right
┌──────┐     ┌───┬───┐       ┌───┬───┐      ┌───┬───┐
│  A   │     │ B │ C │       │   │ B │      │ B │   │
├───┬──┤     ├───┴───┤       │ A ├───┤      ├───┤ A │
│ B │C │     │   A   │       │   │ C │      │ C │   │
└───┴──┘     └───────┘       └───┴───┘      └───┴───┘

Ctrl+T turns any split, Ctrl+Shift+D (alias Ctrl+X) closes the focused panel, and Ctrl+Shift+F / Ctrl+Shift+B (aliases > / <) swap the focused panel's session with the next / previous panel in reading order. p plus a capital deck letter (M / F / T / N) opens or fills the most recently focused other panel with a chosen deck without moving focus (see the deck picker). Ctrl+F / Ctrl+B move focus to the next / previous panel in reading order (split layouts only) and every deck, card, scroll, search, and fold key acts on the focused panel. } / { grow / shrink the focused panel (split layouts only). A new three-pane geometry needs about 8 rows by 40 columns per panel or the key is refused with a toast; shrinking the terminal never closes a panel.

The Ctrl+Shift chords need the kitty → tmux CSI-u chain (kitty plus tmux with extended-keys-format csi-u, and SASE requesting modifyOtherKeys mode 2 inside tmux); in mode 2, tmux re-encodes pasted control characters (newlines arrive as CSI 106;5u), and SASE's input driver decodes them inside bracketed pastes so pasted text arrives intact. > / < / Ctrl+X always work.

The layout, split ratio, focus, node-rail preference, and each panel's deck, preferred card, and deck views persist across restarts in ~/.sase/ace_agents_deck_state.json. See the key tables for the full deck keymap and configuration for the spread setting.

Finalizers on the Agents Tab

Host-owned finalizers (commit, check, tasks, plugin finalizers) run after the model turn, and the Agents tab shows them at four zoom levels over one shared state vocabulary: at a glance on the row, in context in Reply, to diagnose in the ⊛ FINAL deck, and for authorship in the Overview card and sase final status. Every surface agrees because glyph, word, and color for each state live in one mapping. The view is read-only: there are no retry, cancel, or bypass controls.

At a glance: FINALIZING rows and ⊛ chips. While a node's finalizers run, its row status word reads FINALIZING in the Running bucket. This is a presentation overlay only: agent.status, buckets, ordering, filters, capacity, and row actions are untouched. A ⊛ chip names the most severe finalizer state. On a session container the chip considers only runs after the newest successful settled run, picks the highest severity (failed > refused > interrupted > deferred > running), and appends k of n runs when more than one run is in play. The collapsed header panel carries the same signal as an activity chip (⊛ finalizing · <id> · <label>, or ⊛ declaration while the declaration turn runs). A clan whose lone running member is finalizing also reads FINALIZING, while the ⊛ chip stays on the member row.

In context: the ⊛ FINAL receipt. Each turn's Reply phase ends with a short ⊛ FINAL receipt: one line per finalizer instance with its state glyph, per-instance detail (step or op label, attempt n/m when retries are budgeted, failure reason or headline), and duration. Success lines stay quiet; success with warnings gains a dim ⚠N suffix (detail lives in FINAL, never on the row), and failed or refused instances add a dim reason line. When anything needs attention the receipt ends with a p n open FINAL deck hint. There is no receipt before finalization begins (the planned phase), none for turns skipped by a plan handoff, and none for legacy runs with no finalizer summary.

To diagnose: the ⊛ FINAL deck. Press p n (p N opens it in the other panel) or cycle with Ctrl+N / Ctrl+P. The deck accent is rose #FF87D7; the count noun is finalizer/finalizers. Cards are Overview plus one per selected finalizer instance, and FINAL cards stick while j/k move (the status strip and subtitle carry the signal instead). Every FINAL card on a session container holds one card block per turn that ran finalizers, with the same roster-matched ids Reply uses; skipped and not-triggered turns appear only in the ledger. The rail, ( / ), and newest-block landing all work as on Reply.

  • Overview card. The run plan in DAG order with per-instance selection reasons, configured-but-unselected instances dimmed with their reason (%final:!lint, %final:none, not default), the declaration timeline, controller cycles (only when above one), drift, run-level diagnostics, the runs ledger, and pointers to sase final status.
  • Instance cards. Why, trigger, and declared lines; one section per finalizer attempt with the latest (or failing) attempt expanded; operations with outcomes; structured steps; typed evidence (sha, url, bead); deduped diagnostics; and log and protocol hint targets for export and search.
  • States. Planned ◌, declaration/running ▶ (gold), success ✓ (green, plus a dim amber ⚠N in deck and receipt only), failed ✗ (red), refused ⊘ (purple), deferred ⏸ (amber), not triggered / handoff-skipped ○ (dim), blocked / not reached – (dim), interrupted ! (amber), unavailable ⚠ (dim amber). An interrupted or unavailable state never spins.
  • Live tails. A running op shows a sanitized in-card tail of at most 12 lines, but only once the op has run longer than ace.agent_decks.final_tail_delay_seconds (default 5.0; 0 renders immediately). The tail follows the newest run and latest attempt with an arrival marker, pauses while scrolled up, and refreshes on a 1 Hz tick that runs only while FINAL shows the selected agent's active finalization. Fast ops go straight from ▶ to ✓ with no tail.

For authors: sase final status. sase final status [<agent>] projects the same run view the FINAL deck renders — reconciling the sealed plan, progress journal, operation records, steps, and typed evidence — and prints it pretty (colored, default) or as JSON (-f json). With -d/--artifacts-dir it reads finalizer artifacts directly. Inside a SASE turn it defaults to the calling agent. See the CLI docs.

Agents Tab Main Deck

The Agents tab Main deck shows structured information about the selected agent as Context and Reply cards (see Agent data decks and cards):

Pressing V on any local Agents-tab row (running or done) opens that same agent's metadata full-screen in the pager instead, as a sectioned document — IDENTITY, MODEL, WORKSPACE, TIMELINE, CONTENT, SASE CONTEXT, and BEAD (skipping any that would be empty) — with / search, ; goto, Ctrl+N/Ctrl+P section jumps, and y copy. r inside the pager re-snapshots the agent, which matters for a still-running one. The pager also supports split panes (\ below, | beside) — see Split panes. V is unavailable for remote fleet rows and when no agent is selected; other tabs keep V bound to the Agent Run Log modal.

Ctrl+J and Ctrl+K move to the next / previous card in the focused deck panel (wrapping). In a paged Main deck this swaps the visible card; in a spread Main deck it scrolls the next / previous card's header to the top. The chosen card is the panel's preferred card: on a new selection the panel shows the preferred card when the node has it, otherwise the default card (Context, or Summary for clan and tribe documents), while the preference itself is kept. Numbered roster rows (SESSION TURNS, CLAN MEMBERS, TRIBE MEMBERS, NEIGHBORS) live in the jump panel and are not cards; within a session container's SASE CONTEXT region, its lane sub-headings (BEAD, PLAN, ARTIFACTS, MEMORY, GLOSSARY, SKILLS, WORKSPACES) are fold anchors, not cards — za/zA still reach them when they own the viewport's top row. Roster rows are also not za/zA targets and are not covered by ,/ search. The shortcuts act on the focused deck panel, and changing agents or entering/leaving a pinned attempt view keeps the preferred card.

  • Agent details: Name, status, model, provider, Patch association, and chronologically sorted timestamps:
  • Bead — shown for agents launched by sase bead work; modern phase and task rows use explicit bead launch metadata, phase rows also use epic/plan metadata and validated plan frontmatter, and exact epic plus legacy phase/.land rows retain compatibility inference
  • WAIT — when the agent was spawned (waiting for a slot)
  • BEGIN — when runner admission completed, before workspace preparation for slot-participating user agents
  • PLAN — each plan proposal round (multiple entries when re-planning occurs)
  • FBACK — each time the agent requested feedback from the user
  • QUEST — each time the agent asked the user a question
  • RETRY — each time the agent entered retry state (retryable error)
  • CODE — when the agent began writing code
  • EPIC — when an epic follow-up agent was launched after plan approval
  • DONE — when execution completed
  • CLAN / MEMBERS: Shown when a synthetic clan row is selected. The orchid CLAN kind label renders as the header panel title and the identity fields (Name, Tribes, Status, Runtime, Members, Fold) live in the header panel; the CLAN MEMBERS roster lives in the jump panel. Direct member rows use chronological launch order (earliest first), which keeps their numbers stable while statuses change. Each numbered row shows the hood-relative suffix, kind, status, model, and duration; members of a nested sequential session are indented under its aggregate row. Ctrl+J / Ctrl+K move between the Main deck's cards, and pressing the row's number jumps to that member in the Agents list. At most 100 members receive numbers.
  • SESSION: Shown when a real multi-member session root is selected. The cyan kind label renders as the header panel title and the cyan Name: value matches the session row's identity block. The title is header chrome, not a card; the SESSION TURNS roster lives in the jump panel and is not a navigable section. On a session container, its SASE CONTEXT heading opens the Context card region; its per-lane sub-headings (BEAD, PLAN, ARTIFACTS, MEMORY, GLOSSARY, SKILLS, WORKSPACES) stay fold anchors only.
  • AGENT TURN: Shown when a standalone sase agent or session member row is selected. The gold kind label renders as the header panel title and the gold Name: value matches the list-row name annotation. The title is header chrome, not a card. Monitor members and workflow step children (bash / python / parallel) do not get this heading.
  • Header panel: The selected node's identity header (every field from the kind line through Timestamps, plus Fold where present) renders in its own always-visible panel at the top of the detail column, above the deck panels in every deck layout. Collapsed, it keeps the two chip rows — who and how on row 1, what and state on row 2 — and previews the agent's AGENT MACRO below them as a card: an MACRO tab row over a rectangle on the Monokai code surface, so the prompt reads as one block apart from the metadata chips. Every card row carries the ▎ quote bar in the MACRO accent, which acts as the card's accent edge; hard-wrapped prose reflows into wrapped rows, hard breaks render as a dim , and the highlighting matches the expanded prompt. The card fills the panel width and shows at most three rows (ace.agent_header.collapsed_preview_max_rows), fewer when the ace.agent_header.collapsed_max_share height budget of a short column is tighter (0 still turns the preview off and shows no tab). The tab row counts against that budget, next to the border and the chip rows. On overflow the last row ends in … and the border subtitle becomes +N lines · ▾ d more (+1 line when one line is hidden). d expands the panel to the full field list plus the complete AGENT MACRO under its own heading (or collapses it back). The kind label moves into the panel's border title in the node's accent color — AGENT, AGENT TURN, SESSION, CLAN, WORKFLOW, STEP, GATE TURN, MONITOR TURN, NAMED PROC, or, for a selected whole tribe panel, TRIBE — and the border subtitle shows what d will do (▾ d more / ▴ d less, naming the configured toggle_agent_header key). The panel is hidden only for "No agent selected". AGENT MACRO no longer renders in the scrolling body and is not a Ctrl+J/Ctrl+K stop; ,/ still finds the user's words through AGENT PROMPT. A clan's collapsed rows mirror the tribe layout: name, status, and count chip on row 1; tribes, member totals, runtime, and the fold chip on row 2. Collapsed/expanded state is per session and holds across row moves, tribe focus, and layout changes. Hint mode never changes the panel's state: expanded, its fields and AGENT MACRO carry hint markers numbered first; collapsed, it keeps its normal preview, header content gets no markers, and numbering starts in the deck body. Attempt-pinned views never rendered AGENT MACRO and show no preview, and nodes without a macro show exactly the two chip rows inside the border. Deck search (,/) covers the focused panel's deck only, since header fields stay on screen.
  • Jump panel: Every live numbered roster target (session turns, neighbors, clan members, tribe members) lives in its own always-visible panel at the bottom of the detail column, below the deck panels in every deck layout; the Main deck body does not contain these sections. The panel is shown only while the current document has numbered targets — never for "No agent selected", nodes without rosters, fully unnumbered rosters, or file-hint documents — and it stays visible during deck search (,/), since the digits keep working there. It is collapsed by default to at most two packed rows, where each visible number carries its roster label, shortened with a middle ellipsis only as far as it stays distinct from every other label (the packer shows fewer targets rather than ambiguous ones). When targets do not fit, the last cell is a dim +N count (with a lit ▶M when M in-flight targets stay hidden); digits still reach every numbered target, including those counted in +N. In-flight targets fill the visible slots first, still in number order, and render lit in every view (see above). . expands the panel to the full roster sections, with their fold-driven annotations; expanded, it grows to at most 40% of the detail column, scrolls, and returns to its top when you select a different row. The border title is the color legend (JUMP, then one entry per section with its number range), and the border subtitle names the configured toggle_agent_jump_panel key (▴ . more / ▾ . less). Each collapsed cell echoes its roster row: the number chip in the roster style, the label, and the status glyph; a dismissed neighbor renders dim with a ⊘ prefix (and a revive note when narrowed), since its digit revives the agent instead of jumping. After the first digit of a two-key jump, the panel narrows to the matching candidates (JUMP · 1▁, esc cancel), or shows no targets start with <digit>; completing or cancelling the jump restores the collapsed or expanded view. Collapsed/expanded state is per session and holds across row moves, tribe focus, and layout changes. Toggling never rebuilds the document, and a bottom-pinned body stays pinned. Roster headings are not cards; za/zA cannot target a roster section or roster row (rosters follow the global panel fold keys zz, zZ, and the direct level keys); and ,/ deck search does not cover roster rows.
  • SASE CONTEXT / BEAD: Shown for epic phase workers and task workers. For an epic phase worker, the lane is limited to its selected phase. Its fields are Phase Title, Description, Size, Epic Plan, and Epic Title, in that order. The phase title comes from the same validated, frontmatter-ordered phase entry, is normalized to one line, wraps losslessly, and renders a quiet unavailable for missing, unreadable, damaged, or out-of-range entries. Exact validated sizes use literal blue small, gold medium, or rose large chips; missing/unreadable/damaged plans, explicit invalid sizes, and out-of-range phase ordinals also show a quiet unavailable size. Modern explicit phase metadata avoids bead-store reads. The parent goal, dependencies, and peer phases are never rendered, and the parent plan does not become a generic artifact. For a task worker, the fields are Task Title, Description, optional Notes, Size, optional +1 Reports / +1 Evidence, and Created. A multi-line Notes value (both bead types) or +1 Evidence value (task only) collapses to a one-line N lines (zz to show) digest at metadata fold level 1 and renders in full at levels 2-3; single-line values never fold.
  • SASE CONTEXT / PLAN: Shown for the epic-authoring planner, epic lander, and task workers with a distinct authored plan when direct metadata or a confirmed legacy epic association resolves a plan. Phase workers deliberately omit the parent epic lane; no goal or peer roadmap phase is rendered. A task bead's own design field is never rendered as the task worker's PLAN lane. For plan-bearing roles, the body rows are Title, Goal, and canonical Path, in that order; a tale additionally gets a Size row between Goal and Path (the authored xsmall/small/medium chip, or a defaulted medium chip when the tale's size was missing or an over-sized legacy large/xlarge normalized at launch). The lane header carries the effective tier (plan, tale, or epic) and an epic's phase count. An approve action displays plan, tale and legacy commit-only actions display tale, and an epic action displays epic, even when the corresponding commit or launch later fails. Without action metadata, a valid authored tale or epic supplies the tier; legacy committed plans without a readable authored tier display tale, and unresolved values display tier unavailable. Canonical path selection remains separate: committed paths are workspace-relative, while pending or explicitly uncommitted paths use the home-shortened machine-local archive. Valid authored epics then show every phase in authored order with its title, fixed-width literal size chip, ID, dependency IDs, optional model, and optional description; these are static roadmap ordinals, not progress indicators. Launch-consumption validation normalizes only an omitted historical size to small; explicit invalid sizes remain unavailable. The chip stays visible while the title and every other value wrap without truncation in the normal panel and zoomed deck view, and logical text exposes the same labels to deck search and copy. Only the path participates in file hint mode. Invalid known epics show phases unavailable in the lane header without leaking partial entries; tales do not show a phase roadmap. A plan alone renders SASE CONTEXT; across every combination of present lanes, the full order is PLAN, BEAD, ARTIFACTS, MEMORY, GLOSSARY, SKILLS, then WORKSPACES, with absent lanes omitted once they resolve and still-resolving lanes holding their slot with a dim resolving… row.
  • SASE CONTEXT / GLOSSARY: Shown directly after MEMORY whenever the selected agent or session has at least one audited event under the retired, pre-web sase glossary read command's legacy log. Current sase memory read glossary:<keyword> reads are not legacy events, so they surface in the MEMORY lane like any other memory read; this lane is purely historical (see Memory Webs). That lane's hints open the requested memory files (see Audited Reads). The lane header counts reads and distinct requested terms, adding the agent count for a multi-agent session. Each row shows the read's requested terms (truncated the same way MEMORY truncates paths), with a +N related suffix when the closure expanded past the requested terms, and the recorded reason on its own indented line. A numbered hint pages a generated report of that read's output — the reproduced command line, recorded metadata, and the resolved term closure. @ opens the report and % copies its path; the report names the recorded sase/sase.yml source, since that legacy format predates the strand migration. Loading, attribution, and the mtime/size snapshot cache mirror MEMORY's reference implementation; the lane is skipped rather than rendered empty when there are no reads to show.
  • SASE CONTEXT / ARTIFACTS: The plan-adjacent lane groups Beads, Reads, Commits, Deltas, and Files as compact fields, preserves that internal order, and summarizes only the present fields in its header. Beads comes first and lists every bead the agent interacted with, newest first: each row shows the local time, one verb glyph, the full bead ID, an amber ▐CREATED▌ pill for beads the agent created (a standing close keeps its green/grey ▐CLOSED▌ pill priority and adds a compact created chip instead), and verb chips (assigned first for assignment-only rows, then durable verbs such as noted or closed, then read, then viewed; repeats render ×N). Indented ↳ lines show the bead title when available, then a labeled why: filing-reason line for created rows (the bead's creation reason), then the standing-close reason and the newest audited read reason with explicit closed:/read: labels; rows with none of these omit ↳ lines. assigned marks the agent's assigned phase, epic, or sase bead work bead even when it was never touched (such rows show the already-resolved title when available, have no timestamp, and sort last); it never claims the agent created the bead. At most the newest five rows render; when there are more, a dim + N more · HH:MM earliest footer reports the rest. A numbered hint opens the bead's live detail (the sase bead show view) in sase's pager, % copies the bead ID, and @ is not supported for bead rows. Session rows add the producer label when every contributor to that bead shares one producer, and clan rows include their members' beads (de-duplicated and labeled per member). Audited bead: reads live here and are excluded from Reads. The data and glyphs match sase bead touched. Reads is the input side of the lane: each retained audited sase artifact read (including when artifact links are disabled) appears newest-first with local time, the canonical reference, the recorded reason on a wrapped continuation line, and — on a session row — the compact producer label. The header counts every retained read event; the newest five rows render and a dim + N more · HH:MM earliest footer reports overflow. Repeated reads of the same reference stay separate. Prompt citations and silent show / path / open commands never appear. A read with a recorded resolved path participates in hint mode; a pathless or legacy row still renders its reference and reason, consumes no hint number, and never triggers live reference resolution. Commits persisted by the selected agent's post-run steps are grouped by repository; primary workspace, linked-repo, sidecar, and external-repo commits retain their repository identity. Deltas preserve their green +, gold ~, and red - change glyphs and group linked or external files by repository. Artifact type remains visible through its icon shape, while every artifact icon, read row, and path uses the shared blue output-lane/file-path palette. This lane starts painting on the very first navigation frame: Commits is derived from the selected agent's in-memory step metadata and needs no disk reads, so it renders immediately, while Reads, Deltas, and Files — which do need store reads — fill in when the debounced enrichment resolves the lane. The immediate commit-only view is deliberately not cached, so the full lane still resolves on its normal schedule. An agent whose only artifact context is a read still gets SASE CONTEXT and an ARTIFACTS lane.
  • Slow tool calls: The metadata header lists tool calls that took 20 seconds or longer, ordered by start time and capped at 8 rows (an overflow line points to the full LLM Calls panel timeline via p then t). Level 1 is a compact triage table: every row keeps its timestamp, state, tool, duration, and a short path-, query-, or command-aware digest, while a dim tail reports that full commands are hidden. From position 2 upward, each row adds the complete command or target in an indented block that wraps with a hanging indent, plus start/end and outcome facts and any error. The lane's last position also adds output previews, subagent tool/token statistics, and each call's rank and share of selected slow time. These tiers are positional: an ordinary agent uses compact/detail/full across its three levels, while a session uses compact/full across its two. za and zA can change only this section. For a root agent the list aggregates calls across its children while attributing each call to the child that made it.
  • Wait state: For a WAITING agent gated by %wait, a duration wait, or an absolute-time wait, the detail view shows a tagged Wait: block with one lane per active dimension: [agents], [beads], [time], then [capacity]. Present tags occupy a padded gutter, so every value begins in one aligned column and long dependency lists wrap with a hanging indent beneath that value column. The [agents] lane lists the dependency names recorded on the waiting agent, adds per-name status badges for currently known agents, clan containers, or session containers, and marks unknown names with ? so typos and stale references are obvious. When a waited-on target is followed into an epic it launched, the [agents] lane narrates the hand-off in place with a teal ↪: planner ✓ ↪ sase-7k ◐ in progress · 2/5 phases · since 14:32 while following, ↪ epic launching… while the launch is in flight, or ↪ ! epic launch ended without an epic · resume: … when blocked. A canceled or superseded followed epic shows its resolution in place of the status word. Followed epics are never repeated in [beads]; that lane lists only authored bead waits. The [beads] lane uses the same status-bearing token as the compact row, without a count: run-bead ◐, done-bead ●, and bead-id ? for an unknown bead. A WAITING list row keeps agent and bead counts independent while placing matching bead statuses after their present agent status (▶1 ◐2, ✓1 ●1); unmatched bead tokens trail in bead order. Unknown targets can render as adjacent independent counts (?1 ?2). Timed-only and capacity-only waits do not receive those markers. Timed waits add compact duration, target time, and countdown text when available. An authored capacity on a QUEUED row shows as the same cN badge used elsewhere, and its detail context reports occupied capacity against that row's own admission budget. A QUEUED detail uses a separate Queue: line led by its rank and elapsed time since slot_requested_at, followed by cap context. It deliberately suppresses the marker's stale dependency, bead, and time-wait fields.
  • OUTPUT VARIABLES: Small JSON-shaped values written by the selected agent session with sase var set and by sase artifact create (the SASE-managed artifacts list). Strings, numbers, booleans, null, lists, and nested maps retain their types. A single contributing agent renders as a flat sorted key/value block; multiple session members render with compact role labels so root, planner, coder, tester, and follow-up values stay attributable. Lists, maps, and multi-line strings use an indented YAML-shaped block with type-specific colors. The section is omitted when the session has not published variables. These values are stored in agent_meta.json, so they are visible metadata rather than secret storage.
  • TRACEBACK: When an agent or workflow step recorded an error traceback, it renders under its own TRACEBACK heading after the prompt, directly above the reply heading (AGENT REPLY while running, AGENT CHAT once done or failed, or STEP OUTPUT for a workflow step), and is a Ctrl+J/Ctrl+K stop.
  • AGENT REPLY: The agent's live or completed reply content, streamed from live_reply.md during execution and read from the artifacts directory after completion. When per-turn reply timestamps are available (recorded in live_reply_timestamps.jsonl), the reply is displayed with timestamp dividers between each agent turn. While the Agents tab stays on that row, one in-flight reply follows those two files in place. The Reply card replaces only that live reply body; the rest of the detail document stays put, and a change to those two files does not refresh the agent list. The followed row is a selected running agent that is still executing. On a sequential session, that is only the current in-flight turn, and only when that turn itself qualifies; otherwise nothing in that session is followed. A row that is not a sequential session follows the row itself, or the latest of its follow-up agents, when that candidate qualifies. Bash and Python workflow steps are never followed. The first paint of an empty solo reply says Waiting for agent response.... An empty session phase first says No response content yet.. The follow leaves that first-paint text in place until a timestamp chunk or reply text exists. If a reply that was already followed is cleared, the body becomes Waiting for agent response.... Hint mode keeps the first-paint text because the follow stays off. A change to either file schedules an update, and a status check of both files about once a second is the backstop. While the follow is idle, later updates are at least 0.3 seconds apart; the first update can be immediate. The follow waits while you are navigating or the prompt bar is open, and retries every 0.25 seconds. It stops for this row when you leave the Agents tab, when the detail view is pinned to one prior attempt number, or while hint mode is showing. Changing the selection cancels this follow; the newly selected row starts its own when it qualifies. D switches between the merged history and the current attempt only. That switch ends the current follow, and the view you land on starts its own when it still qualifies and no attempt number is pinned. If either file changes while it is being read, that snapshot is discarded and the follow tries again. For agents with follow-up phases (planner, feedback rounds, coder), the AGENT REPLY section consolidates replies from all phases into a single view with phase dividers showing each phase's label and start time. Phases follow the session's chain order: a monitor phase renders immediately after the turn that started it, including a monitor started by the session root, which renders after the root's own phase. Agent-turn members follow one rule, AGENT (<role>), derived from the member's session role: --plan renders as AGENT (plan), --code as AGENT (code), --epic as AGENT (epic), --commit as AGENT (commit), and numeric feedback suffixes such as --2 as AGENT (plan round 2). Custom session members render the same way with their suffix token, e.g. AGENT (bar). A monitor member is a named proc, so its phase renders as an amber ⚙ MONITOR divider followed by the monitor's command, its recorded detail fields, and its full captured output — the same block the monitor's own panel shows. A gate-turn member renders as a lifecycle-colored ⋔ GATE divider with its decision, kind, state, deadline, reason, request identity, branch policy, follow-up disposition, and captured command output. Its phase remains in the consolidated session reply after settlement, including terminal branches that intentionally launch no successor. Legacy dotted and single-dash suffixes render the same way.
  • WORKFLOW VARIABLES: macro workflow output variables from step outputs with additional meta_* keys are grouped under a dedicated header. The special routing keys meta_project, meta_patch, and meta_workspace are promoted into the normal header fields; meta_changespec remains accepted as a legacy alias for meta_patch. Other metadata keys are title-cased and shown in this section.
  • PROMPT: For agents launched from a multi-agent (----separated) prompt, the final, planner, and question transcripts include a PROMPT: row linking the saved original launch prompt (stored under ~/.sase/.../multi_prompts/), so the exact text that fanned out into every segment stays recoverable.

g / G, Ctrl+D / Ctrl+U, and the bottom pin act on the focused deck panel.

Agents Tab LLM Calls Panel

The Tools deck's LLM Calls card shows a chronological timeline of the LLM tool calls the selected agent has made — file reads, edits, bash invocations, web fetches, sub-agent launches, and so on.

Entries are read from the tool_calls.jsonl artifact in the agent's run directory. Each call renders as one timeline row:

  • A status label colored by outcome — ok (success), fail (error), stop (interrupted), agent (sub-agent launch), or wait (the post-call record has not arrived yet).
  • The tool name, optionally followed by a compact target (such as the file path the tool acted on) and the call's duration.
  • A short preview of the call result on the next line, when the collector captured one. Command-output previews keep a marked suffix with at least the final 50 logical lines; the character budget is soft so unusually wide trailing lines remain complete. Other preview types remain head-oriented.

The panel header shows the total call count, the failure count, the interrupted count, and a timestamp for the most recent reload. While a background reload is in flight (because the artifact changed on disk), (refreshing...) appears next to that timestamp. The body shows No provider tool-call artifact available when the file does not yet exist for this agent and No tool calls recorded when the file exists but contains zero records.

The timeline has three detail levels: compact, expanded, and full. While the focused deck panel shows the Tools deck, l / h step the level up / down and L / H jump to full / compact, taking priority over their usual fold actions.

For retry chains and planner-to-coder follow-up sessions, the panel aggregates tool_calls.jsonl from related artifact directories so the selected logical agent shows one ordered tool timeline. Discovery uses the persistent artifact index when it is available; if the index is missing or stale, sase's TUI falls back to direct lineage pointers plus a bounded scan of nearby legacy sibling artifacts.

Records are produced by writers that share one normalized on-disk format. Claude uses the SASE tool-call hook collector as the preferred source and keeps its stream-derived parser as a fallback when hooks are unavailable. Codex writes equivalent rows from its codex exec --json stream with runtime: "codex" and source: "stream"; current Codex start/completion events can show pending rows, result previews, failures, interruptions, and durations, while older completed-only function_call rows remain readable with more limited detail. Qwen writes stream-derived rows from its --output-format stream-json output with runtime: "qwen" and source: "stream"; start/completion (and Qwen's tool_use / tool_result) pairs collapse into single rows the same way Codex pairs do. Muse Code writes stream-derived rows from its muse exec --json JSONL stream with runtime: "muse" and source: "stream"; proposed/scheduled and tool.result pairs collapse into single rows like the others. Muse's stream never carries tool arguments, so a Muse row's input target comes from edit_facts.path for edits, the parsed command for bash, and otherwise a bounded preview of the result text — SASE does not invent arguments Muse did not emit. Antigravity (agy) runs in plain-stdout mode; SASE never scrapes display prose, but supported Antigravity versions may contribute guarded source: "trajectory" rows from the local trajectory DB. When that extractor is unavailable, the panel simply shows nothing for agy runs. Grok Build writes stream-derived rows from its streaming-messages-json output with runtime: "grok" and source: "stream"; Grok's native tool names (run_terminal_command, read_file, search_replace, and so on) are mapped onto the same canonical display names Claude rows use, and its JSON-encoded tool_result envelopes are decoded for exit codes and file paths rather than shown raw. See LLM Providers — Claude tool calls, LLM Providers — Codex tool-call capture, LLM Providers — Qwen tool-call capture, LLM Providers — Muse tool-call capture, LLM Providers — Antigravity (agy) Integration, and LLM Providers — Grok Tool-Call Capture for provider integration details.

Agents Tab Tool Runs

The Agents tab surfaces the machine-local ToolRun ledger (see Named Tools and ToolRuns) at three zoom levels: a live-only ⚒ chip on the owning row, a header chip plus Tool runs field on the selection, and the Tools deck's ⚒ Runs card for diagnosis. Project-wide runs live in the Admin Center Tools tab. ⚒ (U+2692) means ToolRun everywhere — row, header, card, pane, notification icon, and Procs cell. The old chop/Services link-trail icon moved to ⏲ so the two are never confused. Say "tool run" or "ToolRun": never "tool call" for a ToolRun.

The view is read-only. The TUI never reconciles, settles, or writes to the ledger on any UI path: stopping a run goes through the explicit stop flow below, and everything else only reads.

State vocabulary

Every state shows glyph + word + color together, so meaning never depends on color alone:

State Glyph Words (examples) Color
running ⚒ check 7/11, check 2m, check starting, check stopping bold Tools accent #87D7FF
silent ⚒⚠ check silent 4m bold #FF5F5F
pass ✓ pass #5FD75F
new failures ✗ 3 NEW · 1 KNOWN #FF5F5F
known only ≈ known only · 2 KNOWN #87AF87 (calm: the known red, not yours)
undetermined ? 2 UNKNOWN, or untriaged #FFAF5F
killed ⊘ killed at 9m00s · signal, timed out at 30m00s #D75FFF
stopped ⊘ stopped at 1m02s dim
lost ⊘ lost · wrapper_lost dim #FF5F5F

A live run is one in state created or running. A live run is silent when nothing was heard from it for 60 seconds (six missed samples): the TUI says silent in red and never claims an outcome. Severity picks one chip among several tools — new_failures > undetermined > killed > lost > stopped > known_only > pass — with live always outranking settled and silent outranking live. Ages format as <1m, Nm, Nh, Nd.

At a glance: the row chip

A row carries a ⚒ chip only while one of its runs is live; settled history never marks the row. The chip shows the tool label plus progress against the reference run (check 7/11), elapsed minutes (check 2m), starting for a created run, or stopping once a stop is requested. All times are minute-resolution snapshots — a relative time that doesn't tick would be a lie, and rows don't repaint on a tick.

Attribution is by node kind, never by name guessing. A monitor-owned run chips the monitor row, not its starter; a named-proc run chips the proc row. History still shows both relationships (a handed-off run is labeled → monitor <name> or → proc <id>). A session container inherits its members' most severe live chip. Rows on remote machines get no chip: the ledger is machine-local, so absence must never read as "no runs".

In context: the header chip and Tool runs field

The selected node's header chip answers "did its latest check add NEW failures?" at a glance: a live run wins over settled history (silent first), otherwise the most severe settled run per label wins, with +N when other tool labels exist. Live elapsed turns amber past the run's typical duration; settled times are absolute (16:21), never relative. The expanded Tool runs field lists each run with its verdict line, and %r (copy mode) copies the focused run's id.

To diagnose: the ⚒ Runs card

One block per run, newest first; ( / ) step between blocks. Selecting a node lands on the newest block. A reader sitting on the newest block follows new arrivals; a reader who moved back holds position and sees an arrival dot. A nested run renders as a ↳ line inside its parent block, never as its own block.

Each block shows the outcome and context lines (tool label, verdict, argv, owner), a stage waterfall, triage items with cross-run and cross-agent witness counts, child runs, and a bounded log tail. Three detail levels come from h / l (full / compact via H / L), routed by the active card — while the Runs card is active those keys drive run detail, not folds. When the detail or the log is pruned the block says so honestly (detail pruned · summary retained) instead of showing an empty success.

A live block progresses in place: elapsed repaints once a second, pending stages come from the reference run, and the block settles in place when the run does — no full refetch unless the glance drifts. v hint mode gains one ⚒ run log <8hex> target per visible block, opening the retained log in the pager. A node with no runs and no calls shows No tool runs or LLM calls for this node; on a remote row it says ToolRun history lives on <machine> instead.

An LLM Calls row whose command ran sase tool run (or a wrapping sase monitor start) gains a verdict suffix that clicks through to the run's block. A sase tool run row in the Main deck slow-tool list gains a live-stage or verdict suffix. Monitor and named-proc Context cards gain a clickable Tool run row. The run itself is never copied into those surfaces — they link to it. In v hint mode each linked run also offers a ⚒ run <8hex> jump target alongside the ⚒ run log targets.

Stopping a run and running a tool

The command palette offers Stop live tool run and Run project tool… whenever a local live run (for stop) or a local node (for run) is in play.

Stopping asks for confirmation in a DANGER dialog with Cancel focused, and the copy names the consequence for the owner: an inline agent run's sase tool run exits 143 (its agent keeps running and sees a failed check), a monitor-owned run's monitor stops and its follow-up agent never launches, and a hand-off proc is killed with the run settling as stopped. A nested run resolves to its outermost stoppable ancestor first. The stop itself runs as a durable proc (sase tool stop RUN -j with a typed result), so it appears in the Procs tab and survives quit.

Run project tool… confirms the argv and the root, then hands sase tool run -H <tool> off at the current project's primary checkout root through a session worker. When that run settles, an OpenToolRun notification arrives; selecting it jumps to the run's Agents-tab node with its ⚒ Runs card active and the run's block selected (or opens Admin Center → Tools focused on the run).

Plan Workflows

When an agent submits a plan via /sase_plan (or sase plan propose, including the %auto:epic path), it enters a planning phase before executing:

  • TALE / EPIC — The agent has submitted an authored tale or epic and is waiting for user review. Tales are pink/magenta; epics are orchid. PLAN is the compatibility fallback when the authored tier cannot be resolved.
  • PLAN APPROVED — The plan has been approved and the follow-up agent has been spawned. Shown in cyan/turquoise.
  • PLAN REJECTED — The plan was rejected. A no-feedback rejection from sase's TUI or sase plan reject writes the rejection response first, then attempts to dismiss the notification, user-kill the matching planner, and persist dismissed-agent state so the row is hidden on refresh. If the matching row is already gone, the plan is still rejected. Rejected archived plans can still appear in history-oriented views, and redundant completion notifications are suppressed.

Plan files generated by the agent are displayed in the file panel alongside other agent artifacts. Plan approval notifications include the LLM provider and model name, so users can see which model proposed the plan (visible in both the TUI notification modal and Telegram delivery).

sase's TUI arrival toast names the authored tier — Tale ready or Epic ready — instead of using a generic Plan label. An epic toast adds the validated phase count, dependency wave count when available, and the non-zero per-size counts (XS, S, M, L, and XL). Those values are captured when the approval gate is created, so a notification that is snoozed and later resurfaces keeps its original summary even if the bundled plan was edited meanwhile. Batched warning toasts likewise count tales and epics separately.

When sase plan propose writes the plan, it also touches ~/.sase/.ace_refresh_pulse to wake any running TUI immediately — the tier-aware TALE or EPIC status (or fallback PLAN) appears without waiting for the next auto-refresh tick. The pulse file is consumed by the inotify artifact watcher (see Auto-Refresh) and is harmless if no TUI is open.

Root plan workflows also surface their tier-aware pending status when a re-proposed plan is still awaiting review. Plan and feedback timestamps from feedback-round children (--2, --3, ...; legacy -2, .2, etc.) propagate onto the root entry, and whenever the root's latest plan timestamp is newer than its latest feedback timestamp the override engine restores TALE, EPIC, or fallback PLAN over a RUNNING or DONE label. This applies only to root plan workflows that have not yet spawned a terminal follow-up (--code, --epic, ...); once a terminal follow-up is launched, the parent moves on to PLAN APPROVED (or the matching follow-up status) instead.

The Plan Review modal title shows a provider-themed PROVIDER(model) badge between the "Plan Review" label and the plan filename — orange for Claude, lime for Codex, Antigravity indigo (#6E5DE7) for agy, neutral muted for other providers. The badge is omitted when provider/model metadata is absent, leaving the legacy title shape unchanged.

Whole-document Markdown previews in plan, launch, and custom-gate review modals highlight leading YAML frontmatter as YAML and the remaining body as Markdown. Highlighting does not alter validation or the reviewed file contents.

Plan Decisions (see Plan Decisions) add typed choices and toggles to plan frontmatter. The modal renders them in a Decisions section above the verdict: change a value with Space, h/l, or r, and the primary action approves exactly the values on display. decision_<id> fields are treated as already collected, so they do not appear as extra inputs either. Changing decision definitions through the gate's plan editor is refused. Override a default outside the modal with sase gate answer and --set decision_<id>=....

For tale plans, the modal's primary Approve decision includes two independently selectable add-ons: Commit plan file to the plans sidecar and Run coder follow-up. Both are selected by default. Press enter to approve with the current checkbox selection, move to another decision (reject or feedback) with j / k and submit it with Ctrl+S, or press c for Custom Approval.

sase's TUI submits the decision as a tracked sase gate answer proc labeled Plan response: <choice>, so a slow or failed submission shows up in the Procs tab and an error toast rather than blocking the TUI; the inbox and Agents rows refresh once the answer is recorded.

For the Tale and Commit choices, SASE publishes the reviewed plan to the archive before the approval response is made terminal. The response carries a canonical plan: reference, so a coder launched from a different numbered workspace resolves the same archived plan without depending on the approver's checkout path. If publication fails, SASE does not write the terminal response. The PlanApproval notification remains actionable and can be retried after the archive problem is fixed.

The same pending approvals are available from the CLI. Run sase plan to see pending proposals, recent approvals, and inferred rejected archived plans; run sase plan approve <name> --kind approve|commit|epic|tale or sase plan reject <name> to write the same response protocol used by the TUI modal. Use the plan name from a Proposed row (the id_prefix still works); if the selector is omitted, the CLI acts only when exactly one proposal is pending. Omitting --kind uses the plan's authored tier. In the Plan Review modal, enter uses that same authored-tier default; use Custom Approval to pick a different outcome. approve starts the coder without committing an SDD plan, tale commits the plan as an SDD tale and starts the coder, epic commits the matching SDD tier and launches the bead follow-up, and commit records the approved plan in SDD without launching a coder. -m/--model picks the follow-up agent's model, while -p/--prompt adds extra coder instructions for the approve and tale paths. Tale and epic choices validate the plan against the target schema before consuming the approval; failures surface an error and keep the notification actionable. CLI rejection also attempts the durable planner cleanup used by no-feedback TUI rejection.

On an active agent, A toggles bare %auto, and the change takes effect at the next gate. If auto-approval is off, it turns it on exactly as if the agent had been launched with %auto. If any auto-approval is on (including a launch-time %auto:tale / %auto:epic), it turns it off, and the next plan or question gate parks for review.

The agent's next submitted plan is approved at its authored tier. A tier: tale plan is approved and committed as a tale; a tier: epic plan is approved as an epic and follows the epic follow-up path.

The row shows ⚡ while enabled, and the footer label switches between auto-approve and unapprove.

Like bare %auto, it also auto-settles question gates.

Plan Approval Keybindings

Key Action
j / k Move through Actions, then Decisions, then Verdict
Space Next choice (wrapping) or flip a toggle; on an AND member, toggle
l / Right Next choice, or set yes
h / Left Previous choice, or set no
r Reset the focused decision row to ★
R Reset every decision row to ★
Enter Submit the primary (approve) decision with the values on display
Ctrl+S Submit the focused decision (for example reject or feedback)
1–9 Submit the correspondingly numbered decision
i Open the input panel for the focused decision's note or declared fields
c Open Custom Approval
e Edit the plan file in $EDITOR
y Copy the plan file path to the clipboard
Y Copy the plan content to the clipboard
d Open Gate Debug
Ctrl+D/U Scroll plan content down / up
g / G Scroll to top / bottom
q / Esc Cancel

The navigation, submit, and input-panel keys are the shared gate-modal keys; see Remapping Gate Modal Keys.

The question modal also supports y to copy questions and selected answers.

Custom Approval

Pressing c in the plan approval modal opens a custom approval dialog. Choose the approval outcome directly: Approve, Tale, or Epic. These choices map to the same response protocol used by external approval transports: Approve runs the coder without asking the runner to commit an SDD plan, while Tale and Epic commit the plan under the matching tier in the resolved SDD plans root's <YYYYMM>/ directory. The root may be in-tree, a legacy .sase/sdd/ clone, or the split --plans sidecar; sase repo path plans prints it.

Key Action
Enter Choose the highlighted action
a Highlight Approve
t Highlight Tale
e Highlight Epic
m Select coder model
p Edit additional coder prompt
w Edit wait dependencies
c Edit capacity (Epic only)
Ctrl+N/P Next / previous action
q / Esc Cancel

The dialog keeps the custom coder prompt and follow-up model controls for Approve and Tale. Epic approval reads its land and phase models from structured plan frontmatter and launches bead work directly, so those controls are hidden for Epic:

  • Additional prompt — Optional extra instructions for the coder follow-up. It is used by Approve and Tale.
  • Wait for — Optional comma-separated agent names and bead=<id> entries. Press w to edit it. The dialog validates the same grammar as sase plan approve --wait before returning to the approval screen. Approve and Tale hold the coder follow-up; Epic holds the launched bead work.
  • Capacity — Epic only. Press c to set a per-launch runner-capacity budget for the launched bead work; blank keeps the default queue behavior and 1 runs it alone.
  • Coder model — Select an LLM model for the next follow-up agent instead of using the role default. For Approve and Tale that agent is the coder. Shows all registered models grouped by provider (Claude, Codex, Antigravity, Qwen, OpenCode, Muse Code, Grok Build) with a "Custom..." option for freeform input. A model its provider flags with an advisory carries a warning-styled ⚠ <label> suffix on its row (ⓘ for informational advisories), with the full advisory sentence in the row's secondary text — this is where somebody actually chooses a model, so the trade is impossible to miss. See LLM Providers — Model advisories. Type to filter by provider, model id, label, or short alias; use j/k or arrows to navigate, Enter to select, Esc to clear the filter or cancel, and ' for jump hints over the visible selectable rows. The displayed default resolves to the model the handoff will actually use: for Approve and Tale, the validated tale size selects the corresponding @<size> alias directly, with legacy sizeless tales using @medium. Selecting a specific model and then re-opening the picker and choosing "Follow-up default" resets the follow-up back to that role default (distinct from pressing Esc, which keeps the current selection).

The custom approval dialog no longer exposes separate commit/run switches because the selected outcome determines the commit location and follow-up behavior. Additional session members are launched explicitly with %i(suffix, session=parent); they are not selected at the plan gate.

Launch Approval

Launches requested by a running agent (see Agent-initiated launches) arrive as priority notifications with a LaunchApproval action. Selecting one opens the launch approval modal, which renders the request's human-readable preview (launch_preview.md). Clan slots identify their rootless clan alongside the model, kind, and planned member name. Press a to approve, r to reject, and q or Esc to cancel. sase's TUI resolves the same hash-verified command bundle used by mobile and remote callbacks, while retaining legacy launch-request fallback. The CLI equivalents are sase launch approve <selector> and sase launch reject <selector>. From inside an agent, sase launch request creates a LAUNCH gate turn and ends the requesting turn; the process does not wait for the response. By default, approval, rejection, timeout, or gate/dispatch failure settles the turn and resumes the original requester as one session successor. That successor receives the decision, reviewer feedback, typed dispatch result, requester/workspace/session identity, and checkpoint; a stopped gate does not resume. A request can explicitly choose terminal_handoff when every branch should end without a requester continuation. Outside a SASE agent, terminal handoff is the default: the command registers the gate, prints its creation descriptor, and returns. Automation that needs the terminal gate result can then run sase gate wait -i <request-id> -k launch -j.

Agent Holds

An agent hold is a durable reverse wait: while it is active, matching WAITING or QUEUED agents, later launches, and undispatched procs are kept from starting. Holds are armed with sase agent hold or the %hold directive. sase's TUI shows holds in three places:

  • Agents tab rows. A QUEUED row parked by a hold appends held by <armer> after its queue position. If a held agent and the agent that armed the hold end up blocking each other, SASE also sends a Hold deadlock notification; the hold's TTL still guarantees progress, but you can release the hold or kill one side sooner.
  • Launch-preview confirmation. Submitting a prompt with a broad %hold — one that combines future with scope=host, or whose pending capture would freeze more WAITING and QUEUED agents than agent_hold_confirm_capture_threshold (default 10) — opens an Arm this hold? confirmation before anything launches. It lists each broad hold's directive and live pending capture, plus a warning for a host-wide future hold. The accepted prompt is already represented by a preparing proc row. Cancelling restores it when safe, or stashes it when another prompt or modal has focus; a standalone hold confirmation defaults to Cancel. Narrow holds launch without asking. During the current beta, pressing Arm only confirms the launch preview; it does not write a hold to the store.
  • Holds pane. Open SASE Admin Center with #, then Config > 03 Holds (0 then 3; 02 when admin_center_flags is off). Each row shows the armer and its kind, the scope (host or project:<name>), the selectors (names=, hoods=, tribes=, future, and pending=N), and the time until expiry. Press j / k (or the arrow keys) to move, d to release the highlighted hold, r to reload, and q or Esc to close Admin Center.

Linked Chats in Multi-Step Workflows

When a workflow spawns multiple agents (e.g., a planner step followed by a coder step), the chat history files for each step are cross-linked via a ## Linked Chats markdown section. This section is inserted near the top of each chat file and lists all related agents with their roles and file paths, making it easy to trace the full workflow from any individual agent's chat history.

For example, a plan-then-code workflow produces chat files with:

## Linked Chats

- **1. planner** — `/path/to/planner_chat.md`
- 2. coder — `/path/to/coder_chat.md`

The current agent's entry is bolded for quick identification.

Retry/Fallback Display

When an agent encounters a retryable error (configured via llm_provider.retry), the Agents tab shows retry state:

  • RETRYING — Shown in bold orange when waiting before the next retry attempt. Includes a countdown timer: RETRYING (45s).
  • ↻N — Shown after the status for running agents that have retried. The number indicates how many retries have occurred (e.g., ↻2 means two retries so far).
  • ▸Model — Appended to the retry annotation when the agent has fallen back to an alternate model (e.g., ↻3▸flash).

Prior Agent Attempts

Every time the axe retry loop retries an agent — context-limit retry, provider/API-error retry, user-configured retry, or fallback-model switch — the failed attempt's partial reply, error text, timestamps, and model are snapshotted under <artifacts_dir>/attempts/<N>/. The AGENT REPLY area in the Agents tab renders these prior attempts inline with styled dividers before the current/final attempt, so the full arc of the agent's work stays visible in one scroll.

sase's TUI hydrates prior-attempt history lazily. Normal Agents-tab refreshes do not enumerate every attempts/<N>/ directory; the selected detail panel, D attempt-view toggle, and content search hydrate the needed attempt records on demand.

Press D to collapse the view to the current attempt only; press D again to re-expand. The binding only appears in the keybinding footer when the selected agent has one or more prior attempts.

Custom Keymaps

All TUI keybindings are configurable via the ace.keymaps section in sase.yml. You can remap app-level, gate-modal, Memory-panel, and focused Statistics-pane keys and define entirely new prefix-key modes.

Remapping Built-in Keys

Override any app-level keybinding under ace.keymaps.app:

ace:
  keymaps:
    app:
      next_patch: "n" # Remap j -> n
      prev_patch: "p" # Remap k -> p
      edit_query: "f5" # Structured query on Artifacts and Agents
      show_notifications: "N" # Remap i → N

Agents deck search is independent: remap ace.keymaps.modes.leader_mode.keys.search_forward to change the subkey after the configured leader prefix. Structured query editing on every query-capable surface, including Agents, is ace.keymaps.app.edit_query. Stale app.search_forward and leader_mode.keys.edit_query overrides are ignored with a warning pointing at those replacements.

Remapping Statistics Pane Keys

Override focused Statistics bindings under ace.keymaps.statistics:

ace:
  keymaps:
    statistics:
      prev_view: "left_square_bracket"
      next_view: "right_square_bracket"
      select_view: "0"
      jump_to_entry: "apostrophe"
      cycle_range: "f11"
      cycle_range_reverse: "shift+f11"
      custom_range: "c"
      cycle_group: "g"
      cycle_project_filter: "p"
      cycle_project_filter_reverse: "P"
      focus_macro: "x"
      clear_macro_focus: "X"
      scroll_down: "ctrl+d"
      scroll_up: "ctrl+u"
      refresh: "f10"
      help: "f9"

The example above names every Statistics binding, but only some are remapped: it shows cycle_range, cycle_range_reverse, refresh, and help moved off their t, T, r, and ? defaults, and repeats the defaults for the rest. Override only the keys you want to change.

These keys dispatch only while the Admin Center Statistics pane is focused. They may overlap app-level bindings without creating a global conflict, and the pane's hint bar always shows the effective keys. Press the configured select_view prefix and then 1–8 to select the matching view displayed as 01 through 08; bare digits continue to switch the Admin Center's top-level tabs. jump_to_entry arms that same numbered-view selection, which is how the Admin Center-wide ' behaves on a pane that has no row cursor to jump between — the visible strip numbers serve as its hints. The group control is visible and active only in Projects, Macros, and Perf. On the Macros view, the focus key opens a filterable picker and the clear-focus key restores All macros. Project filtering cycles through All projects and the latest cached unfiltered ranking: the configured forward key moves toward the first ranked project, the reverse key moves toward the last, and both wrap. First open seeds the current project when ace.current_project.seed_filters is on; either key can cycle away from that seed. Either key clears an active project filter directly when its loaded result is empty.

Remapping Gate Modal Keys

Override the shared plan/custom gate controls under ace.keymaps.gate:

ace:
  keymaps:
    gate:
      next_control: "down"
      previous_control: "up"
      toggle_option: "space"
      submit_primary: "enter"
      submit_branch: "ctrl+enter"
      open_inputs: "o"
      next_input: "ctrl+n"
      previous_input: "ctrl+p"
      decision_next: "l,right"
      decision_prev: "h,left"
      decision_reset: "r"
      decision_reset_all: "R"

These bindings dispatch only while a branch-driven gate modal is open, and its footer shows the effective keys. The example remaps navigation, submit_branch, and the input panel keys, and repeats the defaults for toggle_option and submit_primary. open_inputs (default i) opens the input panel for the focused option's note or declared fields, including an optional note that would otherwise skip the panel. next_input and previous_input (defaults tab and shift+tab) walk the panel's fields and buttons; they are inactive on the gate modal itself. Confirming the panel submits the branch; cancelling it returns to the gate without answering. The retired activate_control setting is accepted as a deprecated alias for submit_primary.

Remapping Memory Panel Keys

Override Memory panel bindings under ace.keymaps.memory. A value may list more than one key, separated by commas:

ace:
  keymaps:
    memory:
      follow_link: "enter,l"
      travel_back: "backspace,h"
      filter_notes: "slash"
      toggle_body_filter: "greater_than_sign"
      toggle_web: "space"
      next_strand: "s"
      prev_strand: "S"
      add_note: "a"
      edit_note: "e"
      delete_note: "d"
      publish: "I"

These bindings dispatch only while the panel is open. The full action list and defaults are in the ace.keymaps configuration reference. . (full_stop) is reserved for the fixed . then 1–9 numbered-chip prefix; a config that assigns it to a Memory action is warned and reverted to that action's default. > (greater_than_sign) remains a valid configurable key and is the default for toggle_body_filter.

The retired Glossary panel's bindings folded into these Memory panel actions: add_note/delete_note branch on the selected row (a web row adds a strand, a strand row deletes one), and next_link/prev_link/follow_link follow a glossary strand's mention-relation chips the same way they follow a note's PARENT/CHILDREN chips. ace.keymaps.glossary is still accepted in config for one release, but it is inert: no defaults ship for it and it builds no bindings. sase doctor warns when a loaded config layer sets it explicitly and points at ace.keymaps.memory instead.

Remapping Snippets Panel Keys

Override Snippets panel bindings under ace.keymaps.snippets. A value may list more than one key, separated by commas:

ace:
  keymaps:
    snippets:
      follow_relation: "enter,l"
      travel_back: "backspace,h"
      filter_snippets: "slash"
      toggle_body_filter: "full_stop"
      add_snippet: "a"
      edit_snippet: "e"
      delete_snippet: "d"

These bindings dispatch only while the panel is open. The full action list and defaults are in the ace.keymaps configuration reference.

Custom Modes

Define user-defined prefix-key modes under ace.keymaps.modes. Each custom mode has a prefix key and a keys dict where each sub-key specifies either a shell command or a built-in action:

ace:
  keymaps:
    modes:
      my_mode:
        prefix: "B"
        keys:
          run_tests:
            key: "t"
            shell: "just test"
          show_log:
            key: "l"
            shell: "git log --oneline -20"
          refresh:
            key: "r"
            action: "refresh"

Pressing B activates the mode, then pressing t runs just test, l shows the git log, etc.

Validation

The keymap loader validates all configuration:

  • Invalid keys are reverted to their defaults with a warning
  • Duplicate keys within one binding scope are detected and the conflicting override is reverted
  • Stale app.search_forward and leader_mode.keys.edit_query overrides are ignored with a warning; they are not translated into a second live shortcut
  • Prefix conflicts between custom mode prefixes and existing app bindings are warned
  • Key names follow Textual's spelling (slash, dollar_sign, backslash, vertical_line, …); the raw glyphs +, -, $, _, \, and | are also accepted, and key hints display the glyph

See docs/configuration.md for the full ace.keymaps configuration reference.

Prompt Input Widget

The prompt input is a multiline TextArea widget with vim-style INSERT, NORMAL, VISUAL, and V-LINE modes. The widget provides markdown syntax highlighting for prompt content (headings, bold, italic, code blocks, lists, etc.). The first dash of an unindented or space-indented - bullet, and the digits plus delimiter of an unindented or space-indented <N>. / <N>) ordered marker, are additionally bolded with the same theme-aware accent, including inside fenced code; this presentation does not change the prompt text. A tab-indented dash or ordered marker is not treated as a list marker.

Known macro syntax is layered over the Markdown colors using the active theme: #macro references are bold in the theme's success color, %directives are bold in its warning color, /skill references use a tint of the accent color, and --- separators are dimmed. Argument text is split into parts instead of one flat color — delimiters such as :, (, ,, and ) and the = sign are muted, keyword names use a lighter tint of the owning reference's color, and values are tinted by type (strings, numbers, and booleans each get their own hue). Directive arguments get the same treatment in the directive palette. An argument that names an unknown keyword, repeats a keyword, or has a value of the wrong type is additionally underlined.

When loaded prompt text contains literal top-level --- multi-agent separators, sase's TUI renders the text as a prompt stack: one pane per agent segment. YAML frontmatter at the start stays prompt-level metadata, and --- lines inside fenced code blocks are left alone. A #name macro swarm invocation stays a single pane and expands only when it is launched. During live editing, typed --- lines stay literal text; add prompt panes with g- in prompt NORMAL mode. The detailed multi-agent parsing rules live in the Macro reference.

Cursor Readout

Every mounted pane advertises its cursor position as Ln <line>, Col <column>, both 1-based and counted in document columns (the character index within the logical line, not the soft-wrapped screen column). The active pane's readout sits flush right on the bar's bottom border, next to the mode hints; each parked pane's readout sits on the right end of its own ─── ▍ agent N ─── separator rule. The digits are painted the color of that pane's own vim-mode cursor -- gold for NORMAL, cyan for INSERT, magenta for VISUAL / V-LINE -- so the readout and the cursor it describes always match. On a narrow terminal the active-pane readout always wins over the mode hints (which truncate first) and the prompt-search match pill; a parked pane's readout is dropped entirely rather than abbreviated if its separator cannot fit both the readout and the agent N label.

In prompt NORMAL mode, / and ? open an incremental search panel for forward and reverse searches. As you type, matches are highlighted in the active pane and the panel's right edge shows the selected stack-global count, such as 2/3. Enter accepts the current local match and keeps the highlights; Esc or Ctrl+C cancels the search, restores the origin cursor, and clears the highlights. If a query has matches elsewhere in the prompt stack but none in the active pane, the panel says no match in this pane · N in stack; accepting still does nothing until the active pane has a local match.

After a search is accepted, the count moves to a compact two-tone pill on the prompt bar's bottom border, just left of the Ln, Col readout. The query chip is graphite and uses the accent color for the search sigil; the count chip is solid warning gold, the same color as the current-match highlight, with the current ordinal in bold. The chip ink is chosen for maximum contrast from the theme's own foreground and background while avoiding colors that 256-color terminals with base16 palettes repaint. The sigil records how the search was made: / for forward, ? for reverse, * for whole-word forward, and # for whole-word reverse. Non-whole-word word searches (g*, g#, and VISUAL * / #) use / or ?.

n and N repeat the recorded search across every non-auxiliary prompt pane in stack order, with the existing wrap toasts when the traversal crosses the top or bottom. *, #, g*, and g# search from the word under the cursor; VISUAL * and # search the selected text. The pill's number is always stack-global and follows the highlighted match, even if you move the cursor away afterward. It disappears with the highlights: Esc, entering INSERT, edits, pane switches, and starting a new search all clear it. See Operator + Search for operating up to a match with d/, d?, dn, and dN.

On narrow terminals, the bottom border keeps the cursor readout first. The mode hints truncate or drop before the search pill; then the pill drops its query segment and keeps only the count; if even that cannot fit, only Ln, Col remains.

INSERT Mode (Default)

Key Action
Enter Open the submission panel for a non-empty agent prompt; pressing Enter again confirms the primary launch action. Does not accept a completion candidate
Ctrl+S Stash the active pane; from an empty prompt, open the Prompts overlay on Stash
Ctrl+C Cancel the prompt; in a prompt stack, cancel only the selected pane
Ctrl+J Insert a newline; continue a containing - bullet or <N>. item (renumbered), or leave the list from an empty marker
Ctrl+A Move to start of line (jumps to previous line start if already at col 0)
Ctrl+E Move to end of line (jumps to next line end if already at end)
Ctrl+F Accept a highlighted completion-menu candidate; with no menu, take the whole inline ghost when the rest of the line is blank, or move one character forward
Ctrl+G Start the prompt-local prefix (g or Ctrl+G again opens $EDITOR)
Ctrl+G Enter Submit only the selected pane
Ctrl+G j/k Focus the next / previous pane and leave the target pane in INSERT mode
Ctrl+G J/K Move the active pane down / up and leave it in INSERT mode
Ctrl+G - Add an empty bottom pane
Ctrl+G G Open the Memory panel; seeds from the glossary term under the cursor when there is one
Ctrl+G m Open the Memory panel; seeds from the #memory/<stem> reference under the cursor when there is one
Ctrl+G D Choose a local or eligible enrolled launch target and update the pane's %dispatch selector
Ctrl+G d Edit the macro definition under the cursor in the prompt bar
Ctrl+G f Reformat the active prompt pane's Markdown with Prettier
Ctrl+G w Write a bound macro definition; unbound drafts fall through to save-as
Ctrl+G = Show/focus the macro frontmatter panel; its rows-mode g= returns to the originating pane
Ctrl+G s Bundle every non-empty pane into one stash row
Ctrl+G S Overwrite a pinned stashed prompt with the current stack
Ctrl+G x / Ctrl+G Ctrl+X Open, retarget, or edit an existing mini-macro pane
Ctrl+G t / Ctrl+G Ctrl+T Open, retarget, or edit an existing snippet pane (see Authoring a snippet from the prompt bar)
Ctrl+G X Save as reusable macro/snippet; macro mode converts raw <tags>
Ctrl+G L Convert the active pane into a frontmatter-local macro; raw <tags> become inputs
Ctrl+G Ctrl+C Cancel every pane in the prompt stack at once
Ctrl+G p Open the Prompts overlay on Stash
Ctrl+G r Open the recent-files history menu (recently referenced files and @kind:payload references)
Ctrl+Y Open the workflow YAML editor
Ctrl+K Open the Prompts overlay on History from a single-line prompt, scoped to that prompt's project (see Prompts Overlay)
Ctrl+P Cycle toward older workspace MRU prefixes (no-prefix stop before wrapping); in a macro keyword slot, open the keyword menu at its last row
Ctrl+N Cycle toward newer workspace MRU prefixes (no-prefix stop before wrapping); in a macro keyword slot, open the keyword menu at its first row
Ctrl+T Completion (structured tokens, paths, prompt-local words, history words, or next-word ghosts and border peeks; at a whitespace boundary it requests next words instead — recent files moved to Ctrl+G r; a second press accepts the highlighted word-menu row or takes one ghost or peek word; see Completion)
Ctrl+R Recursive fuzzy file finder using the same prompt-aware path root as file completion
Tab Expand a snippet or advance its tabstop; otherwise indent a bullet or nest an ordered item under a preceding marker
Shift+Tab Retreat to the previous snippet tabstop; otherwise dedent a bullet or unnest an ordered item into its enclosing run
#@ Open Macro snippet picker (type # then @)
Escape / Ctrl+] Switch to vim NORMAL mode; Ctrl+] is the race-free alternative when typing following NORMAL commands quickly

In prompt INSERT mode, sase's TUI auto-pairs safe openers for (), [], {}, <>, single quotes, double quotes, and backticks. Typing the matching closer over an auto-inserted closer moves the cursor across it instead of duplicating it, and backspace or delete removes both sides of an empty pair. Pairing is conservative: it is suppressed before token characters, when text is selected (the typed character replaces the selection literally), for contractions or possessives, and for repeated quotes/backticks needed to type Markdown fences or code spans.

When ( is typed immediately after a macro or supported directive argument delimiter, the prompt input normalizes the shorthand in one keyboard edit. A single colon is removed (#review: -> #review()), while :: followed only by ASCII spaces is moved after a complete pair (#review:: body -> #review():: body) with the caret inside the parentheses. Typing ( immediately after a closed macro argument list continues the list by adding a comma and placing the caret before ), for example #review(path=a):: body becomes #review(path=a,|):: body; the remaining macro argument menu opens when auto_macro_menu is enabled. Empty lists and lists that already end in a comma only move the caret. The double-colon form preserves the exact spaces and suffix text; tabs, newlines, nonbreaking spaces, directive argument lists, selected text, and literal regions keep ordinary insertion behavior.

INSERT-mode Ctrl+J and prompt NORMAL-mode o / O continue a containing space-indented - bullet using that bullet's indentation. Prompt NORMAL-mode J is the inverse operation: when it folds the next line into a nonblank current line, it drops that line's supported - marker. Bullet continuation also works from physical continuation lines, including Prettier-wrapped nested bullets; non-bullet lines keep the ordinary bare newline or open-line behavior. In INSERT mode, when there is no selection, pressing Ctrl+J anywhere on a line containing only zero or more leading spaces followed by - replaces that marker with a bare newline and moves the cursor to column zero, ending the list -- but only when the line above that marker is itself part of a hyphen bullet. The common sequence is therefore Ctrl+J once to create the next sibling marker and Ctrl+J again to exit the list. A marker-only line whose preceding line is not part of a bullet -- a freshly typed - on the first line, or one following a blank line or plain prose -- grows a sibling marker on the next line instead, so the exit still happens on the following press. Those two edits are separate undo checkpoints. A selection uses the normal replacement path instead. Extra spaces after the marker, tab indentation, other Markdown markers, and markers containing text do not trigger either path.

Ordered items (<N>. or <N>), one to nine digits) mirror every one of those hyphen rules for Ctrl+J, o, O, and J, and add the one thing ordered lists need: after each structural edit, sase's TUI renumbers the surrounding run -- the maximal sequence of same-indent, same-delimiter siblings, joined across blank lines and each item's own owned continuation lines -- so the live numbers agree with what gf (Prettier formatting) would produce. When a run's second item repeats the first item's number, every item in the run keeps that number (the 1. / 1. / 1. convention Prettier preserves); otherwise later items are numbered sequentially from the first item's start. Ctrl+J, o, and O give a newly inserted item the number after its nearest preceding sibling, or the run's first number when there is none. J drops the pulled-up marker and renumbers the run it left behind. A renumber that changes a marker's width (9. -> 10.) shifts every line that item owns by the same amount so indentation stays correct, and leading zeros (007.) are recognized as a marker but always renumber to plain decimal.

In INSERT mode, Tab and Shift+Tab do snippet work before list shifting. Tab first expands a trigger word immediately before the cursor, then advances to the next live snippet tabstop. Shift+Tab first retreats to a previous live tabstop. When that snippet action reports no movement or expansion, the selection is collapsed, and the cursor is anywhere on a direct marker line beginning with zero or more spaces followed by -, sase's TUI indents or dedents that bullet. Each press shifts only that logical line by the same two-space unit as vim >> / <<; dedent removes up to one unit, and the cursor follows the shifted content. Physical continuation lines, tab indentation, and other Markdown marker styles are excluded.

The same fallback applies to ordered items from anywhere on the direct <N>. / <N>) marker line. Ordered Tab nests at the content column of the nearest preceding marker line (either session, same or lower indent) instead of a fixed two-space unit, because an ordered item can only interrupt its parent's paragraph when numbered 1: Tab with no preceding marker line to nest under is a no-op, Tab landing under an existing nested run continues that run at its next number, and Tab that starts a new nested list numbers the moved item 1. Shift+Tab moves the item back out to its parent's indent and gives it the next number in that outer run; at the outermost level it is a no-op. Both carry the item's owned block along and renumber the source and destination runs as one undo checkpoint.

Text automatically wraps at the terminal width, breaking at spaces (never mid-word). Line numbers appear in cyan when the text exceeds one line. The native cursor cell is color-coded by prompt Vim mode: INSERT uses cyan, NORMAL uses gold, and VISUAL or V-LINE uses magenta.

Uppercase TODO at identifier boundaries is a visual draft marker. sase's TUI gives TODO, TODO:, TODO(owner), and TODO(owner): headers the exact #FFD700 gold used by the Agents-tab RUNNING status with explicit deep navy #00005F text. The deep navy stays legible on gold without relying on the terminal's customizable ANSI black palette entry. Only a header ending in : activates the quiet, theme-aware warm italic annotation style for the rest of that line; punctuation and prose after bare TODO or TODO(owner) retain their ordinary prompt syntax. When the first content in a dash-list item is the exact TODO: header, the body style continues through lazy and indented continuation lines, nested list content, and later paragraphs that Markdown assigns to that item. It stops at the sibling-item or outside-content boundary, and structural list dashes keep their bullet color. Checklist prefixes, TODO(owner):, and a TODO: later in item prose retain the same-line behavior.

Inline backtick spans and closed or live unclosed backtick/tilde fenced code blocks are literal zones: TODO-shaped text inside them receives no marker or body treatment and is omitted from the count. Ordinary quotation marks are not code delimiters. Lowercase todo and identifiers such as TODOS, TODO2, and preTODO remain ordinary text. When markers exist, the prompt border shows a matching deep-navy-on-gold TODO N count pill for every non-literal match across the full prompt stack, including compact inactive panes and markers outside the active viewport. The pill disappears immediately when the last marker is edited away.

TODO treatment does not move the cursor during history or stash restoration, and sase's TUI stashes and opens the literal prompt text in $EDITOR unchanged. Submitting an agent prompt with one or more visible TODO markers opens a neutral y/n confirmation with Keep editing focused by default. Keeping the draft preserves the exact prompt or prompt stack without launching or writing history; approving launches the same literal prompt text unchanged. The warning uses the same detector as the gold marker and count pill, so TODO-shaped text in inline or fenced code, lowercase todo, and non-boundary identifiers such as TODOS, TODO2, and preTODO do not trigger it. Feedback and coder-prompt submission keep their existing unguarded behavior. Only the colon-terminated body-note color follows the active dark or light theme, while the shared deep-navy-on-gold header and count pill remain fixed; search matches, selections, yank feedback, and the cursor retain their higher-priority treatments.

Raw Placeholder Inputs

Raw <placeholder> tags in sase's TUI prompt bar act like ad hoc prompt inputs. When you submit a prompt containing one or more highlighted raw tags, sase's TUI opens the Prompt Inputs panel before launch. The panel lists each unique tag once, shows a one-line context snippet and an occurrence count, and collects values on the same page as any required frontmatter-declared input: arguments. After confirmation, sase's TUI substitutes the collected values into the prompt and records history for the resolved prompt that the agents actually received.

Inline backtick spans, fenced code blocks, and %macros_enabled:false regions are literal zones. Tags inside those zones are not highlighted as raw placeholders, recorded in the saved common-placeholder store, or collected on submit. Their text is still offered as a current-prompt completion candidate, ranked after live tags. Use backticks when a tag-like value is meant to survive literally, for example keep `<div>` unchanged.

Each raw placeholder row must be filled before launch unless it is marked literal. Press Ctrl+L in the Prompt Inputs panel to toggle keep literal for the focused placeholder row; when focus is outside the field list, Ctrl+L marks all still-empty placeholder rows literal. A literal row counts as filled and leaves its original <placeholder> text in the launched prompt.

Set ace.prompt_inputs.collect_raw_placeholders: false to stop collecting raw tags on submit; declared frontmatter inputs are still collected. Set ace.prompt_inputs.macro_placeholder_args: false to keep live raw tags literal when using gX, gL, or a fresh gx extraction and mint no placeholder-derived text inputs; Jinja-variable inference for gL still runs. See Raw Prompt Placeholders for the exact conversion and naming rules.

Launch Target Picker

From any ordinary prompt pane, press gD in NORMAL mode or Ctrl+G D in INSERT mode to open Launch Target. The first row is here, followed by enrolled aliases in alphabetical order. Local enrollment data labels every non-quarantined remote ok and enables it; quarantined rows remain visible with their diagnostic context but are disabled. This is a local eligibility catalog, not a live gateway health check; use the Admin Center Machines tab's s action when current reachability matters. Move with j / k and press Enter, or select one of the first ten rows directly with 1–9 / 0.

Choosing a remote inserts or replaces the pane's single %dispatch:<alias> selector. Choosing here removes it. The prompt context line appears while the pane has a %dispatch selector: a valid one shows the cached Target and Source, and for a remote also states that source proof is checked on submit; an invalid one shows Target error with the reason. It stays hidden for ordinary local launches, unless the pane carries an invalid %auto spelling: that shows an Auto error segment with the same message the launch path raises, and Enter does not submit until the spelling is fixed. On submission, the prompt bar closes immediately and source proof runs off the UI thread as the first pending launch stage. A failed proof restores the prompt with source blocked: <reason> in its context line (or saves it to the stash if another prompt or modal now owns the screen).

After source preflight passes, sase's TUI inserts a provisional QUEUED remote row before the background launch settles. A structured accepted owner response keeps it QUEUED; a settled response changes it to STARTING. If the launch finishes without a structured dispatch result—including the current rejection and failed-receipt paths—the row becomes outcome-unknown WAITING; use Agents: check dispatch launch outcome from the command palette. Once the owner's catalog exposes the matching logical or exact locator, sase's TUI removes the provisional row in favor of that authoritative row. The Remote Dispatch Runbook explains the portable source requirements and remote operation model.

Prompt Stacks

Prompt stacks are sase's TUI editing surface for literal --- multi-agent prompts. Loading multi-agent prompt text from history, a whole-bar editor session, or an editor buffer that returned with a @ review marker splits top-level --- segment separators into panes labeled agent 1, agent 2, and so on; the border title shows Prompt · N agents. Restoring stashed prompts and using marked-agent ,x can also open a stack, but those paths load one pane per selected draft or agent instead of re-parsing each pane's text. Panes are ordered top-to-bottom for whole-stack submission. The bottom pane is active by default so you can keep drafting the newest segment; it is not a priority marker, and pressing Enter immediately opens the submit chooser.

Inactive panes stay compact, and the active pane takes the available height; each parked pane's separator rule also carries a live cursor readout of that pane's own position. A --- line typed while INSERT mode is active stays literal prompt text; use Ctrl+G - while drafting, or g- from prompt NORMAL mode, to add a new bottom pane. Ctrl+G g and Ctrl+G Ctrl+G open the whole stack in $EDITOR when the bar already has multiple panes (a single-pane bar opens just the current prompt). Returning from a whole-bar editor session, or from a single-pane editor buffer with a @ review marker, reloads macro-style Markdown and parses --- separators into fresh panes. History loads parse only real multi-agent prompts; a single history item with leading YAML frontmatter stays one verbatim pane instead of auto-opening the Frontmatter Panel.

A single-pane editor session normally launches the moment you close $EDITOR. To review it in the prompt bar first, end any line of the buffer with the exact suffix @ (a space followed by @). On return, that marker is stripped from every matching line and the cleaned text reloads with editor-file semantics: leading macro frontmatter is lifted into the Frontmatter Panel and real --- separators split into one pane per agent, so a marked multi-agent buffer comes back as a reviewable stack instead of launching. The marker is editor-return-only — typing @ in the prompt bar and submitting carries no special meaning. (This replaces the removed %edit directive.)

In prompt INSERT mode, pressing Ctrl+G opens the same context-aware hint row as prompt NORMAL mode's g prefix, plus the editor continuation. Press Esc while the prefix is pending to cancel it and stay in INSERT mode.

In prompt NORMAL mode, pressing g opens a small hint row for the prompt-local g prefix actions currently available.

Key Action
Enter Open the submit chooser by default; Enter inside that panel confirms the primary action. With ace.prompt_submission.confirm_on_enter: false, submit the active pane immediately
Ctrl+S Stash the active pane; from an empty prompt, open the Prompts overlay on Stash
g<enter> Launch the selected pane and remove it from the stack
Ctrl+C Record the selected pane as cancelled history and remove it; the final remaining pane cancels normally
Escape Enter NORMAL mode for stack navigation
gj / gk Focus the next / previous pane in NORMAL mode; inside the panel, jump to the top / bottom prompt pane
gJ / gK Move the active pane down / up in NORMAL mode; reorder cycles at the stack edges
g- Add an empty bottom pane in NORMAL mode and switch it to INSERT mode
gG Open the Memory panel; seeds from the glossary term under the cursor when there is one
gm Open the Memory panel; seeds from the #memory/<stem> reference under the cursor when there is one
gD Choose a local or eligible enrolled launch target and update the pane's %dispatch selector
gT Open the Snippets panel; seeds from a bare trigger or #[trigger] under the cursor when one can be resolved without I/O
g= Show/focus the macro frontmatter panel; in panel rows mode, return to the originating prompt pane
gs Bundle every non-empty pane into one stash row and dismiss the prompt bar
gS Overwrite a pinned stashed prompt with the current stack, leaving the bar open
gw Write a bound macro definition; unbound drafts fall through to save-as
gd Edit the macro definition under the cursor in the prompt bar
gf Reformat the active prompt pane's Markdown with Prettier
gx Open, retarget, or edit an existing mini-macro pane
gt Open, retarget, or edit an existing snippet pane (see Authoring a snippet from the prompt bar)
gX Save as reusable macro/snippet; macro mode converts raw <tags> and leaves the bar open
gL Convert the active pane into a frontmatter-local macro; raw <tags> become inputs

Submitting one pane at a time re-attaches prompt-level frontmatter to the launched pane so local macros and metadata continue to resolve. Empty selected panes are dropped without launching. Whole-stack submission joins panes in top-to-bottom order and then uses the usual multi-agent launch path, including %wait, %id, %model, and other segment-local directives. A selected-pane TODO warning counts only that pane; a whole-stack warning counts visible markers across all non-empty submitted panes. Choosing Keep editing, n, Escape, or q leaves pane order, selection, frontmatter, and source binding intact. Segment order alone does not make later agents wait; add %wait to a later pane when it must start after the immediately preceding submitted pane succeeds. That bare wait binding also applies when the wait comes from a frontmatter-local or file-backed macro referenced by the pane. Explicit waits, %queue, and waits inside literal code/disabled regions keep their normal meanings.

The Enter submit chooser accepts Enter, a, or Ctrl+S for all panes, c for the current pane, and Esc/q to cancel without changing the stack. For a single ordinary prompt, the panel contains one Launch agent row and Enter/s launches it. Set ace.prompt_submission.confirm_on_enter: false to skip this panel; then plain Enter immediately submits an ordinary single pane, the active pane of a stack, or a targeted pane. Outside that chooser, Ctrl+S is always an active-pane stash shortcut.

Prompt stashes are a per-user draft pile stored outside prompt history. Ctrl+S captures the selected non-empty pane plus the shared prompt frontmatter; when other panes remain the bar stays open, and when the last pane is stashed the bar closes without also recording the draft as cancelled history. If the active pane is empty, Ctrl+S opens the Prompts overlay on Stash instead. gs captures all non-empty panes in their current order as one bundled stash row and dismisses the bar. gS opens an update flow for an existing pinned stash and overwrites the chosen row with the current non-empty panes. Manual and restart stashes remember the active pane and its cursor, and restore focuses that pane in INSERT mode at the same logical line and column. Legacy rows and failed-launch recovery rows have no saved position, so they still restore to the last pane at end of text. gX and Ctrl+G X open one save screen containing the name, storage location, resolved path, and a live preview when the name collides. Inside that screen, Ctrl+X switches between macro and snippet mode. Ctrl+T remains manual completion in the prompt input and does not toggle this save screen. A successful whole-stack save binds the prompt stack to that source. gw then performs atomic write-back, and if the source changed since load it offers overwrite, reload, or save-as instead of clobbering it. gd loads the simple macro under the cursor for the same bound editing loop. gx first shows the location picker and then opens or retargets one focused mini-macro pane. Press e there to fuzzy-find an existing definition and edit it in place, or override a read-only one into a writable destination. Saving that pane publishes the definition without binding the surrounding prompt stack. gL converts the active pane through a prefilled frontmatter ghost row and rewrites the pane to invoke the committed helper. Before gX opens the save preview, its macro version converts live <label> tags into required Jinja text inputs; switching that screen to snippet mode shows and saves the original active-pane body instead. A fresh gx extraction applies the same raw-placeholder conversion to the copied origin-pane body before the mini pane opens and seeds the mini definition's inferred inputs. Raw placeholders typed later in the mini pane are saved as edited; the mini save review does not run another conversion pass. gL also applies the conversion when it creates a frontmatter-local helper. Set ace.prompt_inputs.macro_placeholder_args: false to disable these conversions while preserving gL Jinja-variable inference. gw only writes the currently bound definition—it does not reinterpret newly typed raw placeholders. Tags in inline code, fenced code, and disabled macro regions stay literal throughout. See Raw Prompt Placeholders for the exact launch, conversion, and naming rules.

Ctrl+G p opens the Prompts overlay on the Stash tab from the prompt bar. From the main sase's TUI tabs, when you are not typing in a text field, @ acts on Stash. If the open bar is Plan Feedback or Coder Prompt, @ warns Restore is only available for agent prompts and stops. Otherwise a lone pinned draft is loaded into the prompt bar and stays in Stash. A lone unpinned draft is loaded and then removed from Stash; it is not moved to Trash. When Stash is empty or holds more than one draft, @ opens the overlay on Stash instead of loading one. ,@ or clicking the stash: chip always opens that overlay, including when exactly one draft is stashed and when Stash is empty. The empty placeholder says to stash the current prompt. When Trash has rows, it also names that count and says to press t to view. [ and ] toggle between the Stash and History tabs with wraparound, so ] from Stash opens History. Trash is a view of the Stash tab, not a third tab. Press t on Stash, or select the 🗑️ chip, to open it. From Trash, t or Esc returns to the Stash list. [ or ] from Trash opens History, and the bracket that returns to the Stash tab brings Trash back, because the tab remembers which view was open.

In the Stash tab, space toggles a row's persistent pin, Tab toggles a row's restore mark, d marks one row for discard, D marks every row for discard, a toggles restore marks on all rows, and Enter confirms the marked set; restores are pin-aware, so marked pinned rows stay stashed. With no explicit marks, Enter restores the highlighted row; pinned rows stay stashed when restored, while unpinned rows are removed from Stash and are not moved to Trash. Press y to copy the highlighted row's exact prompt body through the shared nonblocking clipboard path (OSC 52 plus a manual-copy fallback): the payload is the stored body verbatim — multiline and bundled --- content, whitespace, Unicode, and raw references included, without frontmatter or preview metadata — and the panel stays open with its highlight, scroll position, pins, and staged marks intact. A row with an empty body copies that empty string; with no highlighted row, y is a no-op.

Number keys 1-9 and 0 restore rows 1-10 directly with the same pin-aware behavior. @ on the Stash tab restores the newest draft, so @@ pops the latest stash when several are stashed. On the Stash list, Escape or q closes the overlay and discards unconfirmed marks. Confirming discards for only some rows keeps the overlay open and repaints it in place; discarding every row, or combining discards with restores, closes the overlay. A small stash: ≡ N pink-chip top-bar group shows how many restorable drafts are currently stashed; the Trash count appears as the 🗑️ chip on the Stash tab, expanding to Trash N/LIMIT in the Trash view.

Every Prompts overlay entry point — including the History ones (Ctrl+K, ,., ,>) — first reads the stash store. If that read fails (for example a stale core binding, or a parse or lock error), an error toast beginning Failed to read stashed prompts names the cause and no overlay opens.

Discarding a Stash row moves it to the Trash view immediately with no y/n — d/D then Enter sends the marked rows straight to Trash, including pinned rows and batches that overflow the Trash limit — newest-deleted-first with the deletion age visible, unless ace.prompt_stash.trash_limit is 0 (see below). In the Trash view, Enter (or a digit key, or marked Tab/a) restores rows back to Stash while the overlay stays open; it never loads the prompt bar. d/D stage rows for permanent deletion and Enter asks for explicit confirmation before purging; Ctrl+Y copies a row. Trash holds at most ace.prompt_stash.trash_limit entries (default 100; this is an entry-count limit, not a byte quota): when a discard batch overflows it, the success toast names the actual evictions and sase prompt stash-archive for recovery. Setting the limit to 0 disables Trash recovery: Stash d/D plus Enter then delete permanently (still archived; see below), and a partial discard is applied without a confirmation prompt. A lowered limit is applied the next time the overlay opens, before that window is shown. Over-limit rows are permanently deleted, oldest discarded first. The toast reads Trash limit lowered to N: permanently deleted K oldest draft (or drafts). The Trash view in the window that just opened still lists the rows and Trash N/LIMIT count from before that deletion, so the count can be higher than the new limit. Close the overlay and open it again to see the rows that remain. Enter on a row that was already deleted does not restore it and does not show a success toast. The Trash view then repaints from the store, so every already-deleted row disappears. If applying the limit fails, the stored Trash is left unchanged and the overlay still opens. The error toast is Failed to reconcile Trash limit: ..., or Prompt stash is busy — retry when the stash lock times out. Trash recovers only drafts deliberately discarded from Stash. A successful restore of an unpinned Stash row removes that row without putting it in Trash. This overlay has no action that deletes History rows.

A permanent removal, and a replacement of a row's text, frontmatter, or cursor, is archived first to the append-only prompt_stash_archive.jsonl sitting next to prompt_stash.jsonl. The copy keeps the full previous text. Reason names are popped, purged, evicted, and overwritten; see Recover a Stashed Draft. Moving a row into Trash, and bringing it back from Trash, do not write an archive copy. A confirmed purge stays recoverable from that file. List recent archive rows with sase prompt stash-archive (a bare invocation defaults to list), filter with -q/--query and -r/--reason, inspect one draft with sase prompt stash-archive show ID, and append drafts back to Stash with sase prompt stash-archive restore ID... (unique id prefixes work). The purge confirmation, purge and eviction toasts, and the in-place delete toast all name this recovery command.

Compact demo — discard fix flaky parser test, then recover it. Leave at least one other Stash row unmarked. Discarding every Stash row closes the overlay, so the later steps would start by opening Stash again:

  1. Stash tab: highlight the row, press d, then Enter. The toast reads Moved 1 draft to Trash and the Stash tab's 🗑️ chip shows 🗑️ 1.
  2. Press t to open the Trash view: the row sits newest-first with its deletion age under the 🗑️ Trash 1/100 pill.
  3. Press Enter: the toast reads Restored 1 draft to Stash, the overlay stays open, and t/Esc return to a Stash tab holding the recovered draft.

Trash requires the current stash core binding: restart old TUI processes before the new behavior takes effect, since mixed-version operation is unsupported. Before the first new-format write, sase verifies a recoverable pre-upgrade backup of prompt_stash.jsonl under the stash lock and fails closed when backup creation fails; recover pre-upgrade drafts from that backup.

Editing an Existing Macro from the TUI

Loading a definition into the prompt bar for editing puts the bar into a targeting state instead of a plain draft: the bar tracks the exact source file your edits will write to, and everything below applies whenever the bar shows a target. Every surface that loads an editable definition enters this state the same way:

  • The Config Macros child's Enter (see Editing Macros).
  • The Select Macro # picker's Ctrl+O ("edit here"), alongside its existing Ctrl+E (open in $EDITOR) and Ctrl+I (inline-expand) keys.
  • The jump panel (Ctrl+], gd) and gd under the cursor in the prompt bar.
  • Returning from a whole-bar $EDITOR round trip (Ctrl+G g) preserves whatever target the bar already had — your edits stay bound to the same file.

A read-only source (legacy, plugin, built-in) loads without a target: the bar shows a persistent read-only marker instead of binding, and gw falls through to the save-as flow rather than clobbering the original.

Visual state. A targeted bar's border switches from a solid to a double rule so the state reads at a glance in any theme: $secondary when clean, $warning when dirty or when the source changed on disk underneath you, and a dim $foreground when read-only. The border title shows a ✎ <reference> chip using the canonical reference you'd actually type (#foo, #memory/foo, /skill-name) — not the file stem — followed by a state marker: dim ✓ (clean), gold ● (dirty), 🔒 read-only, or ⚠ changed on disk (another process edited the file since you loaded it). The frontmatter panel's border tints to match and auto-shows on a targeted load, even before you add any frontmatter fields.

Saving. Enter opens the submit chooser by default for non-empty prompt drafts: ordinary single panes, targeted panes, and multi-pane stacks all use the same panel. The chooser's rows are target- and pane-count-aware: Enter/s launches a single pane, Enter/a launches all panes in a multi-pane stack, c launches only the current pane, w saves to the targeted reference (its subtitle names the destination when dirty, or says there's nothing to save when clean), and X forks the definition to a new location. Set ace.prompt_submission.confirm_on_enter: false to make plain Enter launch the active pane immediately; gw / Ctrl+G w performs the same save directly, and the bar's subtitle shows a [^G w] save <reference> hint whenever a target is active. If the source changed on disk since you loaded it, the save opens a conflict prompt offering overwrite, reload, or save-as instead of silently clobbering the external edit.

Chezmoi-managed homes. With use_chezmoi: true, editing a definition under $HOME whose chezmoi source already exists writes to that chezmoi source file, not the applied copy — the applied copy is regenerated by chezmoi apply and would otherwise silently discard your edit on the next apply. The border title and the follow-up actions modal both surface this redirect so it's never a surprise.

Follow-up actions. After a save (or an E / $EDITOR edit), a follow-up modal offers only the actions that apply to the file you just wrote, each toggleable and run through the tracked proc queue in order:

  • Commit & push — offered when the write path is inside a git repo with changes.
  • Apply chezmoi — offered when the write redirected to a chezmoi source; scoped to just the one home target, never a whole-home apply.
  • sase memory init — offered instead of the two above when you edited a SASE memory note, since it already commits and pushes for you while regenerating AGENTS.md and the provider instruction shims.
  • sase skill init — offered instead of the two above when you edited a canonical skill source, since it already commits, pushes, and deploys the generated skill files for you.

With a single offered action, Enter runs it and Esc skips, matching the previous plain commit/push confirmation. With more than one, each row's key toggles it and Enter runs everything still selected.

Completion

Press Ctrl+T to activate token completion. The completion kind is determined by the token under the cursor:

  • Macro completion: When the cursor is on a #-prefixed token (e.g., #my_pro), completion shows matching macro names from all discovery sources, including registered workspace workflow macros. Completion rows include the macro kind and visible typed inputs, with required arguments shown as name: type and optional arguments shown as name?: type plus a default when the default is a simple scalar. Standalone workflow references use the #!name insertion form; typing #! filters completion to entries whose canonical insertion starts with #!.
  • Project/Patch completion: When the cursor is on a +query token whose plus is at the start of the prompt or directly after whitespace (including a newline or tab), {, or |, completion opens a project/Patch picker. A plus glued to other text (a+b, c++) and #+query are not project triggers. The picker contains enabled launchable projects plus active PR-sized Patches in WIP, Draft, Ready, or Mailed status; system-managed home, disabled projects, internal sibling backing records, and non-launchable projects are excluded. Typing after the trigger filters by project name, project alias, or Patch name prefix. Project rows show +name in the project's accent color with a provider · #<workflow>:<name> detail (the current project's row adds a current badge), ordered current project first, then most recently launched, then by name; Patch rows keep their [PR] badge. Accepting a project row places its +<project> tag (or #<workflow>:<name> for names outside tag syntax) at the earliest existing workspace target in that --- segment, or at the segment's leading tag position when it has none — and a Patch row places #<workflow>:<patch> the same way. Either way, the other workspace targets in that segment are removed. See Project Tags.
  • VCS ref completion: When the cursor is inside the root segment of a registered VCS workflow ref, such as #gh:, #gh:sa, or #git(, completion lists that provider's projects and active PR-sized Patches. Providers can add namespace rows, such as GitHub organization rows, from local project/config data. Accepting a project or Patch completes only the current ref token, producing #gh:sase in colon form or #gh(sase) in parenthesized form. Accepting a namespace inserts a trailing slash such as #gh:sase-org/ and immediately hands off to repository completion.
  • VCS repository completion: When the cursor is inside a registered VCS workflow ref that already contains an owner or namespace plus /, completion lists repositories for that namespace through the owning workspace plugin. For example, #gh:bbugyi200/ opens GitHub repositories for bbugyi200, and #gh:bbugyi200/sa narrows locally or through the LSP client's filtering. Accepting a row replaces only the current ref value, producing #gh:bbugyi200/sase in colon form or #gh(bbugyi200/sase) in parenthesized form. Failed or empty lookups show a placeholder row in sase's TUI; stale cached results are reused when a refresh fails.
  • Slash-skill completion: When the cursor is on a slash-skill token such as / or /sase_, completion filters the same catalog to skill sources and inserts /<skill_name> — the provider name, not the # reference. The same source completes as #skill/sase_plan after a #, and both forms resolve one definition, so argument hints, previews, and jumps agree. Packaged built-in skills are included, so /sase_plan, /sase_questions, and other bundled SASE skills are available without a project-local skill file.
  • Macro argument completion: When the cursor is inside a known macro argument position, Ctrl+T completes the active argument instead of the macro name. For path inputs it delegates to file path completion, for enum and bool inputs it offers the shared Rust choice menu (canonical values with labels, descriptions, and a quiet default badge; true/false for bools). model inputs use the %model menu, including aliases after @, provider drill-down, and effort choices after the model's @ suffix. Typed model inputs open the existing model picker. Inside parenthesized syntax it completes missing name= arguments without repeating names already present in the argument list. Each keyword row shows the shared type label, its default when optional, and its description. #m: and #m(k= open the value menu immediately when the active input has choices or is typed model. Accepting a name= row chains straight into its value menu, even from a single-candidate name menu; auto-open shows the choice without accepting it, and empty-prefix menus keep declared order with the first row selected. Accepting a choice replaces the whole value element and inserts the canonical value (quoted when needed), preserving adjacent values and surrounding syntax. At a keyword slot (right after #review(, or after a comma and space in #review(a=1,), the keyword menu also opens automatically while typing (unless ace.prompt_completion.auto_macro_menu is off), and INSERT-mode Ctrl+N / Ctrl+P open it with the first / last keyword highlighted. Accepting a keyword immediately opens its value menu when the input has one (enum/bool values, agent targets, paths, or model values). Enter always submits the prompt as typed, even on an automatically opened first-row menu; Ctrl+F accepts the highlighted row without requiring Ctrl+N, Down, or another ownership signal. Agent inputs such as #fork offer agent, proc/monitor, session, clan, and @tribe targets with kind and member context. A proc or monitor row inserts its exact durable proc ID while displaying the friendly, reusable proc name. Session rows also show the associated plan or bead when SASE can resolve one: the row reads <kind> · <phases/waves> · <title> (for example Epic · 5 phases · 2 waves · Bead review hardening), and its plan title is searchable, so typing part of the title filters to that session. Selecting the row fills the panel subtitle with more of the same artifact — phase titles for an epic, the goal for a tale or plain plan, the parent title for a phase or task bead. When nothing resolves, the row falls back to a snippet of the session's launch prompt and the subtitle falls back to member names; completion is never blocked either way. Numeric inputs keep the type hint visible but do not invent values.
  • Frontmatter panel input editing: The frontmatter panel edits input: rows one cell at a time as name, type, choices, default, description. Inline enum inputs author their value set in the choices cell with a flow list such as [wip, draft, {value: ready, label: Ready}]; any other type must leave choices empty. Compact local-macro inputs (name:type[=default], ...) keep the existing rich declaration (choice labels and descriptions, input description, repeatability, and role) and only apply the compact default. Press R for raw-YAML editing, or declare a new inline enum as a top-level input, when the compact list cannot carry its choices.
  • Jinja completion: When the cursor is inside a Jinja {{ }} or {% %} tag, the Jinja menu owns completion ahead of every other surface: variables at expression positions, filters after |, tests after is, members after <namespace>., and statement keywords (closers for open blocks first) at the head of {% %}. Rows show the name, type or signature, source badge (input, local, sase, jinja, %repeat / %wait for conditional names, legacy, closes for), and description; the border title names the slot ({{ variables, | filters, is tests, {% statements, or <namespace> members) plus the scope label when the pane is macro-bound. The menu opens automatically while typing inside a tag — right after {{ / {% auto-pair, on | and ., and on identifier characters — unless ace.prompt_completion.auto_jinja_menu is off; manual Ctrl+T still works when off. With the menu off, an in-tag cursor still claims completion, so placeholder, directive, @, and # menus leave the tag alone. Next-word ghosts and peeks stay off for the whole tag, whether or not the Jinja menu is open. A | inside a tag always inserts literally as a filter pipe and never triggers %{...} alternation normalization. String literals and new-name positions ({% set x, {% for x) offer no menu, and the open menu suppresses the Jinja diagnostics panel so a momentarily empty {{ }} never flashes red.
  • Directive completion: When the cursor is on a %-prefixed directive token (e.g., %m), completion lists user-facing prompt directives and accepts aliases into their canonical forms. For example, %m completes to %model and %w completes to %wait. The same shared directive matrix used by the macro LSP is documented in Directive Completion Matrix: %model completes live model catalog rows, aliases, provider drill-down rows, and parenthesized alias keys; %effort, %auto, %repeat, and %macros_enabled complete their fixed values; %id, %clan, and %wait(...) complete their supported keyword names and keyword-value rows. %wait: never offers structured keywords, so time= and bead= appear only in parenthesized %wait(...). capacity=, priority=/p=, and weight=/w= complete on %queue/%q only. Keyword completion suppresses duplicates and mutually exclusive keywords, and %model(..., alias=...) suppresses the alias's own @alias value, while manually typed values still flow to launch-time validation. Dynamic agent and bead rows come from sase's TUI warmed snapshots rather than synchronous prompt-bar bead-store reads; if a dynamic refresh is unavailable, static directive names, aliases, keyword rows, and fixed values remain available.
  • Model shortcuts: When the cursor is on a =alias or ==model token at the start of a logical line or immediately after a literal ASCII space, completion opens a model shortcut menu. =alias lists alias rows only; for example, typing =la can select @large and rewrite the whole token to %m:@large. ==model lists concrete model rows only; for example, typing ==gpt can select gpt-6.1-sol and rewrite the token to %m:gpt-6.1-sol, while provider-qualified input such as ==codex/g narrows to that provider. The second equals sign switches an open alias shortcut panel into the explicit model panel. If the token already has a following ASCII space, sase's TUI reuses it and leaves the cursor after that space; before a tab it inserts no extra space; before a newline or prompt end it appends one ASCII space. Typing @ directly after a colon-form %m:<model> / %model:<model> value and its single trailing space (at end of line or before whitespace) replaces the space with @ and opens the effort-level menu (subject to auto_directive_menu); undo if you meant a literal @ reference after the space. Ctrl+F and Ctrl+L accept the highlighted row; Enter submits the prompt as typed and dismisses the menu instead of expanding the shortcut. No matches dismiss the panel and leave the literal query text intact, so unknown equals tokens submit as ordinary prose. These shortcuts do not fire inside inline code, fenced code, frontmatter, placeholder/directive contexts, escaped equals signs, Markdown-style =text= / ==text== marker pairs, or path-like tokens such as path/=. The old *alias and **model forms are ordinary prompt text. The macro LSP uses the same shared filter and edit plans; see Equals model shortcuts. ace.prompt_completion.auto_directive_menu only controls whether sase's TUI auto-opens these menus and does not govern an external editor's = trigger.
  • @ reference completion: A bare @ opens the artifact-kind menu before a : appears. Local file rows such as @src/ and @Justfile from the prompt-selected base directory stay hidden while the typed text prefix-matches an artifact kind; the panel advertises [^T] files, and the first Ctrl+T reveals those rows without accepting or extending the kind. A second Ctrl+T behaves as normal completion. The reveal stays active while that menu remains open and the query is narrowed. When no kind prefix-matches, file rows appear automatically, so path-shaped tokens such as @src/ naturally show only files. Matching is fuzzy, so a payload is reachable by any memorable fragment of its path or title — @research:site finds @research:202607/sase_sites_hub_and_pages/sase_sites_hub_and_pages.md — and @rsch finds the research kind. Rows are tiered so a fuzzy hit never outranks a literal one (prefix, then basename prefix, then contiguous substring, then ordered subsequence), then ranked by score, shorter text, and case-insensitive text; see Artifact references for the shared tier table. An empty query is not ranked at all, so opening a menu keeps each group's provider order. After any file reveal, Ctrl+T extends the token to the shared prefix only while every leading row is a literal prefix match; once the query is fuzzy-only there is no shared prefix to insert, so a single remaining row is accepted outright instead. Directory navigation stays exact — only the trailing path segment is fuzzy. Accepting an artifact kind inserts @kind: and immediately opens its payload rows; accepting a directory inserts the @-prefixed directory and drills down; accepting a file inserts the @-prefixed path. Dotfiles are hidden unless the typed path segment starts with .. Documents, explicit artifact files, chats, beads, and agents come from bounded project-scoped catalogs warmed off-thread. Bead rows are loaded from an mtime-cached bead-store snapshot, and agent rows come from a bounded scan of the project's agents sidecar. Agent rows display the readable local name when possible but insert the durable global @agent:<username>.<machine>.<name> spelling. Commit and bug rows appear only from snapshots the mounted Artifacts panes have already loaded, so typing never launches Git, contacts a tracker, or performs unbounded filesystem scans, with one explicit, user-initiated exception: typing a second : right after @<kind>: with an empty payload (the @<kind>:: gesture, gated by the ref_sync_gesture flag) consumes that keystroke and refreshes the kind's backing sources now — cloning a missing sidecar, force-pulling an existing one past the freshness TTL, or rescanning a local/session-only kind — then reopens the payload menu. A pinned, non-selectable status row shows a live spinner while the sync runs (cloning <repo> … on a first-ever clone, syncing <repo> … otherwise) and settles to <repo> synced · N new or <repo> sync failed · <detail>; rows that arrived from the sync are badged [✦] in place of their usual source badge until the panel closes. A successful sync dismisses its status row after 2.5 seconds; a failed one stays until the panel closes and falls back to a toast if the panel is already gone. The gesture never fires with a non-empty payload (so @file:default: still inserts a literal colon), and repeating it while a sync is already running for that kind is a no-op. Disable ref_sync_gesture to fall back to a literal second colon with no sync ever triggered. Payload acceptance replaces the complete @kind:payload context, including when the cursor is in the middle of it. On an un-narrowed bare-@ menu, Enter submits the unexpanded @ and dismisses the menu; Ctrl+F accepts the highlighted row even when it is the untouched first row, and Ctrl+L remains a manual-menu alias. Payload rows are rendered path-first — the source badge, then the reference path with dim directories and a bright basename, then a dim title · detail · age tail truncated to the remaining panel width — so what you see is what gets inserted. Matched characters are highlighted in gold wherever they landed, in the path, in the title, in a kind name, or in a local file row, so every row shows why it is there. The panel subtitle reports the same context: ~ fuzzy when any visible row matched below the literal tiers, N of M for matching rows out of that kind's known payloads, and a ⚠ K not scanned warning when a catalog cap truncated the candidate set, so a bounded search never reads as an exhaustive one.
  • Placeholder completion: When the cursor is inside an incomplete <foobar> tag, completion suggests matching placeholders from the current prompt first, then saved common placeholders learned from tags you have written before. Within the current-prompt group, live tags keep document order and literal-zone tags follow in document order. Current-prompt rows use the cyan <> badge; saved rows use the gold ◆ badge. sase's TUI retains up to ace.prompt_completion.common_placeholder_count saved placeholders. Automatic completion stays quiet for a bare < and adds saved placeholders only after you type at least one prefix character; manual Ctrl+T on a bare < shows the saved list explicitly. A lone match in the highest-priority group is inserted outright, so saved tags never suppress direct insertion of a lone current-prompt match. Set common_placeholder_count: 0 to disable saving and display of common placeholders. In the completion panel, Ctrl+D deletes the highlighted saved (◆) placeholder from the store; current-prompt (<>) rows are not deletable. By default, submitting from sase's TUI opens Fill in this prompt and asks once for each distinct live tag before launch; Ctrl+L can keep a tag literal. Saving a new macro converts the same live tags to typed inputs. Inline-code, fenced-code, and disabled-region tags stay literal in both paths; see Raw Prompt Placeholders.

By default (ace.prompt_completion.placeholder_ranking: smart) saved rows are ranked by the same weighted composite the history-word menu uses: how strongly a tag relates to the words and tags already in the prompt (weight 0.50), how recently it was used (0.30), and how often it has been used (0.20). Each saved row shows the same 5-cell stacked score meter and dominant-reason chip as history words — ⇄ <tag or word> for relation, ◷ <age> for recency, or ✦ <count>× for frequency — with the current-prompt and saved groups aligned on one shared label column. The panel's border subtitle adds a matching ⇄ related · ◷ recent · ✦ frequent legend alongside the <> prompt ◆ saved source legend when both are visible; a narrowing panel drops the source legend first (the badges are already visible in the rows), then the signal legend, leaving today's subtitle, and finally just the [^D] delete hint. The meter and chip degrade the same way on individual rows too narrow to fit them — the chip is dropped first, then the meter. Set placeholder_ranking: recent to restore the previous most-recent-use ordering with no signal column, or placeholder_ranking_signals: false to keep smart ranking but hide the meter, chip, and legend.

  • File path completion: When the cursor is on a path-like token (starting with /, ./, ../, ~/, or containing /), completion shows matching filesystem entries. Tokens starting with @ are also recognized — the @ prefix is preserved in the completed path (useful for file-reference arguments). Relative paths use the prompt-selected base directory: +<project> project tags, registered workspace-provider refs, and known-project refs such as #git:<project> or #gh:<owner>/<repo> can root completion in that project checkout. If no prompt workspace ref resolves, sase's TUI uses the TUI process directory.
  • File-history completion: Ctrl+G r opens a list of recently referenced files and well-formed @kind:payload artifact references drawn from prompt history, ranked by recency. Project-local .sase/ paths are filtered out so internal bead/plan artifacts don't pollute the suggestions. Artifact references retain their leading @. Press Ctrl+D in the completion panel to delete the highlighted entry from the on-disk history. With ace.prompt_completion.next_word: off, Ctrl+T at a whitespace boundary (or an empty prompt prefix) opens this menu instead, preserving the old slot when there is no next-word chain to request.
  • Prompt-local word completion: As the first fallback for a plain prose token, Ctrl+T filters words already in the active prompt by the word prefix immediately left of the cursor. Candidates are drawn only from complete words earlier in the prompt, before that prefix; words later in the prompt (including any suffix already sitting to the right of the cursor) are never candidates. Identifier-like candidates may include ASCII hyphens, so bob-mac-capture is matched and replaced as one word; Unicode dash punctuation still acts as a boundary. Matching is case-insensitive and case variants collapse into one row. Insertion honors the casing you started: a prefix with at least two cased characters that are all uppercase uppercases the rest (GITHU → GITHUB); otherwise intrinsic spellings such as GitHub, README, and iPhone stay intact, while a plain word keeps the typed prefix's case and appends its remembered remainder. Accepting a candidate replaces only the typed prefix, so completion works safely in the middle of a word: foo<cursor>baz completing to foobar becomes foobar<cursor> baz — a single space is inserted to separate the completed word from a preserved right-hand suffix, and the cursor lands immediately after the completed word, before that space. An exact-prefix spelling is only offered when accepting it would have this separating effect; at a plain word boundary with no suffix to separate, that exact match is suppressed as a no-op. While multiple candidates share a longer common prefix, Ctrl+T only narrows the typed prefix and keeps the menu open; it never inserts the separator until a candidate is actually committed. With the menu open, a second Ctrl+T accepts the highlighted row instead of re-dispatching. Candidates shorter than ace.prompt_completion.word_min_length are skipped before history fallback is considered; the default is 5, and the threshold applies to the complete candidate rather than the typed prefix. This provider scans only the current prompt pane and takes precedence over history words when it has an eligible match. Candidates are ordered nearest-first: the word you just wrote is the one you are most likely repeating. When the next-word model is warm, word_ranking is smart (the default), and next_word is not off, candidates it predicts for the words before the cursor sort first instead, each marked with a violet ⇢ <context> chip naming the evidence context; the border subtitle then carries the matching ⇢ context legend entry. A cold model, a session-disabled model, word_ranking: recent, or next_word: off leaves nearest-first order untouched.
  • History-word completion: When prompt-local words have no match, Ctrl+T filters recently used words derived from recorded prompt history using that same left-of-cursor prefix; any suffix under the cursor is never consulted to include or exclude candidates, only to decide whether accepting inserts the same separating space described above. Hyphenated identifier-like spellings are indexed, matched, length-filtered, and replaced as one word, with only the typed prefix replaced so a preserved suffix survives acceptance. Matching and insertion use the same case-variant collapse and typed-case policy as prompt-local completion. sase's TUI retains up to ace.prompt_completion.history_word_count unique words that meet the shared ace.prompt_completion.word_min_length (defaults: 10000 and 5); set history_word_count: 0 to disable only this final fallback. History is loaded off-thread, so a cold cache briefly shows loading history words… without blocking input. Pressing Ctrl+T while that placeholder row is highlighted re-dispatches (refreshing once the cache is warm) instead of accepting; with real candidates, a second Ctrl+T accepts the highlighted row. Ctrl+D deletes the highlighted word instantly, without rebuilding the index, and records it in ~/.sase/prompt_word_deletions.json, so future history derivations continue to filter it out; remove that file to reset all history-word deletions. The former history_word_min_length configuration key has been replaced by word_min_length.

By default (ace.prompt_completion.word_ranking: smart) rows are ranked by a weighted composite of three signals rather than plain recency: how strongly a word relates to the words already in the prompt (weight 0.50), how recently it was used (0.30), and how often it has been used (0.20). Each row shows a 5-cell stacked meter whose filled length is the composite score and whose cell colors show each signal's share, plus a dominant-reason chip — ⇄ <word> for the context word it relates to, ◷ <age> for recency, or ✦ <count>× for frequency — and the panel's border subtitle carries a matching ⇄ related · ◷ recent · ✦ frequent color legend. When the next-word model is warm and word_ranking is smart, rows it predicts for the words before the cursor sort first with a fourth, violet meter share and a ⇢ <context> chip naming the sequence evidence, and the legend gains a matching ⇢ context entry. word_ranking: recent and next_word: off leave that promotion off. The meter and chip are dropped (leaving the word alone) on panels too narrow to fit them, and the legend falls back to the plain [^T] accept [^D] delete hint under the same width pressure. Set word_ranking: recent to restore the previous most-recent-use ordering with no signal column, or word_ranking_signals: false to keep smart ranking but hide the meter, chip, and legend.

Key Action
Ctrl+T Start completion, insert a shared prefix, accept the highlighted word-menu row, reveal a waiting peek, or take one word of a visible ghost or revealed peek
Ctrl+N / Down Next candidate
Ctrl+P / Up Previous candidate
Ctrl+F Accept the highlighted completion-menu row; otherwise act like Right, including accepting an inline ghost when the rest of the line is blank
Ctrl+L Accept the highlighted completion-menu row, or take the whole visible ghost or revealed peek
Right Move right; with only whitespace or nothing after the cursor on this line, take the whole inline ghost
Alt+F Move one word right; with only whitespace or nothing after the cursor on this line, take one inline ghost word
Enter Submit the prompt as typed; a completion candidate stays unaccepted
Ctrl+D Delete a highlighted recent file, saved placeholder, history word, or predicted next word
Escape Cancel completion

Next-word prediction

Confident guesses from your own typed prompt history appear automatically as you type, with no Ctrl+T needed to see them. They show as dim inline ghost text, or as a border peek when the guess cannot sit in the line without shifting your prose (mid-sentence guesses always use the peek, because inline text there would slide the rest of the sentence right on every keystroke). The border hint is [^T] word [^L] all.

ace.prompt_completion.next_word selects how guesses trigger. auto (the default) predicts after each typed character: a typed word character completes the word being typed, and any other typed character asks for the next words. chain shows nothing while typing and arms only after a prompt-local or history-word commit, or on an explicit Ctrl+T at the end of a prose word or at a whitespace boundary. off disables predictions; with off, Ctrl+T at a whitespace boundary opens recent files directly.

With prediction enabled, accepting a prompt-local or history-word completion starts a next-word chain: each accepted suggestion requests another. An explicit Ctrl+T also starts a chain at a whitespace boundary with no token under the cursor. At the end of a prose word, Ctrl+T first tries ordinary current-word completion; it requests next words only when that completion has no candidate. Structured tokens, such as paths and Jinja tags, keep their own completion behavior. This also holds in auto: a guess requested while typing owns Ctrl+T only while its ghost or peek is showing or waiting to be revealed; otherwise Ctrl+T completes structured tokens, paths, and prompt-local and history words exactly as in chain mode.

Where a confident guess appears depends on the cursor:

  • If the rest of the logical line is blank and the cursor is on its last wrapped row, the guess appears inline. Here, “blank” includes trailing whitespace.
  • If only a short closing tail follows, the guess appears inline before that tail, again only on the last wrapped row. A tail has at most eight non-space characters, all from )]}"'”’.,;:!?…*_; spaces between them are allowed, and backticks are excluded.
  • If other text follows, the cursor is on an earlier wrapped row, or even the first suggested word plus the closing tail will not fit, the guess appears as a border peek. The peek shows a violet ⇢, its first word in bold, and later words dimmed.
  • If a letter, digit, _, ', ’, or - immediately follows the cursor, no ghost or peek appears. This keeps suggestions quiet when editing inside an existing word.

ace.prompt_completion.next_word_max_words caps suggestions (default 4, clamped to 1–8). An inline ghost drops trailing words until it fits beside any closing tail. A narrow border drops trailing preview words, then [^L] all, then [^T] word, without cutting a word. If even ⇢ and the first word do not fit, the peek stays hidden. Border shortening affects only the display: Ctrl+L can insert later words omitted from a narrow peek. Use Ctrl+T to accept one word at a time.

A leading space is omitted at the start of the text, after whitespace, and after an opening bracket or quote. A peek is cut before the first suggested word that matches, ignoring case, the first word after the cursor; if its first word already follows the cursor, no peek is shown. A guess based only on a word's overall frequency, without contextual evidence, never reaches the ghost, peek, or next-word menu.

The first explicit request shows a confident guess as a ghost or peek. A weaker guess, or a confident guess that fits neither surface, can open a next word ⇢ "…" menu naming the evidence context. Each row shows a word, a violet confidence meter, and a dim ⇢ continuation preview. Accepting a row inserts its word with a separator and continues the chain. With a ghost or a revealed peek already visible, the next Ctrl+T accepts one word. Ctrl+L accepts the inline ghost or all words stored in the revealed peek. Each accept requests another prediction immediately.

Right and Ctrl+F accept the whole inline ghost only when the rest of the line is blank; Alt+F accepts one ghost word in that same position. Before a closing tail or with a border peek, these keys move the cursor. An open completion menu takes priority: Ctrl+F and Ctrl+L accept its highlighted row.

An automatic guess shows inline ghost text immediately. Its border hint waits until text and cursor have stayed unchanged for 350 ms; an automatic peek stays hidden for that same interval. An explicit Ctrl+T reveals immediately. While a peek is still waiting, the first Ctrl+T reveals it and inserts nothing; the next accepts one word.

Typing characters that continue the ghost consumes them. A different character, Backspace, a cursor move, undo, or redo clears the suggestion. The chain remains usable only while its text and cursor match the position where it was started. An armed chain with no candidates shows no next-word guess, or warming next words… while the model loads. Structural syntax does not open a next-word menu. At a whitespace boundary, a miss adds [^G r] recent files, pointing to the recent-files menu on Ctrl+G r. With next_word: off, that whitespace-boundary Ctrl+T opens recent files directly. Ctrl+D on a highlighted next-word row forgets it through the history-word deletions store.

In auto, a typed non-word character that follows a word token asks for the next words — clause punctuation between the word and the trigger is skipped, so a ghost may appear after , — and, while you type a word with only a boundary after the cursor, the request completes that word. After a sentence-final ., ?, !, or …, the model starts a fresh empty context and stays silent. If the keystroke was consumed by a visible ghost, the ghost is kept and no new request is made.

In auto, current-word completion inserts the untyped suffix plus any continuation. For example, after typing imple, the ghost might be ment it now. If the word is already complete, the ghost starts with a space (it now); an exact word with no continuation stays silent. A current-word peek displays the completed word first. Ctrl+T inserts its missing suffix, or the next continuation word if no suffix is missing. Ctrl+L inserts the suffix and continuation. A current-word request that found a guess shows it as a ghost or peek, and Ctrl+T takes it. When that request shows nothing (including when the core supplies no current-word completion), Ctrl+T runs ordinary completion instead of repeating the request. The completed word counts toward next_word_max_words, leaving at most next_word_max_words - 1 continuation words.

When the text before the cursor exceeds 4000 characters, a typing-triggered current-word request runs off the keystroke path after ace.prompt_completion.debounce_ms (default 90 ms). This cutoff does not apply to an explicit Ctrl+T request or measure text after the cursor.

Ghosts and peeks stay off in NORMAL mode, during file completion, during a snippet session, while text is selected, and for the whole of a Jinja {{ }} or {% %} tag. The gate and plan feedback note editor hosts the same autosuggest — ghost, peek, hint, Ctrl+T, and Ctrl+L — with no completion menu there, so Ctrl+T there still re-requests a typing-armed guess and shows the hint.

Principles: Ctrl+T never inserts an unseen guess, always moves forward, and stays silent when unsure.

Press Ctrl+R to open the recursive fuzzy file finder. With a token such as src/alp, src/ becomes the search root and alp pre-seeds the fuzzy query; with no token, the finder starts at the prompt-selected base directory described above. If a Ctrl+T file, recent-file, or path-argument candidate is highlighted, that highlighted path seeds the recursive root instead. The finder uses git ls-files --cached --others --exclude-standard from the search root when possible, falls back to a bounded filesystem walk, and inserts the selected path into the prompt position captured when the finder opened. Inside the finder, type to filter, use Ctrl+N / Ctrl+P or arrows to move, Ctrl+U to clear the query, Enter to insert, and Esc to cancel.

In prompt NORMAL mode, K previews the macro, slash skill, or file under the cursor. Image files preview inline in the reader, while Ctrl+] still opens images directly in the artifact viewer. Inside #name: / #name:: argument text, K and Ctrl+] prefer a nested reference, file path, glossary term, or plain word under the cursor, and fall back to the macro that owns the argument text only when nothing else matches. On ordinary prompt text, sase's TUI checks the warm project glossary before falling back to plain word lookup or spelling fixes. Ctrl+] jumps to a macro, skill, file, or glossary definition, or opens an action picker when several jump targets are available. Glossary terms come from the project selected by a leading VCS workflow reference, or from the active workspace project when the prompt does not select one.

Glossary terms

Project glossary entries are authored as strand files under sase/memory/glossary/, one term per strand file, described by the sase/memory/glossary.md web descriptor; see Memory Webs. sase's TUI highlights matched glossary phrases in the prompt after the catalog is warm, rendering them bold, underlined, and in a muted blue so they read apart from the lavender repo-name highlight — the same "you can preview this with K or jump to it with Ctrl+]" affordance, a different hue. Matching skips inline code and fenced code and uses the shared longest-match rules from the macro LSP. Loading, validation, and matcher compilation run off the render path and are cached per project/source signature. Strand edits, project changes, and watched memory changes invalidate the cache.

K on a glossary phrase opens a compact definition card. The title shows the canonical term and discloses the matched phrase only when you opened an alias. The body renders the definition as prose, followed by display-alias chips, numbered SEE ALSO chips for glossary terms mentioned by the definition, and a property grid for project, source, and match count. Press 1-9 to follow a SEE ALSO term in place and Backspace to walk back through the card history. y copies the definition, while Y, o, and Z copy the source path, open the owning strand file's definition line in $EDITOR, or hand the file to the artifact viewer when a source path is available. Ctrl+] opens the project-local strand file's definition range through the normal editor/tmux jump flow. If the catalog is still loading, sase's TUI schedules a warm and asks you to retry rather than falling through to word lookup or an unrelated jump target.

The card's SEE ALSO chips are the depth-1 case of the same closure resolver behind sase memory show/read glossary:<keyword> (see Memory Webs): both walk outgoing reference spans from the shared sase.memory.web.resolution module, so the preview card and the CLI can never disagree about which terms a definition references.

Memory panel

The Memory panel is the browse-and-edit surface for SASE memory notes, webs, and strands -- the Markdown files under a content root's sase/memory/ (see Memory). From a prompt pane, press gm in NORMAL mode or Ctrl+G m in INSERT or NORMAL. K previews one highlighted glossary phrase in place (see Glossary terms); gG in NORMAL mode or Ctrl+G G in INSERT or NORMAL opens this same panel seeded on the glossary strand under the cursor, or on the glossary web when the cursor is not on a highlighted term. The which-key hint row lists memory… on both prefixes. If the cursor sits on a #memory/<stem> macro reference, that note is selected; otherwise the panel opens on the first note. Closing with Esc or q restores the prompt pane and the vim mode you left.

The header reads MEMORY · <scope> · N notes · scope i/N, always using the configured PROJECT_NAME: for a project scope, never a ProjectSpec key, plus a right-aligned ⚠ UNPUBLISHED badge once this scope has panel writes that have not been published (see below).

The note rail is a tree, not a flat list, so the parent edge is visible before you follow any link: core (type: core) notes sort first by priority then path, then memory webs (such as glossary) sort by priority then slug, then reference (type: reference) root notes sort alphabetically, each immediately followed by its children indented one level under a └ mark. toggle_web (space) expands or collapses a web row in place, nesting its strands one level deeper the same way note children nest. Each row shows ● for a core note, ○ for a reference note, or ◆ for a memory web, then ⚙ for a generated note and ⚠ for a note with an invalid type or parent, then the stem and a dim description snippet.

Two navigation axes stay synchronized:

  • Tree. j/k move the note rail cursor; the note card follows. g/G jump to the first and last note. next_strand/prev_strand (s/S) jump the cursor directly between an expanded web's strands without walking every intervening row. / filters notes by stem and description; > extends the match into note bodies. Esc closes the filter and keeps the selection when it is still visible. An empty result reads no notes matched: <pattern>.
  • Relational. An ordinary note's card carries a numbered PARENT chip (omitted when the parent is AGENTS.md rather than another memory note) and numbered CHILDREN chips. A strand's card carries the same chip row labeled SEE ALSO (outbound) and REFERENCED BY (inbound) instead, when its web sets link_reference: implicit — the same mention graph sase memory read's depth-limited resolution walks, plus the same-web [[target]] links the strand bodies author (see Memory Webs and Memory Links). Numbering is continuous across both rows so .1–.9 is never ambiguous. Tab / Shift+Tab move a chip cursor, and l follows the focused chip -- or chip ① when none is focused. . then 1–9 jumps straight to a numbered chip. Following pushes the previous note onto a trail bounded at 32 entries and clears an active filter when the target is hidden. h or Backspace walks back. A non-empty trail renders as TRAIL a › b › c above the footer. On the embedded Admin Center Config sub-tab, bare 1–9 remain top-level Admin Center tab selectors.

follow_link ships as enter,l, but only l currently follows a chip: the note rail holds focus and consumes Enter for its own selection action. Use l or .1–.9.

The note card also shows a badge row (CORE · always loaded / REFERENCE · read on demand, plus GENERATED, SHADOWS HOME, ORPHANED, and INVALID when they apply), the description, the rendered Markdown body, and a property grid: type, parent, child count, size (lines and approximate tokens), last modified, last audited read, and the on-disk source path. A web row instead shows WEB plus EXPANDED/COLLAPSED, and its property grid names strand count and scope -- a web descriptor declares no type:, so there is no rendering-type row.

The card head is pinned: the title row plus the path line with the pager's state pill (NOW, uncommitted, UNTRACKED, NO VCS), then a reserved two-row time strip (one row on short cards). At clean now the second row names what changed last; honest states use the pager's exact words. ( and ) step the card through committed versions ({ jumps to the first, } returns to now); a violet frame marks the past and Esc returns to now before closing. = toggles a sticky word-diff view: the shown version against its parent, the latest change at clean now, or pending edits at dirty now. H opens the selection in the pager at the card's exact version and view, and C opens the Changes lens (below). A strand row shows STRAND, plus AUDITED, AUDITING, or AUDIT FAILED once the panel has recorded (or tried to record) an audited read for it: selecting a strand's card records an audited read the same way sase memory read <web>:<keyword> does, so previewing a strand in the panel is itself an attributable access, not a silent peek.

@ turns the rail into the selected subject's timeline: one row per version in the pager picker's exact cells, with ▸ on the cursor, ● on the version the card showed when the lens opened, and ◇ on the compare base. The highlight moves at once while the card previews through the usual 150 ms debounce (a loading vN… strip covers the fetch); rail motion pushes no trail entries and never changes the Notes filter. b marks the cursor row as the compare base (again clears it); the footer reads Compare vA → vB · b clear, oldest first, or Same version. . reveals hidden versions (chip shortcuts are inert in the lens) and / filters the version rows. ⏎, l, and H open the pager at the cursor's pin, view, and base -- a now target can compare against a committed base. Leaving (Esc, @, or h) restores the Notes rail exactly (cursor, filter, expansion, trail) while the card stays on the cursor's version, so a second Esc returns it to now. C is inert inside the lens, and r or a scope switch leaves the lens first. Clicking the time strip opens the lens.

C turns the rail into a day-grouped Changes review of the current scope (or All scopes via p/P, which gains that lens-only entry): one row per changeset with its clock time, first authored subject plus +N more, word delta, and ⌂ for home, under Today/Yesterday/weekday headers that j/k skip. Regen-only changesets fold into one count row per day, and the trailing ··· N older row extends the 100-row window by 100. The card shows the commit subject, ◈ bead / ⬡ agent / ◉ commit provenance chips, subject totals, and one titled diff section per authored subject (at most six inline, then +N more), filling progressively with stale loads dropped. ⏎, l, and H open the first authored subject in the pager diff view at that changeset's version (.1–.9 open subject N); / filters every fetched changeset by commit subject, subject names, bead, and agent; r refetches. Leaving (Esc, C, or h) restores the Notes rail exactly, and @ is inert inside the lens. A scope that fails (or has no VCS) shows as a header chip instead of being silently dropped.

Every Notes rail row ends with a recency glance: the newest change's class glyph and compact age (⇧ 8d, ◆ 3h, ⟳ 1h), right-aligned and built off-thread from one subjects() plus one feed() per scope load, so rows paint first and gain the column when the map lands (a failed load omits the column and keeps the rail). Promotions ⇧/⇩ keep their highlight; narrow rails shed the age first, then the glyph, and the stem never wraps. D toggles a trailing DELETED group of subjects whose latest entry is a deletion (newest deletion first; a deleted-then-recreated subject is not listed): rows read ✖ name deleted 3w, the header gains · N deleted, and each card is the tombstone -- the ✖ DELETED pill, a deleted-style frame, the tombstone strip row, and the last content. Tombstones are read-only ((/{ step older, @ and H work).

A collapsed ▸ INSTRUCTIONS · N group closes the Notes rail, listing every AGENTS.md instruction file (plus provider shims) with ≡ N shims, ⚠ diverged, and TEMPLATE chips where they apply. space toggles it; the group header itself selects nothing. Instruction cards are read-only -- managed files refuse edits (rendered from memory · edit its source notes), hand-written ones point at o -- and carry the pager's cause row instead of a meaning row. Stepping ((/)/{/}), diff (=), the Timeline lens (@), and the pager hand-off (H) all work on instruction rows exactly as on notes.

The Changes header leads with a review chip from the per-scope watermark: ● N new, not reviewed yet · m to mark, or ✓ nothing new (omitted when the review state is unavailable). Rows for changesets newer than the watermark carry a ● dot. m marks the shown scope reviewed -- optimistic, persisted off-thread -- and toasts marked N changesets reviewed. The Config hub MEMORY sub-tab badges the same count (●N), quiet until there is something new.

Memory history reaches the Agents tab too: the SASE CONTEXT / MEMORY lane appends a version chip to each audited read with a captured blob -- dim ≡ now, vK ⟲ N newer, amber ◌ uncommitted at read, or one ⟲ N of M changed aggregate for a batch read -- and leads with an AGENTS.md as launched row resolved from launch evidence (◌ as launched · not in git or snapshot unavailable when it cannot match a commit). Chips load in a later off-thread pass and append at the row end, so they never delay or reflow the lane. A v hint on a chipped row opens the pager pinned to the version read; see Memory History.

p and P cycle the scope ring -- every enabled project with a memory root, the project you opened from even when it has none (so a can bootstrap it), and one Home scope resolved through the active use_chezmoi mode, always ordered by display name with Home last. Precedence for the starting scope is the prompt's launch-workspace project, then the current project when it appears in the ring, then the first ring entry. Switching scopes clears the trail and the filter and restores that scope's last-selected note for the life of the panel. Ctrl+P opens a filterable scope picker showing each scope's display name and note count, for reaching a scope with many registered projects without cycling through all of them.

a and d branch on the selected row. On an ordinary note row, a opens an add form (stem, type, parent, and description) with live validation; the parent list offers AGENTS.md plus every reference note in the scope, illegal stems, types, parents, and parent cycles are rejected before any write, and a successful create offers to open the new note's body in $EDITOR. e opens the same form pre-filled to retype, reparent, or redescribe the selected note; the note body itself is not edited in the panel -- press o to open it in $EDITOR instead. d confirms and deletes the selected note; deleting a note that still has children is refused with an explanation of which children must be reparented first, and deleting a short note warns that always-loaded agent context is being removed. On a web row, a opens an add-strand form (keyword, optional comma-separated aliases, optional summary, body) with the same live-validation shape, against the same mutation engine described in Memory Webs. On a strand row, d confirms and deletes that strand, showing its relative path, keyword, aliases, the first line of its body, and any inbound Referenced by strands before anything is written; there is no strand edit form -- press o to open the strand file in $EDITOR instead. Every delete leaves a timestamped backup and the success toast names it. A generated note (sase/memory/sase.md and the project-only task_types.md / sase_artifacts.md / sase_beads.md / sase_sizes.md) renders its GENERATED badge and refuses edit or delete with an explanation; sase/memory/glossary.md and every other web descriptor are ordinary user-owned notes, not generated ones. A concurrent external edit is caught as a conflict: the write is refused, the panel toasts, and the scope reloads instead of silently overwriting the change.

Every successful write marks its scope UNPUBLISHED, because the write is not visible to agents until sase memory init regenerates AGENTS.md, the provider shims, and the memory README. I -- also offered automatically right after a write -- opens a publish confirmation with a prefilled, editable commit subject and two explicit choices: Publish & commit runs sase memory init --message "<subject>", and Publish only runs sase memory init --no-commit. Both run as a captured, non-interactive command in the scope's content root (Home for the Home scope); a failure surfaces the captured error and leaves the badge set, while success clears it.

A scope with a memory root but no notes shows a centered invitation naming the scope and pointing at a. A scope with no memory root yet says sase/memory/ will be created on the first add. A scope whose read root collided or failed to load shows its diagnostics and the offending paths instead of a note card.

Passive keys: o opens the note body in $EDITOR and re-checks it for a conflicting change on return; Z hands the source file to the artifact viewer; y copies the note body and Y the source path; r re-reads the current scope; ? opens a panel-scoped help overlay.

The panel footer lists only conditional keys: scope keys when the ring has more than one scope, link keys when chips exist, back when a trail exists, edit/delete when a writable note is selected, and publish when the scope is unpublished. Always-available keys live in ? and in this guide.

Most keys named above are remappable under ace.keymaps.memory; see Remapping Memory Panel Keys. Three sets are fixed and are not part of that scope: Esc and q (close), the . then 1–9 link-chip shortcuts, and the ↑/↓/Home/End/PageUp/PageDown cursor keys the underlying list widget supplies alongside the configurable j/k/g/G.

Snippets panel

The Snippets panel is the browse-and-edit surface for one project's composed snippet catalog — the same catalog sase snippet, prompt expansion, and the editor helper use (see Snippets). From a prompt pane, press gT in NORMAL mode or Ctrl+G T in INSERT or NORMAL. Lowercase gt / Ctrl+G t / Ctrl+G Ctrl+T still opens the snippet target pane. The which-key hint row lists snippets… on both prefixes. If the cursor sits on a #[trigger] call or a bare trigger that is already in the in-memory catalog, that entry is selected; otherwise the panel opens on the first visible entry. Closing with Esc or q restores the prompt pane, vim mode, selection, and cursor.

The header reads SNIPPETS · <project> · N snippets · project i/N and always uses the configured PROJECT_NAME:, never a ProjectSpec key. Generated initial-capital aliases are metadata on their source entry, not extra rail rows. Macro-derived entries are viewable and linkable but source-edited: e opens the real macro definition instead of converting a generated template back into Jinja.

Two navigation axes stay synchronized:

  • Alphabetical. j/k move the trigger-list cursor; the card follows. g/G jump to the first and last trigger. / filters triggers and source labels; . extends the match into raw and composed bodies. An empty result reads no snippets matched: <pattern>.
  • Relational. The card keeps the authored template and composed expansion distinct, highlights $0/$N tabstops and #[...] call sites, and carries numbered CALLS chips (outbound) and CALLED BY chips (inbound). Alias calls land on the canonical explicit trigger. Missing targets and cycles stay visible as non-followable diagnostics. Tab / Shift+Tab move a chip cursor, l follows, and 1–9 jump straight to a numbered chip. Following a hidden match clears the filter with a toast. h or Backspace walks a trail bounded at 32 entries.

p and P cycle the enabled-project ring. Order is by display name. Switching projects clears the trail and the filter and restores that project's last-selected trigger for the life of the panel.

a opens a trigger/template form with live trigger and link diagnostics, destination cycling (Ctrl+N / Ctrl+P), a composed preview, and explicit collision wording (replace vs shadow). e preloads an authored config template and its source fingerprint; on a macro entry it opens the real source. d confirms a delete naming backlinks, the exact file being changed, and any lower-priority definition that will become effective. Writes use the same engine as sase snippet add / delete, run as tracked procs with one exclusive scope per project and destination, refresh the panel, publish the change to every mounted prompt immediately (including removing a pending session overlay on delete), and offer the usual commit/push and scoped chezmoi-apply follow-ups. A stale-write conflict keeps the draft and offers reload. A failed write leaves the panel open and unchanged.

A project with no snippets shows a centered empty state. A project whose catalog failed to load shows the diagnostics. ? opens a panel-scoped help overlay. y copies the raw template, Y copies the source path, o opens the source in $EDITOR, Z hands the file to the artifact viewer, and r re-reads the current project.

The panel footer lists only conditional keys: e/d when the selected definition is writable config, relation keys when chips exist, p/P when the ring has more than one project, and back when a trail exists. Always-available keys live in ? and in this guide.

Most keys named above are remappable under ace.keymaps.snippets; see Remapping Snippets Panel Keys. Three sets are fixed and are not part of that scope: Esc and q (close), the 1–9 relation-chip shortcuts, and the ↑/↓/Home/End/PageUp/PageDown cursor keys the underlying list widget supplies alongside the configurable j/k/g/G.

Repo names

sase's TUI highlights the unambiguous identifier of every non-primary repo in the active project — linked names (sase-core), sidecar slugs (sase--beads), and external names (gh:owner/repo) — after the catalog is warm. Matches are bold, underlined, and lavender. The project's own primary name, sidecar role words (beads, plans), and any name the project glossary already claims are left as ordinary text so the two overlays never fight over the same characters.

Matching skips inline and fenced code, ignores path-adjacent hits (../sase-core, sase-core/crates), and drops the matcher's derived plurals so the highlighted characters always equal a real identifier. Loading and compilation run off the render path and are cached per project. Config edits, project changes, and watched sase.yml changes invalidate the cache. A repo opened with sase repo open during a live sase's TUI session does not appear until the next config-driven invalidation.

K on a repo mention opens a compact repo card: kind, description, checkout path, clone coverage, remote URL, and where the repo is declared. The title shows the repo identifier and discloses the matched text only when it differs in case from the identifier — the exact-identifier filter above rules out any other difference. Chips mark the kind, plus AUTO-CLONE and/or AUTO-SYNC when set, and ENV <name> when the record has one. Checkout prefers the clone registered for the active workspace, else the record's own path; when that path does not exist locally it is suffixed (not cloned) and the card prints the exact sase repo open <name> command as a hint — sase's TUI never runs that command itself. Clones shows <existing> of <registered> workspaces when the repo has clone records, and rows whose value is unknown (no remote, no declaration site) are omitted rather than shown empty. y copies the description, p copies the checkout path, and Y, o, and Z copy the declaration path, open the owning sase.yml line in $EDITOR, or hand the file to the artifact viewer — all three warn cleanly for an external repo, which has no declaration site. If the catalog is still loading, sase's TUI schedules a warm and asks you to retry rather than falling through to word lookup or an unrelated jump target.

Ctrl+] on a repo mention opens the resolved checkout — the clone registered for the active workspace, else the record's own path — in $EDITOR or a new tmux pane through the normal jump action chooser, which gains a c — Open declaration choice whenever the repo has one (external repos do not). When the checkout is not cloned in the active workspace, Ctrl+] notifies that the repo is not cloned and prints the exact sase repo open <name> command to run instead of offering to open a path that does not exist: it opens the declaration directly when one exists, or just notifies with no chooser at all for an external repo. sase's TUI never runs sase repo open itself.

Word definitions & spellcheck

When no macro, slash skill, workflow, or file target matches, K treats a plain natural-language word that is not a glossary match as a lookup target. Correctly spelled words open a scrollable definition panel; use j / k, Ctrl+D / Ctrl+U, and g / G to navigate it. Misspelled words open a compact correction panel: press 1–9 to apply a suggestion immediately, or move with j / k and press Enter. The replacement is an ordinary undoable prompt edit.

Definitions require the optional dict command. Spell checking requires GNU aspell with an English dictionary (aspell-en on Debian; Homebrew's package bundles English). If either tool is absent, sase's TUI explains the unavailable feature without affecting the rest of prompt preview. Run sase doctor -D to see the exact optional-tool status and installation hint.

Every word K proves misspelled is remembered durably and gets a red underline in every prompt input from that moment on, in every sase tui session -- no live spell-checking runs on every keystroke; only what K has already checked is ever squiggled. This is distinct from the bold blue glossary underline and the bold lavender repo-name underline, which mark a definable project term or repo rather than a spelling issue. The correction panel offers two ways to stop fighting a word, at two different scopes. Press a to accept a word for SASE only: it is recorded in prompt_misspellings.json, K on it no longer opens the panel, but aspell itself -- and every other consumer of it on the machine -- still rejects the word. Press d to add the word to your aspell personal dictionary instead (usually ~/.aspell.en.pws, though aspell configuration can relocate it), so it stops being flagged everywhere on the machine, not just in sase's TUI; this is reversible by editing that file directly. The add is verified by re-checking the word in a fresh aspell process afterwards, so the squiggle clears only once aspell genuinely accepts it -- a failure leaves the word flagged and reports aspell's own explanation. Case follows aspell: a word added capitalized (Bugyi) stays flagged in lowercase (bugyi). Hyphenated words cannot be added with d -- aspell does not permit - inside a personal-dictionary entry -- and the panel reports that explicitly rather than pretending the add worked. A K press on a now-correctly-spelled remembered word clears its squiggle automatically. The remembered words are stored at sase_home()/prompt_misspellings.json; see ace.prompt_spellcheck to disable the highlight or change how many words are retained.

sase's TUI also computes a non-disruptive live suggestion after a short debounce while the prompt input is in INSERT mode. The suggestion appears in the prompt bar subtitle as [^L] accept ...; press Ctrl+L to accept it. Enter still submits the prompt as typed, so live suggestions cannot accidentally replace text on send.

Live soft completion covers directives, macro names, macro argument names, and enum/bool argument values from the same shared choice rows. File-path soft completion is disabled by default because it can scan the filesystem while typing; enable it with ace.prompt_completion.auto_file_paths: true. The macro/skill menu also opens automatically while typing matching #name, #!name, or /skill tokens; disable that macro auto-open behavior with ace.prompt_completion.auto_macro_menu: false. The directive menu likewise opens automatically while typing matching % directive tokens, fixed values such as %model:, and =alias / ==model shortcuts; disable it with ace.prompt_completion.auto_directive_menu: false. That setting is sase's TUI-only and does not change macro LSP trigger characters in an external editor. The macro/skill auto-menu opens only once at least one identifier character follows its marker, so bare # and / stay quiet. Directive completion opens from a valid bare %, and no automatic menu ever auto-accepts a single match. The grouped @ reference menu opens from a bare @, narrowed artifact/file queries such as @pl or @src/, and syntactically valid @kind: payload contexts; disable automatic opening with ace.prompt_completion.auto_artifact_menu: false. On an un-narrowed bare-@ menu, Enter submits the prompt as typed and Ctrl+F accepts the highlighted first row. The project/Patch picker opens when + completes a token at the start of the prompt or directly after whitespace, {, or |, and is also available through manual Ctrl+T. The VCS ref-root menu opens when : or ( completes a known workflow ref trigger such as #gh: and local candidates exist. The VCS repository menu opens when / completes a known workflow ref trigger such as #gh:owner/; cached rows appear immediately and uncached namespaces fetch in a background worker. Placeholder auto-completion opens only for an incomplete <... context; saved common placeholders join automatic results after the prefix is non-empty, while manual Ctrl+T can show them from a bare <. Manual Ctrl+T inserts a lone match in the highest-priority placeholder source group outright; automatic completion only opens the menu, even for one match. Manual Ctrl+T completion still supports file paths, macro names, directives, skills, =alias / ==model shortcuts, @ references, project/Patch tags, VCS ref roots, VCS repository refs, prompt-local prose words, placeholders, and enabled history words regardless of the automatic settings. Live suggestions pause while the manual completion panel is open, while snippet tabstops are active, in NORMAL mode, and during feedback prompts.

For file completion, directories appear before files in the candidate list. Dotfiles are hidden unless the partial prefix starts with .. Accepting a directory automatically re-opens completion for the next level (drill-down). The completion panel shows up to eight candidates at a time — seven when more candidates remain, so the ↓ N more… line always fits, and one fewer again when the grouped @ reference menu draws its ── files · <base-dir> rule — and scrolls to keep the highlight visible. When exactly one macro or file candidate matches, accepting completion inserts the canonical reference immediately.

Accepting a macro completion, or selecting a macro from the #@ picker, opens an macro args hint panel when the macro has required user-facing inputs. The panel shows the supported arguments and highlights the active one. Press : while the accepted reference is still current to switch to colon syntax, or press ( to insert a required-argument named snippet and use Tab to advance through the snippet fields.

The same smart insertion rules apply to #@ selections and Ctrl+T completions. A selected macro with no required inputs inserts a trailing space, a single required non-text input inserts colon syntax, a single required text input inserts double-colon shorthand, and multiple required inputs insert a parenthesized named-argument snippet. When that trailing space sits at a live snippet tabstop and the next keystroke is Tab or Shift+Tab, the jump removes the space on its way to the next tabstop; when the jump has nowhere to go, the space is kept and ordinary snippet/list fallback continues.

The same hint panel appears while typing narrow, known argument forms such as #name:, #!name:, #ns/name:, #ns__name:, #name!!:, #name??:, #name(, and #name(arg=. The hint is advisory; the backend macro parser still owns expansion semantics when the prompt is submitted. Detection intentionally stays conservative, so prose shorthand, URLs, unknown macro names, #name+, and completed colon text such as #name: value do not keep the prompt-bar hint open.

Alt Brace Syntax (%{...})

The prompt input has dedicated highlighting and editing help for the %{A | B} alt fan-out shorthand (see the Alt Directive reference). It distinguishes the alt delimiters from the branch separators so a fan-out is easy to read at a glance:

  • The %{ opener and } closer are styled as delimiters (bold accent).
  • Top-level | branch separators use a dimmed accent so they read differently from the delimiters.
  • A branch name before a top-level = (e.g. sec= in %{sec=... | perf=...}) is highlighted as a branch name.
  • An unmatched %{ (or stray closer) is flagged as an error span.

The alt overlay layers on top of the existing Jinja and search highlighting rather than replacing it, and it uses the same size guards, so highlighting stays responsive on large prompts.

Editing help in sase's TUI prompt input mirrors the Jinja auto-pair behavior and only fires for the %{...} shorthand. Any % directly before { opens an alternation, wherever it appears: at a word boundary, mid-word (foo%{bar | baz}qux), after punctuation, or nested inside another branch:

  • Auto-pair — typing { immediately after % inserts %{ } and parks the cursor between the two padding spaces. The expansion fires at end of line, before whitespace, before a bracket closer (), ], }, >), and before trailing punctuation (., ,, ;, :, !, ?), so a fan-out can be inserted before the existing ? in Which is better %{ A | B }?. It remains suppressed before word characters and other token-opening characters.
  • Paired delete — backspacing the { in %{|} also removes the auto-inserted }; a forward delete on %|{} removes both braces.
  • | separator normalization — typing | inside a live %{...} span inserts a padded | separator, keeps the cursor after the trailing space and before the closing }, and normalizes comma spacing in the current branch. For example, typing | at the end of %{foo ,bar, and baz yields %{foo, bar, and baz | } with the cursor before }. Openers inside literal zones (inline code, fenced blocks, disabled regions) are ignored; an unclosed span never extends past the cursor's own line, so a stray opener cannot capture a later |; and when alternations nest, the innermost span wins.
  • No Jinja pair after %{ — typing %, #, or { right after an alternation opener starts a branch (%m:, #macro) instead of a Jinja {% %} or {# #} pair.

These edits are suppressed when there is an active selection or when the cursor is not inside a %{...} context, so ordinary { and | typing elsewhere is unaffected. External editor integrations do not own %{} auto-pairing or paired delete; editor-local brace-pair plugins own that lifecycle there. The Neovim plugin still provides the same separator-normalization behavior for prompt buffers.

NORMAL Mode

Press Escape or Ctrl+] in INSERT mode to enter vim-style NORMAL mode. Ctrl+] is equivalent but avoids the terminal escape-sequence ambiguity that can swallow fast following keys after Escape / Ctrl+[. The border title shows [NORMAL] and line numbers switch to relative numbering (current line shows absolute, others show offset).

Motions

Key Action
h / l Move left / right
j / k Move down / up (actual lines)
w / W Next word / WORD start
e / E Next word / WORD end
b / B Previous word / WORD start
ge / gE Previous word / WORD end
f{c} / F{c} Find char forward / backward
t{c} / T{c} Till char forward / backward
; / , Repeat / reverse last f/F/t/T
% Matching bracket
0 / $ Line start / end
^ First non-blank character
{ / } Previous / next paragraph boundary
gg / G Top / bottom of document
Ctrl+D/Ctrl+U Half-page down / up

All motions accept a numeric count prefix (e.g., 3j moves down 3 lines).

Operators

Key Action
d Delete (takes a motion, e.g. dw); copies to clipboard
c Change (takes a motion, e.g. cw); cw/cW stop at the word/WORD end; copies to clipboard
y Yank (takes a motion, e.g. yw); copies to clipboard
> Indent lines covered by a motion by two spaces
< Dedent lines covered by a motion by up to two spaces
gu Lowercase text covered by a motion or text object
gU Uppercase text covered by a motion or text object
g~ Toggle case for text covered by a motion or text object
D Delete to end of line
C Change to end of line
S Change entire line
Y Yank from the cursor to end of line (charwise, like y$)
dd Delete entire line
cc Change entire line
yy Yank entire line
>> Indent current line; count indents multiple lines
<< Dedent current line; count dedents multiple lines
guu Lowercase current line; count lowercases multiple lines
gUU Uppercase current line; count uppercases multiple lines
g~~ Toggle case on current line; count toggles multiple lines
dae Delete entire buffer (copies to clipboard)
cae Change entire buffer (copies to clipboard)
yae Yank entire buffer (copies to clipboard)

Vim-surround commands are also available in NORMAL mode: ys{motion}{delimiter} wraps a motion or text object, yss{delimiter} wraps the current line, ds{delimiter} removes the nearest matching surround, and cs{old}{new} replaces it. Quotes and backticks pair with themselves; either side of (), [], {}, or <> selects the matching pair, with b aliasing parentheses and B aliasing braces. Other single characters pair with themselves. For example, ysiw) changes word to (word), and cs)] changes the parentheses around the cursor to brackets. A count on the ys motion expands its target; a count before ds or cs selects a farther enclosing pair. Successful edits are repeatable with ..

Text Objects

Text objects compose with d, c, and y.

Key Action
iw / aw Inner / a word
iW / aW Inner / a WORD
i" / a" Inner / a double-quoted string
i' / a' Inner / a single-quoted string
i` / a` Inner / a backtick-quoted string
i(/a(, ib/ab Inner / a parenthesized block
i[ / a[ Inner / a square-bracket block
i{/a{, iB/aB Inner / a brace block
i< / a< Inner / an angle-bracket block
ip / ap Inner / a paragraph; ap includes adjacent blank lines
ae Entire buffer

Any operator followed by / or ? acts up to a search match: d/foo<Enter> deletes from the cursor up to, but not including, the next foo, and d?foo<Enter> deletes back to the previous one. It works with d, c, y, gu, gU, g~, >, <, and ys (the delimiter follows).

Key Action
{op}/{pat}<Enter> Operate forward up to the match
{op}?{pat}<Enter> Operate backward back to the match
{op}n / {op}N Operate to the next / previous shared-search match
2{op}/{pat}, {op}2/{pat} Count selects the N-th match (operator × motion)

While you type, the exact region is tinted in the operator's color: red with strikethrough for d/c, green for y, and the theme's secondary color for the transform family. A backward d?foo shows the gold landing match struck through (it will be deleted), while a forward d/foo leaves it intact. The panel says in words what Enter will do, such as delete 42 chars · 3 lines, plus the pane-local count (for example 2/3).

The motion never wraps and stays inside the active pane: d/ only reaches forward and d? only reaches backward. A match starting exactly at the cursor is skipped. When there is no target, Enter changes nothing and reports pattern not found, no match after cursor · 2 before (or no match before cursor · 2 after), or only 1 match after cursor when a count asks for too many. Esc or Ctrl+C restores everything.

The motion is exclusive, with Vim's column-0 adjustment: when the range ends at column 0 of a later row, it becomes linewise if the start is at or before the first non-blank column (for example deleting whole lines), and otherwise stops at the end of the previous row. Matching is the same smartcase literal matching / uses, and a successful operator search records the query so n / N continue from it.

. repeats the same operator, query, direction, and count from the current cursor; a count on . replaces the recorded count, and c replays its inserted text. One u restores the whole edit. Not supported: regex, /e offsets, empty d/<Enter> reusing the last pattern (it cancels), counted plain 3/, VISUAL /, d*/d#/gn, cross-pane operators, and wrapscan for operators.

Other Commands

Key Action
i Enter INSERT mode; inserted text is repeatable with .
v Enter charwise VISUAL mode
V Enter linewise V-LINE mode
a Append after cursor; inserted text is repeatable with .
A Append at end of line; inserted text is repeatable with .
I Insert at line start; inserted text is repeatable with .
o Open below; prompt bullets and ordered items auto-continue, and inserted text repeats with .
O Open above; prompt bullets and ordered items auto-continue, and inserted text repeats with .
[<Space> Insert blank line(s) above current line without leaving NORMAL mode
]<Space> Insert blank line(s) below current line without leaving NORMAL mode
u Undo
Ctrl+R Redo
Ctrl+A Increment the number at/after cursor, wrapping to the prompt top (supports count and .)
Ctrl+X Decrement the number at/after cursor, wrapping to the prompt top (supports count and .)
x Delete character
X Delete character before cursor
r{c} Replace character(s) at cursor (supports count: 3rx)
p Paste after cursor / below line from the internal register
P Paste before cursor / above line from the internal register
~ Toggle case of character(s) at cursor (supports count: 5~)
. Repeat last mutation, including inserted text; a count replaces the recorded count
J Join current line with next, removing a pulled-up prompt - or <N>. marker (supports count: 5J)
K Preview the macro, workflow, skill, file, glossary term, repo name, or plain word under the cursor
Ctrl+] Jump to the macro/workflow/skill/glossary definition, file, or repo checkout under the cursor
/ / ? Search forward / backward in the current prompt pane; after an operator, acts up to the match
n / N Repeat the last confirmed search in its original / opposite direction
* / # Search forward / backward for the whole word under the cursor
g* / g# Like * / #, but also matches the word as a substring

In prompt panes, o and O continue the containing hyphen bullet or ordered item below or above at the same indentation, including when the cursor is on a physical continuation line produced by Prettier wrapping. Non-list lines retain ordinary bare open-line behavior. For an ordered item, O on the marker row takes that item's own number, O on a line the item owns takes the next number (the new marker lands after that item's marker), and o always takes the next number; the surrounding run is renumbered either way. Prompt J removes a supported - or <N>. marker when joining onto a nonblank current line, renumbering the run an ordered item left behind; a blank current line keeps the marker, and non-prompt editors retain vanilla J behavior.

For Ctrl+], sase's TUI opens the target directly in $EDITOR when there is only one available action. Inside tmux, or for loadable Markdown macro definitions, it can show a small chooser for editor, tmux-pane, or load-into-prompt actions. Glossary jumps use the same flow, targeting the owning project's sase/sase.yml definition scalar.

The border subtitle shows pending operators and counts (e.g., 2d when a delete with count 2 is pending).

Search previews matching text as you type. Enter confirms the query, while Esc or Ctrl+C cancels and restores the original cursor. The last confirmed search is shared by every pane in the current ----separated prompt stack and survives a stack rebuild, so switching panes and pressing n or N reuses the same query against the newly active pane.

* and # resolve the keyword run under (or, failing that, forward of) the cursor on the current line and search for it as a whole word (g* / g# match it as a substring instead), landing on the start of the destination match. Unlike / and ?, these are always case-sensitive, matching vim's exemption of * / # from smartcase. n and N afterward repeat with the same whole-word and case-sensitivity rules, not a plain smartcase substring search. When no keyword character follows the cursor on the line, sase's TUI reports "no string under cursor" and leaves the cursor and any existing search state untouched.

Visual Mode

Press v in NORMAL mode for charwise VISUAL mode, or V for linewise V-LINE mode. The border title shows [VISUAL] or [V-LINE]. Escape returns to NORMAL mode, and o swaps the active selection end.

Visual mode supports the NORMAL-mode motions and counts listed above, including word motions, paragraph motions, line motions, f/F/t/T with ;/, repeats, %, gg/G, Ctrl+D/Ctrl+U, and the NORMAL-mode text objects. v exits charwise VISUAL mode; V exits V-LINE mode; pressing the other visual key switches selection kind.

Visual changes (d, c, >/<, u/U, ~) are dot-repeatable over a same-sized range from the current cursor; visual c repeats the replacement text typed before Escape.

Key Action
d / x Delete selection and copy it to the internal register
c / s Change selection and enter INSERT mode
S{char} Surround the exact selection with a delimiter pair
y Yank selection to the internal register and system clipboard
p Replace selection with the internal register
> / < Indent / dedent selected lines by two spaces
u / U Lowercase / uppercase the selection
~ Toggle case in the selection
* / # Search forward / backward for the selected text, literally

Visual * / # search for the exact selected text (including embedded newlines in a multi-line or V-LINE selection) rather than a resolved keyword, are always case-sensitive, and return to NORMAL mode at the search destination.

Visual S uses the same delimiter pairs as NORMAL-mode surround, preserves an exact charwise selection, and leaves the unnamed register unchanged. In V-LINE mode the delimiters go inside the neighboring newlines, so the lines outside the selection do not join the surrounded text. The edit is one undo step and . repeats the saved charwise length or V-LINE row count from the current cursor; a count before . scales that saved shape. Escape or Ctrl+X cancels a pending delimiter without replacing the previous dot-repeat action. Lowercase s keeps its change-selection behavior. V-LINE operators always apply to whole selected lines regardless of the cursor column.

Prompts Overlay

Stash and History live in one Prompts overlay: a single frame with a split-button Stash tab (≡ Stash N plus a 🗑️ chip) and a ↺ History tab, a consistent list/preview split, and one contextual footer per view. Trash is the Stash tab's Trash view, opened with t or the 🗑️ chip; t or Esc there returns to the Stash list, and q closes the overlay. @ on Stash restores the newest draft, so @@ pops the most recently stashed prompt when several are stashed. [ and ] cycle the two top-level tabs with wraparound (even from the focused History filter). From Trash, either bracket opens History, and coming back restores Trash because the Stash tab remembers its view. Clicking the Stash label shows the list, while clicking the 🗑️ chip shows Trash. Esc on the Stash list or on History closes the overlay, and q closes when focus is outside a text input. Each tab keeps its highlight, scroll, filter, loaded pages, preview position, and staged marks across switches, the Stash tab remembers its last view, and History loads lazily on first activation so opening Stash performs no history disk I/O.

Press Ctrl+K from the prompt input to open the overlay on the History tab. That shortcut is available when the current prompt is a single logical line; that line's first active workspace reference (e.g. +sase) becomes an initial project:<name> filter scope once the project-identity snapshot resolves, with the remaining text preserved as a literal search (see Filtering below). A +<project> tag counts when it names a known project or leads the line, a # VCS reference always counts, whichever comes first wins, and references inside code spans are skipped. A blank prompt opens History with no project filter. Press , then . (written ,.) from a main tab, when you are not typing in a text field, to open that same unfiltered History tab. ,. reads the most recently launched workspace prefix other than the built-in #git:home default. Submitting or editing a row replaces that row's workspace prefix with that recorded prefix. A +home launch does not create a qualifying prefix. When none is recorded, sase warns No previously launched VCS macro and opens nothing. ,> opens History with cancelled prompts visible, and ,Ctrl+G skips the overlay and opens the newest history entry in $EDITOR. Both use that same prefix check and the same prefix replacement, so with no qualifying prefix they warn and open nothing. Space on a main tab is separate: it prefills the prompt input with that same non-home prefix, or opens a blank home prompt when none is recorded. The History tab loads prompts previously launched from sase's TUI or sase run in recency pages of ace.page_size rows (default 100). Normal launch writes skip prompts shorter than five words (e.g. y, ok) so they do not clutter the list, while failed-launch recovery can still preserve a short submitted prompt. The same history is available from the shell through sase prompt.

Bare prompts are stored after launch normalization, so a prompt without an explicit workspace reference appears with the default #git:home prefix. Explicit workspace prefixes also feed the prompt-input MRU controls. In the prompt input, the MRU ring is ordered from most recent to oldest: Ctrl+P moves toward older launchable workspace prefixes, while Ctrl+N moves toward newer prefixes. Each edge has a no-prefix stop that removes the first launchable workspace tag from the prompt without touching the remaining prompt text, then wraps. When no workspace tag is present, Ctrl+P starts at the most recent entry and Ctrl+N starts at the oldest one.

Keybindings

Key Action
↑ / ↓ Move the highlight (also Ctrl+P / Ctrl+N)
Enter Submit the highlighted prompt directly
Ctrl+G Open the highlighted prompt in $EDITOR
Tab / Ctrl+I Load prompt into the input widget for editing
Ctrl+J Load older prompts (+ace.page_size, default +100)
Ctrl+K Unload the last page, never dropping below the first page
Ctrl+X Toggle visibility of cancelled prompts
Ctrl+Y Copy prompt to clipboard and close the overlay
[ / ] Cycle the Stash tab and History; Trash is remembered
Esc Trash: back to the Stash list. Otherwise: close
q Close the overlay when focus is outside a text input

Enter submits directly only when the overlay was opened from a History entry point (Ctrl+K, ,., ,>). When it was opened from a Stash entry point (@, ,@, Ctrl+G p, or the stash: chip) and you switch to History, nothing launches: Enter and Tab load the prompt into the prompt bar as a draft, and Ctrl+G opens it in $EDITOR and then loads the edited text into the bar.

Filtering

Type in the search box to filter the prompts that have already been loaded. The grammar is deliberately small: one optional leading project:<value> qualifier, followed by an optional literal text substring matched case-insensitively against the prompt's canonical or humanized text — the plain-substring behavior is unchanged when no qualifier is typed. project: and its value are case-insensitive; a value ends at whitespace, or use a double-quoted value (with \"/\\ escapes) to include spaces. A project: written anywhere other than the leading position is treated as ordinary search text, not a qualifier. Write \project:... to search for literal text that starts with project:.

The value resolves against every loaded project's canonical key, configured display name, and registered aliases (including disabled projects and home) — never by splitting a displayed basename or guessing from a Patch name prefix. A resolved scope matches complete project identity (project:sase excludes sase-core and prose that merely mentions "sase"); for a multi-prompt entry, any segment belonging to the project is enough. An unknown value still matches a prompt whose own historical reference is that exact unresolved text, so a deleted or renamed project stays searchable. A label shared by more than one project is ambiguous and is reported in the helper line below the filter box instead of matching either one silently; an empty or unterminated quoted value is also reported there and selects nothing until fixed. The current scope (or the ambiguous/malformed hint) is shown on that helper line; clearing the project: prefix returns to searching every loaded prompt.

Press Ctrl+J to load older pages, Ctrl+K to unload the last page, and Ctrl+X to toggle cancelled prompts on or off — when enabled, cancelled prompts appear in the results with an x marker. The project: scope only ever considers prompts already loaded into the overlay; it does not search the whole history archive.

Prompt-history rows are compact single-line entries: cancelled marker, last-used timestamp (MM-DD HH:MM when parseable), a project column in the project's accent color, macro/directive chips, and a first-line prompt preview. The preview panel shows the full prompt, with +<project> tags in their project accent colors, and timestamp metadata. History writes use a sidecar lock plus atomic tempfile replacement of monthly shard files under ~/.sase/prompt_history/, so concurrent agent launches do not truncate prompt history. A legacy ~/.sase/prompt_history.json store is migrated into shards before normal reads and writes when the shard directory has not already been created.

Procs Tab

Open the SASE Admin Center with #, then press 4 (or switch tabs until you reach Procs). You can also run the keyless Open procs panel command from the command palette. The tab shows procs (hook runs, mentor executions, agent launches, plugin operations, etc.) with live output for running procs and completed output for finished ones.

Durability and Scope

Procs the TUI runs itself are mirrored into the durable proc store (~/.sase/procs/procs.jsonl, with one combined output log per proc under ~/.sase/procs/logs/), so their outcome survives the session that produced them and is visible from sase proc list / sase proc show. Supervisor-backed procs — commands submitted with sase proc run, programmatic submissions, and the unattributed command fallback for an epic approval whose planner agent session cannot be resolved — are read back out of that store and rendered here, so work that this process never owned still shows up on the tab.

The pane defaults to this session plus unattributed procs; press a to widen it to every session. Historical detached rows remain visible in both modes. The pane title names the active scope and the running-lane counts, e.g. Procs · this session ⚙ 2 ⚒ 1 ⚙ 1 [4 running · 5 done]. The blue gear is running TUI background procs — the same rows the top-bar bg: group counts, excluding monitors, tool-run carriers, and update rows; the sky-blue ⚒ chip is tool-lane procs (an inventory count, so it shows a dim ⚒ 0 when empty — it can differ from the top-bar run count because foreground runs have no proc and a joined run has two); the orange gear is bare monitors. A green ⚙ N chip appears after the orange chip only while update procs run, and update rows carry a green ⚙ marker. All counts follow the tab's current scope, so a moves them with the list. A zero blue/orange lane still renders as a dim ⚙ 0 so a missing chip cannot be read as "unknown". The bracketed totals keep their current meaning: blue plus tool plus green plus orange equals the running count. Rows read from the store carry a colored session chip (ace·sase#14 4f2a) that matches the one sase proc list prints; a session that has since exited renders dim with a †. An ordinary unattributed proc renders a dim —; a historical detached proc carries a cyan ◆ detached marker that makes the legacy row kind explicit.

Store reads happen on a worker thread and are revalidated by store mtime about once a second, so the tab never stats, reads, or locks the store from a render or keystroke path. Retention is governed by procs.history_limit (see configuration): finished rows and their logs age out oldest-first, and running procs are never pruned. Because the store owns that retention, d / D do not dismiss rows; they only explain the retention policy.

The top-bar bg: group's blue chip counts this session's active background procs plus every active unattributed proc globally, including an approved epic that had to use the unattributed command fallback. Tool-run carriers are counted in tools: instead, running bare monitor turns in the orange chip there, and service procs and oneshots are left to the Services tab.

Layout

The tab uses a two-panel layout: a proc list on the left and an output pane on the right. Running procs refresh their output on the pane's 0.25 s tick while the Procs tab is visible.

Proc Status Icons

Icon Color Meaning
◌ Dim Pending (supervisor starting)
● Green Running
✓ Cyan Success
✗ Red Error
⊘ Yellow Killed
? Dim Unknown
⚙ Orange Monitor turn (same mark as the Agents tab and the top bar)
⚒ Sky Tool-run carrier (same mark as the tools: group)

Monitors on this tab

A sase monitor start supervisor is a durable proc like any other, but this tab marks it the same way the rest of sase's TUI does. See Monitors.

  • Orange ⚙. Bare monitor rows carry the orange gear between the status icon and the label (● ⚙ just check-full), matching the Agents tab and the orange chip in the top bar's tools: group. A monitor carrying a live run shows ⚒ instead. The same mark prefixes the output header.
  • Agent name. Each monitor names its member agent (acme--mon) on the list's secondary line (acme--mon · Working...) and on an agent line in the output header.
  • Status chip. When the matching agent row is loaded, the effective status label (TESTING while running, TESTED once settled) appears in that pair's accent color between the agent name and the secondary text (acme--mon · TESTING · Working...), and again on the output header's agent line.
  • Live live_reply.md. While the Procs tab is active, the 0.25 s tick refreshes the selected running proc. For a monitor, that output is the tail of <artifacts_dir>/live_reply.md: the last 400 lines, including text rotated into live_reply.md.1. Until that log has text, the pane still shows Working.... On the Agents tab, the Reply card follows that same file in place when the live-reply follow is active; see Agents Tab Main Deck.
  • <enter> jumps to the agent. On a monitor whose agent row is loaded, <enter> (or a click) closes Admin Center and reveals that agent on the Agents tab. The hints line shows ⏎: agent only when that jump is possible. If the agent is not on the Agents tab, sase's TUI says so and stays put.
  • Visible in both scopes. Monitor procs are unattributed, so they appear in both this session and all sessions.
  • K stops the supervisor. Kill uses the named-proc stop path: it stops the supervisor, settles the session, and runs any --next action.

Durable Procs

Procs are durable records shared by every SASE surface, not just rows in this pane. They live in ~/.sase/procs/procs.jsonl, with one combined stdout/stderr log per proc under ~/.sase/procs/logs/<proc_id>.log. Because the records outlive the process that produced them, sase proc can list and inspect work started anywhere — including a TaskTriage launch or unattributed command submitted from another client.

Each proc carries a 12-character id resolvable by unique prefix (three characters minimum, like a git short SHA), so sase proc show k7m2 works. Statuses are pending, running, success, error, and killed; terminal states are final, and a proc whose supervisor died without reporting is reconciled to error rather than left running forever. sase proc list reuses the icons above and adds ◌ for pending and ⊘ for killed.

A proc may also carry a named proc: sase proc run -N/--name NAME (bare names resolve beneath the calling sase-agent; agent/name is fully qualified) names the proc so sase proc show, sase proc list -N, and sase proc kill can address it by name instead of id. Resolution tries an exact fully qualified name, then an exact proc id, then a unique id prefix. Active uniqueness is scoped per project — starting a proc under a name already held by an active proc in the same project is a conflict — and a name is only reusable once the proc holding it settles. A monitor's member agent name (for example acme--mon) is its own named proc.

Kinds and ownership.

Kind Typical producer Owner and scope
tui Work run and mirrored by sase's TUI sase's TUI process; scoped to its session
command sase proc run or sase.procs.submit_proc() The proc supervisor; attributed to one session or left unattributed
detached Historical rows from retired CLI/API detached submissions The legacy proc supervisor; global because no session owns the row

The programmatic submission API is sase.procs.submit_proc(). Pass session_id=None when no interactive session should own the row; it still writes a command proc. The legacy sase.procs.submit_detached_proc() wrapper remains for old callers but now records the same unattributed command row. The public read_procs() and filter_procs() helpers accept kind= as either one kind or a collection, including detached when a caller needs historical rows. A command or historical detached row that remains pending without a supervisor PID for 60 seconds is reconciled to error; a mirrored tui row is left to its owning TUI. Epic launches record the approving surface as ace, telegram, cli, or axe, with api retained as the fallback for direct or unrecognized API callers.

Session attribution is not delegation: a command proc always executes under its own supervisor, while its session id decides which TUI includes it by default. --session accepts a full session id, a unique id prefix or short handle, or current, latest, and none; the default is this process's sase's TUI session, then the newest live one, then no session. sase proc run --session none creates an unattributed command row. sase proc list scopes work to the resolved session plus unattributed rows by default, and widens to every session with --all. Rows from a session that has since exited render dim with a † marker.

Retention. procs.history_limit caps how many finished procs are kept; pending and running work is never pruned for being old. Lowering the limit removes the oldest finished rows and their log files. Finished procs tagged command-line (TUI Command Line submissions) keep a separate bucket of 50 beside this limit. The legacy tasks.history_limit key is still honored as a deprecated alias.

The CLI equivalents are sase proc list, sase proc show ID (--follow to stream), sase proc run [--session SESSION|none] -- COMMAND (--wait to stream and inherit the exit code), and sase proc kill ID. A hidden legacy --kind filter remains for historical kind rows. Approved epics normally launch as monitor turns; only an unresolvable planner agent session uses an unattributed command proc. See the CLI reference.

Filtering procs

Press / to reveal a query bar above the proc list, prefilled with the query already active. Typing narrows the list live; the status lane shows N matches or a parse error with its exact span. Enter commits and returns focus to the list; Esc restores the query that was active when the bar was opened. Unlike the Artifacts panes, the bar is hidden when there is no query and appears — read-only, syntax-highlighted — only while a filter is active, so an active filter is never invisible without also being silent. The header gains a · N/M shown segment while filtered, and the query survives closing and reopening the Admin Center in the same session.

Free text (and its explicit text: spelling) matches the command string, the row label, and the retained output; cmd: and out: narrow to one side. Every key is negatable with a leading -, and a boolean key takes the bare shorthand (monitor means monitor:true). The service boolean matches service-proc runs (daemon or oneshot), and svc:<name> narrows to one service proc by exact name. tag:<tag> matches a proc tag exactly (for example tag:command-line for Command Line runs, visible by default), and origin:<origin> matches the submitting origin (for example origin:ace).

The seeded default query is -service, so service-proc runs stay hidden unless asked for; ace.procs.default_query in sase.yml changes the seed. It applies only when no committed query is persisted — a user-cleared query stays cleared.

Key Kind Meaning
free text string Command, label, output (implicit AND); same as text:
cmd: string Command string only
out: string Retained output only (last 32 KB)
name: string Row label / display name
agent: string A monitor row's member agent name
project: string Project key or display name
status: enum pending, running, settling, success, error, killed
kind: enum command, tui, detached
monitor bool A sase monitor start named proc
service bool A service proc run (daemon or oneshot)
svc: string Service name (exact)
tag: string Proc tag (exact, for example command-line)
origin: string Submitting origin (exact, for example ace)
running bool Active and owned by a live session
failed bool Terminal status is error or killed
exit: int Exit code (exact)
min: duration Runtime at least N seconds (or 5m, 2h, 1d)
max: duration Runtime at most N
after: date Completed at or after the bound
before: date Completed at or before the bound
since: date Started at or after the bound
until: date Started at or before the bound
limit: host Row cap; all removes it

Runtime is (finished_at or now) - started_at, so min:/max: read sensibly for a still-running proc too. before:/after: bound completion time and since:/ until: bound start time; a running proc has no completion time, so before:/after: never match it and -before:X includes it. For example, "just check" -monitor -min:300 matches non-monitor procs that ran for at least five minutes and mention just check in their command or output.

m cycles the monitor term through three states without opening the bar for editing:

(no monitor term)  →  monitor  →  -monitor  →  (no monitor term)

Each press rewrites the query in place, leaving every other term untouched, and never steals focus from the row list. If the monitor term was the query's last term, the last press clears the filter and removes the bar.

Keybindings

Key Action
j / k Navigate proc list
/ Filter procs
m Cycle the monitor filter
' Jump to a proc row via adaptive hints
a Toggle scope: this session / all sessions
K Request a kill for a selected active durable proc
Enter Open the selected monitor's agent
d Explain finished-proc retention (does not dismiss)
D Explain finished-proc retention (does not dismiss)
e Open proc output in $EDITOR
y Copy proc output to clipboard
Ctrl+D / Ctrl+U Scroll output pane down / up
g / G Jump output pane to top/bottom
Tab / Shift+Tab Switch Admin Center tabs
q / Esc Close SASE Admin Center

K and D also answer to Shift+K and Shift+D, matching the pane's existing Shift+G convention. d and D currently perform no dismissal; both explain that finished rows age out according to procs.history_limit.

K opens a danger confirmation only for a store-backed row that sase's TUI still considers active. The backend stops command/named-proc records, including monitor supervisors. It refuses a TUI-owned record because only its owning sase's TUI session may stop it, and a legacy record whose supervisor has died is reconciled to error rather than killed. Other ineligible rows explain why: a finished proc reports Proc already finished, a proc still being submitted reports Proc is still submitting — try again in a moment, and a session-local row with no durable record reports that it cannot be killed from the Procs tab.

Updates Tab

Open the SASE Admin Center with #, then press 8. The Updates tab is one master/detail inventory: every SASE core package, plugin, and registered agent CLI appears as a row, grouped into SASE, Plugins · Built-in, Plugins · Community, and Agent CLIs sections. An always-visible header above the list shows either the all-current banner or a digest of update counts, cache age, install mode, and any failed source — it never claims everything is current while a source is unknown or failed. The list always shows every row (installed or not), with updatable rows first in each section. ' jumps to any row via adaptive hints, across every section.

Opening the tab never refreshes from the network. The first open in an ACE session builds the inventory from local caches the automatic update check maintains (plugin catalog, latest-version caches, editable checkouts' last-fetched upstream refs), without PyPI/GitHub/npm or git fetch. Later opens reuse the in-memory inventory instantly; when the automatic check has published newer results, the open re-reads those caches instead. r refreshes everything from the network, and checked … ago is the age of the last network check.

Core rows show SASE package versions and incoming commits in their own per-package detail. Plugin rows bring the full sase plugin experience: filter the catalog, inspect a plugin, and install (i), update (U), uninstall (x), or switch install mode (m). Agent CLI rows are provider-colored, showing installed → latest versions, exact update or manual commands, vendor docs links, update marks, and durable update history. Missing agent CLIs that SASE can install show latest v… with an [npm] or [script] badge and carry the same install verb as plugins: i installs the highlighted CLI (or every install-marked row), and Space / I marks it for a bulk install. On any markable row — an installable plugin or CLI, or an updatable CLI — * marks every visible row in that section with the same action, or unmarks them when all are already marked. Marked rows show [✓]. Esc first clears every mark (including filter-hidden ones); with nothing marked, Esc closes the Admin Center. A filter that matches nothing shows Nothing matches the current filter. Every install opens a confirm preview first — the exact command, plus for an agent CLI its target directory and PATH status and, for a script install, the script URL, size, and full SHA-256 — then runs the previewed plan sequentially in one tracked proc, CLIs before plugins when a marked set mixes both. CLIs SASE cannot install toast their manual instructions instead. Providers that opt out of independent CLI management, including the bundled internal Fakey provider, are omitted from the Agent CLIs section. The plain substring filter (/) searches every row's own fields — name, owner/repo, description, and topics for plugins; name, display name, binary, install method, route, package, and not installed for agent CLIs; package name for SASE rows — across all sections at once.

Every sase-managed agent-CLI update run from ,U, ,E, A, or sase agent-cli update — and every install run from the Updates tab — is appended to ~/.sase/logs/agent_cli_updates.jsonl. Runs where no command reaches a terminal outcome are not recorded. Install runs show a green ↓ with installed <version> and carry the i badge; update runs keep their existing glyphs and badges. Highlighting an Agent CLI row renders that journal below its details; H toggles between this CLI's executed install/update rows and a run-grouped timeline across all CLIs. Configure the panel with ace.updates.agent_cli_history and agent_cli_history_max_rows.

Automatic checks publish one composite snapshot after first paint. Ten-minute session ticks only revalidate cached SASE/plugin rows and provider names already known outdated; full discovery waits for the longer configured recompute cadence, and provider registry lookups retain their own cache. The top bar renders the updates: group as its only dark chip (lime ⬆ N SASE and sage CLI ⬆ N segments with separate counts, plus a lime core tag for a sase-core rebuild, plus a green ⚙ gear inset at the left edge while SASE is updating, a yellow ⚙ gear inset while a tracked restart waits for TUI-local tasks and installation changes, or a red ⚙ gear inset while the most recent update attempt failed). Independent commands keep running. Durable operations started from this TUI still finish their result handling first, within the same 60-second wait.

A red gear means the last update or update-planning attempt settled with an error, or ACE exited before it finished (an interrupted attempt, whose install may be incomplete). Clicking the red gear opens the failure report: the failed attempt's label, error, and last output, with u opening the Update panel, d dismissing the recorded failure, y copying the report, and q closing. A later attempt that starts after the failure clears the gear on success, as does dismissing it. The record lives in ~/.sase/update_attempts.json, so it survives ACE restarts and every ACE instance on the machine converges on it at startup and on the ten-minute update-check tick.

For editable host, core, and plugin checkouts, the running TUI also remembers the Git HEAD imported by the process and cheaply checks whether the checkout has moved on disk. It warns once for each new on-disk generation. While any imported root is stale, the global Update panel (,U) adds Restart ACE as its first row; press x or X to restart after the same TUI-task and installation-change drain used by post-update restarts. Independent commands keep running. Durable operations started from this TUI still finish their result handling first, within the same 60-second wait. This reloads the already-updated code and does not fetch or modify a checkout.

Every mutation still plans before it runs, and Ctrl+D / Ctrl+U scroll long preview panes. When commit previews are enabled and a comparable range is available, core and installed-plugin update confirmations load incoming commits by repository in the background; install confirmations do not. The global ,U chord opens the Update panel from already-fetched SASE and provider snapshots — no Admin Center, no live inventory load. e / s / p (or ⏎ on the highlighted row) choose Everything, SASE, or providers and then show the same y/n confirmation sase's TUI uses elsewhere, containing only the selected legs. E / S / P plan those same scopes and skip only that final confirmation after a runnable preview succeeds; failed or already-current previews still do not mutate. ,E is the global direct alias for ,U then capital E: it uses the same cached snapshots, preview planning, no-op handling, and error reporting without mounting the panel. The providers row lists each captured provider with its installed-to-latest version transition and marks manual-only providers by name, and the Everything row summarizes both legs on one line. While a recorded update failure exists, the panel adds Last update failed (or Last update interrupted) as its first row, above Restart ACE, with an ✗ failed chip and the failure's label and error; f / F opens the failure report and d dismisses the failure in place without closing the panel, and the open panel refreshes when the journal view changes. r re-checks in place; q / Esc cancel. An Everything confirmation groups SASE and Agent CLI work into labeled sections with update/current/skipped glyphs, counts, and commands. The tracked proc runs Agent CLI commands first and the SASE/core/plugin leg second. A failure in one leg is reported alongside the independent earlier results. After a changed core/plugin update restarts sase's TUI, the one-shot result toast can show applied commits grouped by repository as well as file/line statistics. Configure the toast with ace.updates.post_update_toast_commits, post_update_toast_max_commits, and post_update_toast_diffstat.

The providers leg still captures the agent-CLI candidates from the latest completed automatic result, revalidates exactly those names, and never broadens the captured set from an Updates-pane load. Manual-only providers remain in the preview with their suggested command or docs. A real SASE/core/plugin code change restarts sase's TUI and its service controller only after provider work finishes, while provider-only updates refresh in place and report per-provider results (versions, failures, and manual commands) in the completion toast instead of restarting. Before that restart, sase's TUI waits up to 60 seconds for TUI-local tasks, in-flight submissions, and installation changes to finish (a toast reports the queued restart) and then restarts anyway with a warning naming whatever is still active. Independent work — agent tool runs, ordinary durable commands, oneshots, monitor turns, and service daemons — keeps running. Durable operations started from this TUI still finish their result handling first, within the same 60-second wait.

u remains pane-wide and updates SASE core plus installed plugins. A is the separate pane-wide agent-CLI action: it updates marked agent CLIs from anywhere in the pane, and with no marks it targets every safely updatable installed CLI. i installs Space/I-marked plugins and agent CLIs together — agent CLIs first, then plugins, in one tracked proc — or the highlighted row when nothing is marked. See the Updates tab reference for the full keymap and behavior, Plugins for the equivalent sase plugin CLI, and Agent providers for the equivalent sase agent-cli CLI.

The Updates pane still binds a to a tracked agents-sidecar publication sync. That action publishes and reconciles agent hoods for every enabled project; it is not part of the comprehensive update. See Agent Hood Synchronization for privacy, publication, status, and recovery behavior.

Snippets

The prompt input supports expandable text snippets triggered by pressing Tab. Snippets are configured in the ace.snippets section of sase.yml as a mapping of trigger words to template strings. Inspect or edit the same catalog from the shell with sase snippet (list, show, add, delete), or from sase's TUI with the Snippets panel (gT / Ctrl+G T).

ace:
  snippets:
    fix: "Please fix the following issue:\n$0"
    review: "Review this code for correctness, performance, and style."
    bug: "Bug in $1:\n\nExpected: $2\nActual: $3\n\nPlease fix.$0"

Usage

  1. Type a trigger word (e.g., fix) in the prompt input.
  2. Press Tab. If the word before the cursor matches a snippet, it is replaced with the template text.
  3. If the template contains tabstop markers ($1, $2, ...), the cursor jumps to $1 first. Press Tab again to advance to $2, then $3, and so on. Shift+Tab retreats through already visited tabstops. $0 marks the final cursor position after all tabstops are visited. If there are no tabstop markers, the cursor moves to the end of the expanded text.

Tab priority: Snippet expansion always takes priority over tabstop advancement. If you type a trigger word at an active tabstop and press Tab, the snippet expands rather than jumping to the next tabstop. Expanding inside the live snippet nests the new snippet session: sase's TUI visits the nested snippet's tabstops first, then resumes the enclosing snippet at the next outer stop. Expanding outside the current snippet resets the tabstop session.

Multi-line indentation: When a multi-line snippet is expanded on an indented line, continuation lines automatically inherit the leading whitespace of the trigger line. Tabstop positions are adjusted accordingly.

Trigger words are matched against the alphanumeric/underscore word immediately before the cursor. If no snippet matches, Tab advances to the next tabstop (if any are remaining from a previous expansion), and Shift+Tab retreats to a previously visited tabstop. If neither snippet action succeeds, the key falls back to INSERT-mode list shifting when the cursor is on a supported marker line. Advancing from the final tabstop clears the session before that same fallback check runs.

Macro-derived snippets compose normal macro references before they enter the snippet registry. After macro-derived snippets and ace.snippets are merged, any snippet can splice another snippet by trigger with #[trigger]. #[trigger(value)] and #[trigger:value] fill the referenced snippet's $1, $2, ... tabstops before splicing. The final template is renumbered so tabstops from the caller and referenced snippets do not collide.

Snippet variables

Snippet templates can reference the prompt's target project with the #{project} variable, which is substituted when the snippet expands on Tab:

ace:
  snippets:
    epic: "the #{project}-$1 epic bead"

With +sase leading the prompt, epic<Tab> inserts the sase- and leaves the cursor at $1; with +bob-cli it inserts the bob-cli-. The value is the project's display name (its PROJECT_NAME, the same prefix bead IDs use), never the directory key.

Resolution order on expansion:

  1. The prompt's own explicit target: a leading +<project> tag or #gh:/#git: VCS ref, then a non-home prompt context.
  2. The SASE current project (the TUI's +<project> chip value).
  3. Otherwise the token stays unresolved.

Only a known variable with a resolved value is replaced. Any other #{name} (for example Ruby's "#{x}") passes through verbatim, as does an unresolved #{project} -- in that case the TUI shows a warning suggesting a +<project> tag. There is no escape syntax. Because substitution happens at expansion time, composed callers inherit the variable for free: a repic snippet built on #[epic] resolves #{project} the same way. Capitalized aliases uppercase only the template's first character before substitution, so a substituted project name keeps its own case.

Capitalized aliases

Every effective snippet also gains a generated initial-capital alias. For each explicit trigger, SASE uppercases only its first character — the rest of the trigger is preserved byte-for-byte — to form a companion trigger, and uppercases only the first character of the resolved template to form the companion expansion. So authoring only

ace:
  snippets:
    foo: "foo bar baz"

exposes both foo → foo bar baz and Foo → Foo bar baz.

  • Only the first character changes. Triggers that are already capitalized, digit-leading, or underscore-leading produce no extra entry, and a template whose first character has no uppercase form expands unchanged (its distinct trigger alias is still created).
  • An explicitly authored capitalized trigger always wins. If both foo and Foo are defined, each keeps its own template, and no alias is generated over the authored Foo.
  • Aliases are runtime-only. They are never written back to sase.yml, macro front matter, or chezmoi source files, and they never prevent you from later defining the capitalized name yourself.
  • Both spellings participate in #[trigger] composition, so #[foo] and #[Foo] both resolve, and generated templates preserve tabstop and escape behavior.

The rule applies uniformly to macro-derived snippets, merged ace.snippets, and snippets saved into the current sase's TUI session — including a second save that updates an already-pending trigger. The same pairs appear through sase's TUI, sase editor helper-bridge snippet-catalog, normal LSP completion, and the native Rust fallback.

You can also create a snippet on the fly from the prompt save panel, opened with gX or Ctrl+G X. Press Ctrl+X in that panel to switch to snippet mode and choose which config file should store the new ace.snippets entry. In snippet mode, rows are grouped by source and sorted alphabetically by trigger; snippet completions elsewhere are listed in trigger order, too, for stable display. As soon as sase's TUI reports the snippet as created or saved, it is available to every prompt input already open in the current TUI; no prompt remount or restart is needed. When ace.snippet_config_path is configured, this panel's row list always offers it, pre-selected, as a synthetic destination row — even when it points outside the standard discovered locations — and shows why if it falls back to a discovered location instead (for example configured path unusable: read-only).

When use_chezmoi is enabled, the save panel writes the chezmoi source file first. sase's TUI keeps that successfully written snippet live as session state even before deployment. Skipping or failing the optional commit/push/apply step does not remove it from the running TUI, but another SASE process will not see the source-only change until chezmoi is applied. SASE applies chezmoi from this flow only after the user confirms the optional commit-and-push action.

Editors using sase lsp can receive the same registry as LSP snippet completions after bare trigger words when the client advertises completionItem.snippetSupport. The server uses the editor helper operation sase editor helper-bridge snippet-catalog as the authoritative source and falls back to native Rust loading only for simple snippets if the helper is unavailable. Clients without snippet support do not receive these entries, because raw $1 / $0 markers would not behave like sase's TUI tabstops.

Save location picker

Starting a new mini-macro (gx, Ctrl+G x, Ctrl+G Ctrl+X) or a new snippet (gt, Ctrl+G t) first shows a location picker: one panel that asks where the new entry should live. The picker opens synchronously, before any disk reads, so keys typed while destinations load are buffered and applied — never dropped into the prompt pane.

An Existing section sits at the top of the list, always in the same place: e opens a fuzzy finder over every physical definition (✎ Edit existing macro… / ✎ Edit existing snippet…). While a pane of that kind is already open the row reads Switch to existing … instead. The Existing row is never the ★ default. With zero definitions it stays visible but disabled (no macros yet / no snippets yet).

One keypress picks a destination and Enter accepts the ★ default, whose reason is shown (★ current, ★ last used, ★ configured, ★ default, ★ override). j/k (or ↑/↓, Ctrl+N/Ctrl+P) move the highlight and skip headers and unavailable rows; Esc (or q) cancels and returns focus to the origin pane.

Hotkeys are mnemonic and scope-first: e is existing, p is the project destination, and h is the home destination in both pickers. In the mini-macro picker the Shift variant picks the config file of the same scope.

If the prompt pane that opened the picker is gone before you choose — closed, or no longer in the stack — the picker closes and sase's TUI warns Prompt pane is no longer available - snippet discarded or Prompt pane is no longer available - mini-macro discarded. A destination load that fails stays on the picker and replaces the list with one red error row. For a snippet that row and the error toast both read Failed to prepare snippet pane: …. For a mini-macro the row reads Failed to load destinations: … and the error toast reads Failed to prepare mini-macro pane: …. Cancel still returns to the origin pane when that pane is still there.

Picker Key Destination
both e Existing-definition fuzzy finder
macro p Project sase/macros/ directory (#<project>/…)
macro P Project sase/sase.yml
macro h Home ~/sase/macros/ directory
macro H User sase.yml
macro 1–9 Other Project/Home rows in display order
snippet p Project sase/sase.yml
snippet h User sase.yml
snippet c Configured ace.snippet_config_path (own top section)
snippet 1–9 User sase_*.yml overlays in display order

The default (↵) precedence is:

  • Mini-macro: ★ current (the open pane's location when retargeting) → ★ last used → ★ default on the Project directory (the Home directory in home mode) → the first writable row.
  • Snippet: ★ current (the open pane's location when renaming) → ★ configured (an explicit ace.snippet_config_path, which outranks last-used) → ★ last used → ★ default on the resolved default file → the first writable row.

The footer previews each destination (→ <dir>/<name>.md · called as #<ns>/<name> · N macros here for directories, → <file> · ace.snippets.<trigger> · N snippets here for snippets). The Existing row previews → fuzzy-find N macros across M files · edit in place or override read-only ones. Rows that already define the typed name show has #name / has ⇥ trigger. Choosing a destination locks it for the name step: the old destination cycling is gone, and ⇧Tab in the name step goes back to the picker while keeping the typed text. Writable plugin and built-in rows stay collapsed behind one Plugins & built-in summary row (+, Enter, or a click toggles it), so plain users never see clutter.

Existing-definition finder

e opens a two-pane finder: matches on the left, a preview on the right. Type to filter; ↑/↓/Ctrl+N/Ctrl+P move the highlight while focus stays in the query; Enter opens the highlighted row; Ctrl+D/Ctrl+U scroll the preview; ⇧Tab returns to the location picker (the query is remembered for the next e); Esc cancels. Keys typed as type-ahead after e while the picker is still loading become the finder's initial query.

Each row is one physical definition. Status chips:

Chip Meaning Enter
● active Runtime winner, writable Edit in place
◐ shadowed Writable, but another definition wins Edit in place, with a warning
🔒 built-in / 🔒 plugin / 🔒 read-only / 🔒 from #macro Not writable here Override detour: pick a writable destination, then the name step
✗ swarm / ✗ workflow / ✗ skill / ✗ memory Not a simple mini target Refused; the verdict explains why

An in-place edit opens the mini-macro or snippet pane on that definition's body. If a pane of the same kind is already open, picking a definition replaces the draft (body, frontmatter, and target) after the usual discard confirmation when the draft is dirty. Picking the definition the pane already targets just focuses it and reports Already editing #name. A read-only pick reopens the location picker in override mode: the Existing row is hidden, destinations that would change the callable name are disabled, rows that would still lose after save are badged ⚠ shadowed by …, and the default is badged ★ override. ⇧Tab from that name step returns to the override picker, not the original one.

Authoring a snippet from the prompt bar

gt (NORMAL), Ctrl+G t, or Ctrl+G Ctrl+T (INSERT or NORMAL) opens a dedicated snippet pane at the bottom of the prompt input stack, a faster loop than the general save panel above when you already know you're authoring a trigger:

  1. Choose where. gt first shows the location picker: one keypress picks the config file and Enter accepts the ★ default. Press e to fuzzy-find an existing snippet and edit it in place, or start a guided override when the definition is read-only. Keys typed while the destinations load are kept as type-ahead for the next step.
  2. Name it. The trigger-name panel shows the locked destination (⇧Tab goes back to change it): type a trigger and it validates live, lists up to six existing triggers that share your typed prefix (Tab completes to the highlighted one, and ↑/↓ or Ctrl+P/Ctrl+N move that highlight). The verdict line reports one of: an invalid trigger; ✓ Create for a fresh trigger; a warning that the trigger already exists in the destination (Enter will load it for editing); a warning that it's defined in a different config file (saving here will shadow or be shadowed by that file, per your project's precedence); or a warning that the trigger is derived from a macro and this entry will override it.
  3. Open the pane. Enter opens the snippet pane — empty for a new trigger, or pre-filled with the current definition (from the destination, the shadowing file, or the derived macro template) when the trigger already exists. The pane always opens in INSERT mode and is unmistakably not a prompt pane: its own separator rule names the ⇥ <trigger> and destination, with a state marker (✓ clean, ● dirty, new for an unsaved trigger), and its own accent color and subtitle. It is never included in a launch, a stash, or a save-as — Enter in it means "save the snippet", not "submit the stack".
  4. Save it. Enter in the pane opens the save confirmation, showing [Draft] for a brand-new trigger or opening straight on Diff — a real difflib unified diff against the existing entry — for an overwrite (Ctrl+O cycles Draft / Existing / Diff, Ctrl+D/Ctrl+U scroll). An empty body refuses to save; a byte-identical overwrite reports ✓ No changes and closes without writing; a destination that changed on disk since the pane opened warns and offers r to reload the current definition instead of overwriting blindly. Enter writes the file, publishes the new template to every open prompt input in the session immediately (no restart needed), and closes the pane only once the write succeeds — a failed write leaves the draft in place to retry.
  5. Follow-ups. A successful save runs the same post-write chooser as the general save panel: an optional commit & push, and — for a chezmoi-managed destination — a scoped chezmoi apply limited to the deployed snippet file.
  6. Discard or rename. Ctrl+C discards the pane; if you've typed anything different from the loaded body, a confirmation guards against losing it. Esc returns to NORMAL mode without discarding. gt again while the pane is open shows the location picker first (defaulting to the pane's current file), then re-opens the trigger-name panel prefilled with the current trigger to rename or re-target it without touching the body you've written. On close (saved or discarded), focus and the cursor return to exactly the pane and position you were at before gt.

Macro Picker (#@)

Typing #@ (the # character followed by @) opens the Macro snippet picker modal. This lists all available macros (including project-local macros from sase/sase.yml files) and inserts the selected reference at the cursor position. Inline-capable macros and workflows insert as #name; standalone workflows insert as #!name. The picker uses the same argument-aware skeletons as macro completion, so typed inputs can be filled immediately after selection. Markdown macro swarms are inline-capable and insert as #name. This is separate from the ace.snippets mechanism — it provides quick access to macro references rather than expanding static templates.

Auto-Refresh

sase's TUI auto-refreshes data at a configurable interval (default: 10 seconds). The remaining time until the next refresh is shown in the info panel. Set --refresh-interval 0 to disable. Press R on Artifacts or Services, or r on Agents, to open the Refresh panel and choose a manual refresh without waiting for the next tick.

Tab switches are instant: cached data is shown immediately while a background refresh runs asynchronously, so moving between tabs never blocks on disk I/O.

When the inotify-based artifact watcher is active, the periodic tick is event-driven: it consults per-surface dirty flags (_dirty_patches, _dirty_agents, _dirty_axe) and short-circuits the whole tick when nothing has changed. With ace_refresh_tokens enabled (the default), cheap stat-only change tokens also skip unchanged surfaces when no watcher is available. A 300-second --sanity-refresh-interval floor still triggers a full reconcile to recover from missed events, so a quiet TUI does ~zero work between real changes without going stale.

Performance Tracing

For diagnosing TUI latency, set SASE_TUI_TRACE=1 before launching sase tui. Tracing is near-zero-cost when the env var is unset; with it enabled, each instrumented hot path emits one JSONL line per span to ~/.sase/perf/tui_trace.jsonl (override via SASE_TUI_TRACE_PATH=…). See docs/perf_runbook.md for the full span catalog, benchmark harness, and per-phase performance targets.