fix(dashboard): repair lint+test regressions from CSS sweep and panel rearrange

- eslint: ignore .claude/** so agent worktree scripts don't pull thousands of
  spurious "console/process is not defined" errors into the workspace lint.
- AgentsView: lift the controls popup max-height/overflow into a CSS modifier
  (.agent-controls-panel--scrollable) instead of an inline style so the
  inline-style discipline test stays at 4 (only runtime health colors).
- board-mobile.test: scan every @media (max-width: 768px) block in the loaded
  CSS bundle, not just the legacy section in styles.css. After the cards/
  inline-create extraction those rules now live in component CSS files.
- AgentsView.test: update the layout assertion for the new "stats above the
  collection" placement and open the controls popup before asserting on
  the token-usage panel (which now lives there, not in the main view body).
- agents-view-mobile.test: open the controls popup before asserting on the
  token-stats panel content.
- TaskDetailModal.test: bump tab-count assertions from 7→8 (or 6→7 for
  triage/todo) to include the new Stats tab, and switch to the Stats tab
  before asserting on token-usage text in the optimistic-loading suite.
- Column.test: add MoreVertical/ChevronDown/ChevronUp/Archive to the
  lucide-react vi.mock so the new column dropdown menu renders.

After this: pnpm lint = 0 errors, pnpm typecheck = clean, pnpm test = all
9132 dashboard + plugin/engine/cli/desktop/mobile/core tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-25 10:21:03 -07:00
parent 945f62ab0c
commit 6ff1617c39
8 changed files with 84 additions and 33 deletions

View File

@@ -114,6 +114,11 @@
gap: var(--space-md);
}
.agent-controls-panel--scrollable {
max-height: 70vh;
overflow-y: auto;
}
.agent-controls-panel .agent-controls {
margin-bottom: 0;
align-items: stretch;

View File

@@ -753,7 +753,6 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
role="dialog"
aria-label="Agent controls"
aria-modal="false"
style={{ maxHeight: "70vh", overflowY: "auto" }}
>
<div className="agent-controls">
<div className="agent-controls-filters">

View File

@@ -170,24 +170,23 @@ describe("AgentsView", () => {
});
});
it("renders secondary sections after the main collection", async () => {
it("renders metrics above the main collection and active panel after it", async () => {
const { container } = render(<AgentsView addToast={mockAddToast} />);
await waitFor(() => {
expect(container.querySelector(".agent-list")).toBeTruthy();
expect(container.querySelector(".agent-metrics-bar")).toBeTruthy();
expect(container.querySelector(".agent-token-stats-panel")).toBeTruthy();
expect(container.querySelector(".active-agents-panel")).toBeTruthy();
});
const list = container.querySelector(".agent-list");
const metrics = container.querySelector(".agent-metrics-bar");
const tokenPanel = container.querySelector(".agent-token-stats-panel");
const activePanel = container.querySelector(".active-agents-panel");
expect(list && metrics && tokenPanel && activePanel).toBeTruthy();
expect(list!.compareDocumentPosition(metrics!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(metrics!.compareDocumentPosition(tokenPanel!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(tokenPanel!.compareDocumentPosition(activePanel!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(list && metrics && activePanel).toBeTruthy();
// Metrics bar sits above the agent list (top-of-view stats placement).
expect(metrics!.compareDocumentPosition(list!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
// Active agents panel comes after the main agent collection.
expect(list!.compareDocumentPosition(activePanel!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
});
it("fetches agents only once on mount (regression: no duplicate initial load path)", async () => {
@@ -205,6 +204,10 @@ describe("AgentsView", () => {
it("renders token stats derived from the currently displayed agents", async () => {
render(<AgentsView addToast={mockAddToast} />);
// Token-usage panel now lives inside the controls popup, not in the
// main view body — open the controls panel before asserting.
await openControlsPanel();
await waitFor(() => {
expect(screen.getByText("Token Usage by Agent")).toBeTruthy();
});

View File

@@ -34,6 +34,10 @@ vi.mock("../QuickEntryBox", () => ({
vi.mock("lucide-react", () => ({
Link: () => null,
Clock: () => null,
ChevronDown: () => null,
ChevronUp: () => null,
Archive: () => null,
MoreVertical: () => null,
}));
// Mock usePluginUiSlots hook

View File

@@ -1532,13 +1532,14 @@ describe("TaskDetailModal", () => {
/>,
);
// For an in-progress task (no workflow steps, no merge commit),
// the top-level tabs are: Definition, Logs, Changes, Comments, Documents, Model, Workflow
const tabTexts = ["Definition", "Logs", "Changes", "Comments", "Documents", "Model", "Workflow"];
// For an in-progress task (no workflow steps, no merge commit), the
// top-level tabs are: Definition, Logs, Changes, Comments, Documents,
// Model, Workflow, Stats.
const tabTexts = ["Definition", "Logs", "Changes", "Comments", "Documents", "Model", "Workflow", "Stats"];
const tabs = screen.getAllByRole("button").filter((b) =>
tabTexts.includes(b.textContent || "")
);
expect(tabs.length).toBe(7);
expect(tabs.length).toBe(8);
expect(tabs[0].textContent).toBe("Definition");
expect(tabs[1].textContent).toBe("Logs");
expect(tabs[2].textContent).toBe("Changes");
@@ -1546,9 +1547,10 @@ describe("TaskDetailModal", () => {
expect(tabs[4].textContent).toBe("Documents");
expect(tabs[5].textContent).toBe("Model");
expect(tabs[6].textContent).toBe("Workflow");
expect(tabs[7].textContent).toBe("Stats");
// Activity and Agent Log are NOT top-level tabs (they are subviews inside Logs)
expect(container.querySelectorAll(".detail-tab").length).toBe(7);
expect(container.querySelectorAll(".detail-tab").length).toBe(8);
// Workflow tab should always appear even when no workflow steps are configured
expect(screen.getByText("Workflow")).toBeInTheDocument();
// Commits tab should NOT appear for non-done tasks
@@ -2415,7 +2417,7 @@ describe("TaskDetailModal", () => {
expect(container.querySelector(".modal-actions .modal-actions-spacer")).toBeTruthy();
expect(container.querySelector(".detail-body")).toBeTruthy();
const tabs = container.querySelectorAll(".detail-tab");
expect(tabs.length).toBe(7);
expect(tabs.length).toBe(8);
expect(tabs[0].classList.contains("detail-tab-active")).toBe(true);
expect(Array.from(tabs).slice(1).every((t) => !t.classList.contains("detail-tab-active"))).toBe(true);
// Responsive CSS controls sizing — no inline padding/fontSize/borderBottom leaks
@@ -3109,10 +3111,10 @@ describe("TaskDetailModal", () => {
/>,
);
// In-progress tasks show exactly 7 tabs:
// Definition, Logs, Changes, Comments, Documents, Model, Workflow
// In-progress tasks show exactly 8 tabs:
// Definition, Logs, Changes, Comments, Documents, Model, Workflow, Stats
const tabs = container.querySelectorAll(".detail-tab");
expect(tabs.length).toBe(7);
expect(tabs.length).toBe(8);
expect(tabs[0].textContent).toBe("Definition");
expect(tabs[1].textContent).toBe("Logs");
expect(tabs[2].textContent).toBe("Changes");
@@ -3120,6 +3122,7 @@ describe("TaskDetailModal", () => {
expect(tabs[4].textContent).toBe("Documents");
expect(tabs[5].textContent).toBe("Model");
expect(tabs[6].textContent).toBe("Workflow");
expect(tabs[7].textContent).toBe("Stats");
// Commits tab should NOT be present for non-done tasks
expect(screen.queryByText("Commits")).toBeNull();
});
@@ -3137,9 +3140,9 @@ describe("TaskDetailModal", () => {
/>,
);
// In-progress task with workflow steps: 7 tabs (Workflow after Model)
// In-progress task with workflow steps: 8 tabs (Workflow after Model, Stats last)
const tabs = container.querySelectorAll(".detail-tab");
expect(tabs.length).toBe(7);
expect(tabs.length).toBe(8);
expect(tabs[0].textContent).toBe("Definition");
expect(tabs[1].textContent).toBe("Logs");
expect(tabs[2].textContent).toBe("Changes");
@@ -3147,6 +3150,7 @@ describe("TaskDetailModal", () => {
expect(tabs[4].textContent).toBe("Documents");
expect(tabs[5].textContent).toBe("Model");
expect(tabs[6].textContent).toBe("Workflow");
expect(tabs[7].textContent).toBe("Stats");
});
it("does NOT show Commits tab for done task with mergeDetails.commitSha (changes merged into Changes tab)", () => {
@@ -3165,9 +3169,9 @@ describe("TaskDetailModal", () => {
/>,
);
// Done task with commit SHA: Definition, Logs, Changes, Comments, Documents, Model, Workflow (7 tabs, no Commits)
// Done task with commit SHA: Definition, Logs, Changes, Comments, Documents, Model, Workflow, Stats (8 tabs, no Commits)
const tabs = container.querySelectorAll(".detail-tab");
expect(tabs.length).toBe(7);
expect(tabs.length).toBe(8);
expect(tabs[0].textContent).toBe("Definition");
expect(tabs[1].textContent).toBe("Logs");
expect(tabs[2].textContent).toBe("Changes");
@@ -3175,11 +3179,12 @@ describe("TaskDetailModal", () => {
expect(tabs[4].textContent).toBe("Documents");
expect(tabs[5].textContent).toBe("Model");
expect(tabs[6].textContent).toBe("Workflow");
expect(tabs[7].textContent).toBe("Stats");
// Commits tab should NOT be present
expect(screen.queryByText("Commits")).toBeNull();
});
it("shows 6 tabs for done task with workflow steps and commit SHA (Commits merged into Changes)", () => {
it("shows 8 tabs for done task with workflow steps and commit SHA (Commits merged into Changes)", () => {
const { container } = render(
<TaskDetailModal
task={makeTask({
@@ -3196,9 +3201,9 @@ describe("TaskDetailModal", () => {
/>,
);
// Done task with workflow steps and commit SHA: 7 tabs (no Commits)
// Done task with workflow steps and commit SHA: 8 tabs (no Commits)
const tabs = container.querySelectorAll(".detail-tab");
expect(tabs.length).toBe(7);
expect(tabs.length).toBe(8);
expect(tabs[0].textContent).toBe("Definition");
expect(tabs[1].textContent).toBe("Logs");
expect(tabs[2].textContent).toBe("Changes");
@@ -3206,6 +3211,7 @@ describe("TaskDetailModal", () => {
expect(tabs[4].textContent).toBe("Documents");
expect(tabs[5].textContent).toBe("Model");
expect(tabs[6].textContent).toBe("Workflow");
expect(tabs[7].textContent).toBe("Stats");
// Commits tab should NOT be present
expect(screen.queryByText("Commits")).toBeNull();
});
@@ -3224,9 +3230,9 @@ describe("TaskDetailModal", () => {
);
const triageTabs = triageContainer.querySelectorAll(".detail-tab");
expect(triageTabs.length).toBe(6); // Definition, Logs, Comments, Documents, Model, Workflow
expect(triageTabs.length).toBe(7); // Definition, Logs, Comments, Documents, Model, Workflow, Stats
expect(Array.from(triageTabs).map(t => t.textContent)).toEqual([
"Definition", "Logs", "Comments", "Documents", "Model", "Workflow",
"Definition", "Logs", "Comments", "Documents", "Model", "Workflow", "Stats",
]);
const { container: todoContainer } = render(
@@ -3242,9 +3248,9 @@ describe("TaskDetailModal", () => {
);
const todoTabs = todoContainer.querySelectorAll(".detail-tab");
expect(todoTabs.length).toBe(6); // Definition, Logs, Comments, Documents, Model, Workflow
expect(todoTabs.length).toBe(7); // Definition, Logs, Comments, Documents, Model, Workflow, Stats
expect(Array.from(todoTabs).map(t => t.textContent)).toEqual([
"Definition", "Logs", "Comments", "Documents", "Model", "Workflow",
"Definition", "Logs", "Comments", "Documents", "Model", "Workflow", "Stats",
]);
});
@@ -5393,6 +5399,9 @@ describe("TaskDetailModal", () => {
);
expect(screen.getByText("Loading specification…")).toBeDefined();
// Token stats now live in their own Stats tab — switch to it before
// asserting on token-loading text.
fireEvent.click(screen.getByRole("button", { name: "Stats" }));
expect(screen.getByText("Loading token statistics…")).toBeDefined();
});
@@ -5451,6 +5460,9 @@ describe("TaskDetailModal", () => {
// Loading indicator should be gone
expect(screen.queryByText("Loading specification…")).toBeNull();
// Token stats live behind the Stats tab now.
fireEvent.click(screen.getByRole("button", { name: "Stats" }));
expect(screen.queryByText("Loading token statistics…")).toBeNull();
expect(screen.getByText((1200).toLocaleString())).toBeInTheDocument();
expect(screen.getByText((450).toLocaleString())).toBeInTheDocument();
@@ -5496,6 +5508,12 @@ describe("TaskDetailModal", () => {
/>,
);
// Token stats live behind the Stats tab now — wait for the modal to
// settle, then switch tabs and assert on the empty state.
await waitFor(() => {
expect(screen.queryByText("Loading specification…")).toBeNull();
});
fireEvent.click(screen.getByRole("button", { name: "Stats" }));
await waitFor(() => {
expect(screen.getByText("No token usage recorded for this task yet.")).toBeInTheDocument();
});

View File

@@ -136,6 +136,12 @@ describe("AgentsView mobile adaptations", () => {
await waitFor(() => {
expect(container.querySelector(".agent-list")).toBeTruthy();
expect(container.querySelectorAll(".agent-card").length).toBeGreaterThan(0);
});
// Token-stats panel now lives in the controls popup; open it before
// asserting on the panel content.
fireEvent.click(screen.getByRole("button", { name: "Controls" }));
await waitFor(() => {
expect(container.querySelector(".agent-token-stats-panel")).toBeTruthy();
expect(screen.getByText("Combined Tokens")).toBeTruthy();
expect(screen.getByText("100")).toBeTruthy();

View File

@@ -47,13 +47,28 @@ vi.mock("../../hooks/useTaskDiffStats", () => ({
function getMainMobileSection(css: string): string {
const sectionStart = css.indexOf("/* === Mobile Responsive Overrides ===");
const sectionEnd = css.indexOf("/* === Tablet Responsive Tier", sectionStart);
// Mobile rules now live across many co-located component CSS files (each
// owns its own @media (max-width: 768px) block) instead of one monolith
// section in styles.css. Concatenate every <=768px media block in the
// bundle so these assertions remain location-agnostic.
const re = /@media\s*\([^)]*max-width:\s*768px[^)]*\)\s*\{/g;
const blocks: string[] = [];
expect(sectionStart).toBeGreaterThan(-1);
expect(sectionEnd).toBeGreaterThan(sectionStart);
for (const match of css.matchAll(re)) {
const start = match.index! + match[0].length;
let depth = 1;
let i = start;
while (i < css.length && depth > 0) {
const ch = css[i];
if (ch === "{") depth++;
else if (ch === "}") depth--;
i++;
}
blocks.push(css.slice(start, i - 1));
}
return css.slice(sectionStart, sectionEnd);
expect(blocks.length).toBeGreaterThan(0);
return blocks.join("\n");
}
function expectRuleToContain(section: string, selectorFragment: string, declaration: string): void {