feat(story-115): hot-reload project.toml agent config without server restart
- Extend `WatcherEvent` to an enum with `WorkItem` and `ConfigChanged` variants so the watcher can distinguish between pipeline-file changes and config changes - Watch `.story_kit/project.toml` at the project root (ignoring worktree copies) and broadcast `WatcherEvent::ConfigChanged` on modification - Forward `agent_config_changed` WebSocket message to connected clients; skip pipeline state refresh for config-only events - Add `is_config_file()` helper with unit tests covering root vs. worktree paths - Accept `configVersion` prop in `AgentPanel` and re-fetch the agent roster whenever it increments - Increment `agentConfigVersion` in `Chat` on receipt of `agent_config_changed` WS event via new `onAgentConfigChanged` handler in `ChatWebSocket` Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -57,7 +57,9 @@ export type WsResponse =
|
||||
story_id: string;
|
||||
status: string;
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
/** `.story_kit/project.toml` was modified; re-fetch the agent roster. */
|
||||
| { type: "agent_config_changed" };
|
||||
|
||||
export interface ProviderConfig {
|
||||
provider: string;
|
||||
@@ -273,6 +275,7 @@ export class ChatWebSocket {
|
||||
status: string,
|
||||
message: string,
|
||||
) => void;
|
||||
private onAgentConfigChanged?: () => void;
|
||||
private connected = false;
|
||||
private closeTimer?: number;
|
||||
private wsPath = DEFAULT_WS_PATH;
|
||||
@@ -322,6 +325,7 @@ export class ChatWebSocket {
|
||||
data.status,
|
||||
data.message,
|
||||
);
|
||||
if (data.type === "agent_config_changed") this.onAgentConfigChanged?.();
|
||||
} catch (err) {
|
||||
this.onError?.(String(err));
|
||||
}
|
||||
@@ -367,6 +371,7 @@ export class ChatWebSocket {
|
||||
status: string,
|
||||
message: string,
|
||||
) => void;
|
||||
onAgentConfigChanged?: () => void;
|
||||
},
|
||||
wsPath = DEFAULT_WS_PATH,
|
||||
) {
|
||||
@@ -378,6 +383,7 @@ export class ChatWebSocket {
|
||||
this.onPermissionRequest = handlers.onPermissionRequest;
|
||||
this.onActivity = handlers.onActivity;
|
||||
this.onReconciliationProgress = handlers.onReconciliationProgress;
|
||||
this.onAgentConfigChanged = handlers.onAgentConfigChanged;
|
||||
this.wsPath = wsPath;
|
||||
this.shouldReconnect = true;
|
||||
|
||||
|
||||
@@ -79,7 +79,12 @@ function agentKey(storyId: string, agentName: string): string {
|
||||
return `${storyId}:${agentName}`;
|
||||
}
|
||||
|
||||
export function AgentPanel() {
|
||||
interface AgentPanelProps {
|
||||
/** Increment this to trigger a re-fetch of the agent roster. */
|
||||
configVersion?: number;
|
||||
}
|
||||
|
||||
export function AgentPanel({ configVersion = 0 }: AgentPanelProps) {
|
||||
const { hiddenRosterAgents } = useLozengeFly();
|
||||
const [agents, setAgents] = useState<Record<string, AgentState>>({});
|
||||
const [roster, setRoster] = useState<AgentConfigInfo[]>([]);
|
||||
@@ -90,13 +95,16 @@ export function AgentPanel() {
|
||||
const [editingEditor, setEditingEditor] = useState(false);
|
||||
const cleanupRefs = useRef<Record<string, () => void>>({});
|
||||
|
||||
// Load roster, existing agents, and editor preference on mount
|
||||
// Re-fetch roster whenever configVersion changes (triggered by agent_config_changed WS event).
|
||||
useEffect(() => {
|
||||
agentsApi
|
||||
.getAgentConfig()
|
||||
.then(setRoster)
|
||||
.catch((err) => console.error("Failed to load agent config:", err));
|
||||
}, [configVersion]);
|
||||
|
||||
// Load existing agents and editor preference on mount
|
||||
useEffect(() => {
|
||||
agentsApi
|
||||
.listAgents()
|
||||
.then((agentList) => {
|
||||
|
||||
@@ -69,6 +69,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
{ id: string; storyId: string; status: string; message: string }[]
|
||||
>([]);
|
||||
const reconciliationEventIdRef = useRef(0);
|
||||
const [agentConfigVersion, setAgentConfigVersion] = useState(0);
|
||||
|
||||
const wsRef = useRef<ChatWebSocket | null>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
@@ -215,6 +216,9 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
});
|
||||
}
|
||||
},
|
||||
onAgentConfigChanged: () => {
|
||||
setAgentConfigVersion((v) => v + 1);
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
@@ -829,7 +833,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
}}
|
||||
>
|
||||
<LozengeFlyProvider pipeline={pipeline}>
|
||||
<AgentPanel />
|
||||
<AgentPanel configVersion={agentConfigVersion} />
|
||||
|
||||
<StagePanel title="To Merge" items={pipeline.merge} />
|
||||
<StagePanel title="QA" items={pipeline.qa} />
|
||||
|
||||
Reference in New Issue
Block a user