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

@@ -12,6 +12,7 @@ import type { ChatInputHandle } from "./ChatInput";
import { ChatInput } from "./ChatInput";
import { LozengeFlyProvider } from "./LozengeFlyContext";
import { MessageItem } from "./MessageItem";
import { SideQuestionOverlay } from "./SideQuestionOverlay";
import { StagePanel } from "./StagePanel";
import { WorkItemDetailPanel } from "./WorkItemDetailPanel";
@@ -197,6 +198,11 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
const [queuedMessages, setQueuedMessages] = useState<
{ id: string; text: string }[]
>([]);
const [sideQuestion, setSideQuestion] = useState<{
question: string;
response: string;
loading: boolean;
} | null>(null);
// Ref so stale WebSocket callbacks can read the current queued messages
const queuedMessagesRef = useRef<{ id: string; text: string }[]>([]);
const queueIdCounterRef = useRef(0);
@@ -360,6 +366,16 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
onOnboardingStatus: (onboarding: boolean) => {
setNeedsOnboarding(onboarding);
},
onSideQuestionToken: (content) => {
setSideQuestion((prev) =>
prev ? { ...prev, response: prev.response + content } : prev,
);
},
onSideQuestionDone: (response) => {
setSideQuestion((prev) =>
prev ? { ...prev, response, loading: false } : prev,
);
},
});
return () => {
@@ -459,6 +475,28 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
const sendMessage = async (messageText: string) => {
if (!messageText.trim()) return;
// /btw <question> — answered from context without disrupting main chat
const btwMatch = messageText.match(/^\/btw\s+(.+)/s);
if (btwMatch) {
const question = btwMatch[1].trim();
setSideQuestion({ question, response: "", loading: true });
const isClaudeCode = model === "claude-code-pty";
const provider = isClaudeCode
? "claude-code"
: model.startsWith("claude-")
? "anthropic"
: "ollama";
const config: ProviderConfig = {
provider,
model,
base_url: "http://localhost:11434",
enable_tools: false,
};
wsRef.current?.sendSideQuestion(question, messages, config);
return;
}
// Agent is busy — queue the message instead of dropping it
if (loading) {
const newItem = {
@@ -1154,6 +1192,15 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
</div>
</div>
)}
{sideQuestion && (
<SideQuestionOverlay
question={sideQuestion.question}
response={sideQuestion.response}
loading={sideQuestion.loading}
onDismiss={() => setSideQuestion(null)}
/>
)}
</div>
);
}