SASE Pager¶
sase pager opens artifact references, file paths, or stdin in the SASE link-traversing
pager. The same surface is used by sase bead show, sase artifact read, and text
artifacts opened through the artifact viewer.
With redirected stdout, --plain, or no controlling terminal, the command writes plain
text. That makes it safe in pipelines and safe as an unconditional pager command.
Usage¶
sase pager bead:sase-uk.7 src/sase/cli_pager.py
git show --stat | sase pager -t "git show"
sase bead show sase-uk.7
sase pager [-c auto|always|never] [-l auto|never] [-p] [-s ALIAS] [-t TITLE] [-w WIDTH] [REF|PATH ...]
| Option | Purpose |
|---|---|
REF\|PATH |
Artifact reference or file path. Omit it, or pass - by itself, to read stdin. |
-c, --color |
Color output mode: auto, always, or never. never also disables syntax highlighting. |
-l, --links |
Link scanning mode: auto or never. never opens the app without painted link labels. |
-p, --plain |
Write plain text without starting the Textual pager. |
-s, --syntax |
Highlighting language: auto, none, or a Pygments alias. Invalid aliases fail before stdin is read. |
-t, --title |
Title for stdin input. |
-w, --wrap |
Prose wrap width; accepts an integer, auto, none, or 0. |
CLI Paging¶
page_or_print() preserves the shared print-vs-page decision for CLI output: never
writes directly, auto pages only on a real terminal when output is taller than the
terminal and SASE_AGENT is unset, and always pages whenever the terminal can host
the Textual app. Paging runs the SASE pager in-process and passes the structured
document directly; SASE no longer shells out to $PAGER or less for this path.
One input creates one section. Multiple references or paths create a section for each
input in command-line order; - cannot be combined with other inputs. Plain output
prints a single section without decoration and separates multiple sections with
-- i/N: title -- headings. If stdout is a TTY but SASE cannot open the controlling
terminal (/dev/tty) for input, it falls back to the same plain output instead of
starting an unusable app.
bead:<id> inputs and followed bead links read the live bead store for the owning
project; they do not require generated Markdown pages under pages/.
Interactive section bodies have an editor-style line-number gutter. Numbers restart at 1 for each section and appear only on the first visual row of a wrapped logical line; continuation rows keep an empty gutter cell. The digit column is sized once from the largest section, so moving between sections does not shift the document. Plain and redirected output does not include the gutter.
Line-addressed links¶
A link to a file or file-backed artifact may carry a line, a column, or a line range:
| Style | Line | Line and column | Range |
|---|---|---|---|
| Colon suffix | src/app.py:12 |
src/app.py:12:5 |
src/app.py:12-40, src/app.py:12:5-40 |
| GitHub-style | README.md#L12 |
src/app.py#L12C5 |
src/app.py#L12-L40, src/app.py#L12C5-L40C2 |
Artifact references accept the colon suffix too, as in plan:202609/x.md:12. Anything
else, such as README.md#usage or src/app.py:0, is followed as a plain target.
Following a located link opens the target and marks the landed line — or every line of a
range, including wrapped continuation rows — with a thick accent rail in the gutter. The
view scrolls so the first line sits a short distance below the top (a quarter of the
viewport, between two and eight lines) and, when the range fits, keeps its last line on
screen too. A line past the end of the file lands on the last line with a toast naming
the file's length. The subject and trail labels show the location as a :12 or :12–40
suffix, and E<label> opens the editor at that line and column.
To jump within the current section, press : or ;, type a line number, and press
Enter; the destination is railed and positioned the same way.
Syntax highlighting¶
Recognized source files, Markdown documents, and diffs pick up a muted language overlay
after the first paint. The underlying characters never change: comments, strings, and
structure are styled in place, links stay the interactive objects, and / search keeps
those colors under the match highlight.
Memory notes and other Markdown documents use a calm reading hierarchy derived from the
painted host surface (not raw theme attributes): neutral prose and frontmatter values,
bold restrained-cool headings at every level (#–###### share one accent), clearly
readable inline code with visible backticks and no boxes, quiet metadata keys and
structural punctuation (list markers, fence delimiters, frontmatter ---/:), and
prose-colored strong/emphasis. Source YAML and fenced languages keep their ordinary code
palette; unknown fences stay readable literal text. Colors resolve after mount from the
host theme plus computed backgrounds (standalone and ACE-embedded alike), target at
least 7:1 for prose, headings, inline code, and frontmatter text on the built-in dark,
light, and Flexoki surfaces (4.5:1 minimum fallback on midtone customs), and degrade
terminal/ANSI themes to readable neutrals without emitting invalid Rich colors.
Detection is conservative. Each section is classified from its own provenance, not from
--title or from paths mentioned in the text:
-s/--syntax ALIASselects a Pygments lexer for eligible initial sections.nonedisables the added layer for the rest of the session.autorequests normal detection.textandplainkeep the source unhighlighted without a language chip. Invalid aliases fail with exit code 2 before stdin is consumed.- Trusted adapter provenance identifies an actual Markdown document or diff body. A bead or card that merely mentions Markdown is still a formatted card.
- Filename policy covers common source families, including
README,Makefile,Dockerfile,uv.lock, and.tcss.Justfileand.txtstay deliberately plain. - Extensionless files may use a bounded first-line shebang (
/usr/bin/envandenv -Sincluded). - Untyped stdin may be recognized as a unified or git diff in a bounded prefix.
Followed targets detect themselves. Revisiting history restores the original section
override. --syntax none, pager.syntax: never, and --color never stay in effect
across follow/back/forward. --plain, redirected stdout, and page_or_print's direct
branch do not lex. Existing producer ANSI is preserved: --syntax none is not the same
as --color never.
sase pager src/sase/cli_pager.py
sase pager notes.md
git diff | sase pager -t "git diff"
cat config.yml | sase pager -s yaml
sase pager --syntax none README
Permanent configuration is pager.syntax: auto | never (default auto). CLI --syntax
overrides it. Files that exceed the syntax caps stay fully visible and searchable, just
without highlighting. Unknown types can still be forced with an explicit lexer.
Markdown sections also color resolved +<project> tags in
top-level prose: a dim + and a bold name in the project's accent color, or neutral for
accent-less projects. Unknown tags stay plain text, and fenced code and frontmatter stay
tag-free. The overlay reads the already-loaded project catalog and never builds it, so
in practice it appears only when the pager runs inside sase's TUI; a standalone
sase pager notes.md (including one launched as a subprocess) does not load the
catalog, so tags there stay uncolored.
Performance¶
The body is a virtualized row model. Layout is computed once per document width as cheap integer arrays, and only the visible rows are styled and rendered on demand through a bounded strip cache (about four viewports, minimum 512 rows). Scrolling, label-prefix keys, syntax publish, and search typing therefore cost O(viewport), independent of document size. The work that still scales with the document is the one-time layout pass, the link scan, and — for source files — the syntax overlay, which publishes after first paint. All caches are per view, dropped on document swap and unmount, and closing a pager releases its document, so nothing accumulates across opens. The syntax caps are unchanged: files that exceed them stay fully visible and searchable, just without highlighting.
Keys¶
| Key | Action |
|---|---|
j / k, Down / Up |
Scroll one line |
Ctrl+D / Ctrl+U |
Scroll half a page |
g / G |
Go to the top / bottom |
: / ; |
Go to a line number in the current section |
Ctrl+N / Ctrl+P |
Go to the next / previous section |
/, n, N |
Search; repeat forward / backward |
Backspace / Ctrl+O |
Follow the pager trail backward; an empty back trail closes the pane, or the pager when single |
Ctrl+I |
Follow the pager trail forward |
r |
Reload the current content, or re-snapshot it for a live source |
y<label> |
Copy a painted link's bare value: a resolved file path, a URL, or a reference's argument such as a bead ID or SHA (never a kind: label) |
yy |
Copy the current section's path or bare reference (a bead ID rather than bead:<id>), or sha:path / a unified diff in memory history |
E<label> |
Open a painted file-backed target in $EDITOR |
EE |
Open the current section in $EDITOR when it is file-backed |
q / Esc |
Close the focused pane, or the pager when single |
\ |
Split below through the focused pane; again erases the stacked divider; with three panes turns the layout |
\| |
Split beside through the focused pane; again erases the side-by-side divider; with three panes turns the layout |
Ctrl+F |
Focus the next pane in reading order |
Ctrl+B |
Focus the previous pane in reading order (with two panes, same as Ctrl+F) |
Ctrl+Shift+F / Ctrl+Shift+B (or > / <) |
Swap the focused pane with the next / previous pane; focus follows the content |
Ctrl+Shift+D (or Ctrl+X) |
Close the focused pane |
Ctrl+T |
Turn the split (stacked ↔ side by side), keeping focus, ratio, and every pane |
+ / - |
Grow / shrink the focused pane |
Ctrl+W <label> |
Follow a painted link in the most recently used pane (opens a split when single); Ctrl+W Ctrl+W focuses that pane |
? |
Show help |
(, ), {, }, =, @, [, ] |
Memory history time axis: step between versions, jump to first/now, switch read/diff views, open the timeline picker, move by change — the footer names each key's destination (( v21 · ) now · } now) — see Memory History |
With link scanning enabled, SASE paints references and file links with case-sensitive
labels drawn from 0-9, a-z, and A-Z. Pager commands reserve q, j, k,
g, G, y, E, r, n, and N, leaving 52 label characters. Up to 52 targets
therefore use one-character labels. Larger documents use a prefix-free mix of one- and
two-character labels; documents beyond the two-character capacity label only a window of
nearby targets. Typing a label follows the target in place; URL targets copy the URL
instead of replacing the document. Each follow records a bounded backward/forward trail
and restores the prior section, scroll position, and search state when revisited.
Following a new target after going back discards the forward branch.
File titles in the subject line and trail keep paths short. A file inside a managed SASE
workspace checkout (other than the primary checkout) is labeled
~ws/<workspace>/<path>, with the ~ws/ root muted; other files under your home
directory are labeled ~/.... The labels are display-only: copy, edit, and trail
identity keep the exact path.
When either trail direction exists, a breadcrumb band appears below the subject line.
The first row shows the retained visit position, total retained visits, available
Back/Forward counts, and a ? trail hint. The second row shows retained visits in
chronological order with › separators; ● marks the current visit and …N marks an
omitted run of exactly N retained visits that did not fit. Returning to the earliest
retained visit still shows forward context. On pager screens of 12 rows or fewer, the
band compacts to one row with the same position, current marker, direction counts, and
help hint. Positions count only retained visits, so older entries evicted by the bounded
trail are not recoverable through the band or help sheet.
Press ? outside search typing or the goto prompt to open the scrollable Trail & keys
sheet. When history exists, the complete retained trail appears before the key guide;
each visit is numbered and marked as back, current, or forward, with identity
details when they disambiguate identical titles. The sheet has its own scrolling keys:
j/k, arrows, Ctrl+D/Ctrl+U, and g/G; q, Esc, or ? dismiss it without
changing document scroll, search state, pending link prefixes, or either history stack.
y and E are prefix keys: follow them with a painted label to copy or edit that
target, or press the prefix twice for the current section. Link scanning can be disabled
with --links never; ordinary reading, search, section, and trail keys still work.
Split panes¶
Press \ to open a second pane below the focused one, or | to open one beside it. The
new pane is a clone of the focused pane — the same document, reading position, and trail
— and takes focus at a 50/50 ratio. Every other key acts on the focused pane only, and
only the focused pane paints link label badges (body and time-band letters alike), so a
label keystroke is never ambiguous. A clone of a memory note also keeps its version and
view, but each pane then steps its own history, so you can read a
past version in one pane against now in the other.
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 panes, the key
draws the divider through the focused pane — the unfocused pane becomes the full-span
main pane without moving or resizing. With three panes, the key turns the layout
instead. From a single pane or a two-pane split the keys never create more than the
documented panes; Ctrl+W never creates a third pane.
The pager shows seven geometries: single, two two-pane splits, and four three-pane T shapes with a full-span main pane:
single stacked side by side
┌──────┐ ┌──────┐ ┌───┬───┐
│ A │ │ A │ │ A │ B │
│ │ ├──────┤ │ │ │
└──────┘ │ B │ └───┴───┘
└──────┘
main-top main-bottom main-left main-right
┌──────┐ ┌───┬───┐ ┌───┬───┐ ┌───┬───┐
│ A │ │ B │ C │ │ │ B │ │ B │ │
├───┬──┤ ├───┴───┤ │ A ├───┤ ├───┤ A │
│ B │C │ │ A │ │ │ C │ │ C │ │
└───┴──┘ └───────┘ └───┴───┘ └───┴───┘
Each pane is framed in its section's accent color at full strength when focused and
dimmed when not. The subject line moves into the frame: the title half becomes the
border title, and the position half becomes the border subtitle. One shared footer sits
at the bottom; in split mode it shows ^F pane (two panes) or ^F/^B pane (three
panes) and q close pane.
The same split key keeps only the focused pane, erasing to the pair when focus is in the
pair and to the main pane alone when focus is on it. Ctrl+T turns any split, q /
Esc (or an exhausted Backspace) closes the focused pane, and closing any of the
three panes keeps a live survivor — the most recently focused one — with its workers,
reading anchor, and scroll position. Ctrl+F / Ctrl+B move to the next / previous
pane in reading order, + / - grows or shrinks the focused pane in steps, and
clicking a pane focuses it. Ctrl+W followed by a label opens that link in the most
recently used pane — opening a split when single — while focus stays put; doubled
Ctrl+W focuses that pane instead. While armed, the target pane's frame lifts to
preview strength and the footer names it with its position glyph. If a structural change
removes the target before the link lands, the follow cancels with a short message
instead of redirecting. Losing focus cancels a pane's transient input (label prefix, y
/ E / Ctrl+W arms, goto prompt, search typing) while committed highlights stay.
A split opens only when every pane keeps at least 7 rows by 32 columns; otherwise the pager refuses with a toast naming the orientation that would fit. Three-pane resize steps clamp silently, and shrinking the terminal never closes a pane. Splitting, turning, resizing, closing a pane, and walking the trail all keep the logical line at the top of the viewport.
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.
Resolution¶
Follow, copy, and edit use the same semantic target: a typed artifact reference without
a leading @, a decoded path (quotes and prompt sigils stripped), a URL, or a
caller-attached object. Painting uses the original character span; @ and quoting are
syntax, not path bytes.
Macro skill sources are followable without expanding or invoking the skill.
#skill/sase_plan, #project/skill/name, and the __ namespace shorthand such as
#skill__sase_plan open the canonical Markdown source. Markdown links use their
destination, so [plan](#skill/sase_plan) follows the skill while
[#skill/sase_plan](https://example.test) stays a URL. A single-segment slash token
such as /sase_plan falls back to the matching skill source only after no real absolute
path owns that spelling; existing /tmp, /etc, /name, multi-segment paths such as
/usr/bin/python, explicit @/name paths, ~/name, ./name, and file: targets keep
path or artifact semantics. If the same slash spelling names multiple eligible skill
sources, the toast lists qualified #.../skill/... alternatives. Copy keeps the
authored skill destination, while E<label> opens the canonical source file.
Resolution uses the document's owning project and already-available repositories, not
the viewer's current directory. Home paths are the exception: ~ and ~/... always
resolve against your home directory, even in a project-owned document, while an
explicitly relative ./~/... still names a repository path. A same-named file in an
unrelated checkout is not a hit. Distinct repositories that each contain the path
produce an ambiguity page with followable candidate links rather than a first-hit guess.
A source path that exists only in a linked repository still resolves even when the
primary Git index has no matching entry.
URL labels copy the exact destination, including query strings and fragments. File and
artifact labels that cannot be resolved stay visible. The toast names the outcome:
missing checkout, unavailable revision, filtered/denied, proven missing, or not found.
Temporary failures and missing checkouts are retryable — r clears the dangling cache
so a later press can succeed after the target appears. Failed navigation leaves the
current document and trail unchanged.
Reload and retry never clone, reset, or clean a workspace. Each press searches once on a background worker; diagnostics travel with that result, so the UI does not run a second Git search to build a toast.
Live refresh¶
In the Agents tab, V opens the selected agent's metadata and conversation together.
After the metadata, AGENT MACRO shows the original input, AGENT PROMPT shows the
expanded prompt, and AGENT REPLY shows the available conversation. Each is a
separate section with line numbers, Markdown syntax colors, searchable text, and
followable links. Use Ctrl+N / Ctrl+P to move between sections. Session and clan
conversations identify each member in the section title and preserve member order.
Content uses the same loaders as the detail panel, including live replies, saved
responses, and chat fallback. A missing prompt does not hide an available reply. Missing
content gets a quiet placeholder; unreadable content gets a retry message. Press r to
pick up new prompts or replies while keeping your current section.
A host embedding the pager (such as sase tui's Agents-tab metadata document, opened
with V) may wire a refresh provider instead of leaving r to recompose the frozen
document. The provider re-snapshots the live source — a still-running agent's status,
timestamps, paths, and conversation — on a background thread and returns a fresh
document. r swaps it in, keeping the current section by identity when possible and
clamping scroll to the new content's bounds. A provider that returns nothing (the source
is gone) or raises leaves the current document and trail untouched, with a brief footer
status standing in for the usual recompose. Without a wired provider, r keeps its
plain reload behavior.
Document origins¶
A document's origin seeds which bare-token link rules apply, since a bare token's
meaning depends on where it came from: bead and agent documents recognize bare bead
ids for the document's own bead prefix, its related beads' prefixes, and every enabled
project's name or alias, for example sase-uk.7 or bob-cli-5s.1, as bead links, and
diff documents recognize a bare short SHA as a commit link. file and research
documents apply no bare-token rules. Documents without declared prefixes fall back to
sase. The Agents-tab metadata pager uses the agent origin so a bead id mentioned in
its BEAD section links exactly as it would in a bead document.