Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a424ff2
feat(top): scaffold + activity view (S1)
May 8, 2026
061791c
fix(top): use is_ok_and for COLORTERM check (clippy 1.95)
May 8, 2026
f0b1327
fix(top): address REV findings — sanitizer, clock, dispatch test
May 8, 2026
9cc6300
docs(demos): add /top live TUI monitor demo
May 8, 2026
312bda2
docs(demos): pause after typed commands; explain visible features
May 8, 2026
15b186a
feat(top): pg_ash colors, locks col, refresh prompt, extended mode, s…
May 8, 2026
a22f856
fix(top): collapse nested if into match guard (clippy 1.95)
May 8, 2026
892f6d6
fix(top): match arm on a single line (CI rustfmt)
May 8, 2026
070aa9f
feat(top): sort indicator (</>r), key overlay, --batch logging mode
May 8, 2026
d930532
feat(top): Space=immediate-refresh, brighter chrome, /top in /? help
May 8, 2026
669d86a
feat(top): k/K cancel/terminate, ←→ sort, autovacuum + slots header line
May 8, 2026
0bed870
fix(top): dedup Activity title, key overlay opt-in, lower-left billboard
May 8, 2026
1f4c089
feat(top): global --show-keys, lower-right billboard, fullwidth label…
May 8, 2026
073110a
fix(top): cleaner key overlay — bigger box, bright yellow, no fullwidth
May 8, 2026
8849cf2
fix(top): drop trailing comma flagged by clippy 1.95
May 8, 2026
2102e7d
feat(top): 5x block-letter font for the key-press overlay
May 8, 2026
fb277dc
fix(top): tighten the key-overlay box around its 5×5 glyph
May 8, 2026
50baec6
feat(top): switch key overlay to 3×5 figlet font
May 9, 2026
63211d7
fix(top): address REV round-11 blocking findings
May 9, 2026
3182ebc
chore(demos): smoother /top gif via ffmpeg palette pipeline
May 9, 2026
5db967b
fix(demos): render-top.sh palette path works on macOS BSD mktemp
May 9, 2026
0d57770
refactor(top): dedup format_secs/truncate, improve test coverage
May 9, 2026
c6c5db9
fix(top): drop TerminalGuard clear-screen after LeaveAlternateScreen
May 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ Thumbs.db
# Debug
*.dSYM/
.claude/worktrees/

# vhs intermediate output for `bash demos/render-top.sh`
demos/top-demo.mp4
712 changes: 712 additions & 0 deletions .samo/spec/top/SPEC.md

Large diffs are not rendered by default.

