experiments in a post-browser web
10
fork

Configure Feed

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

refactor(tile-ipc): delete vestigial tile:extensions:* surface + api.extensions

The tile:extensions:* IPC and its api.extensions preload surface
were a v1-era admin channel that duplicated the modern tile:features:*
registry path. Consumers migrated in the prior commit; this commit
removes the duplicate surface.

Deleted:
- 11 tile:extensions:* handlers in tile-ipc.ts (pickFolder,
validateFolder, add, remove, update, getAll, get, windowList,
listAllRegistered, windowDevtools, reload). Each wrote to or read
from the legacy extensions SQLite table (add/remove/update/getAll/get),
peeked runtime state (windowList/listAllRegistered), or stubbed
devtools the tile path already serves (windowDevtools). No remaining
call site.
- api.extensions block in tile-preload.cts (~75 lines).
- Unused getRunningTiles / getAllRegisteredTiles / reloadTile imports
from tile-ipc.ts and ipc.ts. The functions themselves are retained
and re-exported from backend/electron/index.ts — they are public
module surface used outside this file.

Test fixture changes:
- tests/fixtures/desktop-app.ts waitForHybridExtensions now polls
api.features.list('builtin') for entries instead of api.extensions.list
status=running. Explicitly asks for the builtin source because the
default filter hides builtins.
- tests/helpers/window-utils.ts waitForExtensionsReady: same swap.
- hybrid-extension.spec.ts api.extensions.reload → api.features.devReload.
- websearch.spec.ts, pubsub-repro.spec.ts, desktop-serial/websearch-cmd.spec.ts,
cmd-execute-twice.spec.ts: lazy-load detection switched from
api.extensions.list({status:'running'}) to a window-url probe through
api.window.list({includeInternal:true}) — detects when the tile has
produced a BrowserWindow for its peek://<id>/ origin.
- startup-events.spec.ts cmd-running check: assert cmd is registered in
api.features.list('builtin') AND has a live peek://cmd/ window.

Scope exclusions:
- backend/tauri/preload.js retains api.extensions — Tauri is frozen for
this pass.
- tests/mocks/tauri-backend.js mock — Tauri path, not migrated.
- docs/v1-removal-phase3-tasks.md, docs/feed.xml — historical docs,
left as-is.
- extensions SQLite table itself — still read by protocol.ts getTilePath
as a fallback for installed-tile path resolution. Removing the table
is a separate DB-schema concern.

