experiments in a post-browser web
10
fork

Configure Feed

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

refactor(electron): delete v1 declarative-command machinery, fold publish-action into tile-lazy

`registerDeclarativeCommands` and friends in main.ts processed v1-style
`background: false` manifests, registering pubsub subscribers for
`cmd:execute:{name}` outside the tile-lazy load-on-dispatch path. Two
features (sync, dropzone) were live consumers — both have empty
`tiles: []` and depended on the v1 path to handle their declarative
commands.

Tile-lazy already short-circuits `type: 'window'` actions in the
pre-publish hook without forcing a tile load; this extends the same
treatment to `type: 'publish'` (emit the declared topic + result, skip
the original cmd:execute publish). With that in place every
declarative action shape is handled in one place:

- `type: 'window'` → publishes window-open, no tile load
- `type: 'publish'` → publishes declared topic, no tile load
- `type: 'execute'` → loads the tile, lets the publish reach the
tile's real handler

Deleted:
- `registerDeclarativeCommands`, `registerDeclarativeCommand`,
`registerDeclarativeShortcut`, `executeDeclarativeAction`,
`isDeclarativeOnly`, `declarativeExtensions` Set in main.ts
(~165 LOC)
- `background?: string | false` field from the legacy
`ExtensionManifest` interface (chrome-extensions.ts uses
`manifest.background` for chrome MV3 manifests — different concept,
untouched)
- `background: false` from features/sync/manifest.json and
features/dropzone/manifest.json
- Unused `ManifestCommand` / `ManifestShortcut` / `ExtensionManifest`
type imports in main.ts

`getRunningExtensions` and `getAllRegisteredExtensions` no longer
walk the `declarativeExtensions` set — running = core renderers +
loaded v2 tiles. Sync and dropzone show as `registered` (not
`running`) when they haven't been invoked, which is the truthful
state for a lazy declarative-only feature.

yarn build clean. yarn test:unit: 2277 pass / 0 fail.

