feat: stop and rename a session from its tmux window

Add "stop" and "rename" to the Alt+p tool palette so a session can be
managed from inside its own harness window, mirroring the TUI's s/r keys:

- stop kills the harness window (stop_session_for_window).
- rename prompts for a nickname, pre-filled with the current one. The
  name is read from /dev/tty via readline (the fzf pipe leaves our stdin
  spent) and never passes through tmux's command parser, so there's
  nothing to quote-escape.

Extract require_session_id_for_window as the shared resolve-or-raise
guard behind the tool/stop/rename branches, dropping the duplicated
"not an hqt session window" check.

Also removes the now-shipped feature plans and specs under docs/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 10:55:27 -04:00
parent ff357156d0
commit fc95bf9830
23 changed files with 243 additions and 9370 deletions
+41 -15
View File
@@ -1218,10 +1218,14 @@ async def test_poll_info_empty_names(runner):
@pytest.mark.asyncio
async def test_new_aux_window_spawns_styles_and_selects(runner):
runner._exec.side_effect = [
(0, "0\n", ""), # _next_window_index: indices [0] -> next index 1
(0, "0\n", ""), # _next_window_index: indices [0] -> next index 1
(0, "@7\n", ""), # new-window -P -F '#{window_id}'
(0, "", ""), # set-option (automatic-rename + allow-rename + @hqt_label + theme)
(0, "", ""), # select-window
(
0,
"",
"",
), # set-option (automatic-rename + allow-rename + @hqt_label + theme)
(0, "", ""), # select-window
]
wid = await runner.new_aux_window("lazygit", "/proj", ["lazygit"], "lazygit · proj")
assert wid == "@7"
@@ -1229,12 +1233,26 @@ async def test_new_aux_window_spawns_styles_and_selects(runner):
calls = runner._exec.call_args_list
# new-window: next free index, name, cwd, print window_id, then the command.
assert calls[1].args == (
"new-window", "-t", "hqt-main:1", "-n", "lazygit",
"-c", "/proj", "-P", "-F", "#{window_id}", "lazygit",
"new-window",
"-t",
"hqt-main:1",
"-n",
"lazygit",
"-c",
"/proj",
"-P",
"-F",
"#{window_id}",
"lazygit",
)
# post-creation options target the window_id and NEVER set remain-on-exit.
assert calls[2].args[:6] == (
"set-option", "-w", "-t", "@7", "automatic-rename", "off",
"set-option",
"-w",
"-t",
"@7",
"automatic-rename",
"off",
)
# allow-rename off too, so nvim/lazygit OSC titles can't scramble the label.
assert "allow-rename" in calls[2].args
@@ -1248,7 +1266,7 @@ async def test_new_aux_window_spawns_styles_and_selects(runner):
@pytest.mark.asyncio
async def test_new_aux_window_shell_omits_command_arg(runner):
runner._exec.side_effect = [
(0, "2\n", ""), # indices [2] -> next index 3
(0, "2\n", ""), # indices [2] -> next index 3
(0, "@9\n", ""),
(0, "", ""),
(0, "", ""),
@@ -1257,16 +1275,24 @@ async def test_new_aux_window_shell_omits_command_arg(runner):
assert wid == "@9"
calls = runner._exec.call_args_list
assert calls[1].args == (
"new-window", "-t", "hqt-main:3", "-n", "shell",
"-c", "/proj", "-P", "-F", "#{window_id}",
"new-window",
"-t",
"hqt-main:3",
"-n",
"shell",
"-c",
"/proj",
"-P",
"-F",
"#{window_id}",
)
@pytest.mark.asyncio
async def test_new_aux_window_returns_none_when_new_window_fails(runner):
runner._exec.side_effect = [
(0, "0\n", ""), # next index
(1, "", "boom"), # new-window fails
(0, "0\n", ""), # next index
(1, "", "boom"), # new-window fails
]
wid = await runner.new_aux_window("nvim", "/p", ["nvim"], "nvim · p")
assert wid is None
@@ -1275,10 +1301,10 @@ async def test_new_aux_window_returns_none_when_new_window_fails(runner):
@pytest.mark.asyncio
async def test_new_aux_window_kills_window_when_set_option_fails(runner):
runner._exec.side_effect = [
(0, "0\n", ""), # _next_window_index
(0, "@7\n", ""), # new-window -> window_id @7
(1, "", "nope"), # set-option fails
(0, "", ""), # kill-window cleanup
(0, "0\n", ""), # _next_window_index
(0, "@7\n", ""), # new-window -> window_id @7
(1, "", "nope"), # set-option fails
(0, "", ""), # kill-window cleanup
]
wid = await runner.new_aux_window("nvim", "/p", ["nvim"], "nvim · p")
assert wid is None