Files
hqt/docs/superpowers/specs/2026-06-11-tui-nav-archiving-design.md
T
2026-06-11 12:02:45 -04:00

5.7 KiB

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 Tabscreen.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:

  • haction_focus_projects: focus the #project-list VimListView.
  • laction_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 Tabaction_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 — instant, no confirmation (non-destructive: row + worktree kept, harness resumable)
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)

Archive is instant because it loses nothing: it kills the live tmux window (a resumable process) but keeps the DB row, harness_session_id, and any worktree on disk. Restore is equally instant. Hard delete retains its existing confirmation flow (worktree sessions still prompt via ConfirmDeleteScreen).

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 (instant, no dialog), 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.