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

@@ -409,6 +409,83 @@ where
})
}
/// Answer a one-off side question using the existing conversation as context.
///
/// Unlike `chat`, this function:
/// - Does NOT perform tool calls.
/// - Does NOT modify the main conversation history.
/// - Does NOT touch the shared cancel signal.
/// - Performs a single LLM call and returns the response text.
pub async fn side_question<U>(
context_messages: Vec<Message>,
question: String,
config: ProviderConfig,
store: &dyn StoreOps,
mut on_token: U,
) -> Result<String, String>
where
U: FnMut(&str) + Send,
{
use crate::llm::providers::anthropic::AnthropicProvider;
use crate::llm::providers::ollama::OllamaProvider;
// Use a local cancel channel that is never cancelled, so the side question
// runs to completion independently of any main chat cancel signal.
// Keep `_cancel_tx` alive for the duration of the function so the channel
// stays open and `changed()` inside the providers does not spuriously fire.
let (_cancel_tx, cancel_rx) = tokio::sync::watch::channel(false);
let mut cancel_rx = cancel_rx;
cancel_rx.borrow_and_update();
let base_url = config
.base_url
.clone()
.unwrap_or_else(|| "http://localhost:11434".to_string());
let is_claude_code = config.provider == "claude-code";
let is_claude = !is_claude_code && config.model.starts_with("claude-");
// Build a minimal history: existing context + the side question.
let mut history = context_messages;
history.push(Message {
role: Role::User,
content: question,
tool_calls: None,
tool_call_id: None,
});
// No tools for side questions.
let tools: &[ToolDefinition] = &[];
let response = if is_claude {
let api_key = get_anthropic_api_key_impl(store)?;
let provider = AnthropicProvider::new(api_key);
provider
.chat_stream(
&config.model,
&history,
tools,
&mut cancel_rx,
|token| on_token(token),
|_tool_name| {},
)
.await
.map_err(|e| format!("Anthropic Error: {e}"))?
} else if is_claude_code {
return Err("Claude Code provider does not support side questions".to_string());
} else {
let provider = OllamaProvider::new(base_url);
provider
.chat_stream(&config.model, &history, tools, &mut cancel_rx, |token| {
on_token(token)
})
.await
.map_err(|e| format!("Ollama Error: {e}"))?
};
Ok(response.content.unwrap_or_default())
}
async fn execute_tool(call: &ToolCall, state: &SessionState) -> String {
use crate::io::{fs, search, shell};