From 415119a3dc4f8c55124bd0c42df60a3a16be273b Mon Sep 17 00:00:00 2001 From: badcuban <108198679+badcuban@users.noreply.github.com> Date: Fri, 7 Aug 2026 12:46:18 -0400 Subject: [PATCH] Hold the add-project intent until an environment exists Opening add-project is a one-shot intent, and it was consumed the moment the palette mounted, against whatever environment data had hydrated by then. A click during startup landed on an empty list: the flow dead-ended with guidance meant for genuinely environment-less sessions, and the arriving environments a moment later could not revive it. This is also why the palette browser suites failed most local runs. The intent now waits for the options to exist (the effect re-runs when they arrive; closing the palette still cancels), while hosted-static tabs keep their immediate pairing guidance. Also teaches the suite's waitFor helpers to report what the palette actually rendered at timeout, so the next residual failure diagnoses itself. --- apps/web/src/components/ChatView.browser.tsx | 16 ++++++++++++---- apps/web/src/components/CommandPalette.tsx | 12 +++++++++++- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/ChatView.browser.tsx b/apps/web/src/components/ChatView.browser.tsx index 14e5d409..d364df0c 100644 --- a/apps/web/src/components/ChatView.browser.tsx +++ b/apps/web/src/components/ChatView.browser.tsx @@ -1461,13 +1461,16 @@ async function waitForProductionStyles(): Promise { async function waitForElement( query: () => T | null, - errorMessage: string, + errorMessage: string | (() => string), ): Promise { + // Lazy messages read the DOM at failure time, so a timeout reports what + // actually rendered instead of only what was expected. + const resolveMessage = () => (typeof errorMessage === "string" ? errorMessage : errorMessage()); let element: T | null = null; await vi.waitFor( () => { element = query(); - expect(element, errorMessage).toBeTruthy(); + expect(element, resolveMessage()).toBeTruthy(); }, { timeout: 8_000, @@ -1475,7 +1478,7 @@ async function waitForElement( }, ); if (!element) { - throw new Error(errorMessage); + throw new Error(resolveMessage()); } return element; } @@ -1928,7 +1931,12 @@ async function waitForCommandPaletteShortcutLabel(): Promise { async function waitForCommandPaletteInput(placeholder: string): Promise { return waitForElement( () => document.querySelector(`input[placeholder="${placeholder}"]`) as HTMLInputElement | null, - `Command palette input with placeholder "${placeholder}" did not render.`, + () => { + const palette = document.querySelector('[data-testid="command-palette"]'); + const actual = palette?.querySelector("input")?.getAttribute("placeholder") ?? ""; + const paletteState = palette ? "open" : "closed"; + return `Command palette input with placeholder "${placeholder}" did not render (palette ${paletteState}, actual placeholder: "${actual}").`; + }, ); } diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index d60fbacf..5fefbbc9 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -1312,9 +1312,19 @@ function OpenCommandPaletteDialog() { if (openIntent?.kind !== "add-project") { return; } + // Environments hydrate a beat after the palette can open, and this + // intent is one-shot: consuming it against an empty environment list + // turned an add-project click during startup into a dead end. Hold the + // intent until an environment exists (closing the palette clears it); + // this effect re-runs when the options arrive. Hosted-static tabs know + // immediately that no local environment is coming, so they proceed + // straight to the pairing guidance. + if (addProjectEnvironmentOptions.length === 0 && !isHostedStaticApp()) { + return; + } clearOpenIntent(); openAddProjectFlow(); - }, [clearOpenIntent, openAddProjectFlow, openIntent]); + }, [addProjectEnvironmentOptions.length, clearOpenIntent, openAddProjectFlow, openIntent]); // Sidebar "Search all N threads" hands off here: a submenu view scoped to // one logical project's threads, searchable with the usual palette ranking.