Skip to content

✅ fresh

last synced 2026-09-29T21:12:19.681221+00:00 · coverage 100% (tui)

Validation by Beadloom doc_sync — same source as sync-check.

TUI — Interactive Architecture Dashboard ​

Beadloom TUI is a multi-screen terminal application built on Textual (>=0.80) for browsing the architecture graph, monitoring debt, running lint checks, and tracking documentation health -- all from a single keyboard-driven interface.

Prerequisites ​

The TUI requires the optional tui dependency group:

bash
pip install beadloom[tui]
# or
uv tool install beadloom[tui]

This installs textual>=0.80 and watchfiles>=0.20.

Launch ​

bash
beadloom tui [--project DIR] [--no-watch]
beadloom ui  [--project DIR] [--no-watch]    # backward-compatible alias
FlagDescription
--project DIRProject root directory (default: current directory)
--no-watchDisable the file watcher (useful for CI or testing)

The TUI opens the SQLite database and initializes 7 data providers before displaying the Dashboard screen. The screen paints immediately; the debt score and git activity are filled in by a background worker (see Startup Responsiveness).

Screens ​

The TUI has three screens, accessible via the 1, 2, and 3 keys. Each screen includes a description label below its header explaining the screen's purpose, and an action bar at the bottom with keybinding hints for available actions.

Dashboard (key: 1) ​

Main overview showing architecture health at a glance.

+------------------------------------------------------+
|  beadloom tui -- ProjectName              Debt: 23 ^  |
+------------------------------------------------------+
|  Architecture overview: graph structure, git ...      |
+------------------------+-----------------------------+
|  Graph Tree            |  Activity + Lint            |
|  (left, 40%)           |  (right, 60%)               |
|                        |                             |
+------------------------+-----------------------------+
|  Node summary bar (selected node info)                |
+------------------------------------------------------+
|  [1]dash [2]explore [3]docs  [r]eindex [q]uit  * ok   |
+------------------------------------------------------+
|  [Enter]explore [r]eindex [l]int [s]ync-check ...     |
+------------------------------------------------------+

Widgets:

  • DebtGaugeWidget -- Debt score with severity coloring (green 0-20, yellow 21-50, red 51+) and direction arrow. Shows Debt: computing… until the background worker reports a score.
  • Screen description -- A label describing the screen purpose ("Architecture overview: graph structure, git activity, lint & debt health").
  • GraphTreeWidget -- Interactive tree built from part_of edges showing the architecture hierarchy. Each node label includes a doc status indicator (green circle = fresh, yellow triangle = stale, red X = missing) and an edge count badge. Nodes are sorted by kind (service > domain > feature) then alphabetically. Selecting a node emits a NodeSelected message that updates the summary bar.
  • ActivityWidget -- Per-domain git activity displayed as colored progress bars (green >=70%, yellow >=30%, dim <30%). Shows Analyzing git history… until the background worker reports results.
  • LintPanelWidget -- Violation counts with severity icons (error, warning, info) and individual violation details (rule name, affected node, description). A row stating how much of its edge set a layer rule judged LEADS the list and renders its MESSAGE, where the numbers are: such a row carries no from_ref_id and the rule description beside it describes the boundary rather than how much of it was looked at, so the panel used to print architecture-layers (?) and the rule's own description (BDL-070 A4). It leads for the reason lint --format github puts its ::notice:: first — the reach of a check is what the findings under it are true of. The header counts are unchanged: a population is counted exactly as every other finding is.
  • StatusBarWidget -- Node count, edge count, doc count, a count of stale doc-code PAIRS rendered as N stale pair(s), watcher status indicator, and last action message. Supports auto-dismissing notifications. It printed a bare N stale until BDL-069 beadloom-rqma.5; the sentence now comes from the shared StaleCount, the same one the s key's notification uses.
  • Action bar -- Keybinding hints at the bottom of the screen showing available actions: [Enter]explore, [r]eindex, [l]int, [s]ync-check, [S]napshot, [?]help.

Explorer (key: 2) ​

