diff --git a/packages/opencode/src/cli/cmd/run/runtime.boot.ts b/packages/opencode/src/cli/cmd/run/runtime.boot.ts index 280664f91f..ba1f6f0e4d 100644 --- a/packages/opencode/src/cli/cmd/run/runtime.boot.ts +++ b/packages/opencode/src/cli/cmd/run/runtime.boot.ts @@ -6,6 +6,7 @@ // history ring. All are async because they read config or hit the SDK, but // none block each other. import { TuiConfig } from "../tui/config/tui" +import { reusePendingTask } from "./runtime.shared" import { resolveSession, sessionHistory } from "./session.shared" import type { FooterKeybinds, RunDiffStyle, RunInput, RunPrompt } from "./types" import { pickVariant } from "./variant.shared" @@ -20,28 +21,10 @@ const DEFAULT_KEYBINDS: FooterKeybinds = { inputNewline: "shift+return,ctrl+return,alt+return,ctrl+j", } -let configTask: Promise>> | undefined +const configTask: { current?: ReturnType } = {} function loadConfig() { - if (configTask) { - return configTask - } - - const task = TuiConfig.get() - configTask = task - task.then( - () => { - if (configTask === task) { - configTask = undefined - } - }, - () => { - if (configTask === task) { - configTask = undefined - } - }, - ) - return task + return reusePendingTask(configTask, () => TuiConfig.get()) } export type ModelInfo = { diff --git a/packages/opencode/src/cli/cmd/run/runtime.shared.ts b/packages/opencode/src/cli/cmd/run/runtime.shared.ts new file mode 100644 index 0000000000..7f8f28e5e9 --- /dev/null +++ b/packages/opencode/src/cli/cmd/run/runtime.shared.ts @@ -0,0 +1,17 @@ +type PendingTask = { + current?: Promise +} + +export function reusePendingTask(slot: PendingTask, run: () => Promise) { + if (slot.current) { + return slot.current + } + + const task = run().finally(() => { + if (slot.current === task) { + slot.current = undefined + } + }) + slot.current = task + return task +} diff --git a/packages/opencode/src/cli/cmd/run/runtime.ts b/packages/opencode/src/cli/cmd/run/runtime.ts index 6adcce464a..02eff119fc 100644 --- a/packages/opencode/src/cli/cmd/run/runtime.ts +++ b/packages/opencode/src/cli/cmd/run/runtime.ts @@ -17,6 +17,7 @@ import { Flag } from "@/flag/flag" import { createRunDemo } from "./demo" import { resolveDiffStyle, resolveFooterKeybinds, resolveModelInfo, resolveSessionInfo } from "./runtime.boot" import { createRuntimeLifecycle } from "./runtime.lifecycle" +import { reusePendingTask } from "./runtime.shared" import { trace } from "./trace" import { cycleVariant, formatModelLabel, resolveSavedVariant, resolveVariant, saveVariant } from "./variant.shared" import type { RunInput } from "./types" @@ -61,6 +62,11 @@ type RunLocalInput = { demoText?: RunInput["demoText"] } +type StreamState = { + mod: Awaited + handle: Awaited["createSessionTransport"]>> +} + // Core runtime loop. Boot resolves the SDK context, then we set up the // lifecycle (renderer + footer), wire the stream transport for SDK events, // and feed prompts through the queue until the user exits. @@ -291,28 +297,14 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise { }) const streamTask = import("./stream.transport") - let stream: - | { - mod: Awaited - handle: Awaited["createSessionTransport"]>> - } - | undefined - let loading: - | Promise<{ - mod: Awaited - handle: Awaited["createSessionTransport"]>> - }> - | undefined + let stream: StreamState | undefined + const loading: { current?: Promise } = {} const ensureStream = () => { if (stream) { return Promise.resolve(stream) } - if (loading) { - return loading - } - - const task = (async () => { + return reusePendingTask(loading, async () => { await ensureSession() if (footer.isClosed) { throw new Error("runtime closed") @@ -340,22 +332,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise { const next = { mod, handle } stream = next return next - })() - - loading = task - task.then( - () => { - if (loading === task) { - loading = undefined - } - }, - () => { - if (loading === task) { - loading = undefined - } - }, - ) - return task + }) } try {