From b6fcc3ec93e0f13ac321ef7aa13a40f7211f0f7e Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 26 Feb 2026 15:32:16 +0000 Subject: [PATCH] story-198: distinguish work item types (story/bug/spike) in web UI Add visual type indicators to pipeline stage panels so stories, bugs, and spikes are distinguishable at a glance. Squash merge of feature/story-198 Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/components/StagePanel.test.tsx | 116 ++++++++++++++++++++ frontend/src/components/StagePanel.tsx | 45 ++++++++ 2 files changed, 161 insertions(+) diff --git a/frontend/src/components/StagePanel.test.tsx b/frontend/src/components/StagePanel.test.tsx index 4708d8f..2fc8d39 100644 --- a/frontend/src/components/StagePanel.test.tsx +++ b/frontend/src/components/StagePanel.test.tsx @@ -101,4 +101,120 @@ describe("StagePanel", () => { render(); expect(screen.getByText("Missing front matter")).toBeInTheDocument(); }); + + it("shows STORY badge for story items", () => { + const items: PipelineStageItem[] = [ + { + story_id: "10_story_some_feature", + name: "Some Feature", + error: null, + agent: null, + }, + ]; + render(); + expect( + screen.getByTestId("type-badge-10_story_some_feature"), + ).toHaveTextContent("STORY"); + }); + + it("shows BUG badge for bug items", () => { + const items: PipelineStageItem[] = [ + { + story_id: "11_bug_broken_thing", + name: "Broken Thing", + error: null, + agent: null, + }, + ]; + render(); + expect( + screen.getByTestId("type-badge-11_bug_broken_thing"), + ).toHaveTextContent("BUG"); + }); + + it("shows SPIKE badge for spike items", () => { + const items: PipelineStageItem[] = [ + { + story_id: "12_spike_investigate_perf", + name: "Investigate Perf", + error: null, + agent: null, + }, + ]; + render(); + expect( + screen.getByTestId("type-badge-12_spike_investigate_perf"), + ).toHaveTextContent("SPIKE"); + }); + + it("shows no badge for unrecognised type prefix", () => { + const items: PipelineStageItem[] = [ + { + story_id: "13_task_do_something", + name: "Do Something", + error: null, + agent: null, + }, + ]; + render(); + expect( + screen.queryByTestId("type-badge-13_task_do_something"), + ).not.toBeInTheDocument(); + }); + + it("applies green left border for story items", () => { + const items: PipelineStageItem[] = [ + { + story_id: "20_story_green_border", + name: "Green Border", + error: null, + agent: null, + }, + ]; + render(); + const card = screen.getByTestId("card-20_story_green_border"); + expect(card.style.borderLeft).toContain("rgb(63, 185, 80)"); + }); + + it("applies red left border for bug items", () => { + const items: PipelineStageItem[] = [ + { + story_id: "21_bug_red_border", + name: "Red Border", + error: null, + agent: null, + }, + ]; + render(); + const card = screen.getByTestId("card-21_bug_red_border"); + expect(card.style.borderLeft).toContain("rgb(248, 81, 73)"); + }); + + it("applies blue left border for spike items", () => { + const items: PipelineStageItem[] = [ + { + story_id: "22_spike_blue_border", + name: "Blue Border", + error: null, + agent: null, + }, + ]; + render(); + const card = screen.getByTestId("card-22_spike_blue_border"); + expect(card.style.borderLeft).toContain("rgb(88, 166, 255)"); + }); + + it("applies neutral left border for unrecognised type", () => { + const items: PipelineStageItem[] = [ + { + story_id: "23_task_neutral_border", + name: "Neutral Border", + error: null, + agent: null, + }, + ]; + render(); + const card = screen.getByTestId("card-23_task_neutral_border"); + expect(card.style.borderLeft).toContain("rgb(68, 68, 68)"); + }); }); diff --git a/frontend/src/components/StagePanel.tsx b/frontend/src/components/StagePanel.tsx index eee562d..ebb19e9 100644 --- a/frontend/src/components/StagePanel.tsx +++ b/frontend/src/components/StagePanel.tsx @@ -4,6 +4,32 @@ import { useLozengeFly } from "./LozengeFlyContext"; const { useLayoutEffect, useRef } = React; +type WorkItemType = "story" | "bug" | "spike" | "unknown"; + +const TYPE_COLORS: Record = { + story: "#3fb950", + bug: "#f85149", + spike: "#58a6ff", + unknown: "#444", +}; + +const TYPE_LABELS: Record = { + story: "STORY", + bug: "BUG", + spike: "SPIKE", + unknown: null, +}; + +function getWorkItemType(storyId: string): WorkItemType { + const match = storyId.match(/^\d+_([a-z]+)_/); + if (!match) return "unknown"; + const segment = match[1]; + if (segment === "story" || segment === "bug" || segment === "spike") { + return segment; + } + return "unknown"; +} + interface StagePanelProps { title: string; items: PipelineStageItem[]; @@ -137,13 +163,18 @@ export function StagePanel({ > {items.map((item) => { const itemNumber = item.story_id.match(/^(\d+)/)?.[1]; + const itemType = getWorkItemType(item.story_id); + const borderColor = TYPE_COLORS[itemType]; + const typeLabel = TYPE_LABELS[itemType]; return (
)} + {typeLabel && ( + + {typeLabel} + + )} {item.name ?? item.story_id}
{item.error && (