Private
Public Access
0
0
Files
manual_slop/docs/superpowers/specs/2026-06-03-ui-polish-design.md
T

16 KiB
Raw Blame History

UI Polish Track — Design Spec

Date: 2026-06-03 Status: Approved for implementation Author: Tier 2 Tech Lead Scope: Five discrete UI quality-of-life fixes identified during user review of the GUI. Related tracks: gui_2_cleanup_20260513, selectable_ui_text_20260308, context_comp_decouple_20260510.


1. Problem Statement

User review surfaced five independent UI defects / quality issues that have been outstanding for some time and that previous agent attempts have not been able to fully resolve. This spec decomposes them into five phases of a single track. Each phase is independently shippable and produces visible improvement.

# Severity Issue Prior attempts
1 High Markdown tables in imgui_md are rendered as run-on text — column boundaries lost, headers indistinguishable from body rows. Multiple agents reported attempted fixes; the underlying limitation is that imgui-bundle's imgui_md does not implement GFM tables.
2 High The Keep Pairs numeric input next to Truncate in the discussion panel is clipped to 80 px — single-digit values are visibly truncated. One-line width fix.
3 High The Refresh Registry button in Log Management instantiates a new LogRegistry but does not call .load_registry(), so the displayed table never reflects on-disk state. One-line fix.
4 Medium Operations Hub has no per-vendor session state view (quota, context-window usage, last-error class). Existing "Usage Analytics" tab shows historical aggregates only. New panel.
5 Medium Files & Media > Files shows a flat sorted table; the user wants directory-grouped collapsible tree nodes matching the Context Composition visual style. Refactor mirroring render_context_files_table pattern.

2. Architecture Overview

All five phases target the rendering layer only. No new infrastructure, no threading changes, no provider API changes. The track stays inside the ImGui immediate-mode model and the existing markdown_helper.py / aggregate.py / log_registry.py modules.

2.1 Phase Boundaries

Phase 1 ─ Markdown table pre-processor
Phase 2 ─ Discussion Truncate / Keep Pairs layout fix
Phase 3 ─ Log Management refresh bug fix
Phase 4 ─ Operations Hub > Vendor State panel
Phase 5 ─ Files & Media > Files directory tree

Each phase is one track-level task with its own Red/Green/Commit cycle. Phase 1 is the only one with non-trivial complexity (it introduces a new sub-module). Phases 2, 3, 5 are surgical. Phase 4 adds a new sub-component.

2.2 Shared Conventions

  • All Python code uses 1-space indentation (per conductor/code_styleguides/python.md).
  • No new comments in source files (per AGENTS.md).
  • All new render functions live at module level in src/gui_2.py and follow the (app: App) -> None signature, with a thin _render_vendor_state(self) wrapper on App if they need to be hot-reload-able.
  • SDM dependency tags required on all new public functions.
  • Tests live in tests/test_<module>.py and follow the existing live_gui fixture pattern when they need real GUI state.
  • Branch coverage target: ≥ 80 % for new code; full regression for the touched panel.

3. Per-Phase Design

Phase 1 — Markdown Table Pre-Processor

Problem: imgui-bundle's imgui_md (vendored as src/imgui_md) does not implement GFM table syntax. Lines like:

| Name  | Type |
|-------|------|
| foo   | int  |

render as a single run of | characters with no column alignment, no header/body distinction, and no border. The user has reported this as a recurring frustration for months.

Why previous fixes failed: Attempts to monkey-patch imgui_md or to convert tables to ASCII pre-formatted blocks lose interactivity (links inside cells, syntax highlighting of code cells) and look bad in high-density themes like NERV.

Approach: Insert a GFM-table interceptor into MarkdownRenderer.render() that runs before imgui_md.render(). The interceptor:

  1. Detects table blocks via the GFM signature: a line of |-delimited cells followed by a separator line containing only |, -, :, and spaces.
  2. Computes the natural width of each column by measuring rendered text (using imgui.calc_text_size) — this is what makes it "data-oriented" rather than string-length-based.
  3. Renders the table via imgui.begin_table() with one column per logical column, headers via imgui.table_headers_row(), and body rows via imgui.table_next_row() / imgui.table_set_column_index().
  4. Recursively delegates any markdown inside cell content (links, emphasis, inline code) to imgui_md.render() per cell.
  5. Falls back to the existing imgui_md.render() pass if a block is detected as table-shaped but fails sanity checks (e.g. zero columns, separator with no body).

Module boundary: New module src/markdown_table.py exports a single function render_markdown_tables(text: str) -> str that returns the text with tables replaced by an inert placeholder (\x00TABLE_<idx>\x00), plus a list of table specs. The actual rendering stays inside MarkdownRenderer because it needs an ImGui context.

Files touched:

  • New: src/markdown_table.py — pure parser, no ImGui imports.
  • Modify: src/markdown_helper.pyMarkdownRenderer.render intercepts and dispatches.
  • New tests: tests/test_markdown_table.py (parser unit tests), tests/test_markdown_table_render.py (live_gui render tests).

