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:
@@ -2,9 +2,9 @@
|
||||
* @file StatusBar.test.tsx
|
||||
* @description Tests for the shell's bottom status bar: the live/disconnected
|
||||
* state with the version (click opens the connection details), the running and
|
||||
* needs-you counts from the shared lane list, today's spend from the
|
||||
* pricing cost call's `daily_costs`, and on the Workspace page the open
|
||||
* workspace's and the shown session's totals.
|
||||
* needs-you SESSION counts, today's spend (`date=` on the pricing cost call)
|
||||
* with its tokens and a per-model hover list, and on the Workspace page the
|
||||
* open workspace's and the shown session's totals.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -13,6 +13,8 @@ import { act, render, screen } from "@testing-library/react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
const M = 1_000_000;
|
||||
|
||||
function today() {
|
||||
const d = new Date();
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
@@ -21,24 +23,50 @@ function today() {
|
||||
|
||||
vi.mock("../../lib/api", () => ({
|
||||
api: {
|
||||
lanes: {
|
||||
list: vi.fn().mockResolvedValue({
|
||||
lanes: [
|
||||
{ id: 1, status: "running", needs_action: null },
|
||||
{ id: 2, status: "running", needs_action: "permission" },
|
||||
{ id: 3, status: "idle", needs_action: null },
|
||||
],
|
||||
counts: {},
|
||||
}),
|
||||
sessions: {
|
||||
// 3 active sessions, 1 of them waiting on the user.
|
||||
list: vi.fn().mockImplementation(async (p: { awaiting?: boolean }) => ({
|
||||
sessions: [],
|
||||
total: p.awaiting ? 1 : 3,
|
||||
})),
|
||||
},
|
||||
pricing: {
|
||||
totalCost: vi.fn().mockImplementation(async (opts?: { lane?: number }) => ({
|
||||
total_cost: opts?.lane ? 123.456 : 10,
|
||||
breakdown: [],
|
||||
daily_costs: [
|
||||
{ date: "2000-01-01", cost: 9 },
|
||||
{ date: today(), cost: 1.234 },
|
||||
],
|
||||
totalCost: vi.fn().mockImplementation(async (opts?: { lane?: number; date?: string }) => ({
|
||||
total_cost: opts?.lane ? 123.456 : opts?.date === today() ? 1.234 : 10,
|
||||
breakdown: opts?.lane
|
||||
? []
|
||||
: [
|
||||
// Two buckets of one model fold into one line.
|
||||
{
|
||||
model: "claude-opus-4-8",
|
||||
input_tokens: 1 * M,
|
||||
output_tokens: 0,
|
||||
cache_read_tokens: 10 * M,
|
||||
cache_write_tokens: 0,
|
||||
cost: 1,
|
||||
matched_rule: null,
|
||||
},
|
||||
{
|
||||
model: "claude-opus-4-8",
|
||||
speed: "fast",
|
||||
input_tokens: 0,
|
||||
output_tokens: 1 * M,
|
||||
cache_read_tokens: 0,
|
||||
cache_write_tokens: 0,
|
||||
cost: 0.2,
|
||||
matched_rule: null,
|
||||
},
|
||||
{
|
||||
model: "claude-haiku-4-5",
|
||||
input_tokens: 300_000,
|
||||
output_tokens: 0,
|
||||
cache_read_tokens: 0,
|
||||
cache_write_tokens: 0,
|
||||
cost: 0.034,
|
||||
matched_rule: null,
|
||||
},
|
||||
],
|
||||
daily_costs: [],
|
||||
})),
|
||||
sessionCost: vi.fn().mockImplementation(async (id: string) => ({
|
||||
total_cost: id === "s-live" ? 4.5 : 7,
|
||||
@@ -63,7 +91,13 @@ describe("StatusBar", () => {
|
||||
at("/", <StatusBar wsConnected left="15rem" onConnectionClick={onConnectionClick} />);
|
||||
const conn = screen.getByRole("button", { name: /Live/ });
|
||||
expect(conn).toHaveTextContent(`v${__APP_VERSION__}`);
|
||||
expect(await screen.findByText("Today $1.23")).toBeInTheDocument();
|
||||
const cost = await screen.findByText("Today $1.23 (12.3M Tokens)");
|
||||
expect(api.pricing.totalCost).toHaveBeenCalledWith({ date: today() });
|
||||
expect(cost).toHaveAttribute(
|
||||
"title",
|
||||
"Claude Opus 4.8: 12.0M · $1.20\nClaude Haiku 4.5: 300.0K · $0.03"
|
||||
);
|
||||
await screen.findByText("1 need you");
|
||||
expect(screen.getByTestId("status-bar-counts").textContent).toBe("2 running · 1 need you");
|
||||
await user.click(conn);
|
||||
expect(onConnectionClick).toHaveBeenCalled();
|
||||
@@ -79,7 +113,7 @@ describe("StatusBar", () => {
|
||||
|
||||
it("off the Workspace page shows no workspace or session cost", async () => {
|
||||
at("/sessions", <StatusBar wsConnected left="0" onConnectionClick={() => {}} />);
|
||||
await screen.findByText("Today $1.23");
|
||||
await screen.findByText(/^Today \$1\.23/);
|
||||
expect(screen.queryByTestId("status-bar-workspace-cost")).toBeNull();
|
||||
expect(screen.queryByTestId("status-bar-session-cost")).toBeNull();
|
||||
});
|
||||
@@ -89,8 +123,8 @@ describe("StatusBar", () => {
|
||||
"/workspace?lane=3&session=s-old",
|
||||
<StatusBar wsConnected left="0" onConnectionClick={() => {}} />
|
||||
);
|
||||
expect(await screen.findByText("Workspace $123.46")).toBeInTheDocument();
|
||||
expect(await screen.findByText("Session $7.00")).toBeInTheDocument();
|
||||
expect(await screen.findByText("Workspace $123.46 (0 Tokens)")).toBeInTheDocument();
|
||||
expect(await screen.findByText(/^Session \$7\.00/)).toBeInTheDocument();
|
||||
expect(api.pricing.totalCost).toHaveBeenCalledWith({ lane: 3 });
|
||||
});
|
||||
|
||||
@@ -99,7 +133,7 @@ describe("StatusBar", () => {
|
||||
"/workspace?lane=3&run=agenthub-lane-3-1",
|
||||
<StatusBar wsConnected left="0" onConnectionClick={() => {}} />
|
||||
);
|
||||
await screen.findByText("Workspace $123.46");
|
||||
await screen.findByText(/^Workspace \$123\.46/);
|
||||
expect(screen.queryByTestId("status-bar-session-cost")).toBeNull();
|
||||
act(() => {
|
||||
window.dispatchEvent(
|
||||
@@ -108,6 +142,6 @@ describe("StatusBar", () => {
|
||||
})
|
||||
);
|
||||
});
|
||||
expect(await screen.findByText("Session $4.50")).toBeInTheDocument();
|
||||
expect(await screen.findByText(/^Session \$4\.50/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user