74 changes: 74 additions & 0 deletions .samo/spec/top/architecture.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"$schema": "https://raw.githubusercontent.com/NikolayS/samospec/main/schemas/architecture.schema.json",
"version": "0.2",
"slug": "top",
"title": "rpg /top — live TUI Postgres monitor",
"groups": [
{ "id": "ui", "label": "ratatui UI" },
{ "id": "core", "label": "/top core" },
{ "id": "data", "label": "data plane" },
{ "id": "ai", "label": "AI hand-offs" },
{ "id": "rpg", "label": "rpg host" },
{ "id": "pg", "label": "PostgreSQL 14–18" }
],
"nodes": [
{ "id": "repl", "group": "rpg", "label": "REPL / metacmd dispatcher", "kind": "module" },
{ "id": "clap", "group": "rpg", "label": "clap subcommand: rpg top", "kind": "module" },
{ "id": "log", "group": "rpg", "label": "session log (audit)", "kind": "module" },
{ "id": "highlight", "group": "rpg", "label": "syntax highlighter", "kind": "module" },
{ "id": "ai_cmds", "group": "ai", "label": "src/repl/ai_commands.rs", "kind": "module" },
{ "id": "ai_explain", "group": "ai", "label": "Shift-X — eXplain selection", "kind": "feature" },
{ "id": "ai_info", "group": "ai", "label": "Shift-I — Info on whole view","kind": "feature" },

{ "id": "modrs", "group": "core", "label": "src/top/mod.rs (lifecycle)", "kind": "module" },
{ "id": "state", "group": "core", "label": "state.rs (App, ringbuf, filter, sort)", "kind": "module" },
{ "id": "keys", "group": "core", "label": "keys.rs (keymap)", "kind": "module" },
{ "id": "theme", "group": "core", "label": "theme.rs (palette, thresholds)", "kind": "module" },
{ "id": "admin", "group": "core", "label": "admin.rs (cancel/terminate, bulk)", "kind": "module" },

{ "id": "renderer", "group": "ui", "label": "renderer.rs (draw frame)", "kind": "module" },
{ "id": "views", "group": "ui", "label": "views/* (per-view render+SQL)", "kind": "module" },
{ "id": "overlay", "group": "ui", "label": "overlay.rs (drill-down, modals, help)", "kind": "module" },

{ "id": "sampler", "group": "data", "label": "sampler.rs (tokio task)", "kind": "module" },
{ "id": "sql", "group": "data", "label": "sql.rs (PG-version-gated SQL)", "kind": "module" },
{ "id": "pgconn", "group": "data", "label": "tokio-postgres connection (shared)", "kind": "module" },

{ "id": "pgstats", "group": "pg", "label": "pg_stat_* views", "kind": "external" },
{ "id": "pglocks", "group": "pg", "label": "pg_locks / pg_blocking_pids", "kind": "external" },
{ "id": "pgprogress", "group": "pg", "label": "pg_stat_progress_*", "kind": "external" },
{ "id": "pgrepl", "group": "pg", "label": "pg_stat_replication / slots", "kind": "external" }
],
"edges": [
{ "from": "repl", "to": "modrs", "label": "/top dispatch" },
{ "from": "clap", "to": "modrs", "label": "rpg top alias" },
{ "from": "overlay", "to": "ai_explain","label": "Shift-X" },
{ "from": "renderer", "to": "ai_info", "label": "Shift-I (bottom panel)" },
{ "from": "ai_explain","to": "ai_cmds", "label": "/explain on selected pid" },
{ "from": "ai_info", "to": "ai_cmds", "label": "Info prompt (header + rows + waits)" },
{ "from": "modrs", "to": "state", "label": "owns App" },
{ "from": "modrs", "to": "sampler", "label": "spawn task" },
{ "from": "modrs", "to": "renderer", "label": "draw loop" },
{ "from": "modrs", "to": "keys", "label": "key → action" },
{ "from": "renderer", "to": "views", "label": "delegate per view" },
{ "from": "renderer", "to": "overlay", "label": "modal stack" },
{ "from": "renderer", "to": "theme", "label": "palette + truecolor" },
{ "from": "overlay", "to": "highlight","label": "query syntax" },
{ "from": "sampler", "to": "sql", "label": "fetch query strings" },
{ "from": "sampler", "to": "pgconn", "label": "execute" },
{ "from": "sampler", "to": "state", "label": "Snapshot" },
{ "from": "admin", "to": "pgconn", "label": "pg_cancel / pg_terminate" },
{ "from": "admin", "to": "log", "label": "audit line" },
{ "from": "pgconn", "to": "pgstats", "label": "SELECT" },
{ "from": "pgconn", "to": "pglocks", "label": "SELECT" },
{ "from": "pgconn", "to": "pgprogress","label": "SELECT" },
{ "from": "pgconn", "to": "pgrepl", "label": "SELECT" }
],
"notes": [
"All /top mutating actions (cancel/terminate, EXPLAIN ANALYZE) are gated by an explicit modal confirmation and an audit line.",
"Sampler queries always run with SET LOCAL statement_timeout = '5s' to avoid wedging on hostile locks.",
"v1 ships with no system-stats collection (no procfs, no server-side helpers); the header omits cpu/mem/io/net cells. Reintroduction is a separate spec.",
"Compiled out on wasm32; /top is a native-only command (matches /ash, /rpg).",
"Module layout intentionally mirrors src/ash/{mod,renderer,sampler,state}.rs for reviewer pattern-matching."
]
}
88 changes: 88 additions & 0 deletions .samo/spec/top/decisions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# /top — decisions log

samospec convention: every accepted/rejected/deferred design choice is captured
here so future reviewers can see *why*, not only *what*.

## v0.2 — open-question review (2026-05-07)

### Accepted (this round)

