story-kit: merge 149_bug_web_ui_does_not_update_when_agents_are_started_or_stopped
This commit is contained in:
@@ -61,6 +61,8 @@ export type WsResponse =
|
||||
}
|
||||
/** `.story_kit/project.toml` was modified; re-fetch the agent roster. */
|
||||
| { type: "agent_config_changed" }
|
||||
/** An agent started, stopped, or changed state; re-fetch agent list. */
|
||||
| { type: "agent_state_changed" }
|
||||
| { type: "tool_activity"; tool_name: string }
|
||||
/** Heartbeat response confirming the connection is alive. */
|
||||
| { type: "pong" }
|
||||
@@ -282,6 +284,7 @@ export class ChatWebSocket {
|
||||
message: string,
|
||||
) => void;
|
||||
private onAgentConfigChanged?: () => void;
|
||||
private onAgentStateChanged?: () => void;
|
||||
private onOnboardingStatus?: (needsOnboarding: boolean) => void;
|
||||
private connected = false;
|
||||
private closeTimer?: number;
|
||||
@@ -358,6 +361,7 @@ export class ChatWebSocket {
|
||||
data.message,
|
||||
);
|
||||
if (data.type === "agent_config_changed") this.onAgentConfigChanged?.();
|
||||
if (data.type === "agent_state_changed") this.onAgentStateChanged?.();
|
||||
if (data.type === "onboarding_status")
|
||||
this.onOnboardingStatus?.(data.needs_onboarding);
|
||||
if (data.type === "pong") {
|
||||
@@ -410,6 +414,7 @@ export class ChatWebSocket {
|
||||
message: string,
|
||||
) => void;
|
||||
onAgentConfigChanged?: () => void;
|
||||
onAgentStateChanged?: () => void;
|
||||
onOnboardingStatus?: (needsOnboarding: boolean) => void;
|
||||
},
|
||||
wsPath = DEFAULT_WS_PATH,
|
||||
@@ -423,6 +428,7 @@ export class ChatWebSocket {
|
||||
this.onActivity = handlers.onActivity;
|
||||
this.onReconciliationProgress = handlers.onReconciliationProgress;
|
||||
this.onAgentConfigChanged = handlers.onAgentConfigChanged;
|
||||
this.onAgentStateChanged = handlers.onAgentStateChanged;
|
||||
this.onOnboardingStatus = handlers.onOnboardingStatus;
|
||||
this.wsPath = wsPath;
|
||||
this.shouldReconnect = true;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -89,6 +89,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
>([]);
|
||||
const reconciliationEventIdRef = useRef(0);
|
||||
const [agentConfigVersion, setAgentConfigVersion] = useState(0);
|
||||
const [agentStateVersion, setAgentStateVersion] = useState(0);
|
||||
const [needsOnboarding, setNeedsOnboarding] = useState(false);
|
||||
const onboardingTriggeredRef = useRef(false);
|
||||
const [queuedMessages, setQueuedMessages] = useState<
|
||||
@@ -258,6 +259,9 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
onAgentConfigChanged: () => {
|
||||
setAgentConfigVersion((v) => v + 1);
|
||||
},
|
||||
onAgentStateChanged: () => {
|
||||
setAgentStateVersion((v) => v + 1);
|
||||
},
|
||||
onOnboardingStatus: (onboarding: boolean) => {
|
||||
setNeedsOnboarding(onboarding);
|
||||
},
|
||||
@@ -1065,7 +1069,10 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
}}
|
||||
>
|
||||
<LozengeFlyProvider pipeline={pipeline}>
|
||||
<AgentPanel configVersion={agentConfigVersion} />
|
||||
<AgentPanel
|
||||
configVersion={agentConfigVersion}
|
||||
stateVersion={agentStateVersion}
|
||||
/>
|
||||
|
||||
<StagePanel title="To Merge" items={pipeline.merge} />
|
||||
<StagePanel title="QA" items={pipeline.qa} />
|
||||
|
||||
Reference in New Issue
Block a user