feat: workspace aliases, tab groups of split panes, native path picker (v2.1.0)
- /api/workspaces and `agenthub workspace(s)` / `--workspace` as aliases of /api/lanes and `agenthub lanes` / `--lane`; old names print one stderr note. REPL `help lanes|workspace|workspaces` resolves through the aliases. - A workspace holds up to ten sessions (slots 0-9); every console tab is a group of 1-4 split panes (PaneGroups, agenthub.workspace.paneLayout); parseGroups keeps the stored active tab when an earlier one is dropped. - Browse button beside path fields: POST /api/lanes/pick-path opens the OS picker on the server's desktop — loopback only and no forwarding header (EREMOTE), a display failure is ENODIALOG not a Cancel, the dialog is killed when the request closes, a folder field falls back to the in-app browser on any failure. - Changes rail counts chip, image viewer (/git/raw), status bar session counts and per-day cost (`date=` on /api/pricing/cost). - /agenthub-open and /agenthub-doctor run from the INSTALLED build (supersededBy), doctor offers fixes behind two confirmations; plugin-open treats a locked bootstrap as non-fatal. - New-terminal shell waits for the first attach (issue #55). - repo-onboarding / debug-live-issue skills removed. - OpenAPI: pick-path, lane/date on /pricing/cost, tool on /run/history, /api/workspaces mount.
This commit is contained in:
@@ -81,6 +81,7 @@ vi.mock("../../lib/api", async (importOriginal) => {
|
||||
})),
|
||||
cwds: vi.fn(async () => ({ items: [] })),
|
||||
kill: vi.fn(async () => ({ ok: true })),
|
||||
history: vi.fn(async () => ({ items: [] })),
|
||||
},
|
||||
aiTools: {
|
||||
list: vi.fn(async () => ({ tools: [], default: "claude" })),
|
||||
@@ -307,7 +308,7 @@ 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 () => {
|
||||
it("shows a live tab whose session has no row yet as a New session, and a shell as Terminal", async () => {
|
||||
vi.mocked(api.run.list).mockImplementation(
|
||||
async () =>
|
||||
({
|
||||
@@ -341,27 +342,95 @@ describe("WorkspaceTree", () => {
|
||||
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.getByTestId("workspace-session-new-agenthub-lane-1-3")).toHaveTextContent(
|
||||
"Terminal"
|
||||
);
|
||||
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("lists a workspace's closed terminals, and a click opens a new shell tab", async () => {
|
||||
vi.mocked(api.run.history).mockImplementation(
|
||||
async () =>
|
||||
({
|
||||
items: [
|
||||
{
|
||||
id: "agenthub-lane-1-2",
|
||||
tool: "shell",
|
||||
status: "running",
|
||||
started_at: "2026-10-06T10:00:00Z",
|
||||
ended_at: null,
|
||||
isLive: true,
|
||||
},
|
||||
{
|
||||
id: "agenthub-lane-1-3@a",
|
||||
tool: "shell",
|
||||
status: "killed",
|
||||
started_at: "2026-10-06T09:00:00Z",
|
||||
ended_at: "2026-10-06T09:05:00Z",
|
||||
isLive: false,
|
||||
},
|
||||
{
|
||||
id: "agenthub-lane-1-1@b",
|
||||
tool: "shell",
|
||||
status: "exited",
|
||||
started_at: "2026-10-06T08:00:00Z",
|
||||
ended_at: "2026-10-06T08:05:00Z",
|
||||
isLive: false,
|
||||
},
|
||||
{
|
||||
id: "agenthub-lane-1-1@c",
|
||||
tool: "shell",
|
||||
status: "exited",
|
||||
started_at: "2026-10-06T07:00:00Z",
|
||||
ended_at: "2026-10-06T07:05:00Z",
|
||||
isLive: false,
|
||||
},
|
||||
{
|
||||
id: "agenthub-lane-1-1@d",
|
||||
tool: "shell",
|
||||
status: "exited",
|
||||
started_at: "2026-10-06T06:00:00Z",
|
||||
ended_at: "2026-10-06T06:05:00Z",
|
||||
isLive: false,
|
||||
},
|
||||
],
|
||||
}) as never
|
||||
);
|
||||
vi.mocked(api.lanes.action).mockImplementation(
|
||||
async () => ({ ok: true, run_id: "agenthub-lane-1-3" }) as never
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
renderTree();
|
||||
await user.click(await screen.findByTestId("workspace-toggle-1"));
|
||||
const closed = await screen.findByTestId("workspace-terminal-closed-agenthub-lane-1-3@a");
|
||||
expect(closed).toHaveTextContent("Terminal");
|
||||
expect(api.run.history).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ laneId: 1, tool: "shell" })
|
||||
);
|
||||
// The live one is not history, and only the newest three are listed.
|
||||
expect(screen.queryByTestId("workspace-terminal-closed-agenthub-lane-1-2")).toBeNull();
|
||||
expect(screen.getAllByTestId(/^workspace-terminal-closed-/)).toHaveLength(3);
|
||||
await user.click(closed);
|
||||
expect(api.lanes.action).toHaveBeenCalledWith(1, "start", { shell: true, slot: "auto" });
|
||||
expect(screen.getByTestId("where").textContent).toBe("/workspace?lane=1&run=agenthub-lane-1-3");
|
||||
});
|
||||
|
||||
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.
|
||||
// Lane 1 has a live tab: its row still opens 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&run=agenthub-lane-1-2");
|
||||
expect(screen.getByTestId("where").textContent).toBe("/workspace?lane=1");
|
||||
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 () => {
|
||||
it("opens a workspace's home even while it has live tabs", async () => {
|
||||
vi.mocked(api.run.list).mockImplementation(async () => ({ items: [] }) as never);
|
||||
localStorage.setItem(
|
||||
"agenthub.workspace.paneLayout",
|
||||
@@ -393,7 +462,7 @@ describe("WorkspaceTree", () => {
|
||||
);
|
||||
});
|
||||
await user.click(screen.getByTestId("workspace-open-1"));
|
||||
expect(screen.getByTestId("where").textContent).toBe("/workspace?lane=1&run=agenthub-lane-1-2");
|
||||
expect(screen.getByTestId("where").textContent).toBe("/workspace?lane=1");
|
||||
});
|
||||
|
||||
it("reorders with Alt+ArrowUp and keeps the order in localStorage", async () => {
|
||||
|
||||
Reference in New Issue
Block a user