diff --git a/docs/superpowers/specs/2026-06-11-tui-nav-archiving-design.md b/docs/superpowers/specs/2026-06-11-tui-nav-archiving-design.md new file mode 100644 index 0000000..c867ccf --- /dev/null +++ b/docs/superpowers/specs/2026-06-11-tui-nav-archiving-design.md @@ -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.