story-kit: merge 336_story_web_ui_button_to_start_a_coder_on_a_story
This commit is contained in:
@@ -2,6 +2,8 @@ import * as React from "react";
|
||||
import Markdown from "react-markdown";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
import type { AgentConfigInfo } from "../api/agents";
|
||||
import { agentsApi } from "../api/agents";
|
||||
import type { PipelineState } from "../api/client";
|
||||
import { api, ChatWebSocket } from "../api/client";
|
||||
import { useChatHistory } from "../hooks/useChatHistory";
|
||||
@@ -202,6 +204,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
const [agentConfigVersion, setAgentConfigVersion] = useState(0);
|
||||
const [agentStateVersion, setAgentStateVersion] = useState(0);
|
||||
const [pipelineVersion, setPipelineVersion] = useState(0);
|
||||
const [agentRoster, setAgentRoster] = useState<AgentConfigInfo[]>([]);
|
||||
const [storyTokenCosts, setStoryTokenCosts] = useState<Map<string, number>>(
|
||||
new Map(),
|
||||
);
|
||||
@@ -237,6 +240,26 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
const userScrolledUpRef = useRef(false);
|
||||
const pendingMessageRef = useRef<string>("");
|
||||
|
||||
// Agents currently running or pending across all pipeline stages.
|
||||
const busyAgentNames = useMemo(() => {
|
||||
const busy = new Set<string>();
|
||||
const allItems = [
|
||||
...pipeline.backlog,
|
||||
...pipeline.current,
|
||||
...pipeline.qa,
|
||||
...pipeline.merge,
|
||||
];
|
||||
for (const item of allItems) {
|
||||
if (
|
||||
item.agent &&
|
||||
(item.agent.status === "running" || item.agent.status === "pending")
|
||||
) {
|
||||
busy.add(item.agent.agent_name);
|
||||
}
|
||||
}
|
||||
return busy;
|
||||
}, [pipeline]);
|
||||
|
||||
const contextUsage = useMemo(() => {
|
||||
let totalTokens = 0;
|
||||
totalTokens += 200;
|
||||
@@ -504,6 +527,27 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []);
|
||||
|
||||
// Fetch agent roster whenever the config changes so the start button knows available agents.
|
||||
useEffect(() => {
|
||||
agentsApi
|
||||
.getAgentConfig()
|
||||
.then(setAgentRoster)
|
||||
.catch(() => {
|
||||
// Silently ignore — roster unavailable.
|
||||
});
|
||||
}, [agentConfigVersion]);
|
||||
|
||||
const handleStartAgent = useCallback(
|
||||
async (storyId: string, agentName?: string) => {
|
||||
try {
|
||||
await agentsApi.startAgent(storyId, agentName);
|
||||
} catch (err) {
|
||||
console.error("Failed to start agent:", err);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const cancelGeneration = async () => {
|
||||
// Preserve queued messages by appending them to the chat input box
|
||||
if (queuedMessagesRef.current.length > 0) {
|
||||
@@ -1051,12 +1095,18 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
items={pipeline.current}
|
||||
costs={storyTokenCosts}
|
||||
onItemClick={(item) => setSelectedWorkItemId(item.story_id)}
|
||||
agentRoster={agentRoster}
|
||||
busyAgentNames={busyAgentNames}
|
||||
onStartAgent={handleStartAgent}
|
||||
/>
|
||||
<StagePanel
|
||||
title="Backlog"
|
||||
items={pipeline.backlog}
|
||||
costs={storyTokenCosts}
|
||||
onItemClick={(item) => setSelectedWorkItemId(item.story_id)}
|
||||
agentRoster={agentRoster}
|
||||
busyAgentNames={busyAgentNames}
|
||||
onStartAgent={handleStartAgent}
|
||||
/>
|
||||
<ServerLogsPanel logs={serverLogs} />
|
||||
</>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import * as React from "react";
|
||||
import type { AgentConfigInfo } from "../api/agents";
|
||||
import type { AgentAssignment, PipelineStageItem } from "../api/client";
|
||||
import { useLozengeFly } from "./LozengeFlyContext";
|
||||
|
||||
const { useLayoutEffect, useRef } = React;
|
||||
const { useLayoutEffect, useRef, useState } = React;
|
||||
|
||||
type WorkItemType = "story" | "bug" | "spike" | "refactor" | "unknown";
|
||||
|
||||
@@ -44,6 +45,12 @@ interface StagePanelProps {
|
||||
onItemClick?: (item: PipelineStageItem) => void;
|
||||
/** Map of story_id → total_cost_usd for displaying cost badges. */
|
||||
costs?: Map<string, number>;
|
||||
/** Agent roster to populate the start agent dropdown. */
|
||||
agentRoster?: AgentConfigInfo[];
|
||||
/** Names of agents currently running/pending (busy). */
|
||||
busyAgentNames?: Set<string>;
|
||||
/** Called when the user requests to start an agent on a story. */
|
||||
onStartAgent?: (storyId: string, agentName?: string) => void;
|
||||
}
|
||||
|
||||
function AgentLozenge({
|
||||
@@ -125,13 +132,108 @@ function AgentLozenge({
|
||||
);
|
||||
}
|
||||
|
||||
function StartAgentControl({
|
||||
storyId,
|
||||
agentRoster,
|
||||
busyAgentNames,
|
||||
onStartAgent,
|
||||
}: {
|
||||
storyId: string;
|
||||
agentRoster: AgentConfigInfo[];
|
||||
busyAgentNames: Set<string>;
|
||||
onStartAgent: (storyId: string, agentName?: string) => void;
|
||||
}) {
|
||||
const [selectedAgent, setSelectedAgent] = useState<string>("");
|
||||
|
||||
const allBusy =
|
||||
agentRoster.length > 0 &&
|
||||
agentRoster.every((a) => busyAgentNames.has(a.name));
|
||||
|
||||
const handleStart = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onStartAgent(storyId, selectedAgent || undefined);
|
||||
};
|
||||
|
||||
const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
e.stopPropagation();
|
||||
setSelectedAgent(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "4px",
|
||||
marginTop: "6px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{agentRoster.length > 1 && (
|
||||
<select
|
||||
value={selectedAgent}
|
||||
onChange={handleSelectChange}
|
||||
disabled={allBusy}
|
||||
data-testid={`start-agent-select-${storyId}`}
|
||||
style={{
|
||||
background: "#2a2a2a",
|
||||
color: allBusy ? "#555" : "#ccc",
|
||||
border: "1px solid #444",
|
||||
borderRadius: "5px",
|
||||
padding: "2px 4px",
|
||||
fontSize: "0.75em",
|
||||
cursor: allBusy ? "not-allowed" : "pointer",
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<option value="">Default agent</option>
|
||||
{agentRoster.map((a) => (
|
||||
<option key={a.name} value={a.name}>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleStart}
|
||||
disabled={allBusy}
|
||||
data-testid={`start-agent-btn-${storyId}`}
|
||||
title={allBusy ? "All agents are busy" : "Start a coder on this story"}
|
||||
style={{
|
||||
background: allBusy ? "#1a1a1a" : "#1a3a1a",
|
||||
color: allBusy ? "#555" : "#3fb950",
|
||||
border: `1px solid ${allBusy ? "#333" : "#2a5a2a"}`,
|
||||
borderRadius: "5px",
|
||||
padding: "2px 8px",
|
||||
fontSize: "0.75em",
|
||||
fontWeight: 600,
|
||||
cursor: allBusy ? "not-allowed" : "pointer",
|
||||
whiteSpace: "nowrap",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
▶ Start
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function StagePanel({
|
||||
title,
|
||||
items,
|
||||
emptyMessage = "Empty.",
|
||||
onItemClick,
|
||||
costs,
|
||||
agentRoster,
|
||||
busyAgentNames,
|
||||
onStartAgent,
|
||||
}: StagePanelProps) {
|
||||
const showStartButton =
|
||||
Boolean(onStartAgent) &&
|
||||
agentRoster !== undefined &&
|
||||
agentRoster.length > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -201,6 +303,10 @@ export function StagePanel({
|
||||
font: "inherit",
|
||||
cursor: onItemClick ? "pointer" : "default",
|
||||
};
|
||||
|
||||
// Only offer "Start" when the item has no assigned agent
|
||||
const canStart = showStartButton && !item.agent;
|
||||
|
||||
const cardInner = (
|
||||
<>
|
||||
<div style={{ flex: 1 }}>
|
||||
@@ -287,6 +393,14 @@ export function StagePanel({
|
||||
{item.agent && (
|
||||
<AgentLozenge agent={item.agent} storyId={item.story_id} />
|
||||
)}
|
||||
{canStart && onStartAgent && (
|
||||
<StartAgentControl
|
||||
storyId={item.story_id}
|
||||
agentRoster={agentRoster ?? []}
|
||||
busyAgentNames={busyAgentNames ?? new Set()}
|
||||
onStartAgent={onStartAgent}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return onItemClick ? (
|
||||
|
||||
Reference in New Issue
Block a user