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) */
|
||||
@keyframes agentAppear {
|
||||
from {
|
||||
|
||||
@@ -267,6 +267,7 @@ describe("ChatWebSocket", () => {
|
||||
qa: [],
|
||||
merge: [],
|
||||
done: [],
|
||||
deterministic_merges_in_flight: [],
|
||||
};
|
||||
instances[1].simulateMessage({ type: "pipeline_state", ...freshState });
|
||||
|
||||
|
||||
@@ -69,6 +69,8 @@ export interface PipelineState {
|
||||
qa: PipelineStageItem[];
|
||||
merge: 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. */
|
||||
@@ -84,6 +86,7 @@ export type WsResponse =
|
||||
qa: PipelineStageItem[];
|
||||
merge: PipelineStageItem[];
|
||||
done: PipelineStageItem[];
|
||||
deterministic_merges_in_flight: string[];
|
||||
}
|
||||
| {
|
||||
type: "permission_request";
|
||||
|
||||
@@ -123,6 +123,8 @@ export class ChatWebSocket {
|
||||
qa: data.qa,
|
||||
merge: data.merge,
|
||||
done: data.done,
|
||||
deterministic_merges_in_flight:
|
||||
data.deterministic_merges_in_flight ?? [],
|
||||
});
|
||||
if (data.type === "permission_request")
|
||||
this.onPermissionRequest?.(
|
||||
|
||||
@@ -110,52 +110,66 @@ export function ChatPipelinePanel({
|
||||
configVersion={agentConfigVersion}
|
||||
stateVersion={agentStateVersion}
|
||||
/>
|
||||
<StagePanel
|
||||
title="Done"
|
||||
items={pipeline.done ?? []}
|
||||
costs={storyTokenCosts}
|
||||
onItemClick={(item) => onSelectWorkItem(item.story_id)}
|
||||
onStopAgent={onStopAgent}
|
||||
onDeleteItem={onDeleteItem}
|
||||
/>
|
||||
<StagePanel
|
||||
title="To Merge"
|
||||
items={pipeline.merge}
|
||||
costs={storyTokenCosts}
|
||||
onItemClick={(item) => onSelectWorkItem(item.story_id)}
|
||||
onStopAgent={onStopAgent}
|
||||
onDeleteItem={onDeleteItem}
|
||||
/>
|
||||
<StagePanel
|
||||
title="QA"
|
||||
items={pipeline.qa}
|
||||
costs={storyTokenCosts}
|
||||
onItemClick={(item) => onSelectWorkItem(item.story_id)}
|
||||
onStopAgent={onStopAgent}
|
||||
onDeleteItem={onDeleteItem}
|
||||
/>
|
||||
<StagePanel
|
||||
title="Current"
|
||||
items={pipeline.current}
|
||||
costs={storyTokenCosts}
|
||||
onItemClick={(item) => onSelectWorkItem(item.story_id)}
|
||||
agentRoster={agentRoster}
|
||||
busyAgentNames={busyAgentNames}
|
||||
onStartAgent={onStartAgent}
|
||||
onStopAgent={onStopAgent}
|
||||
onDeleteItem={onDeleteItem}
|
||||
/>
|
||||
<StagePanel
|
||||
title="Backlog"
|
||||
items={pipeline.backlog}
|
||||
costs={storyTokenCosts}
|
||||
onItemClick={(item) => onSelectWorkItem(item.story_id)}
|
||||
agentRoster={agentRoster}
|
||||
busyAgentNames={busyAgentNames}
|
||||
onStartAgent={onStartAgent}
|
||||
onStopAgent={onStopAgent}
|
||||
onDeleteItem={onDeleteItem}
|
||||
/>
|
||||
{(() => {
|
||||
const mergesInFlight = new Set(
|
||||
pipeline.deterministic_merges_in_flight ?? [],
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<StagePanel
|
||||
title="Done"
|
||||
items={pipeline.done ?? []}
|
||||
costs={storyTokenCosts}
|
||||
onItemClick={(item) => onSelectWorkItem(item.story_id)}
|
||||
onStopAgent={onStopAgent}
|
||||
onDeleteItem={onDeleteItem}
|
||||
mergesInFlight={mergesInFlight}
|
||||
/>
|
||||
<StagePanel
|
||||
title="To Merge"
|
||||
items={pipeline.merge}
|
||||
costs={storyTokenCosts}
|
||||
onItemClick={(item) => onSelectWorkItem(item.story_id)}
|
||||
onStopAgent={onStopAgent}
|
||||
onDeleteItem={onDeleteItem}
|
||||
mergesInFlight={mergesInFlight}
|
||||
/>
|
||||
<StagePanel
|
||||
title="QA"
|
||||
items={pipeline.qa}
|
||||
costs={storyTokenCosts}
|
||||
onItemClick={(item) => onSelectWorkItem(item.story_id)}
|
||||
onStopAgent={onStopAgent}
|
||||
onDeleteItem={onDeleteItem}
|
||||
mergesInFlight={mergesInFlight}
|
||||
/>
|
||||
<StagePanel
|
||||
title="Current"
|
||||
items={pipeline.current}
|
||||
costs={storyTokenCosts}
|
||||
onItemClick={(item) => onSelectWorkItem(item.story_id)}
|
||||
agentRoster={agentRoster}
|
||||
busyAgentNames={busyAgentNames}
|
||||
onStartAgent={onStartAgent}
|
||||
onStopAgent={onStopAgent}
|
||||
onDeleteItem={onDeleteItem}
|
||||
mergesInFlight={mergesInFlight}
|
||||
/>
|
||||
<StagePanel
|
||||
title="Backlog"
|
||||
items={pipeline.backlog}
|
||||
costs={storyTokenCosts}
|
||||
onItemClick={(item) => onSelectWorkItem(item.story_id)}
|
||||
agentRoster={agentRoster}
|
||||
busyAgentNames={busyAgentNames}
|
||||
onStartAgent={onStartAgent}
|
||||
onStopAgent={onStopAgent}
|
||||
onDeleteItem={onDeleteItem}
|
||||
mergesInFlight={mergesInFlight}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
<ServerLogsPanel logs={combinedLogs} />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -14,6 +14,7 @@ function makePipeline(overrides: Partial<PipelineState> = {}): PipelineState {
|
||||
qa: [],
|
||||
merge: [],
|
||||
done: [],
|
||||
deterministic_merges_in_flight: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ function makePipeline(overrides: Partial<PipelineState> = {}): PipelineState {
|
||||
qa: [],
|
||||
merge: [],
|
||||
done: [],
|
||||
deterministic_merges_in_flight: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ function makePipeline(overrides: Partial<PipelineState> = {}): PipelineState {
|
||||
qa: [],
|
||||
merge: [],
|
||||
done: [],
|
||||
deterministic_merges_in_flight: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ function makePipeline(overrides: Partial<PipelineState> = {}): PipelineState {
|
||||
qa: [],
|
||||
merge: [],
|
||||
done: [],
|
||||
deterministic_merges_in_flight: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -324,4 +324,71 @@ describe("StagePanel", () => {
|
||||
screen.queryByTestId("merge-failure-reason-31_story_no_failure"),
|
||||
).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>;
|
||||
/** Called when the user requests to start an agent on a story. */
|
||||
onStartAgent?: (storyId: string, agentName?: string) => void;
|
||||
/** Set of story IDs that currently have a deterministic merge in progress. */
|
||||
mergesInFlight?: Set<string>;
|
||||
}
|
||||
|
||||
function AgentLozenge({
|
||||
@@ -259,6 +261,7 @@ export function StagePanel({
|
||||
agentRoster,
|
||||
busyAgentNames,
|
||||
onStartAgent,
|
||||
mergesInFlight,
|
||||
}: StagePanelProps) {
|
||||
const showStartButton =
|
||||
Boolean(onStartAgent) &&
|
||||
@@ -355,6 +358,19 @@ export function StagePanel({
|
||||
✕
|
||||
</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 && (
|
||||
<span
|
||||
style={{
|
||||
|
||||
@@ -103,6 +103,7 @@ export function useChatWebSocket({
|
||||
qa: [],
|
||||
merge: [],
|
||||
done: [],
|
||||
deterministic_merges_in_flight: [],
|
||||
});
|
||||
const [pipelineVersion, setPipelineVersion] = useState(0);
|
||||
const [reconciliationActive, setReconciliationActive] = useState(false);
|
||||
|
||||
Reference in New Issue
Block a user