docs: design spec for TUI h/l nav and session archiving

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 11:22:14 -04:00
parent fc95bf9830
commit 32f7e0ded7
@@ -0,0 +1,83 @@
# TUI Column Navigation & Session Archiving — Design
**Date:** 2026-06-11
**Status:** Approved (ready for implementation plan)
## Goal
Two TUI-UX improvements to hqt's main view:
1. **h/l column navigation** — vim-style horizontal movement between the Projects and Sessions columns, complementing the existing `j/k` within-list movement.
2. **Session archiving** — a recoverable "remove from view" that tears down a finished session's tmux window but keeps its DB record, with a per-project toggle to view and restore archived sessions.
## Background / Current State
- Main view (`src/hqt/tui/app.py`) composes `ProjectList` (docked left) + `SessionList` (fills right) + `Footer`.
- Within-list movement uses `VimListView` (`j`/`k` plus arrows). Cross-column movement today is `Tab``screen.focus_next()` (cycles ProjectList → SessionList → Footer).
- `Session` and `Project` models already carry an `archived: bool` column (`src/hqt/db/models.py`). Every session read path already filters `archived=False` (`list_sessions`, `sync_window_labels`, `session_id_for_window`). **No DB migration is required** — the column exists at baseline schema v1.
- `delete_session` (`src/hqt/sessions/service.py`) kills the tmux window (if alive), removes any worktree (optional), deletes the scheduled prompt, and deletes the DB row. Archiving will reuse the window-teardown portion of this path.
## Feature A — h/l Column Navigation
Add two app-level bindings in `app.py`:
- `h``action_focus_projects`: focus the `#project-list` `VimListView`.
- `l``action_focus_sessions`: focus the `#session-list` `VimListView`.
These focus a specific widget by id (not `focus_next()`), so they are directional regardless of current focus. The existing `Tab``action_toggle_focus` binding **stays** as a cycle alias. No CSS/layout changes.
**Behavior details:**
- Selecting a project continues to drive which sessions are shown (unchanged).
- Pressing `l` when the selected project has zero sessions still moves focus to the (empty) Sessions list — consistent with how `Tab` behaves today.
## Feature B — Session Archiving
### Semantics
Archiving is a **recoverable delete**: it kills the session's tmux window/harness process (freeing resources) but **keeps the DB row** including `harness_session_id`, so the record can be referenced or restored later. It is the everyday "I'm done with this" action; hard `delete` remains for permanent removal.
### Service layer (`src/hqt/sessions/service.py`)
- Parameterize the read path: `list_sessions(project_id, archived=False, now=None)` — same query, with the archived flag configurable.
- `archive_session(session_id)`: kill the tmux window if it exists (reuse the existing teardown used by `delete_session`), then set `archived=True` and commit. Does **not** remove the worktree or DB row.
- `unarchive_session(session_id)`: set `archived=False` and commit. The window stays gone — the restored session is a dead, recoverable record.
`sync_window_labels` and other live-session machinery continue to filter `archived=False`, so archived rows are never polled or labeled.
### TUI actions & keybindings (`src/hqt/tui/app.py`)
| Key | View | Action |
|-----|-----------|----------------------------------------------------------------|
| `x` | active | Archive selected session — gated behind the same confirm dialog as delete |
| `A` | both | Toggle the Sessions column between **active** and **archived** |
| `u` | archived | Restore (unarchive) the selected session |
| `d` | both | Hard delete (unchanged; in archived view the window is already gone) |
### View toggle
- A `_show_archived: bool` flag on the app. `action_toggle_archived` flips it and refreshes the Sessions list for the currently-selected project.
- When archived, the Sessions header reads **"Sessions (archived)"**; when active it reads **"Sessions"**.
- The flag **persists across project switches** (predictable, single global mode). `_refresh_sessions` reads `archived=self._show_archived` when calling `list_sessions`.
- Footer adapts via Textual's `check_action`: `x` is offered only in active view, `u` only in archived view. The footer never advertises an inapplicable action.
### Status of archived sessions
Archived sessions always render as `dead` (their window was killed). No pane capture or status parsing is needed for them; `_poll_sessions` continues to operate on the active list only.
## Out of Scope
- Archiving **projects** (the `Project.archived` column exists but no project-archiving UI is part of this work).
- Worktree removal on archive (archive keeps the row and any worktree; hard delete still handles worktree teardown).
- Bulk archive/restore.
## Testing
**Service tests (`tests/test_sessions.py`):**
- `archive_session` kills the window, keeps the row, and the row is excluded from `list_sessions(archived=False)`.
- `unarchive_session` makes the row reappear in the active list.
- `list_sessions(project_id, archived=True)` returns only archived rows.
**TUI tests (`tests/test_tui*.py`, following existing patterns):**
- `x` triggers archive (with confirm), `A` toggles the view and header text, `u` restores.
- `h`/`l` move focus to the projects/sessions lists respectively.
- Footer/`check_action` adaptation: `x` hidden in archived view, `u` hidden in active view.