Files
hqt/docs/superpowers/plans/2026-06-09-project-edit-frappe-theme.md
T
felixm 90944948f5 Initial commit: hqt — HQ Terminal TUI
TUI for orchestrating AI coding harness sessions (Claude Code, Codex,
Kiro, etc.) via tmux. Click CLI bootstraps a Textual TUI over
ProjectService/SessionService backed by SQLite, spawning harness
sessions as tmux windows through TmuxManager.

Includes recent fixes:
- Visible Tab focus highlight on dialog OK/Cancel buttons
- Auto-select first project on launch
- Auto-select first session + per-project session-selection memory
- tmux new-window targets an explicit free index, fixing
  "index N in use" failures (broken spawn/attach in attached sessions)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 17:06:57 -04:00

809 lines
25 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.
# Project Editing + Frappé Theme Completion Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Let users edit a project's name and path from the TUI, and make the Catppuccin Frappé theme actually look like Frappé (full theme definition, styled dialogs/headers, colored status symbols).
**Architecture:** `ProjectService` gains an `update` method (unique-path violations surface as `ValueError`). The existing `AddProjectScreen` is generalized into `ProjectFormScreen` with optional pre-filled values, used by both the `a` (add) and new `e` (edit) bindings. The half-specified `FRAPPE_THEME` is replaced with a full definition mirroring Textual's built-in `catppuccin-mocha` structure, plus a `styles.tcss` pass and Rich-markup status colors.
**Tech Stack:** Python 3.12, Textual 8.x, SQLAlchemy 2.x, pytest + pytest-asyncio, `uv` for everything (`uv run pytest ...`).
**Spec:** `docs/superpowers/specs/2026-06-09-project-edit-frappe-theme-design.md`
**Known bug fixed in Task 7:** session labels currently pass `[claude]` to `Label`, which parses it as a Rich markup tag and silently swallows the harness name. Verified by experiment: `Label('x [claude] y')` renders as `'x y'`. The fix escapes the brackets.
---
### Task 1: `ProjectService.update`
**Files:**
- Modify: `src/hqt/projects/service.py`
- Test: `tests/test_services.py`
- [ ] **Step 1: Write the failing tests**
Add to the `TestProjectService` class in `tests/test_services.py`:
```python
def test_update_name_and_path(self, db):
svc = ProjectService(db)
p = svc.create("old", "/old/path")
updated = svc.update(p.id, "new", "/new/path")
assert updated.name == "new"
assert updated.path == "/new/path"
assert svc.get(p.id).path == "/new/path"
def test_update_duplicate_path_raises(self, db):
svc = ProjectService(db)
svc.create("a", "/a")
p2 = svc.create("b", "/b")
with pytest.raises(ValueError, match="already uses path"):
svc.update(p2.id, "b", "/a")
# DB session must remain usable after rollback, values unchanged
assert svc.get(p2.id).path == "/b"
def test_update_unknown_id_raises(self, db):
svc = ProjectService(db)
with pytest.raises(ValueError, match="not found"):
svc.update(9999, "x", "/x")
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `uv run pytest tests/test_services.py -v -k update`
Expected: 3 FAILED with `AttributeError: 'ProjectService' object has no attribute 'update'`
- [ ] **Step 3: Implement `update`**
In `src/hqt/projects/service.py`, add the import at the top:
```python
from sqlalchemy.exc import IntegrityError
```
Add this method to `ProjectService` (after `get`, before `archive`):
```python
def update(self, project_id: int, name: str, path: str) -> Project:
project = self.get(project_id)
if project is None:
raise ValueError(f"Project {project_id} not found")
project.name = name
project.path = path
try:
self.db.commit()
except IntegrityError as err:
self.db.rollback()
raise ValueError(f"Another project already uses path {path}") from err
self.db.refresh(project)
return project
```
- [ ] **Step 4: Run tests to verify they pass**
Run: `uv run pytest tests/test_services.py -v`
Expected: all PASS (including the pre-existing tests)
- [ ] **Step 5: Commit**
```bash
git add src/hqt/projects/service.py tests/test_services.py
git commit -m "feat: ProjectService.update with duplicate-path ValueError"
```
---
### Task 2: `ProjectFormScreen` (rename `AddProjectScreen`, add pre-fill)
**Files:**
- Rename: `src/hqt/tui/screens/add_project.py``src/hqt/tui/screens/project_form.py`
- Modify: `src/hqt/tui/app.py` (import at line 17, `action_add_project` at lines 107114)
- Test: `tests/test_tui.py`
- [ ] **Step 1: Write the failing tests**
Add to `tests/test_tui.py`:
```python
# ---------------------------------------------------------------------------
# ProjectFormScreen: shared add/edit form
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_project_form_prefills_initial_values():
app = HqtApp()
async with app.run_test(size=(120, 40)) as pilot:
from hqt.tui.screens.project_form import ProjectFormScreen
from textual.widgets import Input
app.push_screen(
ProjectFormScreen(
title="Edit Project",
initial_name="myproj",
initial_path="/tmp/myproj",
)
)
await pilot.pause()
assert app.screen.query_one("#name-input", Input).value == "myproj"
assert app.screen.query_one("#path-input", Input).value == "/tmp/myproj"
@pytest.mark.asyncio
async def test_project_form_add_mode_defaults_name_to_basename():
app = HqtApp()
async with app.run_test(size=(120, 40)) as pilot:
from hqt.tui.screens.project_form import ProjectFormScreen
from textual.widgets import Button, Input
results = []
app.push_screen(ProjectFormScreen(), results.append)
await pilot.pause()
app.screen.query_one("#path-input", Input).value = "/tmp/somerepo"
app.screen.query_one("#ok-btn", Button).press()
await pilot.pause()
assert results == [("somerepo", "/tmp/somerepo")]
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `uv run pytest tests/test_tui.py -v -k project_form`
Expected: 2 FAILED with `ModuleNotFoundError: No module named 'hqt.tui.screens.project_form'`
- [ ] **Step 3: Rename the file and generalize the class**
```bash
git mv src/hqt/tui/screens/add_project.py src/hqt/tui/screens/project_form.py
```
Replace the entire contents of `src/hqt/tui/screens/project_form.py` with:
```python
from pathlib import Path
from textual.app import ComposeResult
from textual.containers import Vertical
from textual.screen import ModalScreen
from textual.widgets import Button, Input, Label
class ProjectFormScreen(ModalScreen[tuple[str, str] | None]):
def __init__(
self,
title: str = "Add Project",
initial_name: str = "",
initial_path: str = "",
) -> None:
self._form_title = title
self._initial_name = initial_name
self._initial_path = initial_path
super().__init__()
def compose(self) -> ComposeResult:
with Vertical(id="project-form-dialog"):
yield Label(self._form_title)
yield Label("Path:")
yield Input(
value=self._initial_path,
placeholder="/path/to/project",
id="path-input",
)
yield Label("Name (optional):")
yield Input(
value=self._initial_name,
placeholder="project name",
id="name-input",
)
yield Button("OK", variant="primary", id="ok-btn")
yield Button("Cancel", id="cancel-btn")
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "ok-btn":
path = self.query_one("#path-input", Input).value.strip()
name = self.query_one("#name-input", Input).value.strip()
if path:
if not name:
name = Path(path).name
self.dismiss((name, path))
else:
self.dismiss(None)
else:
self.dismiss(None)
```
- [ ] **Step 4: Update `app.py` to use the renamed screen**
In `src/hqt/tui/app.py`, replace the import:
```python
from hqt.tui.screens.add_project import AddProjectScreen
```
with:
```python
from hqt.tui.screens.project_form import ProjectFormScreen
```
and in `action_add_project`, replace:
```python
self.push_screen(AddProjectScreen(), on_dismiss)
```
with:
```python
self.push_screen(ProjectFormScreen(), on_dismiss)
```
- [ ] **Step 5: Run the full TUI test file**
Run: `uv run pytest tests/test_tui.py -v`
Expected: all PASS
- [ ] **Step 6: Commit**
```bash
git add -A src/hqt/tui tests/test_tui.py
git commit -m "refactor: generalize AddProjectScreen into ProjectFormScreen with pre-fill"
```
---
### Task 3: `ProjectList.get_selected_project_id`
**Files:**
- Modify: `src/hqt/tui/widgets/project_list.py`
- Test: `tests/test_tui.py`
- [ ] **Step 1: Write the failing test**
Add to `tests/test_tui.py`:
```python
@pytest.mark.asyncio
async def test_project_list_get_selected_project_id():
app = HqtApp()
async with app.run_test(size=(120, 40)) as pilot:
from hqt.tui.widgets.project_list import ProjectList
p = app._project_service.create("selproj", "/tmp/selproj")
app._load_projects()
await pilot.pause()
pl = app.query_one(ProjectList)
lv = pl.query_one("#project-list")
lv.focus()
await pilot.press("down")
await pilot.pause()
assert pl.get_selected_project_id() == p.id
```
- [ ] **Step 2: Run test to verify it fails**
Run: `uv run pytest tests/test_tui.py::test_project_list_get_selected_project_id -v`
Expected: FAIL with `AttributeError: 'ProjectList' object has no attribute 'get_selected_project_id'`
- [ ] **Step 3: Implement the method**
Add to `ProjectList` in `src/hqt/tui/widgets/project_list.py` (same pattern as `SessionList.get_selected_session_id`):
```python
def get_selected_project_id(self) -> int | None:
lv = self.query_one("#project-list", ListView)
if lv.highlighted_child and hasattr(lv.highlighted_child, "data"):
return lv.highlighted_child.data
return None
```
- [ ] **Step 4: Run test to verify it passes**
Run: `uv run pytest tests/test_tui.py::test_project_list_get_selected_project_id -v`
Expected: PASS
- [ ] **Step 5: Commit**
```bash
git add src/hqt/tui/widgets/project_list.py tests/test_tui.py
git commit -m "feat: ProjectList.get_selected_project_id"
```
---
### Task 4: `e` binding + `action_edit_project`
**Files:**
- Modify: `src/hqt/tui/app.py` (BINDINGS at lines 4251, new action after `action_add_project`)
- Test: `tests/test_tui.py`
- [ ] **Step 1: Write the failing tests**
Add to `tests/test_tui.py`:
```python
# ---------------------------------------------------------------------------
# Edit project: binding + action
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_edit_project_binding_exists():
bindings = {b.key: b for b in HqtApp.BINDINGS}
assert "e" in bindings
assert bindings["e"].action == "edit_project"
@pytest.mark.asyncio
async def test_edit_project_no_selection_warns():
app = HqtApp()
async with app.run_test(size=(120, 40)) as pilot:
from unittest.mock import patch
notify_calls = []
with patch.object(
app, "notify", side_effect=lambda msg, **kw: notify_calls.append((msg, kw))
):
await app.run_action("edit_project")
await pilot.pause()
assert any(
kw.get("severity") == "warning" for _, kw in notify_calls
), f"Expected warning notification, got: {notify_calls}"
@pytest.mark.asyncio
async def test_edit_project_opens_prefilled_form_and_updates():
app = HqtApp()
async with app.run_test(size=(120, 40)) as pilot:
from textual.widgets import Input
from hqt.tui.screens.project_form import ProjectFormScreen
from hqt.tui.widgets.project_list import ProjectList
p = app._project_service.create("oldname", "/tmp/oldpath")
app._load_projects()
await pilot.pause()
pl = app.query_one(ProjectList)
pl.query_one("#project-list").focus()
await pilot.press("down")
await pilot.pause()
assert pl.get_selected_project_id() == p.id
await app.run_action("edit_project")
await pilot.pause()
assert isinstance(app.screen, ProjectFormScreen)
assert app.screen.query_one("#name-input", Input).value == "oldname"
assert app.screen.query_one("#path-input", Input).value == "/tmp/oldpath"
app.screen.dismiss(("newname", "/tmp/newpath"))
await pilot.pause()
refreshed = app._project_service.get(p.id)
assert refreshed.name == "newname"
assert refreshed.path == "/tmp/newpath"
@pytest.mark.asyncio
async def test_edit_project_duplicate_path_notifies_error():
app = HqtApp()
async with app.run_test(size=(120, 40)) as pilot:
from unittest.mock import patch
from hqt.tui.widgets.project_list import ProjectList
app._project_service.create("first", "/tmp/first")
p2 = app._project_service.create("second", "/tmp/second")
app._load_projects()
await pilot.pause()
pl = app.query_one(ProjectList)
pl.query_one("#project-list").focus()
# Two items: press down twice to land on the second project
await pilot.press("down")
await pilot.press("down")
await pilot.pause()
assert pl.get_selected_project_id() == p2.id
notify_calls = []
with patch.object(
app, "notify", side_effect=lambda msg, **kw: notify_calls.append((msg, kw))
):
await app.run_action("edit_project")
await pilot.pause()
app.screen.dismiss(("second", "/tmp/first"))
await pilot.pause()
assert any(
kw.get("severity") == "error" and "already uses path" in msg
for msg, kw in notify_calls
), f"Expected duplicate-path error notification, got: {notify_calls}"
# DB unchanged
assert app._project_service.get(p2.id).path == "/tmp/second"
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `uv run pytest tests/test_tui.py -v -k edit_project`
Expected: 4 FAILED (`"e" not in bindings`; the action-based tests fail because the action doesn't exist)
- [ ] **Step 3: Implement binding and action**
In `src/hqt/tui/app.py`, add to `BINDINGS` (after the `"a"` binding):
```python
Binding("e", "edit_project", "Edit Project"),
```
Add this method after `action_add_project`:
```python
def action_edit_project(self) -> None:
project_id = self.query_one(ProjectList).get_selected_project_id()
if project_id is None:
self.notify("Select a project first", severity="warning")
return
project = self._project_service.get(project_id)
if project is None:
self.notify("Project not found", severity="error")
return
def on_dismiss(result: tuple[str, str] | None) -> None:
if result:
name, path = result
try:
self._project_service.update(project_id, name, path)
except ValueError as err:
self.notify(str(err), severity="error")
return
self._load_projects()
self.push_screen(
ProjectFormScreen(
title="Edit Project",
initial_name=project.name,
initial_path=project.path,
),
on_dismiss,
)
```
- [ ] **Step 4: Run tests to verify they pass**
Run: `uv run pytest tests/test_tui.py -v`
Expected: all PASS
- [ ] **Step 5: Commit**
```bash
git add src/hqt/tui/app.py tests/test_tui.py
git commit -m "feat: edit project name/path via 'e' binding"
```
---
### Task 5: Complete the Frappé theme definition
**Files:**
- Modify: `src/hqt/tui/app.py:23-36` (`FRAPPE_THEME`)
- Test: `tests/test_tui.py`
- [ ] **Step 1: Write the failing test**
Add to `tests/test_tui.py`:
```python
# ---------------------------------------------------------------------------
# Catppuccin Frappé theme: full definition
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_frappe_theme_fully_applied():
app = HqtApp()
async with app.run_test() as pilot:
assert app.current_theme.name == "catppuccin-frappe"
variables = app.get_css_variables()
# Spot-check: explicit values, not Textual auto-derivations
assert variables["primary"] == "#8caaee" # Blue
assert variables["background"] == "#292c3c" # Mantle
assert variables["surface"] == "#414559" # Surface0
assert variables["border"] == "#babbf1" # Lavender
assert variables["footer-background"] == "#51576d" # Surface1
assert variables["input-cursor-background"] == "#f2d5cf" # Rosewater
```
- [ ] **Step 2: Run test to verify it fails**
Run: `uv run pytest tests/test_tui.py::test_frappe_theme_fully_applied -v`
Expected: FAIL on `variables["background"] == "#292c3c"` (currently `#303446`)
- [ ] **Step 3: Replace `FRAPPE_THEME`**
In `src/hqt/tui/app.py`, replace the entire `FRAPPE_THEME = Theme(...)` block with:
```python
# Catppuccin Frappé, mirroring the structure of Textual's built-in
# catppuccin-mocha theme so no colors are auto-derived off-palette.
FRAPPE_THEME = Theme(
name="catppuccin-frappe",
primary="#8caaee", # Blue
secondary="#ca9ee6", # Mauve
accent="#ef9f76", # Peach
success="#a6d189", # Green
warning="#e5c890", # Yellow
error="#e78284", # Red
foreground="#c6d0f5", # Text
background="#292c3c", # Mantle
surface="#414559", # Surface0
panel="#51576d", # Surface1
dark=True,
variables={
"input-cursor-foreground": "#232634", # Crust
"input-cursor-background": "#f2d5cf", # Rosewater
"input-selection-background": "#949cbb 30%", # Overlay2 30%
"border": "#babbf1", # Lavender
"border-blurred": "#626880", # Surface2
"footer-background": "#51576d", # Surface1
"block-cursor-foreground": "#303446", # Base
"block-cursor-text-style": "none",
"button-color-foreground": "#292c3c", # Mantle
},
)
```
- [ ] **Step 4: Run tests to verify they pass**
Run: `uv run pytest tests/test_tui.py -v`
Expected: all PASS
- [ ] **Step 5: Commit**
```bash
git add src/hqt/tui/app.py tests/test_tui.py
git commit -m "feat: fully specify Catppuccin Frappé theme variables"
```
---
### Task 6: Style dialogs and panel headers in `styles.tcss`
**Files:**
- Modify: `src/hqt/tui/styles.tcss`
- Test: `tests/test_tui.py`
- [ ] **Step 1: Write the failing test**
Add to `tests/test_tui.py`:
```python
@pytest.mark.asyncio
async def test_project_form_dialog_styled():
app = HqtApp()
async with app.run_test(size=(120, 40)) as pilot:
from hqt.tui.screens.project_form import ProjectFormScreen
app.push_screen(ProjectFormScreen())
await pilot.pause()
dialog = app.screen.query_one("#project-form-dialog")
assert dialog.styles.width.value == 60
assert dialog.styles.border_top[0] == "solid"
```
- [ ] **Step 2: Run test to verify it fails**
Run: `uv run pytest tests/test_tui.py::test_project_form_dialog_styled -v`
Expected: FAIL (width is not 60; no border set)
- [ ] **Step 3: Extend the stylesheet**
Replace the contents of `src/hqt/tui/styles.tcss` with:
```css
ProjectList {
width: 1fr;
min-width: 20;
max-width: 30;
dock: left;
border-right: solid $primary;
}
SessionList {
width: 3fr;
}
#project-header, #session-header {
width: 100%;
text-style: bold;
background: $surface;
}
ProjectFormScreen, NewSessionScreen {
align: center middle;
}
#project-form-dialog, #new-session-dialog {
width: 60;
height: auto;
padding: 1 2;
background: $surface;
border: solid $border;
}
```
- [ ] **Step 4: Run tests to verify they pass**
Run: `uv run pytest tests/test_tui.py -v`
Expected: all PASS
- [ ] **Step 5: Commit**
```bash
git add src/hqt/tui/styles.tcss tests/test_tui.py
git commit -m "feat: style modal dialogs and panel headers with Frappé palette"
```
---
### Task 7: Colored status symbols + harness-name markup bug fix
**Files:**
- Modify: `src/hqt/tui/widgets/session_list.py`
- Test: `tests/test_tui.py`
Context: labels are Rich markup. The current f-string interpolates `[{s.harness.name}]`, which Rich parses as a markup tag and **silently drops** — the harness name is invisible in the running app today. The new formatting helper escapes user-derived text and adds a Frappé color tag around the status symbol.
- [ ] **Step 1: Write the failing tests**
Add to `tests/test_tui.py`:
```python
# ---------------------------------------------------------------------------
# Status symbol colors + harness-name markup escape
# ---------------------------------------------------------------------------
def test_format_session_text_colors_and_escapes():
from hqt.tui.widgets.session_list import format_session_text
text = format_session_text("mywork", "claude", "working", "")
assert text.startswith("[#a6d189]◐[/]") # Green symbol
assert "\\[claude]" in text # escaped, so brackets render
assert "working" in text
def test_format_session_text_status_colors():
from hqt.tui.widgets.session_list import format_session_text
assert format_session_text("x", "h", "waiting", "").startswith("[#e5c890]") # Yellow
assert format_session_text("x", "h", "idle", "").startswith("[#81c8be]") # Teal
assert format_session_text("x", "h", "active", "").startswith("[#81c8be]") # Teal
assert format_session_text("x", "h", "dead", "").startswith("[#737994]") # Overlay0
@pytest.mark.asyncio
async def test_session_list_renders_harness_name_brackets():
"""Regression: '[claude]' must be visible, not swallowed as a markup tag."""
app = HqtApp()
async with app.run_test(size=(120, 40)) as pilot:
from textual.widgets import Label
from hqt.tui.widgets.session_list import SessionList
sl = app.query_one(SessionList)
infos = [_make_session_info(1, "mywork", "claude", "working", True)]
await sl.refresh_sessions(infos)
await pilot.pause()
lv = sl.query_one("#session-list")
texts = [str(child.query_one(Label).render()) for child in lv.children]
assert any("[claude]" in t for t in texts), f"harness name missing: {texts}"
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `uv run pytest tests/test_tui.py -v -k "format_session_text or renders_harness"`
Expected: 2 FAILED with `ImportError: cannot import name 'format_session_text'`, 1 FAILED on the brackets assertion
- [ ] **Step 3: Implement the formatting helper and use it**
In `src/hqt/tui/widgets/session_list.py`, add at the top:
```python
from rich.markup import escape
```
Add at module level (above the `SessionList` class, next to where `_STATUS_SYMBOLS` will remain inside the class):
```python
_STATUS_COLORS: dict[str, str] = {
"working": "#a6d189", # Green
"waiting": "#e5c890", # Yellow
"active": "#81c8be", # Teal
"idle": "#81c8be", # Teal
"dead": "#737994", # Overlay0
}
def format_session_text(
nickname: str, harness_name: str, status_text: str, symbol: str
) -> str:
color = _STATUS_COLORS.get(status_text, "#c6d0f5") # default: Text
label = escape(f"{nickname} [{harness_name}]")
return f"[{color}]{symbol}[/] {label} {status_text}"
```
In `SessionList.refresh_sessions`, replace:
```python
text = f"{symbol} {s.nickname or s.tmux_session_name} [{s.harness.name}] {status_text}"
```
with:
```python
text = format_session_text(
s.nickname or s.tmux_session_name, s.harness.name, status_text, symbol
)
```
- [ ] **Step 4: Run the full TUI test file**
Run: `uv run pytest tests/test_tui.py -v`
Expected: all PASS — including the pre-existing `test_session_list_label_includes_status`, which reads plain rendered text and is unaffected by markup
- [ ] **Step 5: Commit**
```bash
git add src/hqt/tui/widgets/session_list.py tests/test_tui.py
git commit -m "feat: Frappé-colored status symbols; fix harness name swallowed by markup"
```
---
### Task 8: Full suite + visual verification
**Files:**
- None modified (verification only)
- [ ] **Step 1: Run the entire test suite**
Run: `uv run pytest`
Expected: all tests PASS, no warnings about unknown CSS
- [ ] **Step 2: Render a headless screenshot and inspect it**
```bash
uv run python - <<'EOF'
import asyncio, tempfile
from pathlib import Path
import hqt.config as config
tmp = Path(tempfile.mkdtemp())
config._settings = config.Settings(db_path=tmp/'hqt.db', config_dir=tmp, skills_dir=tmp/'skills')
from hqt.tui.app import HqtApp
async def main():
app = HqtApp()
async with app.run_test(size=(100, 30)) as pilot:
app._project_service.create("demo-project", "/tmp/demo")
app._load_projects()
await pilot.pause()
Path('/tmp/hqt_themed.svg').write_text(app.export_screenshot())
asyncio.run(main())
EOF
rsvg-convert -o /tmp/hqt_themed.png /tmp/hqt_themed.svg
```
Then view `/tmp/hqt_themed.png` (Read tool or image viewer). Check: Mantle background (`#292c3c`), Surface0 panels, Lavender borders, Blue selection highlight, bold panel headers. Repeat with `app.push_screen(ProjectFormScreen())` before the screenshot to verify the dialog styling.
- [ ] **Step 3: Final commit if any fixes were needed**
If the screenshot revealed fixes, commit them:
```bash
git add -A && git commit -m "fix: theme polish from visual verification"
```