feat(workspace): the console tab bar is the session view's only header

The status/name/back-arrow row is gone; Tools sits at the tab bar's right
end and the pipeline/feature pickers show only with a template.
LaneConsolePane is reduced to the terminal (no title, no Active runs, no
New run): RunHistory.tsx, api.run.history and the page's active-sessions
poll are removed.
This commit is contained in:
2026-10-03 12:13:11 +07:00
parent 4df944c6aa
commit b7efba8747
17 changed files with 177 additions and 1941 deletions
@@ -1,8 +1,8 @@
/**
* @file LaneConsolePane.test.tsx
* @description Test suite for the LaneConsolePane component: a pane shows its
* lane's terminal or an empty state whose New session goes to the workspace
* home — a pane has no run form of its own.
* @description Test suite for the LaneConsolePane component: a tab's body shows
* its lane's terminal or an empty state whose New session goes to the
* workspace home — no run form and no header of its own.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
@@ -38,7 +38,6 @@ vi.mock("../../../lib/api", () => ({
},
run: {
list: vi.fn().mockResolvedValue({ items: [] }),
history: vi.fn().mockResolvedValue({ items: [] }),
get: vi.fn(),
start: vi.fn(),
},
@@ -85,16 +84,12 @@ function baseProps() {
return {
lanes: [LANE],
laneId: 1,
showLaneSelector: false,
onLaneIdChange: vi.fn(),
onLaneCreated: vi.fn(),
binaryStatus: {
claude: { found: true, path: "/usr/local/bin/claude" },
codex: { found: true, path: "/usr/local/bin/codex" },
antigravity: { found: true, path: "/usr/local/bin/agy" },
},
activeRuns: { items: [] },
wsConnected: true,
};
}
@@ -113,7 +108,7 @@ describe("LaneConsolePane", () => {
expect(api.lanes.action).not.toHaveBeenCalled();
});
it("goes back to the home when a single pane's run exits, and on New run", async () => {
it("has no header: no title, no Active runs, no New run - the tab bar is the header", async () => {
const live = {
id: "agenthub-lane-1",
laneId: 1,
@@ -128,22 +123,19 @@ describe("LaneConsolePane", () => {
promptPreview: null,
tool: "claude" as const,
};
const onNewSession = vi.fn();
const onRunExit = vi.fn();
render(
<LaneConsolePane
{...baseProps()}
activeRuns={{ items: [live] }}
onNewSession={onNewSession}
onRunExit={onNewSession}
/>
<LaneConsolePane {...baseProps()} activeRuns={{ items: [live] }} onRunExit={onRunExit} />
);
await screen.findByTestId("terminal-view");
fireEvent.click(screen.getByRole("button", { name: "New run" }));
expect(onNewSession).toHaveBeenCalledTimes(1);
expect(screen.queryByRole("heading")).toBeNull();
expect(screen.queryByRole("button", { name: "New run" })).toBeNull();
expect(screen.queryByRole("button", { name: /active run/i })).toBeNull();
// The run exits: the tab bar is told, and the pane is left empty.
fireEvent.click(screen.getByTestId("fake-pane-exit"));
await waitFor(() => expect(screen.queryByTestId("terminal-view")).not.toBeInTheDocument());
expect(onNewSession).toHaveBeenCalledTimes(2);
expect(onRunExit).toHaveBeenCalledOnce();
expect(screen.getByTestId("pane-no-run")).toBeInTheDocument();
});
@@ -185,138 +177,12 @@ describe("LaneConsolePane", () => {
await waitFor(() => expect(screen.queryByTestId("terminal-view")).not.toBeInTheDocument());
});
it("shows a lane dropdown only when showLaneSelector is true", () => {
const { rerender } = render(<LaneConsolePane {...baseProps()} showLaneSelector />);
expect(screen.getByTestId("pane-lane-select")).toBeInTheDocument();
rerender(<LaneConsolePane {...baseProps()} showLaneSelector={false} />);
expect(screen.queryByTestId("pane-lane-select")).not.toBeInTheDocument();
});
it("renders the console body when laneId is null and showLaneSelector is false (layout-1, fresh install)", () => {
render(<LaneConsolePane {...baseProps()} laneId={null} showLaneSelector={false} />);
it("renders the console body when laneId is null (fresh install)", () => {
render(<LaneConsolePane {...baseProps()} laneId={null} />);
expect(screen.getByTestId("console-body")).toBeInTheDocument();
expect(screen.queryByTestId("pane-empty")).not.toBeInTheDocument();
});
it("attaches to the lane's still-live session instead of failing when Resume hits ERUNLIVE", async () => {
const HISTORY_ITEM = {
id: "run-old",
session_id: "sess-old",
cwd: "/workspace/a",
model: null,
permission_mode: null,
effort: null,
resume_session_id: null,
prompt_preview: "earlier prompt",
status: "abandoned" as const,
exit_code: null,
started_at: new Date(1000).toISOString(),
ended_at: new Date(2000).toISOString(),
isLive: false,
tool: "claude" as const,
};
(api.run.history as ReturnType<typeof vi.fn>).mockResolvedValue({ items: [HISTORY_ITEM] });
(api.lanes.action as ReturnType<typeof vi.fn>).mockRejectedValue(
// Matches the real shape `request()` throws: a plain, human-readable
// `.message` plus a `.code` a caller actually branches on.
Object.assign(new Error("lane already has a live run"), { code: "ERUNLIVE", status: 409 })
);
(api.lanes.list as ReturnType<typeof vi.fn>).mockResolvedValue({
lanes: [{ ...LANE, run_id: "run-live" }],
});
(api.run.get as ReturnType<typeof vi.fn>).mockResolvedValue({
id: "run-live",
laneId: 1,
status: "running",
cwd: "/workspace/a",
model: null,
permissionMode: null,
effort: null,
resumeSessionId: null,
sessionId: null,
startedAt: null,
promptPreview: null,
});
render(
<MemoryRouter>
<LaneConsolePane {...baseProps()} />
</MemoryRouter>
);
fireEvent.click(await screen.findByRole("button", { name: /active runs/i }));
const resumeButtons = await screen.findAllByRole("button", {
name: "Resume existing session",
});
fireEvent.click(resumeButtons[0]!);
await waitFor(() =>
expect(screen.getByTestId("terminal-view")).toHaveAttribute("data-run-id", "run-live")
);
expect(screen.getByText(/attached to it instead/i)).toBeInTheDocument();
});
it("renders an empty placeholder with selector when laneId is null but showLaneSelector is true (split-view)", () => {
render(<LaneConsolePane {...baseProps()} laneId={null} showLaneSelector={true} />);
expect(screen.getByTestId("pane-empty")).toBeInTheDocument();
expect(screen.getByTestId("pane-lane-select")).toBeInTheDocument();
expect(screen.queryByTestId("console-body")).not.toBeInTheDocument();
});
it("lists only this lane's runs and sessions in Active runs, not every lane's", () => {
const run = (id: string, laneId: number, promptPreview: string) => ({
id,
laneId,
status: "running",
cwd: "/workspace/x",
model: null,
permissionMode: null,
effort: null,
resumeSessionId: null,
sessionId: null,
startedAt: "2026-09-24T01:00:00Z",
promptPreview,
});
const session = (id: string, cwd: string, name: string) =>
({
id,
cwd,
name,
model: null,
source: "local",
started_at: "2026-09-24T01:00:00Z",
}) as never;
render(
<MemoryRouter>
<LaneConsolePane
{...baseProps()}
lanes={[
LANE,
{ ...LANE, id: 2, cwd: "/workspace/b" },
// Nested inside lane 1: its sessions are ITS own, by longest prefix.
{ ...LANE, id: 3, cwd: "/workspace/a/nested" },
]}
activeRuns={{ items: [run("r1", 1, "mine"), run("r2", 2, "other lane's")] as never }}
externalSessions={[
session("s1", "/workspace/a/src", "my session"),
session("s2", "/workspace/b", "b session"),
session("s3", "/workspace/a/nested/x", "nested session"),
session("s4", "/workspace/ab", "sibling prefix"),
]}
/>
</MemoryRouter>
);
fireEvent.click(screen.getByRole("button", { name: /2 active runs/ }));
expect(screen.getByText("mine")).toBeTruthy();
expect(screen.getByText("my session")).toBeTruthy();
expect(screen.queryByText("other lane's")).toBeNull();
expect(screen.queryByText("b session")).toBeNull();
expect(screen.queryByText("nested session")).toBeNull();
expect(screen.queryByText("sibling prefix")).toBeNull();
});
describe("split panes on one lane", () => {
describe("tabs on one lane", () => {
const run = (id: string, promptPreview = id) => ({
id,
laneId: 1,
@@ -342,24 +208,6 @@ describe("LaneConsolePane", () => {
)
);
});
it("leaves runs other panes show out of Active runs", () => {
render(
<MemoryRouter>
<LaneConsolePane
{...baseProps()}
slot={1}
activeRuns={{
items: [run("agenthub-lane-1", "shown"), run("agenthub-lane-1-3", "free")],
}}
hiddenRunIds={["agenthub-lane-1"]}
/>
</MemoryRouter>
);
fireEvent.click(screen.getByRole("button", { name: /1 active run/ }));
expect(screen.getByText("free")).toBeTruthy();
expect(screen.queryByText("shown")).toBeNull();
});
});
it("attaches to a run picked in the sidebar without starting or resuming anything", async () => {