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:
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user