Deep-dive into a selected architecture node.

+------------------------------------------------------+
|  Explorer: context-oracle                             |
+------------------------------------------------------+
|  Node deep-dive: detail, dependencies, context bundle |
+------------------------+-----------------------------+
|  Node Detail           |  Dependencies / Context     |
|  (connections, symbols)|  (why tree or ctx preview)  |
|                        |                             |
+------------------------+-----------------------------+
|  [u]pstream [d]ownstream [c]ontext [o]pen  [Esc]back  |
+------------------------------------------------------+

The Explorer screen loads the node selected on the Dashboard (tracked via NodeSelected messages at the app level). You can also navigate directly via BeadloomApp.open_explorer(ref_id).

Widgets:

  • Screen description -- A label describing the screen purpose ("Node deep-dive: detail, dependencies, context bundle").
  • NodeDetailPanel -- Shows ref_id, kind, summary, source path, a Connections summary (outgoing/incoming edge counts grouped by edge kind), a Symbols list (top-level functions and classes from the code indexer with kind glyphs and line numbers), and documentation status (documented or missing).
  • DependencyPathWidget -- Renders upstream or downstream dependency trees with connectors, edge types, and an impact summary (direct/transitive counts, and a Stale pairs: row rendering the same count beadloom why prints, labelled from the noun that count carries). Toggle between upstream and downstream views.
  • ContextPreviewWidget -- Shows the context bundle for a node with estimated token count, character length, bundle keys, and the full bundle content. The widget supports vertical scrolling via overflow-y: auto for large context bundles.

Doc Status (key: 3) ​

Documentation health overview with per-node status tracking.

+------------------------------------------------------+
|  Documentation Health -- 73% covered, 4 stale         |
+------------------------------------------------------+
|  Documentation health: coverage, freshness, ...       |
+------------------------------------------------------+
|  Node              Status    Doc Path     Reason      |
|  context-oracle    * fresh   README.md    --          |
|  graph             ^ stale   README.md    symbols     |
|  search            x missing --           --          |
+------------------------------------------------------+
|  [g]enerate  [p]olish  [Esc]back                      |
+------------------------------------------------------+

Widgets:

  • Screen description -- A label describing the screen purpose ("Documentation health: coverage, freshness, staleness reasons").
  • DocHealthTable -- DataTable with columns: Node, Status (indicator + label), Doc Path, Reason. Rows are sorted: stale first, then missing, then fresh. Color-coded by status. Supports row selection for generate/polish actions.
  • Action bar -- Keybinding hints at the bottom showing available actions: [g]enerate, [p]olish, [Esc]back.

Keyboard Bindings ​

Global (available on all screens) ​

KeyAction
1Switch to Dashboard
2Switch to Explorer
3Switch to Doc Status
TabCycle panel focus
qQuit
?Help overlay (keybinding reference)
/Search overlay (FTS5 search)
rTrigger reindex (runs incremental_reindex, refreshes providers -- debt and activity on a background thread)
lRun lint check (shows violation count notification)
sRun sync-check (notification Sync: N stale pair(s) — a count of doc-code pairs, as sync-check counts them)
SSave snapshot (placeholder)

Dashboard ​

KeyAction
EnterExpand/collapse tree node or open detail

Explorer ​

KeyAction
dShow downstream dependents
uShow upstream dependencies
cShow context bundle preview
oOpen primary source file in $EDITOR
EscReturn to previous screen

Doc Status ​

KeyAction
gGenerate doc skeleton for selected node
pView polish data for selected node
EscReturn to previous screen

Overlays ​

Search Overlay (/) ​

Modal screen for FTS5 full-text search across architecture nodes. When the search_index table is populated, search uses FTS5; otherwise it falls back to SQL LIKE matching on the nodes table. Results are displayed as a numbered list with kind, ref_id, and snippet. Press Enter to search, Enter again to navigate to the first result, or Esc to dismiss.

Help Overlay (?) ​

Modal screen showing all keybindings organized by context (Global, Dashboard, Explorer, Doc Status). Dismissed with Esc.

