Initial commit: hqt — HQ Terminal TUI
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>
This commit is contained in:
@@ -0,0 +1,550 @@
|
||||
# Session Attach Simplification + Rename Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Remove the redundant `Shift+R` Resume binding (Enter/attach already auto-resumes) and add an `r` Rename action that edits a session's display nickname.
|
||||
|
||||
**Architecture:** Resume is pure deletion — `attach_session()` already respawns dead/gone windows before attaching. Rename is a synchronous DB write to `Session.nickname`; the existing 3-second poll (`sync_window_labels`) propagates the new label to tmux. A new modal `RenameSessionScreen` mirrors the existing `ProjectFormScreen` pattern.
|
||||
|
||||
**Tech Stack:** Python, Textual (TUI), SQLAlchemy, pytest + pytest-asyncio.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- **Modify** `src/hqt/sessions/service.py` — remove `resume_session()`; add `rename_session()` and `get_session()`.
|
||||
- **Modify** `src/hqt/tui/app.py` — remove the Resume binding + `action_resume_session()`; add the Rename binding + `action_rename_session()`.
|
||||
- **Create** `src/hqt/tui/screens/rename_session.py` — `RenameSessionScreen` modal.
|
||||
- **Modify** `tests/test_tui.py` — delete the resume keypress test; add rename-modal + rename-action tests.
|
||||
- **Modify** `tests/test_sessions.py` — delete four `resume_session` tests; add `rename_session`/`get_session` tests (reusing existing fixtures).
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Remove Resume from the service
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/hqt/sessions/service.py:143-150` (the `resume_session` method)
|
||||
- Modify: `tests/test_sessions.py` (delete four `resume_session` tests)
|
||||
|
||||
- [ ] **Step 1: Inventory every reference**
|
||||
|
||||
Run: `grep -rn "resume_session" src/ tests/`
|
||||
Expected references: the method in `src/hqt/sessions/service.py`; `action_resume_session` in `src/hqt/tui/app.py`; `test_resume_keypress_triggers_resume` in `tests/test_tui.py`; and four tests in `tests/test_sessions.py` — `test_resume_session_passes_env`, `test_resume_session_resume_ok_attaches`, `test_resume_session_resume_fails_fallback_spawn`, `test_resume_session_both_rungs_fail_returns_false`. No other production callers.
|
||||
|
||||
Coverage note: `_respawn_with_fallback`'s rung-1/rung-2 behavior is independently covered by `test_attach_session_dead_resume_ok_attaches_once`, `test_attach_session_dead_resume_fails_fallback_spawn`, and `test_attach_session_both_rungs_fail_returns_false`, so deleting the resume tests loses no fallback-ladder coverage.
|
||||
|
||||
- [ ] **Step 2: Delete the four `resume_session` tests in `tests/test_sessions.py`**
|
||||
|
||||
Remove these four async test functions in full (including decorators and docstrings): `test_resume_session_passes_env`, `test_resume_session_resume_ok_attaches`, `test_resume_session_resume_fails_fallback_spawn`, `test_resume_session_both_rungs_fail_returns_false`. Leave all `test_attach_session_*` tests intact.
|
||||
|
||||
- [ ] **Step 3: Confirm the resume tests are gone but attach tests remain**
|
||||
|
||||
Run: `grep -n "resume_session\|test_attach_session" tests/test_sessions.py`
|
||||
Expected: no `resume_session` matches; the three `test_attach_session_*` tests still listed.
|
||||
|
||||
- [ ] **Step 4: Delete the `resume_session` method**
|
||||
|
||||
In `src/hqt/sessions/service.py`, remove this method entirely (currently lines 143-150):
|
||||
|
||||
```python
|
||||
async def resume_session(self, session_id: int) -> bool:
|
||||
"""Force-restart the harness in this session's window."""
|
||||
sess = self.db.get(Session, session_id)
|
||||
window_name = sess.tmux_session_name
|
||||
ok = await self._respawn_with_fallback(sess, window_name)
|
||||
if not ok:
|
||||
return False
|
||||
return await self.tmux.attach(window_name)
|
||||
```
|
||||
|
||||
Leave `_respawn_with_fallback()` and `attach_session()` untouched — attach depends on the fallback ladder.
|
||||
|
||||
- [ ] **Step 5: Verify nothing in the service references the removed method**
|
||||
|
||||
Run: `grep -n "resume_session" src/hqt/sessions/service.py`
|
||||
Expected: no output.
|
||||
|
||||
- [ ] **Step 6: Run the service tests**
|
||||
|
||||
Run: `python -m pytest tests/test_sessions.py -q`
|
||||
Expected: PASS — resume tests removed, attach/fallback tests still green.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add src/hqt/sessions/service.py tests/test_sessions.py
|
||||
git commit -m "refactor: drop redundant resume_session (attach auto-resumes)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Remove Resume from the app + its test
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/hqt/tui/app.py:62-68` (BINDINGS) and `src/hqt/tui/app.py:235-243` (`action_resume_session`)
|
||||
- Modify: `tests/test_tui.py:822-871` (delete `test_resume_keypress_triggers_resume`)
|
||||
|
||||
- [ ] **Step 1: Delete the resume test first**
|
||||
|
||||
In `tests/test_tui.py`, remove the section header comment block and the entire `test_resume_keypress_triggers_resume` test (currently lines 822-871), including its `@pytest.mark.parametrize` decorator.
|
||||
|
||||
- [ ] **Step 2: Run the suite to confirm only the deleted test is gone**
|
||||
|
||||
Run: `python -m pytest tests/test_tui.py -q`
|
||||
Expected: PASS — the resume test no longer collected; the action still exists so nothing else breaks yet.
|
||||
|
||||
- [ ] **Step 3: Remove the Resume binding**
|
||||
|
||||
In `src/hqt/tui/app.py`, delete these lines from `BINDINGS` (currently lines 63-66):
|
||||
|
||||
```python
|
||||
# A normal terminal sends the character "R" for Shift+R; only the Kitty
|
||||
# keyboard protocol emits "shift+r". Bind both so Resume fires either way
|
||||
# (binding "shift+r" alone never matches on a standard terminal).
|
||||
Binding("R,shift+r", "resume_session", "Resume"),
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Remove `action_resume_session`**
|
||||
|
||||
In `src/hqt/tui/app.py`, delete this method (currently lines 235-243):
|
||||
|
||||
```python
|
||||
def action_resume_session(self) -> None:
|
||||
sid = self.query_one(SessionList).get_selected_session_id()
|
||||
if sid:
|
||||
async def _do() -> None:
|
||||
ok = await self._session_service.resume_session(sid)
|
||||
if not ok:
|
||||
self.notify("Failed to resume session", severity="error")
|
||||
await self._refresh_sessions()
|
||||
self.run_worker(_do())
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Confirm no lingering references**
|
||||
|
||||
Run: `grep -rn "resume_session\|Resume" src/hqt/tui/app.py`
|
||||
Expected: no output.
|
||||
|
||||
- [ ] **Step 6: Run the full suite**
|
||||
|
||||
Run: `python -m pytest tests/test_tui.py -q`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add src/hqt/tui/app.py tests/test_tui.py
|
||||
git commit -m "refactor: remove Shift+R Resume binding and action"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Add `rename_session` + `get_session` to the service
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/hqt/sessions/service.py` (add two methods to `SessionService`)
|
||||
- Test: `tests/test_sessions.py`
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
`tests/test_sessions.py` already exists with shared `db`, `tmux`, `harnesses`, and `service` fixtures (the `db` fixture seeds one `Harness(name="claude-code")` and one `Project` with id 1). Reuse them — do NOT add new setup helpers. Append a small builder and the tests:
|
||||
|
||||
```python
|
||||
def _seed_session(db, nickname=None):
|
||||
"""Create a session for project 1 / harness 'claude-code' (seeded by the db fixture)."""
|
||||
from hqt.db.models import Harness, Session
|
||||
|
||||
harness = db.query(Harness).filter_by(name="claude-code").first()
|
||||
sess = Session(
|
||||
project_id=1,
|
||||
harness_id=harness.id,
|
||||
nickname=nickname,
|
||||
tmux_session_name="hqt-rename",
|
||||
archived=False,
|
||||
)
|
||||
db.add(sess)
|
||||
db.commit()
|
||||
return sess
|
||||
|
||||
|
||||
def test_rename_session_sets_nickname(service, db):
|
||||
from hqt.db.models import Session
|
||||
|
||||
sess = _seed_session(db, nickname="old")
|
||||
service.rename_session(sess.id, "new-name")
|
||||
assert db.get(Session, sess.id).nickname == "new-name"
|
||||
|
||||
|
||||
def test_rename_session_empty_clears_to_none(service, db):
|
||||
from hqt.db.models import Session
|
||||
|
||||
sess = _seed_session(db, nickname="old")
|
||||
service.rename_session(sess.id, "")
|
||||
assert db.get(Session, sess.id).nickname is None
|
||||
|
||||
|
||||
def test_rename_session_whitespace_clears_to_none(service, db):
|
||||
from hqt.db.models import Session
|
||||
|
||||
sess = _seed_session(db, nickname="old")
|
||||
service.rename_session(sess.id, " ")
|
||||
assert db.get(Session, sess.id).nickname is None
|
||||
|
||||
|
||||
def test_get_session_returns_row(service, db):
|
||||
sess = _seed_session(db, nickname="x")
|
||||
assert service.get_session(sess.id).id == sess.id
|
||||
|
||||
|
||||
def test_get_session_missing_returns_none(service):
|
||||
assert service.get_session(99999) is None
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the tests to verify they fail**
|
||||
|
||||
Run: `python -m pytest tests/test_sessions.py -k "rename_session or get_session" -q`
|
||||
Expected: FAIL with `AttributeError: 'SessionService' object has no attribute 'rename_session'` (and `get_session`).
|
||||
|
||||
- [ ] **Step 3: Implement both methods**
|
||||
|
||||
In `src/hqt/sessions/service.py`, add these methods to `SessionService` (place them next to `delete_session`):
|
||||
|
||||
```python
|
||||
def get_session(self, session_id: int) -> Session | None:
|
||||
"""Return the session row, or None if it does not exist."""
|
||||
return self.db.get(Session, session_id)
|
||||
|
||||
def rename_session(self, session_id: int, nickname: str | None) -> None:
|
||||
"""Update a session's display nickname.
|
||||
|
||||
An empty/blank nickname clears it to None, so the label falls back to
|
||||
the tmux window name. The tmux window label is refreshed by the next
|
||||
poll (sync_window_labels), so no immediate tmux call is needed here.
|
||||
"""
|
||||
sess = self.db.get(Session, session_id)
|
||||
sess.nickname = (nickname or "").strip() or None
|
||||
self.db.commit()
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the tests to verify they pass**
|
||||
|
||||
Run: `python -m pytest tests/test_sessions.py -k "rename_session or get_session" -q`
|
||||
Expected: PASS (5 tests).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/hqt/sessions/service.py tests/test_sessions.py
|
||||
git commit -m "feat: add rename_session and get_session to SessionService"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Create the RenameSessionScreen modal
|
||||
|
||||
**Files:**
|
||||
- Create: `src/hqt/tui/screens/rename_session.py`
|
||||
- Test: `tests/test_tui.py` (append)
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
Append to `tests/test_tui.py`:
|
||||
|
||||
```python
|
||||
# ---------------------------------------------------------------------------
|
||||
# RenameSessionScreen: prefill + submit
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rename_screen_prefills_current_nickname():
|
||||
app = HqtApp()
|
||||
async with app.run_test(size=(120, 40)) as pilot:
|
||||
from hqt.tui.screens.rename_session import RenameSessionScreen
|
||||
from textual.widgets import Input
|
||||
|
||||
app.push_screen(RenameSessionScreen(initial_nickname="mywork"))
|
||||
await pilot.pause()
|
||||
assert app.screen.query_one("#nickname-input", Input).value == "mywork"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rename_screen_ok_returns_stripped_value():
|
||||
app = HqtApp()
|
||||
async with app.run_test(size=(120, 40)) as pilot:
|
||||
from hqt.tui.screens.rename_session import RenameSessionScreen
|
||||
from textual.widgets import Button, Input
|
||||
|
||||
results = []
|
||||
app.push_screen(RenameSessionScreen(initial_nickname=""), results.append)
|
||||
await pilot.pause()
|
||||
app.screen.query_one("#nickname-input", Input).value = " renamed "
|
||||
app.screen.query_one("#ok-btn", Button).press()
|
||||
await pilot.pause()
|
||||
assert results == ["renamed"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rename_screen_cancel_returns_none():
|
||||
app = HqtApp()
|
||||
async with app.run_test(size=(120, 40)) as pilot:
|
||||
from hqt.tui.screens.rename_session import RenameSessionScreen
|
||||
from textual.widgets import Button
|
||||
|
||||
results = []
|
||||
app.push_screen(RenameSessionScreen(initial_nickname="x"), results.append)
|
||||
await pilot.pause()
|
||||
app.screen.query_one("#cancel-btn", Button).press()
|
||||
await pilot.pause()
|
||||
assert results == [None]
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the tests to verify they fail**
|
||||
|
||||
Run: `python -m pytest tests/test_tui.py -k rename_screen -q`
|
||||
Expected: FAIL with `ModuleNotFoundError: No module named 'hqt.tui.screens.rename_session'`.
|
||||
|
||||
- [ ] **Step 3: Implement the modal**
|
||||
|
||||
Create `src/hqt/tui/screens/rename_session.py`:
|
||||
|
||||
```python
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Horizontal, Vertical
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Button, Input, Label
|
||||
|
||||
|
||||
class RenameSessionScreen(ModalScreen[str | None]):
|
||||
"""Rename a session's display nickname.
|
||||
|
||||
Dismisses with the stripped nickname on OK, or None on Cancel. Mirrors
|
||||
ProjectFormScreen's structure so it picks up the same dialog styling
|
||||
(#project-form-dialog rules are reused via the shared id).
|
||||
"""
|
||||
|
||||
def __init__(self, initial_nickname: str = "") -> None:
|
||||
self._initial_nickname = initial_nickname
|
||||
super().__init__()
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(id="project-form-dialog"):
|
||||
yield Label("Rename Session")
|
||||
yield Label("Name:")
|
||||
yield Input(
|
||||
value=self._initial_nickname,
|
||||
placeholder="session name",
|
||||
id="nickname-input",
|
||||
)
|
||||
with Horizontal(classes="dialog-actions"):
|
||||
yield Button("OK", variant="primary", id="ok-btn")
|
||||
yield Button("Cancel", id="cancel-btn")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "ok-btn":
|
||||
self.dismiss(self.query_one("#nickname-input", Input).value.strip())
|
||||
else:
|
||||
self.dismiss(None)
|
||||
|
||||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
||||
# Enter in the text field confirms, matching the New Session dialog.
|
||||
self.dismiss(self.query_one("#nickname-input", Input).value.strip())
|
||||
```
|
||||
|
||||
Note: reusing `id="project-form-dialog"` is intentional — it inherits the existing dialog styling in `styles.tcss` (width 60, border, `$surface` fill) verified by `test_project_form_dialog_styled`. No new CSS needed.
|
||||
|
||||
- [ ] **Step 4: Run the tests to verify they pass**
|
||||
|
||||
Run: `python -m pytest tests/test_tui.py -k rename_screen -q`
|
||||
Expected: PASS (3 tests).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/hqt/tui/screens/rename_session.py tests/test_tui.py
|
||||
git commit -m "feat: add RenameSessionScreen modal"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Wire the `r` Rename binding + action into the app
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/hqt/tui/app.py` (BINDINGS, import, new action)
|
||||
- Test: `tests/test_tui.py` (append)
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
Append to `tests/test_tui.py`:
|
||||
|
||||
```python
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rename session: binding + action
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rename_session_binding_exists():
|
||||
bindings = {b.key: b for b in HqtApp.BINDINGS}
|
||||
assert "r" in bindings
|
||||
assert bindings["r"].action == "rename_session"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rename_session_no_selection_warns():
|
||||
app = HqtApp()
|
||||
async with app.run_test(size=(120, 40)) as pilot:
|
||||
from unittest.mock import patch
|
||||
|
||||
notify_calls = []
|
||||
with patch.object(
|
||||
app, "notify", side_effect=lambda msg, **kw: notify_calls.append((msg, kw))
|
||||
):
|
||||
await app.run_action("rename_session")
|
||||
await pilot.pause()
|
||||
|
||||
assert any(
|
||||
kw.get("severity") == "warning" for _, kw in notify_calls
|
||||
), f"Expected warning notification, got: {notify_calls}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rename_session_opens_prefilled_and_updates():
|
||||
app = HqtApp()
|
||||
async with app.run_test(size=(120, 40)) as pilot:
|
||||
from hqt.db.models import Project, Harness, Session
|
||||
from hqt.tui.widgets.session_list import SessionList
|
||||
from hqt.tui.screens.rename_session import RenameSessionScreen
|
||||
from textual.widgets import Input
|
||||
|
||||
proj = Project(name="ren-proj", path="/tmp/ren-proj")
|
||||
app._db_session.add(proj)
|
||||
app._db_session.flush()
|
||||
harness = app._db_session.query(Harness).first()
|
||||
sess = Session(
|
||||
project_id=proj.id,
|
||||
harness_id=harness.id,
|
||||
nickname="before",
|
||||
tmux_session_name="hqt-ren",
|
||||
archived=False,
|
||||
)
|
||||
app._db_session.add(sess)
|
||||
app._db_session.commit()
|
||||
|
||||
app._selected_project_id = proj.id
|
||||
await app._refresh_sessions()
|
||||
await pilot.pause()
|
||||
|
||||
sl = app.query_one(SessionList)
|
||||
sl.query_one("#session-list").focus()
|
||||
await pilot.press("down")
|
||||
await pilot.pause()
|
||||
assert sl.get_selected_session_id() == sess.id
|
||||
|
||||
await app.run_action("rename_session")
|
||||
await pilot.pause()
|
||||
|
||||
assert isinstance(app.screen, RenameSessionScreen)
|
||||
assert app.screen.query_one("#nickname-input", Input).value == "before"
|
||||
|
||||
app.screen.dismiss("after")
|
||||
await pilot.pause()
|
||||
await app.workers.wait_for_complete()
|
||||
await pilot.pause()
|
||||
|
||||
assert app._db_session.get(Session, sess.id).nickname == "after"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the tests to verify they fail**
|
||||
|
||||
Run: `python -m pytest tests/test_tui.py -k rename_session -q`
|
||||
Expected: FAIL — `"r"` not in BINDINGS / no `rename_session` action.
|
||||
|
||||
- [ ] **Step 3: Add the import**
|
||||
|
||||
In `src/hqt/tui/app.py`, add next to the other screen imports (near line 19):
|
||||
|
||||
```python
|
||||
from hqt.tui.screens.rename_session import RenameSessionScreen
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add the binding**
|
||||
|
||||
In `src/hqt/tui/app.py` `BINDINGS`, add this entry (e.g. after the `delete_session` binding):
|
||||
|
||||
```python
|
||||
Binding("r", "rename_session", "Rename"),
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Add the action**
|
||||
|
||||
In `src/hqt/tui/app.py`, add this method (e.g. after `action_attach_session`):
|
||||
|
||||
```python
|
||||
def action_rename_session(self) -> None:
|
||||
sid = self.query_one(SessionList).get_selected_session_id()
|
||||
if sid is None:
|
||||
self.notify("Select a session first", severity="warning")
|
||||
return
|
||||
sess = self._session_service.get_session(sid)
|
||||
if sess is None:
|
||||
self.notify("Session not found", severity="error")
|
||||
return
|
||||
|
||||
def on_dismiss(result: str | None) -> None:
|
||||
if result is not None:
|
||||
self._session_service.rename_session(sid, result)
|
||||
self.run_worker(self._refresh_sessions())
|
||||
|
||||
self.push_screen(
|
||||
RenameSessionScreen(initial_nickname=sess.nickname or ""),
|
||||
on_dismiss,
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run the tests to verify they pass**
|
||||
|
||||
Run: `python -m pytest tests/test_tui.py -k rename_session -q`
|
||||
Expected: PASS (3 tests).
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add src/hqt/tui/app.py tests/test_tui.py
|
||||
git commit -m "feat: add 'r' rename-session binding and action"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Full verification + docs
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md` (if it documents keybindings — check first)
|
||||
|
||||
- [ ] **Step 1: Run the entire test suite**
|
||||
|
||||
Run: `python -m pytest -q`
|
||||
Expected: PASS, no failures, no reference to resume.
|
||||
|
||||
- [ ] **Step 2: Update keybinding docs if present**
|
||||
|
||||
Run: `grep -rn "Resume\|Shift+R\|shift+r" README.md`
|
||||
If matches exist, replace the Resume entry with a Rename (`r`) entry and note that attach (Enter) now auto-resumes. If no matches, skip.
|
||||
|
||||
- [ ] **Step 3: Commit any doc change**
|
||||
|
||||
```bash
|
||||
git add README.md
|
||||
git commit -m "docs: replace Resume keybinding with Rename ('r')"
|
||||
```
|
||||
|
||||
(Skip this commit if README needed no change.)
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Notes
|
||||
|
||||
- **Spec coverage:** Change 1 (remove Resume) → Tasks 1-2. Change 2 (rename: service, modal, wiring) → Tasks 3-5. Testing section → tests embedded in every task + Task 6. Out-of-scope items (no `tmux_session_name` change, no migration) are respected — only `nickname` is written.
|
||||
- **Empty-input → None:** handled in `rename_session` (`(nickname or "").strip() or None`) and covered by `test_rename_session_empty_clears_to_none`.
|
||||
- **Type consistency:** `get_session(session_id) -> Session | None`, `rename_session(session_id, nickname)`, and `RenameSessionScreen(initial_nickname=...)` are used identically across the service, app action, and tests.
|
||||
- **Label propagation:** no immediate tmux call; the existing `sync_window_labels` poll (app.py `set_interval(3, ...)`) reads the new nickname — consistent with the approved design.
|
||||
Reference in New Issue
Block a user