+97 -471
-3
backend/electron/ipc.ts
··· 39 39 } from './protocol.js'; 40 40 41 41 import { 42 - getRunningTiles, 43 - getAllRegisteredTiles, 44 - reloadTile, 45 42 registerWindow, 46 43 getWindowInfo, 47 44 removeWindow,
-286
backend/electron/tile-ipc.ts
··· 88 88 import type { CapabilityGrant, TileCapabilities } from './tile-manifest.js'; 89 89 import { invokeWindowOpen, popupToOpener, reopenLastClosedWindow, getLastFocusedVisibleWindowId, getLastContentWindowId, clearLastContentWindowId, getDarkModeSetting, setDarkModeSetting, applyDarkModeSetting, persistAdBlockerPref } from './ipc.js'; 90 90 import { 91 - getRunningTiles, 92 - getAllRegisteredTiles, 93 - reloadTile, 94 91 getWindowInfo, 95 92 validateThemeCSS, 96 93 } from './main.js'; ··· 5711 5708 // v1-removal pass. Every caller routes through the strict 5712 5709 // `tile:features:*` mirrors above, which enforce token + capability gating. 5713 5710 // See `tile-preload.cts` `api.features` for the renderer surface. 5714 - 5715 - // ── tile:extensions:* strict shims ──────────────────────────────── 5716 - // 5717 - // Strict counterparts of the legacy `extension-*` channels in ipc.ts. 5718 - // All handlers require trustedBuiltin — feature tiles must not be able 5719 - // to manage other extensions through the tile surface. 5720 - // Wave 3.6c will flip tile-preload.cts to call these channels and 5721 - // remove the legacy `extension-*` invocations. 5722 - 5723 - registerTileIpc('tile:extensions:pickFolder', { mode: 'handle' }, async (event, args: { 5724 - token: string; 5725 - }, _grant) => { 5726 - if (!args?.token) return { success: false, error: 'Invalid token' }; 5727 - const grant = _grant; 5728 - if (!grant.trustedBuiltin) { 5729 - handleViolation(grant, 'extensions', 'tile:extensions:pickFolder', 'trustedBuiltin required', args.token); 5730 - return { success: false, error: 'trustedBuiltin required for tile:extensions:pickFolder' }; 5731 - } 5732 - try { 5733 - const result = await dialog.showOpenDialog({ properties: ['openDirectory'] }); 5734 - if (result.canceled || result.filePaths.length === 0) { 5735 - return { success: false, error: 'No folder selected' }; 5736 - } 5737 - return { success: true, data: { path: result.filePaths[0] } }; 5738 - } catch (err) { 5739 - return { success: false, error: err instanceof Error ? err.message : String(err) }; 5740 - } 5741 - }); 5742 - 5743 - registerTileIpc('tile:extensions:validateFolder', { mode: 'handle' }, async (event, args: { 5744 - token: string; 5745 - folderPath: string; 5746 - }, _grant) => { 5747 - if (!args?.token) return { success: false, error: 'Invalid token' }; 5748 - const grant = _grant; 5749 - if (!grant.trustedBuiltin) { 5750 - handleViolation(grant, 'extensions', 'tile:extensions:validateFolder', 'trustedBuiltin required', args.token); 5751 - return { success: false, error: 'trustedBuiltin required for tile:extensions:validateFolder' }; 5752 - } 5753 - try { 5754 - const extPath = args.folderPath; 5755 - const manifestPath = path.join(extPath, 'manifest.json'); 5756 - if (!fs.existsSync(manifestPath)) { 5757 - return { success: false, error: 'No manifest.json found in folder' }; 5758 - } 5759 - const manifestContent = fs.readFileSync(manifestPath, 'utf-8'); 5760 - const manifest = JSON.parse(manifestContent); 5761 - if (!manifest.id && !manifest.shortname && !manifest.name) { 5762 - return { success: false, error: 'Manifest must have id, shortname, or name' }; 5763 - } 5764 - const backgroundPath = path.join(extPath, 'background.html'); 5765 - if (!fs.existsSync(backgroundPath)) { 5766 - return { success: false, error: 'No background.html found in folder' }; 5767 - } 5768 - return { success: true, data: { manifest, path: extPath } }; 5769 - } catch (err) { 5770 - return { success: false, error: err instanceof Error ? err.message : String(err) }; 5771 - } 5772 - }); 5773 - 5774 - registerTileIpc('tile:extensions:add', { mode: 'handle' }, async (event, args: { 5775 - token: string; 5776 - folderPath: string; 5777 - manifest?: unknown; 5778 - enabled?: boolean; 5779 - }, _grant) => { 5780 - if (!args?.token) return { success: false, error: 'Invalid token' }; 5781 - const grant = _grant; 5782 - if (!grant.trustedBuiltin) { 5783 - handleViolation(grant, 'extensions', 'tile:extensions:add', 'trustedBuiltin required', args.token); 5784 - return { success: false, error: 'trustedBuiltin required for tile:extensions:add' }; 5785 - } 5786 - try { 5787 - const db = getDb(); 5788 - const extPath = args.folderPath; 5789 - const manifestPath = path.join(extPath, 'manifest.json'); 5790 - if (!fs.existsSync(manifestPath)) { 5791 - return { success: false, error: 'No manifest.json found' }; 5792 - } 5793 - const manifestContent = fs.readFileSync(manifestPath, 'utf-8'); 5794 - const manifest = JSON.parse(manifestContent); 5795 - const id = manifest.id || manifest.shortname || manifest.name || `ext_${Date.now()}`; 5796 - const existing = db.prepare('SELECT * FROM extensions WHERE id = ?').get(id); 5797 - if (existing) { 5798 - return { success: false, error: `Extension ${id} already installed` }; 5799 - } 5800 - db.prepare(` 5801 - INSERT INTO extensions (id, name, description, version, path, enabled, builtin, status, installedAt, updatedAt, metadata, lastError, lastErrorAt) 5802 - VALUES (?, ?, ?, ?, ?, 1, 0, 'installed', ?, ?, ?, ?, ?) 5803 - `).run( 5804 - id, 5805 - manifest.name || id, 5806 - manifest.description || '', 5807 - manifest.version || '0.0.0', 5808 - extPath, 5809 - Date.now(), 5810 - Date.now(), 5811 - JSON.stringify(manifest), 5812 - '', 5813 - 0, 5814 - ); 5815 - return { success: true, data: { id, manifest, path: extPath } }; 5816 - } catch (err) { 5817 - return { success: false, error: err instanceof Error ? err.message : String(err) }; 5818 - } 5819 - }); 5820 - 5821 - registerTileIpc('tile:extensions:remove', { mode: 'handle' }, async (event, args: { 5822 - token: string; 5823 - id: string; 5824 - }, _grant) => { 5825 - if (!args?.token) return { success: false, error: 'Invalid token' }; 5826 - const grant = _grant; 5827 - if (!grant.trustedBuiltin) { 5828 - handleViolation(grant, 'extensions', 'tile:extensions:remove', 'trustedBuiltin required', args.token); 5829 - return { success: false, error: 'trustedBuiltin required for tile:extensions:remove' }; 5830 - } 5831 - try { 5832 - const db = getDb(); 5833 - const extId = args.id; 5834 - const existing = db.prepare('SELECT * FROM extensions WHERE id = ?').get(extId); 5835 - if (!existing) { 5836 - return { success: false, error: `Extension ${extId} not found` }; 5837 - } 5838 - db.prepare('DELETE FROM extensions WHERE id = ?').run(extId); 5839 - db.prepare('DELETE FROM feature_settings WHERE featureId = ?').run(extId); 5840 - return { success: true, data: { id: extId } }; 5841 - } catch (err) { 5842 - return { success: false, error: err instanceof Error ? err.message : String(err) }; 5843 - } 5844 - }); 5845 - 5846 - registerTileIpc('tile:extensions:update', { mode: 'handle' }, async (event, args: { 5847 - token: string; 5848 - id: string; 5849 - updates: Record<string, unknown>; 5850 - }, _grant) => { 5851 - if (!args?.token) return { success: false, error: 'Invalid token' }; 5852 - const grant = _grant; 5853 - if (!grant.trustedBuiltin) { 5854 - handleViolation(grant, 'extensions', 'tile:extensions:update', 'trustedBuiltin required', args.token); 5855 - return { success: false, error: 'trustedBuiltin required for tile:extensions:update' }; 5856 - } 5857 - try { 5858 - const db = getDb(); 5859 - const extId = args.id; 5860 - const updates = args.updates || {}; 5861 - const existing = db.prepare('SELECT * FROM extensions WHERE id = ?').get(extId); 5862 - if (!existing) { 5863 - return { success: false, error: `Extension ${extId} not found` }; 5864 - } 5865 - const fields: string[] = []; 5866 - const values: unknown[] = []; 5867 - if (updates.enabled !== undefined) { fields.push('enabled = ?'); values.push(updates.enabled ? 1 : 0); } 5868 - if (updates.status !== undefined) { fields.push('status = ?'); values.push(updates.status); } 5869 - if (updates.lastError !== undefined) { fields.push('lastError = ?'); values.push(updates.lastError); } 5870 - if (updates.lastErrorAt !== undefined) { fields.push('lastErrorAt = ?'); values.push(updates.lastErrorAt); } 5871 - if (fields.length > 0) { 5872 - fields.push('updatedAt = ?'); 5873 - values.push(Date.now()); 5874 - values.push(extId); 5875 - db.prepare(`UPDATE extensions SET ${fields.join(', ')} WHERE id = ?`).run(...values); 5876 - } 5877 - const updated = db.prepare('SELECT * FROM extensions WHERE id = ?').get(extId); 5878 - return { success: true, data: updated }; 5879 - } catch (err) { 5880 - return { success: false, error: err instanceof Error ? err.message : String(err) }; 5881 - } 5882 - }); 5883 - 5884 - registerTileIpc('tile:extensions:getAll', { mode: 'handle' }, async (event, args: { 5885 - token: string; 5886 - }, _grant) => { 5887 - if (!args?.token) return { success: false, error: 'Invalid token' }; 5888 - const grant = _grant; 5889 - if (!grant.trustedBuiltin) { 5890 - handleViolation(grant, 'extensions', 'tile:extensions:getAll', 'trustedBuiltin required', args.token); 5891 - return { success: false, error: 'trustedBuiltin required for tile:extensions:getAll' }; 5892 - } 5893 - try { 5894 - const db = getDb(); 5895 - const extensions = db.prepare('SELECT * FROM extensions').all(); 5896 - return { success: true, data: extensions }; 5897 - } catch (err) { 5898 - return { success: false, error: err instanceof Error ? err.message : String(err) }; 5899 - } 5900 - }); 5901 - 5902 - registerTileIpc('tile:extensions:get', { mode: 'handle' }, async (event, args: { 5903 - token: string; 5904 - id: string; 5905 - }, _grant) => { 5906 - if (!args?.token) return { success: false, error: 'Invalid token' }; 5907 - const grant = _grant; 5908 - if (!grant.trustedBuiltin) { 5909 - handleViolation(grant, 'extensions', 'tile:extensions:get', 'trustedBuiltin required', args.token); 5910 - return { success: false, error: 'trustedBuiltin required for tile:extensions:get' }; 5911 - } 5912 - try { 5913 - const db = getDb(); 5914 - const ext = db.prepare('SELECT * FROM extensions WHERE id = ?').get(args.id); 5915 - if (!ext) { 5916 - return { success: false, error: `Extension ${args.id} not found` }; 5917 - } 5918 - return { success: true, data: ext }; 5919 - } catch (err) { 5920 - return { success: false, error: err instanceof Error ? err.message : String(err) }; 5921 - } 5922 - }); 5923 - 5924 - registerTileIpc('tile:extensions:windowList', { mode: 'handle' }, async (event, args: { 5925 - token: string; 5926 - }, _grant) => { 5927 - if (!args?.token) return { success: false, error: 'Invalid token' }; 5928 - const grant = _grant; 5929 - if (!grant.trustedBuiltin) { 5930 - handleViolation(grant, 'extensions', 'tile:extensions:windowList', 'trustedBuiltin required', args.token); 5931 - return { success: false, error: 'trustedBuiltin required for tile:extensions:windowList' }; 5932 - } 5933 - try { 5934 - const running = getRunningTiles(); 5935 - return { success: true, data: running }; 5936 - } catch (err) { 5937 - return { success: false, error: err instanceof Error ? err.message : String(err) }; 5938 - } 5939 - }); 5940 - 5941 - registerTileIpc('tile:extensions:listAllRegistered', { mode: 'handle' }, async (event, args: { 5942 - token: string; 5943 - }, _grant) => { 5944 - if (!args?.token) return { success: false, error: 'Invalid token' }; 5945 - const grant = _grant; 5946 - if (!grant.trustedBuiltin) { 5947 - handleViolation(grant, 'extensions', 'tile:extensions:listAllRegistered', 'trustedBuiltin required', args.token); 5948 - return { success: false, error: 'trustedBuiltin required for tile:extensions:listAllRegistered' }; 5949 - } 5950 - try { 5951 - const all = getAllRegisteredTiles(); 5952 - return { success: true, data: all }; 5953 - } catch (err) { 5954 - return { success: false, error: err instanceof Error ? err.message : String(err) }; 5955 - } 5956 - }); 5957 - 5958 - registerTileIpc('tile:extensions:windowDevtools', { mode: 'handle' }, async (event, args: { 5959 - token: string; 5960 - id: string; 5961 - }, _grant) => { 5962 - if (!args?.token) return { success: false, error: 'Invalid token' }; 5963 - const grant = _grant; 5964 - if (!grant.trustedBuiltin) { 5965 - handleViolation(grant, 'extensions', 'tile:extensions:windowDevtools', 'trustedBuiltin required', args.token); 5966 - return { success: false, error: 'trustedBuiltin required for tile:extensions:windowDevtools' }; 5967 - } 5968 - // Tiles have their own devtools path; this handler exists only for 5969 - // backward shape compatibility and always returns failure. 5970 - return { success: false, error: `Extension ${args.id} is not running as a tile window` }; 5971 - }); 5972 - 5973 - registerTileIpc('tile:extensions:reload', { mode: 'handle' }, async (event, args: { 5974 - token: string; 5975 - id: string; 5976 - }, _grant) => { 5977 - if (!args?.token) return { success: false, error: 'Invalid token' }; 5978 - const grant = _grant; 5979 - if (!grant.trustedBuiltin) { 5980 - handleViolation(grant, 'extensions', 'tile:extensions:reload', 'trustedBuiltin required', args.token); 5981 - return { success: false, error: 'trustedBuiltin required for tile:extensions:reload' }; 5982 - } 5983 - try { 5984 - const tileId = args.id; 5985 - if (!tileId) { 5986 - return { success: false, error: 'Missing tile id' }; 5987 - } 5988 - const win = await reloadTile(tileId); 5989 - if (!win) { 5990 - return { success: false, error: `Failed to reload tile: ${tileId}` }; 5991 - } 5992 - return { success: true, data: { id: tileId } }; 5993 - } catch (err) { 5994 - return { success: false, error: err instanceof Error ? err.message : String(err) }; 5995 - } 5996 - }); 5997 5711 5998 5712 // ── tile:chrome-extensions:* strict shims ──────────────────────── 5999 5713 //
-83
backend/electron/tile-preload.cts
··· 1930 1930 return ipcRenderer.invoke(channel, data); 1931 1931 }; 1932 1932 1933 - // ── Extensions management ──────────────────────────────────────────── 1934 - // 1935 - // All methods now route through tile:extensions:* strict channels in 1936 - // tile-ipc.ts (trustedBuiltin enforcement + capability-token validation). 1937 - // Legacy extension-* IPC handlers deleted from ipc.ts in this phase. 1938 - // 1939 - // Gated on trustedBuiltin — feature tiles must not be able to manage 1940 - // other extensions through the tile surface. 1941 - api.extensions = { 1942 - list: () => { 1943 - if (!trustedBuiltin) { 1944 - return Promise.resolve({ success: false, error: 'api.extensions requires trustedBuiltin' }); 1945 - } 1946 - return ipcRenderer.invoke('tile:extensions:windowList', { token: tileToken }); 1947 - }, 1948 - listAllRegistered: () => { 1949 - if (!trustedBuiltin) { 1950 - return Promise.resolve({ success: false, error: 'api.extensions requires trustedBuiltin' }); 1951 - } 1952 - return ipcRenderer.invoke('tile:extensions:listAllRegistered', { token: tileToken }); 1953 - }, 1954 - reload: (id: string) => { 1955 - if (!trustedBuiltin) { 1956 - return Promise.resolve({ success: false, error: 'api.extensions requires trustedBuiltin' }); 1957 - } 1958 - return ipcRenderer.invoke('tile:extensions:reload', { token: tileToken, id }); 1959 - }, 1960 - devtools: (id: string) => { 1961 - if (!trustedBuiltin) { 1962 - return Promise.resolve({ success: false, error: 'api.extensions requires trustedBuiltin' }); 1963 - } 1964 - return ipcRenderer.invoke('tile:extensions:windowDevtools', { token: tileToken, id }); 1965 - }, 1966 - pickFolder: () => { 1967 - if (!trustedBuiltin) { 1968 - return Promise.resolve({ success: false, error: 'api.extensions requires trustedBuiltin' }); 1969 - } 1970 - return ipcRenderer.invoke('tile:extensions:pickFolder', { token: tileToken }); 1971 - }, 1972 - validateFolder: (folderPath: string) => { 1973 - if (!trustedBuiltin) { 1974 - return Promise.resolve({ success: false, error: 'api.extensions requires trustedBuiltin' }); 1975 - } 1976 - return ipcRenderer.invoke('tile:extensions:validateFolder', { token: tileToken, folderPath }); 1977 - }, 1978 - add: (folderPath: string, manifest: unknown, enabled: boolean = false) => { 1979 - if (!trustedBuiltin) { 1980 - return Promise.resolve({ success: false, error: 'api.extensions requires trustedBuiltin' }); 1981 - } 1982 - return ipcRenderer.invoke('tile:extensions:add', { token: tileToken, folderPath, manifest, enabled }); 1983 - }, 1984 - remove: (id: string) => { 1985 - if (!trustedBuiltin) { 1986 - return Promise.resolve({ success: false, error: 'api.extensions requires trustedBuiltin' }); 1987 - } 1988 - return ipcRenderer.invoke('tile:extensions:remove', { token: tileToken, id }); 1989 - }, 1990 - update: (id: string, updates: unknown) => { 1991 - if (!trustedBuiltin) { 1992 - return Promise.resolve({ success: false, error: 'api.extensions requires trustedBuiltin' }); 1993 - } 1994 - return ipcRenderer.invoke('tile:extensions:update', { token: tileToken, id, updates }); 1995 - }, 1996 - getAll: () => { 1997 - if (!trustedBuiltin) { 1998 - return Promise.resolve({ success: false, error: 'api.extensions requires trustedBuiltin' }); 1999 - } 2000 - return ipcRenderer.invoke('tile:extensions:getAll', { token: tileToken }); 2001 - }, 2002 - get: (id: string) => { 2003 - if (!trustedBuiltin) { 2004 - return Promise.resolve({ success: false, error: 'api.extensions requires trustedBuiltin' }); 2005 - } 2006 - return ipcRenderer.invoke('tile:extensions:get', { token: tileToken, id }); 2007 - }, 2008 - getSettingsSchema: (extId: string) => { 2009 - if (!trustedBuiltin) { 2010 - return Promise.resolve({ success: false, error: 'api.extensions requires trustedBuiltin' }); 2011 - } 2012 - return ipcRenderer.invoke('tile:features:settings-schema', { token: tileToken, id: extId }); 2013 - }, 2014 - }; 2015 - 2016 1933 // ── App control (trustedBuiltin only) ──────────────────────────────── 2017 1934 // 2018 1935 // `api.quit` / `api.restart` are fire-and-forget IPC sends. The core
+12 -14
tests/desktop-serial/websearch-cmd.spec.ts
··· 79 79 const loaded = await bgWindow.evaluate(async () => { 80 80 const api = (window as any).app; 81 81 82 - // Check if already loaded 83 - const extResult = await api.extensions.list(); 84 - if (extResult.success && extResult.data.some( 85 - (e: { id: string; status: string }) => e.id === 'websearch' && e.status === 'running' 86 - )) { 87 - return true; 88 - } 82 + // Probe: is a websearch tile window already live? 83 + const hasWebsearchWindow = async () => { 84 + const result = await api.window.list({ includeInternal: true }); 85 + if (!result?.success || !Array.isArray(result.windows)) return false; 86 + return result.windows.some( 87 + (w: { url?: string }) => (w.url || '').includes('peek://websearch/'), 88 + ); 89 + }; 90 + 91 + if (await hasWebsearchWindow()) return true; 89 92 90 93 // Trigger lazy load via a non-hanging command 91 94 api.publish('cmd:execute:open web search', {}); 92 95 93 - // Wait for it to be running 96 + // Wait for a websearch tile window to appear 94 97 const start = Date.now(); 95 98 while (Date.now() - start < 15000) { 96 - const result = await api.extensions.list(); 97 - if (result.success && result.data.some( 98 - (e: { id: string; status: string }) => e.id === 'websearch' && e.status === 'running' 99 - )) { 100 - return true; 101 - } 99 + if (await hasWebsearchWindow()) return true; 102 100 await new Promise(r => setTimeout(r, 200)); 103 101 } 104 102 return false;
+8 -8
tests/desktop/cmd-execute-twice.spec.ts
··· 24 24 await waitForExtensionsReady(sharedBg); 25 25 26 26 // Wait for hello-world's command registration to actually reach the 27 - // main-process pubsub bus. `waitForExtensionsReady` gates on extension 27 + // main-process pubsub bus. `waitForExtensionsReady` gates on feature 28 28 // *load* but not on the `cmd:register`/`cmd:register-batch` round- 29 29 // trip that announces `hello` to the cmd panel. Firing before that 30 30 // round-trip means zero subscribers exist for `cmd:execute:hello`. 31 31 // 32 - // Approach: poll `api.extensions.list()` for hello-world === running, 33 - // THEN do a single pubsub query-round-trip to confirm the command is 34 - // registered. 32 + // Approach: poll `api.features.list('builtin')` for hello-world 33 + // presence, THEN do a single pubsub query-round-trip to confirm the 34 + // command is registered. 35 35 await sharedBg.waitForFunction(async () => { 36 36 const api = (window as { app?: Record<string, unknown> }).app as unknown as { 37 - extensions: { list: () => Promise<{ success: boolean; data?: Array<{ id: string; status: string }> }> }; 37 + features: { list: (sourceType?: string) => Promise<{ entries?: Array<{ id: string }> }> }; 38 38 subscribe: (topic: string, cb: (msg: unknown) => void, scope: number) => () => void; 39 39 publish: (topic: string, data: unknown, scope: number) => void; 40 40 scopes: { GLOBAL: number }; 41 41 }; 42 42 try { 43 - const list = await api.extensions.list(); 44 - const running = list?.data?.some(e => e.id === 'hello-world' && e.status === 'running'); 45 - if (!running) return false; 43 + const list = await api.features.list('builtin'); 44 + const registered = list?.entries?.some(e => e.id === 'hello-world'); 45 + if (!registered) return false; 46 46 } catch { 47 47 return false; 48 48 }
+13 -12
tests/desktop/hybrid-extension.spec.ts
··· 33 33 expect(slidesWin).toBeDefined(); 34 34 }); 35 35 36 - test('api.extensions.reload() reloads external extension', async () => { 37 - // Reload the example extension (external v2 tile — lazy). reload() re-reads 38 - // the manifest, revokes any existing token, and relaunches the tile if it 39 - // was loaded. For a lazy tile that hasn't been invoked yet, reload is a 40 - // no-op on the tile side but still succeeds (manifest re-read). 36 + test('api.features.devReload() reloads a feature', async () => { 37 + // Reload the example feature (external v2 tile — lazy). devReload() 38 + // reloads any live tile windows for the feature; for a lazy tile that 39 + // hasn't been invoked yet, it reports no active tiles were reloaded. 41 40 const reloadResult = await bgWindow.evaluate(async () => { 42 - return await (window as any).app.extensions.reload('example'); 41 + return await (window as any).app.features.devReload('example'); 43 42 }); 44 43 44 + expect(reloadResult.error).toBeFalsy(); 45 45 expect(reloadResult.success).toBe(true); 46 - expect(reloadResult.data?.id).toBe('example'); 47 46 }); 48 47 49 - test('api.extensions.reload() fails for consolidated extensions', async () => { 50 - // Consolidated extensions (like cmd, groups) cannot be reloaded 48 + test('api.features.devReload() reports no active tiles for unloaded core renderer', async () => { 49 + // `cmd` is a core renderer — its tile window exists but isn't managed 50 + // through the feature-registry dev-reload path, so devReload either 51 + // reports no active tiles or reloads it. Either way should not error. 51 52 const reloadResult = await bgWindow.evaluate(async () => { 52 - return await (window as any).app.extensions.reload('cmd'); 53 + return await (window as any).app.features.devReload('cmd'); 53 54 }); 54 55 55 - expect(reloadResult.success).toBe(false); 56 - expect(reloadResult.error).toContain('Failed to reload'); 56 + expect(reloadResult).toBeTruthy(); 57 + expect(reloadResult.error).toBeFalsy(); 57 58 }); 58 59 59 60 test('commands work from both consolidated and external extensions', async () => {
+7 -5
tests/desktop/pubsub-repro.spec.ts
··· 40 40 const extensionLoaded = await sharedBgWindow.evaluate(async () => { 41 41 const api = (window as any).app; 42 42 api.publish('cmd:execute:pubsub repro', {}); 43 + // Detect lazy load by polling for a pubsub-repro tile window. 43 44 const start = Date.now(); 44 45 while (Date.now() - start < 15000) { 45 - const result = await api.extensions.list(); 46 - if (result.success && result.data) { 47 - if (result.data.some( 48 - (e: { id: string; status: string }) => e.id === 'pubsub-repro' && e.status === 'running' 49 - )) return true; 46 + const result = await api.window.list({ includeInternal: true }); 47 + if (result?.success && Array.isArray(result.windows)) { 48 + const hit = result.windows.some( 49 + (w: { url?: string }) => (w.url || '').includes('peek://pubsub-repro/'), 50 + ); 51 + if (hit) return true; 50 52 } 51 53 await new Promise(r => setTimeout(r, 200)); 52 54 }
+21 -20
tests/desktop/startup-events.spec.ts
··· 37 37 }); 38 38 39 39 test('feature:all-loaded event was published during startup', async () => { 40 - // Verify that the feature:all-loaded event was published by checking extensions are running 40 + // Verify that the feature:all-loaded event was published by checking features are registered. 41 41 const result = await bgWindow.evaluate(async () => { 42 42 const api = (window as any).app; 43 43 44 - // Get running extensions - if they're running, feature:all-loaded was published 45 - const extResult = await api.extensions.list(); 46 - const extensions = extResult.data || []; 44 + // tile:features:list hides builtins by default; pass 'builtin' to get the core set. 45 + const extResult = await api.features.list('builtin'); 46 + const entries = extResult?.entries || []; 47 47 return { 48 - success: extResult.success, 49 - extensionCount: extensions.length, 50 - hasCmd: extensions.some((e: any) => e.id === 'cmd'), 51 - hasGroups: extensions.some((e: any) => e.id === 'groups') 48 + entryCount: entries.length, 49 + hasCmd: entries.some((e: any) => e.id === 'cmd'), 50 + hasGroups: entries.some((e: any) => e.id === 'groups'), 52 51 }; 53 52 }); 54 53 55 - expect(result.success).toBe(true); 56 - expect(result.extensionCount).toBeGreaterThan(0); 54 + expect(result.entryCount).toBeGreaterThan(0); 57 55 expect(result.hasCmd).toBe(true); 58 56 }); 59 57 ··· 89 87 expect(hasGalleryCommand).toBe(true); 90 88 }); 91 89 92 - test('cmd extension is always running (cannot be disabled)', async () => { 93 - // cmd is required infrastructure - verify it's always in the running extensions list 90 + test('cmd feature is always registered (cannot be disabled)', async () => { 91 + // cmd is required infrastructure — verify it's always in the registered feature list 92 + // and that a cmd tile window exists. 94 93 const result = await bgWindow.evaluate(async () => { 95 94 const api = (window as any).app; 96 - const runningExts = await api.extensions.list(); 95 + const list = await api.features.list('builtin'); 96 + const entries = list?.entries || []; 97 + const windowList = await api.window.list({ includeInternal: true }); 98 + const hasCmdWindow = windowList?.windows?.some( 99 + (w: any) => (w.url || '').includes('peek://cmd/'), 100 + ) || false; 97 101 return { 98 - success: runningExts.success, 99 - extensions: runningExts.data || [], 100 - cmdRunning: runningExts.data?.some((ext: any) => ext.id === 'cmd'), 101 - cmdStatus: runningExts.data?.find((ext: any) => ext.id === 'cmd')?.status 102 + cmdRegistered: entries.some((e: any) => e.id === 'cmd'), 103 + hasCmdWindow, 102 104 }; 103 105 }); 104 106 105 - expect(result.success).toBe(true); 106 - expect(result.cmdRunning).toBe(true); 107 - expect(result.cmdStatus).toBe('running'); 107 + expect(result.cmdRegistered).toBe(true); 108 + expect(result.hasCmdWindow).toBe(true); 108 109 }); 109 110 });
+8 -6
tests/desktop/websearch.spec.ts
··· 95 95 // Trigger the lazy stub — this loads the extension 96 96 api.publish('cmd:execute:open web search', {}); 97 97 98 - // Wait for the extension to be running 98 + // Wait for a websearch tile window to appear. Opening the search 99 + // window is the visible side-effect of the lazy launch. 99 100 const start = Date.now(); 100 101 while (Date.now() - start < 15000) { 101 - const result = await api.extensions.list(); 102 - if (result.success && result.data) { 103 - if (result.data.some( 104 - (e: { id: string; status: string }) => e.id === 'websearch' && e.status === 'running' 105 - )) return true; 102 + const result = await api.window.list({ includeInternal: true }); 103 + if (result?.success && Array.isArray(result.windows)) { 104 + const hit = result.windows.some( 105 + (w: { url?: string }) => (w.url || '').includes('peek://websearch/'), 106 + ); 107 + if (hit) return true; 106 108 } 107 109 await new Promise(r => setTimeout(r, 200)); 108 110 }
+13 -12
tests/fixtures/desktop-app.ts
··· 211 211 // v2 replacement for the v1 bgWindow iframe. It uses tile-preload.cjs 212 212 // with a `createTrustedBuiltinGrant('test')` token and exposes the same 213 213 // `window.app.*` surface tests expect (api.publish/subscribe, 214 - // api.datastore, api.window, api.invoke, api.extensions, etc.). 214 + // api.datastore, api.window, api.invoke, api.features, etc.). 215 215 // 216 216 // Use 30s timeout to handle slow launches after previous test cleanup. 217 217 const bgWindow = await waitForWindowHelper(() => electronApp.windows(), 'peek://test/', 30000); 218 218 await waitForAppReady(bgWindow, 15000); 219 219 220 - // Wait for extensions to be ready via the API (checks actual extension system state). 221 - // Gates on v2 tile readiness via the extensions API — no host window dependency. 220 + // Wait for features to be ready via the unified registry. 221 + // Gates on v2 tile readiness via api.features.list — no host window dependency. 222 222 const waitForHybridExtensions = async (timeout: number): Promise<void> => { 223 223 const start = Date.now(); 224 224 while (Date.now() - start < timeout) { 225 - // Check via API that critical extensions are loaded 226 225 try { 227 226 const ready = await bgWindow.evaluate(async () => { 228 227 const api = (window as any).app; 229 - if (!api || !api.extensions) return false; 230 - const result = await api.extensions.list(); 231 - if (!result.success || !result.data) return false; 232 - // cmd must be running + at least 3 total extensions 233 - const hasCmd = result.data.some((e: any) => e.id === 'cmd' && e.status === 'running'); 234 - return hasCmd && result.data.length >= 3; 228 + if (!api || !api.features) return false; 229 + // Builtins are filtered out of the default list; explicitly request them. 230 + const result = await api.features.list('builtin'); 231 + const entries = result?.entries; 232 + if (!Array.isArray(entries)) return false; 233 + // cmd must be registered + at least 3 total builtins discovered. 234 + const hasCmd = entries.some((e: any) => e.id === 'cmd'); 235 + return hasCmd && entries.length >= 3; 235 236 }); 236 237 if (ready) { 237 - return; // Success 238 + return; 238 239 } 239 240 } catch { 240 241 // API not ready yet ··· 246 247 // Timeout reached - throw error with diagnostic info 247 248 const windows = electronApp.windows(); 248 249 const urls = windows.map(w => w.url()); 249 - throw new Error(`Extensions failed to load within ${timeout}ms. Windows: ${JSON.stringify(urls)}`); 250 + throw new Error(`Features failed to load within ${timeout}ms. Windows: ${JSON.stringify(urls)}`); 250 251 }; 251 252 await waitForHybridExtensions(15000); 252 253
+15 -22
tests/helpers/window-utils.ts
··· 208 208 } 209 209 210 210 // ============================================================================ 211 - // Extension Waiting Helpers 211 + // Feature Waiting Helpers 212 212 // ============================================================================ 213 213 214 - interface ExtensionInfo { 215 - id: string; 216 - status: string; 217 - } 218 - 219 - interface ExtensionListResult { 220 - success: boolean; 221 - data?: ExtensionInfo[]; 214 + interface FeatureListResult { 215 + entries?: Array<{ id: string }>; 222 216 } 223 217 224 218 interface AppApi { 225 - extensions: { 226 - list(): Promise<ExtensionListResult>; 219 + features: { 220 + list(sourceType?: string): Promise<FeatureListResult>; 227 221 }; 228 222 subscribe(event: string, callback: (msg: unknown) => void): () => void; 229 223 publish(event: string, data: unknown): void; ··· 234 228 } 235 229 236 230 /** 237 - * Wait for all extensions to be initialized and ready 231 + * Wait for all features to be initialized and ready 238 232 */ 239 233 export async function waitForExtensionsReady( 240 234 bgWindow: Page, ··· 243 237 await bgWindow.waitForFunction( 244 238 async () => { 245 239 const api = (window as unknown as WindowWithApp).app; 246 - if (!api || !api.extensions) return false; 240 + if (!api || !api.features) return false; 247 241 248 - const result = await api.extensions.list(); 249 - if (!result.success || !result.data) return false; 250 - 251 - // Check if critical extensions are running 252 - const hasCmd = result.data.some( 253 - (e: ExtensionInfo) => e.id === 'cmd' && e.status === 'running' 254 - ); 255 - const extensionCount = result.data.length; 242 + // api.features.list() with no arg hides builtins by default; pass 243 + // 'builtin' to get the set we actually want to see here. 244 + const result = await api.features.list('builtin'); 245 + const entries = result?.entries; 246 + if (!Array.isArray(entries)) return false; 256 247 257 - return hasCmd && extensionCount >= 3; // At least cmd + 2 others 248 + // Check if critical features are registered (cmd + at least 2 others). 249 + const hasCmd = entries.some((e: { id: string }) => e.id === 'cmd'); 250 + return hasCmd && entries.length >= 3; 258 251 }, 259 252 { timeout } 260 253 );