experiments in a post-browser web
10
fork

Configure Feed

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

feat(v1-removal): Phase 4 — manifest schema cleanup (drop type/windowHints, bump to v3)

Drops the legacy compat fields from the tile manifest schema in lockstep:

- `TileEntry.type?: 'window' | 'background' | 'panel'` removed.
- `type: "background"` migrated to `resident: true`.
- `type: "window"` / `type: "panel"` were no-ops; stripped.
- `TileEntry.windowHints?: TileWindowHints` removed; the
`TileWindowHints` interface is gone. Window properties are flat on
`TileEntry` only — every consumer migrated to hoisted fields.
- `manifestVersion` bumped from `2` → `3` everywhere (29 feature
manifests, 2 scaffold templates in `tile-ipc.ts`, audit script,
every unit test fixture, and the hello-world prose example).

Validator behaviour:
- `validateTileManifest` now rejects both `tile.type` and
`tile.windowHints` outright with explicit migration messages — no
normalization layer, no warnings.
- `detectManifestVersion` recognises `manifestVersion: 3` as v2 (the
current architectural namespace) and reclassifies `manifestVersion: 2`
as v1 (legacy). Re-installing an unmigrated v2 feature now fails.

Consumer migrations:
- `tile-launcher.ts`: introduces `TileWindowOverrides` (subset of flat
`TileEntry` window fields) to replace `Partial<TileEntry['windowHints']>`
in `TileLaunchOptions.windowOverrides`. Hint extraction inside
`createTileBrowserWindow` reads the flat fields directly.
- `tile-compat.ts`: residency check is `t.resident === true` — no
`t.type === 'background'` fallback.
- `tile-ipc.ts`: enable-feature handler residency check matches above;
both `feature-dev:scaffold` manifest templates emit v3 shape.
- `tile-lazy.ts`: `find(t => t.type === 'background')` replaced with
`find(t => t.lazy === true) ?? tiles[0]` (lazy entries by convention).
- `convertV1ToV2`: emits `manifestVersion: 3`, sets `resident: !lazy`
on the synthesised background entry.

Docs:
- `docs/tiles-single-file.md`: scaffold example bumped to v3, removed
the "backwards compatibility" section, added a "Migration from v2 → v3"
section, refreshed the runtime-changes section to reflect the
hard-cutover state.
- `docs/v1-removal-plan.md`: status updated from NOT started → DONE.
- `features/features-manager/PLAN.md`: scaffold example bumped to v3.

Tests:
- All ~1700 unit tests passing (588 in tests/unit + 1698 in
dist/backend/electron — no regressions).
- Two test files (`tile-features-strict.test.ts`, `tile-izui-strict.test.ts`)
had `tile-{ id, type: 'background', url }` fixtures migrated to
`{ id, url, resident: true }`. Two new validator tests cover the
rejection of `tile.type` and `tile.windowHints`.

