From 2dfd08edc9eeef6377267bdeb31f3331401ba848 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:53:27 +0800 Subject: [PATCH] refactor(app): record /btw chats without waiting for the store to load (#53312) --- packages/gui-extensions/src/btw/model.ts | 64 ++++++++---------------- 1 file changed, 20 insertions(+), 44 deletions(-) diff --git a/packages/gui-extensions/src/btw/model.ts b/packages/gui-extensions/src/btw/model.ts index 86fbd722888..dbb1b2df0b7 100644 --- a/packages/gui-extensions/src/btw/model.ts +++ b/packages/gui-extensions/src/btw/model.ts @@ -1,7 +1,7 @@ -import { batch, createRoot, getOwner, onCleanup } from "solid-js" +import { batch, onCleanup } from "solid-js" import { createStore } from "solid-js/store" import { showToast } from "@opencode/ui/toast" -import { createKeyed, type MountedSession, type SetupContext } from "../sdk" +import type { MountedSession, SetupContext } from "../sdk" import type Btw from "./index" const instructions = [ @@ -12,7 +12,6 @@ const instructions = [ /** One-shot side questions per session, one tab each, stored until the tab closes. */ export function createBtw(ctx: SetupContext) { - const owner = getOwner() const controllers = new Map() const [requests, setRequests] = createStore<{ pending: string[] }>({ pending: [] }) const saved = (session: MountedSession) => ctx.stores.chats(session) @@ -30,10 +29,9 @@ export function createBtw(ctx: SetupContext) { const entry = (session: MountedSession, id: string) => saved(session).value?.chats.find((item) => item.id === id) const pending = (id: string) => requests.pending.includes(id) - const generate = (session: MountedSession, id: string) => { - const item = entry(session, id) - - if (!item || pending(id)) return + // Takes the question rather than reading it back, because a new chat's write may still wait for the store to load. + const generate = (session: MountedSession, id: string, question: string) => { + if (pending(id)) return const store = saved(session) const controller = new AbortController() @@ -43,7 +41,7 @@ export function createBtw(ctx: SetupContext) { return ( session.server.client.session .generate( - { sessionID: session.id, prompt: [instructions, item.question].join("\n\n") }, + { sessionID: session.id, prompt: [instructions, question].join("\n\n") }, { signal: controller.signal }, ) .then((result) => { @@ -75,44 +73,18 @@ export function createBtw(ctx: SetupContext) { if (!session?.id) return - const store = saved(session) const id = crypto.randomUUID() - // Desktop storage loads asynchronously. Record the question and open its tab once the store has loaded, in one - // batch, because the host drops a transient tab its panel does not list. Leaving the session first cancels it. - return new Promise((resolve) => { - const controller = new AbortController() - const signal = AbortSignal.any([controller.signal, ctx.signal]) - - createRoot((dispose) => { - const done = () => { - signal.removeEventListener("abort", done) - dispose() - resolve() - } - - signal.addEventListener("abort", done, { once: true }) - createKeyed( - () => ctx.sessions.current()?.key === session.key, - () => onCleanup(() => controller.abort()), - ) - createKeyed( - () => store.ready(), - () => { - if (signal.aborted) return - batch(() => { - store.update((draft) => { - draft.chats.push({ id, question }) - }) - ctx.layout.open(`${ctx.id}:${id}`, session, { tab: "select" }) - }) - // Release the composer once the request starts, not when its answer arrives. - void generate(session, id) - done() - }, - ) - }, owner) + // Desktop storage loads asynchronously. A write before load queues and lands in the same batch that marks the + // store ready, and the panel keeps a tab hidden until then, so the chat and its tab need not wait. The session's + // store lives while its tab stays open, so leaving the session first still records the chat. + batch(() => { + saved(session).update((draft) => { + draft.chats.push({ id, question }) + }) + ctx.layout.open(`${ctx.id}:${id}`, session, { tab: "select" }) }) + void generate(session, id, question) } return { @@ -126,7 +98,11 @@ export function createBtw(ctx: SetupContext) { return !!item && item.answer === undefined && !pending(id) }, - retry: generate, + retry: (session: MountedSession, id: string) => { + const question = entry(session, id)?.question + + if (question) void generate(session, id, question) + }, /** Closing a tab forgets its question and answer. */ remove: (session: MountedSession, id: string) => { stop(id)