···17171818import { describe, it, beforeEach, afterEach } from 'node:test';
1919import * as assert from 'node:assert';
2020+import { readFileSync } from 'node:fs';
2121+import { fileURLToPath } from 'node:url';
2222+import { dirname, join } from 'node:path';
20232124import {
2225 generateToken,
···142145 assert.strictEqual(drift.events[0].reason, 'sender-mismatch');
143146 });
144147});
148148+149149+describe('Phase 3: tile:command:result is gone', () => {
150150+ // At runtime under ELECTRON_RUN_AS_NODE=1 the test file lives at
151151+ // dist/backend/electron/tile-ipc.test.js and the compiled handler
152152+ // module is its sibling tile-ipc.js. We assert the compiled output
153153+ // contains no reference to the legacy side-channel — neither the
154154+ // preload send site nor the main-process handler registration can
155155+ // leave a stub behind without this failing.
156156+ const hereDir = dirname(fileURLToPath(import.meta.url));
157157+ const compiledTileIpc = join(hereDir, 'tile-ipc.js');
158158+159159+ it('compiled tile-ipc.js does not register a tile:command:result handler', () => {
160160+ const src = readFileSync(compiledTileIpc, 'utf8');
161161+ assert.ok(
162162+ !src.includes("'tile:command:result'") && !src.includes('"tile:command:result"'),
163163+ 'tile-ipc.js still contains a tile:command:result reference — Phase 3 removed that channel'
164164+ );
165165+ });
166166+});
···268268every tile to explicitly declare every possible UUID-suffixed topic in
269269its manifest.
270270271271-**Legacy side-channel to remove**: tile-preload currently also sends a
272272-parallel `ipcRenderer.send('tile:command:result', ...)` frame
273273-(tile-preload.cts:411) that hits an unrestricted main-process publish
274274-handler (tile-ipc.ts `ipcMain.on('tile:command:result', ...)`). This
275275-duplicate path predates the allowlist carve-out and bypasses the gate
276276-entirely. It is redundant with the pubsub publish above and must be
277277-deleted — one auth path, one code path, one place to reason about who
278278-can emit a command result.
271271+**Legacy side-channel removed (Phase 3, 2026-04-23)**: tile-preload
272272+previously also sent a parallel `ipcRenderer.send('tile:command:result',
273273+...)` frame that hit an unrestricted main-process publish handler
274274+(`ipcMain.on('tile:command:result', ...)` in tile-ipc.ts). That
275275+duplicate path predated the allowlist carve-out and bypassed the gate
276276+entirely. Both sites are gone — one auth path, one code path, one
277277+place to reason about who can emit a command result.
279278280279```
281280 Dispatcher Owning tile
···300299 resolve proxy promise
301300```
302301303303-**Concrete removals**:
302302+**Concrete removals (done Phase 3)**:
304303- `ipcRenderer.send('tile:command:result', ...)` call in
305305- `api.commands.register` handler (tile-preload.cts:411). Keep the
306306- `tile:pubsub:publish` call at line 401.
307307-- `ipcMain.on('tile:command:result', ...)` handler in tile-ipc.ts.
304304+ `api.commands.register` handler — removed from tile-preload.cts.
305305+ The `tile:pubsub:publish` to `msg.resultTopic` is now the sole path.
306306+- `ipcMain.on('tile:command:result', ...)` handler in tile-ipc.ts —
307307+ removed.
308308309309## Scope semantics — delete
310310
+1-1
docs/tasks.md
···14141515- [ ] **Root-cause: only hello-world commands visible in cmd panel.** After Phases E/F, only hello-world's commands (`hello`, "hello world trace") appear in the cmd panel. Every other lazy tile's commands — tag, untag, kagi, google, ddg, lists, peeks, slides, and dozens more — are missing. Agent hypothesis: Phase E's `registerLazyTile()` publishes `cmd:register-batch` at boot before the cmd panel's core/background subscription lands. Do NOT band-aid; fix this only after the state machine is in place so the fix comes from the right abstraction. Manual test case: `yarn start`, open cmd panel, type a few characters, should see many matches — currently only sees hello-world's.
16161717-- [ ] **Consolidate command-result paths into one.** Two paths exist today: (A) `tile:command:result` IPC from tile-preload's `api.commands.register` wrapper → main-process unrestricted `publish()`; (B) handlers that manually `tile:pubsub:publish` to `cmd:execute:X:result:...`. This duality is why the Phase E bug hid for so long — pre-Phase-E tests only exercised path (A), which bypassed the capability allowlist; path (B) was silently rejected until the 2026-04-23 fix (`tolwnovr f32063db`). Collapse to one (probably A). Pre-req: state machine task above.
1717+- [x] **Consolidate command-result paths into one.** DONE 2026-04-23 as Phase 3 of pubsub-state-machine.md. The `tile:command:result` side-channel (preload send site + main-process unrestricted `publish()` handler) is gone; all command results now flow through the capability-gated `tile:pubsub:publish` of `cmd:execute:{name}:result:{uuid}` via the infra carve-out (commit `f32063db`).
18181919- [ ] **UI-level tests for cmd-panel repeat invocation.** The 2026-04-23 repro (`tests/desktop/cmd-execute-twice.spec.ts`) fires on the pubsub bus directly and passes 3/3. Manual testing caught a 3rd-invocation stall that the bus test misses — the difference must be panel UI state (`state.executionTimeout`, `urlSearchTimer`, subscription lifetime in `ensureCmdRequestRegistersListener`) leaking across repeats. Add Playwright tests that drive Cmd+K → type → Enter 3-5x in a row and assert panel closes + no spinner.
2020