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 && (