import type { AssistantMessage } from "@oh-my-pi/pi-ai"; import { prompt } from "@oh-my-pi/pi-utils"; import btwUserPrompt from "../../prompts/system/btw-user.md" with { type: "text" }; import { BtwPanelComponent } from "../components/btw-panel"; import type { InteractiveModeContext } from "../types"; interface BtwRequest { component: BtwPanelComponent; abortController: AbortController; question: string; } export class BtwController { #activeRequest: BtwRequest | undefined; #lastQuestion: string | undefined; #lastReplyText: string | undefined; #lastAssistantMessage: AssistantMessage | undefined; #branchInFlight = false; constructor(private readonly ctx: InteractiveModeContext) {} hasActiveRequest(): boolean { return this.#activeRequest !== undefined; } canBranch(): boolean { return ( !this.#branchInFlight && this.#activeRequest?.component.isBranchable() === true && this.#lastQuestion !== undefined && this.#lastReplyText !== undefined && this.#lastAssistantMessage !== undefined ); } async handleBranch(): Promise { if (!this.canBranch() || !this.#lastQuestion || !this.#lastAssistantMessage) return false; this.#branchInFlight = true; try { await this.ctx.handleBtwBranch(this.#lastQuestion, this.#lastAssistantMessage); return true; } finally { this.#branchInFlight = false; } } handleEscape(): boolean { if (!this.#activeRequest) return false; this.#closeActiveRequest({ abort: this.#activeRequest.abortController.signal.aborted === false }); return true; } dispose(): void { this.#closeActiveRequest({ abort: true }); } async start(question: string): Promise { const trimmedQuestion = question.trim(); if (!trimmedQuestion) { this.ctx.showStatus("Usage: /btw "); return; } const model = this.ctx.session.model; if (!model) { this.ctx.showError("No active model available for /btw."); return; } this.#closeActiveRequest({ abort: true }); const request: BtwRequest = { component: new BtwPanelComponent({ question: trimmedQuestion, tui: this.ctx.ui }), abortController: new AbortController(), question: trimmedQuestion, }; this.ctx.btwContainer.clear(); this.ctx.btwContainer.addChild(request.component); this.ctx.ui.requestRender(); this.#activeRequest = request; void this.#runRequest(request); } async #runRequest(request: BtwRequest): Promise { try { const promptText = prompt.render(btwUserPrompt, { question: request.question }); const { replyText, assistantMessage } = await this.ctx.session.runEphemeralTurn({ promptText, onTextDelta: delta => { if (this.#isActiveRequest(request)) { request.component.appendText(delta); } }, signal: request.abortController.signal, }); if (!this.#isActiveRequest(request)) { return; } if (replyText) { request.component.setAnswer(replyText); } request.component.markComplete(); if (request.component.isBranchable()) { this.#lastQuestion = request.question; this.#lastReplyText = replyText; this.#lastAssistantMessage = assistantMessage; } else { this.#clearBranchState(); } } catch (error) { if (!this.#isActiveRequest(request)) { return; } if (request.abortController.signal.aborted) { request.component.markAborted(); return; } request.component.markError(error instanceof Error ? error.message : String(error)); } } #closeActiveRequest(options: { abort: boolean }): void { const request = this.#activeRequest; if (!request) return; this.#activeRequest = undefined; this.#clearBranchState(); if (options.abort) { request.abortController.abort(); } request.component.close(); this.ctx.btwContainer.clear(); this.ctx.ui.requestRender(); } #clearBranchState(): void { this.#lastQuestion = undefined; this.#lastReplyText = undefined; this.#lastAssistantMessage = undefined; } #isActiveRequest(request: BtwRequest): boolean { return this.#activeRequest === request; } }