Merge branch 'tool-windows'

Add tool windows: tool registry, styled aux tmux windows
(TmuxRunner.new_aux_window / TmuxManager.open_aux_window),
SessionService tool-window + clone-for-window methods, and
hqt tool / hqt palette CLI subcommands (fzf tool launcher).

# Conflicts:
#	src/hqt/cli.py
#	src/hqt/tmux/manager.py
#	tests/test_cli.py
#	tests/test_sessions.py
This commit is contained in:
2026-06-11 09:43:37 -04:00
9 changed files with 639 additions and 7 deletions
+118
View File
@@ -2010,3 +2010,121 @@ async def test_sandboxed_worktree_binds_repo_git_dir(
)
binds = mock_wrap.call_args.args[3]
assert Bind(Path("/tmp/myproj/.git"), writable=True) in binds
# ---------------------------------------------------------------------------
# Task 4: SessionService tool-window methods
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_open_tool_window_spawns_styled_window(service, db, tmux, monkeypatch):
await service.create_session(project_id=1, harness_name="claude-code")
monkeypatch.setattr("hqt.sessions.service.shutil.which", lambda b: "/usr/bin/" + b)
tmux.open_aux_window = AsyncMock(return_value="@5")
wid = await service.open_tool_window(1, "lazygit")
assert wid == "@5"
tmux.open_aux_window.assert_awaited_once_with(
"lazygit", "/tmp/myproj", ["lazygit"], "lazygit · myproj"
)
@pytest.mark.asyncio
async def test_open_tool_window_shell_skips_which_check(service, db, tmux, monkeypatch):
await service.create_session(project_id=1, harness_name="claude-code")
monkeypatch.setattr("hqt.sessions.service.shutil.which", lambda b: None)
tmux.open_aux_window = AsyncMock(return_value="@3")
wid = await service.open_tool_window(1, "shell")
assert wid == "@3"
tmux.open_aux_window.assert_awaited_once_with(
"shell", "/tmp/myproj", [], "shell · myproj"
)
@pytest.mark.asyncio
async def test_open_tool_window_unknown_tool_raises(service):
with pytest.raises(ServiceError):
await service.open_tool_window(1, "emacs")
@pytest.mark.asyncio
async def test_open_tool_window_missing_binary_raises(service, db, monkeypatch):
await service.create_session(project_id=1, harness_name="claude-code")
monkeypatch.setattr("hqt.sessions.service.shutil.which", lambda b: None)
with pytest.raises(ServiceError):
await service.open_tool_window(1, "lazygit")
@pytest.mark.asyncio
async def test_open_tool_window_unknown_session_raises(service, monkeypatch):
monkeypatch.setattr("hqt.sessions.service.shutil.which", lambda b: "/usr/bin/" + b)
with pytest.raises(ServiceError):
await service.open_tool_window(999, "nvim")
@pytest.mark.asyncio
async def test_session_id_for_window_resolves_and_misses(service, db):
await service.create_session(project_id=1, harness_name="claude-code")
assert service.session_id_for_window("hqt-1") == 1
assert service.session_id_for_window("not-an-hqt-window") is None
@pytest.mark.asyncio
async def test_open_tool_window_for_window_resolves_by_name(
service, db, tmux, monkeypatch
):
await service.create_session(project_id=1, harness_name="claude-code")
monkeypatch.setattr("hqt.sessions.service.shutil.which", lambda b: "/usr/bin/" + b)
tmux.open_aux_window = AsyncMock(return_value="@2")
wid = await service.open_tool_window_for_window("hqt-1", "nvim")
assert wid == "@2"
tmux.open_aux_window.assert_awaited_once_with(
"nvim", "/tmp/myproj", ["nvim"], "nvim · myproj"
)
@pytest.mark.asyncio
async def test_open_tool_window_for_window_unknown_window_raises(service):
with pytest.raises(ServiceError):
await service.open_tool_window_for_window("not-an-hqt-window", "nvim")
# ---------------------------------------------------------------------------
# Task 5: clone_session_for_window
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_clone_session_for_window_reuses_project_harness_model(
service, db, monkeypatch
):
await service.create_session(
project_id=1, harness_name="claude-code", nickname="orig", model="opus"
)
captured = {}
async def fake_create(
project_id, harness_name, nickname=None, model=None, worktree_branch=None
):
captured["args"] = (project_id, harness_name, nickname, model)
return "SENTINEL"
monkeypatch.setattr(service, "create_session", fake_create)
result = await service.clone_session_for_window("hqt-1")
assert result == "SENTINEL"
# Same project + harness + model; a fresh sibling, so no nickname.
assert captured["args"] == (1, "claude-code", None, "opus")
@pytest.mark.asyncio
async def test_clone_session_for_window_unknown_window_raises(service):
with pytest.raises(ServiceError):
await service.clone_session_for_window("not-an-hqt-window")