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 <noreply@anthropic.com>
This commit is contained in:
@@ -266,6 +266,7 @@ describe("ChatWebSocket", () => {
|
|||||||
current: [],
|
current: [],
|
||||||
qa: [],
|
qa: [],
|
||||||
merge: [],
|
merge: [],
|
||||||
|
done: [],
|
||||||
};
|
};
|
||||||
instances[1].simulateMessage({ type: "pipeline_state", ...freshState });
|
instances[1].simulateMessage({ type: "pipeline_state", ...freshState });
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export interface PipelineState {
|
|||||||
current: PipelineStageItem[];
|
current: PipelineStageItem[];
|
||||||
qa: PipelineStageItem[];
|
qa: PipelineStageItem[];
|
||||||
merge: PipelineStageItem[];
|
merge: PipelineStageItem[];
|
||||||
|
done: PipelineStageItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WsResponse =
|
export type WsResponse =
|
||||||
@@ -45,6 +46,7 @@ export type WsResponse =
|
|||||||
current: PipelineStageItem[];
|
current: PipelineStageItem[];
|
||||||
qa: PipelineStageItem[];
|
qa: PipelineStageItem[];
|
||||||
merge: PipelineStageItem[];
|
merge: PipelineStageItem[];
|
||||||
|
done: PipelineStageItem[];
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "permission_request";
|
type: "permission_request";
|
||||||
@@ -346,6 +348,7 @@ export class ChatWebSocket {
|
|||||||
current: data.current,
|
current: data.current,
|
||||||
qa: data.qa,
|
qa: data.qa,
|
||||||
merge: data.merge,
|
merge: data.merge,
|
||||||
|
done: data.done,
|
||||||
});
|
});
|
||||||
if (data.type === "permission_request")
|
if (data.type === "permission_request")
|
||||||
this.onPermissionRequest?.(
|
this.onPermissionRequest?.(
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
current: [],
|
current: [],
|
||||||
qa: [],
|
qa: [],
|
||||||
merge: [],
|
merge: [],
|
||||||
|
done: [],
|
||||||
});
|
});
|
||||||
const [claudeSessionId, setClaudeSessionId] = useState<string | null>(null);
|
const [claudeSessionId, setClaudeSessionId] = useState<string | null>(null);
|
||||||
const [activityStatus, setActivityStatus] = useState<string | null>(null);
|
const [activityStatus, setActivityStatus] = useState<string | null>(null);
|
||||||
@@ -1074,6 +1075,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
stateVersion={agentStateVersion}
|
stateVersion={agentStateVersion}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<StagePanel title="Done" items={pipeline.done} />
|
||||||
<StagePanel title="To Merge" items={pipeline.merge} />
|
<StagePanel title="To Merge" items={pipeline.merge} />
|
||||||
<StagePanel title="QA" items={pipeline.qa} />
|
<StagePanel title="QA" items={pipeline.qa} />
|
||||||
<StagePanel title="Current" items={pipeline.current} />
|
<StagePanel title="Current" items={pipeline.current} />
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ function makePipeline(overrides: Partial<PipelineState> = {}): PipelineState {
|
|||||||
current: [],
|
current: [],
|
||||||
qa: [],
|
qa: [],
|
||||||
merge: [],
|
merge: [],
|
||||||
|
done: [],
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,9 +36,10 @@ pub struct PipelineState {
|
|||||||
pub current: Vec<UpcomingStory>,
|
pub current: Vec<UpcomingStory>,
|
||||||
pub qa: Vec<UpcomingStory>,
|
pub qa: Vec<UpcomingStory>,
|
||||||
pub merge: Vec<UpcomingStory>,
|
pub merge: Vec<UpcomingStory>,
|
||||||
|
pub done: Vec<UpcomingStory>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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<PipelineState, String> {
|
pub fn load_pipeline_state(ctx: &AppContext) -> Result<PipelineState, String> {
|
||||||
let agent_map = build_active_agent_map(ctx);
|
let agent_map = build_active_agent_map(ctx);
|
||||||
Ok(PipelineState {
|
Ok(PipelineState {
|
||||||
@@ -46,6 +47,7 @@ pub fn load_pipeline_state(ctx: &AppContext) -> Result<PipelineState, String> {
|
|||||||
current: load_stage_items(ctx, "2_current", &agent_map)?,
|
current: load_stage_items(ctx, "2_current", &agent_map)?,
|
||||||
qa: load_stage_items(ctx, "3_qa", &agent_map)?,
|
qa: load_stage_items(ctx, "3_qa", &agent_map)?,
|
||||||
merge: load_stage_items(ctx, "4_merge", &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"),
|
("2_current", "20_story_current"),
|
||||||
("3_qa", "30_story_qa"),
|
("3_qa", "30_story_qa"),
|
||||||
("4_merge", "40_story_merge"),
|
("4_merge", "40_story_merge"),
|
||||||
|
("5_done", "50_story_done"),
|
||||||
] {
|
] {
|
||||||
let dir = root.join(".story_kit").join("work").join(stage);
|
let dir = root.join(".story_kit").join("work").join(stage);
|
||||||
fs::create_dir_all(&dir).unwrap();
|
fs::create_dir_all(&dir).unwrap();
|
||||||
@@ -631,6 +634,9 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(state.merge.len(), 1);
|
assert_eq!(state.merge.len(), 1);
|
||||||
assert_eq!(state.merge[0].story_id, "40_story_merge");
|
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]
|
#[test]
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ enum WsResponse {
|
|||||||
current: Vec<crate::http::workflow::UpcomingStory>,
|
current: Vec<crate::http::workflow::UpcomingStory>,
|
||||||
qa: Vec<crate::http::workflow::UpcomingStory>,
|
qa: Vec<crate::http::workflow::UpcomingStory>,
|
||||||
merge: Vec<crate::http::workflow::UpcomingStory>,
|
merge: Vec<crate::http::workflow::UpcomingStory>,
|
||||||
|
done: Vec<crate::http::workflow::UpcomingStory>,
|
||||||
},
|
},
|
||||||
/// `.story_kit/project.toml` was modified; the frontend should re-fetch the
|
/// `.story_kit/project.toml` was modified; the frontend should re-fetch the
|
||||||
/// agent roster. Does NOT trigger a pipeline state refresh.
|
/// agent roster. Does NOT trigger a pipeline state refresh.
|
||||||
@@ -136,6 +137,7 @@ impl From<PipelineState> for WsResponse {
|
|||||||
current: s.current,
|
current: s.current,
|
||||||
qa: s.qa,
|
qa: s.qa,
|
||||||
merge: s.merge,
|
merge: s.merge,
|
||||||
|
done: s.done,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -569,12 +571,14 @@ mod tests {
|
|||||||
current: vec![],
|
current: vec![],
|
||||||
qa: vec![],
|
qa: vec![],
|
||||||
merge: vec![],
|
merge: vec![],
|
||||||
|
done: vec![],
|
||||||
};
|
};
|
||||||
let json = serde_json::to_value(&resp).unwrap();
|
let json = serde_json::to_value(&resp).unwrap();
|
||||||
assert_eq!(json["type"], "pipeline_state");
|
assert_eq!(json["type"], "pipeline_state");
|
||||||
assert_eq!(json["upcoming"].as_array().unwrap().len(), 1);
|
assert_eq!(json["upcoming"].as_array().unwrap().len(), 1);
|
||||||
assert_eq!(json["upcoming"][0]["story_id"], "10_story_test");
|
assert_eq!(json["upcoming"][0]["story_id"], "10_story_test");
|
||||||
assert!(json["current"].as_array().unwrap().is_empty());
|
assert!(json["current"].as_array().unwrap().is_empty());
|
||||||
|
assert!(json["done"].as_array().unwrap().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -696,6 +700,12 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
qa: vec![],
|
qa: vec![],
|
||||||
merge: 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 resp: WsResponse = state.into();
|
||||||
let json = serde_json::to_value(&resp).unwrap();
|
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_eq!(json["current"][0]["story_id"], "2_story_b");
|
||||||
assert!(json["qa"].as_array().unwrap().is_empty());
|
assert!(json["qa"].as_array().unwrap().is_empty());
|
||||||
assert!(json["merge"].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]
|
#[test]
|
||||||
@@ -715,6 +727,7 @@ mod tests {
|
|||||||
current: vec![],
|
current: vec![],
|
||||||
qa: vec![],
|
qa: vec![],
|
||||||
merge: vec![],
|
merge: vec![],
|
||||||
|
done: vec![],
|
||||||
};
|
};
|
||||||
let resp: WsResponse = state.into();
|
let resp: WsResponse = state.into();
|
||||||
let json = serde_json::to_value(&resp).unwrap();
|
let json = serde_json::to_value(&resp).unwrap();
|
||||||
@@ -723,6 +736,7 @@ mod tests {
|
|||||||
assert!(json["current"].as_array().unwrap().is_empty());
|
assert!(json["current"].as_array().unwrap().is_empty());
|
||||||
assert!(json["qa"].as_array().unwrap().is_empty());
|
assert!(json["qa"].as_array().unwrap().is_empty());
|
||||||
assert!(json["merge"].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) ────────────────────
|
// ── WsResponse JSON round-trip (string form) ────────────────────
|
||||||
@@ -849,6 +863,7 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
qa: vec![],
|
qa: vec![],
|
||||||
merge: vec![],
|
merge: vec![],
|
||||||
|
done: vec![],
|
||||||
};
|
};
|
||||||
let resp: WsResponse = state.into();
|
let resp: WsResponse = state.into();
|
||||||
let json = serde_json::to_value(&resp).unwrap();
|
let json = serde_json::to_value(&resp).unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user