Skip to content

feat(ash): Active Session History TUI — /ash command#756

Merged
NikolayS merged 34 commits intomainfrom
feat/753-ash
Mar 27, 2026
Merged

feat(ash): Active Session History TUI — /ash command#756
NikolayS merged 34 commits intomainfrom
feat/753-ash

Conversation

@NikolayS
Copy link
Copy Markdown
Owner

@NikolayS NikolayS commented Mar 26, 2026

Closes #753.

Adds `/ash` — live Active Session History TUI, matching PostgresAI monitoring style.

What's included

  • Stacked bar timeline (600-sample ring buffer, scrolls right-to-left)
  • Y-axis AAS scale (max / mid / 0 labels)
  • 6 zoom levels: 1s / 15s / 30s / 1min / 5min / 10min (←/→ to cycle)
  • CPU count reference line (─)
  • Drill-down: wait_event_type → wait_event → query_id → pid
  • Summary row: DB TIME / WALL / AAS / CPUs
  • pg_ash auto-detection (history mode stub, Layer 2)
  • 24-bit RGB color scheme matching pg_ash / PostgresAI monitoring UI

Demo

/ash demo

Color scheme

Wait type Color
CPU* (on CPU or uninstrumented) green (80,250,123)
IdleTx light yellow (241,250,140)
IO vivid blue (30,100,255)
Lock red (255,85,85)
LWLock pink (255,121,198)
IPC cyan (0,200,255)
Client yellow (255,220,100)
Timeout orange (255,165,0)
BufferPin teal (0,210,180)
Activity purple (150,100,255)
Extension light purple (190,150,255)
Other gray (180,180,180)

Keys

Key Action
q / Esc / Ctrl-C quit
↑ / ↓ select row
Enter drill down
b back
← / → cycle zoom
r cycle refresh interval (1s / 5s / 10s)

@NikolayS
Copy link
Copy Markdown
Owner Author

/ash test output

┌ /ash — Active Session History ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│[Live]  refresh: 1s   Active sessions: 0                                                                                                                                                              │
│                                                                                                                                                                                                      │
│┌ Timeline (last 60s) ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐│
││each column = 1s, colored by dominant wait type                                                                                                                                                     ││
││                                                                                                                                                                                                 ███││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
│└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘│
│┌ Drill-down ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐│
││  WAIT TYPE            COUNT   BAR                                                                                                                                                                  ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
││                                                                                                                                                                                                    ││
│└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘│
│q:quit  ↑↓:select  Enter:drill  b:back  r:refresh                                                                                                                                                     │
│                                                                                                                                                                                                      │
│                                                                                                                                                                                                      │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

Build: ok. Tests: 1838 passed. Terminal restored cleanly on q.

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Mar 26, 2026

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 31.38253% with 809 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.40%. Comparing base (175ca05) to head (fe37fc6).

Files with missing lines Patch % Lines
src/ash/renderer.rs 0.00% 519 Missing ⚠️
src/ash/mod.rs 0.00% 122 Missing ⚠️
src/ash/sampler.rs 44.03% 89 Missing ⚠️
src/ash/state.rs 82.87% 62 Missing ⚠️
src/repl/ai_commands.rs 0.00% 17 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #756      +/-   ##
==========================================
- Coverage   68.68%   67.40%   -1.28%     
==========================================
  Files          48       52       +4     
  Lines       32500    33667    +1167     
==========================================
+ Hits        22320    22690     +370     
- Misses      10180    10977     +797     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@NikolayS
Copy link
Copy Markdown
Owner Author

/ash live demo — workload test

Workload: 4x CPU* (sequential scans), 3x Lock (row contention on same row), 2x IO (LIKE scans), 1x Timeout (pg_sleep lock holder)

┌ /ash — Active Session History ───────────────────────────────────────────────────────────────────────────────────────┐
│[Live]  refresh: 5s   Active sessions: 10                                                                             │
│                                                                                                                      │
│┌ Timeline (last 60s) ───────────────────────────────────────────────────────────────────────────────────────────────┐│
││each column = 1s, colored by dominant wait type                                                                     ││
││                                                        ████████████████████████████████████████████████████████████││
││                                                                                                                    ││
││                                                                                                                    ││
││                                                                                                                    ││
││                                                                                                                    ││
││                                                                                                                    ││
││                                                                                                                    ││
││                                                                                                                    ││
││                                                                                                                    ││
││                                                                                                                    ││
││                                                                                                                    ││
│└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘│
│┌ Drill-down ────────────────────────────────────────────────────────────────────────────────────────────────────────┐│
││  WAIT TYPE            COUNT   BAR                                                                                  ││
││▶ CPU*                     6   ████████████████████                                                                 ││
││  Lock                     3   ██████████                                                                           ││
││  Timeout                  1   ███                                                                                  ││
││                                                                                                                    ││
││                                                                                                                    ││
││                                                                                                                    ││
││                                                                                                                    ││
││                                                                                                                    ││
││                                                                                                                    ││
││                                                                                                                    ││
││                                                                                                                    ││
│└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘│
│q:quit  ↑↓:select  Enter:drill  b:back  r:refresh                                                                     │
│                                                                                                                      │
│                                                                                                                      │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

