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 ScrollBox, { type ScrollBoxHandle, } from '../../ink/components/ScrollBox.js' import type { KeyboardEvent } from '../../ink/events/keyboard-event.js' import { Box, Text } from '../../ink.js' 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 ( ) }