···361361 api.pubsub.publish('cmd:query-commands-response', { commands }, api.scopes.GLOBAL);
362362 }, api.scopes.GLOBAL);
363363364364- // Request every booted tile re-emit its cmd:register events. Tiles that
365365- // registered their commands during their own init may have done so before
366366- // this extension's `cmd:register` subscriber was live on main-side — the
367367- // broadcaster iterates tileWindows at publish time, so any tile registered
368368- // in tileWindows after the publish never saw those events. Each tile-
369369- // preload caches its `cmd:register` payloads and replays them in response
370370- // to this request. See smoke.spec.ts:1502 flake analysis.
371371- api.pubsub.publish('cmd:request-registers', {}, api.scopes.GLOBAL);
372372-373364 log('ext:cmd', 'Command registry initialized');
374365};
375366
···11+/**
22+ * Phase 5: replay machinery removal.
33+ *
44+ * Asserts the compiled tile-preload.cjs carries no reference to the
55+ * deleted `cmd:request-registers` replay topic, the `registeredPayloads`
66+ * cache, or the `ensureCmdRequestRegistersListener` installer. Phase 4
77+ * made the subscribe-before-publish invariant load-bearing (Core's
88+ * subscribers are live before any tile reaches `ready`), so the replay
99+ * topic has no job and must not linger as dead code.
1010+ *
1111+ * Mirrors the "tile-ipc.js does not register a tile:command:result
1212+ * handler" style assertion from tile-ipc.test.ts (Phase 3).
1313+ */
1414+1515+import { describe, it } from 'node:test';
1616+import * as assert from 'node:assert';
1717+import { readFileSync } from 'node:fs';
1818+import { fileURLToPath } from 'node:url';
1919+import { dirname, join } from 'node:path';
2020+2121+// At runtime under ELECTRON_RUN_AS_NODE=1 this test file lives at
2222+// dist/backend/electron/tile-preload-no-request-registers.test.js and the
2323+// compiled preload module is its sibling tile-preload.cjs (emitted from
2424+// tile-preload.cts).
2525+const hereDir = dirname(fileURLToPath(import.meta.url));
2626+const compiledPreload = join(hereDir, 'tile-preload.cjs');
2727+2828+describe('Phase 5: cmd:request-registers replay machinery is gone', () => {
2929+ it('compiled tile-preload.cjs contains no reference to cmd:request-registers', () => {
3030+ const src = readFileSync(compiledPreload, 'utf8');
3131+ assert.ok(
3232+ !src.includes('cmd:request-registers'),
3333+ 'tile-preload.cjs still references cmd:request-registers — Phase 5 deleted that replay topic'
3434+ );
3535+ });
3636+3737+ it('compiled tile-preload.cjs does not install the replay listener or cache', () => {
3838+ const src = readFileSync(compiledPreload, 'utf8');
3939+ assert.ok(
4040+ !src.includes('ensureCmdRequestRegistersListener'),
4141+ 'tile-preload.cjs still installs ensureCmdRequestRegistersListener — Phase 5 deleted it'
4242+ );
4343+ assert.ok(
4444+ !src.includes('registeredPayloads'),
4545+ 'tile-preload.cjs still maintains the registeredPayloads cache — Phase 5 deleted it'
4646+ );
4747+ });
4848+});
-34
backend/electron/tile-preload.cts
···247247 // re-registers the same command (swap handler in-place instead of adding a
248248 // second `pubsub:cmd:execute:{name}` listener).
249249 const registeredCommands = new Map<string, (params: unknown) => unknown>();
250250- // Cache of cmd:register publish payloads so we can re-emit them if a later-
251251- // booting cmd extension asks via `cmd:request-registers`. Without this, a
252252- // tile that registers its commands during module init can have its events
253253- // dropped if the cmd extension's subscriber wasn't yet live on main-side —
254254- // the broadcaster iterates `tileWindows` unconditionally but only at the
255255- // moment of publish. See smoke.spec.ts:1502 flake analysis.
256256- const registeredPayloads = new Map<string, Record<string, unknown>>();
257257- let cmdRequestRegistersListenerInstalled = false;
258258-259259- function ensureCmdRequestRegistersListener(): void {
260260- if (cmdRequestRegistersListenerInstalled) return;
261261- cmdRequestRegistersListenerInstalled = true;
262262- // Synchronous upstream subscribe (same pattern as subscribeImpl post-fix).
263263- // On request, re-emit every cached cmd:register payload so a freshly-
264264- // booted cmd extension gets this tile's registry.
265265- ipcRenderer.send('tile:pubsub:subscribe', {
266266- token: tileToken,
267267- source: sourceAddress,
268268- topic: 'cmd:request-registers',
269269- });
270270- ipcRenderer.on('pubsub:cmd:request-registers', () => {
271271- for (const payload of registeredPayloads.values()) {
272272- ipcRenderer.send('tile:pubsub:publish', {
273273- token: tileToken,
274274- source: sourceAddress,
275275- scope: 3, // GLOBAL
276276- topic: 'cmd:register',
277277- data: payload,
278278- });
279279- }
280280- });
281281- }
282250283251 function hasCommandsCapability(): boolean {
284252 const cc = grantedCapabilities?.commands;
···368336 produces: produces || [],
369337 params: params || [],
370338 };
371371- registeredPayloads.set(name, registerPayload);
372372- ensureCmdRequestRegistersListener();
373339 ipcRenderer.send('tile:pubsub:publish', {
374340 token: tileToken,
375341 source: sourceAddress,
+1-1
docs/tasks.md
···16161717- [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.
1919+- [ ] **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`, command subscription lifetime) leaking across repeats. Add Playwright tests that drive Cmd+K → type → Enter 3-5x in a row and assert panel closes + no spinner.
20202121---
2222