From ace537e3d66464870e3ec609dc76507980e7eb3f Mon Sep 17 00:00:00 2001 From: Felix Martin Date: Wed, 10 Jun 2026 20:49:12 -0400 Subject: [PATCH] fix: worktree session UI safety and visibility (review) Co-Authored-By: Claude Fable 5 --- src/hqt/tui/app.py | 20 +++++++++++++------- src/hqt/tui/widgets/session_list.py | 13 +++++++++---- tests/test_tui.py | 14 ++++++++++---- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/hqt/tui/app.py b/src/hqt/tui/app.py index 163aaf8..82a1de1 100644 --- a/src/hqt/tui/app.py +++ b/src/hqt/tui/app.py @@ -287,8 +287,15 @@ class HqtApp(App): async def _open() -> None: # is_git_repo is async; resolve it before pushing so the dialog can - # disable the worktree control for non-repo projects. - is_git_repo = await worktree.is_git_repo(Path(project_path)) + # disable the worktree control for non-repo projects. This worker is + # run raw (not via _run_service_worker), so it must never raise: any + # failure (e.g. git not on PATH) falls back to False, which simply + # disables the worktree checkbox in the dialog. + try: + is_git_repo = await worktree.is_git_repo(Path(project_path)) + except Exception as exc: # noqa: BLE001 - keep the worker from crashing the TUI + log.warning("is_git_repo check failed for %s: %s", project_path, exc) + is_git_repo = False self.push_screen(NewSessionScreen(harness_names, is_git_repo), on_dismiss) self.run_worker(_open()) @@ -343,11 +350,10 @@ class HqtApp(App): parts.append("uncommitted changes") if state and state.unique_commits > 0: parts.append(f"{state.unique_commits} unmerged commit(s)") - warning = ( - " and ".join(parts).capitalize() + " — work will be lost" - if parts - else None - ) + warning = None + if parts: + joined = " and ".join(parts) + warning = joined[:1].upper() + joined[1:] + " — work will be lost" def on_dismiss(result: tuple[bool, bool] | None) -> None: if result is None: diff --git a/src/hqt/tui/widgets/session_list.py b/src/hqt/tui/widgets/session_list.py index 445a833..efe5742 100644 --- a/src/hqt/tui/widgets/session_list.py +++ b/src/hqt/tui/widgets/session_list.py @@ -37,10 +37,15 @@ def format_session_text( ) -> str: color = _STATUS_COLORS.get(status_text, "#c6d0f5") # default: Text name = nickname - # Mark worktree sessions with their branch. The nickname defaults to the - # branch at creation, so only append the marker when it adds information. - if worktree_branch and nickname != worktree_branch: - name = f"{nickname} ⎇ {worktree_branch}" + # Worktree sessions always carry the ⎇ marker so they're visually distinct + # from plain sessions. The nickname defaults to the branch at creation, so + # when they match we show the glyph once (no duplicated branch); when they + # differ we show both so the branch is still visible. + if worktree_branch: + if nickname == worktree_branch: + name = f"⎇ {nickname}" + else: + name = f"{nickname} ⎇ {worktree_branch}" label = escape(f"{name} [{harness_name}]") return f"[{color}]{symbol}[/] {label} {status_text}" diff --git a/tests/test_tui.py b/tests/test_tui.py index 460de7f..0329954 100644 --- a/tests/test_tui.py +++ b/tests/test_tui.py @@ -1404,18 +1404,24 @@ async def test_confirm_delete_cancel_returns_none(): def test_format_session_text_appends_worktree_branch_marker(): + """nickname != branch → show both nickname and branch around the ⎇ glyph.""" from hqt.tui.widgets.session_list import format_session_text text = format_session_text("mywork", "claude", "idle", "●", "feature-x") - assert "⎇ feature-x" in text + assert "mywork ⎇ feature-x" in text -def test_format_session_text_no_marker_when_nickname_equals_branch(): - """Avoid duplication: when nickname already equals the branch, no marker.""" +def test_format_session_text_marker_shows_once_when_nickname_equals_branch(): + """nickname == branch (the common case) → ⎇ still appears, glyph once, no + duplicated branch name.""" from hqt.tui.widgets.session_list import format_session_text text = format_session_text("feature-x", "claude", "idle", "●", "feature-x") - assert "⎇" not in text + assert "⎇" in text + # Glyph appears exactly once, and the branch is not duplicated. + assert text.count("⎇") == 1 + assert "⎇ feature-x" in text + assert "feature-x ⎇" not in text def test_format_session_text_no_marker_for_plain_session():