From 84432d9733cc2e05083f0399dd4223e09ede4b2a Mon Sep 17 00:00:00 2001 From: ZeroPointSix Date: Sat, 6 Jun 2026 08:33:42 +0800 Subject: [PATCH 1/7] fix: defer launcher ready until initial data settles --- prompt-launcher/src/lib/tauriClient.ts | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/prompt-launcher/src/lib/tauriClient.ts b/prompt-launcher/src/lib/tauriClient.ts index b976946..f5e1068 100644 --- a/prompt-launcher/src/lib/tauriClient.ts +++ b/prompt-launcher/src/lib/tauriClient.ts @@ -1,11 +1,25 @@ import { invoke } from "@tauri-apps/api/core"; +import { createLauncherReadyGate } from "./launcherReadyGate.js"; import type { AppConfig, PromptEntry, RecentState } from "./types"; +const launcherReadyGate = createLauncherReadyGate( + () => invoke("frontend_ready"), + (callback) => setTimeout(callback, 0), + (error) => console.warn("[frontend_ready] Failed to notify backend", error) +); + export const tauriClient = { getConfig: () => invoke("get_config"), listPrompts: () => invoke("list_prompts"), - searchPrompts: (query: string, limit: number, favoritesOnly: boolean) => - invoke("search_prompts", { query, limit, favoritesOnly }), + searchPrompts: async (query: string, limit: number, favoritesOnly: boolean) => { + const prompts = await invoke("search_prompts", { + query, + limit, + favoritesOnly + }); + launcherReadyGate.scheduleAfterInitialData(); + return prompts; + }, setPromptsDir: (path: string) => invoke("set_prompts_dir", { path }), createPromptFile: (name: string) => @@ -38,5 +52,7 @@ export const tauriClient = { captureActiveWindow: () => invoke("capture_active_window"), focusLastWindow: (autoPaste: boolean) => invoke("focus_last_window", { autoPaste }), - frontendReady: () => invoke("frontend_ready") -}; + frontendReady: async () => { + launcherReadyGate.markMounted(); + } +}; \ No newline at end of file From efa21ac48e569f96997c3849196156a8d727fecf Mon Sep 17 00:00:00 2001 From: ZeroPointSix Date: Sat, 6 Jun 2026 08:33:50 +0800 Subject: [PATCH 2/7] test: add launcher ready gate --- prompt-launcher/src/lib/launcherReadyGate.js | 41 ++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 prompt-launcher/src/lib/launcherReadyGate.js diff --git a/prompt-launcher/src/lib/launcherReadyGate.js b/prompt-launcher/src/lib/launcherReadyGate.js new file mode 100644 index 0000000..2a62587 --- /dev/null +++ b/prompt-launcher/src/lib/launcherReadyGate.js @@ -0,0 +1,41 @@ +/** + * Creates a small gate that keeps the backend window hidden until the launcher + * has mounted and the initial prompt search has had a chance to update UI state. + * + * @param {() => Promise} notifyBackendReady + * @param {(callback: () => void) => void} [schedule] + * @param {(error: unknown) => void} [onError] + */ +export function createLauncherReadyGate( + notifyBackendReady, + schedule = (callback) => setTimeout(callback, 0), + onError = () => {} +) { + let mounted = false; + let scheduled = false; + let notified = false; + + return { + markMounted() { + mounted = true; + }, + + scheduleAfterInitialData() { + if (!mounted || scheduled || notified) { + return; + } + + scheduled = true; + schedule(() => { + void notifyBackendReady() + .then(() => { + notified = true; + }) + .catch((error) => { + scheduled = false; + onError(error); + }); + }); + } + }; +} From 3d9ecfb46b2eb00a95e96fb9f86ffb47544db65d Mon Sep 17 00:00:00 2001 From: ZeroPointSix Date: Sat, 6 Jun 2026 08:34:02 +0800 Subject: [PATCH 3/7] test: cover launcher ready notification gate --- .../src/lib/launcherReadyGate.test.js | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 prompt-launcher/src/lib/launcherReadyGate.test.js diff --git a/prompt-launcher/src/lib/launcherReadyGate.test.js b/prompt-launcher/src/lib/launcherReadyGate.test.js new file mode 100644 index 0000000..e4b1a6b --- /dev/null +++ b/prompt-launcher/src/lib/launcherReadyGate.test.js @@ -0,0 +1,80 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createLauncherReadyGate } from "./launcherReadyGate.js"; + +const flushMicrotasks = () => Promise.resolve(); + +test("does not notify the backend before the frontend is mounted", async () => { + const calls = []; + const scheduled = []; + const gate = createLauncherReadyGate( + async () => calls.push("frontend_ready"), + (callback) => scheduled.push(callback) + ); + + gate.scheduleAfterInitialData(); + assert.equal(scheduled.length, 0); + assert.deepEqual(calls, []); + + gate.markMounted(); + gate.scheduleAfterInitialData(); + assert.equal(scheduled.length, 1); + + scheduled[0](); + await flushMicrotasks(); + assert.deepEqual(calls, ["frontend_ready"]); +}); + +test("coalesces repeated ready requests into one backend notification", async () => { + const calls = []; + const scheduled = []; + const gate = createLauncherReadyGate( + async () => calls.push("frontend_ready"), + (callback) => scheduled.push(callback) + ); + + gate.markMounted(); + gate.scheduleAfterInitialData(); + gate.scheduleAfterInitialData(); + gate.scheduleAfterInitialData(); + + assert.equal(scheduled.length, 1); + scheduled[0](); + await flushMicrotasks(); + + gate.scheduleAfterInitialData(); + assert.equal(scheduled.length, 1); + assert.deepEqual(calls, ["frontend_ready"]); +}); + +test("allows retry when the backend notification fails", async () => { + const errors = []; + const scheduled = []; + let attempts = 0; + const gate = createLauncherReadyGate( + async () => { + attempts += 1; + if (attempts === 1) { + throw new Error("temporary failure"); + } + }, + (callback) => scheduled.push(callback), + (error) => errors.push(error) + ); + + gate.markMounted(); + gate.scheduleAfterInitialData(); + scheduled[0](); + await flushMicrotasks(); + + assert.equal(attempts, 1); + assert.equal(errors.length, 1); + + gate.scheduleAfterInitialData(); + assert.equal(scheduled.length, 2); + scheduled[1](); + await flushMicrotasks(); + + assert.equal(attempts, 2); + assert.equal(errors.length, 1); +}); From 1b6500c8d500890dc98f34e304df284bf5b17188 Mon Sep 17 00:00:00 2001 From: ZeroPointSix Date: Sat, 6 Jun 2026 08:35:53 +0800 Subject: [PATCH 4/7] test: type launcher ready gate tests --- prompt-launcher/src/lib/launcherReadyGate.test.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/prompt-launcher/src/lib/launcherReadyGate.test.js b/prompt-launcher/src/lib/launcherReadyGate.test.js index e4b1a6b..2951a6f 100644 --- a/prompt-launcher/src/lib/launcherReadyGate.test.js +++ b/prompt-launcher/src/lib/launcherReadyGate.test.js @@ -5,7 +5,9 @@ import { createLauncherReadyGate } from "./launcherReadyGate.js"; const flushMicrotasks = () => Promise.resolve(); test("does not notify the backend before the frontend is mounted", async () => { + /** @type {string[]} */ const calls = []; + /** @type {Array<() => void>} */ const scheduled = []; const gate = createLauncherReadyGate( async () => calls.push("frontend_ready"), @@ -26,7 +28,9 @@ test("does not notify the backend before the frontend is mounted", async () => { }); test("coalesces repeated ready requests into one backend notification", async () => { + /** @type {string[]} */ const calls = []; + /** @type {Array<() => void>} */ const scheduled = []; const gate = createLauncherReadyGate( async () => calls.push("frontend_ready"), @@ -48,7 +52,9 @@ test("coalesces repeated ready requests into one backend notification", async () }); test("allows retry when the backend notification fails", async () => { + /** @type {unknown[]} */ const errors = []; + /** @type {Array<() => void>} */ const scheduled = []; let attempts = 0; const gate = createLauncherReadyGate( From 32ab1975c3400228f06ca3b34c515ad1830e50c3 Mon Sep 17 00:00:00 2001 From: ZeroPointSix Date: Sat, 6 Jun 2026 08:37:37 +0800 Subject: [PATCH 5/7] test: wait for ready gate async handlers --- prompt-launcher/src/lib/launcherReadyGate.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/prompt-launcher/src/lib/launcherReadyGate.test.js b/prompt-launcher/src/lib/launcherReadyGate.test.js index 2951a6f..9122964 100644 --- a/prompt-launcher/src/lib/launcherReadyGate.test.js +++ b/prompt-launcher/src/lib/launcherReadyGate.test.js @@ -2,7 +2,7 @@ import test from "node:test"; import assert from "node:assert/strict"; import { createLauncherReadyGate } from "./launcherReadyGate.js"; -const flushMicrotasks = () => Promise.resolve(); +const flushAsyncHandlers = () => new Promise((resolve) => setImmediate(resolve)); test("does not notify the backend before the frontend is mounted", async () => { /** @type {string[]} */ @@ -23,7 +23,7 @@ test("does not notify the backend before the frontend is mounted", async () => { assert.equal(scheduled.length, 1); scheduled[0](); - await flushMicrotasks(); + await flushAsyncHandlers(); assert.deepEqual(calls, ["frontend_ready"]); }); @@ -44,7 +44,7 @@ test("coalesces repeated ready requests into one backend notification", async () assert.equal(scheduled.length, 1); scheduled[0](); - await flushMicrotasks(); + await flushAsyncHandlers(); gate.scheduleAfterInitialData(); assert.equal(scheduled.length, 1); @@ -71,7 +71,7 @@ test("allows retry when the backend notification fails", async () => { gate.markMounted(); gate.scheduleAfterInitialData(); scheduled[0](); - await flushMicrotasks(); + await flushAsyncHandlers(); assert.equal(attempts, 1); assert.equal(errors.length, 1); @@ -79,7 +79,7 @@ test("allows retry when the backend notification fails", async () => { gate.scheduleAfterInitialData(); assert.equal(scheduled.length, 2); scheduled[1](); - await flushMicrotasks(); + await flushAsyncHandlers(); assert.equal(attempts, 2); assert.equal(errors.length, 1); From 5eefaab5b2b7a4c86255b5057dacc8f61b915e03 Mon Sep 17 00:00:00 2001 From: CodeXWeb Date: Wed, 10 Jun 2026 19:27:54 +0000 Subject: [PATCH 6/7] fix: notify launcher ready after failed initial search --- DECISION_JOURNAL.md | 5 +++++ prompt-launcher/src/lib/launcherReadyGate.js | 17 +++++++++++++++ .../src/lib/launcherReadyGate.test.js | 21 ++++++++++++++++++- prompt-launcher/src/lib/tauriClient.ts | 20 +++++++++++------- 4 files changed, 54 insertions(+), 9 deletions(-) diff --git a/DECISION_JOURNAL.md b/DECISION_JOURNAL.md index 355ee91..ae1acb7 100644 --- a/DECISION_JOURNAL.md +++ b/DECISION_JOURNAL.md @@ -8,6 +8,11 @@ ## Log +### 2026-06-10 19:21 UTC +- CodeXWeb claimed PR #43 review follow-up comment `4670504519` with claim comment `4673644135`. +- Scope: keep the PR's mounted + initial data settled design, but make the initial prompt search failure path also release the hidden launcher window so users can see the existing error state. +- Validation passed in remote sandbox: `npm ci --include=dev --no-audit --no-fund`, `npm run test:unit` (15 passed), `npm run check` (0 errors, 0 warnings), `npm run build`, and `git diff --check`. + ### 2026-01-22 21:49 - Checked required project guidance files in this worktree: - CLAUDE.md: missing diff --git a/prompt-launcher/src/lib/launcherReadyGate.js b/prompt-launcher/src/lib/launcherReadyGate.js index 2a62587..42b75b6 100644 --- a/prompt-launcher/src/lib/launcherReadyGate.js +++ b/prompt-launcher/src/lib/launcherReadyGate.js @@ -39,3 +39,20 @@ export function createLauncherReadyGate( } }; } + +/** + * Treat the first prompt search as settled whether it succeeds or fails so the + * launcher can show either results or the existing error state. + * + * @template T + * @param {Promise} initialDataPromise + * @param {{ scheduleAfterInitialData: () => void }} gate + * @returns {Promise} + */ +export async function notifyWhenInitialDataSettles(initialDataPromise, gate) { + try { + return await initialDataPromise; + } finally { + gate.scheduleAfterInitialData(); + } +} diff --git a/prompt-launcher/src/lib/launcherReadyGate.test.js b/prompt-launcher/src/lib/launcherReadyGate.test.js index 9122964..377460c 100644 --- a/prompt-launcher/src/lib/launcherReadyGate.test.js +++ b/prompt-launcher/src/lib/launcherReadyGate.test.js @@ -1,6 +1,9 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { createLauncherReadyGate } from "./launcherReadyGate.js"; +import { + createLauncherReadyGate, + notifyWhenInitialDataSettles +} from "./launcherReadyGate.js"; const flushAsyncHandlers = () => new Promise((resolve) => setImmediate(resolve)); @@ -84,3 +87,19 @@ test("allows retry when the backend notification fails", async () => { assert.equal(attempts, 2); assert.equal(errors.length, 1); }); + +test("schedules readiness after initial data loading fails", async () => { + const searchError = new Error("search_prompts failed"); + let scheduled = false; + + await assert.rejects( + notifyWhenInitialDataSettles(Promise.reject(searchError), { + scheduleAfterInitialData: () => { + scheduled = true; + } + }), + searchError + ); + + assert.equal(scheduled, true); +}); diff --git a/prompt-launcher/src/lib/tauriClient.ts b/prompt-launcher/src/lib/tauriClient.ts index f5e1068..23d62ac 100644 --- a/prompt-launcher/src/lib/tauriClient.ts +++ b/prompt-launcher/src/lib/tauriClient.ts @@ -1,5 +1,8 @@ import { invoke } from "@tauri-apps/api/core"; -import { createLauncherReadyGate } from "./launcherReadyGate.js"; +import { + createLauncherReadyGate, + notifyWhenInitialDataSettles +} from "./launcherReadyGate.js"; import type { AppConfig, PromptEntry, RecentState } from "./types"; const launcherReadyGate = createLauncherReadyGate( @@ -12,13 +15,14 @@ export const tauriClient = { getConfig: () => invoke("get_config"), listPrompts: () => invoke("list_prompts"), searchPrompts: async (query: string, limit: number, favoritesOnly: boolean) => { - const prompts = await invoke("search_prompts", { - query, - limit, - favoritesOnly - }); - launcherReadyGate.scheduleAfterInitialData(); - return prompts; + return notifyWhenInitialDataSettles( + invoke("search_prompts", { + query, + limit, + favoritesOnly + }), + launcherReadyGate + ); }, setPromptsDir: (path: string) => invoke("set_prompts_dir", { path }), From 030d8c80491ac86ca03c38041aaffb150c36d18b Mon Sep 17 00:00:00 2001 From: CodeXWeb Date: Wed, 10 Jun 2026 19:29:19 +0000 Subject: [PATCH 7/7] docs: record pr43 validation --- DECISION_JOURNAL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/DECISION_JOURNAL.md b/DECISION_JOURNAL.md index ae1acb7..47b17c6 100644 --- a/DECISION_JOURNAL.md +++ b/DECISION_JOURNAL.md @@ -12,6 +12,7 @@ - CodeXWeb claimed PR #43 review follow-up comment `4670504519` with claim comment `4673644135`. - Scope: keep the PR's mounted + initial data settled design, but make the initial prompt search failure path also release the hidden launcher window so users can see the existing error state. - Validation passed in remote sandbox: `npm ci --include=dev --no-audit --no-fund`, `npm run test:unit` (15 passed), `npm run check` (0 errors, 0 warnings), `npm run build`, and `git diff --check`. +- Additional validation attempt: `npm run tauri build` was blocked by the remote sandbox lacking `cargo` (`cargo metadata` not found); no Tauri/Rust source was changed in this follow-up. ### 2026-01-22 21:49 - Checked required project guidance files in this worktree: