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

278 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.py``MarkdownRenderer.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`:
```python
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`:
```python
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):**
```python
if imgui.button("Refresh Registry"):
if app._log_registry is not None:
app._log_registry.load_registry()
```
**B — Re-instantiate + load (defensive):**
```python
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:**
```python
@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:
```python
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:**
```python
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).