story-kit: merge 339_story_web_ui_agent_assignment_dropdown_on_work_items
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
import * as React from "react";
|
||||
import Markdown from "react-markdown";
|
||||
import type { AgentEvent, AgentInfo, AgentStatusValue } from "../api/agents";
|
||||
import type {
|
||||
AgentConfigInfo,
|
||||
AgentEvent,
|
||||
AgentInfo,
|
||||
AgentStatusValue,
|
||||
} from "../api/agents";
|
||||
import { agentsApi, subscribeAgentStream } from "../api/agents";
|
||||
import type {
|
||||
AgentCostEntry,
|
||||
@@ -10,7 +15,7 @@ import type {
|
||||
} from "../api/client";
|
||||
import { api } from "../api/client";
|
||||
|
||||
const { useEffect, useRef, useState } = React;
|
||||
const { useCallback, useEffect, useRef, useState } = React;
|
||||
|
||||
const STAGE_LABELS: Record<string, string> = {
|
||||
backlog: "Backlog",
|
||||
@@ -131,6 +136,9 @@ export function WorkItemDetailPanel({
|
||||
null,
|
||||
);
|
||||
const [tokenCost, setTokenCost] = useState<TokenCostResponse | null>(null);
|
||||
const [agentConfig, setAgentConfig] = useState<AgentConfigInfo[]>([]);
|
||||
const [assigning, setAssigning] = useState(false);
|
||||
const [assignError, setAssignError] = useState<string | null>(null);
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
const cleanupRef = useRef<(() => void) | null>(null);
|
||||
|
||||
@@ -242,6 +250,59 @@ export function WorkItemDetailPanel({
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [onClose]);
|
||||
|
||||
// Load agent config roster for the dropdown.
|
||||
useEffect(() => {
|
||||
agentsApi
|
||||
.getAgentConfig()
|
||||
.then((config) => {
|
||||
setAgentConfig(config);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
console.error("Failed to load agent config:", err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Map pipeline stage → agent stage filter.
|
||||
const STAGE_TO_AGENT_STAGE: Record<string, string> = {
|
||||
current: "coder",
|
||||
qa: "qa",
|
||||
merge: "mergemaster",
|
||||
};
|
||||
|
||||
const filteredAgents = agentConfig.filter(
|
||||
(a) => a.stage === STAGE_TO_AGENT_STAGE[stage],
|
||||
);
|
||||
|
||||
// The currently active agent name for this story (running or pending).
|
||||
const activeAgentName =
|
||||
agentInfo && (agentStatus === "running" || agentStatus === "pending")
|
||||
? agentInfo.agent_name
|
||||
: null;
|
||||
|
||||
const handleAgentAssign = useCallback(
|
||||
async (selectedAgentName: string) => {
|
||||
setAssigning(true);
|
||||
setAssignError(null);
|
||||
try {
|
||||
// Stop current running agent if there is one.
|
||||
if (activeAgentName) {
|
||||
await agentsApi.stopAgent(storyId, activeAgentName);
|
||||
}
|
||||
// Start the new agent (or skip if "none" selected).
|
||||
if (selectedAgentName) {
|
||||
await agentsApi.startAgent(storyId, selectedAgentName);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
setAssignError(
|
||||
err instanceof Error ? err.message : "Failed to assign agent",
|
||||
);
|
||||
} finally {
|
||||
setAssigning(false);
|
||||
}
|
||||
},
|
||||
[storyId, activeAgentName],
|
||||
);
|
||||
|
||||
const stageLabel = STAGE_LABELS[stage] ?? stage;
|
||||
const hasTestResults =
|
||||
testResults &&
|
||||
@@ -301,7 +362,72 @@ export function WorkItemDetailPanel({
|
||||
{stageLabel}
|
||||
</div>
|
||||
)}
|
||||
{assignedAgent ? (
|
||||
{filteredAgents.length > 0 && (
|
||||
<div
|
||||
data-testid="detail-panel-agent-assignment"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
marginTop: "4px",
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: "0.75em", color: "#666" }}>Agent:</span>
|
||||
<select
|
||||
data-testid="agent-assignment-dropdown"
|
||||
disabled={assigning}
|
||||
value={activeAgentName ?? assignedAgent ?? ""}
|
||||
onChange={(e) => handleAgentAssign(e.target.value)}
|
||||
style={{
|
||||
background: "#1a1a1a",
|
||||
border: "1px solid #444",
|
||||
borderRadius: "4px",
|
||||
color: "#ccc",
|
||||
cursor: assigning ? "not-allowed" : "pointer",
|
||||
fontSize: "0.75em",
|
||||
padding: "2px 6px",
|
||||
opacity: assigning ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
<option value="">— none —</option>
|
||||
{filteredAgents.map((a) => {
|
||||
const isRunning =
|
||||
agentInfo?.agent_name === a.name &&
|
||||
agentStatus === "running";
|
||||
const isPending =
|
||||
agentInfo?.agent_name === a.name &&
|
||||
agentStatus === "pending";
|
||||
const statusLabel = isRunning
|
||||
? " — running"
|
||||
: isPending
|
||||
? " — pending"
|
||||
: " — idle";
|
||||
const modelPart = a.model ? ` (${a.model})` : "";
|
||||
return (
|
||||
<option key={a.name} value={a.name}>
|
||||
{a.name}
|
||||
{modelPart}
|
||||
{statusLabel}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
{assigning && (
|
||||
<span style={{ fontSize: "0.7em", color: "#888" }}>
|
||||
Assigning…
|
||||
</span>
|
||||
)}
|
||||
{assignError && (
|
||||
<span
|
||||
data-testid="agent-assignment-error"
|
||||
style={{ fontSize: "0.7em", color: "#f85149" }}
|
||||
>
|
||||
{assignError}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{filteredAgents.length === 0 && assignedAgent ? (
|
||||
<div
|
||||
data-testid="detail-panel-assigned-agent"
|
||||
style={{ fontSize: "0.75em", color: "#888" }}
|
||||
|
||||
Reference in New Issue
Block a user