From 1e0c2eed3b60fadc9a99e4e2faaf8c082f9d927b Mon Sep 17 00:00:00 2001 From: Molecule AI Frontend Engineer Date: Fri, 17 Apr 2026 05:16:57 +0000 Subject: [PATCH] feat(canvas): scaffold WorkspaceUsage component for #592 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds WorkspaceUsage component to canvas/src/components/ with three placeholder stat rows (Input tokens, Output tokens, Estimated cost) and a "pending #593" badge. Wires into DetailsTab between the Workspace and Skills sections. No API calls yet — fetch logic will be added once GET /workspaces/:id/metrics lands in #593. 9 tests in WorkspaceUsage.test.tsx; all 548 canvas tests pass. Co-Authored-By: Claude Sonnet 4.6 --- canvas/src/components/WorkspaceUsage.tsx | 55 ++++++++++++++ .../__tests__/WorkspaceUsage.test.tsx | 75 +++++++++++++++++++ canvas/src/components/tabs/DetailsTab.tsx | 4 + 3 files changed, 134 insertions(+) create mode 100644 canvas/src/components/WorkspaceUsage.tsx create mode 100644 canvas/src/components/__tests__/WorkspaceUsage.test.tsx diff --git a/canvas/src/components/WorkspaceUsage.tsx b/canvas/src/components/WorkspaceUsage.tsx new file mode 100644 index 00000000..f09b6932 --- /dev/null +++ b/canvas/src/components/WorkspaceUsage.tsx @@ -0,0 +1,55 @@ +'use client'; + +// WorkspaceUsage — Usage panel for a single workspace. +// Currently renders placeholder stat rows. +// TODO: fetch GET /workspaces/:id/metrics when #593 lands and replace +// placeholder values with real token/cost data from the response. + +export interface WorkspaceUsageProps { + workspaceId: string; +} + +export function WorkspaceUsage({ workspaceId: _workspaceId }: WorkspaceUsageProps) { + return ( +
+
+

+ Usage +

+ + pending #593 + +
+ + {/* Placeholder stat rows — will be replaced with live data once #593 lands */} +
+ + + +
+
+ ); +} + +function StatRow({ + label, + value, + testId, +}: { + label: string; + value: string; + testId?: string; +}) { + return ( +
+ {label} + {value} +
+ ); +} diff --git a/canvas/src/components/__tests__/WorkspaceUsage.test.tsx b/canvas/src/components/__tests__/WorkspaceUsage.test.tsx new file mode 100644 index 00000000..af9facc6 --- /dev/null +++ b/canvas/src/components/__tests__/WorkspaceUsage.test.tsx @@ -0,0 +1,75 @@ +// @vitest-environment jsdom +import { describe, it, expect, afterEach } from "vitest"; +import { render, screen, cleanup } from "@testing-library/react"; +import { WorkspaceUsage } from "../WorkspaceUsage"; + +afterEach(() => { + cleanup(); +}); + +describe("WorkspaceUsage", () => { + it("renders without crashing", () => { + const { container } = render( + + ); + expect(container.firstChild).toBeTruthy(); + }); + + it("renders the Usage heading", () => { + render(); + expect(screen.getByText("Usage")).toBeTruthy(); + }); + + it("renders the pending #593 badge", () => { + render(); + const badge = screen.getByTestId("usage-pending-badge"); + expect(badge).toBeTruthy(); + expect(badge.textContent).toBe("pending #593"); + }); + + it("renders the outer container and stats container", () => { + render(); + expect(screen.getByTestId("workspace-usage")).toBeTruthy(); + expect(screen.getByTestId("usage-stats")).toBeTruthy(); + }); + + it("renders Input tokens row with placeholder dash", () => { + render(); + const row = screen.getByTestId("usage-input-tokens"); + expect(row).toBeTruthy(); + expect(row.textContent).toContain("Input tokens"); + expect(row.textContent).toContain("—"); + }); + + it("renders Output tokens row with placeholder dash", () => { + render(); + const row = screen.getByTestId("usage-output-tokens"); + expect(row).toBeTruthy(); + expect(row.textContent).toContain("Output tokens"); + expect(row.textContent).toContain("—"); + }); + + it("renders Estimated cost row with placeholder dash", () => { + render(); + const row = screen.getByTestId("usage-estimated-cost"); + expect(row).toBeTruthy(); + expect(row.textContent).toContain("Estimated cost"); + expect(row.textContent).toContain("—"); + }); + + it("accepts any workspaceId without throwing", () => { + const ids = ["", "ws-abc", "00000000-0000-0000-0000-000000000000"]; + for (const id of ids) { + const { unmount } = render(); + expect(screen.getByTestId("workspace-usage")).toBeTruthy(); + unmount(); + } + }); + + it("does not display live token counts or dollar amounts", () => { + render(); + const stats = screen.getByTestId("usage-stats"); + // Placeholder state must not contain any digit sequences + expect(stats.textContent).not.toMatch(/\d+/); + }); +}); diff --git a/canvas/src/components/tabs/DetailsTab.tsx b/canvas/src/components/tabs/DetailsTab.tsx index f4a53639..8891fee1 100644 --- a/canvas/src/components/tabs/DetailsTab.tsx +++ b/canvas/src/components/tabs/DetailsTab.tsx @@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from "react"; import { api } from "@/lib/api"; import { useCanvasStore, type WorkspaceNodeData } from "@/store/canvas"; import { StatusDot } from "../StatusDot"; +import { WorkspaceUsage } from "../WorkspaceUsage"; interface Props { workspaceId: string; @@ -190,6 +191,9 @@ export function DetailsTab({ workspaceId, data }: Props) { )} + {/* Token usage + spend (scaffold — wired to GET /workspaces/:id/metrics once #593 lands) */} + + {/* Agent Card / Skills */} {skills.length > 0 && (