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:
@@ -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.
|
||||
Reference in New Issue
Block a user