File Watcher ​

The TUI includes a background file watcher powered by watchfiles (optional dependency). It monitors:

  • The graph YAML directory (.beadloom/_graph/)
  • All source directories discovered from GraphDataProvider.get_source_paths()

Behavior:

  • 500ms debounce window to avoid event spam
  • Filters by watched extensions: .py, .yml, .yaml, .md, .ts, .tsx, .js, .jsx, .go, .rs
  • Skips temporary files (~ prefix, .tmp suffix) and hidden directories (except .beadloom)
  • Posts ReindexNeeded message with changed paths to the app
  • Status bar shows "changes detected (N)" badge when files change
  • Pressing r triggers reindex, refreshes providers (debt and activity on a background thread), and clears the badge

Disable: Use --no-watch to run without the file watcher. If watchfiles is not installed, the watcher is disabled gracefully with a log warning.

Architecture ​

Data Flow ​

BeadloomApp
  |-- on_mount: open DBs -> init providers -> install screens -> push Dashboard -> start watcher
  |
  Screens
  |-- DashboardScreen.on_mount -> load graph, lint, status bar; queue debt + activity
  |-- ExplorerScreen.set_ref_id -> load node detail, dependencies, context
  |-- DocStatusScreen.on_mount -> compute doc rows, coverage stats
  |
  Data Providers (thin read-only wrappers)
  |-- GraphDataProvider    -> SQLite: nodes, edges, hierarchy, doc_ref_ids, source_paths
  |-- LintDataProvider     -> rule_engine.load_rules() + evaluate_all()
  |-- SyncDataProvider     -> engine.check_sync()
  |-- DebtDataProvider     -> debt_report.collect_debt_data() + compute_debt_score()
  |-- ActivityDataProvider -> git_activity.analyze_git_activity()
  |-- WhyDataProvider      -> why.analyze_node() (on-demand per ref_id)
  |-- ContextDataProvider  -> builder.build_context() + estimate_tokens() (on-demand)
  |
  DashboardSlowWorker (threaded Textual Worker)
  |-- refreshes DebtDataProvider + ActivityDataProvider
  |-- posts results back via call_from_thread
  |
  FileWatcherWorker (threaded Textual Worker)
  |-- watches source dirs from graph
  |-- debounce 500ms
  |-- posts ReindexNeeded -> StatusBar badge

Startup Responsiveness ​

Textual switches the terminal to the alternate screen buffer as soon as the app starts, so anything the first paint waits on is time the user spends looking at a blank terminal. The Dashboard therefore splits its load in two:

  • On the event loop — the graph tree, lint panel and status bar counts. These are graph-index reads and return in milliseconds.
  • On a background thread — DebtDataProvider and ActivityDataProvider. Debt walks the working tree (via test mapping) and activity shells out to git log; together they can take seconds on a large repository. The gauge shows Debt: computing… and the activity panel Analyzing git history… until the worker delivers real values through call_from_thread.

Pressing r (reindex) follows the same split: BeadloomApp._refresh_providers() refreshes only the event-loop providers, and the Dashboard re-queues the worker for debt and activity.

Because a thread worker cannot be pre-empted, a re-queued slow load can overlap the one still running. The two slow providers therefore use a second SQLite connection opened with check_same_thread=False, and every worker holds BeadloomApp.worker_conn_lock while using it, so only one thread touches that connection at a time. Workers also check BeadloomApp.is_shutting_down before publishing results, and unmount skips closing the worker connection if a thread still holds the lock rather than stalling quit.

Module Structure ​

Each provider accepts sqlite3.Connection and Path (project_root), supports refresh() for reactive updates. Screens follow Textual's Screen pattern with push_screen()/pop_screen() navigation. CSS layout is defined in separate .tcss files per screen.

