Skip to content

Commit 082aa3e

Browse files
saravmajesticclaude
andcommitted
fix: [AI-7520] first-run gate keys off sync.status; scan-gate fallback; port cleanup; marker
Round-3 review (rizvi): - BLOCKING (app.tsx): the first-run onboarding gate keyed off `ready()` (plugin-host startup only), which can settle before sync loads providers — transiently making a returning, connected user look un-onboarded and re-showing the welcome picker + scan gate. Now also require `sync.status === "complete"` (the provider-load signal) before deciding, so a returning user never sees the first-run flow. - scan gate (app.tsx): if the prompt ref isn't mounted, `onChoose` no longer silently drops the Yes/No — it surfaces a toast telling the user how to continue. - port cleanup (altimate.ts): the loopback callback server is now stopped on the pending-flow TIMEOUT path too (not only in callback().finally), so a dismissed sign-in can't leave port 7317 bound for the process lifetime. - Marker Guard: wrap the fork-added `writeJsonAtomic` in `util/filesystem.ts` (introduced by the merged #1046 into an upstream-shared file) in altimate_change markers so the guard passes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 6f42289 commit 082aa3e

3 files changed

Lines changed: 23 additions & 2 deletions

File tree

packages/opencode/src/altimate/plugin/altimate.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,10 @@ function registerPending(state: string, timeoutMs = 5 * 60 * 1000): Promise<Call
172172
return new Promise<CallbackResult>((resolve, reject) => {
173173
const timeout = setTimeout(() => {
174174
if (pending.delete(state)) reject(new Error("Timed out waiting for browser sign-in"))
175+
// If the dialog was dismissed and callback() never ran its finally, the
176+
// loopback server would otherwise stay bound past the timeout. Free the
177+
// port once nothing is waiting on it.
178+
if (pending.size === 0) stopCallbackServer()
175179
}, timeoutMs)
176180
pending.set(state, {
177181
resolve: (creds) => {

packages/opencode/src/util/filesystem.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ export namespace Filesystem {
272272
}
273273
}
274274

275+
// altimate_change start — fork util (AI-7520 onboarding/materialize): atomic JSON write
275276
/**
276277
* Atomic JSON write via tmp-file + rename. Serializes `value` with 2-space
277278
* indent + trailing newline, writes to `<targetPath>.tmp-<random>`, then
@@ -292,6 +293,7 @@ export namespace Filesystem {
292293
writeFileSync(tmpPath, JSON.stringify(value, null, 2) + "\n")
293294
renameSync(tmpPath, targetPath)
294295
}
296+
// altimate_change end
295297

296298
export async function findUp(target: string, start: string, stop?: string) {
297299
let current = start

packages/tui/src/app.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,13 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
562562
let armScanGate = false
563563
createEffect(() => {
564564
if (firstRunPickerHandled) return
565-
if (!ready()) return // wait until providers are loaded before deciding
565+
// Decide only once BOTH the plugin host has started AND sync has finished
566+
// loading providers. `ready()` alone is plugin-host startup, which can settle
567+
// before sync populates `sync.data.provider` — deciding then would transiently
568+
// see a returning (connected) user as un-onboarded and re-show the picker +
569+
// scan gate (the AI-7774 regression). `sync.status` is the provider-load signal
570+
// (same one used for continue/fork above).
571+
if (!ready() || sync.status !== "complete") return
566572
firstRunPickerHandled = true
567573
if (onboardingReady()) return // already set up — no gate
568574
armScanGate = true
@@ -592,7 +598,16 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
592598
// menu (sample dbt, downstream impact, SQL PR, or free chat).
593599
// Template lives at packages/opencode/src/command/template/onboard-connect.txt.
594600
const ref = promptRef.current
595-
if (!ref) return
601+
if (!ref) {
602+
// The prompt should be mounted by the time the gate resolves, but
603+
// if it isn't, don't silently drop the user's choice — tell them
604+
// how to continue instead of a dead keypress.
605+
toast.show({
606+
message: `Run /onboard-connect ${arg} to continue.`,
607+
variant: "error",
608+
})
609+
return
610+
}
596611
ref.set({ input: `/onboard-connect ${arg}`, parts: [] })
597612
ref.submit()
598613
}}

0 commit comments

Comments
 (0)