From 150f654e0480aec7dffd05d44a886182d54c9f85 Mon Sep 17 00:00:00 2001 From: Dave Date: Tue, 24 Feb 2026 23:42:59 +0000 Subject: [PATCH] story-kit: merge 166_story_add_done_column_to_pipeline_board Add Done column to pipeline board. Adds the 'done' stage to PipelineState, exposes it via the WebSocket and REST API, and renders a Done column in the frontend pipeline board view. Squash merge from feature/story-166_story_add_done_column_to_pipeline_board. Co-Authored-By: Claude Opus 4.6 --- frontend/src/api/client.test.ts | 1 + frontend/src/api/client.ts | 3 +++ frontend/src/components/Chat.tsx | 2 ++ .../src/components/LozengeFlyContext.test.tsx | 1 + server/src/http/workflow.rs | 8 +++++++- server/src/http/ws.rs | 15 +++++++++++++++ 6 files changed, 29 insertions(+), 1 deletion(-) diff --git a/frontend/src/api/client.test.ts b/frontend/src/api/client.test.ts index a0bd504..8a3c737 100644 --- a/frontend/src/api/client.test.ts +++ b/frontend/src/api/client.test.ts @@ -266,6 +266,7 @@ describe("ChatWebSocket", () => { current: [], qa: [], merge: [], + done: [], }; instances[1].simulateMessage({ type: "pipeline_state", ...freshState }); diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 7ec2745..5ae4dc4 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -32,6 +32,7 @@ export interface PipelineState { current: PipelineStageItem[]; qa: PipelineStageItem[]; merge: PipelineStageItem[]; + done: PipelineStageItem[]; } export type WsResponse = @@ -45,6 +46,7 @@ export type WsResponse = current: PipelineStageItem[]; qa: PipelineStageItem[]; merge: PipelineStageItem[]; + done: PipelineStageItem[]; } | { type: "permission_request"; @@ -346,6 +348,7 @@ export class ChatWebSocket { current: data.current, qa: data.qa, merge: data.merge, + done: data.done, }); if (data.type === "permission_request") this.onPermissionRequest?.( diff --git a/frontend/src/components/Chat.tsx b/frontend/src/components/Chat.tsx index 382fc0b..1aca024 100644 --- a/frontend/src/components/Chat.tsx +++ b/frontend/src/components/Chat.tsx @@ -72,6 +72,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) { current: [], qa: [], merge: [], + done: [], }); const [claudeSessionId, setClaudeSessionId] = useState(null); const [activityStatus, setActivityStatus] = useState(null); @@ -1074,6 +1075,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) { stateVersion={agentStateVersion} /> + diff --git a/frontend/src/components/LozengeFlyContext.test.tsx b/frontend/src/components/LozengeFlyContext.test.tsx index 7d72d8a..c0db6d2 100644 --- a/frontend/src/components/LozengeFlyContext.test.tsx +++ b/frontend/src/components/LozengeFlyContext.test.tsx @@ -13,6 +13,7 @@ function makePipeline(overrides: Partial = {}): PipelineState { current: [], qa: [], merge: [], + done: [], ...overrides, }; } diff --git a/server/src/http/workflow.rs b/server/src/http/workflow.rs index 4a2bf36..9634bea 100644 --- a/server/src/http/workflow.rs +++ b/server/src/http/workflow.rs @@ -36,9 +36,10 @@ pub struct PipelineState { pub current: Vec, pub qa: Vec, pub merge: Vec, + pub done: Vec, } -/// Load the full pipeline state (all 4 active stages). +/// Load the full pipeline state (all 5 active stages). pub fn load_pipeline_state(ctx: &AppContext) -> Result { let agent_map = build_active_agent_map(ctx); Ok(PipelineState { @@ -46,6 +47,7 @@ pub fn load_pipeline_state(ctx: &AppContext) -> Result { current: load_stage_items(ctx, "2_current", &agent_map)?, qa: load_stage_items(ctx, "3_qa", &agent_map)?, merge: load_stage_items(ctx, "4_merge", &agent_map)?, + done: load_stage_items(ctx, "5_done", &HashMap::new())?, }) } @@ -607,6 +609,7 @@ mod tests { ("2_current", "20_story_current"), ("3_qa", "30_story_qa"), ("4_merge", "40_story_merge"), + ("5_done", "50_story_done"), ] { let dir = root.join(".story_kit").join("work").join(stage); fs::create_dir_all(&dir).unwrap(); @@ -631,6 +634,9 @@ mod tests { assert_eq!(state.merge.len(), 1); assert_eq!(state.merge[0].story_id, "40_story_merge"); + + assert_eq!(state.done.len(), 1); + assert_eq!(state.done[0].story_id, "50_story_done"); } #[test] diff --git a/server/src/http/ws.rs b/server/src/http/ws.rs index 561f5b8..57cd5cb 100644 --- a/server/src/http/ws.rs +++ b/server/src/http/ws.rs @@ -73,6 +73,7 @@ enum WsResponse { current: Vec, qa: Vec, merge: Vec, + done: Vec, }, /// `.story_kit/project.toml` was modified; the frontend should re-fetch the /// agent roster. Does NOT trigger a pipeline state refresh. @@ -136,6 +137,7 @@ impl From for WsResponse { current: s.current, qa: s.qa, merge: s.merge, + done: s.done, } } } @@ -569,12 +571,14 @@ mod tests { current: vec![], qa: vec![], merge: vec![], + done: vec![], }; let json = serde_json::to_value(&resp).unwrap(); assert_eq!(json["type"], "pipeline_state"); assert_eq!(json["upcoming"].as_array().unwrap().len(), 1); assert_eq!(json["upcoming"][0]["story_id"], "10_story_test"); assert!(json["current"].as_array().unwrap().is_empty()); + assert!(json["done"].as_array().unwrap().is_empty()); } #[test] @@ -696,6 +700,12 @@ mod tests { }], qa: vec![], merge: vec![], + done: vec![UpcomingStory { + story_id: "50_story_done".to_string(), + name: Some("Done Story".to_string()), + error: None, + agent: None, + }], }; let resp: WsResponse = state.into(); let json = serde_json::to_value(&resp).unwrap(); @@ -706,6 +716,8 @@ mod tests { assert_eq!(json["current"][0]["story_id"], "2_story_b"); assert!(json["qa"].as_array().unwrap().is_empty()); assert!(json["merge"].as_array().unwrap().is_empty()); + assert_eq!(json["done"].as_array().unwrap().len(), 1); + assert_eq!(json["done"][0]["story_id"], "50_story_done"); } #[test] @@ -715,6 +727,7 @@ mod tests { current: vec![], qa: vec![], merge: vec![], + done: vec![], }; let resp: WsResponse = state.into(); let json = serde_json::to_value(&resp).unwrap(); @@ -723,6 +736,7 @@ mod tests { assert!(json["current"].as_array().unwrap().is_empty()); assert!(json["qa"].as_array().unwrap().is_empty()); assert!(json["merge"].as_array().unwrap().is_empty()); + assert!(json["done"].as_array().unwrap().is_empty()); } // ── WsResponse JSON round-trip (string form) ──────────────────── @@ -849,6 +863,7 @@ mod tests { }], qa: vec![], merge: vec![], + done: vec![], }; let resp: WsResponse = state.into(); let json = serde_json::to_value(&resp).unwrap();