feat(workflows): show lane pipelines, drop the deep analytics charts

The Workflows page rendered eleven charts of Claude Code harness internals
(collaboration network, subagent effectiveness, patterns, model delegation,
error propagation, concurrency, complexity, compaction). Too specialized to
read and nothing to act on, so they are gone along with their i18n keys.

What is left is what a reader can act on: headline stats, Workflow-tool runs,
the spawning DAG, the tool flow and the per-session drill-in — plus a new
first section for this dashboard's OWN pipelines. LanePipelinePanel renders a
horizontally scrolling lane strip and the selected lane's PipelineMap,
reusing the Workspace components so a stage looks identical on both pages.

The panel is read-only: the console-never-writes-a-stage boundary applies
here too, so every lane action stays on the Workspace page. The workflows API
response shape is untouched.

Closes #6
This commit is contained in:
2026-08-26 10:05:35 +07:00
parent bcfeeb5140
commit 4a0f99583d
16 changed files with 351 additions and 4766 deletions
@@ -0,0 +1,115 @@
/**
* @file Tests for the Workflows page's lane pipeline panel: it lists every lane
* as a tile, shows the first lane's pipeline before anything is clicked, and
* swaps the map when another lane is selected. A wrong selection would show one
* lane's stage under another lane's name.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { LanePipelinePanel } from "../LanePipelinePanel";
const { LANES } = vi.hoisted(() => {
const laneStub = (id: number, stageId: string) => ({
id,
title: `lane-${id}`,
cwd: `/workspace/lane-${id}`,
branch: null,
kind: "adopted",
source_repo: null,
pipeline: "default",
session_id: null,
run_id: null,
stage: stageId,
stage_since: null,
status: "idle",
gate_decision: null,
ci_status: null,
needs_action: null,
links: {},
stages: {},
notes: null,
pipeline_name: "Default",
pipeline_nodes: [
{
id: stageId,
label: stageId,
icon: "🧭",
gate: false,
state: "current",
},
],
progress: 25,
stage_seconds: null,
last_event_seconds: null,
liveness: "idle",
detected_stage: null,
detected_signal: null,
slot: null,
ports: {},
active_feature_id: null,
});
return { LANES: [laneStub(1, "plan"), laneStub(2, "tests")] };
});
const listMock = vi.fn();
vi.mock("../../../lib/api", async (importOriginal) => {
const actual = await importOriginal<Record<string, unknown>>();
return { ...actual, api: { lanes: { list: () => listMock() } } };
});
function renderPanel() {
return render(
<MemoryRouter>
<LanePipelinePanel />
</MemoryRouter>
);
}
describe("LanePipelinePanel", () => {
it("shows the first lane's pipeline before any tile is clicked", async () => {
listMock.mockResolvedValue({
lanes: LANES,
counts: { total: 2, running: 0, needs_you: 0, dead: 0 },
});
renderPanel();
expect(await screen.findByTestId("lane-tile-1")).toHaveAttribute("data-selected", "true");
expect(screen.getByTestId("pipeline-node-plan")).toBeInTheDocument();
expect(screen.queryByTestId("pipeline-node-tests")).not.toBeInTheDocument();
});
it("swaps the pipeline map when another lane is selected", async () => {
listMock.mockResolvedValue({
lanes: LANES,
counts: { total: 2, running: 0, needs_you: 0, dead: 0 },
});
renderPanel();
fireEvent.click(await screen.findByTestId("lane-tile-2"));
expect(screen.getByTestId("pipeline-node-tests")).toBeInTheDocument();
expect(screen.queryByTestId("pipeline-node-plan")).not.toBeInTheDocument();
});
it("renders the CLI hint when no lane exists", async () => {
listMock.mockResolvedValue({
lanes: [],
counts: { total: 0, running: 0, needs_you: 0, dead: 0 },
});
renderPanel();
expect(await screen.findByText(/ccam lanes add/)).toBeInTheDocument();
expect(screen.queryByTestId("workflows-lane-strip")).not.toBeInTheDocument();
});
it("surfaces a failed fetch instead of an empty strip", async () => {
listMock.mockRejectedValue(new Error("lanes unreachable"));
renderPanel();
expect(await screen.findByText("lanes unreachable")).toBeInTheDocument();
});
});