+276 -364
+2 -2
backend/electron/atproto-install-hardening.test.ts
··· 36 36 37 37 function makeManifest(id: string, overrides: Record<string, unknown> = {}): TileManifestV2 { 38 38 return { 39 - manifestVersion: 2, 39 + manifestVersion: 3, 40 40 id, 41 41 name: `Feature ${id}`, 42 42 version: '1.0.0', 43 43 tiles: [ 44 - { id: 'background', type: 'background', url: 'background.html' }, 44 + { id: 'background', url: 'background.html', resident: true }, 45 45 ], 46 46 capabilities: { 47 47 pubsub: { scopes: ['self', 'global'] },
+4 -4
backend/electron/feature-installer.test.ts
··· 38 38 39 39 function makeV2Manifest(id: string, overrides: Record<string, unknown> = {}): Record<string, unknown> { 40 40 return { 41 - manifestVersion: 2, 41 + manifestVersion: 3, 42 42 id, 43 43 name: `Feature ${id}`, 44 44 version: '1.0.0', 45 45 tiles: [ 46 - { id: 'background', type: 'background', url: 'background.html' }, 46 + { id: 'background', url: 'background.html', resident: true }, 47 47 ], 48 48 capabilities: { 49 49 pubsub: { scopes: ['self', 'global'] }, ··· 136 136 137 137 it('should skip invalid manifests', () => { 138 138 const invalidDir = path.join(featuresDir, 'broken'); 139 - writeManifest(invalidDir, { manifestVersion: 2 }); // missing required fields 139 + writeManifest(invalidDir, { manifestVersion: 3 }); // missing required fields 140 140 141 141 const registry = new FeatureRegistry(registryDir); 142 142 registry.loadRegistry(); ··· 239 239 240 240 it('should reject invalid manifests', () => { 241 241 const sourceDir = path.join(tmpDir, 'invalid-source'); 242 - writeManifest(sourceDir, { manifestVersion: 2 }); // missing id, name, etc. 242 + writeManifest(sourceDir, { manifestVersion: 3 }); // missing id, name, etc. 243 243 244 244 const registry = new FeatureRegistry(registryDir); 245 245 registry.loadRegistry();
+1 -1
backend/electron/settings-defaults.test.ts
··· 41 41 42 42 function makeManifest(id: string, defaults?: Record<string, unknown>): TileManifestV2 { 43 43 return { 44 - manifestVersion: 2, 44 + manifestVersion: 3, 45 45 id, 46 46 name: id, 47 47 tiles: [],
+2 -2
backend/electron/tile-command-registration.test.ts
··· 76 76 77 77 function makeManifestWithCommands(id: string, commands: TileCommand[]): TileManifestV2 { 78 78 return { 79 - manifestVersion: 2, 79 + manifestVersion: 3, 80 80 id, 81 81 name: id, 82 82 builtin: true, 83 83 tiles: [ 84 - { id: 'background', type: 'background', url: 'background.html', lazy: true }, 84 + { id: 'background', url: 'background.html', lazy: true }, 85 85 ], 86 86 capabilities: { pubsub: { scopes: ['self', 'global'] }, commands: true }, 87 87 commands,
+2 -2
backend/electron/tile-compat.ts
··· 137 137 const manifest = ext.v2Manifest; 138 138 const isEager = config.eagerIds?.has(ext.id) ?? false; 139 139 140 - // A tile is resident if any entry declares `resident: true` OR `type: "background"` (compat). 141 - const residentEntry = manifest.tiles.find(t => t.resident === true || t.type === 'background'); 140 + // A tile is resident if any entry declares `resident: true`. 141 + const residentEntry = manifest.tiles.find(t => t.resident === true); 142 142 const hasLazyEntries = manifest.tiles.some(t => t.lazy === true); 143 143 const hasLazyEvents = manifest.tiles.some(t => Array.isArray(t.lazyEvents) && t.lazyEvents.length > 0); 144 144 // Non-eager tiles with no resident entry are lazy-loaded on first use.
+7 -7
backend/electron/tile-features-strict.test.ts
··· 418 418 const { validateTileManifest } = await import('./tile-manifest.js'); 419 419 const errors = validateTileManifest({ 420 420 id: 't', name: 'T', 421 - tiles: [{ id: 'bg', type: 'background', url: 'bg.html' }], 421 + tiles: [{ id: 'bg', url: 'bg.html', resident: true }], 422 422 capabilities: { features: true }, 423 423 }); 424 424 assert.deepStrictEqual(errors, []); ··· 428 428 const { validateTileManifest } = await import('./tile-manifest.js'); 429 429 const errors = validateTileManifest({ 430 430 id: 't', name: 'T', 431 - tiles: [{ id: 'bg', type: 'background', url: 'bg.html' }], 431 + tiles: [{ id: 'bg', url: 'bg.html', resident: true }], 432 432 capabilities: { 433 433 features: { 434 434 read: true, install: true, manage: true, ··· 443 443 const { validateTileManifest } = await import('./tile-manifest.js'); 444 444 const errors = validateTileManifest({ 445 445 id: 't', name: 'T', 446 - tiles: [{ id: 'bg', type: 'background', url: 'bg.html' }], 446 + tiles: [{ id: 'bg', url: 'bg.html', resident: true }], 447 447 capabilities: { 448 448 features: { 449 449 update: true, dev: true, publish: true, devtools: true, browse: true, ··· 457 457 const { validateTileManifest } = await import('./tile-manifest.js'); 458 458 const errors = validateTileManifest({ 459 459 id: 't', name: 'T', 460 - tiles: [{ id: 'bg', type: 'background', url: 'bg.html' }], 460 + tiles: [{ id: 'bg', url: 'bg.html', resident: true }], 461 461 capabilities: { 462 462 features: { update: 'yes' as unknown as boolean }, 463 463 }, ··· 472 472 const { validateTileManifest } = await import('./tile-manifest.js'); 473 473 const errors = validateTileManifest({ 474 474 id: 't', name: 'T', 475 - tiles: [{ id: 'bg', type: 'background', url: 'bg.html' }], 475 + tiles: [{ id: 'bg', url: 'bg.html', resident: true }], 476 476 capabilities: { 477 477 features: { sources: ['atproto', 'unknown-source'] as unknown as string[] }, 478 478 }, ··· 487 487 const { validateTileManifest } = await import('./tile-manifest.js'); 488 488 const errors = validateTileManifest({ 489 489 id: 't', name: 'T', 490 - tiles: [{ id: 'bg', type: 'background', url: 'bg.html' }], 490 + tiles: [{ id: 'bg', url: 'bg.html', resident: true }], 491 491 capabilities: { 492 492 features: { read: 'yes' as unknown as boolean }, 493 493 }, ··· 502 502 const { validateTileManifest } = await import('./tile-manifest.js'); 503 503 const errors = validateTileManifest({ 504 504 id: 't', name: 'T', 505 - tiles: [{ id: 'bg', type: 'background', url: 'bg.html' }], 505 + tiles: [{ id: 'bg', url: 'bg.html', resident: true }], 506 506 capabilities: { 507 507 features: 'yes' as unknown as boolean, 508 508 },
+9 -13
backend/electron/tile-ipc.ts
··· 1217 1217 const manifestPath = path.join(entry.path, 'manifest.json'); 1218 1218 const parsed = parseManifestFile(manifestPath); 1219 1219 if (parsed?.version === 'v2' && parsed.v2) { 1220 - const residentTile = parsed.v2.tiles.find(t => t.resident === true || t.type === 'background'); 1220 + const residentTile = parsed.v2.tiles.find(t => t.resident === true); 1221 1221 if (residentTile && !isTileLoaded(args.id, residentTile.id)) { 1222 1222 launchTile({ 1223 1223 tilePath: entry.path, ··· 1969 1969 const featureName = args.featureName.trim(); 1970 1970 1971 1971 const manifestContent = JSON.stringify({ 1972 - manifestVersion: 2, 1972 + manifestVersion: 3, 1973 1973 id: featureId, 1974 1974 name: featureName, 1975 1975 version: '0.1.0', 1976 1976 description: '', 1977 1977 tiles: [ 1978 - { id: 'background', type: 'background', url: 'background.html', lazy: true }, 1978 + { id: 'background', url: 'background.html', lazy: true }, 1979 1979 { 1980 - id: 'home', type: 'window', url: 'home.html', 1981 - windowHints: { width: 800, height: 600, title: featureName }, 1980 + id: 'home', url: 'home.html', 1981 + width: 800, height: 600, title: featureName, 1982 1982 }, 1983 1983 ], 1984 1984 capabilities: { ··· 6298 6298 6299 6299 // Write scaffold files 6300 6300 const manifestContent = JSON.stringify({ 6301 - manifestVersion: 2, 6301 + manifestVersion: 3, 6302 6302 id: featureId, 6303 6303 name: featureName, 6304 6304 version: '0.1.0', ··· 6306 6306 tiles: [ 6307 6307 { 6308 6308 id: 'background', 6309 - type: 'background', 6310 6309 url: 'background.html', 6311 6310 lazy: true, 6312 6311 }, 6313 6312 { 6314 6313 id: 'home', 6315 - type: 'window', 6316 6314 url: 'home.html', 6317 - windowHints: { 6318 - width: 800, 6319 - height: 600, 6320 - title: featureName, 6321 - }, 6315 + width: 800, 6316 + height: 600, 6317 + title: featureName, 6322 6318 }, 6323 6319 ], 6324 6320 capabilities: {
+3 -3
backend/electron/tile-izui-strict.test.ts
··· 70 70 it('accepts izui: true', () => { 71 71 const errors = validateTileManifest({ 72 72 id: 't', name: 'T', 73 - tiles: [{ id: 'bg', type: 'background', url: 'bg.html' }], 73 + tiles: [{ id: 'bg', url: 'bg.html', resident: true }], 74 74 capabilities: { izui: true }, 75 75 }); 76 76 assert.deepStrictEqual(errors, []); ··· 79 79 it('accepts izui: false (declared but off)', () => { 80 80 const errors = validateTileManifest({ 81 81 id: 't', name: 'T', 82 - tiles: [{ id: 'bg', type: 'background', url: 'bg.html' }], 82 + tiles: [{ id: 'bg', url: 'bg.html', resident: true }], 83 83 capabilities: { izui: false }, 84 84 }); 85 85 assert.deepStrictEqual(errors, []); ··· 88 88 it('rejects non-boolean izui value', () => { 89 89 const errors = validateTileManifest({ 90 90 id: 't', name: 'T', 91 - tiles: [{ id: 'bg', type: 'background', url: 'bg.html' }], 91 + tiles: [{ id: 'bg', url: 'bg.html', resident: true }], 92 92 capabilities: { izui: 'yes' as unknown as boolean }, 93 93 }); 94 94 assert.ok(
+21 -13
backend/electron/tile-launcher.ts
··· 115 115 preloadPath: string; 116 116 /** Which tile entry to launch (defaults to first) */ 117 117 entryId?: string; 118 - /** Override window hints */ 119 - windowOverrides?: Partial<TileEntry['windowHints']>; 118 + /** Override window hints (subset of TileEntry's flat window fields) */ 119 + windowOverrides?: TileWindowOverrides; 120 120 } 121 + 122 + /** 123 + * Subset of `TileEntry`'s flat window fields that can be overridden at 124 + * launch time. Mirrors the fields the launcher actually feeds to 125 + * `BrowserWindowConstructorOptions`. 126 + */ 127 + export type TileWindowOverrides = Partial<Pick<TileEntry, 128 + | 'width' | 'height' | 'minWidth' | 'minHeight' 129 + | 'alwaysOnTop' | 'transparent' | 'focusable' | 'frame' | 'resizable' 130 + | 'role' | 'key' | 'title' 131 + >>; 121 132 122 133 /** 123 134 * Result of launching a tile ··· 178 189 * per-entry pubsub cleanup are wired identically every time. 179 190 * 180 191 * Merge order (later wins): 181 - * baseline defaults ◁ manifest-entry windowHints ◁ caller overrides 192 + * baseline defaults ◁ manifest-entry flat fields ◁ caller overrides 182 193 * 183 194 * Returns the BrowserWindow. Caller is responsible for `loadURL` (the 184 195 * URL is not part of this helper so the window-open IPC branch can do ··· 200 211 tileLifecycle.transition(tileId, entryId, tileLifecycle.TRIGGERS.LOAD, 201 212 { source: 'createTileBrowserWindow' }); 202 213 203 - // Hints from the manifest entry (windowHints + flat fields on the 204 - // entry itself, which we treat as shorthand for the same thing). 205 - let hints: Partial<TileEntry['windowHints']> & Partial<TileEntry> = {}; 214 + // Hints from the manifest entry (flat window fields on the entry). 215 + let hints: TileWindowOverrides = {}; 206 216 if (entry) { 207 217 const { width, height, minWidth, minHeight, alwaysOnTop, transparent, 208 218 focusable, frame, resizable, role, key: entryKey, title } = entry; 209 - const entryFlat = { width, height, minWidth, minHeight, alwaysOnTop, transparent, 210 - focusable, frame, resizable, role, key: entryKey, title }; 211 - hints = { ...entry.windowHints, ...entryFlat }; 219 + hints = { width, height, minWidth, minHeight, alwaysOnTop, transparent, 220 + focusable, frame, resizable, role, key: entryKey, title }; 212 221 } 213 222 214 223 const transparentResolved = browserWindowOverrides?.transparent ?? hints?.transparent === true; ··· 390 399 // Generate token 391 400 const token = generateToken(manifest.id, entry.id, grant); 392 401 393 - // Apply per-launch windowOverrides by merging them onto the entry 394 - // before we hand it to the helper. windowOverrides are defined as 395 - // `Partial<TileEntry['windowHints']>` so they're semantically hints. 402 + // Apply per-launch windowOverrides by merging them onto the entry's 403 + // flat window fields before we hand it to the helper. 396 404 const entryWithOverrides: TileEntry = windowOverrides 397 - ? { ...entry, windowHints: { ...entry.windowHints, ...windowOverrides } } 405 + ? { ...entry, ...windowOverrides } 398 406 : entry; 399 407 400 408 const win = createTileBrowserWindow({
+3 -7
backend/electron/tile-lazy-events.test.ts
··· 78 78 79 79 function makeManifest(id: string, lazyEvents: string[]): TileManifestV2 { 80 80 return { 81 - manifestVersion: 2, 81 + manifestVersion: 3, 82 82 id, 83 83 name: id, 84 84 builtin: true, 85 85 tiles: [ 86 86 { 87 87 id: 'background', 88 - type: 'background', 89 88 url: 'background.html', 90 89 lazy: true, 91 90 lazyEvents, ··· 307 306 }); 308 307 309 308 const base = { 310 - manifestVersion: 2, 309 + manifestVersion: 3, 311 310 id: 't', 312 311 name: 'T', 313 312 capabilities: {}, ··· 319 318 tiles: [ 320 319 { 321 320 id: 'background', 322 - type: 'background', 323 321 url: 'bg.html', 324 322 lazy: true, 325 323 lazyEvents: ['editor:open', 'editor:add'], ··· 332 330 it('accepts omitted lazyEvents', () => { 333 331 const errors = validateTileManifest({ 334 332 ...base, 335 - tiles: [{ id: 'background', type: 'background', url: 'bg.html', lazy: true }], 333 + tiles: [{ id: 'background', url: 'bg.html', lazy: true }], 336 334 } as Record<string, unknown>); 337 335 assert.strictEqual(errors.length, 0); 338 336 }); ··· 343 341 tiles: [ 344 342 { 345 343 id: 'background', 346 - type: 'background', 347 344 url: 'bg.html', 348 345 lazyEvents: 'editor:open', 349 346 }, ··· 358 355 tiles: [ 359 356 { 360 357 id: 'background', 361 - type: 'background', 362 358 url: 'bg.html', 363 359 lazyEvents: ['editor:open', 42, ''], 364 360 },
+1 -2
backend/electron/tile-lazy-timeout.test.ts
··· 70 70 71 71 function makeCommandManifest(id: string, cmdName: string): TileManifestV2 { 72 72 return { 73 - manifestVersion: 2, 73 + manifestVersion: 3, 74 74 id, 75 75 name: id, 76 76 builtin: true, 77 77 tiles: [ 78 78 { 79 79 id: 'background', 80 - type: 'background', 81 80 url: 'background.html', 82 81 lazy: true, 83 82 },
+3 -3
backend/electron/tile-lazy.ts
··· 501 501 502 502 loadingTiles.add(tileId); 503 503 try { 504 - const bgTile = config.manifest.tiles.find(t => t.type === 'background'); 504 + const bgTile = config.manifest.tiles.find(t => t.lazy === true) ?? config.manifest.tiles[0]; 505 505 if (!bgTile) { 506 - throw new Error(`[tile-lazy] No background tile entry in ${tileId}`); 506 + throw new Error(`[tile-lazy] No tile entry to launch in ${tileId}`); 507 507 } 508 508 509 509 launcher.launchTile({ ··· 538 538 function getBackgroundEntry(tileId: string): string | null { 539 539 const config = lazyTileRegistry.get(tileId); 540 540 if (!config) return null; 541 - const bgTile = config.manifest.tiles.find(t => t.type === 'background'); 541 + const bgTile = config.manifest.tiles.find(t => t.lazy === true) ?? config.manifest.tiles[0]; 542 542 return bgTile ? bgTile.id : null; 543 543 } 544 544
+26 -14
backend/electron/tile-manifest.test.ts
··· 17 17 // ─── Fixtures ──────────────────────────────────────────────────────── 18 18 19 19 const VALID_V2_MANIFEST = { 20 - manifestVersion: 2, 20 + manifestVersion: 3, 21 21 id: 'test-tile', 22 22 name: 'Test Tile', 23 23 description: 'A test tile', ··· 26 26 tiles: [ 27 27 { 28 28 id: 'background', 29 - type: 'background', 30 29 url: 'background.html', 30 + resident: true, 31 31 }, 32 32 ], 33 33 capabilities: { ··· 60 60 // ─── Version Detection ─────────────────────────────────────────────── 61 61 62 62 describe('detectManifestVersion', () => { 63 - it('should detect v2 by manifestVersion field', () => { 64 - assert.strictEqual(detectManifestVersion({ manifestVersion: 2 }), 'v2'); 63 + it('should detect v2 by manifestVersion field (current schema = 3)', () => { 64 + assert.strictEqual(detectManifestVersion({ manifestVersion: 3 }), 'v2'); 65 65 }); 66 66 67 67 it('should detect v2 by tiles + capabilities', () => { ··· 99 99 it('should detect v1 for manifestVersion 1', () => { 100 100 assert.strictEqual(detectManifestVersion({ manifestVersion: 1 }), 'v1'); 101 101 }); 102 + 103 + it('should detect v1 for legacy manifestVersion 2 (pre-v1-removal compat fields)', () => { 104 + assert.strictEqual(detectManifestVersion({ manifestVersion: 2 }), 'v1'); 105 + }); 102 106 }); 103 107 104 108 // ─── Validation ────────────────────────────────────────────────────── ··· 129 133 130 134 it('should reject too many tiles', () => { 131 135 const tiles = Array.from({ length: 11 }, (_, i) => ({ 132 - id: `tile-${i}`, type: 'window', url: 'index.html', 136 + id: `tile-${i}`, url: 'index.html', 133 137 })); 134 138 const manifest = { ...VALID_V2_MANIFEST, tiles }; 135 139 const errors = validateTileManifest(manifest as Record<string, unknown>); ··· 138 142 139 143 it('should reject duplicate tile ids', () => { 140 144 const tiles = [ 141 - { id: 'dup', type: 'window', url: 'a.html' }, 142 - { id: 'dup', type: 'background', url: 'b.html' }, 145 + { id: 'dup', url: 'a.html' }, 146 + { id: 'dup', url: 'b.html', resident: true }, 143 147 ]; 144 148 const manifest = { ...VALID_V2_MANIFEST, tiles }; 145 149 const errors = validateTileManifest(manifest as Record<string, unknown>); 146 150 assert.ok(errors.some(e => e.message.includes('duplicate'))); 147 151 }); 148 152 149 - it('should reject invalid tile type', () => { 150 - const tiles = [{ id: 'bad', type: 'invalid', url: 'index.html' }]; 153 + it('should reject removed tile.type field (compat dropped in manifestVersion 3)', () => { 154 + const tiles = [{ id: 'bad', type: 'background', url: 'index.html' }]; 151 155 const manifest = { ...VALID_V2_MANIFEST, tiles }; 152 156 const errors = validateTileManifest(manifest as Record<string, unknown>); 153 - assert.ok(errors.some(e => e.message.includes('type'))); 157 + assert.ok(errors.some(e => e.path.endsWith('.type'))); 158 + }); 159 + 160 + it('should reject removed tile.windowHints field (compat dropped in manifestVersion 3)', () => { 161 + const tiles = [{ id: 'bad', url: 'index.html', windowHints: { width: 100 } }]; 162 + const manifest = { ...VALID_V2_MANIFEST, tiles }; 163 + const errors = validateTileManifest(manifest as Record<string, unknown>); 164 + assert.ok(errors.some(e => e.path.endsWith('.windowHints'))); 154 165 }); 155 166 156 167 it('should reject tile without url', () => { 157 - const tiles = [{ id: 'no-url', type: 'window' }]; 168 + const tiles = [{ id: 'no-url' }]; 158 169 const manifest = { ...VALID_V2_MANIFEST, tiles }; 159 170 const errors = validateTileManifest(manifest as Record<string, unknown>); 160 171 assert.ok(errors.some(e => e.message.includes('url'))); ··· 236 247 237 248 it('should return null for invalid v2 manifest', () => { 238 249 const result = parseManifestJSON({ 239 - manifestVersion: 2, 250 + manifestVersion: 3, 240 251 // missing required fields 241 252 }); 242 253 assert.strictEqual(result, null); ··· 389 400 describe('convertV1ToV2', () => { 390 401 it('should convert basic v1 manifest', () => { 391 402 const v2 = convertV1ToV2(VALID_V1_MANIFEST as Record<string, unknown>); 392 - assert.strictEqual(v2.manifestVersion, 2); 403 + assert.strictEqual(v2.manifestVersion, 3); 393 404 assert.strictEqual(v2.id, 'legacy-ext'); 394 405 assert.strictEqual(v2.name, 'Legacy Extension'); 395 406 assert.strictEqual(v2.builtin, true); 396 407 assert.strictEqual(v2.tiles.length, 1); 397 - assert.strictEqual(v2.tiles[0].type, 'background'); 408 + assert.strictEqual(v2.tiles[0].resident, true); 398 409 assert.strictEqual(v2.tiles[0].url, 'background.html'); 399 410 }); 400 411 ··· 427 438 const v1 = { ...VALID_V1_MANIFEST, lazy: true }; 428 439 const v2 = convertV1ToV2(v1 as Record<string, unknown>); 429 440 assert.strictEqual(v2.tiles[0].lazy, true); 441 + assert.strictEqual(v2.tiles[0].resident, false); 430 442 }); 431 443 });
+29 -49
backend/electron/tile-manifest.ts
··· 347 347 // ─── Tile Definition Types ─────────────────────────────────────────── 348 348 349 349 /** 350 - * Window hints for a tile's UI 351 - */ 352 - export interface TileWindowHints { 353 - width?: number; 354 - height?: number; 355 - minWidth?: number; 356 - minHeight?: number; 357 - alwaysOnTop?: boolean; 358 - transparent?: boolean; 359 - focusable?: boolean; 360 - frame?: boolean; 361 - resizable?: boolean; 362 - /** Window role for the IZUI system */ 363 - role?: string; 364 - /** Unique key for window deduplication */ 365 - key?: string; 366 - title?: string; 367 - } 368 - 369 - /** 370 - * A tile entry point — each tile in the manifest creates one context 350 + * A tile entry point — each tile in the manifest creates one context. 371 351 * 372 - * New shape (preferred): use `resident` and flat window fields. 373 - * Old shape (compat): `type` and `windowHints` are still accepted. 352 + * Window properties are flat on this interface; there is no nested hints 353 + * object. Residency is declared via `resident: true` (loaded at startup 354 + * and kept live) — there is no `type` field. 374 355 */ 375 356 export interface TileEntry { 376 357 /** Tile entry ID (unique within the manifest) */ 377 358 id: string; 378 - /** 379 - * Tile type — accepted for backwards compatibility only. 380 - * - `"background"` is treated as an alias for `resident: true`. 381 - * - `"window"` and `"panel"` are tolerated as no-ops. 382 - * New manifests should omit this field and use `resident` instead. 383 - */ 384 - type?: 'window' | 'background' | 'panel'; 385 359 /** HTML file to load (relative to extension directory) */ 386 360 url: string; 387 361 /** 388 362 * If true, this tile is loaded at startup and kept live for the session. 389 363 * If false/absent, the tile is loaded lazily on first use. 390 - * Replaces `type: "background"` as the canonical residency marker. 391 364 */ 392 365 resident?: boolean; 393 - // ── Flat window properties (preferred over windowHints) ────────────── 366 + // ── Flat window properties ──────────────────────────────────────────── 394 367 width?: number; 395 368 height?: number; 396 369 minWidth?: number; ··· 405 378 /** Unique key for window deduplication */ 406 379 key?: string; 407 380 title?: string; 408 - /** 409 - * Window hints — accepted for backwards compatibility. 410 - * Top-level fields win if both are set. 411 - */ 412 - windowHints?: TileWindowHints; 413 381 /** If true, this tile is loaded lazily on first use */ 414 382 lazy?: boolean; 415 383 /** ··· 460 428 // ─── Manifest Types ────────────────────────────────────────────────── 461 429 462 430 /** 463 - * V2 Tile Manifest format 431 + * Tile Manifest format (the v2 architecture, schema bumped to v3 with the 432 + * v1-removal milestone — `type`/`windowHints` compat fields dropped). 464 433 */ 465 434 export interface TileManifestV2 { 466 - /** Manifest version — must be 2 for tile manifests */ 467 - manifestVersion: 2; 435 + /** Manifest version — must be 3 for tile manifests */ 436 + manifestVersion: 3; 468 437 /** Unique tile ID */ 469 438 id: string; 470 439 /** Short name for display */ ··· 574 543 // ─── Detection ─────────────────────────────────────────────────────── 575 544 576 545 /** 577 - * Detect whether a manifest JSON is v1 (legacy extension) or v2 (tile) 546 + * Detect whether a manifest JSON is v1 (legacy extension) or v2 (tile). 578 547 * 579 - * v2 manifests are identified by having: 580 - * - `manifestVersion: 2`, OR 548 + * The v2 architecture's schema is currently version 3 (bumped with the 549 + * v1-removal milestone). v2 manifests are identified by having: 550 + * - `manifestVersion: 3`, OR 581 551 * - Both `tiles` array AND `capabilities` object present 552 + * 553 + * `manifestVersion: 2` is rejected — pre-v3 tile manifests carried the 554 + * `type` / `windowHints` compat fields that have since been removed; we 555 + * treat them as legacy and force authors to migrate (lockstep cutover). 582 556 */ 583 557 export function detectManifestVersion(json: Record<string, unknown>): ManifestVersion { 584 - if (json.manifestVersion === 2) return 'v2'; 558 + if (json.manifestVersion === 3) return 'v2'; 559 + if (json.manifestVersion === 2) return 'v1'; 585 560 if (Array.isArray(json.tiles) && typeof json.capabilities === 'object' && json.capabilities !== null) { 586 561 return 'v2'; 587 562 } ··· 633 608 tileIds.add(tile.id as string); 634 609 } 635 610 636 - // type is optional — when present it must be one of the known values 637 - if (tile.type !== undefined && !['window', 'background', 'panel'].includes(tile.type as string)) { 638 - errors.push({ path: `${prefix}.type`, message: 'tile type must be window, background, or panel' }); 611 + // `type` and `windowHints` were removed in manifestVersion 3 — reject 612 + // them so stale manifests fail loudly instead of silently dropping 613 + // their declared shape. 614 + if (tile.type !== undefined) { 615 + errors.push({ path: `${prefix}.type`, message: 'tile.type was removed in manifestVersion 3 — use `resident: true` instead of `type: "background"`, drop `type: "window"`/`"panel"`' }); 616 + } 617 + if (tile.windowHints !== undefined) { 618 + errors.push({ path: `${prefix}.windowHints`, message: 'tile.windowHints was removed in manifestVersion 3 — hoist its fields onto the tile entry directly' }); 639 619 } 640 620 641 621 if (!tile.url || typeof tile.url !== 'string') { ··· 1258 1238 if (v1.background && typeof v1.background === 'string') { 1259 1239 tiles.push({ 1260 1240 id: 'background', 1261 - type: 'background', 1262 1241 url: v1.background as string, 1242 + resident: v1.lazy !== true, 1263 1243 lazy: v1.lazy === true, 1264 1244 }); 1265 1245 } ··· 1272 1252 }; 1273 1253 1274 1254 return { 1275 - manifestVersion: 2, 1255 + manifestVersion: 3, 1276 1256 id, 1277 1257 shortname: (v1.shortname as string) || undefined, 1278 1258 name,
+1 -1
backend/electron/tile-settings-defaults.test.ts
··· 36 36 settingsSchema?: string 37 37 ): TileManifestV2 { 38 38 return { 39 - manifestVersion: 2, 39 + manifestVersion: 3, 40 40 id, 41 41 name: id, 42 42 tiles: [],
+33 -34
docs/tiles-single-file.md
··· 41 41 42 42 ```json 43 43 { 44 - "manifestVersion": 2, 44 + "manifestVersion": 3, 45 45 "id": "my-tile", 46 46 "name": "My Tile", 47 47 "tiles": [{ ··· 77 77 | `lazy` | boolean | false | explicit lazy flag (legacy compat) | 78 78 | `lazyEvents` | string[] | — | topics that trigger lazy load | 79 79 80 - **No `type` field.** The old `type: "window" | "background" | "panel"` is gone from 81 - the design. It is still accepted for backwards compatibility (see below) but has no 82 - behavioral effect except as an alias for `resident`. 80 + **No `type` field.** The old `type: "window" | "background" | "panel"` was 81 + removed in `manifestVersion: 3`. Use `resident: true` to mark a tile that 82 + must be loaded at startup; omit it (or set `lazy: true`) for lazy-loaded 83 + tiles. `type: "window"` and `type: "panel"` were no-ops — drop them. 83 84 84 - **No `windowHints` nested object.** Window properties are flat on `TileEntry`. This 85 - removes one level of indirection for the common case. 85 + **No `windowHints` nested object.** Window properties are flat on `TileEntry`. 86 + This removes one level of indirection for the common case. The validator 87 + rejects `windowHints` outright in `manifestVersion: 3`. 86 88 87 89 --- 88 90 89 - ## Backwards compatibility (transition period) 91 + ## Migration from manifestVersion 2 → 3 90 92 91 - Existing manifests keep working. The runtime applies these normalizations on load: 93 + The v1-removal milestone bumped `manifestVersion` from 2 to 3 and dropped 94 + the legacy `type` / `windowHints` compat fields in lockstep. To migrate a 95 + v2 manifest: 92 96 93 - 1. **`type: "background"` → `resident: true`** — A background tile was always 94 - resident (loaded at startup). Now it's expressed as `resident: true`. The 95 - forced 0×0 geometry quirk is dropped — dimensions come from `width`/`height`. 97 + 1. Bump `manifestVersion` to `3`. 98 + 2. Replace `type: "background"` with `resident: true` (drop `type` either 99 + way — `type: "window"`/`"panel"` were no-ops). 100 + 3. Hoist every `windowHints` inner field up to the tile entry directly, 101 + then delete the `windowHints` object. 96 102 97 - 2. **`type: "window"` → tolerated no-op** — Window tiles were just tiles with 98 - visible windows. The `type: "window"` field is accepted and ignored. 99 - 100 - 3. **`windowHints.width` / `windowHints.height` → merged onto entry** — Legacy 101 - `windowHints` object is merged in the launcher; top-level fields win if both 102 - are set. 103 - 104 - No migration is required. A manifest using `type: "background"` and `windowHints` 105 - continues to work exactly as before. 103 + `manifestVersion: 2` is now treated as a legacy/v1 manifest by the 104 + detector — there is no compat layer. Re-installing a v2 feature without 105 + migration will fail validation. 106 106 107 107 --- 108 108 ··· 177 177 178 178 ### `tile-manifest.ts` 179 179 180 - - Added `resident?: boolean` and flat window fields (`width`, `height`, etc.) to 181 - `TileEntry`. 182 - - Kept `windowHints?: TileWindowHints` and `type?: string` in `TileEntry` for 183 - backwards compat. 184 - - Updated `validateTileManifest` to accept the new shape — `type` is now optional 185 - (tolerated but no longer required). 180 + - `TileEntry` carries `resident?: boolean` and flat window fields 181 + (`width`, `height`, etc.). The legacy `type?: 'window' | 'background' | 182 + 'panel'` field and the `windowHints?: TileWindowHints` nested object 183 + were removed in `manifestVersion: 3`. 184 + - `validateTileManifest` rejects both `tile.type` and `tile.windowHints` 185 + outright — there is no normalization layer. 186 + - `detectManifestVersion` recognises `manifestVersion: 3` as v2 (the 187 + current schema); `manifestVersion: 2` is now classified as v1 (legacy). 186 188 187 189 ### `tile-launcher.ts` 188 190 189 - - Merged top-level window fields with `windowHints` (top-level wins on conflict). 190 - - Dropped the `type === 'background'` → 0×0/skipTaskbar block. All tiles use 191 - declared dimensions (default 800×600). 192 - - `show: false` is the default for all tiles (not just background tiles). 193 - - Dropped the `type === 'window'` URL match preference; plain `url === pathPart` 194 - match is used. 191 + - Window options come from the entry's flat fields; `TileWindowOverrides` 192 + exposes the same subset to `windowOverrides`. 193 + - `show: false` is the default for all tiles. 195 194 196 195 ### `tile-compat.ts` 197 196 198 - - Residency decision: `t.resident === true || t.type === 'background'` (compat). 199 - - Eager launch reads the resident entry instead of the background-typed entry. 197 + - Residency decision: `t.resident === true` (no `type` fallback). 198 + - Eager launch reads the resident entry directly. 200 199 201 200 ### `tile-ipc.ts` 202 201
+1 -1
docs/v1-removal-plan.md
··· 17 17 18 18 - Phase 3.7a/c: tile-preload still has window-* fallback branches (`if (!hasWindowCapability()) return ipcRenderer.invoke('window-*')`) that route to legacy handlers. Each fallback can be deleted once we confirm every deployed tile declares the relevant capability. No tile-preload caller of window-* is unconditional anymore — the only LIVE callers are: capability-fallbacks + my new drag IIFE (which now uses strict). 19 19 - Phase 3.8 (final preload.js cleanup): preload.js itself is gone; `setPreloadPath`/`getPreloadPath` may still be referenced — quick audit needed. 20 - - Phase 4 (manifest cleanup): NOT started. Drop `type?` field, drop `windowHints`, decide manifestVersion bump, update 26 feature manifests. 20 + - Phase 4 (manifest cleanup): DONE. `type?` and `windowHints` removed from `TileEntry`; `manifestVersion` bumped to `3` in lockstep across every feature manifest, scaffolding template, audit script, and unit test fixture. Validator now rejects both legacy fields outright; `detectManifestVersion` treats `manifestVersion: 2` as v1 (legacy). `tile-launcher.ts` exposes `TileWindowOverrides` (subset of flat `TileEntry` fields) instead of the deleted `Partial<TileEntry['windowHints']>`. `tile-compat.ts` / `tile-ipc.ts` / `tile-lazy.ts` no longer fall back on `t.type === 'background'` — residency is `t.resident === true`, lazy entries are matched by `t.lazy === true`. 21 21 22 22 Validation today: `yarn tsc --noEmit` clean after each commit. Unit tests 2284/0 across all commits. Full Playwright suite NOT run during this session — user explicitly asked to defer until morning review. 23 23
+1 -1
features/dropzone/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "dropzone", 4 4 "shortname": "dropzone", 5 5 "name": "Drop Zone",
+5 -9
features/editor/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "editor", 4 4 "shortname": "editor", 5 5 "name": "Editor", ··· 9 9 "tiles": [ 10 10 { 11 11 "id": "background", 12 - "type": "background", 13 12 "url": "background.html", 14 13 "lazy": true, 15 14 "lazyEvents": ["editor:open", "editor:add"] 16 15 }, 17 16 { 18 17 "id": "home", 19 - "type": "window", 20 18 "url": "home.html", 21 - "windowHints": { 22 - "role": "workspace", 23 - "width": 1024, 24 - "height": 1000, 25 - "title": "Editor" 26 - } 19 + "role": "workspace", 20 + "width": 1024, 21 + "height": 1000, 22 + "title": "Editor" 27 23 } 28 24 ], 29 25 "capabilities": {
+1 -1
features/entities/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "entities", 4 4 "shortname": "entities", 5 5 "name": "Entity Recognition",
+5 -9
features/example/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "example", 4 4 "shortname": "example", 5 5 "name": "Example Gallery", ··· 9 9 "tiles": [ 10 10 { 11 11 "id": "background", 12 - "type": "background", 13 12 "url": "background.html", 14 13 "lazy": true 15 14 }, 16 15 { 17 16 "id": "gallery", 18 - "type": "window", 19 17 "url": "gallery.html", 20 - "windowHints": { 21 - "key": "example-gallery", 22 - "width": 1024, 23 - "height": 768, 24 - "title": "Image Gallery" 25 - } 18 + "key": "example-gallery", 19 + "width": 1024, 20 + "height": 768, 21 + "title": "Image Gallery" 26 22 } 27 23 ], 28 24 "capabilities": {
+4 -8
features/features-manager/PLAN.md
··· 203 203 The manifest.json scaffold: 204 204 ```json 205 205 { 206 - "manifestVersion": 2, 206 + "manifestVersion": 3, 207 207 "id": "{feature-id}", 208 208 "name": "{Feature Name}", 209 209 "version": "0.1.0", ··· 211 211 "tiles": [ 212 212 { 213 213 "id": "background", 214 - "type": "background", 215 214 "url": "background.html", 216 215 "lazy": true 217 216 }, 218 217 { 219 218 "id": "home", 220 - "type": "window", 221 219 "url": "home.html", 222 - "windowHints": { 223 - "width": 800, 224 - "height": 600, 225 - "title": "{Feature Name}" 226 - } 220 + "width": 800, 221 + "height": 600, 222 + "title": "{Feature Name}" 227 223 } 228 224 ], 229 225 "capabilities": {
+16 -26
features/features-manager/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "features-manager", 4 4 "shortname": "features-manager", 5 5 "name": "Features Manager", ··· 9 9 "tiles": [ 10 10 { 11 11 "id": "background", 12 - "type": "background", 13 12 "url": "background.html", 14 13 "lazy": true 15 14 }, 16 15 { 17 16 "id": "manage", 18 - "type": "window", 19 17 "url": "manage.html", 20 - "windowHints": { 21 - "role": "workspace", 22 - "key": "features-manage", 23 - "width": 900, 24 - "height": 650, 25 - "title": "Features" 26 - } 18 + "role": "workspace", 19 + "key": "features-manage", 20 + "width": 900, 21 + "height": 650, 22 + "title": "Features" 27 23 }, 28 24 { 29 25 "id": "browse", 30 - "type": "window", 31 26 "url": "browse.html", 32 - "windowHints": { 33 - "role": "workspace", 34 - "key": "features-browse", 35 - "width": 960, 36 - "height": 720, 37 - "title": "Browse Features" 38 - } 27 + "role": "workspace", 28 + "key": "features-browse", 29 + "width": 960, 30 + "height": 720, 31 + "title": "Browse Features" 39 32 }, 40 33 { 41 34 "id": "publish", 42 - "type": "window", 43 35 "url": "publish.html", 44 - "windowHints": { 45 - "role": "workspace", 46 - "key": "features-publish", 47 - "width": 700, 48 - "height": 600, 49 - "title": "Publish Feature" 50 - } 36 + "role": "workspace", 37 + "key": "features-publish", 38 + "width": 700, 39 + "height": 600, 40 + "title": "Publish Feature" 51 41 } 52 42 ], 53 43 "capabilities": {
+5 -9
features/feeds/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "feeds", 4 4 "shortname": "feeds", 5 5 "name": "Feed Reader", ··· 10 10 "tiles": [ 11 11 { 12 12 "id": "background", 13 - "type": "background", 14 13 "url": "background.html", 15 14 "lazy": true 16 15 }, 17 16 { 18 17 "id": "feeds-reader", 19 - "type": "window", 20 18 "url": "feeds.html", 21 - "windowHints": { 22 - "key": "feeds-reader", 23 - "width": 900, 24 - "height": 700, 25 - "title": "Feed Reader" 26 - } 19 + "key": "feeds-reader", 20 + "width": 900, 21 + "height": 700, 22 + "title": "Feed Reader" 27 23 } 28 24 ], 29 25 "capabilities": {
+1 -2
features/files/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "files", 4 4 "name": "Files", 5 5 "description": "File and format commands (CSV, save)", ··· 8 8 "tiles": [ 9 9 { 10 10 "id": "background", 11 - "type": "background", 12 11 "url": "background.html", 13 12 "lazy": true 14 13 }
+1 -1
features/groups/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "groups", 4 4 "shortname": "groups", 5 5 "name": "Groups",
+1 -1
features/hello-world/home.html
··· 34 34 <div class="section-title">Creating a tile</div> 35 35 <p>Minimal manifest (<code>manifest.json</code>) for a resident tile:</p> 36 36 <pre><span class="str">{ 37 - "manifestVersion": 2, 37 + "manifestVersion": 3, 38 38 "id": "my-tile", 39 39 "name": "My Tile", 40 40 "tiles": [{
+1 -1
features/hello-world/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "hello-world", 4 4 "shortname": "hello-world", 5 5 "name": "Hello World",
+5 -8
features/lex/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "lex", 4 4 "shortname": "lex", 5 5 "name": "Lexicon Studio", ··· 19 19 }, 20 20 { 21 21 "id": "chain-form", 22 - "type": "window", 23 22 "url": "chain-form.html", 24 - "windowHints": { 25 - "role": "panel", 26 - "width": 520, 27 - "height": 560, 28 - "title": "New Record" 29 - } 23 + "role": "panel", 24 + "width": 520, 25 + "height": 560, 26 + "title": "New Record" 30 27 } 31 28 ], 32 29 "capabilities": {
+6 -10
features/lists/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "lists", 4 4 "shortname": "lists", 5 5 "name": "Lists", ··· 9 9 "tiles": [ 10 10 { 11 11 "id": "background", 12 - "type": "background", 13 12 "url": "background.html", 14 13 "lazy": true 15 14 }, 16 15 { 17 16 "id": "home", 18 - "type": "window", 19 17 "url": "home.html", 20 - "windowHints": { 21 - "role": "workspace", 22 - "key": "lists-home", 23 - "width": 700, 24 - "height": 768, 25 - "title": "Lists" 26 - } 18 + "role": "workspace", 19 + "key": "lists-home", 20 + "width": 700, 21 + "height": 768, 22 + "title": "Lists" 27 23 } 28 24 ], 29 25 "capabilities": {
+1 -1
features/mcp-server/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "mcp-server", 4 4 "name": "MCP Server", 5 5 "version": "1.0.0",
+5 -9
features/pagestream/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "pagestream", 4 4 "shortname": "pagestream", 5 5 "name": "Pagestream", ··· 9 9 "tiles": [ 10 10 { 11 11 "id": "background", 12 - "type": "background", 13 12 "url": "background.html", 14 13 "lazy": true 15 14 }, 16 15 { 17 16 "id": "home", 18 - "type": "window", 19 17 "url": "home.html", 20 - "windowHints": { 21 - "role": "workspace", 22 - "key": "peek://pagestream/home.html", 23 - "transparent": true, 24 - "title": "Pagestream" 25 - } 18 + "role": "workspace", 19 + "key": "peek://pagestream/home.html", 20 + "transparent": true, 21 + "title": "Pagestream" 26 22 } 27 23 ], 28 24 "capabilities": {
+1 -2
features/pagewidgets-sample/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "pagewidgets-sample", 4 4 "shortname": "pagewidgets-sample", 5 5 "name": "Page Widgets Sample", ··· 9 9 "tiles": [ 10 10 { 11 11 "id": "background", 12 - "type": "background", 13 12 "url": "background.html", 14 13 "lazy": true 15 14 }
+3 -3
features/peeks/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "peeks", 4 4 "shortname": "peeks", 5 5 "name": "Peeks", ··· 10 10 "tiles": [ 11 11 { 12 12 "id": "background", 13 - "type": "background", 14 - "url": "background.html" 13 + "url": "background.html", 14 + "resident": true 15 15 } 16 16 ], 17 17 "capabilities": {
+6 -10
features/pubsub-repro/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "pubsub-repro", 4 4 "shortname": "pubsub-repro", 5 5 "name": "Pubsub Repro", ··· 9 9 "tiles": [ 10 10 { 11 11 "id": "background", 12 - "type": "background", 13 12 "url": "background.html", 14 13 "lazy": true 15 14 }, 16 15 { 17 16 "id": "home", 18 - "type": "window", 19 17 "url": "home.html", 20 - "windowHints": { 21 - "role": "workspace", 22 - "key": "pubsub-repro-home", 23 - "width": 400, 24 - "height": 300, 25 - "title": "Pubsub Repro" 26 - } 18 + "role": "workspace", 19 + "key": "pubsub-repro-home", 20 + "width": 400, 21 + "height": 300, 22 + "title": "Pubsub Repro" 27 23 } 28 24 ], 29 25 "capabilities": {
+5 -9
features/scripts/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "scripts", 4 4 "shortname": "scripts", 5 5 "name": "Scripts", ··· 10 10 "tiles": [ 11 11 { 12 12 "id": "background", 13 - "type": "background", 14 13 "url": "background.html", 15 14 "lazy": true 16 15 }, 17 16 { 18 17 "id": "manager", 19 - "type": "window", 20 18 "url": "manager.html", 21 - "windowHints": { 22 - "key": "scripts-manager", 23 - "width": 1200, 24 - "height": 800, 25 - "title": "Scripts Manager" 26 - } 19 + "key": "scripts-manager", 20 + "width": 1200, 21 + "height": 800, 22 + "title": "Scripts Manager" 27 23 } 28 24 ], 29 25 "capabilities": {
+6 -10
features/search/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "search", 4 4 "shortname": "search", 5 5 "name": "Search", ··· 9 9 "tiles": [ 10 10 { 11 11 "id": "background", 12 - "type": "background", 13 12 "url": "background.html", 14 13 "lazy": true 15 14 }, 16 15 { 17 16 "id": "home", 18 - "type": "window", 19 17 "url": "home.html", 20 - "windowHints": { 21 - "role": "workspace", 22 - "key": "search-home", 23 - "width": 700, 24 - "height": 768, 25 - "title": "Search" 26 - } 18 + "role": "workspace", 19 + "key": "search-home", 20 + "width": 700, 21 + "height": 768, 22 + "title": "Search" 27 23 } 28 24 ], 29 25 "capabilities": {
+2 -6
features/sheets/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "sheets", 4 4 "shortname": "sheets", 5 5 "name": "Sheets", ··· 9 9 "tiles": [ 10 10 { 11 11 "id": "background", 12 - "type": "background", 13 12 "url": "background.html", 14 13 "lazy": true 15 14 }, 16 15 { 17 16 "id": "sheet", 18 - "type": "window", 19 17 "url": "sheet.html", 20 - "windowHints": { 21 - "title": "Sheet" 22 - } 18 + "title": "Sheet" 23 19 } 24 20 ], 25 21 "capabilities": {
+3 -3
features/slides/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "slides", 4 4 "shortname": "slides", 5 5 "name": "Slides", ··· 10 10 "tiles": [ 11 11 { 12 12 "id": "background", 13 - "type": "background", 14 - "url": "background.html" 13 + "url": "background.html", 14 + "resident": true 15 15 } 16 16 ], 17 17 "capabilities": {
+10 -17
features/spaces/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "spaces", 4 4 "shortname": "spaces", 5 5 "name": "Spaces", ··· 9 9 "tiles": [ 10 10 { 11 11 "id": "background", 12 - "type": "background", 13 12 "url": "background.html", 14 13 "lazy": true 15 14 }, 16 15 { 17 16 "id": "home", 18 - "type": "window", 19 17 "url": "home.html", 20 - "windowHints": { 21 - "role": "workspace", 22 - "width": 800, 23 - "height": 600, 24 - "title": "Spaces" 25 - } 18 + "role": "workspace", 19 + "width": 800, 20 + "height": 600, 21 + "title": "Spaces" 26 22 }, 27 23 { 28 24 "id": "border", 29 - "type": "window", 30 25 "url": "border.html", 31 - "windowHints": { 32 - "role": "overlay", 33 - "transparent": true, 34 - "frame": false, 35 - "focusable": false, 36 - "alwaysOnTop": true 37 - } 26 + "role": "overlay", 27 + "transparent": true, 28 + "frame": false, 29 + "focusable": false, 30 + "alwaysOnTop": true 38 31 } 39 32 ], 40 33 "capabilities": {
+1 -1
features/sync/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "sync", 4 4 "name": "Sync", 5 5 "version": "2.0.0",
+1 -1
features/tag-actions/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "tag-actions", 4 4 "shortname": "tag-actions", 5 5 "name": "Tag Actions",
+6 -10
features/tags/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "tags", 4 4 "shortname": "tags", 5 5 "name": "Tags", ··· 9 9 "tiles": [ 10 10 { 11 11 "id": "background", 12 - "type": "background", 13 12 "url": "background.html", 14 13 "lazy": true 15 14 }, 16 15 { 17 16 "id": "home", 18 - "type": "window", 19 17 "url": "home.html", 20 - "windowHints": { 21 - "role": "workspace", 22 - "key": "tags-home", 23 - "width": 900, 24 - "height": 700, 25 - "title": "Tags" 26 - } 18 + "role": "workspace", 19 + "key": "tags-home", 20 + "width": 900, 21 + "height": 700, 22 + "title": "Tags" 27 23 } 28 24 ], 29 25 "capabilities": {
+6 -10
features/timers/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "timers", 4 4 "shortname": "timers", 5 5 "name": "Timers", ··· 9 9 "tiles": [ 10 10 { 11 11 "id": "background", 12 - "type": "background", 13 12 "url": "background.html", 14 13 "lazy": true 15 14 }, 16 15 { 17 16 "id": "home", 18 - "type": "window", 19 17 "url": "home.html", 20 - "windowHints": { 21 - "role": "workspace", 22 - "key": "peek://timers/home.html", 23 - "width": 700, 24 - "height": 600, 25 - "title": "Timers" 26 - } 18 + "role": "workspace", 19 + "key": "peek://timers/home.html", 20 + "width": 700, 21 + "height": 600, 22 + "title": "Timers" 27 23 } 28 24 ], 29 25 "capabilities": {
+1 -1
features/websearch/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "websearch", 4 4 "shortname": "websearch", 5 5 "name": "Web Search",
+1 -1
features/widget-demo/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "widget-demo", 4 4 "shortname": "widget-demo", 5 5 "name": "Widget Demo",
+5 -9
features/windows/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "windows", 4 4 "shortname": "windows", 5 5 "name": "Windows", ··· 9 9 "tiles": [ 10 10 { 11 11 "id": "background", 12 - "type": "background", 13 12 "url": "background.html", 14 13 "lazy": true 15 14 }, 16 15 { 17 16 "id": "windows-overlay", 18 - "type": "window", 19 17 "url": "windows.html", 20 - "windowHints": { 21 - "role": "overlay", 22 - "transparent": true, 23 - "alwaysOnTop": true, 24 - "resizable": false 25 - } 18 + "role": "overlay", 19 + "transparent": true, 20 + "alwaysOnTop": true, 21 + "resizable": false 26 22 } 27 23 ], 28 24 "capabilities": {
+6 -10
features/wonderwall/manifest.json
··· 1 1 { 2 - "manifestVersion": 2, 2 + "manifestVersion": 3, 3 3 "id": "wonderwall", 4 4 "shortname": "wonderwall", 5 5 "name": "Wonderwall", ··· 9 9 "tiles": [ 10 10 { 11 11 "id": "background", 12 - "type": "background", 13 12 "url": "background.html", 14 13 "lazy": true 15 14 }, 16 15 { 17 16 "id": "home", 18 - "type": "window", 19 17 "url": "home.html", 20 - "windowHints": { 21 - "role": "workspace", 22 - "key": "wonderwall-home", 23 - "width": 1024, 24 - "height": 768, 25 - "title": "Wonderwall" 26 - } 18 + "role": "workspace", 19 + "key": "wonderwall-home", 20 + "width": 1024, 21 + "height": 768, 22 + "title": "Wonderwall" 27 23 } 28 24 ], 29 25 "capabilities": {
+3 -3
scripts/audit-tile-caps.js
··· 2 2 /** 3 3 * audit-tile-caps.js 4 4 * 5 - * Scans every v2 feature (manifestVersion === 2) under features/ and detects 5 + * Scans every v2 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 * ··· 139 139 const featureDir = path.join(FEATURES_DIR, featureId); 140 140 const manifestPath = path.join(featureDir, 'manifest.json'); 141 141 const manifest = readJsonSafe(manifestPath); 142 - if (!manifest || manifest.manifestVersion !== 2) { 142 + if (!manifest || manifest.manifestVersion !== 3) { 143 143 return null; 144 144 } 145 145 ··· 192 192 if (!ent.isDirectory()) continue; 193 193 if (ent.name.startsWith('.')) continue; 194 194 const manifest = readJsonSafe(path.join(FEATURES_DIR, ent.name, 'manifest.json')); 195 - if (manifest && manifest.manifestVersion === 2) { 195 + if (manifest && manifest.manifestVersion === 3) { 196 196 out.push(ent.name); 197 197 } 198 198 }
+4 -4
tests/unit/atproto-source.test.js
··· 330 330 describe('manifest validation for atproto records', () => { 331 331 it('validates a correct v2 manifest', () => { 332 332 const manifest = { 333 - manifestVersion: 2, 333 + manifestVersion: 3, 334 334 id: 'test-feature', 335 335 name: 'Test Feature', 336 336 version: '1.0.0', 337 337 tiles: [ 338 - { id: 'background', type: 'background', url: 'background.html' }, 338 + { id: 'background', url: 'background.html', resident: true }, 339 339 ], 340 340 capabilities: { 341 341 pubsub: { scopes: ['self', 'global'] }, ··· 353 353 354 354 it('rejects manifest missing id', () => { 355 355 const manifest = { 356 - manifestVersion: 2, 356 + manifestVersion: 3, 357 357 name: 'Test Feature', 358 358 tiles: [], 359 359 capabilities: {}, ··· 363 363 364 364 it('rejects manifest missing tiles', () => { 365 365 const manifest = { 366 - manifestVersion: 2, 366 + manifestVersion: 3, 367 367 id: 'test', 368 368 name: 'Test', 369 369 capabilities: {},