experiments in a post-browser web
10
fork

Configure Feed

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

chore(tiles): drop 'v2' qualifier from tile vocabulary

Tiles are how features work — there's no v1/v2 distinction left in the
runtime since the v1 removal landed (2026-04-25). This sweep cleans up
stale 'v2' mentions everywhere they appeared:

- Comments, doc references, manifest descriptions, feature READMEs/PLANs
- Type rename: TileManifestV2 -> TileManifest
- Function renames: loadV2Tile -> loadTile, loadV2Feature -> loadTileFromEntry
- Local var: isRealV2TileLaunch -> isRealTileLaunch
- Field renames on parse result and AnnotatedTile (.v2 / .v2Manifest -> .manifest)
- ManifestVersion type: ('v1', 'v2') -> ('legacy', 'tile')

Untouched: external OAuth URLs, manifest-version-2 wire-format references
in v1-removal docs (historical), Chrome MV2 fixtures (different domain).

+320 -320
+2 -2
app/index.js
··· 831 831 features().forEach(initFeature); 832 832 833 833 // Features (tiles) are loaded by the main process feature pipeline. 834 - // The main-process startup (entry.ts -> loadFeatures()) runs the v2 834 + // The main-process startup (entry.ts -> loadFeatures()) runs the 835 835 // tile pipeline (tile-compat / tile-loader); core background just 836 836 // initializes in-process features and waits for `feature:all-loaded`. 837 837 log('core', 'Core features initialized. Tiles loaded by main process.'); ··· 840 840 841 841 842 842 // Note: init() is now called explicitly by app/background.html AFTER 843 - // `await api.initialize()` resolves, so the v2 tile-preload capability 843 + // `await api.initialize()` resolves, so the tile-preload capability 844 844 // token is validated before any subscribers / datastore / window calls 845 845 // fire. Previously this ran on `window.load`, which raced the async 846 846 // `tile:validate-token` IPC round-trip under tile-preload.cjs.
+4 -4
backend/electron/atproto-install-hardening.test.ts
··· 22 22 } from './atproto-source.js'; 23 23 import { FeatureRegistry } from './feature-registry.js'; 24 24 import { installFromBundle, type TileBundle } from './feature-installer.js'; 25 - import type { TileManifestV2 } from './tile-manifest.js'; 25 + import type { TileManifest } from './tile-manifest.js'; 26 26 27 27 // ─── Helpers ──────────────────────────────────────────────────────── 28 28 ··· 34 34 return fs.mkdtempSync(path.join(os.tmpdir(), 'atproto-hardening-test-')); 35 35 } 36 36 37 - function makeManifest(id: string, overrides: Record<string, unknown> = {}): TileManifestV2 { 37 + function makeManifest(id: string, overrides: Record<string, unknown> = {}): TileManifest { 38 38 return { 39 39 manifestVersion: 3, 40 40 id, ··· 51 51 { name: `${id} cmd`, action: { type: 'execute' } }, 52 52 ], 53 53 ...overrides, 54 - } as unknown as TileManifestV2; 54 + } as unknown as TileManifest; 55 55 } 56 56 57 57 // ─── Global fetch stub ────────────────────────────────────────────── ··· 115 115 handle: string; 116 116 pdsUrl: string; 117 117 rkey: string; 118 - manifest: TileManifestV2; 118 + manifest: TileManifest; 119 119 files: BundleFixtureFile[]; 120 120 minPeekVersion?: string; 121 121 }
+4 -4
backend/electron/atproto-source.ts
··· 11 11 import crypto from 'node:crypto'; 12 12 13 13 import type { 14 - TileManifestV2, 14 + TileManifest, 15 15 } from './tile-manifest.js'; 16 16 import { 17 17 validateTileManifest, ··· 69 69 handle: string; 70 70 pdsUrl: string; 71 71 record: FeatureReleaseRecord; 72 - manifest: TileManifestV2; 72 + manifest: TileManifest; 73 73 atUri: string; 74 74 } 75 75 ··· 379 379 } 380 380 } 381 381 382 - // 6. Download manifest blob and parse as TileManifestV2. 382 + // 6. Download manifest blob and parse as TileManifest. 383 383 // Verify CID matches the record's manifest ref. 384 384 const manifestBuffer = await getBlob(pdsUrl, did, record.manifest.ref.$link); 385 385 verifyBlobCid('manifest.json', manifestBuffer, record.manifest.ref.$link); ··· 399 399 throw new Error(`Invalid manifest: ${msgs}`); 400 400 } 401 401 402 - const manifest = manifestRaw as unknown as TileManifestV2; 402 + const manifest = manifestRaw as unknown as TileManifest; 403 403 404 404 // 8. Cross-validate: record featureId should match manifest id 405 405 if (manifest.id !== record.featureId) {
+2 -2
backend/electron/config.ts
··· 41 41 } 42 42 43 43 /** 44 - * Set the v2 tile preload script path (called during app initialization) 44 + * Set the tile preload script path (called during app initialization) 45 45 */ 46 46 export function setTilePreloadPath(tilePreloadPath: string): void { 47 47 _tilePreloadPath = tilePreloadPath; 48 48 } 49 49 50 50 /** 51 - * Get the v2 tile preload script path 51 + * Get the tile preload script path 52 52 */ 53 53 export function getTilePreloadPath(): string { 54 54 return _tilePreloadPath;
+3 -3
backend/electron/entry.ts
··· 304 304 if (!tilePreloadPath) return; 305 305 306 306 // Special-case: HUD widget webviews (peek://hud/widgets/*.html). 307 - // HUD has no v2 manifest; mint a trustedBuiltin token for all widgets 307 + // HUD has no tile manifest; mint a trustedBuiltin token for all widgets 308 308 // under a unified hud-widget tile id so they can use api.window.*, 309 309 // api.context.*, api.izui.*, api.pubsub.*, etc. 310 310 if (params.src.startsWith('peek://hud/widgets/')) { ··· 331 331 (webPreferences as { additionalArguments?: string[] }).additionalArguments = 332 332 tileWebPrefs.additionalArguments; 333 333 DEBUG && console.log( 334 - `[webview] Injecting v2 tile-preload for ${params.src} (tile=${tileWebPrefs.tileId}, entry=${tileWebPrefs.entryId})` 334 + `[webview] Injecting tile-preload for ${params.src} (tile=${tileWebPrefs.tileId}, entry=${tileWebPrefs.entryId})` 335 335 ); 336 336 } else { 337 337 // Any peek:// URL that reaches here is either a developer-extension ··· 1024 1024 } 1025 1025 }); 1026 1026 1027 - // Create the core background window (v2 tile mechanics — see 1027 + // Create the core background window (tile mechanics — see 1028 1028 // backend/electron/core-glue.ts::initCore). Awaited so startup code 1029 1029 // downstream sees a fully-initialised renderer with all subscribers 1030 1030 // registered before e.g. CLI URLs get replayed.
+17 -17
backend/electron/feature-installer.test.ts
··· 36 36 fs.writeFileSync(path.join(dir, 'manifest.json'), JSON.stringify(manifest), 'utf-8'); 37 37 } 38 38 39 - function makeV2Manifest(id: string, overrides: Record<string, unknown> = {}): Record<string, unknown> { 39 + function makeTileManifest(id: string, overrides: Record<string, unknown> = {}): Record<string, unknown> { 40 40 return { 41 41 manifestVersion: 3, 42 42 id, ··· 104 104 // ─── Builtin Source ───────────────────────────────────────────────── 105 105 106 106 describe('scanBuiltinFeatures', () => { 107 - it('should register v2 features from features dir', () => { 108 - writeManifest(path.join(featuresDir, 'alpha'), makeV2Manifest('alpha')); 109 - writeManifest(path.join(featuresDir, 'beta'), makeV2Manifest('beta')); 107 + it('should register tile features from features dir', () => { 108 + writeManifest(path.join(featuresDir, 'alpha'), makeTileManifest('alpha')); 109 + writeManifest(path.join(featuresDir, 'beta'), makeTileManifest('beta')); 110 110 111 111 const registry = new FeatureRegistry(registryDir); 112 112 registry.loadRegistry(); ··· 124 124 125 125 it('should skip v1 manifests', () => { 126 126 writeManifest(path.join(featuresDir, 'legacy'), makeV1Manifest('legacy')); 127 - writeManifest(path.join(featuresDir, 'modern'), makeV2Manifest('modern')); 127 + writeManifest(path.join(featuresDir, 'modern'), makeTileManifest('modern')); 128 128 129 129 const registry = new FeatureRegistry(registryDir); 130 130 registry.loadRegistry(); ··· 165 165 }); 166 166 167 167 it('should resolve capabilities as builtin (all granted)', () => { 168 - writeManifest(path.join(featuresDir, 'priv'), makeV2Manifest('priv', { 168 + writeManifest(path.join(featuresDir, 'priv'), makeTileManifest('priv', { 169 169 capabilities: { 170 170 pubsub: { scopes: ['self', 'global', 'system'] }, 171 171 sync: true, ··· 192 192 describe('installFromLocalDirectory', () => { 193 193 it('should install a valid feature from directory', () => { 194 194 const sourceDir = path.join(tmpDir, 'source-feature'); 195 - writeManifest(sourceDir, makeV2Manifest('local-feat')); 195 + writeManifest(sourceDir, makeTileManifest('local-feat')); 196 196 fs.writeFileSync(path.join(sourceDir, 'background.html'), '<html></html>', 'utf-8'); 197 197 198 198 const registry = new FeatureRegistry(registryDir); ··· 234 234 235 235 const result = installFromLocalDirectory(sourceDir, registry); 236 236 assert.strictEqual(result.success, false); 237 - assert.ok(result.error?.includes('v2')); 237 + assert.ok(result.error?.includes('tile manifests')); 238 238 }); 239 239 240 240 it('should reject invalid manifests', () => { ··· 251 251 252 252 it('should reject duplicate id without force', () => { 253 253 const sourceDir = path.join(tmpDir, 'dup-source'); 254 - writeManifest(sourceDir, makeV2Manifest('dup-id')); 254 + writeManifest(sourceDir, makeTileManifest('dup-id')); 255 255 256 256 const registry = new FeatureRegistry(registryDir); 257 257 registry.loadRegistry(); ··· 268 268 269 269 it('should allow duplicate id with force', () => { 270 270 const sourceDir = path.join(tmpDir, 'force-source'); 271 - writeManifest(sourceDir, makeV2Manifest('force-id')); 271 + writeManifest(sourceDir, makeTileManifest('force-id')); 272 272 273 273 const registry = new FeatureRegistry(registryDir); 274 274 registry.loadRegistry(); ··· 280 280 281 281 it('should report denied capabilities for non-builtin', () => { 282 282 const sourceDir = path.join(tmpDir, 'priv-source'); 283 - writeManifest(sourceDir, makeV2Manifest('priv-local', { 283 + writeManifest(sourceDir, makeTileManifest('priv-local', { 284 284 capabilities: { 285 285 filesystem: { read: ['/tmp/*'] }, 286 286 sync: true, ··· 299 299 300 300 it('should copy nested directories', () => { 301 301 const sourceDir = path.join(tmpDir, 'nested-source'); 302 - writeManifest(sourceDir, makeV2Manifest('nested')); 302 + writeManifest(sourceDir, makeTileManifest('nested')); 303 303 const subDir = path.join(sourceDir, 'assets', 'icons'); 304 304 fs.mkdirSync(subDir, { recursive: true }); 305 305 fs.writeFileSync(path.join(subDir, 'icon.png'), 'fake-png', 'utf-8'); ··· 319 319 320 320 describe('syncBuiltinFeatures', () => { 321 321 it('should add new builtins to empty registry', () => { 322 - writeManifest(path.join(featuresDir, 'sync'), makeV2Manifest('sync')); 323 - writeManifest(path.join(featuresDir, 'tags'), makeV2Manifest('tags')); 322 + writeManifest(path.join(featuresDir, 'sync'), makeTileManifest('sync')); 323 + writeManifest(path.join(featuresDir, 'tags'), makeTileManifest('tags')); 324 324 325 325 const registry = new FeatureRegistry(registryDir); 326 326 registry.loadRegistry(); ··· 338 338 }); 339 339 340 340 it('should remove builtins no longer on disk', () => { 341 - writeManifest(path.join(featuresDir, 'stays'), makeV2Manifest('stays')); 341 + writeManifest(path.join(featuresDir, 'stays'), makeTileManifest('stays')); 342 342 343 343 const registry = new FeatureRegistry(registryDir); 344 344 registry.loadRegistry(); ··· 362 362 }); 363 363 364 364 it('should update path for moved builtins', () => { 365 - writeManifest(path.join(featuresDir, 'moved'), makeV2Manifest('moved')); 365 + writeManifest(path.join(featuresDir, 'moved'), makeTileManifest('moved')); 366 366 367 367 const registry = new FeatureRegistry(registryDir); 368 368 registry.loadRegistry(); ··· 387 387 }); 388 388 389 389 it('should not touch non-builtin entries with same id', () => { 390 - writeManifest(path.join(featuresDir, 'conflict'), makeV2Manifest('conflict')); 390 + writeManifest(path.join(featuresDir, 'conflict'), makeTileManifest('conflict')); 391 391 392 392 const registry = new FeatureRegistry(registryDir); 393 393 registry.loadRegistry();
+14 -14
backend/electron/feature-installer.ts
··· 16 16 import crypto from 'node:crypto'; 17 17 18 18 import { 19 - type TileManifestV2, 19 + type TileManifest, 20 20 type TileCapabilities, 21 21 validateTileManifest, 22 22 detectManifestVersion, ··· 34 34 * A bundle of files that make up a feature 35 35 */ 36 36 export interface TileBundle { 37 - manifest: TileManifestV2; 37 + manifest: TileManifest; 38 38 files: Map<string, Buffer>; // relative path -> file contents 39 39 } 40 40 ··· 43 43 */ 44 44 export interface FeatureSourceMetadata { 45 45 source: FeatureSource; 46 - manifest: TileManifestV2; 46 + manifest: TileManifest; 47 47 } 48 48 49 49 /** ··· 81 81 /** 82 82 * Scan a features directory for builtin features and register them. 83 83 * 84 - * For each subdirectory with a valid v2 manifest.json: 84 + * For each subdirectory with a valid tile manifest.json: 85 85 * - Registers in the registry with source type 'builtin' 86 86 * - Path points directly at the repo directory (no copy) 87 87 * - manifestCid is null (dev mode) ··· 113 113 const manifestJson = fs.readFileSync(manifestPath, 'utf-8'); 114 114 const raw = JSON.parse(manifestJson) as Record<string, unknown>; 115 115 116 - // Only process v2 manifests 116 + // Only process tile manifests 117 117 const version = detectManifestVersion(raw); 118 - if (version !== 'v2') continue; 118 + if (version !== 'tile') continue; 119 119 120 120 // Validate 121 121 const errors = validateTileManifest(raw); ··· 127 127 continue; 128 128 } 129 129 130 - const manifest = raw as unknown as TileManifestV2; 130 + const manifest = raw as unknown as TileManifest; 131 131 const id = manifest.id; 132 132 133 133 // Resolve capabilities (builtin = true, all granted) ··· 190 190 return { success: false, error: `Failed to read manifest: ${message}` }; 191 191 } 192 192 193 - // Must be v2 193 + // Must be a valid tile manifest 194 194 const version = detectManifestVersion(raw); 195 - if (version !== 'v2') { 196 - return { success: false, error: 'Only v2 manifests are supported for local installation' }; 195 + if (version !== 'tile') { 196 + return { success: false, error: 'Only tile manifests (manifestVersion: 3) are supported for local installation' }; 197 197 } 198 198 199 199 // Validate ··· 203 203 return { success: false, error: `Invalid manifest: ${msgs}` }; 204 204 } 205 205 206 - const manifest = raw as unknown as TileManifestV2; 206 + const manifest = raw as unknown as TileManifest; 207 207 const id = manifest.id; 208 208 209 209 // Check for conflicts ··· 360 360 /** 361 361 * Synchronize builtin features with the registry at app startup. 362 362 * 363 - * 1. Scan features/ directory for v2 manifests 363 + * 1. Scan features/ directory for tile manifests 364 364 * 2. Register new builtins, update paths for existing ones 365 365 * 3. Remove registry entries for builtins no longer on disk 366 366 * 4. Save registry ··· 382 382 383 383 try { 384 384 const raw = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) as Record<string, unknown>; 385 - if (detectManifestVersion(raw) !== 'v2') continue; 385 + if (detectManifestVersion(raw) !== 'tile') continue; 386 386 const errors = validateTileManifest(raw); 387 387 if (errors.length > 0) continue; 388 388 389 - const manifest = raw as unknown as TileManifestV2; 389 + const manifest = raw as unknown as TileManifest; 390 390 onDiskIds.add(manifest.id); 391 391 392 392 const existing = registry.getFeature(manifest.id);
+10 -10
backend/electron/ipc.ts
··· 527 527 } 528 528 } 529 529 530 - // Exported reference to the window-open handler so other IPC surfaces (like v2 tile 530 + // Exported reference to the window-open handler so other IPC surfaces (like tile 531 531 // IPC) can delegate to it without duplicating ~1300 lines of window creation logic. 532 532 // Populated by registerWindowHandlers() at startup. 533 533 // Use `any` for parameters to match the original implicit-any ergonomics of the ··· 607 607 // blur → hide(), and the NSPanel's floating-level order race 608 608 // leaves the panel rendered even though Electron reports 609 609 // isVisible=false. See commit 1b568d40 for original fix; this 610 - // was lost in the tiles v2 refactor. 610 + // was lost in the tile system refactor. 611 611 (existingWindow.window as { __lastShowTime?: number }).__lastShowTime = Date.now(); 612 612 existingWindow.window.show(); 613 613 existingWindow.window.focus(); ··· 712 712 713 713 // Pseudo-tile lookup: settings / diagnostic / hud-overlay / cmd panel / 714 714 // chain editor / about:blank — URLs that need tile-preload but don't 715 - // ship a v2 manifest. Registry is in pseudo-tiles.ts; see comments 715 + // ship a tile manifest. Registry is in pseudo-tiles.ts; see comments 716 716 // there for extension. Match handles hash/query fragments so URLs 717 717 // restored from session snapshots still resolve. 718 718 if (!tileWebPrefs) { ··· 950 950 951 951 // Create new window. 952 952 // 953 - // For real v2 feature tile URLs (tileWebPrefs resolved from the 953 + // For real feature tile URLs (tileWebPrefs resolved from the 954 954 // manifest, NOT a special-case trustedBuiltin synthesis and NOT a 955 955 // canvas page host), route through `createTileBrowserWindow` — the 956 956 // same helper `launchTile` uses. This is Phase D of the tile- ··· 963 963 // Canvas page hosts, trustedBuiltin special cases (settings, 964 964 // diagnostic, hud-overlay, cmd-ui, about:blank, datastore-viewer), 965 965 // and plain web pages still go through the direct `new 966 - // BrowserWindow(winOptions)` path — they're not v2 feature tiles 966 + // BrowserWindow(winOptions)` path — they're not feature tiles 967 967 // and unifying them is a separate cleanup. 968 - const isRealV2TileLaunch = 968 + const isRealTileLaunch = 969 969 tileWebPrefs !== null && 970 970 !specialCaseToken && 971 971 !useCanvas && 972 972 tileWebPrefs.token !== undefined; 973 973 let win: BrowserWindow; 974 - if (isRealV2TileLaunch && tileWebPrefs) { 974 + if (isRealTileLaunch && tileWebPrefs) { 975 975 // Pull webPreferences overrides out so the helper's baseline tile 976 976 // webPreferences (preload, additionalArguments, session, sandbox) 977 977 // remain in control. Anything else the caller asked for (webviewTag, ··· 1023 1023 if (specialCaseToken && tileWebPrefs?.tileId && tileWebPrefs?.entryId) { 1024 1024 registerTrustedBuiltinWindow(tileWebPrefs.tileId, tileWebPrefs.entryId, win, specialCaseToken); 1025 1025 DEBUG && console.log(`[window-open] registered ${tileWebPrefs.tileId}:${tileWebPrefs.entryId} with tile broadcaster`); 1026 - } else if (!isRealV2TileLaunch && tileWebPrefs?.tileId && tileWebPrefs?.entryId && tileWebPrefs?.token) { 1026 + } else if (!isRealTileLaunch && tileWebPrefs?.tileId && tileWebPrefs?.entryId && tileWebPrefs?.token) { 1027 1027 // Feature tile window-entry opened via api.window.open(): register the 1028 1028 // BrowserWindow with the tile broadcaster so GLOBAL-scope pubsub messages 1029 1029 // reach it. Without this, the window gets tile-preload + a valid token ··· 1034 1034 // the subscribe-side handler never fires. See pubsub-repro.spec.ts for 1035 1035 // a minimal repro. 1036 1036 // 1037 - // Gate on !isRealV2TileLaunch — when the helper above created the 1037 + // Gate on !isRealTileLaunch — when the helper above created the 1038 1038 // window it already called tileWindows.set + wired the close 1039 1039 // handler, so this branch would double-register. Only reached now 1040 1040 // for legacy/edge paths that still go through `new BrowserWindow`. 1041 1041 registerTrustedBuiltinWindow(tileWebPrefs.tileId, tileWebPrefs.entryId, win, tileWebPrefs.token); 1042 - DEBUG && console.log(`[window-open] registered v2 tile window ${tileWebPrefs.tileId}:${tileWebPrefs.entryId} with tile broadcaster`); 1042 + DEBUG && console.log(`[window-open] registered tile window ${tileWebPrefs.tileId}:${tileWebPrefs.entryId} with tile broadcaster`); 1043 1043 } 1044 1044 1045 1045 // Track window position for display-watcher home display restoration
+18 -18
backend/electron/main.ts
··· 74 74 let _sessionRestoreDone = false; 75 75 76 76 // Tiles that must load eagerly (not lazy) — needed at startup. 77 - // Passed to the v2 tile-compat path via `eagerIds`; tile-compat checks 77 + // Passed to the tile-compat path via `eagerIds`; tile-compat checks 78 78 // this set in addition to the manifest `resident: true` flag. 79 79 const EAGER_TILE_IDS = new Set(['entities']); 80 80 ··· 171 171 // Without this, core renderers (cmd, hud, page) launching with 172 172 // tile-preload would silently drop their `tile:validate-token` and 173 173 // `tile:pubsub:*` IPC sends — handlers were historically registered 174 - // lazily from `loadV2Tile()` which runs after core glue. 174 + // lazily from `loadTile()` which runs after core glue. 175 175 ensureTileIpcHandlers(); 176 176 177 177 // Phase 8 — install dev-mode bypass detectors. These monkey-patch ··· 229 229 // bgWindow hosts app/index.js which runs cmd/page/hud registries and is a 230 230 // first-class pubsub subscriber. Every publish must reach it — historical 231 231 // omissions from this loop were directly responsible for "hello-world-only 232 - // commands visible in cmd panel" and "v2 tile cmd:execute:*:result never 232 + // commands visible in cmd panel" and "tile cmd:execute:*:result never 233 233 // reaches core subscribers" bug classes. bgWindow is NOT a tile; it is 234 234 // broadcast to explicitly rather than via getAllTileWindows(). 235 235 const bgWindow = getCoreBackgroundWindow(); ··· 244 244 } 245 245 } 246 246 247 - // Broadcast to v2 tile BrowserWindows (launched by tile-launcher). 248 - // Without this, v2 tiles that subscribe to global pubsub events (editor:open, 247 + // Broadcast to tile BrowserWindows (launched by tile-launcher). 248 + // Without this, tiles that subscribe to global pubsub events (editor:open, 249 249 // item:created, etc.) never receive them. 250 250 // 251 251 // Skip bgWindow if it appears in the tile registry — it is registered there ··· 646 646 } 647 647 648 648 /** 649 - * Discover and register built-in features (each feature is a tile in v2). 649 + * Discover and register built-in features (each feature is a tile). 650 650 */ 651 651 export function discoverBuiltinFeatures(featuresDir: string): void { 652 652 const discovered = discoverFeatures(featuresDir); ··· 768 768 769 769 770 770 /** 771 - * Load all enabled features. Each feature is a tile in v2. 771 + * Load all enabled features. Each feature is a tile. 772 772 * - Builtin features (cmd/hud/page core renderers) launch via core-glue. 773 - * - Feature tiles in `features/` launch via the v2 tile launcher (eager 773 + * - Feature tiles in `features/` launch via the tile launcher (eager 774 774 * if `resident: true` or in `EAGER_TILE_IDS`, otherwise lazy stubs). 775 775 */ 776 776 export async function loadFeatures(): Promise<number> { ··· 803 803 } 804 804 805 805 // ── Feature Startup ── 806 - // Run the feature-startup pipeline: discover, sync registry, and load v2 tile manifests. 806 + // Run the feature-startup pipeline: discover, sync registry, and load tile manifests. 807 807 // This runs AFTER initCmd so tile-lazy's cmd:register-batch publishes reach cmd. 808 808 try { 809 809 const featureResult: FeatureStartupResult = initializeFeatures({ ··· 846 846 } 847 847 848 848 // Core renderers that are always considered "running" once the app has started. 849 - // These are not v2 feature tiles (no tile manifest in features/) but the 849 + // These are not feature tiles (no tile manifest in features/) but the 850 850 // Features pane should always show them as running. 851 851 const CORE_RENDERER_IDS = ['cmd', 'hud', 'page']; 852 852 ··· 869 869 }); 870 870 } 871 871 872 - // Add v2 tiles launched by the tile-launcher as separate BrowserWindows. 872 + // Add tiles launched by the tile-launcher as separate BrowserWindows. 873 873 for (const tileId of getLoadedTileIds()) { 874 874 if (running.some(r => r.id === tileId)) continue; 875 875 if (!isBuiltinFeatureEnabled(tileId)) continue; ··· 893 893 const result: Array<{ id: string; manifest: unknown; status: string }> = []; 894 894 895 895 // Build a set of running tile IDs for status info. 896 - // Sources: core renderers (always up) and loaded v2 tiles. 896 + // Sources: core renderers (always up) and loaded tiles. 897 897 const runningSet = new Set<string>(CORE_RENDERER_IDS); 898 898 for (const tileId of getLoadedTileIds()) { 899 899 runningSet.add(tileId); ··· 941 941 return null; 942 942 } 943 943 944 - // Relaunch as v2 tile: re-read manifest, revoke old token, close old window, launch fresh. 944 + // Relaunch tile: re-read manifest, revoke old token, close old window, launch fresh. 945 945 const result = await relaunchTile(tileId); 946 946 if (!result) { 947 947 return null; ··· 1003 1003 return loadDevTile(tileId); 1004 1004 } 1005 1005 1006 - // Relaunch as v2 tile: re-read manifest, revoke old token, close old window, launch fresh. 1006 + // Relaunch tile: re-read manifest, revoke old token, close old window, launch fresh. 1007 1007 return relaunchTile(tileId); 1008 1008 } 1009 1009 ··· 1193 1193 * Shutdown the application 1194 1194 * 1195 1195 * Fires the `app:shutdown` pubsub event, then broadcasts `tile:lifecycle:shutdown` 1196 - * to every live v2 tile and waits one grace period so tile `api.onShutdown()` 1196 + * to every live tile and waits one grace period so tile `api.onShutdown()` 1197 1197 * callbacks can run before the BrowserWindows are closed. Finally closes the 1198 1198 * database. 1199 1199 * ··· 1227 1227 let backgroundWindow: BrowserWindow | null = null; 1228 1228 1229 1229 /** 1230 - * Launch the core background renderer via v2 tile mechanics. 1230 + * Launch the core background renderer via tile mechanics. 1231 1231 * 1232 1232 * Post-Phase-2.5-#1, the core background window uses `tile-preload.cjs` 1233 1233 * with a `createTrustedBuiltinGrant('core')` token — same shape as cmd, ··· 1277 1277 // Devtools for the background window (dev-profile only, not tests / 1278 1278 // headless). The core renderer was traditionally helpful to inspect 1279 1279 // for command registration + pubsub traffic while debugging; keeping 1280 - // the same affordance post-v2. 1280 + // the same affordance. 1281 1281 if (config.isDev && !isTestProfile() && !isHeadless()) { 1282 1282 win.webContents.openDevTools({ mode: 'detach', activate: false }); 1283 1283 } ··· 1286 1286 } 1287 1287 1288 1288 // Legacy `setWindowOpenHandler` child-window path removed in Phase 2.5 1289 - // #1. Under v2, `api.window.open(...)` routes through `tile:window:open` 1289 + // #1. `api.window.open(...)` routes through `tile:window:open` 1290 1290 // (handled in tile-ipc.ts) and delegates to the centralised window-open 1291 1291 // IPC handler. Native `window.open()` calls from the core background 1292 1292 // HTML never happen — the renderer has no `<a target="_blank">` or
+10 -10
backend/electron/manifest-cache.ts
··· 29 29 30 30 import { getDb } from './datastore.js'; 31 31 import { parseManifestFile, resolveCapabilities } from './tile-manifest.js'; 32 - import type { TileManifestV2, CapabilityGrant } from './tile-manifest.js'; 32 + import type { TileManifest, CapabilityGrant } from './tile-manifest.js'; 33 33 34 34 // --------------------------------------------------------------------------- 35 35 // Row shape ··· 38 38 export interface ManifestCacheRow { 39 39 featureId: string; 40 40 version: string; 41 - manifest: TileManifestV2; 41 + manifest: TileManifest; 42 42 commands: ManifestCommandDecl[]; 43 43 nouns: ManifestNounDecl[]; 44 44 shortcuts: ManifestShortcutDecl[]; ··· 97 97 if (!parseResult) { 98 98 throw new Error(`[manifest-cache] ${featureId}: manifest missing or invalid at ${manifestPath}`); 99 99 } 100 - if (parseResult.version !== 'v2' || !parseResult.v2) { 101 - throw new Error(`[manifest-cache] ${featureId}: v1 manifests not cached`); 100 + if (parseResult.version !== 'tile' || !parseResult.manifest) { 101 + throw new Error(`[manifest-cache] ${featureId}: legacy manifests not cached`); 102 102 } 103 - const manifest = parseResult.v2; 103 + const manifest = parseResult.manifest; 104 104 if (manifest.id !== featureId) { 105 105 throw new Error(`[manifest-cache] manifest id mismatch: file=${featureId} manifest=${manifest.id}`); 106 106 } ··· 296 296 return { 297 297 featureId: raw.featureId, 298 298 version: raw.version, 299 - manifest: safeJson<TileManifestV2>(raw.manifest, null as unknown as TileManifestV2), 299 + manifest: safeJson<TileManifest>(raw.manifest, null as unknown as TileManifest), 300 300 commands: safeJson<ManifestCommandDecl[]>(raw.commands ?? '[]', []), 301 301 nouns: safeJson<ManifestNounDecl[]>(raw.nouns ?? '[]', []), 302 302 shortcuts: safeJson<ManifestShortcutDecl[]>(raw.shortcuts ?? '[]', []), ··· 315 315 } 316 316 } 317 317 318 - function extractCommands(manifest: TileManifestV2): ManifestCommandDecl[] { 318 + function extractCommands(manifest: TileManifest): ManifestCommandDecl[] { 319 319 if (!Array.isArray(manifest.commands)) return []; 320 320 return manifest.commands.map(cmd => ({ 321 321 name: cmd.name, ··· 329 329 })); 330 330 } 331 331 332 - function extractNouns(manifest: TileManifestV2): ManifestNounDecl[] { 332 + function extractNouns(manifest: TileManifest): ManifestNounDecl[] { 333 333 // Manifests don't yet declare nouns statically — nouns come from 334 334 // runtime `registerNoun()` calls. Phase C will extend the manifest 335 335 // schema to allow static noun declarations so browse/list commands ··· 339 339 return Array.isArray(nouns) ? nouns : []; 340 340 } 341 341 342 - function extractShortcuts(manifest: TileManifestV2): ManifestShortcutDecl[] { 342 + function extractShortcuts(manifest: TileManifest): ManifestShortcutDecl[] { 343 343 if (!Array.isArray(manifest.shortcuts)) return []; 344 344 return manifest.shortcuts.map(sc => ({ 345 345 keys: sc.keys, ··· 348 348 })); 349 349 } 350 350 351 - function extractLazyEvents(manifest: TileManifestV2): Array<{ topic: string; command: string }> { 351 + function extractLazyEvents(manifest: TileManifest): Array<{ topic: string; command: string }> { 352 352 const out: Array<{ topic: string; command: string }> = []; 353 353 if (!Array.isArray(manifest.tiles)) return out; 354 354 for (const tile of manifest.tiles) {
+3 -3
backend/electron/protocol.ts
··· 45 45 { get: (_t, prop) => (getElectron().net as any)[prop] } 46 46 ); 47 47 48 - // Tile path cache: tileId -> filesystem path. Populated when a v2 tile 48 + // Tile path cache: tileId -> filesystem path. Populated when a tile 49 49 // is registered (tile-compat) or when a dev tile is loaded via 50 50 // --load-extension. Used by the per-tile origin resolver below 51 51 // (`peek://{tileId}/...`) and by anyone needing a tile's on-disk path. ··· 615 615 const tilePathSuffix = pathname || 'background.html'; 616 616 const absolutePath = path.resolve(tileBasePath, tilePathSuffix); 617 617 618 - // A v2 tile origin is identified by having either a loaded manifest 618 + // A tile origin is identified by having either a loaded manifest 619 619 // (eager tiles) or a registered lazy manifest (not yet instantiated). 620 620 const tileManifest = getTileManifest(host); 621 621 const isTileOrigin = tileManifest !== null || isLazyTileRegistered(host); ··· 632 632 response = await fetchFile(absolutePath, req.url); 633 633 } 634 634 635 - // CSP injection for v2 tile origins. 635 + // CSP injection for tile origins. 636 636 // Apply to EVERY response regardless of status or Content-Type so 637 637 // error responses (404/500), redirects (3xx), and sub-resources 638 638 // (JS/CSS/fonts) cannot be used to escape the tile's declared
+2 -2
backend/electron/pseudo-tiles.ts
··· 2 2 * Pseudo-tile registry — central table of URL → trustedBuiltin tile binding. 3 3 * 4 4 * Pseudo-tiles are URLs that need tile-preload + a trustedBuiltin grant but 5 - * don't ship a v2 manifest.json (settings, cmd panel, HUD overlay, etc.). 5 + * don't ship a manifest.json (settings, cmd panel, HUD overlay, etc.). 6 6 * Historically each entry was a copy-pasted `if (url === '…') { … }` block 7 7 * in `ipc.ts` — this module replaces that ladder with a single lookup. 8 8 * ··· 10 10 * dedicated tileId + entryId so capability grants and IPC sender-frame 11 11 * bindings stay distinct. 12 12 * 13 - * Long-term: fold these into the v2 manifest registry (one manifest.json 13 + * Long-term: fold these into the manifest registry (one manifest.json 14 14 * per pseudo-tile under `app/`) so discovery is fully uniform. This table 15 15 * is the intermediate step from "hardcoded if ladder" to that. 16 16 */
+2 -2
backend/electron/pubsub.test.ts
··· 131 131 pubsub.setPubsubBroadcaster(() => {}); 132 132 }); 133 133 134 - it('broadcaster receives a v2-tile publish of cmd:execute:*:result so bgWindow-subscriber dispatchers can resolve', () => { 135 - // This is the Phase-1 regression guard. A v2 tile publishes a command 134 + it('broadcaster receives a tile publish of cmd:execute:*:result so bgWindow-subscriber dispatchers can resolve', () => { 135 + // This is the Phase-1 regression guard. A tile publishes a command 136 136 // result topic. The main.ts broadcaster callback is responsible for 137 137 // forwarding this to bgWindow (core), which is where the dispatcher 138 138 // that set `resultTopic` lives. If the broadcaster is not invoked
+1 -1
backend/electron/pubsub.ts
··· 135 135 } 136 136 137 137 // Route to renderer windows. In Phase 5 this was gated on GLOBAL scope; 138 - // with scope gone, every publish routes to the broadcaster so v2 tile 138 + // with scope gone, every publish routes to the broadcaster so tile 139 139 // windows and bgWindow subscribers see the event. 140 140 if (pubsubBroadcaster) { 141 141 pubsubBroadcaster(topic, msg, source);
+1 -1
backend/electron/session.ts
··· 432 432 // Legacy form: peek://ext/<id>/... 433 433 extIdFromUrl = u.pathname.split('/').filter(Boolean)[0] ?? null; 434 434 } else if (u.host !== 'app') { 435 - // v2 canonical: peek://<id>/... 435 + // Canonical tile URL: peek://<id>/... 436 436 extIdFromUrl = u.host; 437 437 } 438 438 }
+3 -3
backend/electron/settings-defaults.test.ts
··· 30 30 import { fileURLToPath } from 'node:url'; 31 31 32 32 import { resolveSettingDefault } from './tile-settings-defaults.js'; 33 - import type { TileManifestV2 } from './tile-manifest.js'; 33 + import type { TileManifest } from './tile-manifest.js'; 34 34 35 35 const __dirname = path.dirname(fileURLToPath(import.meta.url)); 36 36 const TEST_DB_PATH = path.join(__dirname, 'test-settings-defaults.db'); ··· 39 39 40 40 // ─── Helpers ───────────────────────────────────────────────────────── 41 41 42 - function makeManifest(id: string, defaults?: Record<string, unknown>): TileManifestV2 { 42 + function makeManifest(id: string, defaults?: Record<string, unknown>): TileManifest { 43 43 return { 44 44 manifestVersion: 3, 45 45 id, ··· 56 56 * key with no declared default surfaces as `{ value: null }`. 57 57 */ 58 58 function readWithFallback( 59 - manifest: TileManifestV2, 59 + manifest: TileManifest, 60 60 tileId: string, 61 61 key: string 62 62 ): { value: unknown } {
+3 -3
backend/electron/tile-command-registration.test.ts
··· 1 1 /** 2 - * Unit tests for v2 tile command registration plumbing. 2 + * Unit tests for tile command registration plumbing. 3 3 * 4 4 * Phase E rewrote the dispatch machinery — per-command stubs are gone, 5 5 * replaced by a single dispatch hook. These tests cover what survives: ··· 26 26 __setTileLazyLauncherForTest, 27 27 __resetTileLazyStateForTest, 28 28 } from './tile-lazy.js'; 29 - import type { TileManifestV2, TileCommand } from './tile-manifest.js'; 29 + import type { TileManifest, TileCommand } from './tile-manifest.js'; 30 30 import type { TileLaunchOptions, TileLaunchResult } from './tile-launcher.js'; 31 31 32 32 // ─── Test Launcher ──────────────────────────────────────────────────── ··· 74 74 return new Promise(resolve => setImmediate(resolve)); 75 75 } 76 76 77 - function makeManifestWithCommands(id: string, commands: TileCommand[]): TileManifestV2 { 77 + function makeManifestWithCommands(id: string, commands: TileCommand[]): TileManifest { 78 78 return { 79 79 manifestVersion: 3, 80 80 id,
+9 -9
backend/electron/tile-compat.ts
··· 1 1 /** 2 2 * Tile loader bridge — registers `tile:*` IPC handlers and routes a 3 - * parsed v2 tile manifest to either the eager `launchTile()` path or 3 + * parsed tile manifest to either the eager `launchTile()` path or 4 4 * the lazy registration path in `tile-lazy.ts`. 5 5 */ 6 6 7 - import { type TileManifestV2 } from './tile-manifest.js'; 7 + import { type TileManifest } from './tile-manifest.js'; 8 8 import { launchTile, type TileLaunchResult } from './tile-launcher.js'; 9 9 import { registerLazyTile } from './tile-lazy.js'; 10 10 import { registerTileIpcHandlers } from './tile-ipc.js'; ··· 14 14 // ─── Types ─────────────────────────────────────────────────────────── 15 15 16 16 /** 17 - * A tile annotated with its parsed v2 manifest, ready for loading. 17 + * A tile annotated with its parsed manifest, ready for loading. 18 18 */ 19 19 export interface AnnotatedTile { 20 20 id: string; 21 21 path: string; 22 - v2Manifest: TileManifestV2; 22 + manifest: TileManifest; 23 23 } 24 24 25 25 // ─── State ─────────────────────────────────────────────────────────── ··· 29 29 /** 30 30 * Ensure the `tile:*` ipcMain handlers are registered. Idempotent. 31 31 * 32 - * Historically called lazily from `loadV2Tile()`, so when trustedBuiltin 33 - * core renderers (cmd, hud, page) launched before any v2 feature tile, 32 + * Historically called lazily from `loadTile()`, so when trustedBuiltin 33 + * core renderers (cmd, hud, page) launched before any feature tile, 34 34 * the handlers weren't yet registered and every 35 35 * `ipcRenderer.send('tile:pubsub:publish', …)` silently dropped. Core 36 36 * renderers and any other caller that needs tile IPC up front should ··· 56 56 } 57 57 58 58 /** 59 - * Load a v2 tile. 59 + * Load a tile. 60 60 * 61 61 * Registers the tile's path with the protocol handler (for peek://{tileId}/ URLs), 62 62 * then either launches it eagerly or registers it for lazy loading. 63 63 */ 64 - export function loadV2Tile( 64 + export function loadTile( 65 65 tile: AnnotatedTile, 66 66 config: TileCompatConfig 67 67 ): TileLaunchResult | null { ··· 74 74 // Register with protocol handler so peek://{tileId}/ URLs resolve 75 75 registerTilePath(tile.id, tile.path); 76 76 77 - const manifest = tile.v2Manifest; 77 + const manifest = tile.manifest; 78 78 const isEager = config.eagerIds?.has(tile.id) ?? false; 79 79 80 80 // A tile is resident if any entry declares `resident: true`.
+2 -2
backend/electron/tile-csp.ts
··· 3 3 * 4 4 * Generates CSP headers based on tile manifest capabilities. 5 5 * The protocol handler calls buildCSPForTile() when serving HTML 6 - * files for v2 tile manifests. 6 + * files for tile manifests. 7 7 */ 8 8 9 - import type { TileCapabilities, TileManifestV2 } from './tile-manifest.js'; 9 + import type { TileCapabilities, TileManifest } from './tile-manifest.js'; 10 10 11 11 // ─── CSP Directive Builders ────────────────────────────────────────── 12 12
+2 -2
backend/electron/tile-feature-state.ts
··· 2 2 * Tile feature state — discovery and enabled-state helpers 3 3 * 4 4 * Thin filesystem + datastore queries that the rest of the backend uses 5 - * to enumerate features (each feature is a tile in v2) and check whether 5 + * to enumerate features and check whether 6 6 * a builtin is enabled. The actual tile launch / lifecycle lives in 7 7 * `tile-launcher.ts` / `tile-lifecycle.ts`; manifest parsing lives in 8 - * `tile-manifest.ts` (returns the strict v3 `TileManifestV2` shape). 8 + * `tile-manifest.ts` (returns the strict v3 `TileManifest` shape). 9 9 * 10 10 * Returned manifest objects here are deliberately untyped (`unknown`) — 11 11 * callers that need typed access go through `tile-manifest.ts`. This
+7 -7
backend/electron/tile-ipc.ts
··· 86 86 import type { TableName } from '../types/index.js'; 87 87 import { installFromBundle } from './feature-installer.js'; 88 88 import { resolveCapabilities, validateTileManifest, detectManifestVersion } from './tile-manifest.js'; 89 - import type { CapabilityGrant, TileCapabilities } from './tile-manifest.js'; 89 + import type { CapabilityGrant, TileCapabilities, TileManifest } from './tile-manifest.js'; 90 90 import { invokeWindowOpen, popupToOpener, reopenLastClosedWindow, getLastFocusedVisibleWindowId, getLastContentWindowId, clearLastContentWindowId, getDarkModeSetting, setDarkModeSetting, applyDarkModeSetting, persistAdBlockerPref } from './ipc.js'; 91 91 import { 92 92 getWindowInfo, ··· 1225 1225 try { 1226 1226 const manifestPath = path.join(entry.path, 'manifest.json'); 1227 1227 const parsed = parseManifestFile(manifestPath); 1228 - if (parsed?.version === 'v2' && parsed.v2) { 1229 - const residentTile = parsed.v2.tiles.find(t => t.resident === true); 1228 + if (parsed?.version === 'tile' && parsed.manifest) { 1229 + const residentTile = parsed.manifest.tiles.find(t => t.resident === true); 1230 1230 if (residentTile && !isTileLoaded(args.id, residentTile.id)) { 1231 1231 launchTile({ 1232 1232 tilePath: entry.path, 1233 - manifest: parsed.v2, 1233 + manifest: parsed.manifest, 1234 1234 preloadPath: args.tilePreloadPath, 1235 1235 entryId: residentTile.id, 1236 1236 }); ··· 1864 1864 } 1865 1865 1866 1866 const version = detectManifestVersion(raw); 1867 - if (version !== 'v2') errors.push(`Manifest is ${version}, expected v2`); 1867 + if (version !== 'tile') errors.push(`Manifest is not a valid tile manifest (got: ${version})`); 1868 1868 1869 1869 const validationErrors = validateTileManifest(raw); 1870 1870 for (const ve of validationErrors) errors.push(`${ve.path}: ${ve.message}`); ··· 2300 2300 2301 2301 try { 2302 2302 const manifest = entry.path ? parseManifestFile(path.join(entry.path, 'manifest.json')) : null; 2303 - // settingsSchema lives under v2 for tile manifests; fall back to raw for v1 extensions. 2303 + // settingsSchema lives on the parsed manifest; fall back to raw for legacy extensions. 2304 2304 const settingsSchema: string | undefined = 2305 - manifest?.v2?.settingsSchema ?? (manifest?.raw?.settingsSchema as string | undefined); 2305 + manifest?.manifest?.settingsSchema ?? (manifest?.raw?.settingsSchema as string | undefined); 2306 2306 if (!settingsSchema) return { success: true, data: null }; 2307 2307 2308 2308 const schemaPath = path.join(entry.path, settingsSchema);
+16 -16
backend/electron/tile-launcher.ts
··· 46 46 } 47 47 48 48 import { 49 - type TileManifestV2, 49 + type TileManifest, 50 50 type TileEntry, 51 51 type TileCapabilities, 52 52 type CapabilityGrant, ··· 107 107 const tileWindows = new Map<string, BrowserWindow>(); 108 108 109 109 /** Loaded manifests: tileId -> manifest */ 110 - const loadedManifests = new Map<string, TileManifestV2>(); 110 + const loadedManifests = new Map<string, TileManifest>(); 111 111 112 112 /** Tiles that have signaled ready */ 113 113 const readyTiles = new Set<string>(); ··· 126 126 export interface TileLaunchOptions { 127 127 /** Path to the tile's directory on disk */ 128 128 tilePath: string; 129 - /** The parsed v2 manifest */ 130 - manifest: TileManifestV2; 129 + /** The parsed manifest */ 130 + manifest: TileManifest; 131 131 /** Path to the tile preload script */ 132 132 preloadPath: string; 133 133 /** Which tile entry to launch (defaults to first) */ ··· 173 173 * manifest; callers that have no manifest (e.g. trustedBuiltin callers 174 174 * doing post-hoc registration) can omit this. 175 175 */ 176 - manifest?: TileManifestV2; 176 + manifest?: TileManifest; 177 177 entry?: TileEntry; 178 178 /** 179 179 * Extra BrowserWindow options applied after manifest hints. Used by the ··· 223 223 // trustedBuiltin callers that never ran through the feature 224 224 // registry (hud-glue, cmd-glue, etc.) the entry starts in 225 225 // UNREGISTERED and this call is rejected. That's fine; the FSM is 226 - // only authoritative for v2 feature tiles today, and a silent 226 + // only authoritative for feature tiles today, and a silent 227 227 // rejection on trustedBuiltin entries doesn't harm drift detection. 228 228 tileLifecycle.transition(tileId, entryId, tileLifecycle.TRIGGERS.LOAD, 229 229 { source: 'createTileBrowserWindow' }); ··· 554 554 /** 555 555 * Get the manifest for a loaded tile 556 556 */ 557 - export function getTileManifest(tileId: string): TileManifestV2 | null { 557 + export function getTileManifest(tileId: string): TileManifest | null { 558 558 return loadedManifests.get(tileId) || null; 559 559 } 560 560 ··· 567 567 568 568 /** 569 569 * Get all BrowserWindow instances for loaded tiles. 570 - * Used by the pubsub broadcaster to forward messages to v2 tile windows. 570 + * Used by the pubsub broadcaster to forward messages to tile windows. 571 571 */ 572 572 export function getAllTileWindows(): BrowserWindow[] { 573 573 return Array.from(tileWindows.values()).filter(win => !win.isDestroyed()); ··· 639 639 export function getTileWebPreferencesForUrl( 640 640 url: string, 641 641 tilePreloadPath: string, 642 - lookupLazyManifest: (tileId: string) => TileManifestV2 | null, 642 + lookupLazyManifest: (tileId: string) => TileManifest | null, 643 643 ): { preload: string; additionalArguments: string[]; tileId: string; entryId: string; token?: string } | null { 644 644 let host: string; 645 645 let pathPart: string; ··· 689 689 } 690 690 691 691 /** 692 - * Resolve a peek:// URL to v2 tile web preferences for a `<webview>` child. 692 + * Resolve a peek:// URL to tile web preferences for a `<webview>` child. 693 693 * 694 694 * Like `getTileWebPreferencesForUrl`, but treats child URLs that don't 695 695 * correspond to a declared tile entry as belonging to the parent tile's ··· 702 702 * Resolution rules: 703 703 * - URL must be peek:// 704 704 * - Tile-id (URL host, or first path segment under `peek://ext/` for legacy compat) must 705 - * match a registered v2 manifest 705 + * match a registered tile manifest 706 706 * - If a tile entry's `url` matches the path, mint a token for that 707 707 * specific entry (same as `getTileWebPreferencesForUrl`) 708 708 * - Otherwise, fall back to the manifest's first tile entry — webview ··· 714 714 export function getTileWebPreferencesForWebviewUrl( 715 715 url: string, 716 716 tilePreloadPath: string, 717 - lookupLazyManifest: (tileId: string) => TileManifestV2 | null, 717 + lookupLazyManifest: (tileId: string) => TileManifest | null, 718 718 ): { preload: string; additionalArguments: string[]; tileId: string; entryId: string } | null { 719 719 let host: string; 720 720 let pathPart: string; ··· 852 852 // Re-read manifest from disk so any changes (e.g. during dev reload) are picked up. 853 853 const manifestPath = path.join(tilePath, 'manifest.json'); 854 854 const parsed = parseManifestFile(manifestPath); 855 - if (!parsed || parsed.version !== 'v2' || !parsed.v2) { 856 - console.error(`[tile-launcher] relaunchTile: cannot re-read v2 manifest for ${tileId} at ${manifestPath}`); 855 + if (!parsed || parsed.version !== 'tile' || !parsed.manifest) { 856 + console.error(`[tile-launcher] relaunchTile: cannot re-read tile manifest for ${tileId} at ${manifestPath}`); 857 857 return null; 858 858 } 859 - const manifest = parsed.v2; 859 + const manifest = parsed.manifest; 860 860 861 861 // Determine which entry to (re)launch — use the first entry, matching the 862 - // pattern used in loadV2Tile / tile-compat. 862 + // pattern used in loadTile / tile-compat. 863 863 const entry = manifest.tiles[0]; 864 864 if (!entry) { 865 865 console.error(`[tile-launcher] relaunchTile: manifest for ${tileId} has no tile entries`);
+2 -2
backend/electron/tile-lazy-events.test.ts
··· 22 22 __setTileLazyLauncherForTest, 23 23 __resetTileLazyStateForTest, 24 24 } from './tile-lazy.js'; 25 - import type { TileManifestV2 } from './tile-manifest.js'; 25 + import type { TileManifest } from './tile-manifest.js'; 26 26 import type { TileLaunchOptions, TileLaunchResult } from './tile-launcher.js'; 27 27 28 28 // ─── Test Launcher ──────────────────────────────────────────────────── ··· 76 76 77 77 // ─── Manifest Fixtures ──────────────────────────────────────────────── 78 78 79 - function makeManifest(id: string, lazyEvents: string[]): TileManifestV2 { 79 + function makeManifest(id: string, lazyEvents: string[]): TileManifest { 80 80 return { 81 81 manifestVersion: 3, 82 82 id,
+2 -2
backend/electron/tile-lazy-timeout.test.ts
··· 28 28 __setTileLazyLauncherForTest, 29 29 __resetTileLazyStateForTest, 30 30 } from './tile-lazy.js'; 31 - import type { TileManifestV2 } from './tile-manifest.js'; 31 + import type { TileManifest } from './tile-manifest.js'; 32 32 import type { TileLaunchOptions, TileLaunchResult } from './tile-launcher.js'; 33 33 34 34 // ─── Fake launcher that never reaches ready ────────────────────────── ··· 68 68 69 69 // ─── Manifest fixture ───────────────────────────────────────────────── 70 70 71 - function makeCommandManifest(id: string, cmdName: string): TileManifestV2 { 71 + function makeCommandManifest(id: string, cmdName: string): TileManifest { 72 72 return { 73 73 manifestVersion: 3, 74 74 id,
+7 -7
backend/electron/tile-lazy.ts
··· 1 1 /** 2 2 * Lazy Loading for Tiles — load-on-dispatch 3 3 * 4 - * A lazy v2 tile is registered at boot with its manifest + launch 4 + * A lazy tile is registered at boot with its manifest + launch 5 5 * config, but no BrowserWindow is created. The tile's declared 6 6 * commands are announced to the cmd panel via `cmd:register-batch` so 7 7 * it can list them; its background process boots the first time it ··· 40 40 type TileLaunchOptions, 41 41 type TileLaunchResult, 42 42 } from './tile-launcher.js'; 43 - import { type TileManifestV2, type TileCommand } from './tile-manifest.js'; 43 + import { type TileManifest, type TileCommand } from './tile-manifest.js'; 44 44 import { DEBUG, isDevProfile } from './config.js'; 45 45 import * as tileLifecycle from './tile-lifecycle.js'; 46 46 ··· 91 91 92 92 /** Tiles registered for lazy loading: tileId -> launch options (without window) */ 93 93 const lazyTileRegistry = new Map<string, { 94 - manifest: TileManifestV2; 94 + manifest: TileManifest; 95 95 tilePath: string; 96 96 preloadPath: string; 97 97 }>(); ··· 218 218 * intercept point for load-on-dispatch. 219 219 */ 220 220 export function registerLazyTile( 221 - manifest: TileManifestV2, 221 + manifest: TileManifest, 222 222 tilePath: string, 223 223 preloadPath: string, 224 224 ): void { ··· 258 258 ); 259 259 } 260 260 261 - function collectLazyEventTopics(manifest: TileManifestV2): string[] { 261 + function collectLazyEventTopics(manifest: TileManifest): string[] { 262 262 const topics = new Set<string>(); 263 263 if (!Array.isArray(manifest.tiles)) return []; 264 264 for (const tile of manifest.tiles) { ··· 591 591 592 592 /** 593 593 * Get the manifest for a lazy-registered tile (without forcing load). 594 - * Used by the window-open IPC handler to recognise v2 tile URLs 594 + * Used by the window-open IPC handler to recognise tile URLs 595 595 * before the tile has actually booted. 596 596 */ 597 - export function getLazyTileManifest(tileId: string): TileManifestV2 | null { 597 + export function getLazyTileManifest(tileId: string): TileManifest | null { 598 598 return lazyTileRegistry.get(tileId)?.manifest || null; 599 599 } 600 600
+17 -17
backend/electron/tile-loader.ts
··· 18 18 import { FeatureRegistry, type FeatureRegistryEntry } from './feature-registry.js'; 19 19 import { 20 20 parseManifestFile, 21 - type TileManifestV2, 21 + type TileManifest, 22 22 type ParsedManifest, 23 23 } from './tile-manifest.js'; 24 - import { type AnnotatedTile, loadV2Tile, type TileCompatConfig } from './tile-compat.js'; 24 + import { type AnnotatedTile, loadTile, type TileCompatConfig } from './tile-compat.js'; 25 25 import * as manifestCache from './manifest-cache.js'; 26 26 import { DEBUG } from './config.js'; 27 27 ··· 53 53 * 54 54 * For each registered feature: 55 55 * - Reads manifest from entry.path/manifest.json 56 - * - Detects v1 vs v2 57 - * - V2: passes to tile launcher with granted capabilities 58 - * - V1: calls onV1Feature callback for legacy handling 56 + * - Detects legacy vs current tile manifest 57 + * - Current: passes to tile launcher with granted capabilities 58 + * - Legacy: surfaces a clear error (reinstall or update manifest) 59 59 * 60 60 * Returns results for each feature attempted. 61 61 */ ··· 90 90 try { 91 91 const cached = manifestCache.get(entry.id); 92 92 if (cached && cached.manifest) { 93 - // The cache stores the full parsed v2 manifest JSON; it IS both 94 - // the validated `v2` view and the `raw` view as far as downstream 95 - // consumers care (they only touch v2 fields when version is 'v2'). 93 + // The cache stores the full parsed tile manifest JSON; it IS both 94 + // the validated `manifest` view and the `raw` view as far as downstream 95 + // consumers care (they only touch manifest fields when version is 'tile'). 96 96 return { 97 - version: 'v2', 98 - v2: cached.manifest, 97 + version: 'tile', 98 + manifest: cached.manifest, 99 99 raw: cached.manifest as unknown as Record<string, unknown>, 100 100 }; 101 101 } ··· 138 138 }; 139 139 } 140 140 141 - if (parsed.version === 'v2' && parsed.v2) { 142 - return loadV2Feature(entry, parsed.v2, config); 141 + if (parsed.version === 'tile' && parsed.manifest) { 142 + return loadTileFromEntry(entry, parsed.manifest, config); 143 143 } 144 144 145 145 // Legacy (pre-v3 manifestVersion) — rejected by parseManifestFile in ··· 152 152 } 153 153 154 154 /** 155 - * Load a v2 feature via the tile compat layer 155 + * Load a tile feature via the tile compat layer 156 156 */ 157 - function loadV2Feature( 157 + function loadTileFromEntry( 158 158 entry: FeatureRegistryEntry, 159 - manifest: TileManifestV2, 159 + manifest: TileManifest, 160 160 config: TileLoaderConfig 161 161 ): FeatureLoadResult { 162 162 // Skip features that haven't been user-approved (non-builtin) ··· 172 172 const annotated: AnnotatedTile = { 173 173 id: entry.id, 174 174 path: entry.path, 175 - v2Manifest: manifest, 175 + manifest, 176 176 }; 177 177 178 178 const compatConfig: TileCompatConfig = { ··· 181 181 }; 182 182 183 183 try { 184 - loadV2Tile(annotated, compatConfig); 184 + loadTile(annotated, compatConfig); 185 185 return { id: entry.id, loaded: true }; 186 186 } catch (err) { 187 187 const message = err instanceof Error ? err.message : String(err);
+44 -44
backend/electron/tile-manifest.test.ts
··· 1 1 /** 2 - * Unit tests for Tile Manifest v2 — parsing, validation, capability resolution 2 + * Unit tests for Tile Manifest — parsing, validation, capability resolution 3 3 */ 4 4 5 5 import { describe, it } from 'node:test'; ··· 15 15 16 16 // ─── Fixtures ──────────────────────────────────────────────────────── 17 17 18 - const VALID_V2_MANIFEST = { 18 + const VALID_TILE_MANIFEST = { 19 19 manifestVersion: 3, 20 20 id: 'test-tile', 21 21 name: 'Test Tile', ··· 59 59 // ─── Version Detection ─────────────────────────────────────────────── 60 60 61 61 describe('detectManifestVersion', () => { 62 - it('should detect v2 by manifestVersion field (current schema = 3)', () => { 63 - assert.strictEqual(detectManifestVersion({ manifestVersion: 3 }), 'v2'); 62 + it('should detect tile by manifestVersion field (current schema = 3)', () => { 63 + assert.strictEqual(detectManifestVersion({ manifestVersion: 3 }), 'tile'); 64 64 }); 65 65 66 - it('should detect v2 by tiles + capabilities', () => { 66 + it('should detect tile by tiles + capabilities', () => { 67 67 assert.strictEqual( 68 68 detectManifestVersion({ tiles: [], capabilities: {} }), 69 - 'v2' 69 + 'tile' 70 70 ); 71 71 }); 72 72 73 - it('should detect v1 for legacy manifest', () => { 73 + it('should detect legacy for legacy manifest', () => { 74 74 assert.strictEqual( 75 75 detectManifestVersion(VALID_V1_MANIFEST as Record<string, unknown>), 76 - 'v1' 76 + 'legacy' 77 77 ); 78 78 }); 79 79 80 - it('should detect v1 when only tiles present (no capabilities)', () => { 80 + it('should detect legacy when only tiles present (no capabilities)', () => { 81 81 assert.strictEqual( 82 82 detectManifestVersion({ tiles: [] }), 83 - 'v1' 83 + 'legacy' 84 84 ); 85 85 }); 86 86 87 - it('should detect v1 when only capabilities present (no tiles)', () => { 87 + it('should detect legacy when only capabilities present (no tiles)', () => { 88 88 assert.strictEqual( 89 89 detectManifestVersion({ capabilities: {} }), 90 - 'v1' 90 + 'legacy' 91 91 ); 92 92 }); 93 93 94 - it('should detect v1 for empty object', () => { 95 - assert.strictEqual(detectManifestVersion({}), 'v1'); 94 + it('should detect legacy for empty object', () => { 95 + assert.strictEqual(detectManifestVersion({}), 'legacy'); 96 96 }); 97 97 98 - it('should detect v1 for manifestVersion 1', () => { 99 - assert.strictEqual(detectManifestVersion({ manifestVersion: 1 }), 'v1'); 98 + it('should detect legacy for manifestVersion 1', () => { 99 + assert.strictEqual(detectManifestVersion({ manifestVersion: 1 }), 'legacy'); 100 100 }); 101 101 102 - it('should detect v1 for legacy manifestVersion 2 (pre-v3 compat fields)', () => { 103 - assert.strictEqual(detectManifestVersion({ manifestVersion: 2 }), 'v1'); 102 + it('should detect legacy for legacy manifestVersion 2 (pre-v3 compat fields)', () => { 103 + assert.strictEqual(detectManifestVersion({ manifestVersion: 2 }), 'legacy'); 104 104 }); 105 105 }); 106 106 107 107 // ─── Validation ────────────────────────────────────────────────────── 108 108 109 109 describe('validateTileManifest', () => { 110 - it('should pass for valid v2 manifest', () => { 111 - const errors = validateTileManifest(VALID_V2_MANIFEST as Record<string, unknown>); 110 + it('should pass for valid tile manifest', () => { 111 + const errors = validateTileManifest(VALID_TILE_MANIFEST as Record<string, unknown>); 112 112 assert.strictEqual(errors.length, 0); 113 113 }); 114 114 115 115 it('should reject missing id', () => { 116 - const manifest = { ...VALID_V2_MANIFEST, id: undefined }; 116 + const manifest = { ...VALID_TILE_MANIFEST, id: undefined }; 117 117 const errors = validateTileManifest(manifest as Record<string, unknown>); 118 118 assert.ok(errors.some(e => e.path === 'id')); 119 119 }); 120 120 121 121 it('should reject missing name', () => { 122 - const manifest = { ...VALID_V2_MANIFEST, name: undefined }; 122 + const manifest = { ...VALID_TILE_MANIFEST, name: undefined }; 123 123 const errors = validateTileManifest(manifest as Record<string, unknown>); 124 124 assert.ok(errors.some(e => e.path === 'name')); 125 125 }); 126 126 127 127 it('should reject missing tiles array', () => { 128 - const manifest = { ...VALID_V2_MANIFEST, tiles: 'not-array' }; 128 + const manifest = { ...VALID_TILE_MANIFEST, tiles: 'not-array' }; 129 129 const errors = validateTileManifest(manifest as Record<string, unknown>); 130 130 assert.ok(errors.some(e => e.path === 'tiles')); 131 131 }); ··· 134 134 const tiles = Array.from({ length: 11 }, (_, i) => ({ 135 135 id: `tile-${i}`, url: 'index.html', 136 136 })); 137 - const manifest = { ...VALID_V2_MANIFEST, tiles }; 137 + const manifest = { ...VALID_TILE_MANIFEST, tiles }; 138 138 const errors = validateTileManifest(manifest as Record<string, unknown>); 139 139 assert.ok(errors.some(e => e.path === 'tiles' && e.message.includes('maximum'))); 140 140 }); ··· 144 144 { id: 'dup', url: 'a.html' }, 145 145 { id: 'dup', url: 'b.html', resident: true }, 146 146 ]; 147 - const manifest = { ...VALID_V2_MANIFEST, tiles }; 147 + const manifest = { ...VALID_TILE_MANIFEST, tiles }; 148 148 const errors = validateTileManifest(manifest as Record<string, unknown>); 149 149 assert.ok(errors.some(e => e.message.includes('duplicate'))); 150 150 }); 151 151 152 152 it('should reject removed tile.type field (compat dropped in manifestVersion 3)', () => { 153 153 const tiles = [{ id: 'bad', type: 'background', url: 'index.html' }]; 154 - const manifest = { ...VALID_V2_MANIFEST, tiles }; 154 + const manifest = { ...VALID_TILE_MANIFEST, tiles }; 155 155 const errors = validateTileManifest(manifest as Record<string, unknown>); 156 156 assert.ok(errors.some(e => e.path.endsWith('.type'))); 157 157 }); 158 158 159 159 it('should reject removed tile.windowHints field (compat dropped in manifestVersion 3)', () => { 160 160 const tiles = [{ id: 'bad', url: 'index.html', windowHints: { width: 100 } }]; 161 - const manifest = { ...VALID_V2_MANIFEST, tiles }; 161 + const manifest = { ...VALID_TILE_MANIFEST, tiles }; 162 162 const errors = validateTileManifest(manifest as Record<string, unknown>); 163 163 assert.ok(errors.some(e => e.path.endsWith('.windowHints'))); 164 164 }); 165 165 166 166 it('should reject tile without url', () => { 167 167 const tiles = [{ id: 'no-url' }]; 168 - const manifest = { ...VALID_V2_MANIFEST, tiles }; 168 + const manifest = { ...VALID_TILE_MANIFEST, tiles }; 169 169 const errors = validateTileManifest(manifest as Record<string, unknown>); 170 170 assert.ok(errors.some(e => e.message.includes('url'))); 171 171 }); 172 172 173 173 it('should reject missing capabilities object', () => { 174 - const manifest = { ...VALID_V2_MANIFEST, capabilities: null }; 174 + const manifest = { ...VALID_TILE_MANIFEST, capabilities: null }; 175 175 const errors = validateTileManifest(manifest as Record<string, unknown>); 176 176 assert.ok(errors.some(e => e.path === 'capabilities')); 177 177 }); 178 178 179 179 it('should reject invalid pubsub scope', () => { 180 180 const manifest = { 181 - ...VALID_V2_MANIFEST, 181 + ...VALID_TILE_MANIFEST, 182 182 capabilities: { pubsub: { scopes: ['invalid'] } }, 183 183 }; 184 184 const errors = validateTileManifest(manifest as Record<string, unknown>); ··· 188 188 it('should reject too many network domains', () => { 189 189 const domains = Array.from({ length: 51 }, (_, i) => `domain${i}.com`); 190 190 const manifest = { 191 - ...VALID_V2_MANIFEST, 191 + ...VALID_TILE_MANIFEST, 192 192 capabilities: { network: { domains } }, 193 193 }; 194 194 const errors = validateTileManifest(manifest as Record<string, unknown>); ··· 197 197 198 198 it('should reject commands without name', () => { 199 199 const manifest = { 200 - ...VALID_V2_MANIFEST, 200 + ...VALID_TILE_MANIFEST, 201 201 commands: [{ action: { type: 'execute' } }], 202 202 }; 203 203 const errors = validateTileManifest(manifest as Record<string, unknown>); ··· 206 206 207 207 it('should reject commands without action', () => { 208 208 const manifest = { 209 - ...VALID_V2_MANIFEST, 209 + ...VALID_TILE_MANIFEST, 210 210 commands: [{ name: 'test' }], 211 211 }; 212 212 const errors = validateTileManifest(manifest as Record<string, unknown>); ··· 214 214 }); 215 215 216 216 it('should accept empty tiles array', () => { 217 - const manifest = { ...VALID_V2_MANIFEST, tiles: [] }; 217 + const manifest = { ...VALID_TILE_MANIFEST, tiles: [] }; 218 218 const errors = validateTileManifest(manifest as Record<string, unknown>); 219 219 assert.strictEqual(errors.length, 0); 220 220 }); 221 221 222 222 it('should accept manifest with no commands', () => { 223 - const { commands, ...manifest } = VALID_V2_MANIFEST; 223 + const { commands, ...manifest } = VALID_TILE_MANIFEST; 224 224 const errors = validateTileManifest(manifest as Record<string, unknown>); 225 225 assert.strictEqual(errors.length, 0); 226 226 }); ··· 229 229 // ─── Parsing ───────────────────────────────────────────────────────── 230 230 231 231 describe('parseManifestJSON', () => { 232 - it('should parse valid v2 manifest', () => { 233 - const result = parseManifestJSON(VALID_V2_MANIFEST as Record<string, unknown>); 232 + it('should parse valid tile manifest', () => { 233 + const result = parseManifestJSON(VALID_TILE_MANIFEST as Record<string, unknown>); 234 234 assert.ok(result); 235 - assert.strictEqual(result.version, 'v2'); 236 - assert.ok(result.v2); 237 - assert.strictEqual(result.v2.id, 'test-tile'); 235 + assert.strictEqual(result.version, 'tile'); 236 + assert.ok(result.manifest); 237 + assert.strictEqual(result.manifest.id, 'test-tile'); 238 238 }); 239 239 240 - it('should parse v1 manifest', () => { 240 + it('should parse legacy manifest', () => { 241 241 const result = parseManifestJSON(VALID_V1_MANIFEST as Record<string, unknown>); 242 242 assert.ok(result); 243 - assert.strictEqual(result.version, 'v1'); 244 - assert.strictEqual(result.v2, undefined); 243 + assert.strictEqual(result.version, 'legacy'); 244 + assert.strictEqual(result.manifest, undefined); 245 245 }); 246 246 247 - it('should return null for invalid v2 manifest', () => { 247 + it('should return null for invalid tile manifest', () => { 248 248 const result = parseManifestJSON({ 249 249 manifestVersion: 3, 250 250 // missing required fields
+27 -27
backend/electron/tile-manifest.ts
··· 1 1 /** 2 - * Tile Manifest v2 Schema and Parser 2 + * Tile Manifest Schema and Parser 3 3 * 4 4 * Handles: 5 - * - TypeScript types for the v2 tile manifest format 6 - * - Detection of v1 vs v2 manifests 7 - * - Parsing and validation of v2 manifests 5 + * - TypeScript types for the tile manifest format 6 + * - Detection of legacy vs current manifests 7 + * - Parsing and validation of manifests 8 8 * - Capability grant resolution (manifest declares needs, runtime validates) 9 9 */ 10 10 ··· 400 400 } 401 401 402 402 /** 403 - * Command definition in v2 manifest (same structure as v1 for compat) 403 + * Command definition in tile manifest 404 404 */ 405 405 export interface TileCommand { 406 406 name: string; ··· 426 426 } 427 427 428 428 /** 429 - * Shortcut definition in v2 manifest 429 + * Shortcut definition in tile manifest 430 430 */ 431 431 export interface TileShortcut { 432 432 keys: string; ··· 441 441 * Tile Manifest format. Current schema version is 3; legacy `type` and 442 442 * `windowHints` compat fields are no longer accepted by the validator. 443 443 */ 444 - export interface TileManifestV2 { 444 + export interface TileManifest { 445 445 /** Manifest version — must be 3 for tile manifests */ 446 446 manifestVersion: 3; 447 447 /** Unique tile ID */ ··· 485 485 /** 486 486 * Result of manifest detection 487 487 */ 488 - export type ManifestVersion = 'v1' | 'v2'; 488 + export type ManifestVersion = 'legacy' | 'tile'; 489 489 490 490 /** 491 - * Parsed manifest result — either v1 or v2 491 + * Parsed manifest result — either legacy or current tile manifest 492 492 */ 493 493 export interface ParsedManifest { 494 494 version: ManifestVersion; 495 495 /** Original raw JSON */ 496 496 raw: Record<string, unknown>; 497 - /** Only set if version is 'v2' */ 498 - v2?: TileManifestV2; 497 + /** Only set if version is 'tile' */ 498 + manifest?: TileManifest; 499 499 } 500 500 501 501 // ─── Capability Grant ──────────────────────────────────────────────── ··· 553 553 // ─── Detection ─────────────────────────────────────────────────────── 554 554 555 555 /** 556 - * Detect whether a manifest JSON is v1 (legacy extension) or v2 (tile). 556 + * Detect whether a manifest JSON is legacy or a current tile manifest. 557 557 * 558 - * The v2 architecture's schema is currently version 3. v2 manifests are 558 + * The current tile manifest schema is version 3. Tile manifests are 559 559 * identified by having: 560 560 * - `manifestVersion: 3`, OR 561 561 * - Both `tiles` array AND `capabilities` object present ··· 565 565 * treat them as legacy and force authors to migrate (lockstep cutover). 566 566 */ 567 567 export function detectManifestVersion(json: Record<string, unknown>): ManifestVersion { 568 - if (json.manifestVersion === 3) return 'v2'; 569 - if (json.manifestVersion === 2) return 'v1'; 568 + if (json.manifestVersion === 3) return 'tile'; 569 + if (json.manifestVersion === 2) return 'legacy'; 570 570 if (Array.isArray(json.tiles) && typeof json.capabilities === 'object' && json.capabilities !== null) { 571 - return 'v2'; 571 + return 'tile'; 572 572 } 573 - return 'v1'; 573 + return 'legacy'; 574 574 } 575 575 576 576 // ─── Validation ────────────────────────────────────────────────────── ··· 584 584 } 585 585 586 586 /** 587 - * Validate a v2 tile manifest 587 + * Validate a tile manifest 588 588 * Returns array of errors (empty = valid) 589 589 */ 590 590 export function validateTileManifest(json: Record<string, unknown>): ValidationError[] { ··· 940 940 const raw = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) as Record<string, unknown>; 941 941 const version = detectManifestVersion(raw); 942 942 943 - if (version === 'v1') { 944 - return { version: 'v1', raw }; 943 + if (version === 'legacy') { 944 + return { version: 'legacy', raw }; 945 945 } 946 946 947 - // Validate v2 947 + // Validate tile manifest 948 948 const errors = validateTileManifest(raw); 949 949 if (errors.length > 0) { 950 950 console.error(`[tile-manifest] Validation errors in ${manifestPath}:`); ··· 954 954 return null; 955 955 } 956 956 957 - const v2 = raw as unknown as TileManifestV2; 958 - return { version: 'v2', raw, v2 }; 957 + const manifest = raw as unknown as TileManifest; 958 + return { version: 'tile', raw, manifest }; 959 959 } catch (err) { 960 960 const message = err instanceof Error ? err.message : String(err); 961 961 console.error(`[tile-manifest] Failed to parse ${manifestPath}:`, message); ··· 969 969 export function parseManifestJSON(json: Record<string, unknown>): ParsedManifest | null { 970 970 const version = detectManifestVersion(json); 971 971 972 - if (version === 'v1') { 973 - return { version: 'v1', raw: json }; 972 + if (version === 'legacy') { 973 + return { version: 'legacy', raw: json }; 974 974 } 975 975 976 976 const errors = validateTileManifest(json); ··· 978 978 return null; 979 979 } 980 980 981 - const v2 = json as unknown as TileManifestV2; 982 - return { version: 'v2', raw: json, v2 }; 981 + const manifest = json as unknown as TileManifest; 982 + return { version: 'tile', raw: json, manifest }; 983 983 } 984 984 985 985 // ─── Capability Resolution ───────────────────────────────────────────
+7 -7
backend/electron/tile-preload.cts
··· 28 28 const tileToken = getArg('--tile-token='); 29 29 30 30 if (!tileId || !tileEntry || !tileToken) { 31 - console.error('[tile-preload] Missing tile arguments. This preload is only for v2 tiles.'); 31 + console.error('[tile-preload] Missing tile arguments. This preload is only for tiles.'); 32 32 // Expose minimal error API 33 33 contextBridge.exposeInMainWorld('app', { 34 34 error: 'Tile preload misconfigured: missing tile-id, tile-entry, or tile-token', ··· 197 197 // ── PubSub (if granted) ── 198 198 // 199 199 // Publish supports two call shapes: 200 - // api.publish(topic, data) // v1-style 201 - // api.pubsub.publish(topic, data) // v2-style (kept for clarity) 200 + // api.publish(topic, data) // flat 201 + // api.pubsub.publish(topic, data) // namespaced (kept for clarity) 202 202 // 203 203 // The legacy SYSTEM / SELF / GLOBAL scope param is gone (see 204 204 // docs/pubsub-state-machine.md). Privilege is enforced by the ··· 518 518 /** 519 519 * Flush any pending command registrations immediately. 520 520 * 521 - * In the v2 tile path, commands are published synchronously via 522 - * ipcRenderer.send() in register() — there is no batch queue. 523 - * This method exists for API compatibility with v1 callers that 524 - * called flush() after a burst of register() calls; it is a no-op. 521 + * Commands are published synchronously via ipcRenderer.send() in 522 + * register() — there is no batch queue. This method exists for API 523 + * compatibility with legacy callers that called flush() after a burst 524 + * of register() calls; it is a no-op. 525 525 */ 526 526 flush: (): Promise<{ success: true }> => { 527 527 // No-op: register() already sends each cmd:register publish
+2 -2
backend/electron/tile-settings-defaults.test.ts
··· 26 26 resolveSettingDefault, 27 27 loadSchemaDefaults, 28 28 } from './tile-settings-defaults.js'; 29 - import type { TileManifestV2 } from './tile-manifest.js'; 29 + import type { TileManifest } from './tile-manifest.js'; 30 30 31 31 // ─── Helpers ───────────────────────────────────────────────────────── 32 32 ··· 34 34 id: string, 35 35 defaults?: Record<string, unknown>, 36 36 settingsSchema?: string 37 - ): TileManifestV2 { 37 + ): TileManifest { 38 38 return { 39 39 manifestVersion: 3, 40 40 id,
+3 -3
backend/electron/tile-settings-defaults.ts
··· 11 11 * 1. `loadSchemaDefaults(tilePath, settingsSchemaRelPath)` — reads the 12 12 * schema file from disk and returns its `defaults` object (or null). 13 13 * Called once at tile-load time; the resulting object is cached on 14 - * the in-memory manifest via `TileManifestV2.defaults`. 14 + * the in-memory manifest via `TileManifest.defaults`. 15 15 * 2. `resolveSettingDefault(manifest, key)` — pure lookup against 16 16 * `manifest.defaults`. Returns the default value or `undefined` 17 17 * when none is declared. Supports both flat keys ··· 24 24 import fs from 'node:fs'; 25 25 import path from 'node:path'; 26 26 27 - import type { TileManifestV2 } from './tile-manifest.js'; 27 + import type { TileManifest } from './tile-manifest.js'; 28 28 29 29 // ─── Schema Loading ────────────────────────────────────────────────── 30 30 ··· 87 87 * "no default declared" from "the declared default is `null`". 88 88 */ 89 89 export function resolveSettingDefault( 90 - manifest: TileManifestV2 | null | undefined, 90 + manifest: TileManifest | null | undefined, 91 91 key: string 92 92 ): unknown | undefined { 93 93 if (!manifest || !manifest.defaults) return undefined;
+1 -1
backend/electron/tile-window-strict.test.ts
··· 3 3 * 4 4 * Covers the pure-helper `checkWindowAllowed` used by the 5 5 * `tile:window:open` / `close` / `list` / `info` IPC handlers in 6 - * `tile-ipc.ts`. Prior to this, v2 tiles could open arbitrary windows 6 + * `tile-ipc.ts`. Prior to this, tiles could open arbitrary windows 7 7 * via the un-gated `window-open` channel without declaring the 8 8 * `window` capability. 9 9 *
+3 -3
docs/architecture.md
··· 37 37 - Holds a per-window **capability token** that the main process mints at 38 38 load time and revokes at unload 39 39 40 - The v1 era — `preload.js`, an `extensionHostWindow` that loaded multiple 40 + The legacy era — `preload.js`, an `extensionHostWindow` that loaded multiple 41 41 features as iframes, and a parallel set of legacy IPC channels — was removed 42 42 in April 2026 ([docs/v1-removal-plan.md](./v1-removal-plan.md), Status: 43 - DONE). There is no longer a v1↔v2 seam. 43 + DONE). The tile path is the only path. 44 44 45 45 ### Manifest schema (v3) 46 46 ··· 250 250 `windowRegistry.params.placement` and never re-derived from flag soup 251 251 (`center: true`, explicit `x/y`, `useCanvas`, etc.). All consumers — the 252 252 URL-reuse and keepLive-reuse paths in `ipc.ts`, the fresh-open fallback, 253 - the display-watcher second pass, and the v2-tile open path in 253 + the display-watcher second pass, and the tile open path in 254 254 `tile-ipc.ts` — call the same `computePlacement(input)` and apply its 255 255 `reposition` / `no-change` result. Stranded-rescue (<50% area on any 256 256 display) is built in for every mode. Unit-tested with synthetic display
+5 -5
docs/command-audit.md
··· 1 - # Command Audit — v2 Tile Migration 1 + # Command Audit — Tile Migration 2 2 3 3 Snapshot generated from `/Users/dietrich/misc/mpeek/features/*/manifest.json` on 4 4 the overnight audit pass. 5 5 6 6 ## Summary 7 7 8 - - 72 commands across 29 v2 manifests 8 + - 72 commands across 29 manifests 9 9 - 1 critical runtime bug (FIXED) 10 10 - 5 preload API mismatches between v1 `preload.js` and `tile-preload.ts` (FIXED 11 11 with v1-compat shims) ··· 18 18 ### 1. `tile:window:open` was a stub 19 19 20 20 `backend/electron/tile-ipc.ts` used to return `{success: true, url}` without 21 - actually opening anything. Every `window`-type command from a v2 tile 21 + actually opening anything. Every `window`-type command from a tile 22 22 (20+ commands) silently failed. 23 23 24 24 Fix: exposed `invokeWindowOpen` from `ipc.ts` and have the tile handler call ··· 152 152 153 153 ## Remaining Known Gaps 154 154 155 - These are not command-level bugs but v2 API coverage issues that will surface 155 + These are not command-level bugs but tile API coverage issues that will surface 156 156 once specific features are exercised. All are marked as explicit 157 157 "not implemented" rather than silently returning success, so failures are 158 158 loud. ··· 161 161 `api.datastore.*` v1-compat shims give features the same access they had 162 162 under v1. 163 163 - `api.web.*` — if any feature uses `window.app.web.*` it will error; currently 164 - unused by v2 features audited. 164 + unused by features audited. 165 165 - `api.network.fetch` — the capability-gated fetch is wired, but features 166 166 typically use standard `fetch()` directly. No regressions observed. 167 167
+1 -1
docs/core-extraction-plan.md
··· 2 2 3 3 ## Context 4 4 5 - During the tiles v2 migration, we identified that cmd, page, and hud aren't features — they're platform infrastructure. Features are things users can install, enable, disable, remove, publish, and distribute. cmd/page/hud are prerequisites for Peek existing. 5 + During the tiles migration, we identified that cmd, page, and hud aren't features — they're platform infrastructure. Features are things users can install, enable, disable, remove, publish, and distribute. cmd/page/hud are prerequisites for Peek existing. 6 6 7 7 This document plans moving them out of `features/` and into `./app/` (cross-platform frontend code), with thin backend-specific glue where needed. 8 8
+1 -1
docs/pubsub-state-machine.md
··· 475 475 `extensionBroadcaster` → `pubsubBroadcaster` (the "extension" label 476 476 is a stale v1 term — Peek now reserves "extension" for bundled 477 477 Chromium extensions; features/tiles are the current vocabulary). 478 - This alone unblocks the "only hello-world visible" and "v2 result 478 + This alone unblocks the "only hello-world visible" and "tile result 479 479 doesn't reach subscribers" bugs. No semantic changes. 480 480 **Test**: tag/untag/widget-update smoke tests go green. 481 481 2. **Phase 2 — Sender-frame cross-check (security hardening).**
+2 -2
docs/tile-api.md
··· 1 1 # Tile API Reference (Phase 4 — strict surface only) 2 2 3 - This document lists the API surfaces available to v2 tiles after Phase 4 shim removal. 3 + This document lists the API surfaces available to tiles after Phase 4 shim removal. 4 4 All surfaces are injected via `contextBridge.exposeInMainWorld('app', api)` from 5 - `backend/electron/tile-preload.ts`. Deprecated v1-compat shims are gone; calling any 5 + `backend/electron/tile-preload.ts`. Deprecated compat shims are gone; calling any 6 6 removed name throws `Error('[tile-preload] ... removed in Phase 4; use ...')`. 7 7 8 8 ---
+4 -4
docs/tile-preload-trimming-matrix.md
··· 1 1 # Tile API Usage Matrix 2 2 3 - Generated by `scripts/audit-tile-caps.js`. Input: every v2 feature manifest + source files. 3 + Generated by `scripts/audit-tile-caps.js`. Input: every feature manifest + source files. 4 4 5 5 Features scanned: 28 6 6 ··· 381 381 382 382 ## Plan corrections 383 383 384 - The numbers below were produced by this audit script (reading every `.js/.mjs/.cjs/.ts/.html` file under v2 feature directories). They differ from the hand-counts in `docs/tile-preload-trimming-plan.md` in a few places. Noted here as follow-ups — do not retroactively change the plan. 384 + The numbers below were produced by this audit script (reading every `.js/.mjs/.cjs/.ts/.html` file under feature directories). They differ from the hand-counts in `docs/tile-preload-trimming-plan.md` in a few places. Noted here as follow-ups — do not retroactively change the plan. 385 385 386 386 - **`api.subscribe`**: plan said 198 calls across 71 files. Audit finds **184 calls across 27 features** (feature-count not file-count, so not directly comparable). Close enough; trivial. 387 387 - **`api.publish`**: plan said 147 calls across 58 files. Audit finds **145 calls across 27 features**. Close. ··· 400 400 - **Extended `api.window.*` missing surfaces**: plan said "~15 sites". Audit finds many more. Non-{open, close, getInfo} calls: `setTitle` (1), `list` (5: entities 1, groups 1, tags 1, windows 1, helpdocs effectively 0 but various others), `exists` (7), `show` (5), `hide` (3), `getFocusedVisibleWindowId` (6), `setOverlayFocusTarget` (2), `getBounds` (1), `setIgnoreMouseEvents` (2), `center` (1), `centerAll` (1), `fullscreen` (1), `maximize` (1), `setVisibleOnAllWorkspaces` (1). **Correction: ~37 call sites to missing window surfaces, not ~15.** The `windows` feature is the heaviest user (9 missing-surface calls); `spaces` (5), `groups` (5), `helpdocs` (5), `slides` (4), `pagestream` (3) also need migration. 401 401 - **`api.network`**: audit finds **0 call sites** across features, even though 5 features declare the `network` capability (entities, features-manager, feeds, lex, websearch, wonderwall). Network calls may use `fetch()` directly in the renderer rather than the capability-gated `api.network.fetch()` wrapper — worth a follow-up audit. 402 402 - **`api.filesystem`**: audit finds **0 call sites**. `files` feature declares the capability but uses `api.files.*` (the dialog shim), not `api.filesystem.read/write`. 403 - - **`api.theme`**: audit finds **0 call sites** in any v2 feature. No theme capability is used by v2 tiles today. 404 - - **`api.sync`**: audit finds **1 call site** (`sync/background.js` calls `api.sync.syncAll`). `api.sync` is NOT currently exposed in `tile-preload.ts` — this is a reference to a missing surface that would need adding if sync is migrated to v2 with capability gating. **Correction: treat `api.sync.syncAll` as a missing surface, not a shim.** 403 + - **`api.theme`**: audit finds **0 call sites** in any feature. No theme capability is used by tiles today. 404 + - **`api.sync`**: audit finds **1 call site** (`sync/background.js` calls `api.sync.syncAll`). `api.sync` is NOT currently exposed in `tile-preload.ts` — this is a reference to a missing surface that would need adding if sync is migrated with capability gating. **Correction: treat `api.sync.syncAll` as a missing surface, not a shim.** 405 405 - **`api.pubsub.*` (method form)**: audit finds **0 call sites**. Every feature uses the top-level `api.subscribe` / `api.publish` shims rather than the strict `api.pubsub.subscribe/publish` form. This means the top-level shim removal in Phase 4 will require rewriting **all 27 features** that use pubsub at all. (The plan already marks this as "trivial" — audit confirms it's mechanical, just wide.) 406 406 - **Scopes**: audit detects **326 references to `api.scopes.*`** (GLOBAL, SELF, SYSTEM) across 27 features. Plan said "~145 refs" — **actual count is ~2.2× higher**. Most common is `GLOBAL`; `SYSTEM` appears in 27 features (likely from shared boilerplate). Not shimmed, not a removal target — just a count correction. 407 407
+1 -1
docs/tile-preload-trimming-plan.md
··· 2 2 3 3 ## 0. Context and Scope 4 4 5 - The overnight audit added v1-compat shims to `backend/electron/tile-preload.ts` so that 26+ v2 tiles (migrated features) keep working. The shims are a **superset** of the strict capability-gated surface and bypass enforcement: they delegate directly to the legacy `datastore-*`, `registershortcut`, `file-save-dialog`, `context-get`, and `modes:*` handlers in `backend/electron/ipc.ts`, which do not check `CapabilityGrant`. 5 + The overnight audit added v1-compat shims to `backend/electron/tile-preload.ts` so that 26+ migrated features keep working. The shims are a **superset** of the strict capability-gated surface and bypass enforcement: they delegate directly to the legacy `datastore-*`, `registershortcut`, `file-save-dialog`, `context-get`, and `modes:*` handlers in `backend/electron/ipc.ts`, which do not check `CapabilityGrant`. 6 6 7 7 Strict handlers in `backend/electron/tile-ipc.ts` (`tile:datastore:*`, `tile:settings:*`, `tile:theme:*`, `tile:filesystem:*`, `tile:network:fetch`, `tile:window:*`) already call `getGrantForToken(args.token)` and check `hasCapability`, `hasDatastoreTable`, `isNetworkDomainAllowed`, etc. The shims bypass all of that. 8 8
+4 -4
docs/window-placement-refactor.md
··· 160 160 **Files touched:** 161 161 - `backend/electron/main.ts` (registry shape) 162 162 - `backend/electron/ipc.ts` (window-open handler — assign placement) 163 - - `backend/electron/tile-ipc.ts` (v2 tile open path — assign placement) 163 + - `backend/electron/tile-ipc.ts` (tile open path — assign placement) 164 164 - `features/slides/background.js` (pass `screenEdge` in options) 165 165 166 166 **Exit criteria:** ··· 251 251 - Replace the position-computation calls in `tile-ipc.ts` (audit identified 252 252 lines ~2884–2891, ~2919–2926, ~3344) with `computePlacement`. 253 253 - `createTileBrowserWindow` and friends should populate `placement` on the 254 - registered tile params so reuse + display-watcher see consistent data for 255 - v2 tiles. 254 + registered tile params so reuse + display-watcher see consistent data 255 + across tiles. 256 256 257 257 **Files touched:** 258 258 - `backend/electron/tile-ipc.ts` ··· 262 262 - Unit tests pass. 263 263 - No remaining direct `screen.*` position calls in `tile-ipc.ts`. 264 264 265 - **Out of scope:** anything that's not v2 tile positioning. 265 + **Out of scope:** anything that's not tile positioning. 266 266 267 267 --- 268 268
+1 -1
features/editor/manifest.json
··· 75 75 ], 76 76 "settingsSchema": "./settings-schema.json", 77 77 "_notes": { 78 - "lazyEventInterceptors": "The background tile declares `lazyEvents: ['editor:open', 'editor:add']` so tile-lazy.ts registers pubsub stubs that load the tile on first event publish. This replaces the v1 main.ts registerLazyEventInterceptors() path for editor — v2 features declare their lazy-triggering events directly in their manifest and the runtime wires the interceptor stubs via tile-lazy.ts." 78 + "lazyEventInterceptors": "The background tile declares `lazyEvents: ['editor:open', 'editor:add']` so tile-lazy.ts registers pubsub stubs that load the tile on first event publish. Features declare their lazy-triggering events directly in their manifest and the runtime wires the interceptor stubs via tile-lazy.ts." 79 79 } 80 80 }
+2 -2
features/entities/background.html
··· 14 14 15 15 console.log(`[ext:${extId}] background.html loaded`); 16 16 17 - // ── V2 Tile Runtime ── 17 + // ── Tile Runtime ── 18 18 // Strict mode: background.js uses api.pubsub.* / api.context.* directly. 19 - console.log(`[ext:${extId}] initializing v2 tile`); 19 + console.log(`[ext:${extId}] initializing tile`); 20 20 await api.initialize(); 21 21 22 22 // Initialize extension BEFORE signaling ready
+1 -1
features/entities/home.js
··· 1069 1069 console.log('[ext:entities] home.js loaded'); 1070 1070 1071 1071 // Initialize tile — validates capability token with main process 1072 - console.log('[ext:entities] initializing v2 tile'); 1072 + console.log('[ext:entities] initializing tile'); 1073 1073 await api.initialize(); 1074 1074 1075 1075 // Initialize background logic (extraction, commands, pubsub handlers)
+2 -2
features/example/README.md
··· 1 1 # Example Extension - Image Gallery 2 2 3 3 This example extension is the **reference template for new Peek extensions**. 4 - It demonstrates the strict (v2 tile) API shape, which is the supported form 4 + It demonstrates the strict tile API shape, which is the supported form 5 5 for all newly written features. 6 6 7 7 ## Features ··· 31 31 32 32 ## Tile Initialization 33 33 34 - Every v2 tile must call `api.initialize()` before using any capability-gated 34 + Every tile must call `api.initialize()` before using any capability-gated 35 35 APIs — this validates the capability token with the main process. 36 36 `background.html` does this before calling `extension.init()`; any other 37 37 window tile (like `gallery.html`) does the same at the top of its script.
+2 -2
features/example/background.html
··· 14 14 15 15 console.log(`[ext:${extId}] background.html loaded`); 16 16 17 - // ── V2 Tile Runtime ── 17 + // ── Tile Runtime ── 18 18 // Initialize tile — validates capability token with main process 19 - console.log(`[ext:${extId}] initializing v2 tile`); 19 + console.log(`[ext:${extId}] initializing tile`); 20 20 await api.initialize(); 21 21 22 22 // Initialize extension (registers commands)
+1 -1
features/example/background.js
··· 8 8 * - Displaying stored images 9 9 * 10 10 * This is the reference template for new Peek extensions. It uses the 11 - * strict (v2 tile) API shape directly: 11 + * strict tile API shape directly: 12 12 * - api.pubsub.publish / api.pubsub.subscribe (no flat api.publish/subscribe) 13 13 * - api.datastore.* with tables declared in the manifest 14 14 * - api.window.open guarded by the window capability
+3 -3
features/features-manager/PLAN.md
··· 78 78 79 79 ## Manifest 80 80 81 - v2 tile manifest. Feature id: `features-manager`. Builtin, lazy background. 4 tiles: 81 + Tile manifest. Feature id: `features-manager`. Builtin, lazy background. 4 tiles: 82 82 - `background` (background, lazy) 83 83 - `browse` (window, 960x720) 84 84 - `manage` (window, 900x650) ··· 166 166 4. Scaffold is written to the chosen directory: 167 167 ``` 168 168 my-feature/ 169 - manifest.json # v2 manifest with id, name, one background tile, minimal capabilities 169 + manifest.json # manifest with id, name, one background tile, minimal capabilities 170 170 background.html # minimal with api.initialize() call 171 171 background.js # empty init function, ready to add logic 172 172 home.html # optional UI page template ··· 247 247 248 248 ### Manifest Versioning 249 249 250 - The v2 tile manifest already has a `version` field (semver string). This is the version used for update comparisons. When a publisher creates a new `space.peek.feature.release` record with the same `featureId` but a higher `version`, that constitutes an available update. 250 + The tile manifest already has a `version` field (semver string). This is the version used for update comparisons. When a publisher creates a new `space.peek.feature.release` record with the same `featureId` but a higher `version`, that constitutes an available update. 251 251 252 252 The registry entry stores the installed version. Comparing against the latest release record on the publisher's PDS determines if an update is available. 253 253
+2 -2
features/features-manager/background.html
··· 14 14 15 15 console.log(`[ext:${extId}] background.html loaded`); 16 16 17 - // ── V2 Tile Runtime (strict) ── 18 - console.log(`[ext:${extId}] initializing v2 tile`); 17 + // ── Tile Runtime (strict) ── 18 + console.log(`[ext:${extId}] initializing tile`); 19 19 await api.initialize(); 20 20 21 21 if (extension.init) {
+1 -1
features/feeds/background.html
··· 15 15 console.log(`[ext:${extId}] background.html loaded`); 16 16 17 17 // Initialize tile — validates capability token with main process 18 - console.log(`[ext:${extId}] initializing v2 tile`); 18 + console.log(`[ext:${extId}] initializing tile`); 19 19 await api.initialize(); 20 20 21 21 if (extension.init) {
+2 -2
features/files/background.html
··· 14 14 15 15 console.log(`[ext:${extId}] background.html loaded`); 16 16 17 - // ── V2 Tile Runtime ── 18 - console.log(`[ext:${extId}] initializing v2 tile`); 17 + // ── Tile Runtime ── 18 + console.log(`[ext:${extId}] initializing tile`); 19 19 await api.initialize(); 20 20 21 21 if (extension.init) {
+2 -2
features/lex/background.html
··· 14 14 15 15 console.log(`[ext:${extId}] background.html loaded`); 16 16 17 - // ── V2 Tile Runtime (strict) ── 18 - console.log(`[ext:${extId}] initializing v2 tile`); 17 + // ── Tile Runtime (strict) ── 18 + console.log(`[ext:${extId}] initializing tile`); 19 19 await api.initialize(); 20 20 21 21 if (extension.init) {
+2 -2
features/lex/home.js
··· 2012 2012 (async () => { 2013 2013 console.log('[ext:lex] home.js loaded'); 2014 2014 2015 - // Initialize v2 tile — validates capability token with main process 2016 - console.log('[ext:lex] initializing v2 tile'); 2015 + // Initialize tile — validates capability token with main process 2016 + console.log('[ext:lex] initializing tile'); 2017 2017 await api.initialize(); 2018 2018 2019 2019 // Register noun (formerly background.js responsibility)
+2 -2
features/mcp-server/PLAN.md
··· 101 101 ``` 102 102 features/mcp-server/ 103 103 PLAN.md — this file 104 - manifest.json — v2 tile manifest (background only, no UI needed initially) 104 + manifest.json — tile manifest (background only, no UI needed initially) 105 105 background.html/js — tile that manages MCP server config and status 106 106 server.js — the actual MCP server (standalone Node.js, spawned by Claude Code) 107 107 db.js — SQLite read/write helpers (shared by server.js) ··· 189 189 7. MCP resources: `peek://tasks`, `peek://tasks/{tag}` 190 190 191 191 ### Phase 3 — Peek Tile Integration 192 - 8. `manifest.json` — v2 tile manifest (background tile) 192 + 8. `manifest.json` — tile manifest (background tile) 193 193 9. `background.js` — shows MCP server status, provides setup button 194 194 10. Optional: manage UI page showing connected clients, recent tool calls 195 195
+2 -2
features/pagestream/background.html
··· 14 14 15 15 console.log(`[ext:${extId}] background.html loaded`); 16 16 17 - // ── V2 Tile Runtime ── 18 - console.log(`[ext:${extId}] initializing v2 tile`); 17 + // ── Tile Runtime ── 18 + console.log(`[ext:${extId}] initializing tile`); 19 19 await api.initialize(); 20 20 21 21 // Initialize extension BEFORE completing setup
+2 -2
features/pagewidgets-sample/background.html
··· 14 14 15 15 console.log(`[ext:${extId}] background.html loaded`); 16 16 17 - // ── V2 Tile Runtime ── 18 - console.log(`[ext:${extId}] initializing v2 tile`); 17 + // ── Tile Runtime ── 18 + console.log(`[ext:${extId}] initializing tile`); 19 19 await api.initialize(); 20 20 21 21 if (extension.init) {
+2 -2
features/peeks/background.html
··· 14 14 15 15 console.log(`[ext:${extId}] background.html loaded`); 16 16 17 - // ── V2 Tile Runtime ── 17 + // ── Tile Runtime ── 18 18 // Initialize tile — validates capability token with main process 19 - console.log(`[ext:${extId}] initializing v2 tile`); 19 + console.log(`[ext:${extId}] initializing tile`); 20 20 await api.initialize(); 21 21 22 22 // Initialize extension BEFORE publishing ext:ready
+2 -2
features/scripts/background.html
··· 14 14 15 15 console.log(`[ext:${extId}] background.html loaded`); 16 16 17 - // ── V2 Tile Runtime ── 18 - console.log(`[ext:${extId}] initializing v2 tile`); 17 + // ── Tile Runtime ── 18 + console.log(`[ext:${extId}] initializing tile`); 19 19 await api.initialize(); 20 20 21 21 if (extension.init) {
+2 -2
features/search/background.html
··· 14 14 15 15 console.log(`[ext:${extId}] background.html loaded`); 16 16 17 - // ── V2 Tile Runtime ── 18 - console.log(`[ext:${extId}] initializing v2 tile`); 17 + // ── Tile Runtime ── 18 + console.log(`[ext:${extId}] initializing tile`); 19 19 await api.initialize(); 20 20 21 21 if (extension.init) {
+2 -2
features/sheets/background.html
··· 14 14 15 15 console.log(`[ext:${extId}] background.html loaded`); 16 16 17 - // ── V2 Tile Runtime ── 18 - console.log(`[ext:${extId}] initializing v2 tile`); 17 + // ── Tile Runtime ── 18 + console.log(`[ext:${extId}] initializing tile`); 19 19 await api.initialize(); 20 20 21 21 if (extension.init) {
+2 -2
features/slides/background.html
··· 14 14 15 15 console.log(`[ext:${extId}] background.html loaded`); 16 16 17 - // ── V2 Tile Runtime ── 17 + // ── Tile Runtime ── 18 18 // Initialize tile — validates capability token with main process 19 - console.log(`[ext:${extId}] initializing v2 tile`); 19 + console.log(`[ext:${extId}] initializing tile`); 20 20 await api.initialize(); 21 21 22 22 // Initialize extension BEFORE publishing ext:ready
+1 -1
features/spaces/background.html
··· 15 15 console.log(`[ext:${extId}] background.html loaded`); 16 16 17 17 // Initialize tile — validates capability token with main process 18 - console.log(`[ext:${extId}] initializing v2 tile`); 18 + console.log(`[ext:${extId}] initializing tile`); 19 19 await api.initialize(); 20 20 21 21 if (extension.init) {
+2 -2
features/tag-actions/background.html
··· 14 14 15 15 console.log(`[ext:${extId}] background.html loaded`); 16 16 17 - // ── V2 Tile Runtime ── 18 - console.log(`[ext:${extId}] initializing v2 tile`); 17 + // ── Tile Runtime ── 18 + console.log(`[ext:${extId}] initializing tile`); 19 19 await api.initialize(); 20 20 21 21 if (extension.init) {
+2 -2
features/tags/background.html
··· 14 14 15 15 console.log(`[ext:${extId}] background.html loaded`); 16 16 17 - // ── V2 Tile Runtime ── 17 + // ── Tile Runtime ── 18 18 // Initialize tile — validates capability token with main process 19 - console.log(`[ext:${extId}] initializing v2 tile`); 19 + console.log(`[ext:${extId}] initializing tile`); 20 20 await api.initialize(); 21 21 22 22 // Initialize extension (registers commands, shortcuts)
+2 -2
features/timers/background.html
··· 12 12 const api = window.app; 13 13 const extId = extension.id; 14 14 15 - // ── V2 Tile Runtime ── 15 + // ── Tile Runtime ── 16 16 // Initialize tile — validates capability token with main process 17 - console.log(`[ext:${extId}] initializing v2 tile`); 17 + console.log(`[ext:${extId}] initializing tile`); 18 18 await api.initialize(); 19 19 20 20 // Initialize extension BEFORE signaling ready
+1 -1
features/todo/background.html
··· 12 12 const api = window.app; 13 13 const extId = extension.id; 14 14 15 - console.log(`[ext:${extId}] initializing v2 tile`); 15 + console.log(`[ext:${extId}] initializing tile`); 16 16 await api.initialize(); 17 17 18 18 if (extension.init) {
+1 -1
features/websearch/home.js
··· 1106 1106 console.log(`[ext:${extId}] home.js loaded`); 1107 1107 1108 1108 // Initialize tile — validates capability token with main process 1109 - console.log(`[ext:${extId}] initializing v2 tile`); 1109 + console.log(`[ext:${extId}] initializing tile`); 1110 1110 await api.initialize(); 1111 1111 1112 1112 // Load settings and engines
+2 -2
features/windows/background.html
··· 14 14 15 15 console.log(`[ext:${extId}] background.html loaded`); 16 16 17 - // ── V2 Tile Runtime ── 18 - console.log(`[ext:${extId}] initializing v2 tile`); 17 + // ── Tile Runtime ── 18 + console.log(`[ext:${extId}] initializing tile`); 19 19 await api.initialize(); 20 20 21 21 if (extension.init) {
+2 -2
features/wonderwall/background.html
··· 14 14 15 15 console.log(`[ext:${extId}] background.html loaded`); 16 16 17 - // ── V2 Tile Runtime ── 18 - console.log(`[ext:${extId}] initializing v2 tile`); 17 + // ── Tile Runtime ── 18 + console.log(`[ext:${extId}] initializing tile`); 19 19 await api.initialize(); 20 20 21 21 if (extension.init) {
+2 -2
scripts/audit-tile-caps.js
··· 2 2 /** 3 3 * audit-tile-caps.js 4 4 * 5 - * Scans every v2 feature (manifestVersion === 3) under features/ and detects 5 + * Scans every feature (manifestVersion === 3) under features/ and detects 6 6 * all `api.*` call patterns in source files. Produces a markdown matrix of 7 7 * which features use which shim surfaces, with per-method counts. 8 8 * ··· 222 222 const lines = []; 223 223 lines.push('# Tile API Usage Matrix'); 224 224 lines.push(''); 225 - lines.push('Generated by `scripts/audit-tile-caps.js`. Input: every v2 feature manifest + source files.'); 225 + lines.push('Generated by `scripts/audit-tile-caps.js`. Input: every feature manifest + source files.'); 226 226 lines.push(''); 227 227 lines.push(`Features scanned: ${features.length}`); 228 228 lines.push('');