story-kit: merge 148_story_interactive_onboarding_guides_user_through_project_setup_after_init

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dave
2026-02-24 15:34:31 +00:00
parent 81ac2f309a
commit 5567cdf480
7 changed files with 476 additions and 4 deletions

View File

@@ -1,5 +1,6 @@
use crate::http::context::AppContext;
use crate::http::workflow::{PipelineState, load_pipeline_state};
use crate::io::onboarding;
use crate::io::watcher::WatcherEvent;
use crate::llm::chat;
use crate::llm::types::Message;
@@ -97,6 +98,11 @@ enum WsResponse {
/// Heartbeat response to a client `Ping`. Lets the client confirm the
/// connection is alive and cancel any stale-connection timeout.
Pong,
/// Sent on connect when the project's spec files still contain scaffold
/// placeholder content and the user needs to go through onboarding.
OnboardingStatus {
needs_onboarding: bool,
},
}
impl From<WatcherEvent> for Option<WsResponse> {
@@ -155,6 +161,19 @@ pub async fn ws_handler(ws: WebSocket, ctx: Data<&Arc<AppContext>>) -> impl poem
let _ = tx.send(state.into());
}
// Push onboarding status so the frontend knows whether to show
// the onboarding welcome flow.
{
let needs = ctx
.state
.get_project_root()
.map(|root| onboarding::check_onboarding_status(&root).needs_onboarding())
.unwrap_or(false);
let _ = tx.send(WsResponse::OnboardingStatus {
needs_onboarding: needs,
});
}
// Subscribe to filesystem watcher events and forward them to the client.
// After each work-item event, also push the updated pipeline state.
// Config-changed events are forwarded as-is without a pipeline refresh.
@@ -564,6 +583,26 @@ mod tests {
assert_eq!(json["type"], "pong");
}
#[test]
fn serialize_onboarding_status_true() {
let resp = WsResponse::OnboardingStatus {
needs_onboarding: true,
};
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["type"], "onboarding_status");
assert_eq!(json["needs_onboarding"], true);
}
#[test]
fn serialize_onboarding_status_false() {
let resp = WsResponse::OnboardingStatus {
needs_onboarding: false,
};
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["type"], "onboarding_status");
assert_eq!(json["needs_onboarding"], false);
}
#[test]
fn serialize_permission_request_response() {
let resp = WsResponse::PermissionRequest {
@@ -878,7 +917,8 @@ mod tests {
>;
/// Helper: connect and return (sink, stream) plus read the initial
/// pipeline_state message that is always sent on connect.
/// pipeline_state and onboarding_status messages that are always sent
/// on connect.
async fn connect_ws(
url: &str,
) -> (
@@ -905,6 +945,22 @@ mod tests {
other => panic!("expected text message, got: {other:?}"),
};
// The second message is the onboarding_status — consume it so
// callers only see application-level messages.
let second = tokio::time::timeout(std::time::Duration::from_secs(2), stream.next())
.await
.expect("timeout waiting for onboarding_status")
.expect("stream ended")
.expect("ws error");
let onboarding: serde_json::Value = match second {
tungstenite::Message::Text(t) => serde_json::from_str(t.as_ref()).unwrap(),
other => panic!("expected text message, got: {other:?}"),
};
assert_eq!(
onboarding["type"], "onboarding_status",
"expected onboarding_status, got: {onboarding}"
);
(sink, stream, initial)
}