Skip to content

Commit d139e48

Browse files
committed
feat(workspace): add token trend section actions
1 parent cb7bac4 commit d139e48

10 files changed

Lines changed: 211 additions & 38 deletions

packages/web/src/features/workspace/views/desktop/workspace-desktop-view.test.tsx

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@ vi.mock("../../../../lib/i18n", () => ({
1919
"workspace.sidebar.label": "Workspace Sidebar",
2020
"workspace.sidebar.workspace": "Workspace",
2121
"workspace.sidebar.open_editors": "Open Files",
22+
"workspace.sidebar.agent_instructions": "Agent Instructions",
2223
"workspace.no_workspace": "No workspace",
2324
"workspace.search.empty": "Type to search across file contents",
2425
"workspace.search.placeholder": "Search",
26+
"workspace.agent_instructions.project_title": "Project Agent.md",
27+
"workspace.agent_instructions.token_trend.title": "Token Trend",
2528
"common.loading": "Loading",
2629
"topbar.current_project": "Current Project",
2730
};
@@ -80,7 +83,27 @@ vi.mock("../shared/git-panel", () => ({
8083
GitPanel: () => <div>Source Control body</div>,
8184
}));
8285

83-
function renderDesktopView(activeView: "explorer" | "search" | "source-control") {
86+
vi.mock("../shared/agent-instructions-section", () => ({
87+
AgentInstructionsSection: ({ workspaceId }: { workspaceId: string }) => (
88+
<section
89+
className="workspace-sidebar-section workspace-agent-instructions"
90+
data-testid="agent-instructions-section"
91+
data-workspace-id={workspaceId}
92+
>
93+
<h2 className="workspace-sidebar-section__title">Project Agent.md</h2>
94+
</section>
95+
),
96+
}));
97+
98+
vi.mock("../shared/agent-instructions-token-trend", () => ({
99+
AgentInstructionsTokenTrend: ({ workspacePath }: { workspacePath: string }) => (
100+
<div data-testid="agent-token-trend" data-workspace-path={workspacePath} />
101+
),
102+
}));
103+
104+
function renderDesktopView(
105+
activeView: "explorer" | "search" | "source-control" | "agent-instructions"
106+
) {
84107
const store = createStore();
85108
store.set(connectionStatusAtom, "connected");
86109
store.set(wsClientAtom, {
@@ -147,4 +170,23 @@ describe("WorkspaceDesktopView", () => {
147170
expect(screen.getByText("Source Control body")).toBeInTheDocument();
148171
expect(screen.queryByText("Source Control", { selector: ".panel-header" })).toBeNull();
149172
});
173+
174+
it("renders token trend as a separate section above AGENT.MD", () => {
175+
renderDesktopView("agent-instructions");
176+
177+
const trendHeading = screen.getByRole("heading", { level: 2, name: "Token Trend" });
178+
const trendSection = trendHeading.closest(".workspace-sidebar-section");
179+
const agentInstructionsSection = screen.getByTestId("agent-instructions-section");
180+
181+
expect(trendSection).toHaveClass("workspace-agent-token-trend-section");
182+
expect(screen.getByTestId("agent-token-trend")).toHaveAttribute(
183+
"data-workspace-path",
184+
"/tmp/ws-test"
185+
);
186+
expect(
187+
trendSection?.compareDocumentPosition(agentInstructionsSection) &
188+
Node.DOCUMENT_POSITION_FOLLOWING
189+
).toBeTruthy();
190+
expect(agentInstructionsSection).not.toContainElement(screen.getByTestId("agent-token-trend"));
191+
});
150192
});

packages/web/src/features/workspace/views/desktop/workspace-desktop-view.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { useWorkspaceScreenModel } from "../../actions/use-workspace-screen-mode
1313
import { sidebarCollapsedAtom } from "../../atoms";
1414
import { sanitizeDesktopSidebarView } from "../../atoms/layout";
1515
import { AgentInstructionsSection } from "../shared/agent-instructions-section";
16+
import { AgentTokenTrendSection } from "../shared/agent-token-trend-section";
1617
import { ExplorerPanel } from "../shared/explorer-panel";
1718
import { GitPanel } from "../shared/git-panel";
1819
import { SearchPanel } from "../shared/search-panel";
@@ -162,6 +163,7 @@ const WorkspaceDesktopScene: FC = () => {
162163
{activeSidebarView === "agent-instructions" ? (
163164
<div className="workspace-sidebar-view">
164165
<div className="workspace-sidebar-panel__body workspace-sidebar-panel__body--stacked">
166+
<AgentTokenTrendSection workspacePath={workspace.path} />
165167
<AgentInstructionsSection workspaceId={workspace.id} />
166168
</div>
167169
</div>

packages/web/src/features/workspace/views/shared/agent-instructions-section.test.tsx

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ vi.mock("../../../../lib/i18n", () => ({
128128
"workspace.agent_instructions.collapse_label": "Collapse Project Agent.md",
129129
"workspace.agent_instructions.system_expand_label": "Expand System Agent.md",
130130
"workspace.agent_instructions.system_collapse_label": "Collapse System Agent.md",
131+
"workspace.agent_instructions.token_trend.title": "Token Trend",
131132
"common.loading": "Loading...",
132133
};
133134

@@ -149,14 +150,6 @@ vi.mock("../../../code-editor/actions/use-open-location", () => ({
149150
}),
150151
}));
151152

152-
vi.mock("./agent-instructions-token-trend", () => ({
153-
AgentInstructionsTokenTrend: ({ workspacePath }: { workspacePath: string }) => (
154-
<section data-testid="agent-token-trend" data-workspace-path={workspacePath}>
155-
Token trend mock
156-
</section>
157-
),
158-
}));
159-
160153
function createProvider(
161154
overrides: Partial<TestProviderListItem> & Pick<TestProviderListItem, "id">
162155
): TestProviderListItem {
@@ -475,19 +468,14 @@ describe("AgentInstructionsSection", () => {
475468
expect(toggle).toHaveAttribute("aria-expanded", "true");
476469
});
477470

478-
it("renders the token trend as the first expanded body block for the current workspace", async () => {
471+
it("does not render the token trend inside the AGENT.MD section", async () => {
479472
renderSection({});
480473

481-
const trend = await screen.findByTestId("agent-token-trend");
482-
const projectHeading = await screen.findByRole("heading", {
483-
level: 3,
484-
name: "Project Agent.md",
485-
});
474+
await screen.findByRole("heading", { level: 2, name: "Project Agent.md" });
486475

487-
expect(trend).toHaveAttribute("data-workspace-path", "/workspace");
488476
expect(
489-
trend.compareDocumentPosition(projectHeading) & Node.DOCUMENT_POSITION_FOLLOWING
490-
).toBeTruthy();
477+
screen.queryByRole("heading", { level: 3, name: "Token Trend" })
478+
).not.toBeInTheDocument();
491479
});
492480

493481
it("renders project and system agent instruction groups", async () => {
@@ -496,6 +484,7 @@ describe("AgentInstructionsSection", () => {
496484
expect(
497485
await screen.findByRole("heading", { level: 2, name: "Project Agent.md" })
498486
).toBeInTheDocument();
487+
expect(screen.queryByRole("heading", { level: 3, name: "Project Agent.md" })).toBeNull();
499488
expect(screen.getByRole("heading", { level: 3, name: "System Agent.md" })).toBeInTheDocument();
500489
expect(screen.getByText("Codex")).toBeInTheDocument();
501490
expect(screen.getByText("~/.codex/AGENTS.md")).toBeInTheDocument();

packages/web/src/features/workspace/views/shared/agent-instructions-section.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { Button, IconButton, Notice, Tooltip } from "../../../../components/ui";
55
import { useTranslation } from "../../../../lib/i18n";
66
import { useAgentInstructionsActions } from "../../actions/use-agent-instructions-actions";
77
import { AgentInstructionsGenerateDialog } from "./agent-instructions-generate-dialog";
8-
import { AgentInstructionsTokenTrend } from "./agent-instructions-token-trend";
98

109
interface AgentInstructionsSectionProps {
1110
workspaceId: string;
@@ -105,18 +104,13 @@ export const AgentInstructionsSection: FC<AgentInstructionsSectionProps> = ({ wo
105104

106105
{isExpanded ? (
107106
<div className="workspace-agent-instructions__body">
108-
{workspace?.path ? <AgentInstructionsTokenTrend workspacePath={workspace.path} /> : null}
109-
110107
{error ? (
111108
<Notice className="workspace-agent-instructions__notice" message={error} tone="error" />
112109
) : null}
113110

114111
{status ? (
115112
<>
116113
<div className="workspace-agent-instructions__group">
117-
<h3 className="workspace-sidebar-section__title workspace-agent-instructions__group-title">
118-
{projectTitle}
119-
</h3>
120114
<div className="workspace-agent-instructions__status" aria-label={projectTitle}>
121115
<div className="workspace-agent-instructions__status-main">
122116
<span className="workspace-agent-instructions__status-pill">{statusLabel}</span>

packages/web/src/features/workspace/views/shared/agent-instructions-token-trend.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,9 @@ export function AgentInstructionsTokenTrend({ workspacePath }: AgentInstructions
198198
}, [chartData]);
199199

200200
return (
201-
<section className="workspace-agent-instructions__token-trend">
201+
<div className="workspace-agent-instructions__token-trend">
202202
<div className="workspace-agent-instructions__token-trend-header">
203203
<div>
204-
<h3 className="workspace-agent-instructions__token-trend-title">
205-
{t("workspace.agent_instructions.token_trend.title")}
206-
</h3>
207204
<p className="workspace-agent-instructions__token-trend-subtitle">
208205
{t("workspace.agent_instructions.token_trend.subtitle")}
209206
</p>
@@ -257,6 +254,6 @@ export function AgentInstructionsTokenTrend({ workspacePath }: AgentInstructions
257254
{t("workspace.agent_instructions.token_trend.error")}
258255
</p>
259256
) : null}
260-
</section>
257+
</div>
261258
);
262259
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// @vitest-environment jsdom
2+
3+
import { fireEvent, render, screen } from "@testing-library/react";
4+
import { beforeEach, describe, expect, it, vi } from "vitest";
5+
import { AgentTokenTrendSection } from "./agent-token-trend-section";
6+
7+
const navigateMock = vi.fn();
8+
9+
vi.mock("react-router-dom", async () => {
10+
const actual = await vi.importActual<typeof import("react-router-dom")>("react-router-dom");
11+
12+
return {
13+
...actual,
14+
useNavigate: () => navigateMock,
15+
};
16+
});
17+
18+
vi.mock("../../../../lib/i18n", () => ({
19+
useTranslation: () => (key: string) => {
20+
const translations: Record<string, string> = {
21+
"workspace.agent_instructions.token_trend.collapse_label": "Collapse Token Trend",
22+
"workspace.agent_instructions.token_trend.expand_label": "Expand Token Trend",
23+
"workspace.agent_instructions.token_trend.more_data": "More Data",
24+
"workspace.agent_instructions.token_trend.title": "Token Trend",
25+
};
26+
27+
return translations[key] ?? key;
28+
},
29+
}));
30+
31+
vi.mock("./agent-instructions-token-trend", () => ({
32+
AgentInstructionsTokenTrend: ({ workspacePath }: { workspacePath: string }) => (
33+
<div data-testid="agent-token-trend" data-workspace-path={workspacePath} />
34+
),
35+
}));
36+
37+
describe("AgentTokenTrendSection", () => {
38+
beforeEach(() => {
39+
navigateMock.mockClear();
40+
});
41+
42+
it("renders token trend with collapsible standalone sidebar section chrome", () => {
43+
render(<AgentTokenTrendSection workspacePath="/repo/project" />);
44+
45+
const heading = screen.getByRole("heading", { level: 2, name: "Token Trend" });
46+
const section = heading.closest("section");
47+
const toggle = screen.getByRole("button", { name: "Collapse Token Trend" });
48+
49+
expect(section).toHaveClass("workspace-sidebar-section");
50+
expect(section).toHaveClass("workspace-agent-token-trend-section");
51+
expect(heading).toHaveClass("workspace-sidebar-section__title");
52+
expect(toggle).toHaveClass("workspace-sidebar-section__chevron");
53+
expect(toggle).toHaveAttribute("aria-expanded", "true");
54+
expect(toggle).toHaveAttribute("aria-controls");
55+
expect(screen.getByTestId("agent-token-trend")).toHaveAttribute(
56+
"data-workspace-path",
57+
"/repo/project"
58+
);
59+
60+
fireEvent.click(toggle);
61+
62+
expect(screen.queryByTestId("agent-token-trend")).not.toBeInTheDocument();
63+
expect(screen.getByRole("button", { name: "Expand Token Trend" })).toHaveAttribute(
64+
"aria-expanded",
65+
"false"
66+
);
67+
});
68+
69+
it("navigates to work analysis scoped to the current workspace from the more data action", () => {
70+
render(<AgentTokenTrendSection workspacePath="/repo/project" />);
71+
72+
const moreDataButton = screen.getByRole("button", { name: "More Data" });
73+
74+
expect(moreDataButton.closest(".workspace-sidebar-section__actions")).toHaveClass(
75+
"workspace-sidebar-panel__actions"
76+
);
77+
78+
fireEvent.click(moreDataButton);
79+
80+
expect(navigateMock).toHaveBeenCalledWith(
81+
"/settings?section=analysis&workspacePath=%2Frepo%2Fproject"
82+
);
83+
});
84+
});
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { ChevronDown, ChevronRight } from "lucide-react";
2+
import type { FC } from "react";
3+
import { useState } from "react";
4+
import { useNavigate } from "react-router-dom";
5+
import { Button, IconButton, Tooltip } from "../../../../components/ui";
6+
import { useTranslation } from "../../../../lib/i18n";
7+
import { buildWorkAnalyticsPath } from "../../../work-analysis/navigation";
8+
import { AgentInstructionsTokenTrend } from "./agent-instructions-token-trend";
9+
10+
interface AgentTokenTrendSectionProps {
11+
workspacePath: string;
12+
}
13+
14+
export const AgentTokenTrendSection: FC<AgentTokenTrendSectionProps> = ({ workspacePath }) => {
15+
const t = useTranslation();
16+
const [isExpanded, setIsExpanded] = useState(true);
17+
const panelId = "workspace-agent-token-trend-panel";
18+
const toggleLabel = isExpanded
19+
? t("workspace.agent_instructions.token_trend.collapse_label")
20+
: t("workspace.agent_instructions.token_trend.expand_label");
21+
const navigate = useNavigate();
22+
23+
return (
24+
<section className="workspace-sidebar-section workspace-agent-token-trend-section">
25+
<div className="workspace-sidebar-section__header">
26+
<div className="workspace-sidebar-section__header-main">
27+
<Tooltip content={toggleLabel}>
28+
<IconButton
29+
aria-controls={panelId}
30+
aria-expanded={isExpanded}
31+
aria-label={toggleLabel}
32+
className="workspace-sidebar-section__chevron"
33+
icon={isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
34+
onClick={() => setIsExpanded((current) => !current)}
35+
size="sm"
36+
/>
37+
</Tooltip>
38+
<h2 className="workspace-sidebar-section__title">
39+
{t("workspace.agent_instructions.token_trend.title")}
40+
</h2>
41+
</div>
42+
<div className="workspace-sidebar-panel__actions workspace-sidebar-section__actions">
43+
<Button
44+
className="workspace-agent-token-trend-section__more-data"
45+
onClick={() =>
46+
navigate(
47+
buildWorkAnalyticsPath({
48+
workspacePaths: [workspacePath],
49+
})
50+
)
51+
}
52+
size="sm"
53+
type="button"
54+
variant="ghost"
55+
>
56+
{t("workspace.agent_instructions.token_trend.more_data")}
57+
</Button>
58+
</div>
59+
</div>
60+
{isExpanded ? (
61+
<div id={panelId}>
62+
<AgentInstructionsTokenTrend workspacePath={workspacePath} />
63+
</div>
64+
) : null}
65+
</section>
66+
);
67+
};

packages/web/src/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,9 @@
255255
},
256256
"token_trend": {
257257
"title": "Token Trend",
258+
"expand_label": "Expand Token Trend",
259+
"collapse_label": "Collapse Token Trend",
260+
"more_data": "More Data",
258261
"subtitle": "Current project · Last 24 hours",
259262
"loading": "Loading token trend...",
260263
"empty": "No token data in the last 24 hours.",

packages/web/src/locales/zh.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,9 @@
255255
},
256256
"token_trend": {
257257
"title": "Token 消耗趋势",
258+
"expand_label": "展开 Token 消耗趋势",
259+
"collapse_label": "收起 Token 消耗趋势",
260+
"more_data": "更多数据",
258261
"subtitle": "当前项目 · 最近 24 小时",
259262
"loading": "正在加载 token 趋势...",
260263
"empty": "最近 24 小时暂无 token 数据。",

packages/web/src/styles/components.css

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14838,16 +14838,8 @@ body.is-dragging-pane .session-action-btn-drag {
1483814838
gap: var(--gap-tight);
1483914839
}
1484014840

14841-
.workspace-agent-instructions__token-trend-title {
14842-
margin: 0;
14843-
color: var(--text-primary);
14844-
font-size: var(--type-body-5-size);
14845-
line-height: var(--type-body-5-line-height);
14846-
font-weight: var(--type-body-6-weight);
14847-
}
14848-
1484914841
.workspace-agent-instructions__token-trend-subtitle {
14850-
margin: 2px 0 0;
14842+
margin: 0;
1485114843
color: var(--text-tertiary);
1485214844
font-size: var(--type-body-6-size);
1485314845
line-height: var(--type-body-6-line-height);

0 commit comments

Comments
 (0)