story-kit: merge 149_bug_web_ui_does_not_update_when_agents_are_started_or_stopped

This commit is contained in:
Dave
2026-02-24 23:09:13 +00:00
parent 51303c07d8
commit dc631d1933
8 changed files with 187 additions and 108 deletions

View File

@@ -136,9 +136,14 @@ function agentKey(storyId: string, agentName: string): string {
interface AgentPanelProps {
/** Increment this to trigger a re-fetch of the agent roster. */
configVersion?: number;
/** Increment this to trigger a re-fetch of the agent list (agent state changed). */
stateVersion?: number;
}
export function AgentPanel({ configVersion = 0 }: AgentPanelProps) {
export function AgentPanel({
configVersion = 0,
stateVersion = 0,
}: AgentPanelProps) {
const { hiddenRosterAgents } = useLozengeFly();
const [agents, setAgents] = useState<Record<string, AgentState>>({});
const [roster, setRoster] = useState<AgentConfigInfo[]>([]);
@@ -157,52 +162,6 @@ export function AgentPanel({ configVersion = 0 }: AgentPanelProps) {
.catch((err) => console.error("Failed to load agent config:", err));
}, [configVersion]);
// Load existing agents and editor preference on mount
useEffect(() => {
agentsApi
.listAgents()
.then((agentList) => {
const agentMap: Record<string, AgentState> = {};
const now = Date.now();
for (const a of agentList) {
const key = agentKey(a.story_id, a.agent_name);
const isTerminal = a.status === "completed" || a.status === "failed";
agentMap[key] = {
agentName: a.agent_name,
status: a.status,
log: [],
thinking: "",
thinkingDone: false,
sessionId: a.session_id,
worktreePath: a.worktree_path,
baseBranch: a.base_branch,
terminalAt: isTerminal ? now : null,
};
if (a.status === "running" || a.status === "pending") {
subscribeToAgent(a.story_id, a.agent_name);
}
}
setAgents(agentMap);
setLastRefresh(new Date());
})
.catch((err) => console.error("Failed to load agents:", err));
settingsApi
.getEditorCommand()
.then((s) => {
setEditorCommand(s.editor_command);
setEditorInput(s.editor_command ?? "");
})
.catch((err) => console.error("Failed to load editor command:", err));
return () => {
for (const cleanup of Object.values(cleanupRefs.current)) {
cleanup();
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const subscribeToAgent = useCallback((storyId: string, agentName: string) => {
const key = agentKey(storyId, agentName);
cleanupRefs.current[key]?.();
@@ -296,6 +255,65 @@ export function AgentPanel({ configVersion = 0 }: AgentPanelProps) {
cleanupRefs.current[key] = cleanup;
}, []);
/** Shared helper: fetch the agent list and update state + SSE subscriptions. */
const refreshAgents = useCallback(() => {
agentsApi
.listAgents()
.then((agentList) => {
const agentMap: Record<string, AgentState> = {};
const now = Date.now();
for (const a of agentList) {
const key = agentKey(a.story_id, a.agent_name);
const isTerminal = a.status === "completed" || a.status === "failed";
agentMap[key] = {
agentName: a.agent_name,
status: a.status,
log: [],
thinking: "",
thinkingDone: false,
sessionId: a.session_id,
worktreePath: a.worktree_path,
baseBranch: a.base_branch,
terminalAt: isTerminal ? now : null,
};
if (a.status === "running" || a.status === "pending") {
subscribeToAgent(a.story_id, a.agent_name);
}
}
setAgents(agentMap);
setLastRefresh(new Date());
})
.catch((err) => console.error("Failed to load agents:", err));
}, [subscribeToAgent]);
// Load existing agents and editor preference on mount
useEffect(() => {
refreshAgents();
settingsApi
.getEditorCommand()
.then((s) => {
setEditorCommand(s.editor_command);
setEditorInput(s.editor_command ?? "");
})
.catch((err) => console.error("Failed to load editor command:", err));
return () => {
for (const cleanup of Object.values(cleanupRefs.current)) {
cleanup();
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Re-fetch agent list when agent state changes (via WebSocket notification).
// Skip the initial render (stateVersion=0) since the mount effect handles that.
useEffect(() => {
if (stateVersion > 0) {
refreshAgents();
}
}, [stateVersion, refreshAgents]);
const handleSaveEditor = async () => {
try {
const trimmed = editorInput.trim() || null;