✅ fresh
last synced 2026-09-29T21:12:19.681221+00:00 · coverage 100% (
tui)Validation by Beadloom
doc_sync— same source assync-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:
pip install beadloom[tui]
# or
uv tool install beadloom[tui]This installs textual>=0.80 and watchfiles>=0.20.
Launch
beadloom tui [--project DIR] [--no-watch]
beadloom ui [--project DIR] [--no-watch] # backward-compatible alias| Flag | Description |
|---|---|
--project DIR | Project root directory (default: current directory) |
--no-watch | Disable 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_ofedges 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 aNodeSelectedmessage 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_idand the rule description beside it describes the boundary rather than how much of it was looked at, so the panel used to printarchitecture-layers (?)and the rule's own description (BDL-070 A4). It leads for the reasonlint --format githubputs 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 bareN staleuntil BDL-069beadloom-rqma.5; the sentence now comes from the sharedStaleCount, the same one theskey'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 countbeadloom whyprints, labelled from the noun that count carries). Toggle betweenupstream anddownstream 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: autofor 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)
| Key | Action |
|---|---|
1 | Switch to Dashboard |
2 | Switch to Explorer |
3 | Switch to Doc Status |
Tab | Cycle panel focus |
q | Quit |
? | Help overlay (keybinding reference) |
/ | Search overlay (FTS5 search) |
r | Trigger reindex (runs incremental_reindex, refreshes providers -- debt and activity on a background thread) |
l | Run lint check (shows violation count notification) |
s | Run sync-check (notification Sync: N stale pair(s) — a count of doc-code pairs, as sync-check counts them) |
S | Save snapshot (placeholder) |
Dashboard
| Key | Action |
|---|---|
Enter | Expand/collapse tree node or open detail |
Explorer
| Key | Action |
|---|---|
d | Show downstream dependents |
u | Show upstream dependencies |
c | Show context bundle preview |
o | Open primary source file in $EDITOR |
Esc | Return to previous screen |
Doc Status
| Key | Action |
|---|---|
g | Generate doc skeleton for selected node |
p | View polish data for selected node |
Esc | Return 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,.tmpsuffix) and hidden directories (except.beadloom) - Posts
ReindexNeededmessage with changed paths to the app - Status bar shows "changes detected (N)" badge when files change
- Pressing
rtriggers 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 badgeStartup 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 —
DebtDataProviderandActivityDataProvider. Debt walks the working tree (via test mapping) and activity shells out togit log; together they can take seconds on a large repository. The gauge showsDebt: computing…and the activity panelAnalyzing git history…until the worker delivers real values throughcall_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 layoutConstraints
- Requires
textual>=0.80optional 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 fortui/uiClick commands; resolvesproject_root, validates that.beadloom/beadloom.dbexists, then callsbeadloom.tui.launch(). Exits with an error message if thetextualoptional 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 fortui; 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 carriesrule_name,rule_type,severity,from_ref_id,to_ref_id,descriptionandmessage. This provider is one of the two surfaces in the product that callevaluate_allwithout ever building aLintResult, so a finding is the only thing that reaches it — and the layer rule states its population IN the message.rule_typeandmessagewere both dropped before BDL-070 A4, which is why the numbers never reached the screenSyncDataProvider--get_sync_results(),get_stale_count(),get_coverage(). Since BDL-061 S4b it passes the project's resolved document-section requirements intocheck_sync, so the dashboard sees theincompletedocument-shape rows the CI gate sees rather than a quieter view of the same projectDebtDataProvider--get_debt_report(),get_score()ActivityDataProvider--get_activity(); reads git activity throughapplication/graph_reads.analyze_git_activity, neverinfrastructure.git_activitydirectly — thetui-no-direct-infraboundary forbids it, and since BDL-UX #172 the rule can actually say soWhyDataProvider--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)-- returnsWorkerorNoneReindexNeeded(changed_paths)-- custom Textual message
Testing
TUI tests use Textual's headless pilot framework (app.run_test()).
uv run pytest tests/integration/tui/test_tui.py -vTests 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.