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>
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
# Project Editing + Catppuccin Frappé Theme Completion
|
||||
|
||||
**Date:** 2026-06-09
|
||||
**Status:** Approved
|
||||
|
||||
## Problem
|
||||
|
||||
1. 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.
|
||||
2. The TUI registers a Catppuccin Frappé theme (`FRAPPE_THEME` in
|
||||
`src/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`):
|
||||
|
||||
```python
|
||||
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`: catch
|
||||
`IntegrityError`, roll back, raise `ValueError` with 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] | None` of `(name, path)`.
|
||||
- Add-mode behavior unchanged: empty name defaults to `Path(path).name`;
|
||||
empty path dismisses with `None`.
|
||||
|
||||
### 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. Catch `ValueError` and show its message as an error
|
||||
notification.
|
||||
- `action_add_project` switches to the renamed `ProjectFormScreen` with
|
||||
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`/`SessionList` layout rules.
|
||||
- Modal dialogs: `ProjectFormScreen` renames its container id to
|
||||
`#project-form-dialog`; `NewSessionScreen` keeps `#new-session-dialog`.
|
||||
Both get: centered (`align: center middle` on the modal screen), fixed
|
||||
width (~60 cells), `$surface` background, Lavender border, padding.
|
||||
- Panel headers (`#project-header`, `#session-header`): bold text on
|
||||
`$surface` background.
|
||||
|
||||
### 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`): `update` renames and repaths;
|
||||
duplicate path raises `ValueError` and leaves the DB session usable
|
||||
(rollback); unknown id raises `ValueError`.
|
||||
- **TUI** (pilot tests, same style as `tests/test_tui.py`):
|
||||
- Pressing `e` with 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 `e` with no project selected shows a warning notification.
|
||||
- The add-project flow still works through the renamed screen.
|
||||
- **Theme**: assert `app.current_theme.name == "catppuccin-frappe"` and
|
||||
spot-check resolved CSS variables (e.g. `background` is `#292c3c`,
|
||||
`border` variable 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.
|
||||
Reference in New Issue
Block a user