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:
@@ -39,6 +39,7 @@ enum WsRequest {
|
||||
/// - `update` pushes the updated message history.
|
||||
/// - `error` reports a request or processing failure.
|
||||
/// - `work_item_changed` notifies that a `.story_kit/work/` file changed.
|
||||
/// - `agent_config_changed` notifies that `.story_kit/project.toml` was modified.
|
||||
enum WsResponse {
|
||||
Token {
|
||||
content: String,
|
||||
@@ -62,13 +63,16 @@ enum WsResponse {
|
||||
action: String,
|
||||
commit_msg: String,
|
||||
},
|
||||
/// Full pipeline state pushed on connect and after every watcher event.
|
||||
/// Full pipeline state pushed on connect and after every work-item watcher event.
|
||||
PipelineState {
|
||||
upcoming: Vec<crate::http::workflow::UpcomingStory>,
|
||||
current: Vec<crate::http::workflow::UpcomingStory>,
|
||||
qa: Vec<crate::http::workflow::UpcomingStory>,
|
||||
merge: Vec<crate::http::workflow::UpcomingStory>,
|
||||
},
|
||||
/// `.story_kit/project.toml` was modified; the frontend should re-fetch the
|
||||
/// agent roster. Does NOT trigger a pipeline state refresh.
|
||||
AgentConfigChanged,
|
||||
/// Claude Code is requesting user approval before executing a tool.
|
||||
PermissionRequest {
|
||||
request_id: String,
|
||||
@@ -89,13 +93,21 @@ enum WsResponse {
|
||||
},
|
||||
}
|
||||
|
||||
impl From<WatcherEvent> for WsResponse {
|
||||
impl From<WatcherEvent> for Option<WsResponse> {
|
||||
fn from(e: WatcherEvent) -> Self {
|
||||
WsResponse::WorkItemChanged {
|
||||
stage: e.stage,
|
||||
item_id: e.item_id,
|
||||
action: e.action,
|
||||
commit_msg: e.commit_msg,
|
||||
match e {
|
||||
WatcherEvent::WorkItem {
|
||||
stage,
|
||||
item_id,
|
||||
action,
|
||||
commit_msg,
|
||||
} => Some(WsResponse::WorkItemChanged {
|
||||
stage,
|
||||
item_id,
|
||||
action,
|
||||
commit_msg,
|
||||
}),
|
||||
WatcherEvent::ConfigChanged => Some(WsResponse::AgentConfigChanged),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,7 +150,8 @@ pub async fn ws_handler(ws: WebSocket, ctx: Data<&Arc<AppContext>>) -> impl poem
|
||||
}
|
||||
|
||||
// Subscribe to filesystem watcher events and forward them to the client.
|
||||
// After each watcher event, also push the updated pipeline state.
|
||||
// After each work-item event, also push the updated pipeline state.
|
||||
// Config-changed events are forwarded as-is without a pipeline refresh.
|
||||
let tx_watcher = tx.clone();
|
||||
let ctx_watcher = ctx.clone();
|
||||
let mut watcher_rx = ctx.watcher_tx.subscribe();
|
||||
@@ -146,11 +159,16 @@ pub async fn ws_handler(ws: WebSocket, ctx: Data<&Arc<AppContext>>) -> impl poem
|
||||
loop {
|
||||
match watcher_rx.recv().await {
|
||||
Ok(evt) => {
|
||||
if tx_watcher.send(evt.into()).is_err() {
|
||||
let is_work_item =
|
||||
matches!(evt, crate::io::watcher::WatcherEvent::WorkItem { .. });
|
||||
let ws_msg: Option<WsResponse> = evt.into();
|
||||
if let Some(msg) = ws_msg && tx_watcher.send(msg).is_err() {
|
||||
break;
|
||||
}
|
||||
// Push refreshed pipeline state after the change.
|
||||
if let Ok(state) = load_pipeline_state(ctx_watcher.as_ref())
|
||||
// Only push refreshed pipeline state after work-item changes,
|
||||
// not after config-file changes.
|
||||
if is_work_item
|
||||
&& let Ok(state) = load_pipeline_state(ctx_watcher.as_ref())
|
||||
&& tx_watcher.send(state.into()).is_err()
|
||||
{
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user