src/beadloom/tui/
  __init__.py             -- launch() entry point
  app.py                  -- BeadloomApp: screens, bindings, providers, watcher
  data_providers.py       -- 7 data provider classes
  file_watcher.py         -- FileWatcherWorker, ReindexNeeded message
  screens/
    dashboard.py          -- DashboardScreen
    explorer.py           -- ExplorerScreen
    doc_status.py         -- DocStatusScreen
  widgets/
    graph_tree.py          -- GraphTreeWidget + NodeSelected message
    debt_gauge.py          -- DebtGaugeWidget
    lint_panel.py          -- LintPanelWidget
    activity.py            -- ActivityWidget
    status_bar.py          -- StatusBarWidget
    node_detail_panel.py   -- NodeDetailPanel
    dependency_path.py     -- DependencyPathWidget
    context_preview.py     -- ContextPreviewWidget
    doc_health.py          -- DocHealthTable + compute helpers
    help_overlay.py        -- HelpOverlay (ModalScreen)
    search_overlay.py      -- SearchOverlay (ModalScreen)
  styles/
    app.tcss               -- app-level styles
    dashboard.tcss         -- dashboard layout
    explorer.tcss          -- explorer layout
    doc_status.tcss        -- doc status layout

Constraints ​

  • Requires textual>=0.80 optional dependency (beadloom[tui])
  • Read-only database access (except reindex action which writes)
  • No network access -- fully local
  • Data providers are read-only wrappers -- no new DB tables
  • No built-in LLM calls (agent-native principle)

API ​

Module src/beadloom/services/commands/dashboard.py (CLI command layer):

  • _launch_tui(*, project, no_watch) -- shared implementation for tui/ui Click commands; resolves project_root, validates that .beadloom/beadloom.db exists, then calls beadloom.tui.launch(). Exits with an error message if the textual optional dependency is missing or the database has not been created.
  • tui(*, project, no_watch) -- Click command registered on the root CLI group; launches the interactive terminal dashboard.
  • ui(*, project, no_watch) -- Click command alias for tui; identical behaviour, preserved for backward compatibility.

Module src/beadloom/tui/__init__.py:

  • launch(db_path, project_root, *, no_watch=False) -- entry point

Module src/beadloom/tui/app.py:

  • BeadloomApp(db_path, project_root, *, no_watch=False) -- main Textual App

Module src/beadloom/tui/data_providers.py:

  • GraphDataProvider -- get_nodes(), get_edges(), get_node(ref_id), get_node_with_source(ref_id), get_hierarchy(), get_edge_counts(), get_doc_ref_ids(), get_source_paths(), get_symbols(ref_id)
  • LintDataProvider -- get_violations(), get_violation_count(). Each row carries rule_name, rule_type, severity, from_ref_id, to_ref_id, description and message. This provider is one of the two surfaces in the product that call evaluate_all without ever building a LintResult, so a finding is the only thing that reaches it — and the layer rule states its population IN the message. rule_type and message were both dropped before BDL-070 A4, which is why the numbers never reached the screen
  • SyncDataProvider -- get_sync_results(), get_stale_count(), get_coverage(). Since BDL-061 S4b it passes the project's resolved document-section requirements into check_sync, so the dashboard sees the incomplete document-shape rows the CI gate sees rather than a quieter view of the same project
  • DebtDataProvider -- get_debt_report(), get_score()
  • ActivityDataProvider -- get_activity(); reads git activity through application/graph_reads.analyze_git_activity, never infrastructure.git_activity directly — the tui-no-direct-infra boundary forbids it, and since BDL-UX #172 the rule can actually say so
  • WhyDataProvider -- analyze(ref_id, reverse=False)
  • ContextDataProvider -- get_context(ref_id), estimate_tokens(text)

Module src/beadloom/tui/file_watcher.py:

  • start_file_watcher(app, project_root, source_paths, *, debounce_ms=500) -- returns Worker or None
  • ReindexNeeded(changed_paths) -- custom Textual message

Testing ​

TUI tests use Textual's headless pilot framework (app.run_test()).

bash
uv run pytest tests/integration/tui/test_tui.py -v

Tests cover all 7 data providers, app shell instantiation, screen switching, CLI commands (tui and ui), all dashboard and explorer widgets, file watcher integration, overlays, keyboard actions, and status bar notifications.