Safety: The interceptor only activates when a block matches the GFM table shape; everything else passes through unchanged. The placeholder scheme is robust to nested imgui_md quirks because we substitute placeholder after imgui_md has finished its pass.

Acceptance criteria:

  • A representative 4-column, 3-row table renders with aligned borders, bold header, and proper column widths.
  • Tables inside code fences (```) are NOT touched.
  • Inline code / links inside cells still render correctly.
  • Performance: rendering a 5-table, 50-row document is < 16 ms (one frame budget).
  • Existing markdown tests still pass.

Phase 2 — Truncate / Keep Pairs Input Width

Problem: src/gui_2.py:3829:

imgui.text("Keep Pairs:"); imgui.same_line(); imgui.set_next_item_width(80)
ch, app.ui_disc_truncate_pairs = imgui.input_int("##trunc_pairs", app.ui_disc_truncate_pairs, 1)

The width of 80 px is too narrow for a 23 digit number. Image evidence: the digit 2 is partially cut off on the right border.

Fix: Increase set_next_item_width from 80 to 140 (matches the width of the adjacent Truncate button + spacing). Additionally, switch from imgui.input_int to imgui.drag_int for the same field — this gives the user the +/- stepper buttons inline without needing the same_line button pair, and prevents the digit-clipping behavior of input_int when the value approaches the field width.

Files touched:

  • Modify: src/gui_2.py — single line at 3829 plus the surrounding same_line() chain.

Acceptance criteria:

  • 3-digit values (999) render fully inside the input.
  • The Truncate button remains clickable and aligned on the same row.
  • ui_disc_truncate_pairs still floors at 1.

Phase 3 — Log Management Refresh Registry Bug

Problem: src/gui_2.py:1675:

if imgui.button("Refresh Registry"):
    app._log_registry = log_registry.LogRegistry(str(paths.get_logs_dir() / "log_registry.toml"))

The new LogRegistry instance is constructed (which opens the file) but .load_registry() is never called, so app._log_registry.data stays empty. The user sees the same stale table.

Fix: Two acceptable shapes:

A — In-place reload (preferred):

if imgui.button("Refresh Registry"):
    if app._log_registry is not None:
        app._log_registry.load_registry()

B — Re-instantiate + load (defensive):

if imgui.button("Refresh Registry"):
    app._log_registry = log_registry.LogRegistry(str(paths.get_logs_dir() / "log_registry.toml"))
    app._log_registry.load_registry()

We pick A because it preserves any in-memory state (e.g. a pending update_session_metadata call) and is what the user likely meant. We add load_registry to LogRegistry's public API (it already exists privately at lines 75-103 — we just stop reading the wrong attribute).

Files touched:

  • Modify: src/gui_2.py — single line at 1675.
  • New test: tests/test_log_management_refresh.py — drives a temp registry, calls the button via live_gui or via direct function call, asserts table count increases.

Acceptance criteria:

  • Pressing the button updates the visible table when the on-disk TOML has changed.
  • No regression to existing log_management tests.

Phase 4 — Operations Hub > Vendor State Panel

Problem: The current Operations Hub has tabs for Comms / Tool Calls / Usage Analytics / External Tools / Workspace Layouts. There is no at-a-glance view of "what is the current vendor's session state?" — things like:

  • Current provider + model.
  • Context-window utilization (used / limit, percentage bar).
  • Cache hit rate for the active session.
  • Last error class (if any).
  • Quota state (when the vendor exposes it; Anthropic and Gemini CLI expose different signals).

Approach: Add a new tab Vendor State between Usage Analytics and External Tools. The tab shows a single high-density imgui.begin_table() with one row per tracked metric. The metrics are pulled from a new module-level helper get_vendor_state(app: App) -> list[VendorMetric] that aggregates from:

  • app.current_provider, app.current_model (from models.PROVIDERS).
  • app.controller.token_tracker for used / limit / cache stats.
  • app.controller.last_error for error class.
  • A new app.controller.vendor_quota (lazy-loaded dict) populated by ai_client when the provider returns a quota-bearing response.

Component shape:

@dataclass(frozen=True)
class VendorMetric:
    key: str         # e.g. "context_window"
    label: str       # e.g. "Context Window"
    value: str       # e.g. "78,234 / 200,000 (39%)"
    state: str       # "ok" | "warn" | "error" | "info"
    tooltip: str     # long-form explanation, shown on hover

The new tab is a thin renderer:

def render_vendor_state(app: App) -> None:
    metrics = get_vendor_state(app)
    if imgui.begin_table("vendor_state", 3, imgui.TableFlags_.row_bg | imgui.TableFlags_.borders):
        imgui.table_setup_column("Metric", imgui.TableColumnFlags_.width_fixed, 180)
        imgui.table_setup_column("Value", imgui.TableColumnFlags_.width_stretch)
        imgui.table_setup_column("State", imgui.TableColumnFlags_.width_fixed, 60)
        imgui.table_headers_row()
        for m in metrics:
            ...
            if imgui.is_item_hovered(): imgui.set_tooltip(m.tooltip)

Files touched:

  • New: src/vendor_state.py — pure aggregator, no ImGui imports.
  • Modify: src/gui_2.py — add render_vendor_state, new tab in render_operations_hub.
  • Modify: src/app_controller.py — add vendor_quota: dict[str, Any] = field(default_factory=dict) and a set_vendor_quota(provider, payload) callback.
  • Modify: src/ai_client.py — call set_vendor_quota on quota-bearing responses (Anthropic usage blocks, Gemini metadata.tokenInfo, DeepSeek rate-limit headers).
  • New tests: tests/test_vendor_state.py (aggregator logic), tests/test_vendor_state_render.py (live_gui rendering).

Acceptance criteria:

  • Tab appears and is the default-open tab when Operations Hub is opened.
  • All four metric categories (provider/model, context window, cache, last error, quota) render with stable keys.
  • Missing data renders as (em dash), not as a crash.
  • No regression in live_gui test suite.

Phase 5 — Files & Media > Files Directory Tree

Problem: src/gui_2.py:2689 render_files_and_media renders app.files as a flat 3-column table (Act / Path / Status), sorted alphabetically. This is hard to scan for a user with 50+ files. The user wants the directory-grouped, collapsible tree node style used in render_context_files_table (lines 3111-3260).

Approach: Reuse the existing aggregate.group_files_by_dir() helper. For each directory key returned, render a tree_node_ex(..., default_open) with the directory name as the label, then iterate the files under it as the leaf rows. The 3-column layout (Act / Path / Status) is preserved at the leaf level.

Component shape:

def render_files_and_media(app: App) -> None:
    if imgui.collapsing_header("Files", imgui.TreeNodeFlags_.default_open):
        with imscope.group():
            grouped = aggregate.group_files_by_dir(app.files)
            for dir_name, g_files in sorted(grouped.items()):
                with imscope.tree_node_ex(f"{dir_name}##files_dir", imgui.TreeNodeFlags_.default_open) as is_open:
                    if is_open:
                        # ... existing per-file row logic, with all `i` indices scoped to the directory
            # ... existing "Add Files to Inventory" button

Files touched:

  • Modify: src/gui_2.py:2689-2750 — wrap the inner per-file loop in a directory group loop.
  • New test: tests/test_files_and_media_tree.py — asserts that two files in different directories render with the directory labels visible, and that tree_node open/closed state is preserved across frames.

Acceptance criteria:

  • Two files in the same directory are grouped under one collapsible node.
  • One-file "directories" still render (no special-case).
  • The Act / Path / Status columns still function (Add, Remove, Active / Cached / disabled).
  • No regression in tests/test_gui_fast_render.py::test_render_files_and_media_fast.

4. Cross-Cutting Risks & Mitigations

Risk Mitigation
Phase 1 markdown change regresses existing markdown rendering (communications, log previews, discussion responses). All live_gui tests for those panels must pass before merging Phase 1. Add a snapshot test that hashes a fixed multi-table markdown input.
Phase 4 vendor_quota thread-safety — providers fire callbacks on background threads. The new set_vendor_quota is a pure field write under the controller's existing lock; get_vendor_state reads under the same lock. Document in SDM tag.
Phase 5 changes the row identity for the + and x buttons — indices must be globally unique across all directories, not per-directory. The current code uses i = enumerate(app.files); we preserve the original global index for button IDs by computing it via app.files.index(f_item) once per iteration.
ImGui scope mismatches introduced by the new tree_node_ex blocks. Run scripts/check_imgui_scopes.py after each phase.
Plan exceeds one track's worth of work; some phases might be deferred. Each phase is independently shippable and self-contained; we will checkpoint after each phase rather than at the end.

5. Testing Strategy

  • Unit tests for every new module (markdown_table, vendor_state) — pure parser/aggregator logic, no ImGui context needed.
  • live_gui tests for any change that touches gui_2.py render functions. Use the session-scoped live_gui fixture from tests/conftest.py.
  • Regression — full targeted batch of tests/test_gui_fast_render.py, tests/test_log_management_ui.py, tests/test_markdown_*.py, tests/test_discussion_hub_*.py after each phase.
  • No new unittest.mock.patch on core infrastructure (per the Structural Testing Contract).

6. Rollout

Phases are checkpointed independently:

Phase 1 ─ checkpoint ─ Phase 2 ─ checkpoint ─ Phase 3 ─ checkpoint ─ Phase 4 ─ checkpoint ─ Phase 5 ─ final

Each checkpoint:

  1. Run targeted test batch.
  2. Spawn live_gui in headless mode and confirm a smoke screenshot of the touched panel.
  3. Attach a git note with the verification report.
  4. Update conductor/tracks.md with the phase SHA.

If a phase is approved out-of-order, the others remain independently executable. There are no inter-phase dependencies (Phase 1, 2, 3, 5 do not depend on Phase 4; Phase 4 does not depend on any of them).