story-kit: done 240_story_btw_side_question_slash_command

Implement /btw side question slash command — lets users ask quick
questions from conversation context without disrupting the main chat.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dave
2026-03-14 18:09:30 +00:00
parent 6a7baa4a15
commit 3a430dfaa2
5 changed files with 394 additions and 2 deletions

View File

@@ -35,6 +35,14 @@ enum WsRequest {
/// Heartbeat ping from the client. The server responds with `Pong` so the
/// client can detect stale (half-closed) connections.
Ping,
/// A quick side question answered from current conversation context.
/// The question and response are NOT added to the conversation history
/// and no tool calls are made.
SideQuestion {
question: String,
context_messages: Vec<Message>,
config: chat::ProviderConfig,
},
}
#[derive(Serialize)]
@@ -116,6 +124,14 @@ enum WsResponse {
OnboardingStatus {
needs_onboarding: bool,
},
/// Streaming token from a `/btw` side question response.
SideQuestionToken {
content: String,
},
/// Final signal that the `/btw` side question has been fully answered.
SideQuestionDone {
response: String,
},
}
impl From<WatcherEvent> for Option<WsResponse> {
@@ -344,6 +360,33 @@ pub async fn ws_handler(ws: WebSocket, ctx: Data<&Arc<AppContext>>) -> impl poem
Ok(WsRequest::Ping) => {
let _ = tx.send(WsResponse::Pong);
}
Ok(WsRequest::SideQuestion { question, context_messages, config }) => {
let tx_side = tx.clone();
let store = ctx.store.clone();
tokio::spawn(async move {
let result = chat::side_question(
context_messages,
question,
config,
store.as_ref(),
|token| {
let _ = tx_side.send(WsResponse::SideQuestionToken {
content: token.to_string(),
});
},
).await;
match result {
Ok(response) => {
let _ = tx_side.send(WsResponse::SideQuestionDone { response });
}
Err(err) => {
let _ = tx_side.send(WsResponse::SideQuestionDone {
response: format!("Error: {err}"),
});
}
}
});
}
_ => {}
}
}
@@ -370,6 +413,39 @@ pub async fn ws_handler(ws: WebSocket, ctx: Data<&Arc<AppContext>>) -> impl poem
Ok(WsRequest::PermissionResponse { .. }) => {
// Permission responses outside an active chat are ignored.
}
Ok(WsRequest::SideQuestion {
question,
context_messages,
config,
}) => {
let tx_side = tx.clone();
let store = ctx.store.clone();
tokio::spawn(async move {
let result = chat::side_question(
context_messages,
question,
config,
store.as_ref(),
|token| {
let _ = tx_side.send(WsResponse::SideQuestionToken {
content: token.to_string(),
});
},
)
.await;
match result {
Ok(response) => {
let _ = tx_side
.send(WsResponse::SideQuestionDone { response });
}
Err(err) => {
let _ = tx_side.send(WsResponse::SideQuestionDone {
response: format!("Error: {err}"),
});
}
}
});
}
Err(err) => {
let _ = tx.send(WsResponse::Error {
message: format!("Invalid request: {err}"),