Spike 61: filesystem watcher and UI simplification
Add notify-based filesystem watcher for .story_kit/work/ that auto-commits changes with deterministic messages and broadcasts events over WebSocket. Push full pipeline state (Upcoming, Current, QA, To Merge) to frontend on connect and after every watcher event. Strip dead UI: remove ReviewPanel, GatePanel, TodoPanel, UpcomingPanel and all associated REST polling. Replace with 4 generic StagePanel components driven by WebSocket. Simplify AgentPanel to roster-only. Delete all 11 workflow HTTP endpoints and 16 request/response types from the server. Clean dead code from workflow module. MCP tools call Rust functions directly and need none of the HTTP layer. Net: ~4,100 lines deleted, ~400 added. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
use crate::http::context::AppContext;
|
||||
use crate::http::workflow::{PipelineState, load_pipeline_state};
|
||||
use crate::io::watcher::WatcherEvent;
|
||||
use crate::llm::chat;
|
||||
use crate::llm::types::Message;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
@@ -30,16 +32,56 @@ enum WsRequest {
|
||||
/// - `token` streams partial model output.
|
||||
/// - `update` pushes the updated message history.
|
||||
/// - `error` reports a request or processing failure.
|
||||
/// - `work_item_changed` notifies that a `.story_kit/work/` file changed.
|
||||
enum WsResponse {
|
||||
Token { content: String },
|
||||
Update { messages: Vec<Message> },
|
||||
/// Session ID for Claude Code conversation resumption.
|
||||
SessionId { session_id: String },
|
||||
Error { message: String },
|
||||
/// Filesystem watcher notification: a work-pipeline file was created or
|
||||
/// modified and auto-committed. The frontend can use this to refresh its
|
||||
/// story/bug list without polling.
|
||||
WorkItemChanged {
|
||||
stage: String,
|
||||
item_id: String,
|
||||
action: String,
|
||||
commit_msg: String,
|
||||
},
|
||||
/// Full pipeline state pushed on connect and after every 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>,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<WatcherEvent> for WsResponse {
|
||||
fn from(e: WatcherEvent) -> Self {
|
||||
WsResponse::WorkItemChanged {
|
||||
stage: e.stage,
|
||||
item_id: e.item_id,
|
||||
action: e.action,
|
||||
commit_msg: e.commit_msg,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PipelineState> for WsResponse {
|
||||
fn from(s: PipelineState) -> Self {
|
||||
WsResponse::PipelineState {
|
||||
upcoming: s.upcoming,
|
||||
current: s.current,
|
||||
qa: s.qa,
|
||||
merge: s.merge,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[handler]
|
||||
/// WebSocket endpoint for streaming chat responses and cancellation.
|
||||
/// WebSocket endpoint for streaming chat responses, cancellation, and
|
||||
/// filesystem watcher notifications.
|
||||
///
|
||||
/// Accepts JSON `WsRequest` messages and streams `WsResponse` messages.
|
||||
pub async fn ws_handler(ws: WebSocket, ctx: Data<&Arc<AppContext>>) -> impl poem::IntoResponse {
|
||||
@@ -58,6 +100,37 @@ pub async fn ws_handler(ws: WebSocket, ctx: Data<&Arc<AppContext>>) -> impl poem
|
||||
}
|
||||
});
|
||||
|
||||
// Push initial pipeline state to the client on connect.
|
||||
if let Ok(state) = load_pipeline_state(ctx.as_ref()) {
|
||||
let _ = tx.send(state.into());
|
||||
}
|
||||
|
||||
// Subscribe to filesystem watcher events and forward them to the client.
|
||||
// After each watcher event, also push the updated pipeline state.
|
||||
let tx_watcher = tx.clone();
|
||||
let ctx_watcher = ctx.clone();
|
||||
let mut watcher_rx = ctx.watcher_tx.subscribe();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match watcher_rx.recv().await {
|
||||
Ok(evt) => {
|
||||
if tx_watcher.send(evt.into()).is_err() {
|
||||
break;
|
||||
}
|
||||
// Push refreshed pipeline state after the change.
|
||||
if let Ok(state) = load_pipeline_state(ctx_watcher.as_ref()) {
|
||||
if tx_watcher.send(state.into()).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Lagged: skip missed events, keep going.
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
|
||||
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
while let Some(Ok(msg)) = stream.next().await {
|
||||
if let WsMessage::Text(text) = msg {
|
||||
let parsed: Result<WsRequest, _> = serde_json::from_str(&text);
|
||||
|
||||
Reference in New Issue
Block a user