Merge pull request #600 from Molecule-AI/feat/issue-592-workspace-cost-transparency

feat(canvas): scaffold WorkspaceUsage component for #592
This commit is contained in:
molecule-ai[bot] 2026-04-17 05:32:40 +00:00 committed by GitHub
commit 6c72958e80
3 changed files with 134 additions and 0 deletions

View File

@ -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 (
<div
className="rounded-md border border-zinc-700 bg-zinc-900 p-3 space-y-2"
data-testid="workspace-usage"
>
<div className="flex items-center justify-between">
<h4 className="text-xs font-semibold text-zinc-400 uppercase tracking-wider">
Usage
</h4>
<span
className="text-[10px] text-zinc-500 bg-zinc-800 border border-zinc-700 rounded px-1.5 py-0.5"
data-testid="usage-pending-badge"
>
pending #593
</span>
</div>
{/* Placeholder stat rows — will be replaced with live data once #593 lands */}
<div className="space-y-1.5" data-testid="usage-stats">
<StatRow label="Input tokens" value="—" testId="usage-input-tokens" />
<StatRow label="Output tokens" value="—" testId="usage-output-tokens" />
<StatRow label="Estimated cost" value="—" testId="usage-estimated-cost" />
</div>
</div>
);
}
function StatRow({
label,
value,
testId,
}: {
label: string;
value: string;
testId?: string;
}) {
return (
<div className="flex justify-between items-center" data-testid={testId}>
<span className="text-xs text-zinc-500">{label}</span>
<span className="text-xs text-zinc-400 font-mono">{value}</span>
</div>
);
}

View File

@ -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(
<WorkspaceUsage workspaceId="ws-test-123" />
);
expect(container.firstChild).toBeTruthy();
});
it("renders the Usage heading", () => {
render(<WorkspaceUsage workspaceId="ws-test-123" />);
expect(screen.getByText("Usage")).toBeTruthy();
});
it("renders the pending #593 badge", () => {
render(<WorkspaceUsage workspaceId="ws-test-123" />);
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(<WorkspaceUsage workspaceId="ws-test-123" />);
expect(screen.getByTestId("workspace-usage")).toBeTruthy();
expect(screen.getByTestId("usage-stats")).toBeTruthy();
});
it("renders Input tokens row with placeholder dash", () => {
render(<WorkspaceUsage workspaceId="ws-test-123" />);
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(<WorkspaceUsage workspaceId="ws-test-123" />);
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(<WorkspaceUsage workspaceId="ws-test-123" />);
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(<WorkspaceUsage workspaceId={id} />);
expect(screen.getByTestId("workspace-usage")).toBeTruthy();
unmount();
}
});
it("does not display live token counts or dollar amounts", () => {
render(<WorkspaceUsage workspaceId="ws-test-123" />);
const stats = screen.getByTestId("usage-stats");
// Placeholder state must not contain any digit sequences
expect(stats.textContent).not.toMatch(/\d+/);
});
});

View File

@ -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) {
)}
</Section>
{/* Token usage + spend (scaffold — wired to GET /workspaces/:id/metrics once #593 lands) */}
<WorkspaceUsage workspaceId={workspaceId} />
{/* Agent Card / Skills */}
{skills.length > 0 && (
<Section title="Skills">