- **System stats out of v1.** No procfs reader and no server-side helpers
in v1 of `/top`. Header omits cpu/mem/io/net cells with a one-line
caption. Reintroduction lives in a separate spec
(`.samo/spec/top-system-stats/`, future). `Snapshot` keeps the optional
fields so adding it later is additive.
- **`rpg top` clap subcommand alias.** Bare `rpg top [args...]` aliases
to `rpg --command "/top args..."`. Mirrors `psql -c "\l"` muscle memory
and removes a quoting layer.
- **Two AI hand-off keys, both in S4.** Mnemonic split: `X` = eXplain
(matches the existing `/explain` slash-command), `I` = Info.
- `Shift-X` — eXplain the selected pid (sends row + cached EXPLAIN
through `/explain`, streams into the drill-down overlay).
- `Shift-I` — Info overview of the whole view (packages header,
sparklines, top-N rows, waits, blocking tree; streams to a bottom-up
panel; has `--ai-info` for headless mode).
Both reuse `src/repl/ai_commands.rs` plumbing; no new AI surface.
- **Mouse on by default.** `crossterm::EnableMouseCapture` is set on
alt-screen entry. Opt-out via `[top] mouse = false` or `--no-mouse`
(useful for native terminal copy/paste).

### Implications for sprint plan

- S4 grows to absorb both AI keys (still bounded by stub-provider tests,
no real API calls in CI).
- S5 no longer carries any procfs / system-stats work — pure DB-side
features (replication, progress, sparklines).
- No new dependencies needed. Ratatui 0.30 and crossterm already cover
mouse capture; AI plumbing already in tree.

---

## v0.1 — initial draft (2026-05-07)

### Accepted

- **Single new command, `/top`, in the `/` namespace.** Per `docs/COMMANDS.md`,
any rpg-specific extension uses `/`. Adding `/top` (vs hijacking the existing
psql-style `\watch`) keeps the namespace policy intact.
- **Mirror `/ash` module layout.** New code under `src/top/{mod,renderer,
sampler,state}.rs` so reviewers can pattern-match. Reuse `terminal_has_
truecolor()` and the small-terminal stub from `src/ash/renderer.rs`.
- **Ratatui 0.30** (already in `Cargo.toml`). No new heavyweight dep.
- **Native-only.** `#[cfg(not(target_arch = "wasm32"))]` gate, mirroring
`/ash` and `/rpg` in `src/repl/ai_commands.rs`.
- **Read-only by default.** Cancel/terminate require an explicit modal
confirm; bulk &gt;200 needs `--kill-allow-bulk`.
- **Eight-sprint plan.** S1 ships an MVP demo (activity view + header)
behind CI-green + REV review; later sprints layer features.

### Deferred

