Skip to content

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.

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:

  1. -s/--syntax ALIAS selects a Pygments lexer for eligible initial sections. none disables the added layer for the rest of the session. auto requests normal detection. text and plain keep the source unhighlighted without a language chip. Invalid aliases fail with exit code 2 before stdin is consumed.
  2. Trusted adapter provenance identifies an actual Markdown document or diff body. A bead or card that merely mentions Markdown is still a formatted card.
  3. Filename policy covers common source families, including README, Makefile, Dockerfile, uv.lock, and .tcss. Justfile and .txt stay deliberately plain.
  4. Extensionless files may use a bounded first-line shebang (/usr/bin/env and env -S included).
  5. 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.