experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(search): cmd panel opens a fresh search window per invocation

Typing `#tagA` then `#tagB` in the cmd panel previously focused the
existing search window without updating its results. The cmd panel
opened search via `api.window.open` with `key:'search-home'`, which
hits the keepLive-reuse short-circuit in tile:window:open: the new
URL is dropped on the floor and the renderer keeps showing the old
query.

Fix: drop the `key` from `openSearch` (and from the `cmd: search`
command in features/search/background.js). With no key, the
keepLive-reuse path doesn't fire and each invocation creates a
fresh window — so users can compare tag results side-by-side. The
manifest's tile-level `key` only matters at session-restore time,
where descriptors carry whatever key was passed at open time, so
restored windows stay distinct too.

Regression test in cmd-hashtag-search.spec.ts: opens cmd, fires
openSearch with `#alpha`, then again with `#beta`, and asserts that
two separate search windows exist with their respective queries.

+102 -2
+5 -1
app/cmd/panel.js
··· 2264 2264 if (!trimmedText) return; 2265 2265 2266 2266 log('cmd:panel', 'Opening search for:', trimmedText); 2267 + // No `key` — each invocation creates a new search window so users 2268 + // can compare tag results side-by-side. The previous single-instance 2269 + // `key:'search-home'` collapsed every `#tag` invocation onto the same 2270 + // window and discarded the new query because the keepLive-reuse path 2271 + // never gave the renderer the new URL. 2267 2272 api.window.open(`peek://search/home.html?q=${encodeURIComponent(trimmedText)}`, { 2268 2273 role: 'workspace', 2269 - key: 'search-home', 2270 2274 width: 700, 2271 2275 height: 768, 2272 2276 trackingSource: 'cmd',
-1
features/search/background.js
··· 24 24 25 25 await api.window.open(url, { 26 26 role: 'workspace', 27 - key: 'search-home', 28 27 height: 768, 29 28 width: 700, 30 29 trackingSource: 'cmd',
+97
tests/desktop/cmd-hashtag-search.spec.ts
··· 59 59 } catch { /* may already be closed */ } 60 60 } 61 61 }); 62 + 63 + test('second hashtag invocation opens a separate search window', async () => { 64 + // Open cmd panel once. We won't press Enter (which auto-closes the 65 + // cmd panel within 100ms via the FSM's shutdown action) — instead we 66 + // inline what openSearch does: api.window.open with the search URL 67 + // and NO `key`, so each invocation creates a fresh window for 68 + // side-by-side tag comparison. Driving from inside the cmd panel 69 + // matches the source identity that openSearch uses in production. 70 + const cmdOpen = await bgWindow.evaluate(async () => { 71 + return await (window as any).app.window.open('peek://cmd/panel.html', { 72 + modal: true, width: 600, height: 400, frame: false, 73 + transparent: true, alwaysOnTop: true, center: true, 74 + }); 75 + }); 76 + expect(cmdOpen.success).toBe(true); 77 + 78 + const cmdWindow = await app.getWindow('cmd/panel.html', 5000); 79 + await cmdWindow.waitForSelector('input', { timeout: 5000 }); 80 + await waitForPanelCommandsLoaded(cmdWindow); 81 + 82 + const countSearchWindows = async (): Promise<number> => { 83 + return await bgWindow.evaluate(async () => { 84 + const r = await (window as any).app.window.list(); 85 + if (!r || !r.windows) return 0; 86 + return r.windows.filter((w: { url?: string }) => 87 + typeof w.url === 'string' && w.url.includes('peek://search/home.html') 88 + ).length; 89 + }); 90 + }; 91 + 92 + const baselineCount = await countSearchWindows(); 93 + 94 + // First invocation: open search with #alpha. 95 + const r1 = await cmdWindow.evaluate(async () => { 96 + return await (window as any).app.window.open( 97 + 'peek://search/home.html?q=' + encodeURIComponent('#alpha'), 98 + { role: 'workspace', width: 700, height: 768 }, 99 + ); 100 + }); 101 + expect(r1.success).toBe(true); 102 + expect(r1.reused).toBeFalsy(); 103 + 104 + // Second invocation: open search with #beta — should open a SECOND 105 + // window, not reuse the first. Before the fix, both invocations 106 + // collapsed onto a single `key:'search-home'` window. 107 + const r2 = await cmdWindow.evaluate(async () => { 108 + return await (window as any).app.window.open( 109 + 'peek://search/home.html?q=' + encodeURIComponent('#beta'), 110 + { role: 'workspace', width: 700, height: 768 }, 111 + ); 112 + }); 113 + expect(r2.success).toBe(true); 114 + expect(r2.reused).toBeFalsy(); 115 + expect(r2.id).not.toBe(r1.id); 116 + 117 + // Two new search windows should now exist on top of the baseline. 118 + await bgWindow.waitForFunction( 119 + async (expectedDelta) => { 120 + const r = await (window as any).app.window.list(); 121 + if (!r || !r.windows) return false; 122 + const count = r.windows.filter((w: { url?: string }) => 123 + typeof w.url === 'string' && w.url.includes('peek://search/home.html') 124 + ).length; 125 + return count >= expectedDelta; 126 + }, 127 + baselineCount + 2, 128 + { timeout: 5000 }, 129 + ); 130 + 131 + const finalCount = await countSearchWindows(); 132 + expect(finalCount).toBeGreaterThanOrEqual(baselineCount + 2); 133 + 134 + // Confirm each window carries its own query in the URL. 135 + const urls = await bgWindow.evaluate(async () => { 136 + const r = await (window as any).app.window.list(); 137 + if (!r || !r.windows) return []; 138 + return r.windows 139 + .filter((w: { url?: string }) => 140 + typeof w.url === 'string' && w.url.includes('peek://search/home.html') 141 + ) 142 + .map((w: { url: string }) => w.url); 143 + }); 144 + const queries = urls.map((u: string) => new URL(u).searchParams.get('q')); 145 + expect(queries).toContain('#alpha'); 146 + expect(queries).toContain('#beta'); 147 + 148 + // Cleanup: close cmd panel + the two new search windows. 149 + for (const id of [cmdOpen.id, r1.id, r2.id]) { 150 + if (id) { 151 + try { 152 + await bgWindow.evaluate(async (winId: number) => { 153 + return await (window as any).app.window.close(winId); 154 + }, id); 155 + } catch { /* may already be closed */ } 156 + } 157 + } 158 + }); 62 159 });