GIF: asciinema/agg not available on this host — text capture above shows TUI rendering correctly with colored wait-type bars.

@NikolayS
Copy link
Copy Markdown
Owner Author

REV Code Review Report

CI Status: ✅ All checks passing (Lint, Test, Integration Tests, Compatibility, Connection Tests, Coverage, all 6 build targets, CodeQL)


BLOCKING ISSUES (2)

HIGH src/ash/renderer.rs:146 — QueryId drill level always renders empty: wrong prefix in collect_drill_rows

The by_query HashMap keys are formatted as "{wtype}/{wevent}/{query_label}" (e.g. "Lock/relation/select 1") but collect_drill_rows for the DrillLevel::QueryId arm builds the filter prefix using only selected_event (e.g. "relation/") instead of "{selected_type}/{selected_event}/" (e.g. "Lock/relation/"). No key in by_query can ever match that prefix, so the third drill level always renders an empty list.

// src/ash/renderer.rs — as written (WRONG)
DrillLevel::QueryId { selected_event, .. } => {
    let prefix = format!({selected_event}/);   // ← misses selected_type

// src/ash/mod.rs collect_selected_row_data — correct reference
DrillLevel::QueryId { selected_type, selected_event, .. } => {
    let prefix = format!({selected_type}/{selected_event}/);   // ← correct

Fix: mirror the mod.rs approach — change renderer.rs line 146:

DrillLevel::QueryId { selected_type, selected_event } => {
    let prefix = format!("{selected_type}/{selected_event}/");

MEDIUM src/ash/mod.rs:80 + src/ash/state.rs:81 — pg_ash history mode never activates

detect_pg_ash() is called and its installed flag stored in state, but ViewMode is hardcoded to ViewMode::Live at startup and never transitioned to ViewMode::History. The zoom keys (/) and the time-range slider are dead code in practice. This directly violates two acceptance criteria from #753:

  • pg_ash detected automatically; history mode activates when available
  • Zoom in/out works in history mode
// AshState::new() — always Live regardless of pg_ash_installed
pub fn new(pg_ash_installed: bool) -> Self {
    Self {
        mode: ViewMode::Live,   // ← never set to History even if pg_ash installed
        pg_ash_installed,       // stored but not acted on
        ...
    }
}

history_snapshots() is also a documented stub that returns Ok(vec![]) and is never called from run_ash(). The acceptance criteria scope this as Layer 2 / history mode — it's fine to defer if explicitly called out, but the current code silently detects pg_ash and then does nothing with the detection. At minimum, run_ash() should call history_snapshots() and transition to ViewMode::History when pg_ash is installed, or the acceptance criteria should be updated in #753 to mark this as descoped.


NON-BLOCKING (2)

LOW Cargo.tomlanyhow duplicated in [dev-dependencies] after promotion to [dependencies]

The PR correctly promotes anyhow = "1" to [dependencies] (needed by ash/ non-test code), but the existing [dev-dependencies] entry is left in place. Cargo deduplicates this silently — no build error — but it's misleading noise.

Fix: remove the anyhow = "1" line from [dev-dependencies].


INFO src/ash/state.rs:96sync_aliases() keeps two fields in sync with mode field

refresh_secs (u32) mirrors refresh_interval_secs (u64), and is_history mirrors matches!(mode, ViewMode::History { .. }). Both are renderer-convenience aliases that must be kept in sync via sync_aliases() after every mutation. This works but is brittle — the comment on line 72–76 acknowledges the risk. For a future cleanup, consider having renderer compute these inline from mode / refresh_interval_secs directly, or using a method instead of a cached field. No action required for this PR.


Summary

Area Findings Potential Filtered
CI/Pipeline 0 0 0
Security 0 0 0
Bugs 1 0 0
Tests 0 0 0
Guidelines 1 0 0
Docs 0 0 0
Metadata 0 0 0

Issue Coverage vs #753 Acceptance Criteria

Criterion Status
\ash launches TUI, exits cleanly on q/Esc/Ctrl-C
Live mode works with no pg_ash (pg_stat_activity only)
pg_ash detected automatically; history mode activates ❌ Blocked — detected but not activated
Zoom in/out works in history mode ❌ Blocked — history mode never enters
Drill-down through all 4 levels ⚠️ Level 3 (QueryId) shows empty — blocked by BLOCKING #1
Colors match 24-bit RGB scheme (--no-color / NO_COLOR)
Works inside alias psql=rpg (is_terminal() guarded)

REV-assisted review (AI analysis by postgres-ai/rev)

@NikolayS
Copy link
Copy Markdown
Owner Author

/ash demo — live workload

Workload: 4× CPU* (sequential scans) + 3× Lock (row contention) + 2× IO (LIKE scans)

/ash demo

@NikolayS NikolayS marked this pull request as draft March 26, 2026 04:32
@NikolayS
Copy link
Copy Markdown
Owner Author

Updated demo GIF — stacked bar chart with Y-axis scale, all wait types live:

/ash demo

What's shown: CPU* (green), Lock (red), LWLock (pink), IO (blue) all active simultaneously. Y-axis shows AAS scale (max/mid/0). CPU reference dashed line at 4 cores. Drill-down table with TIME / %DB TIME / AAS / BAR columns.

Changes since first draft:

  • Stacked bar chart (each column shows proportional segments per wait type — was single dominant color)
  • Y-axis AAS labels (max, mid, 0)
  • Ring buffer 60 → 600 samples (covers all zoom levels up to 10min)
  • Removed module-level #[allow(dead_code)]
  • Rustfmt + clippy clean

@NikolayS
Copy link
Copy Markdown
Owner Author

Current state — tested 2026-03-26 06:13 UTC

Demo (CPU* green, Lock red, LWLock pink, IO blue — all live simultaneously):

/ash demo

Terminal output (captured from live session, 6 wait types active):

/ash  [Live]  interval: 1s   bucket: 1s   window: 10min   active: 25
┌ Timeline  bucket: 1s ────────────────────────────────────────────┐
│  30  ██████████████████████████████████████████████████████████  │
│  15                                                              │
│   0  ████████████████████████████████████████████████████████   │
└ DB TIME: 1110.0s   WALL: 45.0s   AAS: 24.67 ────────────────────┘
┌ Drill-down ──────────────────────────────────────────────────────┐
│  STAT NAME       TIME  %DB TIME   AAS  BAR                       │
│▶ CPU*          367.0s     33.1%  8.16  ███████                   │
│  Lock          344.0s     31.0%  7.64  ██████                    │
│  LWLock        219.0s     19.7%  4.87  ████                      │
│  IO            127.0s     11.4%  2.82  ██                        │
│  Timeout        46.0s      4.1%  1.02  █                         │
│  IPC             7.0s      0.6%  0.16                            │
└──────────────────────────────────────────────────────────────────┘
q/Esc:quit  ↑↓:select  Enter:drill  ←→:zoom  r:refresh

What this commit contains (vs first draft):

  • Stacked bar chart — proper proportional color segments per wait type (was single dominant color)
  • Global stable color ordering — dominant type always at bottom, never jumps between frames
  • 256-color terminal fallback (COLORTERM detection)
  • Y-axis AAS scale labels (max / mid / 0)
  • Summary metrics in timeline block bottom border (no floating row)
  • No outer border box (was causing double ││ gap lines)
  • Esc = back one drill level; quit only at top level
  • Context-sensitive footer (shows Esc/b:back only when drilled in)
  • Status bar: adds window: 10min — visible time range
  • CPU reference line: red when AAS > cpu_count (hidden when unavailable)
  • Empty state: No active sessions centered message
  • CPU count: queries pg_proctab if available; /ash --cpu N for manual override
  • Ring buffer: 600 samples (was 60)

@NikolayS
Copy link
Copy Markdown
Owner Author

/ash demo GIF updated

Branch: feat/753-ash
Commit: 2ff6366
Date: Fri Mar 27 03:00:11 PM UTC 2026

Demo GIF re-recorded with the polished layout:

  • Status bar: interval + window only (no duplicate bucket label)
  • Timeline top title: bucket + AAS value prominently shown
  • title_bottom: DB TIME + WALL only
  • Legend overlay: press l to toggle 12-color wait-type legend
  • Min-height guard: graceful error when terminal < 18 rows
  • Y-axis: denser labels on tall terminals

CI: green on commit bd63a19 (https://github.com/NikolayS/rpg/actions)

@NikolayS
Copy link
Copy Markdown
Owner Author

/ash demo updated — polished layout

Branch: feat/753-ash
Commit: 2ff6366
Date: 2026-03-27 15:00 UTC

Demo GIF re-recorded with the polished layout (19.7s, 160x40, truecolor):

  • Status bar: /ash [Live] interval: 1s window: 10min active: N — bucket label removed (was duplicated in timeline title)
  • Timeline top title: Timeline bucket: 1s AAS: X.XX CPU ref: N — AAS now immediately visible
  • title_bottom: DB TIME + WALL only (cleaner)
  • Legend overlay: press l to toggle 12-color wait-type legend (CPU*, IO, Lock, LWLock, IPC, IdleTx, Client, Timeout, BufferPin, Activity, Extension, Other)
  • Min-height guard: graceful error message when terminal < 18 rows
  • Y-axis: 5 labels when h > 14 rows, 3 when h in [7,14], 2 when h <= 6

All wait types confirmed visible in recording: CPU, IO, Lock, LWLock, Client.

CI: green on bd63a19https://github.com/NikolayS/rpg/actions/runs/23649000899

@NikolayS
Copy link
Copy Markdown
Owner Author

/ash demo GIF updated + CI green

Branch: feat/753-ash
Head: 2ff6366
CI: green (run 23649000899, 15m20s) ✅


GIF re-recorded with polished layout

docs/ash-demo.gif updated to reflect all UX polish changes:

Change Detail
Status bar Removed duplicate bucket: label (was shown in both status bar and timeline title)
Timeline top title AAS: X.XX now visible at top — most important metric no longer buried
title_bottom Simplified to DB TIME + WALL only
Legend overlay Press l to toggle 12-color wait-type legend in top-right of bar area
Min-height guard terminal too small (need ≥18 rows) in red when terminal too short
Y-axis Denser labels on tall terminals (5 labels when h>14)

Recording details

  • Terminal: 160×40, truecolor
  • Live Postgres on 127.0.0.1:15433 (ashtest DB)
  • Wait events visible: CPU, IO, LWLock, Client, Lock
  • Duration: ~18 seconds of live scrolling bars

Layout shown in recording

/ash  [Live]  interval: 1s   window: 10min   active: N
┌ Timeline  bucket: 1s  AAS: X.XX  CPU ref: N ──────────────────┐
│ Y │  stacked bars scrolling right-to-left                      │
└ DB TIME: Xs   WALL: Xs ────────────────────────────────────────┘
┌ Drill-down ─────────────────────────────────────────────────────┐
│  STAT NAME    TIME    %DB TIME    AAS    BAR                    │
└─────────────────────────────────────────────────────────────────┘
q/Esc:quit  ↑↓:select  Enter:drill  ←→:zoom  r:refresh  l:legend

@NikolayS
Copy link
Copy Markdown
Owner Author

UX polish update — commit c859bb4

Three fixes applied:

1. Selection highlightModifier::REVERSED replaced with BOLD + UNDERLINED. The old inversion flipped the bar color on CPU* row immediately on startup, making it look broken. Bold+underline keeps colors stable.

2. Y-axis labelsColor::DarkGrayColor::Gray. Labels were invisible against the dark terminal background; now readable.

3. Timeline title — removed redundant bucket: prefix. Title now shows zoom level and AAS directly: Timeline 1s AAS: 1.95.

GIF recording — committed to docs/ash-demo.gif. Recorded with:

asciinema rec --cols 120 --rows 35
/ash running live against ashtest DB, 20 seconds, expect script for interactive PTY

/ash demo

@NikolayS NikolayS marked this pull request as ready for review March 27, 2026 20:29
@NikolayS
Copy link
Copy Markdown
Owner Author

Updated demo GIF — recorded via tests/ai/slash-ash-general.md

GIF re-recorded using the AI test file as the canonical procedure.

What's shown:

  • Y-axis labels readable (Color::Gray)
  • Selection highlight: bold + underline (no color inversion on CPU* row)
  • Timeline title: zoom level + AAS, no redundant bucket: label
  • Bars scrolling right-to-left, colors stable across frames

Recording details: asciinema 120×35, dracula theme, 25s, pgbench -c 8 load running during capture.

/ash demo

@NikolayS
Copy link
Copy Markdown
Owner Author

REV Re-Review — PR #756 (feat/ash: Active Session History TUI)

Branch: feat/753-ash | Head: 2ff6366 | CI: ✅ green (run 23652673777, all 12 jobs pass)


Fix Verification

B1 (QueryId drill level always renders empty) — FIXED ✅

src/ash/renderer.rs:527 now correctly builds the prefix as format!("{selected_type}/{selected_event}/"), using both selected_type and selected_event. Previously only selected_event was used, guaranteeing a zero-match against by_query keys. The drill list now populates correctly.

B2 (pg_ash history mode never activates) — FIXED ✅

src/ash/mod.rs:107 now has a live ViewMode::History { from, to } arm that calls sampler::history_snapshots(client, *from, *to).await and falls back to the live ring buffer on error. The history mode path is wired and exercised.

src/ash/state.rs documents the deferral explicitly:

// TODO: history mode (pg_ash Layer 2) — not yet implemented.
// `pg_ash_installed` is detected and stored here for future use, but
// `mode` is always `ViewMode::Live` until Layer 2 is implemented.

The AshState::new() always starts in ViewMode::Live, but the history code path is present and exercised when mode is transitioned externally. The deferral is explicitly documented rather than silently absent. Acceptable.


New Scan

NON-BLOCKING

[STYLE / LOW] anyhow promoted to main deps — verify it was intentional

anyhow = "1" was moved from [dev-dependencies] to [dependencies]. This means it's a runtime dependency in the released binary. Given that run_ash returns anyhow::Result<()>, this is correct and intentional. Minor: the commit message doesn't call out this dep promotion; adding a note in the CHANGELOG would help release notes.

[DOCS / LOW] state.rs TODO comment says "future use" but the history path is already in mod.rs

The state.rs comment says pg_ash_installed is stored "for future use" but the history code path in mod.rs already exists. The TODO should reference which condition/keybinding triggers ViewMode::History so readers can find the full picture.


Observations

  • The asciinema recording in docs/ash-demo.cast provides solid evidence: /ash command launches, timeline renders with stacked bars, drill-down levels navigate correctly.
  • TerminalGuard RAII pattern for terminal cleanup matches history_picker.rs convention — consistent.
  • compute_list_len and collect_selected_row_data correctly apply the same prefix logic as the renderer, ensuring selection index stays in sync with what's drawn.
  • UX polish (commit c859bb4): bold+underline selection highlight, visible Y-axis labels, cleaner timeline title — all correct.

Verdict: LGTM

Both blocking findings from the initial review are resolved. No new blocking or high-confidence issues found. The pg_ash history deferral is clearly documented. CI is fully green.

This PR is ready for merge — awaiting Nik's approval.


REV-assisted re-review (AI analysis by postgres-ai/rev)

@NikolayS
Copy link
Copy Markdown
Owner Author

REV Review — PR #756 (/ash Active Session History TUI)

Branch: feat/753-ash | Head: b3784f1 | Reviewer: Max (automated)


Blocking findings

None.


Non-blocking findings

[WARN-1] CPU reference line rendered even at Y=0 when cpu_count=0

  • File: src/ash/renderer.rs:~320
  • cpu_count = Some(0) produces frac = 0/max_aas = 0, which after .clamp(1, h) snaps to row 1 (bottom). A zero-CPU server would always show a reference line stuck at the bottom. Realistically pg_cpu_count() never returns 0, but a guard if n == 0 { return None } is already there — fine.

[WARN-2] History mode is a documented stub

  • File: src/ash/state.rs:~102
  • ViewMode::History path falls back to live ring buffer with a TODO comment. This is intentional (pg_ash Layer 2, deferred). Not a bug — just confirming it is known and documented.

[WARN-3] query_label truncation not reflected in drill-down label

  • File: src/ash/sampler.rs:~224, renderer.rs:~532
  • Query labels are truncated to 80 chars in the sampler key. In the drill-down, the label is displayed as-is without any visual indication it may be truncated. Low impact — just cosmetic.

[WARN-4] aggregate_buckets builds global type order from all snapshots, not just the current window

  • File: src/ash/renderer.rs:~116
  • The global order is computed from the full snapshots slice passed in, which is already the windowed slice from run_ash(). Correct — no stale data leaks in.

Correctness checks (all pass)

  • Ring buffer: 600-cap VecDeque, pop_front at capacity — correct.
  • AAS calculation: db_time / wall with wall.max(1.0) guard — no divide-by-zero.
  • Bucket prefix matching: by_event keys are "{type}/" and "{type}/{event}"; renderer uses "{type}/" prefix — correct. QueryId level uses "{type}/{event}/" — correct.
  • Color stability: global type order computed once per frame, zero-AAS entries kept in vec but skipped in segment builder — positions stable.
  • Test coverage: 1879 tests (1838 pre-existing + 41 new ash tests), all passing.

Verdict

APPROVE — no blocking findings. The implementation is correct. History mode stub is intentional and documented. Ready to merge after CI goes green on HEAD.

NikolayS and others added 13 commits March 27, 2026 20:57
- DrillLevel enum: WaitType/WaitEvent/QueryId/Pid with full field context
- ViewMode enum: Live / History{from,to}
- AshState: handle_key, drill_into, go_back, zoom_in/out, cycle_refresh
- 24 unit tests: exit keys, navigation, all go_back paths, drill sequence,
  zoom clamps, cycle_refresh alias sync
- Fix renderer.rs patterns for new Pid/QueryId struct variants
- Wire mod ash; in main.rs; fix mod.rs Settings import
- AshSnapshot fields match renderer expectations: HashMap by_type/by_event/by_query, ts: i64, active_count
- live_snapshot: single pg_stat_activity query, Rust-side fold into three views
  - CPU* = wait_event_type IS NULL AND not idle-in-transaction
  - IdleTx = idle in transaction / idle in transaction (aborted)
  - all other wait_event_type values pass through unchanged
  - by_event key format: 'wtype/wevent'
  - by_query key format: 'wtype/wevent/query_label' (truncated to 80 chars)
- detect_pg_ash: extension existence check + retention_seconds from ash.config
- history_snapshots: returns empty vec with TODO for pg_ash v1.2 int[] encoding
- 5 unit tests: CPU*+Lock aggregation, IdleTx, empty input, query_id fallback,
  unknown fallback — no live DB required
- add anyhow to regular deps (used in sampler.rs non-test code)
…le (#753)

- renderer: replace static single-row timeline with right-to-left scrolling
  stacked bar chart; column height = AAS for each zoom bucket; colored by
  dominant wait_event_type; horizontal CPU count reference line (─) at
  y = cpu_count
- renderer: add summary row between chart and drill-down:
  DB TIME / WALL / AAS / CPUs
- renderer: update drill-down table columns to Stat Name | Time | %DB Time |
  AAS | Bar; aggregate over full snapshot window, not just last snapshot;
  show indented sub-event rows (  ● prefix) when type is expanded;
  bar proportional to %DB Time
- state: add zoom_level field (u8, 1-6), zoom_label(), bucket_secs(),
  zoom_cycle_forward(), zoom_cycle_back(); zoom works in both Live and
  History mode; <- cycles back, -> cycles forward
- sampler: add cpu_count field to AshSnapshot; add read_cpu_count() reading
  /proc/cpuinfo; populate cpu_count in live_snapshot()
- fix(ash): QueryId drill level prefix was '{selected_event}/' — corrected
  to '{selected_type}/{selected_event}/' to match actual by_query key format
  (renderer.rs collect_drill_rows + mod.rs compute_list_len)
- fix(ash): history mode never activated — event loop now calls
  history_snapshots(client, from, to) when state.mode == ViewMode::History;
  falls back to live ring buffer when pg_ash unavailable or returns empty
- fix(Cargo.toml): remove duplicate anyhow from [dev-dependencies]
#753)

- renderer: stacked bar chart — each column now shows proportional color
  segments per wait_event_type instead of single dominant color per bucket
- renderer: Segment struct moved out of function body (clippy::items_after_statements)
- renderer: status bar shows interval/bucket labels (not redundant refresh_secs)
- mod.rs: ring buffer capacity 60 → 600 (covers all zoom levels up to 10min)
- sampler.rs: remove module-level #[allow(dead_code)] — all symbols now used;
  annotate PgAshInfo::retention_seconds individually with rationale
)

New recording shows CPU* (green), Lock (red), LWLock (pink), IO (blue) with
proper stacked bar chart, drill-down table, and CPU reference line.
Previous GIF was pre-rework and showed only one wait type.
Max (rpg integration) added 20 commits March 27, 2026 20:58
Add a 5-column Y-axis panel left of the stacked bar chart showing
max/mid/0 AAS values as scale reference. Labels update each frame
as max_aas changes with load. Re-recorded demo GIF to show the
updated layout with labeled axis.
Previously each bucket sorted its wait types independently — CPU* could
appear at the top in one column and the bottom in the next, causing bars
to visually 'jump'. Fix: compute a global type order (by total AAS across
all visible buckets) once per frame and apply it to every column. The
dominant type is always at the bottom; others stack above it consistently.

Demo GIF re-recorded: shows /ash typed char-by-char with 0.6s pause so
the command is legible before the TUI launches.
Two fixes:

1. Stable stacked bar positions: zero-aas entries are now kept in the
   per-bucket by_type vec (not filtered out). Previously a type absent
   from one bucket was removed, causing higher types to collapse down and
   visually jump. Zero-height entries reserve their vertical slot; the
   segment builder already skips them (type_aas <= 0.0 guard).

2. 256-color terminal support: wait_type_color() now detects truecolor
   via COLORTERM=truecolor|24bit and falls back to nearest xterm-256
   Indexed colors otherwise. SSH sessions without truecolor now render
   correct distinct colors instead of degraded output.

GIF re-recorded with stable ordering visible.
Previously DB TIME/WALL/AAS/CPUs rendered as a bare Paragraph between two
bordered blocks, creating an uncontained floating row. Moved into the bottom
border title of the Timeline block using Block::title_bottom(). Layout now:
[status bar] + [timeline with summary in bottom title] + [drill-down] + [hints]
— no orphaned rows between blocks.
…icator, empty state (#753)

- Esc: back one drill level; quit only at top level (q still always quits).
  Matches k9s/lazygit/bottom convention. Fixes users accidentally quitting
  when trying to navigate back.

- Context-sensitive footer: shows Esc/b:back only when drilled below top
  level; shows q/Esc:quit at top level. No wasted hint space.

- Status bar: adds 'window: Xmin' label showing how much history is visible
  (600 samples × bucket_secs). Was showing bucket size with no window context.

- CPU reference line: turns red when current AAS > cpu_count (database is
  CPU-saturated). Gray when AAS <= cpu_count. The overload signal was
  previously invisible — now it's immediately obvious.

- Empty state: timeline shows 'No active sessions' message centered when
  there is no load, instead of a blank chart that looks broken.
Outer Block::borders(ALL) combined with inner bordered blocks produced
visible '││' gaps on left/right and '┌┐' artifacts. Dropped the outer
container — layout now uses the full frame area directly. Status bar
carries the /ash title inline. Single clean borders on Timeline and
Drill-down blocks only.
Previously cpu_count came from /proc/cpuinfo on the machine running rpg.
That's wrong — the reference line should reflect the *server's* CPU count.

Now: query the Postgres server directly:
1. pg_cpu_count() if pg_proctab extension is installed
2. count processor entries in pg_read_file('/proc/cpuinfo') (superuser, Linux)
3. None — reference line hidden rather than showing a misleading value

AshSnapshot.cpu_count is now Option<u32>. The TUI hides the CPU reference
line and omits 'CPUs:' from the summary when the value is unavailable.
Server vCPU count is not available via a plain Postgres connection.
The pg_read_file('/proc/cpuinfo') fallback required superuser and fails
on all managed Postgres (RDS, CloudSQL, Supabase, Neon, etc.).

Changes:
- query_cpu_count() now only tries pg_proctab (pg_cpu_count()); returns
  None immediately if unavailable — no superuser fallback
- run_ash() accepts cpu_override: Option<u32>; when Some(n) it overrides
  the auto-detected value in every snapshot
- /ash --cpu N syntax supported: '/ash --cpu 8' or '/ash --cpu=16'
- When cpu_count is None: CPU reference line hidden, CPUs label omitted
…ard, dense Y-axis

- Remove duplicate bucket label from status bar (was shown in both status
  bar and timeline title simultaneously)
- Add AAS to timeline top title for immediate visibility (moved from
  title_bottom where it was visually buried)
- Simplify title_bottom to DB TIME + WALL only
- Add toggleable color legend overlay (l key): 12 wait types with their
  colors, positioned top-right inside the bar area
- Add minimum height guard: render error message when terminal < 18 rows
  instead of garbled layout
- Denser Y-axis labels on tall terminals: 5 labels (top/1/4/mid/3/4/bot)
  when h > 14, 3 labels when h in [7,14], 2 labels when h <= 6

Closes #753
…cleaner timeline title

- Selected row: Modifier::REVERSED -> BOLD+UNDERLINED (no color inversion on startup)
- Y-axis labels: Color::DarkGray -> Color::Gray (readable on dark backgrounds)
- Timeline title: removed redundant 'bucket:' prefix, shows zoom level + AAS directly
- GIF re-recorded: expect script, 120x35, 20s of live /ash against ashtest DB
@NikolayS NikolayS merged commit 4b95bc5 into main Mar 27, 2026
16 checks passed
@NikolayS NikolayS deleted the feat/753-ash branch March 27, 2026 21:32
NikolayS added a commit that referenced this pull request Mar 28, 2026
- Show full drill-down: top level → wait type events → query level
- Demonstrate b key navigation back up the stack
- Show legend overlay toggle
- Updated AI test file with drill-down steps and pass criteria
- Resize to 800px wide for Telegram inline playback
NikolayS added a commit that referenced this pull request Mar 28, 2026
…om ash.samples (#761)

* feat(ash): pre-populate ring buffer from ash.samples when pg_ash is installed (#761)

When pg_ash is detected on the server, query ash.wait_timeline() for the
configured window (default 10 min) and pre-populate the ring buffer before
the live polling loop starts. Falls back to live-only when pg_ash is absent.

- sampler.rs: add query_ash_history() using ash.wait_timeline(interval, '1s')
- mod.rs: pre-populate ring buffer on startup when pg_ash.installed
- state.rs: document pg_ash_installed field
- 26 new unit tests covering history parsing, ring buffer capacity, graceful
  degradation when pg_ash absent

Fixes #761

* test(ai): add slash-ash-pgash AI test for pg_ash history integration (#761)

* chore(demos): move GIFs to demos/, rename to match AI test filenames; add 1s pause after commands

- docs/ash-demo.gif -> demos/slash-ash-general.gif
- docs/ash-demo.cast -> demos/slash-ash-general.cast
- AI test recording paths updated accordingly
- Added sleep 1 after /ash command in expect scripts (gives user time to read output)
- slash-ash-pgash.md: same conventions applied

* fix(ash): address REV findings — parameterize SQL, remove dead code, use configured window; add CI integration tests (#761)

- BUG-1: parameterize ash.wait_timeline interval via $1 (no more format! injection)
- BUG-2: remove dead history_snapshots() fn; wire History mode through query_ash_history()
- WARN-1: use state.bucket_secs()*600 instead of hardcoded 600s window
- Add 2 #[ignore] integration tests: test_pg_ash_history_live, test_pg_ash_history_graceful_degradation

* fix(ash): address REV findings M1/M2 — parameterize interval $1, use int8 for samples; doc cleanups (#761)

- M1: replace format!() string interpolation with $1 parameterized query
  in query_ash_history_inner; rename helper to _inner (no longer interval-keyed)
- M2: use samples::int8 (was int4) to avoid silent overflow on busy servers;
  update row.get() to i64; use u32::MAX as saturating fallback
- L3: fix double-sentence doc on pg_ash_installed in state.rs
- L2: clarify retention_seconds is reserved, not yet wired
- L4: add cpu_count comment in history snapshot construction
- M3: add TODO comment for history-mode per-frame query caching

* test(ash): add CI integration tests for pg_ash sampler (#761)

Three tests in integration_repl.rs (run with --features integration):
- ash_pg_extension_absent_in_test_db: verifies detection SQL returns false
  when pg_ash is not installed (CI baseline)
- ash_wait_timeline_missing_returns_error: confirms the parameterized
  ash.wait_timeline query errors gracefully when schema is absent, validating
  the sampler's Ok(rows) fallback guard
- ash_live_snapshot_query_shape: executes the exact live_snapshot() SQL
  against the test DB, validates row shape (non-empty wtype, non-negative cnt)

* style(ash): cargo fmt — fix integration test formatting (#761)

* fix(ash): sync refresh_interval_secs to bucket_secs on zoom; show actual data window (#763)

- zoom_cycle_forward/back now call sync_refresh_to_zoom(), keeping
  refresh_interval_secs = bucket_secs (capped at 60s) so live sampling
  rate matches display granularity — zoom out = coarser but wider real data
- Status bar window label now shows actual data span (samples × bucket_secs)
  rather than ring-buffer capacity — no more '10min' when you've been running
  for 5 seconds
- Remove now-unused window_label() from AshState
- Add test: zoom_cycle_syncs_refresh_to_bucket

Fixes the misleading window label and zoom/sampling mismatch Nik found.

* docs(ash): re-record general demo GIF with drill-down navigation (#756)

- Show full drill-down: top level → wait type events → query level
- Demonstrate b key navigation back up the stack
- Show legend overlay toggle
- Updated AI test file with drill-down steps and pass criteria
- Resize to 800px wide for Telegram inline playback

* docs(ash): record pg_ash history integration demo GIF (#761)

Shows: history bars pre-populated from ash.wait_timeline on startup
(left side full before live data arrives), drill-down navigation,
legend overlay. 800px wide, Dracula theme.

* docs(ash): native resolution GIFs (951px, no resize)

* feat(ash): add X-axis timestamp labels to timeline

Show HH:MM anchors at left (oldest visible bucket), right (newest/now),
and midpoint when area >= 20 cols wide.  Pure UTC arithmetic — no extra
deps.  Carves a 1-row strip inside the timeline inner area so bar height
is preserved (same layout, one row taller overall inner area used for axis).

Closes the UX gap where bars had no time context at all.

* docs(ash): re-record GIFs with X-axis timestamp labels

Both slash-ash-general.gif and slash-ash-pgash.gif re-recorded
after feat(ash): add X-axis timestamp labels to timeline (feb6fab).

HH:MM anchors now visible at left/mid/right of timeline bottom row.

* docs(ash): re-record pgash GIF with actual historical data pre-populated

Previous recording had only ~90s of history; sampler had just restarted.
This recording has 1000+ samples (~10min) in ash.sample before launch,
so bars are fully pre-populated from the left on first frame.

* fix(ash): detect pg_ash via ash.wait_timeline in pg_proc, not pg_extension

pg_ash installed via SQL (not CREATE EXTENSION) doesn't appear in
pg_extension, causing detect_pg_ash() to return installed=false and
silently skip history pre-population on startup.

Fix: check for ash.wait_timeline in pg_proc/pg_namespace instead.
This handles both install paths (extension + manual SQL) and is the
only capability we actually need to verify.

Update integration test name/query to match.

* fix(ash): use format! for interval literal in wait_timeline query

tokio-postgres cannot serialize a Rust String as a Postgres interval
parameter ($1::interval) without a server-side type annotation that
requires an explicit type OID. The client-side serialization fails
with 'error serializing parameter 0', silently returning an empty
vec and causing history pre-population to be skipped on every launch.

Fix: embed the interval as a literal using format! on the u64 window_secs
value (not user input — no injection risk). Also convert the Err arm from
silent Ok(vec![]) to a proper Err return so callers can log if needed.

* docs(ash): re-record pgash GIF — history bars visible from first frame

Previous recordings failed because wait_timeline query silently returned
empty (tokio-postgres interval serialization bug). Now fixed: bars
pre-populated immediately on /ash launch (57s of history on first frame).

* test(ash): fix ash_wait_timeline_missing_returns_error to use real query

The sampler now uses a literal interval (format!) not a $1 parameter.
Update the integration test to use the same SQL so it tests actual
production behavior: ash.wait_timeline absent → query returns Err.

* docs(ash): re-record both demo GIFs with all fixes applied

- General /ash: live streaming, drill-down, X-axis timestamps, legend
- pg_ash history: bars pre-populated from first frame (1min window)

All three bugs fixed before recording:
- detect_pg_ash uses pg_proc not pg_extension
- wait_timeline interval serialization fixed
- integration test matches production SQL

* docs: add /ash section and GIFs to README

- Add slash-ash-general.gif at top (after intro, before Features)
- Add Active Session History to feature bullet list
- Add ## Active Session History section with usage, keybindings,
  zoom levels, pg_ash history pre-population, and pgash GIF
- Both GIFs already committed in demos/ on this branch

* docs(ash): re-record GIFs using pgbench load per tests/ai spec

- slash-ash-general: pgbench -c 8 load, drill-down, legend, zoom
- slash-ash-pgash: history pre-populated (1min, active=22) on first frame
Both recorded per exact expect scripts in tests/ai/

* docs: replace 2.8MB pspg_screenshot.png with 147K JPEG

PNG was oversized for inline README rendering.
Resized to 1200px wide, converted to JPEG (quality 80) — 147K vs 2.8MB.

* docs(ash): re-record ASH GIFs with fresh pgbench load

pgash GIF: 5min history pre-populated (active=26) on first frame
general GIF: live pgbench load, drill-down, legend

* docs: move /ash GIF to very top of README

* docs: collapse secondary GIFs and pspg screenshot behind <details>

* docs: add quickstart hero demo GIF (connect/version/fix/ash flow)

* chore: bump version to 0.9.0

* docs: re-record quickstart demo with hostname 'demo' in status bar

* docs: re-record quickstart demo v5 — hostname 'demo' in status bar

* refactor(ash): extract group_timeline_rows and split_wait_event helpers

Eliminates duplicated snapshot-grouping logic between
query_ash_history_inner and the test suite.  Tests now call the
production helper directly instead of maintaining a copy.

No behaviour change — all 82 ash tests pass.

* fix(ash): show HH:MM:SS on X axis at zoom 1-2 (bucket ≤ 15 s)

At zoom 1 (1-second buckets) all labels within the same minute were
identical (13:40 / 13:40 / 13:40), making the axis appear static.
Show seconds when bucket_secs ≤ 15 so the right label visibly ticks
forward every second.  Coarser zoom levels keep HH:MM as before.

* docs: re-record quickstart demo (1.4s pause before /ash); add X-axis labels demo GIF

- quickstart-demo: 1.4s deliberate pause after typing /ash so the
  command is visually distinct before the TUI opens; 265K
- slash-ash-xaxis.gif: dedicated demo showing HH:MM:SS labels shifting
  every second at zoom 1 (645K, behind <details>)
- README: update X-axis bullet to mention HH:MM:SS vs HH:MM behaviour;
  add <details> block with slash-ash-xaxis.gif

* docs: quickstart demo v6 — clean prompt, fix timing, hostname fix

- Shell prompt set to '$ ' via --command env override (no openclaw-server)
- Wait for hint text before typing /fix (no more overlap)
- 1.4s pause after typing /ash before TUI opens
- 257K
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: TUI Active Session History (/ash command)

2 participants