Skip to content

feat: prospecta-tui v0.1 — interactive TUI for the memory substrate - #4

Merged
witt3rd merged 12 commits into
mainfrom
tui-v0.1
Jun 2, 2026
Merged

feat: prospecta-tui v0.1 — interactive TUI for the memory substrate#4
witt3rd merged 12 commits into
mainfrom
tui-v0.1

Conversation

@witt3rd

@witt3rd witt3rd commented Jun 2, 2026

Copy link
Copy Markdown
Owner

Lands prospecta-tui v0.1 — the interactive terminal UI for the prospecta memory substrate. Co-located Rust + Ratatui workspace under tui/, sibling to the Python library.

The schema is the contract: read paths go direct via sqlx (compile-time-checked against the live schema), the retain write path shells out to the Python prospecta CLI so the LLM-in-the-loop work and the spine stay library-owned. The TUI never mutates the schema.

Five tabs, four feature axes

  • banks — all banks + per-bank counts; Enter drills into documents
  • docs — documents → memory_items drill-down (the question-shaped index_text)
  • events — live tail across all five event tables; Enter on a recall/retain row opens the full thread (formulate → recall → synthesis → llm_calls, or retain → document → items → index_text call)
  • dashboard — per-bank 24h health: counts, retain/recall rates, latency, the P18 parse_fallback safety signal, per-prompt llm_calls, and the sweep status panel (latest pass per corpus, ok/error/running + age)
  • search — free-text over both lexical channels (content_tsv = anticipated questions, body_tsv = source body); each hit labelled both/question/body so the P14 honest-safety-net is visible. R opens the retain form

Manual ops (the write path)

The retain form shells out to prospecta retain. Connection + embedder env inherit from the TUI; only the bank is pinned per-form. Invocation is configurable (PROSPECTA_CLI / PROSPECTA_CLI_CWD) so the same binary works in dev (uv run prospecta) and deployed. The form reports the honest outcome — the document UUID on success, the verbatim CLI error on failure. It never fabricates success.

