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>
6.3 KiB
Project Editing + Catppuccin Frappé Theme Completion
Date: 2026-06-09 Status: Approved
Problem
- Projects cannot be edited after creation. If a repository moves on disk, the only options are archiving the project or editing the database by hand.
- The TUI registers a Catppuccin Frappé theme (
FRAPPE_THEMEinsrc/hqt/tui/app.py) but only sets the basic semantic fields. Textual auto-derives every other color (footer keys, borders, selection highlights), which drifts off-palette, and the modal dialogs, panel headers, and status symbols are entirely unstyled. The result does not look like Frappé.
Part 1: Project editing
Service layer
Add to ProjectService (src/hqt/projects/service.py):
def update(self, project_id: int, name: str, path: str) -> Project
- Updates both fields and commits.
- Raises
ValueError("Project not found")for an unknown id. - A duplicate path violates the unique constraint on
Project.path: catchIntegrityError, roll back, raiseValueErrorwith a readable message (e.g."Another project already uses path <path>"). - No filesystem validation (consistent with
create, which accepts any string).
Form screen
Rename AddProjectScreen (src/hqt/tui/screens/add_project.py) to
ProjectFormScreen:
- Constructor:
ProjectFormScreen(title="Add Project", initial_name="", initial_path=""). - The dialog heading label shows
title; inputs are pre-filled with the initial values. - Return type unchanged:
tuple[str, str] | Noneof(name, path). - Add-mode behavior unchanged: empty name defaults to
Path(path).name; empty path dismisses withNone.
Project list
Add ProjectList.get_selected_project_id() -> int | None
(src/hqt/tui/widgets/project_list.py), same pattern as
SessionList.get_selected_session_id(): read data off the ListView's
highlighted child.
App wiring
In HqtApp (src/hqt/tui/app.py):
- New binding:
Binding("e", "edit_project", "Edit Project"). action_edit_project:- No project highlighted →
notify("Select a project first", severity="warning")and return. - Otherwise push
ProjectFormScreen(title="Edit Project", initial_name=project.name, initial_path=project.path). - On dismiss with a result: call
ProjectService.update, refresh the project list. CatchValueErrorand show its message as an error notification.
- No project highlighted →
action_add_projectswitches to the renamedProjectFormScreenwith defaults; behavior otherwise unchanged.
Session semantics
Editing a path never touches tmux. Running sessions keep the working directory they were spawned with; only sessions created after the edit use the new path. This matches archive semantics (DB-only operation).
Part 2: Frappé theme completion
Theme definition
Replace FRAPPE_THEME with a fully specified Theme, mirroring the
structure of Textual's built-in catppuccin-mocha theme with Frappé values:
| Field | Frappé color | Hex |
|---|---|---|
primary |
Blue | #8caaee |
secondary |
Mauve | #ca9ee6 |
accent |
Peach | #ef9f76 |
success |
Green | #a6d189 |
warning |
Yellow | #e5c890 |
error |
Red | #e78284 |
foreground |
Text | #c6d0f5 |
background |
Mantle | #292c3c |
surface |
Surface0 | #414559 |
panel |
Surface1 | #51576d |
dark |
— | True |
variables dict (same keys the built-in Mocha theme sets):
| Variable | Frappé color | Value |
|---|---|---|
input-cursor-foreground |
Crust | #232634 |
input-cursor-background |
Rosewater | #f2d5cf |
input-selection-background |
Overlay2 30% | #949cbb 30% |
border |
Lavender | #babbf1 |
border-blurred |
Surface2 | #626880 |
footer-background |
Surface1 | #51576d |
block-cursor-foreground |
Base | #303446 |
block-cursor-text-style |
— | none |
button-color-foreground |
Mantle | #292c3c |
Note: background moves from Base to Mantle to match how the built-in
Catppuccin themes layer surfaces — panels (Surface0/1) then sit visibly
above the background.
Widget styling (src/hqt/tui/styles.tcss)
- Keep the existing
ProjectList/SessionListlayout rules. - Modal dialogs:
ProjectFormScreenrenames its container id to#project-form-dialog;NewSessionScreenkeeps#new-session-dialog. Both get: centered (align: center middleon the modal screen), fixed width (~60 cells),$surfacebackground, Lavender border, padding. - Panel headers (
#project-header,#session-header): bold text on$surfacebackground.
Status symbol colors (src/hqt/tui/widgets/session_list.py)
Color the status symbol via Rich markup with Frappé hex literals:
| Status | Frappé color | Hex |
|---|---|---|
working |
Green | #a6d189 |
waiting |
Yellow | #e5c890 |
active |
Teal | #81c8be |
idle |
Teal | #81c8be |
dead |
Overlay0 | #737994 |
Testing
- Service (
tests/test_services.py):updaterenames and repaths; duplicate path raisesValueErrorand leaves the DB session usable (rollback); unknown id raisesValueError. - TUI (pilot tests, same style as
tests/test_tui.py):- Pressing
ewith a project highlighted opens the form pre-filled with that project's name and path. - Submitting the edit form updates the DB and refreshes the list.
- Pressing
ewith no project selected shows a warning notification. - The add-project flow still works through the renamed screen.
- Pressing
- Theme: assert
app.current_theme.name == "catppuccin-frappe"and spot-check resolved CSS variables (e.g.backgroundis#292c3c,bordervariable is#babbf1) so a regression to auto-derived colors is caught.
Out of scope
- Archiving/un-archiving projects from the TUI.
- Filesystem validation of project paths.
- Moving or migrating existing tmux sessions when a path changes.
- Light-mode (Latte) theme variant.