+31 -198
-1
backend/electron/extensions.ts
··· 61 61 name?: string; 62 62 description?: string; 63 63 version?: string; 64 - background?: string | false; 65 64 builtin?: boolean; 66 65 lazy?: boolean; // If true, extension iframe is not loaded at startup; loaded on first command use 67 66 commands?: ManifestCommand[];
+8 -191
backend/electron/main.ts
··· 14 14 import { registerScheme, initProtocol, registerExtensionPath, getExtensionPath, getRegisteredExtensionIds, registerThemePath, getRegisteredThemeIds } from './protocol.js'; 15 15 import { initCore, getCoreBackgroundWindow } from './core-glue.js'; 16 16 import { initTestFixture } from './test-fixture-glue.js'; 17 - import { discoverExtensions, loadExtensionManifest, isBuiltinExtensionEnabled, type ExtensionManifest, type ManifestCommand, type ManifestShortcut } from './extensions.js'; 17 + import { discoverExtensions, loadExtensionManifest, isBuiltinExtensionEnabled } from './extensions.js'; 18 18 import { initializeFeatures, type FeatureStartupResult } from './feature-startup.js'; 19 19 import { ensureTileIpcHandlers } from './tile-compat.js'; 20 20 import { ··· 599 599 } 600 600 } 601 601 602 - // Track extensions with declarative-only commands (no background page needed) 603 - const declarativeExtensions = new Set<string>(); 604 - 605 - /** 606 - * Check if an extension has background: false in its manifest, 607 - * meaning it only uses declarative commands/shortcuts and doesn't need 608 - * a background page. 609 - */ 610 - export function isDeclarativeOnly(extId: string): boolean { 611 - return declarativeExtensions.has(extId); 612 - } 613 - 614 - /** 615 - * Register declarative commands and shortcuts from extension manifests. 616 - * 617 - * Called after cmd extension is loaded (so the command registry is ready). 618 - * Extensions with `"background": false` in their manifest declare commands 619 - * and shortcuts in the manifest itself. The main process registers them 620 - * directly without creating a background page. 621 - * 622 - * Supported action types: 623 - * - "window": Open a window with the specified URL 624 - * - "publish": Publish a pubsub message 625 - */ 626 - export function registerDeclarativeCommands(): void { 627 - const registeredExtIds = getRegisteredExtensionIds(); 628 - 629 - for (const extId of registeredExtIds) { 630 - if (!isBuiltinExtensionEnabled(extId)) continue; 631 - 632 - const extPath = getExtensionPath(extId); 633 - if (!extPath) continue; 634 - 635 - const manifest = loadExtensionManifest(extPath); 636 - if (!manifest) continue; 637 - 638 - // Only process extensions that explicitly opt out of background pages 639 - if (manifest.background !== false) continue; 640 - 641 - const commands = manifest.commands || []; 642 - const shortcuts = manifest.shortcuts || []; 643 - 644 - if (commands.length === 0 && shortcuts.length === 0) continue; 645 - 646 - DEBUG && console.log(`[ext:declarative] Registering declarative commands for ${extId}: ${commands.length} commands, ${shortcuts.length} shortcuts`); 647 - 648 - // Mark this extension as declarative-only 649 - declarativeExtensions.add(extId); 650 - 651 - // Register each command 652 - for (const cmd of commands) { 653 - registerDeclarativeCommand(extId, cmd); 654 - } 655 - 656 - // Register each shortcut 657 - for (const shortcut of shortcuts) { 658 - registerDeclarativeShortcut(extId, shortcut, commands); 659 - } 660 - } 661 - } 662 - 663 - /** 664 - * Register a single declarative command. 665 - * Sets up a pubsub subscription for cmd:execute:{name} and publishes 666 - * the command metadata to the cmd registry. 667 - */ 668 - function registerDeclarativeCommand(extId: string, cmd: ManifestCommand): void { 669 - const source = `peek://${extId}/`; 670 - 671 - // Subscribe to execution requests from the cmd panel 672 - subscribe(source, `cmd:execute:${cmd.name}`, (msg: unknown) => { 673 - DEBUG && console.log(`[ext:declarative] Executing command: ${cmd.name}`); 674 - executeDeclarativeAction(cmd, msg as Record<string, unknown>); 675 - // Signal completion so the cmd panel proxy resolves immediately 676 - // (publish-type actions are fire-and-forget — don't block the UI) 677 - publish(source, `cmd:execute:${cmd.name}:result`, { success: true }); 678 - }); 679 - 680 - // Register the command with the cmd extension's registry 681 - publish(source, 'cmd:register', { 682 - name: cmd.name, 683 - description: cmd.description || '', 684 - source, 685 - scope: cmd.scope || 'global', 686 - modes: cmd.modes || [], 687 - }); 688 - 689 - DEBUG && console.log(`[ext:declarative] Registered command: ${cmd.name} (${cmd.action.type})`); 690 - } 691 - 692 - /** 693 - * Register a single declarative shortcut. 694 - * Maps the shortcut keys to execute the associated command's action. 695 - */ 696 - function registerDeclarativeShortcut(extId: string, shortcut: ManifestShortcut, commands: ManifestCommand[]): void { 697 - const source = `peek://${extId}/`; 698 - 699 - // Find the associated command 700 - const cmd = commands.find(c => c.name === shortcut.command); 701 - if (!cmd) { 702 - console.error(`[ext:declarative] Shortcut references unknown command: ${shortcut.command} in ${extId}`); 703 - return; 704 - } 705 - 706 - const callback = () => { 707 - DEBUG && console.log(`[ext:declarative] Shortcut triggered: ${shortcut.keys} -> ${shortcut.command}`); 708 - if (cmd.action.type === 'execute') { 709 - // For execute-type commands, publish to cmd:execute so lazy stubs can intercept 710 - publish(`peek://${extId}/`, `cmd:execute:${cmd.name}`, {}); 711 - } else { 712 - executeDeclarativeAction(cmd, {}); 713 - } 714 - }; 715 - 716 - if (shortcut.global) { 717 - registerGlobalShortcut(shortcut.keys, source, callback); 718 - } else { 719 - const modeConditions = shortcut.mode ? { majorMode: shortcut.mode as any } : undefined; 720 - registerLocalShortcut(shortcut.keys, source, callback, modeConditions); 721 - } 722 - 723 - DEBUG && console.log(`[ext:declarative] Registered ${shortcut.global ? 'global' : 'local'} shortcut: ${shortcut.keys} -> ${shortcut.command}`); 724 - } 725 - 726 - /** 727 - * Execute a declarative command action. 728 - */ 729 - function executeDeclarativeAction(cmd: ManifestCommand, ctx: Record<string, unknown>): void { 730 - const action = cmd.action; 731 - 732 - switch (action.type) { 733 - case 'window': { 734 - // Open a window with the specified URL 735 - const url = action.url; 736 - const options = action.options || {}; 737 - DEBUG && console.log(`[ext:declarative] Opening window: ${url}`); 738 - 739 - // Use window:reopen-request which app/index.js already handles 740 - // (creates a window via windowManager.createWindow) 741 - publish(getSystemAddress(), 'window:reopen-request', { 742 - url, 743 - options: { 744 - ...options, 745 - trackingSource: 'declarative', 746 - trackingSourceId: cmd.name, 747 - }, 748 - }); 749 - break; 750 - } 751 - 752 - case 'publish': { 753 - // Publish a pubsub message 754 - const topic = action.topic; 755 - const data = { ...(action.data || {}), ...ctx }; 756 - DEBUG && console.log(`[ext:declarative] Publishing: ${topic}`); 757 - publish(getSystemAddress(), topic, data); 758 - break; 759 - } 760 - 761 - default: { 762 - console.error(`[ext:declarative] Unknown action type: ${(action as any).type}`); 763 - } 764 - } 765 - } 766 - 767 602 /** 768 603 * Required CSS variables that every theme must define. 769 604 * Used by validateThemeCSS() to verify theme completeness. ··· 944 779 // Phase 2: Commands 945 780 publish('system', 'ext:startup:phase', { phase: 'commands' }); 946 781 947 - // Register declarative commands/shortcuts from manifests (after cmd is ready) 948 - registerDeclarativeCommands(); 949 - 950 - // Lazy command stubs and lazy event interceptors live in `tile-lazy.ts` 951 - // now — `registerLazyTile()` (called from tile-compat) publishes 782 + // Lazy command stubs and lazy event interceptors live in `tile-lazy.ts` — 783 + // `registerLazyTile()` (called from tile-compat) publishes 952 784 // `cmd:register-batch` and the pre-publish dispatch hook handles 953 - // load-on-dispatch for both `cmd:execute:*` and declared `lazyEvents`. 785 + // load-on-dispatch for `cmd:execute:*` and declared `lazyEvents`. 786 + // Declarative `type: 'window'` and `type: 'publish'` commands are 787 + // short-circuited inside the hook without loading the tile. 954 788 955 789 DEBUG && console.log(`[ext:timing] hybrid total: ${Date.now() - extStart}ms`); 956 790 ··· 971 805 972 806 /** 973 807 * Get running extensions info 974 - * Includes core renderers, declarative extensions, and v2 tiles 808 + * Includes core renderers and v2 tiles. 975 809 */ 976 810 export function getRunningExtensions(): Array<{ id: string; manifest: unknown; status: string }> { 977 811 const running: Array<{ id: string; manifest: unknown; status: string }> = []; ··· 988 822 }); 989 823 } 990 824 991 - // Add declarative-only extensions (no background page, but still active) 992 - for (const extId of declarativeExtensions) { 993 - // Skip if already included (shouldn't happen, but be safe) 994 - if (running.some(r => r.id === extId)) continue; 995 - 996 - const extPath = getExtensionPath(extId); 997 - const manifest = extPath ? loadExtensionManifest(extPath) : null; 998 - running.push({ 999 - id: extId, 1000 - manifest: manifest || { id: extId }, 1001 - status: 'running' 1002 - }); 1003 - } 1004 - 1005 825 // Add v2 tiles launched by the tile-launcher as separate BrowserWindows. 1006 826 for (const tileId of getLoadedTileIds()) { 1007 827 if (running.some(r => r.id === tileId)) continue; ··· 1026 846 const result: Array<{ id: string; manifest: unknown; status: string }> = []; 1027 847 1028 848 // Build a set of running extension IDs for status info. 1029 - // Sources: core renderers (always up), declarative extensions, and loaded v2 tiles. 849 + // Sources: core renderers (always up) and loaded v2 tiles. 1030 850 const runningSet = new Set<string>(CORE_RENDERER_IDS); 1031 - for (const extId of declarativeExtensions) { 1032 - runningSet.add(extId); 1033 - } 1034 851 for (const tileId of getLoadedTileIds()) { 1035 852 runningSet.add(tileId); 1036 853 }
+23 -4
backend/electron/tile-lazy.ts
··· 21 21 * boot; that's gone. The hook is the one and only dispatch-site 22 22 * intercept, scoped by topic predicate. 23 23 * 24 - * - Declarative `type: 'window'` commands bypass tile load — the 25 - * hook publishes `window:reopen-request` directly and returns 26 - * 'skip' so the cmd:execute publish never reaches a handler 27 - * (there wouldn't have been one anyway; the tile never loads). 24 + * - Declarative `type: 'window'` and `type: 'publish'` commands 25 + * bypass tile load — the hook publishes `window:reopen-request` 26 + * (window) or the declared action topic (publish) directly and 27 + * returns 'skip' so the cmd:execute publish never reaches a 28 + * handler (there wouldn't have been one anyway; the tile never 29 + * loads). Only `type: 'execute'` commands force a tile load. 28 30 */ 29 31 30 32 import { ··· 378 380 trackingSourceId: name, 379 381 }, 380 382 }); 383 + const typed = msg as { resultTopic?: string } | null; 384 + if (typed?.resultTopic) { 385 + publish(getSystemAddress(), typed.resultTopic, { success: true }); 386 + } 387 + return 'skip'; 388 + } 389 + 390 + // Declarative `type: 'publish'` commands: the main process emits the 391 + // declared topic directly with merged action.data + invocation msg, 392 + // then resolves the result topic. No tile load required — the topic's 393 + // real subscribers (tiles, core, etc.) handle it. Short-circuit and 394 + // skip the original cmd:execute publish. 395 + if (cmd.action?.type === 'publish' && cmd.action.topic) { 396 + DEBUG && console.log(`[tile-lazy] Declarative publish action for ${name}, emitting ${cmd.action.topic}`); 397 + const ctx = (msg && typeof msg === 'object') ? msg as Record<string, unknown> : {}; 398 + const data = { ...(cmd.action.data || {}), ...ctx }; 399 + publish(getSystemAddress(), cmd.action.topic, data); 381 400 const typed = msg as { resultTopic?: string } | null; 382 401 if (typed?.resultTopic) { 383 402 publish(getSystemAddress(), typed.resultTopic, { success: true });
-1
features/dropzone/manifest.json
··· 5 5 "name": "Drop Zone", 6 6 "description": "Drop anything to inspect its metadata and content", 7 7 "version": "2.0.0", 8 - "background": false, 9 8 "tiles": [], 10 9 "capabilities": { 11 10 "commands": true
-1
features/sync/manifest.json
··· 6 6 "description": "Sync commands and functionality", 7 7 "builtin": true, 8 8 "hidden": true, 9 - "background": false, 10 9 "tiles": [], 11 10 "capabilities": { 12 11 "pubsub": {