fix(workspace): a new tab shows in its workspace; a row opens its tabs

A tab opened bare from + had no session row until its first hook, and
the sidebar re-read its live panes only when a workspace's status or
session moved, which a tab in slots 1-3 never does: the new tab showed
nowhere. The sidebar now lists a live AI tab whose session has no row
yet as "New session" straight from the run (its session id is known
from the start) and follows the Workspace page's own run-list poll
(LIVE_RUNS_EVENT).

A sidebar workspace row now opens the workspace's live tabs on the pane
it last focused, and its home only when it has none; a Home button at
the left end of the tab bar reaches the home of a workspace whose tabs
keep running.
This commit is contained in:
2026-10-03 16:05:15 +07:00
parent 326dfa9d19
commit 8098f446b6
10 changed files with 205 additions and 21 deletions
@@ -11,7 +11,7 @@
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor, within, fireEvent } from "@testing-library/react";
import { act, render, screen, waitFor, within, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
import type { Lane, Session } from "../../lib/types";
@@ -243,13 +243,93 @@ describe("WorkspaceTree", () => {
expect(api.lanes.action).not.toHaveBeenCalled();
});
it("shows a live tab whose session has no row yet as a New session, never a shell", async () => {
vi.mocked(api.run.list).mockImplementation(
async () =>
({
items: [
{
id: "agenthub-lane-1-2",
laneId: 1,
status: "running",
sessionId: "s-live",
tool: "claude",
},
{
id: "agenthub-lane-1",
laneId: 1,
status: "running",
sessionId: "s-fresh",
tool: "claude",
},
{
id: "agenthub-lane-1-3",
laneId: 1,
status: "running",
sessionId: null,
tool: "shell",
},
],
}) as never
);
const user = userEvent.setup();
renderTree();
await user.click(await screen.findByTestId("workspace-toggle-1"));
const fresh = await screen.findByTestId("workspace-session-new-agenthub-lane-1");
expect(fresh).toHaveTextContent("New session");
expect(screen.queryByTestId("workspace-session-new-agenthub-lane-1-3")).toBeNull();
expect(screen.queryByTestId("workspace-session-new-agenthub-lane-1-2")).toBeNull();
await user.click(fresh);
expect(screen.getByTestId("where").textContent).toBe("/workspace?lane=1&run=agenthub-lane-1");
});
it("selects by URL, and a click selects and expands", async () => {
const user = userEvent.setup();
renderTree("/workspace?lane=2");
expect(await screen.findByTestId("workspace-open-2")).toHaveAttribute("aria-current", "true");
// Lane 1 has a live tab: its row opens the tab, not the home.
await waitFor(() => expect(api.run.list).toHaveBeenCalled());
await user.click(screen.getByTestId("workspace-open-1"));
expect(screen.getByTestId("where").textContent).toBe("/workspace?lane=1");
expect(screen.getByTestId("where").textContent).toBe("/workspace?lane=1&run=agenthub-lane-1-2");
expect(await screen.findByTestId("workspace-sessions-1")).toBeInTheDocument();
// Lane 2 has none: its home.
await user.click(screen.getByTestId("workspace-open-2"));
expect(screen.getByTestId("where").textContent).toBe("/workspace?lane=2");
});
it("opens a workspace on the pane it last focused, and follows the page's run list", async () => {
vi.mocked(api.run.list).mockImplementation(async () => ({ items: [] }) as never);
localStorage.setItem(
"agenthub.workspace.paneLayout",
JSON.stringify({
1: {
root: {
type: "split",
dir: "row",
ratio: 0.5,
a: { type: "pane", slot: 0 },
b: { type: "pane", slot: 2 },
},
focus: 2,
},
})
);
const user = userEvent.setup();
renderTree();
await screen.findByTestId("workspace-open-1");
// The Workspace page's poll lands: two live tabs on lane 1.
act(() => {
window.dispatchEvent(
new CustomEvent("agenthub:live-runs", {
detail: [
{ id: "agenthub-lane-1", laneId: 1, slot: 0, status: "running", tool: "claude" },
{ id: "agenthub-lane-1-2", laneId: 1, slot: 2, status: "running", tool: "claude" },
],
})
);
});
await user.click(screen.getByTestId("workspace-open-1"));
expect(screen.getByTestId("where").textContent).toBe("/workspace?lane=1&run=agenthub-lane-1-2");
});
it("reorders with Alt+ArrowUp and keeps the order in localStorage", async () => {