huskies: merge 820
This commit is contained in:
@@ -209,6 +209,16 @@ body,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Spinner for in-progress deterministic merges */
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Agent lozenge appearance animation (simulates arriving from agents panel) */
|
/* Agent lozenge appearance animation (simulates arriving from agents panel) */
|
||||||
@keyframes agentAppear {
|
@keyframes agentAppear {
|
||||||
from {
|
from {
|
||||||
|
|||||||
@@ -267,6 +267,7 @@ describe("ChatWebSocket", () => {
|
|||||||
qa: [],
|
qa: [],
|
||||||
merge: [],
|
merge: [],
|
||||||
done: [],
|
done: [],
|
||||||
|
deterministic_merges_in_flight: [],
|
||||||
};
|
};
|
||||||
instances[1].simulateMessage({ type: "pipeline_state", ...freshState });
|
instances[1].simulateMessage({ type: "pipeline_state", ...freshState });
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ export interface PipelineState {
|
|||||||
qa: PipelineStageItem[];
|
qa: PipelineStageItem[];
|
||||||
merge: PipelineStageItem[];
|
merge: PipelineStageItem[];
|
||||||
done: PipelineStageItem[];
|
done: PipelineStageItem[];
|
||||||
|
/** Story IDs that currently have a deterministic merge in progress. */
|
||||||
|
deterministic_merges_in_flight: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A message received from the Huskies server over WebSocket. */
|
/** A message received from the Huskies server over WebSocket. */
|
||||||
@@ -84,6 +86,7 @@ export type WsResponse =
|
|||||||
qa: PipelineStageItem[];
|
qa: PipelineStageItem[];
|
||||||
merge: PipelineStageItem[];
|
merge: PipelineStageItem[];
|
||||||
done: PipelineStageItem[];
|
done: PipelineStageItem[];
|
||||||
|
deterministic_merges_in_flight: string[];
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "permission_request";
|
type: "permission_request";
|
||||||
|
|||||||
@@ -123,6 +123,8 @@ export class ChatWebSocket {
|
|||||||
qa: data.qa,
|
qa: data.qa,
|
||||||
merge: data.merge,
|
merge: data.merge,
|
||||||
done: data.done,
|
done: data.done,
|
||||||
|
deterministic_merges_in_flight:
|
||||||
|
data.deterministic_merges_in_flight ?? [],
|
||||||
});
|
});
|
||||||
if (data.type === "permission_request")
|
if (data.type === "permission_request")
|
||||||
this.onPermissionRequest?.(
|
this.onPermissionRequest?.(
|
||||||
|
|||||||
@@ -110,6 +110,12 @@ export function ChatPipelinePanel({
|
|||||||
configVersion={agentConfigVersion}
|
configVersion={agentConfigVersion}
|
||||||
stateVersion={agentStateVersion}
|
stateVersion={agentStateVersion}
|
||||||
/>
|
/>
|
||||||
|
{(() => {
|
||||||
|
const mergesInFlight = new Set(
|
||||||
|
pipeline.deterministic_merges_in_flight ?? [],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
<StagePanel
|
<StagePanel
|
||||||
title="Done"
|
title="Done"
|
||||||
items={pipeline.done ?? []}
|
items={pipeline.done ?? []}
|
||||||
@@ -117,6 +123,7 @@ export function ChatPipelinePanel({
|
|||||||
onItemClick={(item) => onSelectWorkItem(item.story_id)}
|
onItemClick={(item) => onSelectWorkItem(item.story_id)}
|
||||||
onStopAgent={onStopAgent}
|
onStopAgent={onStopAgent}
|
||||||
onDeleteItem={onDeleteItem}
|
onDeleteItem={onDeleteItem}
|
||||||
|
mergesInFlight={mergesInFlight}
|
||||||
/>
|
/>
|
||||||
<StagePanel
|
<StagePanel
|
||||||
title="To Merge"
|
title="To Merge"
|
||||||
@@ -125,6 +132,7 @@ export function ChatPipelinePanel({
|
|||||||
onItemClick={(item) => onSelectWorkItem(item.story_id)}
|
onItemClick={(item) => onSelectWorkItem(item.story_id)}
|
||||||
onStopAgent={onStopAgent}
|
onStopAgent={onStopAgent}
|
||||||
onDeleteItem={onDeleteItem}
|
onDeleteItem={onDeleteItem}
|
||||||
|
mergesInFlight={mergesInFlight}
|
||||||
/>
|
/>
|
||||||
<StagePanel
|
<StagePanel
|
||||||
title="QA"
|
title="QA"
|
||||||
@@ -133,6 +141,7 @@ export function ChatPipelinePanel({
|
|||||||
onItemClick={(item) => onSelectWorkItem(item.story_id)}
|
onItemClick={(item) => onSelectWorkItem(item.story_id)}
|
||||||
onStopAgent={onStopAgent}
|
onStopAgent={onStopAgent}
|
||||||
onDeleteItem={onDeleteItem}
|
onDeleteItem={onDeleteItem}
|
||||||
|
mergesInFlight={mergesInFlight}
|
||||||
/>
|
/>
|
||||||
<StagePanel
|
<StagePanel
|
||||||
title="Current"
|
title="Current"
|
||||||
@@ -144,6 +153,7 @@ export function ChatPipelinePanel({
|
|||||||
onStartAgent={onStartAgent}
|
onStartAgent={onStartAgent}
|
||||||
onStopAgent={onStopAgent}
|
onStopAgent={onStopAgent}
|
||||||
onDeleteItem={onDeleteItem}
|
onDeleteItem={onDeleteItem}
|
||||||
|
mergesInFlight={mergesInFlight}
|
||||||
/>
|
/>
|
||||||
<StagePanel
|
<StagePanel
|
||||||
title="Backlog"
|
title="Backlog"
|
||||||
@@ -155,7 +165,11 @@ export function ChatPipelinePanel({
|
|||||||
onStartAgent={onStartAgent}
|
onStartAgent={onStartAgent}
|
||||||
onStopAgent={onStopAgent}
|
onStopAgent={onStopAgent}
|
||||||
onDeleteItem={onDeleteItem}
|
onDeleteItem={onDeleteItem}
|
||||||
|
mergesInFlight={mergesInFlight}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
<ServerLogsPanel logs={combinedLogs} />
|
<ServerLogsPanel logs={combinedLogs} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ function makePipeline(overrides: Partial<PipelineState> = {}): PipelineState {
|
|||||||
qa: [],
|
qa: [],
|
||||||
merge: [],
|
merge: [],
|
||||||
done: [],
|
done: [],
|
||||||
|
deterministic_merges_in_flight: [],
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ function makePipeline(overrides: Partial<PipelineState> = {}): PipelineState {
|
|||||||
qa: [],
|
qa: [],
|
||||||
merge: [],
|
merge: [],
|
||||||
done: [],
|
done: [],
|
||||||
|
deterministic_merges_in_flight: [],
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ function makePipeline(overrides: Partial<PipelineState> = {}): PipelineState {
|
|||||||
qa: [],
|
qa: [],
|
||||||
merge: [],
|
merge: [],
|
||||||
done: [],
|
done: [],
|
||||||
|
deterministic_merges_in_flight: [],
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ function makePipeline(overrides: Partial<PipelineState> = {}): PipelineState {
|
|||||||
qa: [],
|
qa: [],
|
||||||
merge: [],
|
merge: [],
|
||||||
done: [],
|
done: [],
|
||||||
|
deterministic_merges_in_flight: [],
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -324,4 +324,71 @@ describe("StagePanel", () => {
|
|||||||
screen.queryByTestId("merge-failure-reason-31_story_no_failure"),
|
screen.queryByTestId("merge-failure-reason-31_story_no_failure"),
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows merge-in-flight icon when story is in mergesInFlight set", () => {
|
||||||
|
const items: PipelineStageItem[] = [
|
||||||
|
{
|
||||||
|
story_id: "40_story_merging",
|
||||||
|
name: "Merging Story",
|
||||||
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
|
agent: null,
|
||||||
|
review_hold: null,
|
||||||
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const mergesInFlight = new Set(["40_story_merging"]);
|
||||||
|
render(
|
||||||
|
<StagePanel title="To Merge" items={items} mergesInFlight={mergesInFlight} />,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByTestId("merge-in-flight-icon-40_story_merging"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not show merge-in-flight icon when story is not in mergesInFlight set", () => {
|
||||||
|
const items: PipelineStageItem[] = [
|
||||||
|
{
|
||||||
|
story_id: "41_story_not_merging",
|
||||||
|
name: "Idle Story",
|
||||||
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
|
agent: null,
|
||||||
|
review_hold: null,
|
||||||
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const mergesInFlight = new Set(["99_story_other"]);
|
||||||
|
render(
|
||||||
|
<StagePanel
|
||||||
|
title="To Merge"
|
||||||
|
items={items}
|
||||||
|
mergesInFlight={mergesInFlight}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId("merge-in-flight-icon-41_story_not_merging"),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not show merge-in-flight icon when mergesInFlight prop is absent", () => {
|
||||||
|
const items: PipelineStageItem[] = [
|
||||||
|
{
|
||||||
|
story_id: "42_story_no_prop",
|
||||||
|
name: "No Prop Story",
|
||||||
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
|
agent: null,
|
||||||
|
review_hold: null,
|
||||||
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
render(<StagePanel title="To Merge" items={items} />);
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId("merge-in-flight-icon-42_story_no_prop"),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ interface StagePanelProps {
|
|||||||
busyAgentNames?: Set<string>;
|
busyAgentNames?: Set<string>;
|
||||||
/** Called when the user requests to start an agent on a story. */
|
/** Called when the user requests to start an agent on a story. */
|
||||||
onStartAgent?: (storyId: string, agentName?: string) => void;
|
onStartAgent?: (storyId: string, agentName?: string) => void;
|
||||||
|
/** Set of story IDs that currently have a deterministic merge in progress. */
|
||||||
|
mergesInFlight?: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AgentLozenge({
|
function AgentLozenge({
|
||||||
@@ -259,6 +261,7 @@ export function StagePanel({
|
|||||||
agentRoster,
|
agentRoster,
|
||||||
busyAgentNames,
|
busyAgentNames,
|
||||||
onStartAgent,
|
onStartAgent,
|
||||||
|
mergesInFlight,
|
||||||
}: StagePanelProps) {
|
}: StagePanelProps) {
|
||||||
const showStartButton =
|
const showStartButton =
|
||||||
Boolean(onStartAgent) &&
|
Boolean(onStartAgent) &&
|
||||||
@@ -355,6 +358,19 @@ export function StagePanel({
|
|||||||
✕
|
✕
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{mergesInFlight?.has(item.story_id) && (
|
||||||
|
<span
|
||||||
|
data-testid={`merge-in-flight-icon-${item.story_id}`}
|
||||||
|
title="Deterministic merge in progress"
|
||||||
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
marginRight: "6px",
|
||||||
|
animation: "spin 1s linear infinite",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
⟳
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{itemNumber && (
|
{itemNumber && (
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ export function useChatWebSocket({
|
|||||||
qa: [],
|
qa: [],
|
||||||
merge: [],
|
merge: [],
|
||||||
done: [],
|
done: [],
|
||||||
|
deterministic_merges_in_flight: [],
|
||||||
});
|
});
|
||||||
const [pipelineVersion, setPipelineVersion] = useState(0);
|
const [pipelineVersion, setPipelineVersion] = useState(0);
|
||||||
const [reconciliationActive, setReconciliationActive] = useState(false);
|
const [reconciliationActive, setReconciliationActive] = useState(false);
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ pub struct PipelineState {
|
|||||||
pub qa: Vec<UpcomingStory>,
|
pub qa: Vec<UpcomingStory>,
|
||||||
pub merge: Vec<UpcomingStory>,
|
pub merge: Vec<UpcomingStory>,
|
||||||
pub done: Vec<UpcomingStory>,
|
pub done: Vec<UpcomingStory>,
|
||||||
|
/// Story IDs that currently have a deterministic merge in progress.
|
||||||
|
pub deterministic_merges_in_flight: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load the full pipeline state (all 5 active stages).
|
/// Load the full pipeline state (all 5 active stages).
|
||||||
@@ -71,12 +73,19 @@ pub fn load_pipeline_state(ctx: &AppContext) -> Result<PipelineState, String> {
|
|||||||
|
|
||||||
let typed_items = crate::pipeline_state::read_all_typed();
|
let typed_items = crate::pipeline_state::read_all_typed();
|
||||||
|
|
||||||
|
let deterministic_merges_in_flight = ctx
|
||||||
|
.services
|
||||||
|
.agents
|
||||||
|
.list_running_merges()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let mut state = PipelineState {
|
let mut state = PipelineState {
|
||||||
backlog: Vec::new(),
|
backlog: Vec::new(),
|
||||||
current: Vec::new(),
|
current: Vec::new(),
|
||||||
qa: Vec::new(),
|
qa: Vec::new(),
|
||||||
merge: Vec::new(),
|
merge: Vec::new(),
|
||||||
done: Vec::new(),
|
done: Vec::new(),
|
||||||
|
deterministic_merges_in_flight,
|
||||||
};
|
};
|
||||||
|
|
||||||
for item in typed_items {
|
for item in typed_items {
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ pub fn pipeline_state_to_response(s: PipelineState) -> WsResponse {
|
|||||||
qa: s.qa,
|
qa: s.qa,
|
||||||
merge: s.merge,
|
merge: s.merge,
|
||||||
done: s.done,
|
done: s.done,
|
||||||
|
deterministic_merges_in_flight: s.deterministic_merges_in_flight,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,6 +239,7 @@ mod tests {
|
|||||||
blocked: None,
|
blocked: None,
|
||||||
depends_on: None,
|
depends_on: None,
|
||||||
}],
|
}],
|
||||||
|
deterministic_merges_in_flight: vec![],
|
||||||
};
|
};
|
||||||
let resp = pipeline_state_to_response(state);
|
let resp = pipeline_state_to_response(state);
|
||||||
let json = serde_json::to_value(&resp).unwrap();
|
let json = serde_json::to_value(&resp).unwrap();
|
||||||
@@ -260,6 +262,7 @@ mod tests {
|
|||||||
qa: vec![],
|
qa: vec![],
|
||||||
merge: vec![],
|
merge: vec![],
|
||||||
done: vec![],
|
done: vec![],
|
||||||
|
deterministic_merges_in_flight: vec![],
|
||||||
};
|
};
|
||||||
let resp = pipeline_state_to_response(state);
|
let resp = pipeline_state_to_response(state);
|
||||||
let json = serde_json::to_value(&resp).unwrap();
|
let json = serde_json::to_value(&resp).unwrap();
|
||||||
@@ -294,6 +297,7 @@ mod tests {
|
|||||||
qa: vec![],
|
qa: vec![],
|
||||||
merge: vec![],
|
merge: vec![],
|
||||||
done: vec![],
|
done: vec![],
|
||||||
|
deterministic_merges_in_flight: 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();
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ pub enum WsResponse {
|
|||||||
qa: Vec<UpcomingStory>,
|
qa: Vec<UpcomingStory>,
|
||||||
merge: Vec<UpcomingStory>,
|
merge: Vec<UpcomingStory>,
|
||||||
done: Vec<UpcomingStory>,
|
done: Vec<UpcomingStory>,
|
||||||
|
/// Story IDs that currently have a deterministic merge in progress.
|
||||||
|
deterministic_merges_in_flight: Vec<String>,
|
||||||
},
|
},
|
||||||
/// `.huskies/project.toml` was modified; the frontend should re-fetch the
|
/// `.huskies/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.
|
||||||
@@ -215,6 +217,7 @@ mod tests {
|
|||||||
qa: vec![],
|
qa: vec![],
|
||||||
merge: vec![],
|
merge: vec![],
|
||||||
done: vec![],
|
done: vec![],
|
||||||
|
deterministic_merges_in_flight: 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");
|
||||||
|
|||||||
Reference in New Issue
Block a user