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>
3.2 KiB
Simplify session attach + add rename
Date: 2026-06-10 Status: Approved
Goal
Streamline session handling with two changes:
- Remove the redundant Resume (
Shift+R) binding — attach already auto-resumes. - Add a Rename action (
r) to relabel the selected session.
Background
Each session row carries two names:
tmux_session_name(hqt-{id}) — the stable internal identifier used to target the tmux window and as the unique DB key. Never user-facing.nickname— an optional display label shown in the session list and in the tmux window label (falls back totmux_session_namewhen unset).
attach_session() (src/hqt/sessions/service.py) already handles every window
state: alive → switch to it; dead pane or missing window → respawn via the
fallback ladder (_respawn_with_fallback) then attach. The resume_session()
method and its R,shift+r binding only duplicate that respawn-then-attach path,
so Resume is dead weight.
Change 1 — Remove the Resume binding (deletion only)
src/hqt/tui/app.py: delete theBinding("R,shift+r", "resume_session", "Resume")entry and theaction_resume_session()method.src/hqt/sessions/service.py: delete theresume_session()method.
Keep _respawn_with_fallback() — attach_session() depends on it. Attach
behavior is unchanged; Enter becomes the single way into a session, resuming
transparently when the window is dead or gone.
Change 2 — Add Rename (r)
Service
New synchronous method on SessionService (pure DB write — no tmux call):
def rename_session(self, session_id: int, nickname: str | None) -> None:
sess = self.db.get(Session, session_id)
sess.nickname = nickname or None # empty -> None -> label falls back to tmux name
self.db.commit()
The next 3-second poll (sync_window_labels()) reads the updated nickname and
refreshes the tmux window label automatically — no immediate label push needed.
A small get_session(session_id) -> Session | None accessor is added so the app
layer can read the current nickname to prefill the dialog without reaching into
db directly.
Modal
New RenameSessionScreen(ModalScreen[str | None]) in
src/hqt/tui/screens/rename_session.py, mirroring ProjectFormScreen:
- Titled dialog with a single
Inputprefilled with the current nickname. - OK / Cancel buttons in a
.dialog-actionsrow. - OK dismisses with the stripped input value; Cancel dismisses with
None.
App wiring (src/hqt/tui/app.py)
- Add
Binding("r", "rename_session", "Rename"). action_rename_session():- Get the selected session id; warn ("Select a session first") if none.
- Load the session via
get_session()to read its current nickname. - Push
RenameSessionScreenprefilled with that nickname. - On a non-
Nonedismiss, callrename_session()then_refresh_sessions().
Testing
rename_sessionsetsnickname; an empty string clears it toNone.- Remove or update any test referencing
resume_sessionor the Resume binding. - Pilot test: press
r, type a name, confirm, assert the session's nickname / displayed label updates.
Out of scope
- No change to
tmux_session_name. - No new harness logic.
- No DB migration (reuses the existing
nicknamecolumn).