feat(ui): sidebar lists workspaces with their sessions, Claude Desktop style

The sidebar's project tree becomes a flat workspace list (WorkspaceTree):
one row per workspace in the stored order, each expanding to its sessions.
A session that still has a live pane opens /workspace?lane=<id>&run=<runId>,
which only attaches pane 0 to that tmux session; any other session opens
its read-only transcript. Nothing in the sidebar starts or resumes a run.
This commit is contained in:
2026-10-01 22:23:01 +07:00
parent 744e9c6cda
commit 2e051e23b6
16 changed files with 616 additions and 538 deletions
@@ -526,4 +526,34 @@ describe("LaneConsolePane", () => {
await screen.findByTestId("terminal-view");
expect(api.lanes.action).toHaveBeenCalledTimes(1);
});
it("attaches to a run picked in the sidebar without starting or resuming anything", async () => {
vi.mocked(api.run.get).mockResolvedValue({
id: "agenthub-lane-1-2",
laneId: 1,
slot: 2,
status: "running",
cwd: "/workspace/a",
model: null,
permissionMode: null,
effort: null,
resumeSessionId: null,
sessionId: "s-9",
startedAt: null,
promptPreview: null,
tool: "claude",
});
render(
<MemoryRouter>
<LaneConsolePane
{...baseProps()}
attachRequest={{ runId: "agenthub-lane-1-2", nonce: "k1" }}
/>
</MemoryRouter>
);
await screen.findByTestId("terminal-view");
expect(api.run.get).toHaveBeenCalledWith("agenthub-lane-1-2");
expect(api.lanes.action).not.toHaveBeenCalled();
expect(api.run.start).not.toHaveBeenCalled();
});
});