Verified end-to-end against a live dim-384 bank, fully offline (PROSPECTA_EMBEDDER=sentence-transformers, no API key): success retains a real document with the UUID parsed from stdout; a bad bank surfaces the real FK-constraint error. (The offline embedder path itself shipped in #3.)

Ship-readiness

  • Offline build — committed .sqlx/ query cache (24 queries) means the crate builds with SQLX_OFFLINE and no database at compile time. cargo install --path tui produces a working prospecta-tui on PATH; verified from a clean cargo clean with DATABASE_URL unset.
  • README — real install/run/keybindings/tabs doc (replaces the scaffold stub) + a top-level README pointer under Observability.
  • CI.github/workflows/tui-ci.yml runs fmt --check + build + test on PRs touching tui/. Tests are hermetic (ratatui TestBackend + in-memory fixtures + the .sqlx cache, no Postgres), so this runs anywhere and gives the branch-protection ruleset a status check it can require.

Tests

56 green (10 unit + 46 integration), all hermetic — fmt, build, and test pass offline with --locked. Coverage spans every view's render paths, the dashboard health/sweep signals, the search channel attribution, and the retain form (fields, status banners, submit gating, the stderr_tail error-line preference that filters provider noise).

Out of scope (tracked, deliberately not half-built)

  • Testcontainers integration tests — the current 56 tests exercise render/format logic with fixtures, not the live sqlx query path. True integration against a throwaway Postgres is a distinct focused effort, tracked on prospecta-tui v0.1: Rust TUI inspection surface (tui/ subtree) #1.
  • recall-synth manual form — needs a prospecta recall-synth --json subcommand on the library side; tracked separately.

Commits

Scaffold → bank list → event stream → docs drill-down → recall/retain threads → dashboard → sweep panel → search axis → manual retain → ship-readiness. Tracking checklist on #1.

⚒️ Forge — built bench-beside-bench with @witt3rd.

Co-located Rust workspace under tui/, sibling to the Python library.
sqlx with compile-time-checked queries against the live prospecta schema —
honors P19 (durable observability) by treating the schema as the contract.

This commit ships only the scaffold:
- Cargo.toml with locked stack (ratatui 0.30, crossterm 0.29, sqlx 0.8, tokio 1, clap 4)
- main.rs with --smoke flag that connects via DATABASE_URL and counts banks
- .env.example, .gitignore, README pointing at the handoff doc

The compile-time SELECT against banks verifies the toolchain end-to-end:
build only succeeds when DATABASE_URL points at a migrated prospecta DB.

Smoke verified against local docker compose substrate (schema v3, 1 bank).

Next: Ratatui event loop + bank-list view, then event-stream view, then
fan out across manage/use/search/observability axes. See HANDOFF in
~/forge/notes/ for the v0.1 plan and locked decisions.
The skeleton everything else hangs off:

- src/lib.rs exposes the modules so integration tests and the binary share
  one source of truth; main.rs imports app+config from the lib.
- src/theme.rs centralizes styles + honors NO_COLOR (accessibility convention).
- src/views/common.rs renders the chrome (header bar with redacted DATABASE_URL,
  footer hints, ?-overlay help popup) and a chrome_layout helper every view uses.
- src/views/manage.rs implements the bank-list Table widget with selection,
  empty-state copy, and an error-state panel.
- src/db/banks.rs runs the canonical 'bank stats one-liner' from
  docs/observability.md (correlated subqueries for documents/items/retains/recalls
  count rollups) via sqlx::query! — compile-time-checked against the live schema.
- src/app.rs wires the event loop: 250ms poll cadence (a free hook for the
  upcoming event-stream view), j/k/↑/↓/g/G navigation, r to refresh, ? to help,
  q/Esc/Ctrl-C to quit. Help overlay swallows the next key to close — keeps the
  UX consistent (no per-key special-case).
- src/config.rs has redact_database_url + validate; 5 unit tests cover redaction
  and scheme validation.
- tests/render.rs covers the bank-list view headless against TestBackend:
  populated render, empty state, error state, and selection wrap.

9 tests passing (5 unit + 4 integration). Cargo build clean.
Smoke verified against the live substrate: 3 banks visible.
Axis 4 'review observability' entry point — tail view of the append-only
event tables, interleaved by created_at DESC, color-coded by kind.

db/events.rs:
- One sqlx::query! per table (retain/recall/formulate/llm_call/sweep_pass)
  so compile-time schema checking stays intact — UNION ALL across
  heterogeneous tables defeats type inference.
- Five queries run concurrently via tokio::try_join!; wall-clock is the
  slowest, not the sum.
- Per-kind row → Event with normalized {kind, id, bank_id, created_at,
  duration_ms, has_error, summary}, then sorted DESC and truncated to
  EVENTS_TOTAL_CAP (200).
- Optional bank filter is wired through (None = all banks); v0.1 leaves
  the bank-filter keybinding for a future commit.

views/observability.rs:
- Ratatui Table with kind color-coding (LightGreen retain, LightBlue
  recall, LightCyan formulate, LightYellow llm_call, Magenta sweep);
  NO_COLOR honored.
- Auto-scroll mode pins selection to row 0 across refreshes; manual
  navigation (j/k/Up/Down) turns it off, 'g'/Home/'f' toggles it back.
  Title shows ▼ marker while auto-scroll is on.
- Duration humanized: <1000ms shown as 'NNNms', ≥1000ms as 'N.Ns'.
- Error marker '!' surfaces parse_fallback, llm_call.error, and
  sweep_pass.errors_count > 0.

app.rs:
- Two-tab navigation (banks ↔ events) via Tab/Shift-Tab/1/2.
- Per-tab refresh: events tab polls every 750ms (only when visible);
  banks tab refreshes on 'r'. The 100ms key-poll cadence stays
  responsive while the 750ms refresh tick fires on its own schedule.
- Help overlay enumerates the new keybindings (Tab, 1/2, f).

main.rs:
- New --dump-events flag dumps the recent-events query to stdout
  (parallel to --smoke). Sanity check without launching the TUI.

tests/render_events.rs adds 4 integration tests: all-kinds render,
empty state, error state, auto-scroll state machine.

Test suite: 13 passing (5 unit + 8 integration). dump-events verified
against the live substrate (14 fixture events across all five kinds).
Adds the manage-axis depth that pairs with banks rollup and event stream.
Three tabs at v0.1: banks, docs, events. Same chrome shape; the drill-down
state machine carries a level field so docs renders documents-or-items.

db/documents.rs:
- list_for_bank: documents in a bank with rolled-up item_count via
  correlated subquery, ordered by created_at DESC, paginated by limit+offset.
- list_for_document: memory_items for one document, ordered for stable
  display. Both via sqlx::query!, schema-verified at compile time.

views/docs.rs:
- DocsState carries (level, bank_id, documents, focused_document, items)
  plus independent TableState per level — selection on items doesn't
  clobber selection on documents.
- Documents table: short uuid prefix, items count, source, tags, created_at.
- Items table: short uuid, src marker (llm/caller), tags, content.
- Empty-state panels distinguish 'no bank picked' from 'bank has no docs'.
- short_uuid() helper exported for the CLI dump path.

app.rs:
- Three-tab cycle via Tab/⇧Tab/1/2/3; descent via Enter/→/l from
  banks (auto-binds the selected bank and loads its documents) and from
  docs-level (loads items for the selected document); ascent via Esc/←/h.
  Esc at the top level still quits (preserves v0.1 contract from c3877ea).
- Per-tab refresh on 'r' dispatches to the right loader.
- Help overlay enumerates the new keybindings.

main.rs:
- New --dump-docs <bank> CLI flag for live verification without launching
  the TUI; --doc-id <uuid> descends into one document's memory_items.
- SIGPIPE handler set to SIG_DFL so 'prospecta-tui --dump-docs ... | head'
  exits cleanly instead of panicking.

tests/render_docs.rs: 5 integration tests (no-bank hint, documents render,
items render, navigation wraps independently per level, empty-bank hint).

Live-verified against the substrate: 3 documents in default bank, 6/3/3
items respectively. 18 tests passing total (5 unit + 13 integration).
…vent

The load-bearing observability surface per docs/observability.md
'Reading a recall event end-to-end'. Pressing Enter on a recall row in
the event stream opens a one-pane thread view showing:

  • the preceding formulate_event (verbatim user message + raw LLM JSON
    expansion + parse_fallback diagnostics including error_kind)
  • the recall body (queries list, per-chunk results with rank/source/
    document_id/per-channel scores/200-char preview, synthesis text)
  • the paired llm_calls in the ±10s window (prompt_name, verbatim
    prompt_text and response_text, duration, error markers)

db/recall_thread.rs:
- Three sqlx::query! calls (recall + LATERAL-style most-recent-formulate-
  before-recall + ±10s llm_calls window), with formulate+llm_calls
  fetched concurrently via tokio::try_join!.
- Schema-as-contract preserved end-to-end; all compile-time-checked.

views/recall_thread.rs:
- Single scrollable Paragraph rendering — multi-section layout via
  push_section helpers, ratatui Wrap{trim:false} handles long content.
- chunk_lines() soft-wraps long verbatim payloads at ~90 cols on word
  boundaries while preserving the raw newlines in JSON / prompts.
- Honest about missing data: explicit hints when results JSONB is null
  (pre-0003 row), when synthesis is null (plain recall, not recall_synth),
  when llm_calls window is empty, when persist_llm_text=false stripped
  the verbatim prompt/response.

app.rs:
- recall_thread overlay swallows keys on the events tab while open;
  navigation = scroll (j/k/↑/↓, Space/PgDn, g/Home), Esc/← closes.
- Enter on a non-recall event row prints a status hint rather than
  silently doing nothing.
- Footer hints + help overlay updated for both events-tab modes.

main.rs: --dump-recall-thread <RECALL_ID> CLI verifier, same family as
--smoke / --dump-events / --dump-docs.

tests/render_recall_thread.rs: 6 integration tests covering empty state,
load-error state, full multi-section render (all four section headers,
queries list, per-chunk results with scores, synthesis, llm_call
prompt/response sections), missing-formulate, missing-results-and-
synthesis, scroll state machine.

Live-verified: dump_recall_thread on recall id=1 surfaces the preceding
formulate, 3 llm_calls in the ±10s window, and the synthesis. 24 tests
green (5 unit + 19 integration).
…vent

Sibling to the recall thread view per docs/observability.md. From the
event stream, Enter on a retain row opens a one-pane thread showing:

  • the retain_event metadata (items_count, caller_supplied flag,
    duration, error)
  • the source document (id, source, content_hash, tags, full
    original_text — P5 honored, no truncation)
  • every memory_item the retain produced (id, llm/caller marker, tags,
    content — the question-shaped index_text strings)
  • the paired index_text llm_call (verbatim prompt_text and
    response_text), plus the verbatim index_text_generated array from
    the retain row when LLM-authored

db/retain_thread.rs:
- Four sqlx::query! calls: retain row, document by id, items by
  document_id, and the nearest index_text llm_call within ±10s. The
  llm_call query orders by ABS(EXTRACT EPOCH ...) so the closest call
  to the retain timestamp wins.
- Document/items/index_text fetched concurrently via tokio::try_join!.
- Schema-as-contract preserved end-to-end.

views/retain_thread.rs:
- Single scrollable Paragraph rendering, same shape as recall_thread.
- Honest about boundaries: distinct hints for 'no document linked'
  (retain pre-dated documents column or doc deleted), 'no items
  linked', 'caller-supplied → no LLM call expected', 'no index_text
  call in window' (with raw_llm_response fallback when present), and
  'no prompt/response captured' (persist_llm_text=false or pre-0003).
- chunk_lines() soft-wraps long original_text and verbatim payloads
  on word boundaries.

app.rs:
- retain_thread_open state + open/close helpers.
- Enter on Events tab routes by event kind: Recall → recall thread,
  Retain → retain thread, anything else → status hint.
- Retain-thread overlay swallows the same scroll keys as the recall
  thread overlay (j/k, Space/PgDn, g/Home, Esc/← closes).
- Footer hints union both threads under one Events-overlay shape.

main.rs:
- --dump-retain-thread <RETAIN_ID> CLI verifier, same family as
  --dump-recall-thread.

tests/render_retain_thread.rs: 6 integration tests covering empty,
load-error, full multi-section render, caller-supplied skip path,
missing-document hint, scroll state.

Live-verified via --dump-retain-thread against retain id=1: retain
metadata + index_text llm_call surface correctly, honest 'none linked'
report when document_id is null on the seed row. 30 tests passing
(5 unit + 25 integration).
Fourth tab, workhorse register. Five concurrent compile-time-checked
queries condensed into a 2x2 card grid + per-prompt llm_call table.

db/dashboard.rs:
- One sqlx::query! per axis (bank meta, doc counts, retain+recall
  with mean durations, formulate fallback rate, per-prompt llm_calls
  + total llm_ms). Five fetches in parallel via tokio::try_join!.
- 24h window matches the cheat-sheet 'bank stats one-liner' and
  'volume + timing distribution' recipes from docs/observability.md.

views/dashboard.rs:
- 2x2 card grid: substrate (counts), activity (24h volumes), latency
  (24h means + Σ llm time), health (parse_fallback rate + llm error
  rate). Bottom strip: per-prompt llm_calls table.
- parse_fallback rate is the load-bearing P18 health signal — color
  graduates green/yellow/red at 0% / >0% / ≥10% thresholds with
  formatted percentage + raw fraction (e.g. '15.0%  (3/20)').
- Latency formatting humanized: <1s as 'NNNms', ≥1s as 'N.Ns'; total
  llm time over a minute renders as 'N.Nm'.

app.rs:
- Fourth tab plumbed through Tab/⇧Tab cycle, 1/2/3/4 direct jumps,
  Tab::Dashboard navigation arms (no-op on j/k/g/G — dashboard is a
  read-only surface), r refresh, render dispatch, footer hints.
- 5s background poll while the dashboard tab is visible; tighter
  interval than the events stream because health signals settle slower
  but matter as live indicators. Pinned to currently-selected bank;
  falls through to bank-list selection when the user tabs in cold.

main.rs:
- --dump-dashboard <bank> CLI flag — fifth verifier in the family.
  Live-verified against the real substrate: 3 docs, 12 items, 50%
  parse_fallback rate (1/2 formulates), per-prompt breakdown shows
  formulate/index_text/synthesize all in the window.

tests/render_dashboard.rs: 4 integration tests cover empty state,
load error, full multi-card render (substrate/activity/latency/health
cards + llm table + P18 percentage + bank id), and no-activity state
(hint text when 24h window is empty).

34 tests green (5 unit + 29 integration). cargo build + cargo fmt
clean. Schema-as-contract preserved end-to-end via sqlx::query!.
The "is the sweeper running? did it stop? is it erroring?" signal, sitting
on the dashboard tab beside the llm_calls table — same workhorse register,
same bank scope, same is-this-healthy question. Started as a card per the
last handoff; it earned a table once the per-corpus shape was clear.

db/dashboard.rs:
- SweepStat struct + fetch_sweeps(), sixth concurrent query folded into
  the existing tokio::try_join!. Derives from the append-only sweep_passes
  history via DISTINCT ON (corpus_path) ORDER BY started_at DESC — the
  history is the durable truth and survives a sweeper_state snapshot that
  was never written (which is exactly the live state: 2 passes, 0 snapshot
  rows). Schema-as-contract preserved via sqlx::query!.

views/dashboard.rs:
- Bottom region now splits 55/45: llm_calls table | sweep table. Sweep
  table is one row per corpus showing the latest pass: corpus tail, status
  (ok/error/running), files indexed/seen, error count, and age-since-start.
- Status colors graduate: green 'ok', red 'error' (fatal error OR any
  per-file errors_count > 0), accent 'running' (started, never ended).
  errors_count cell reddens when nonzero. Age column is the load-bearing
  "did it stop?" signal — coarse 12s/5m/3h/4d so a stale last-pass reads
  loud. Empty state: "(this bank has never been swept)".
- shorten_path tails long corpus paths (/home/dt/notes -> notes); format_age
  is the coarse relative-time helper.

main.rs:
- --dump-dashboard now prints the sweep section too (sixth signal in the
  family verifier). Live-verified against the real substrate: /home/dt/notes
  reads 'error' (errs=1) — DISTINCT ON correctly picked the most recent
  degraded pass over the earlier healthy one.

tests/render_dashboard.rs: dummy_stats() carries two sweeps (one healthy,
one errored). Full-render test asserts the panel title, headers, corpus
tail, and both ok/error statuses paint. Two new tests: never-swept hint
and in-flight 'running' status.

36 tests green (5 unit + 31 integration). cargo build + cargo fmt clean,
zero warnings.
The fourth and final feature axis (manage/use/search/observability). Fifth
tab `search` with a query box + results table + preview pane. Searches both
tsvector channels the library indexes and reports which one(s) surfaced each
hit — making the P14 honest-safety-net visible at the operator surface.

db/search.rs:
- SearchHit + search(): one websearch_to_tsquery (same parser the library's
  lexical legs use, so operator queries behave like agent-path retrieval —
  quoted phrases, OR, -negation) CROSS JOINed against memory_items. Reports
  ts_rank and @@ match for BOTH content_tsv (the index_text questions) and
  body_tsv (the source body, migration 0002). ORDER BY GREATEST(rank) DESC.
  Empty queries short-circuit to no results. Schema-as-contract via query!.
- channel_label() collapses (content_hit, body_hit) to both/question/body —
  the load-bearing signal: did the anticipated question match, or did the
  body channel rescue a doc whose index_text drifted from query language?

views/search.rs:
- Two input modes (Editing / Browsing) so the app loop knows whether keys
  edit the query or navigate results. Three zones: query box (top, cursor
  when editing), results table (channel/rank/gen/matched-index_text/source),
  preview pane (full index_text + body chunk + per-channel rank breakdown
  for the selected hit). Channel labels color-graduate: both=green,
  question=accent, body=blue.

app.rs:
- Fifth tab plumbed through: `5` jump, Tab/⇧Tab cycle (now 5-way), r runs
  the query, j/k/g/G navigate results. Editing mode intercepts text input
  ahead of the main match (mirrors the thread-overlay pattern): chars append,
  Backspace deletes, Enter runs + lands in Browsing, Esc toggles back to the
  query box (or quits when the list is empty). bind_search_bank() falls
  through to the banks-tab selection, mirroring how dashboard auto-binds.
  Footer hints + `?` help overlay updated for both modes.

main.rs:
- --dump-search "BANK:QUERY" verifier (sixth in the family). Live-verified
  against the real substrate: "default:bilateral synthesis" → 6 hits with
  correct both/body attribution; "compile-time checking" → 1 question-channel
  hit at rank 0.461 (denser match in index_text than diffuse body); a
  nonsense term → 0 hits. All four channel states confirmed.

tests/render_search.rs: 6 tests — pre-search prompt, no-results hint, the
three channel labels + ranks painting, preview pane content, editing-mode
query echo, and select_next/prev clamping.

42 tests green (5 unit + 37 integration). cargo build + cargo fmt clean,
zero warnings.
Closes the manual-ops milestone (#1): the last write-path surface. Read
paths go direct via sqlx; the retain write path shells out to the Python
`prospecta retain` so the LLM-in-the-loop work and the spine stay
Python-owned — the schema is the contract, the CLI is the writer
(handoff decision 3).

shell.rs:
- Async shell-out via tokio::process. RetainArgs → `prospecta retain` with
  --source / --tags / --index-text (repeatable). Connection + embedder env
  (DATABASE_URL, PROSPECTA_EMBEDDER, PROSPECTA_EMBED_MODEL) inherit from the
  TUI; only PROSPECTA_BANK is pinned per-form. Invocation is configurable so
  the same binary works in dev and deployed:
    PROSPECTA_CLI      — command prefix (default `prospecta`; e.g.
                         "uv run prospecta")
    PROSPECTA_CLI_CWD  — child working dir (lets `uv run` resolve the project)
- RetainOutcome is the honest contract: ok flag, parsed document_id, verbatim
  stdout/stderr, exit code. Never fabricates success.

views/retain.rs: modal form — content (multiline, required) / source / tags /
index_text, field focus with Tab/arrows, a result banner that graduates
idle→running→ok(document_id)→failed(reason). Centered overlay, NO_COLOR-safe.

app.rs:
- Retain modal opened with `R` from the search tab (the "use" axis pairs with
  search). Modal intercepts all input while open: text edits the focused
  field, Tab/↑↓ move fields, Enter is newline in content / next-field
  elsewhere, Ctrl-S submits, Esc cancels. Drawn last over the full frame.
- submit_retain() records the honest outcome — document UUID on success, or
  the CLI's real stderr via stderr_tail() which prefers the `error:` line and
  filters litellm/provider warnings + progress-bar noise.
- Footer + `?` help updated; tokio gains the `process` feature.

main.rs: --dump-retain "BANK:CONTENT" verifier (seventh in the --dump-*
family). Auto-derives index_text so it runs offline.

Verified end-to-end against the live tui-st (dim-384) bank, fully offline via
`PROSPECTA_CLI="uv run prospecta" PROSPECTA_EMBEDDER=sentence-transformers`:
  - success: retained document ead4453b-… (exit 0), real UUID parsed
  - failure: nonexistent bank surfaces the real FK-constraint error, no fake
    success — the honest outcome contract holds both ways.

tests: render_retain.rs (9 — fields, typed content, ok/running/failed
banners, field cycling, can_submit gating, focused_mut, reset) + app unit
tests (5 — opt + stderr_tail error-line preference / noise filtering /
fallbacks).

56 tests green (10 unit + 46 integration). cargo build + cargo fmt clean,
zero warnings.
Closes the remaining issue #1 ship-readiness boxes (README, cargo install)
and adds a requirable CI status check.

Offline build (.sqlx/ cache):
- sqlx query! macros previously needed a live DATABASE_URL (or .env) at
  compile time, so a clean `cargo install` by anyone else failed without a
  running DB. Committed the .sqlx/ offline query cache (24 queries) generated
  via `cargo sqlx prepare`. The crate now builds with SQLX_OFFLINE — no DB
  needed at compile time.
- Verified: `cargo clean -p prospecta-tui && SQLX_OFFLINE=true cargo build`
  with DATABASE_URL unset builds clean; `cargo install --path tui` produces a
  working `prospecta-tui` on PATH; `--help` runs.

tui/README.md: real install/run/keybindings doc replacing the scaffold stub —
tabs table, full keybinding reference, the manual-retain shell-out env
(PROSPECTA_CLI / PROSPECTA_CLI_CWD + the offline sentence-transformers path),
the --dump-* headless verifier family, and the offline-build / cargo sqlx
prepare note.

Top-level README: a "prospecta-tui — the interactive inspection surface"
pointer under Observability, framing the TUI as the legible shape of the
durable trace.

CI (.github/workflows/tui-ci.yml): fmt --check + build + test on push/PR
touching tui/. Tests are hermetic — ratatui TestBackend + dummy fixtures +
the .sqlx offline cache, no Postgres needed — so this runs anywhere and gives
the branch-protection ruleset a status check it can require. Verified the
exact CI commands pass locally offline + --locked (56 tests green).

Note on testcontainers (still open on #1): the existing 56 tests are already
CI-hermetic but they exercise render/format logic with in-memory fixtures, not
the live sqlx query path. True integration tests against a throwaway Postgres
are a distinct, focused piece of work — deliberately NOT half-built into this
ship-readiness PR. Tracked separately.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Lands prospecta-tui v0.1 — a new co-located Rust + Ratatui workspace under tui/ that provides an interactive terminal inspection surface for the prospecta memory substrate. Read paths use sqlx direct against Postgres with compile-time schema checking (via a committed .sqlx/ offline query cache), and the retain write path shells out to the Python prospecta CLI. Five tabs cover banks, docs, events (with recall/retain thread drill-downs), a per-bank dashboard (24h health + sweep status), and cross-channel search with a retain form.

Changes:

  • New tui/ Rust crate: binary + library, modular views/ (banks, docs, events, recall/retain threads, dashboard, search, retain form), db/ query modules, shell.rs for CLI shell-out, theme.rs, config.rs, plus README, .env.example, and 24 committed .sqlx/ query cache files for offline builds.
  • 56 hermetic tests across 7 integration files using ratatui::TestBackend + in-memory fixtures (no Postgres required).
  • New .github/workflows/tui-ci.yml running fmt --check, build, and test with SQLX_OFFLINE=true; top-level README pointer added under Observability.

Reviewed changes

Copilot reviewed 38 out of 63 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tui/Cargo.toml, tui/.gitignore, tui/.env.example Crate manifest, ignores, local-dev env scaffold.
tui/src/main.rs CLI entrypoint: --smoke + --dump-* verifiers, TUI bootstrap with raw-mode/alt-screen restore.
tui/src/lib.rs, tui/src/views/mod.rs, tui/src/db/mod.rs Module wiring.
tui/src/config.rs DATABASE_URL validation + password redaction (with unit tests).
tui/src/shell.rs Async shell-out to prospecta retain honoring PROSPECTA_CLI/PROSPECTA_CLI_CWD.
tui/src/theme.rs Centralized styles + NO_COLOR honoring.
tui/src/db/{banks,documents,events,dashboard,search,recall_thread,retain_thread}.rs Per-topic schema-checked sqlx queries; events fan out to 5 tables in parallel via try_join!.
tui/src/views/common.rs Shared chrome (header/footer/help overlay).
tui/src/views/manage.rs Bank-list table with row selection (contains dead-code workaround flagged).
tui/src/views/docs.rs Two-level documents → memory_items drill-down.
tui/src/views/observability.rs Event-stream tail with kind color-coding + auto-scroll.
tui/src/views/recall_thread.rs, tui/src/views/retain_thread.rs End-to-end thread inspection panes.
tui/src/views/dashboard.rs 2x2 health cards + per-prompt llm_calls + sweep-status table.
tui/src/views/search.rs Dual-channel search with input/results/preview split and channel labels.
tui/src/views/retain.rs Modal retain form with field focus, status banner, submit gating.
tui/tests/render*.rs (7 files) Hermetic render + state tests via TestBackend.
tui/.sqlx/query-*.json (×24) Offline query cache for sqlx::query! compile-time checks.
tui/README.md, README.md TUI user-facing docs + top-level pointer.
.github/workflows/tui-ci.yml CI: fmt + build + test, hermetic via SQLX_OFFLINE.
Files not reviewed (24)
  • tui/.sqlx/query-0e31ead873bf598cc23b7148b4f6f6115885be16c1b6e03a754bc253935b5ec7.json: Language not supported
  • tui/.sqlx/query-19b9647d6d8bbf688376a2ddee0c58f282ffef1de489f1c32f953191b16d1839.json: Language not supported
  • tui/.sqlx/query-1ac0645e1f1bb6bf04be8fa1af7896903ac09374a68aa54804d961b1430c36af.json: Language not supported
  • tui/.sqlx/query-1c3cde708f73858657bdf1720958ae1d7274ce70603f675e2348e7d691955183.json: Language not supported
  • tui/.sqlx/query-226afdbe4a64c0b0c5895e5ed75f1bec5681efa89e69dff50e618506b1d67465.json: Language not supported
  • tui/.sqlx/query-2526e9744ef464aa4fa49f3278c833c105fdbca566580b373ef7ca09f56fb600.json: Language not supported
  • tui/.sqlx/query-37e41ab1a55228c51e3ba8686b18218c33c90fd2758cef54bc75c62e72b71b18.json: Language not supported
  • tui/.sqlx/query-38d74f1e800e5016e6294a869b6c822ac8906e2072513e859ca68093f1dd2587.json: Language not supported
  • tui/.sqlx/query-413de3d76a0cca0b6842befe82f485c7370d4746b22bcb6b0287f2b0c0e2242c.json: Language not supported
  • tui/.sqlx/query-4dc3f08975fae225e5a0cb397d5e7c01d9c24e9f296d10efeb9d71fe2686be8c.json: Language not supported
  • tui/.sqlx/query-509a7057b0ef3f0a05fcb31fd790e29186ad267636f177a658d9621538ed99b4.json: Language not supported
  • tui/.sqlx/query-5edb89eb6a86f53cee05322b23e15d7dbf20143d4fe675c13bf991153c6e7100.json: Language not supported
  • tui/.sqlx/query-76f4a0664fb71fb84ede551a9359145520ddbbf9e558d7cc94ee96dabab0c774.json: Language not supported
  • tui/.sqlx/query-78ea78e125e1136a3b3ede153577e3d6ba7ac1d234c0a7d3bbe413df71d7cd9e.json: Language not supported
  • tui/.sqlx/query-93bba2459ec72364def23751398693acf2398a3cd6c4da1f27d4814e589aa5eb.json: Language not supported
  • tui/.sqlx/query-a79f07ab1088ea7f5e2af520ff12db9bf3607632c0a60e7bed841ef077614174.json: Language not supported
  • tui/.sqlx/query-ad4394062829cec6b5b2e39c1f32cbf076c3ea3a2f477b1cd45a56a98a8b0930.json: Language not supported
  • tui/.sqlx/query-af1615c7e7c64fc5f68da920f7aecfe7d8c236f35dd373bc5a0cfcaee60304f2.json: Language not supported
  • tui/.sqlx/query-b3c4ee505669a6bef7a2c51733aec004982e3999b531a22fdaaf96c46dbca7b5.json: Language not supported
  • tui/.sqlx/query-bbac0eb64d127f5ab4c5f2153e277e50cab3ee738ea2fb01636583722dee9d4f.json: Language not supported
  • tui/.sqlx/query-be142a62e92a4222c91b3c850eb61082f81836aa192395a82a8a0d164beef5ef.json: Language not supported
  • tui/.sqlx/query-c5dcc0c422ea6870142e0248efa529a2f43b7898237650e8b9ea339175f26531.json: Language not supported
  • tui/.sqlx/query-d7322dd02c8a52308b661f1a8f0e5092a2a5209bf88c08c211e270e88caef151.json: Language not supported
  • tui/.sqlx/query-efeffb6d175ad90a9fe69a5c2141b494f6da304ad7b2c3441a23b17855c2142e.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tui/src/views/manage.rs Outdated
Addresses Copilot review on #4. Lines 124-134 were leftover scaffolding from
the bank-list commit: an `if let Some(b) = banks.get(i) { let _ = b; }` dance
plus `let _ = Style::default;` whose only job was silencing the unused-Style
import warning. Removed both no-op blocks and the unused import — the build is
genuinely warning-free without the workaround. If the selected-bank detail
strip lands in v0.2 it'll come with real rendering, not a placeholder.
@witt3rd
witt3rd merged commit edefe3e into main Jun 2, 2026
1 check passed
@witt3rd
witt3rd deleted the tui-v0.1 branch June 2, 2026 03:21
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.

3 participants