- **Server-side OS-stats helper functions** (`rpg.system_stats()`). Out of
scope for v1; v0.2 confirms system stats overall are deferred to a
separate future spec. v1 of `/top` shows DB-side stats only.
- **`/profile` (wait-event sampler) and `/record`/`/report` (offline
recordings).** These are separate features identified during the same
pg_top + pgcenter study; their specs will live alongside this one under
`.samo/spec/{profile,record,report}/`.
- **`pg_proctab`-based remote OS stats** (pg_top's mechanism). Not adopted;
we prefer rpg-native helper functions when extension installation is
acceptable, and procfs when local.
- **AI hand-off (`Shift-I` send selected query to `/explain`).** Listed in
Open Questions; deferred to S4 or later depending on review.

### Rejected

- **Extending `/ash` instead of a new command.** `/ash` is a *history* view;
`/top` is a *now* view. Conflating them confuses semantics and key bindings.
Cross-link with `Shift-A` instead.
- **Pure-SQL recursive blocking tree.** Doable via CTE on `pg_blocking_pids`
but harder to enrich with query summaries and wait events. Rust-side
walk picked for clarity; SQL fallback can be added later if needed.
- **Hard requirement on `pg_stat_statements`.** Optional — the Statements
view shows a clear "extension not installed" stub instead of breaking.

### Open

See §9 of `SPEC.md` ("Open questions for the review round"). These are
expected to converge by v0.2 of this spec.
26 changes: 24 additions & 2 deletions demos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ the [VHS](https://github.com/charmbracelet/vhs) tape files used to render them.
| `gif3_t2s.gif` | `\t2s` text-to-SQL with confirmation, then `\yolo` auto-execute |
| `gif4_pspg.gif` | Built-in pager → `\set PAGER pspg` → same query routed through pspg |
| `gif5_lua.gif` | Custom Lua commands: `\commands`, `\slow_mean`, `\slow_total`, `\table_info` |
| `top-demo.gif` | `/top` live TUI Postgres monitor — Activity view + cursor navigation + `/top --once` headless mode |

## Prerequisites

Expand Down Expand Up @@ -46,12 +47,33 @@ vhs demos/gif2_typo.tape
vhs demos/gif3_t2s.tape
vhs demos/gif4_pspg.tape
vhs demos/gif5_lua.tape
bash demos/render-top.sh # /top — live TUI monitor (PR #837)
```

Or render all at once:
The `/top` demo uses `demos/top-workload.sh` to spawn a steady stream of
mixed backends (active queries, idle-in-tx, advisory-lock contention).
The tape expects a local Postgres on `localhost:55433` as `postgres` —
adjust the `PG{HOST,PORT,USER,DATABASE}` env block at the top of the
tape and the `CONNINFO` argument of `top-workload.sh` for your setup.

`render-top.sh` runs vhs to produce both `.gif` and `.mp4`, then re-encodes
the gif from the mp4 through ffmpeg's palette pipeline (15 fps, bayer
dither). The native vhs gif quantizer drops frames during the busy
workload section and looks laggy; the ffmpeg pass costs ~3× file size
(11 MiB vs 3 MiB) but plays smoothly. The intermediate `top-demo.mp4` is
gitignored. The tape passes `--show-keys` so each keystroke flashes on
screen — drop the flag if you want a clean recording.

Or render all at once (the `top-demo` tape is skipped here because it
needs the `render-top.sh` ffmpeg post-process — running plain `vhs` on
it produces the laggy gif the script was written to fix):

```bash
for tape in demos/*.tape; do vhs "$tape"; done
for tape in demos/*.tape; do
[[ "$tape" == *top-demo.tape ]] && continue
vhs "$tape"
done
bash demos/render-top.sh
```

## Note on gif1 re-renders
Expand Down
57 changes: 57 additions & 0 deletions demos/render-top.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'

# Render the /top demo gif. vhs's native gif quantizer produces visible
# stuttering on the long workload section; using vhs's mp4 as the source
# and re-encoding through ffmpeg's palette pipeline gives a much smoother
# result at the cost of file size (~11 MiB vs ~3 MiB).
#
# Usage:
# bash demos/render-top.sh
#
# Output: demos/top-demo.gif (final), demos/top-demo.mp4 (intermediate,
# not committed to the repo).

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# Explicit ${TMPDIR}-rooted path: macOS BSD `mktemp -t prefix` does not
# treat XXXXXX as a template — it appends a random suffix to the whole
# string, so a request for `…palette.XXXXXX.png` ends in `.RANDOM` and
# ffmpeg's image2 muxer can't infer the codec. PID-stamped path keeps
# the .png extension intact and is safe under the trap-based cleanup.
PALETTE="${TMPDIR:-/tmp}/top-demo-palette-$$.png"
readonly REPO_ROOT
readonly TAPE="${REPO_ROOT}/demos/top-demo.tape"
readonly GIF="${REPO_ROOT}/demos/top-demo.gif"
readonly MP4="${REPO_ROOT}/demos/top-demo.mp4"
readonly PALETTE

cleanup() {
rm -f "${PALETTE}"
rm -f "${MP4}"
}
trap cleanup EXIT

main() {
pkill -f top-workload 2>/dev/null || true
sleep 1

# vhs reads the tape; the tape declares both gif + mp4 outputs.
vhs "${TAPE}"

# Re-encode the mp4 through ffmpeg's palette pipeline (15 fps, 1200 px
# wide, bayer dither) and overwrite the vhs gif with the smoother one.
ffmpeg -y -i "${MP4}" \
-vf "fps=15,scale=1200:-1:flags=lanczos,palettegen=stats_mode=diff" \
"${PALETTE}"

ffmpeg -y -i "${MP4}" -i "${PALETTE}" \
-lavfi "fps=15,scale=1200:-1:flags=lanczos[x];\
[x][1:v]paletteuse=dither=bayer:bayer_scale=4:diff_mode=rectangle" \
"${GIF}"

printf "Wrote %s (%s bytes)\n" "${GIF}" \
"$(stat -f '%z' "${GIF}" 2>/dev/null || stat -c '%s' "${GIF}")"
}

main "$@"
Binary file added demos/top-demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading