import * as React from 'react'; import { useEffect, useRef, useState } from 'react'; import { useInterval } from 'usehooks-ts'; import type { CommandResultDisplay } from '../../commands.js'; import { Markdown } from '../../components/Markdown.js'; import { SpinnerGlyph } from '../../components/Spinner/SpinnerGlyph.js'; import { DOWN_ARROW, UP_ARROW } from '../../constants/figures.js'; import { getSystemPrompt } from '../../constants/prompts.js'; import { useModalOrTerminalSize } from '../../context/modalContext.js'; import { getSystemContext, getUserContext } from '../../context.js'; import { useTerminalSize } from '../../hooks/useTerminalSize.js'; import { type KeyboardEvent, type ScrollBoxHandle, ScrollBox } from '@anthropic/ink'; import { Box, Text } from '@anthropic/ink'; import type { LocalJSXCommandOnDone } from '../../types/command.js'; import type { Message } from '../../types/message.js'; import { createAbortController } from '../../utils/abortController.js'; import { saveGlobalConfig } from '../../utils/config.js'; import { errorMessage } from '../../utils/errors.js'; import { type CacheSafeParams, getLastCacheSafeParams } from '../../utils/forkedAgent.js'; import { getMessagesAfterCompactBoundary } from '../../utils/messages.js'; import type { ProcessUserInputContext } from '../../utils/processUserInput/processUserInput.js'; import { runSideQuestion } from '../../utils/sideQuestion.js'; import { asSystemPrompt } from '../../utils/systemPromptType.js'; type BtwComponentProps = { question: string; context: ProcessUserInputContext; onDone: (result?: string, options?: { display?: CommandResultDisplay }) => void; }; const CHROME_ROWS = 5; const OUTER_CHROME_ROWS = 6; const SCROLL_LINES = 3; function BtwSideQuestion({ question, context, onDone }: BtwComponentProps): React.ReactNode { const [response, setResponse] = useState(null); const [error, setError] = useState(null); const [frame, setFrame] = useState(0); const scrollRef = useRef(null); const { rows } = useModalOrTerminalSize(useTerminalSize()); // Animate spinner while loading useInterval(() => setFrame(f => f + 1), response || error ? null : 80); function handleKeyDown(e: KeyboardEvent): void { if (e.key === 'escape' || e.key === 'return' || e.key === ' ' || (e.ctrl && (e.key === 'c' || e.key === 'd'))) { e.preventDefault(); onDone(undefined, { display: 'skip' }); return; } if (e.key === 'up' || (e.ctrl && e.key === 'p')) { e.preventDefault(); scrollRef.current?.scrollBy(-SCROLL_LINES); } if (e.key === 'down' || (e.ctrl && e.key === 'n')) { e.preventDefault(); scrollRef.current?.scrollBy(SCROLL_LINES); } } useEffect(() => { const abortController = createAbortController(); async function fetchResponse(): Promise { try { const cacheSafeParams = await buildCacheSafeParams(context); const result = await runSideQuestion({ question, cacheSafeParams }); if (!abortController.signal.aborted) { if (result.response) { setResponse(result.response); } else { setError('No response received'); } } } catch (err) { if (!abortController.signal.aborted) { setError(errorMessage(err) || 'Failed to get response'); } } } void fetchResponse(); return () => { abortController.abort(); }; }, [question, context]); const maxContentHeight = Math.max(5, rows - CHROME_ROWS - OUTER_CHROME_ROWS); return ( /btw{' '} {question} {error ? ( {error} ) : response ? ( {response} ) : ( Answering... )} {(response || error) && ( {UP_ARROW}/{DOWN_ARROW} to scroll · Space, Enter, or Escape to dismiss )} ); } /** * Build CacheSafeParams for the side question fork. * * The preferred source is getLastCacheSafeParams — the exact * systemPrompt/userContext/systemContext bytes the main thread sent on its * last request (captured in stopHooks). Reusing them guarantees a byte- * identical prefix and thus a prompt cache hit. We pair these with the * current toolUseContext (for thinkingConfig/tools) and current messages * (for up-to-date context). * * Fallback (first turn before stop hooks fire, or prompt-suggestion * disabled): rebuild from scratch. This may miss the cache if the main loop * applied buildEffectiveSystemPrompt extras (--agent, --system-prompt, * --append-system-prompt, coordinator mode). */ function stripInProgressAssistantMessage(messages: Message[]): Message[] { const last = messages.at(-1); if (last?.type === 'assistant' && last.message!.stop_reason === null) { return messages.slice(0, -1); } return messages; } async function buildCacheSafeParams(context: ProcessUserInputContext): Promise { const forkContextMessages = getMessagesAfterCompactBoundary(stripInProgressAssistantMessage(context.messages)); const saved = getLastCacheSafeParams(); if (saved) { return { systemPrompt: saved.systemPrompt, userContext: saved.userContext, systemContext: saved.systemContext, toolUseContext: context, forkContextMessages, }; } const [rawSystemPrompt, userContext, systemContext] = await Promise.all([ getSystemPrompt(context.options.tools, context.options.mainLoopModel, [], context.options.mcpClients), getUserContext(), getSystemContext(), ]); return { systemPrompt: asSystemPrompt(rawSystemPrompt), userContext, systemContext, toolUseContext: context, forkContextMessages, }; } export async function call( onDone: LocalJSXCommandOnDone, context: ProcessUserInputContext, args: string, ): Promise { const question = args?.trim(); if (!question) { onDone('Usage: /btw ', { display: 'system' }); return null; } saveGlobalConfig(current => ({ ...current, btwUseCount: current.btwUseCount + 1, })); return ; }