diff --git a/src/herdr_workflow/workflows/inbox.py b/src/herdr_workflow/workflows/inbox.py index 213ca5c..dc31f59 100644 --- a/src/herdr_workflow/workflows/inbox.py +++ b/src/herdr_workflow/workflows/inbox.py @@ -18,10 +18,38 @@ from herdr_workflow.herdr import ops from herdr_workflow.herdr.agents import start_agent from herdr_workflow.herdr.client import HerdrClient +from herdr_workflow.protocol.messages import Snapshot ROUTER_TAB = "router" +def _bootstrap_workspace_id(snapshot: Snapshot, home: Path) -> str | None: + """Return Herdr's untouched startup workspace, if it is still present. + + Herdr creates a ``~`` workspace with one tab named ``1`` when a session starts. Once + the inbox exists that shell is just clutter. Match the complete pristine shape so an + intentional home-directory workspace is never closed merely because it is named ``~``. + """ + for workspace in snapshot.workspaces: + if workspace.label != "~" or workspace.tab_count != 1 or workspace.pane_count != 1: + continue + tabs = [tab for tab in snapshot.tabs if tab.workspace_id == workspace.workspace_id] + panes = [pane for pane in snapshot.panes if pane.workspace_id == workspace.workspace_id] + agents = [ + agent for agent in snapshot.agents if agent.workspace_id == workspace.workspace_id + ] + if ( + len(tabs) == 1 + and tabs[0].label == "1" + and len(panes) == 1 + and panes[0].cwd == str(home) + and panes[0].revision == 0 + and not agents + ): + return workspace.workspace_id + return None + + @dataclass(frozen=True) class UpResult: workspace_id: str @@ -46,6 +74,7 @@ async def inbox_workspace_id(client: HerdrClient, config: Config) -> str: async def up(client: HerdrClient, config: Config, home: Path) -> UpResult: snapshot = await client.snapshot() workspace = ops.workspace_by_label(snapshot, config.herdr.inbox_label) + bootstrap_workspace_id = _bootstrap_workspace_id(snapshot, home) created_workspace = False if workspace is None: @@ -78,6 +107,15 @@ async def up(client: HerdrClient, config: Config, home: Path) -> UpResult: await ops.workspace_focus(client, workspace_id) await ops.pane_focus(client, pane_id) + # A fresh Herdr session starts with an otherwise unused ``~`` workspace. Close it only + # after the inbox is ready and focused, so the session is never left without a workspace. + if bootstrap_workspace_id is not None and bootstrap_workspace_id != workspace_id: + # Agent startup takes long enough for a person to begin using the shell. Recheck + # before closing so a workspace changed since the first snapshot is preserved. + current = await client.snapshot() + if _bootstrap_workspace_id(current, home) == bootstrap_workspace_id: + await ops.workspace_close(client, bootstrap_workspace_id) + return UpResult( workspace_id=workspace_id, pane_id=pane_id, diff --git a/tests/unit/test_inbox_and_clean.py b/tests/unit/test_inbox_and_clean.py index 58316bf..4b777d0 100644 --- a/tests/unit/test_inbox_and_clean.py +++ b/tests/unit/test_inbox_and_clean.py @@ -39,7 +39,15 @@ def _tab(tab_id: str, ws_id: str, label: str) -> dict[str, Any]: } -def _pane(pane_id: str, ws_id: str, tab_id: str, label: str | None = None) -> dict[str, Any]: +def _pane( + pane_id: str, + ws_id: str, + tab_id: str, + label: str | None = None, + *, + cwd: str | None = None, + revision: int = 1, +) -> dict[str, Any]: return { "pane_id": pane_id, "terminal_id": f"t-{pane_id}", @@ -47,8 +55,9 @@ def _pane(pane_id: str, ws_id: str, tab_id: str, label: str | None = None) -> di "tab_id": tab_id, "focused": False, "agent_status": "unknown", - "revision": 1, + "revision": revision, "label": label, + "cwd": cwd, } @@ -112,6 +121,70 @@ async def test_up_creates_the_workspace_when_absent( assert fake.calls("tab.rename")[0]["params"]["label"] == "router" +async def test_up_closes_herdrs_pristine_home_workspace( + client: HerdrClient, fake: FakeHerdr, tmp_path: Path +) -> None: + fake.on( + "session.snapshot", + _snapshot( + workspaces=[_ws("~", "w0")], + tabs=[_tab("w0:t1", "w0", "1")], + panes=[_pane("w0:p1", "w0", "w0:t1", cwd=str(tmp_path), revision=0)], + ), + ) + fake.on( + "workspace.create", + { + "type": "workspace_created", + "workspace": _ws("inbox", "w1"), + "tab": _tab("w1:t1", "w1", "inbox"), + "root_pane": _pane("w1:p1", "w1", "w1:t1"), + }, + ) + fake.on("agent.list", {"type": "agent_list", "agents": []}) + _ok( + fake, + "tab.rename", + "agent.start", + "pane.rename", + "workspace.focus", + "pane.focus", + "workspace.close", + ) + + await inbox.up(client, Config(), tmp_path) + + assert fake.calls("workspace.close")[0]["params"]["workspace_id"] == "w0" + + +async def test_up_keeps_a_home_workspace_that_is_not_pristine( + client: HerdrClient, fake: FakeHerdr, tmp_path: Path +) -> None: + fake.on( + "session.snapshot", + _snapshot( + workspaces=[_ws("~", "w0")], + tabs=[_tab("w0:t1", "w0", "shell")], + panes=[_pane("w0:p1", "w0", "w0:t1", cwd=str(tmp_path), revision=0)], + ), + ) + fake.on( + "workspace.create", + { + "type": "workspace_created", + "workspace": _ws("inbox", "w1"), + "tab": _tab("w1:t1", "w1", "inbox"), + "root_pane": _pane("w1:p1", "w1", "w1:t1"), + }, + ) + fake.on("agent.list", {"type": "agent_list", "agents": []}) + _ok(fake, "tab.rename", "agent.start", "pane.rename", "workspace.focus", "pane.focus") + + await inbox.up(client, Config(), tmp_path) + + assert fake.calls("workspace.close") == [] + + async def test_up_is_idempotent_when_the_router_is_already_running( client: HerdrClient, fake: FakeHerdr, tmp_path: Path ) -> None: