···77</head>
88<body>
99<!--
1010- Privileged test renderer (v1-removal Phase 2).
1010+ Privileged test renderer.
11111212 This is a hidden resident BrowserWindow launched by
1313 `backend/electron/test-fixture-glue.ts` only when `E2E_TEST=true`. It
1414- replaces the v1 `peek://app/background.html` iframe that Playwright
1515- tests previously used as `bgWindow` for pub/sub + datastore calls.
1414+ is the test fixture that Playwright tests use as `bgWindow` for
1515+ pub/sub + datastore calls.
16161717 Mechanics are identical to cmd/hud/page core renderers:
1818 - Loaded with `tile-preload.cjs` + a `createTrustedBuiltinGrant('test')`
+1-1
app/background.html
···88 </head>
99 <body>
1010 <!--
1111- Core background renderer (v1-removal Phase 2.5 #1).
1111+ Core background renderer.
12121313 Hidden resident BrowserWindow created by
1414 `backend/electron/core-glue.ts::initCore()`. Uses the same tile
+6-6
app/cmd/panel.js
···1818const api = window.app;
19192020// Initialize the tile-preload surface before any API calls. Without this,
2121-// `tokenValid` is false and api.settings/api.subscribe/api.commands all
2222-// silently no-op or reject — see docs/v1-removal-smoke-regressions.md §1.
2121+// `tokenValid` is false and api.settings / api.subscribe / api.commands
2222+// all silently no-op or reject.
2323if (api.initialize) {
2424 await api.initialize();
2525}
···3535let adaptiveDataCache = null;
36363737/**
3838- * Load persisted adaptive data from extension settings.
3939- * Phase 4 API: each key fetched individually via api.settings.get(key).
3838+ * Load persisted adaptive data from tile settings. Each key is fetched
3939+ * individually via api.settings.get(key).
4040 */
4141const loadAdaptiveData = async () => {
4242 if (adaptiveDataCache) {
···100100// Cached prefs for URL search behavior
101101let showUrlsInResults = defaults.prefs.showUrlsInResults;
102102103103-// Load prefs on startup. Phase 4: api.settings.get(key) — request the
104104-// 'prefs' row directly rather than the (now removed) no-arg whole-object form.
103103+// Load prefs on startup. api.settings.get(key) requires the per-row key;
104104+// the legacy no-arg whole-object form was removed.
105105api.settings.get('prefs').then(result => {
106106 if (result.success && result.data) {
107107 showUrlsInResults = result.data.showUrlsInResults ?? defaults.prefs.showUrlsInResults;
···9595export { PeekTabs, PeekTab, PeekTabPanel } from './peek-tabs.js';
9696export { PeekDetails } from './peek-details.js';
97979898-// Components - Phase 4
9898+// Components - Form controls / overlays
9999export { PeekSelect } from './peek-select.js';
100100export { PeekDropdown, PeekDropdownItem, PeekDropdownDivider } from './peek-dropdown.js';
101101export { PeekSwitch } from './peek-switch.js';
+3-3
app/settings/settings.js
···32123212// Initialize
32133213const init = async () => {
32143214 // Initialize the tile-preload surface before any API calls. Without this,
32153215- // `tokenValid` is false and api.datastore/api.theme/api.subscribe all silently
32163216- // no-op or reject — createDatastoreStore returns empty, theme CSS is never
32173217- // injected, and --theme-font-sans is absent (see docs/v1-removal-smoke-regressions.md §7).
32153215+ // `tokenValid` is false and api.datastore / api.theme / api.subscribe all
32163216+ // silently no-op or reject — createDatastoreStore returns empty, theme
32173217+ // CSS is never injected, and --theme-font-sans is absent.
32183218 if (api.initialize) {
32193219 await api.initialize();
32203220 }
-2
backend/electron/core-glue.ts
···3939 * depends on the renderer being initialized (e.g. the
4040 * `core:ready` subscriber that kicks off extension loading) can trust
4141 * the renderer's subscribers are in place.
4242- *
4343- * See docs/v1-removal-plan.md — Phase 2.5 #1.
4442 */
45434644import { BrowserWindow } from 'electron';
+2-2
backend/electron/protocol.ts
···623623 }
624624625625 // peek://test/{path} resolves to app/_test/ — the privileged test
626626- // fixture renderer used by the Playwright suite (v1-removal Phase 2).
627627- // Only launched when E2E_TEST=true, via test-fixture-glue.ts.
626626+ // fixture renderer used by the Playwright suite. Only launched when
627627+ // E2E_TEST=true, via test-fixture-glue.ts.
628628 if (host === 'test') {
629629 const testPath = pathname || 'index.html';
630630 const absolutePath = path.resolve(rootDir, 'app', '_test', testPath);
+4-8
backend/electron/session.ts
···11/**
22 * Session Snapshot — Save/Restore window state
33 *
44- * Phase 1: Save session snapshot on quit.
55- * Phase 2: Restore session snapshot on startup.
66- * Phase 3: Periodic autosave timer + manual save/restore commands.
77- * Phase 4: Crash recovery dialog, snapshot validation, error handling.
88- *
99- * Captures all visible user windows and writes to feature_settings
1010- * using synchronous better-sqlite3 APIs (safe for before-quit handler).
1111- * On startup, reads the snapshot and recreates windows.
44+ * Captures all visible user windows and writes to feature_settings using
55+ * synchronous better-sqlite3 APIs (safe for before-quit handler). On startup,
66+ * reads the snapshot and recreates windows. Periodic autosave timer + manual
77+ * save/restore commands; crash recovery dialog with snapshot validation.
128 *
139 * NOTE: Pure logic shared copy lives in app/lib/session.js for frontend/Tauri use.
1410 */
+5-5
backend/electron/tile-api.d.ts
···1010 * - docs/tile-api.md (reference documentation)
1111 *
1212 * This file does NOT auto-generate — update manually when tile-preload.ts or
1313- * tile-ipc.ts adds/removes surfaces. The surface is stable post-Phase 4.
1313+ * tile-ipc.ts adds/removes surfaces.
1414 *
1515 * Usage in a tile:
1616 * /// <reference path="../../backend/electron/tile-api.d.ts" />
···650650 adblocker: TileAdblocker;
651651 darkMode: TileDarkMode;
652652653653- // ── Removed v1 shims (Phase 4) — these throw if called ────────────
653653+ // ── Removed v1 shims — these throw if called ──────────────────────
654654655655- /** @deprecated Removed in Phase 4. Use api.pubsub.publish. */
655655+ /** @deprecated Removed. Use api.pubsub.publish. */
656656 publish(...args: unknown[]): never;
657657- /** @deprecated Removed in Phase 4. Use api.pubsub.subscribe. */
657657+ /** @deprecated Removed. Use api.pubsub.subscribe. */
658658 subscribe(...args: unknown[]): never;
659659- /** @deprecated Removed in Phase 4. Use api.window.close(). */
659659+ /** @deprecated Removed. Use api.window.close(). */
660660 closeWindow(...args: unknown[]): never;
661661662662 // ── Capability-gated surfaces ──────────────────────────────────────
+2-2
backend/electron/tile-ipc.ts
···466466 // ── Tile Ready ──
467467 //
468468 // Private lifecycle IPC (`tile:lifecycle:ready` / `tile:lifecycle:shutdown`)
469469- // moved to tile-lifecycle.ts in Phase 4. See
470470- // docs/pubsub-state-machine.md §Topics that are NOT pubsub.
469469+ // lives in tile-lifecycle.ts. See docs/pubsub-state-machine.md
470470+ // §Topics that are NOT pubsub.
471471472472 // ── PubSub ──
473473
-2
backend/electron/tile-launcher.ts
···547547 * - Add the window to the `tileWindows` registry so the pubsub
548548 * broadcaster in main.ts forwards messages to it.
549549 * - Wire the same close/revoke cleanup the regular launcher uses.
550550- *
551551- * See docs/v1-removal-plan.md — Phase 1a/1b.
552550 */
553551export function registerTrustedBuiltinWindow(
554552 tileId: string,
+3-3
backend/electron/tile-lifecycle.test.ts
···11/**
22- * Unit tests for tile-lifecycle.ts — Phase 4.
22+ * Unit tests for tile-lifecycle.ts.
33 *
44 * Covers:
55 * - `tile:state-changed` System-topic emission on every transition.
···246246 it('a subscriber registered at t=0 boot receives a cmd:register published immediately after', () => {
247247 // This simulates the core subscriber (inside app/index.js → cmd
248248 // background) landing synchronously during core init, BEFORE any
249249- // tile fires its first `cmd:register`. With the Phase 4 gate in
250250- // place, tiles can't transition REGISTERED→LOADING until bgWindow
249249+ // tile fires its first `cmd:register`. With the bgWindow-ready gate
250250+ // in place, tiles can't transition REGISTERED→LOADING until bgWindow
251251 // signals ready — by that time these subscribers exist, so the
252252 // first publishes are never dropped.
253253 const received: unknown[] = [];
+5-5
backend/electron/tile-lifecycle.ts
···1212 * engine that applies those transitions against concrete electron
1313 * primitives.
1414 *
1515- * Phase 4: ownership of the private lifecycle IPC channels
1616- * (`tile:lifecycle:ready` / `tile:lifecycle:shutdown`) moves here
1717- * from tile-ipc.ts. Also introduces:
1515+ * Owns the private lifecycle IPC channels
1616+ * (`tile:lifecycle:ready` / `tile:lifecycle:shutdown`). Also provides:
1817 * - `registerBgWindow()` + bgWindow-ready latch: the subscribe-
1918 * before-publish invariant. No non-bgWindow tile may transition
2019 * REGISTERED→LOADING until bgWindow signals `tile:lifecycle:ready`.
···123122//
124123// Implementation: a simple one-shot latch. Once bgWindow signals ready,
125124// the latch resolves and stays resolved for the rest of the process
126126-// lifetime. Crashing bgWindow mid-boot is a separate concern (Phase 7
127127-// timeout UX) — for Phase 4 a permanent latch is sufficient.
125125+// lifetime. Crashing bgWindow mid-boot is a separate concern (handled
126126+// by the load-on-dispatch timeout UX in tile-lazy); a permanent latch
127127+// is sufficient here.
128128129129interface BgWindowRegistration {
130130 tileId: string;
···428428// ─── Manifest Types ──────────────────────────────────────────────────
429429430430/**
431431- * Tile Manifest format (the v2 architecture, schema bumped to v3 with the
432432- * v1-removal milestone — `type`/`windowHints` compat fields dropped).
431431+ * Tile Manifest format. Current schema version is 3; legacy `type` and
432432+ * `windowHints` compat fields are no longer accepted by the validator.
433433 */
434434export interface TileManifestV2 {
435435 /** Manifest version — must be 3 for tile manifests */
···545545/**
546546 * Detect whether a manifest JSON is v1 (legacy extension) or v2 (tile).
547547 *
548548- * The v2 architecture's schema is currently version 3 (bumped with the
549549- * v1-removal milestone). v2 manifests are identified by having:
548548+ * The v2 architecture's schema is currently version 3. v2 manifests are
549549+ * identified by having:
550550 * - `manifestVersion: 3`, OR
551551 * - Both `tiles` array AND `capabilities` object present
552552 *
···11641164 * that could be non-exhaustive is omitted — when the bypass is
11651165 * respected, the allowlist is never consulted.
11661166 *
11671167- * See `docs/v1-removal-plan.md` for the rationale: core renderers
11681168- * participate in the same tile mechanics as feature tiles, but do not
11691169- * require capability gating because they are trusted code.
11671167+ * Core renderers (cmd, hud, page) participate in the same tile mechanics
11681168+ * as feature tiles, but do not require capability gating because they
11691169+ * are trusted bundled code.
11701170 */
11711171export function createTrustedBuiltinGrant(rendererId: string): CapabilityGrant {
11721172 const capabilities: TileCapabilities = {
···11/**
22- * Phase 5: replay machinery removal.
33- *
42 * Asserts the compiled tile-preload.cjs carries no reference to the
53 * deleted `cmd:request-registers` replay topic, the `registeredPayloads`
66- * cache, or the `ensureCmdRequestRegistersListener` installer. Phase 4
77- * made the subscribe-before-publish invariant load-bearing (Core's
88- * subscribers are live before any tile reaches `ready`), so the replay
99- * topic has no job and must not linger as dead code.
1010- *
1111- * Mirrors the "tile-ipc.js does not register a tile:command:result
1212- * handler" style assertion from tile-ipc.test.ts (Phase 3).
44+ * cache, or the `ensureCmdRequestRegistersListener` installer. The
55+ * subscribe-before-publish invariant is load-bearing (Core's subscribers
66+ * are live before any tile reaches `ready`), so the replay topic has
77+ * no job and must not linger as dead code.
138 */
1491510import { describe, it } from 'node:test';
+47-44
backend/electron/tile-preload-stubs.test.ts
···11/**
22 * tile-preload-stubs.test.ts
33 *
44- * Phase 4: Verify that removed v1-compat shim names on tile-preload expose
55- * hard-fail stubs that throw the correct error messages. These tests import
66- * the compiled preload and exercise only the stub detection logic — we don't
77- * need a full Electron environment because the stubs are plain arrow functions
88- * registered on the api object before contextBridge.exposeInMainWorld runs.
44+ * Verify that removed v1-compat shim names on tile-preload expose
55+ * hard-fail stubs that throw the correct error messages. These tests
66+ * import the compiled preload and exercise only the stub detection
77+ * logic — we don't need a full Electron environment because the stubs
88+ * are plain arrow functions registered on the api object before
99+ * contextBridge.exposeInMainWorld runs.
910 *
1010- * Strategy: rather than trying to invoke the preload (which needs ipcRenderer),
1111- * we parse the compiled JS text to assert that each removed name:
1111+ * Strategy: rather than trying to invoke the preload (which needs
1212+ * ipcRenderer), we parse the compiled JS text to assert that each
1313+ * removed name:
1214 * 1. Is assigned a function (not an alias to a compat implementation)
1313- * 2. Contains a throw with the '[tile-preload]' prefix and 'Phase 4' marker
1515+ * 2. Contains a throw with the '[tile-preload]' prefix and a
1616+ * 'removed' marker
1417 *
1515- * This is a static analysis test — it is fast, offline, and does not require
1616- * Electron's IPC to be wired up.
1818+ * This is a static analysis test — it is fast, offline, and does not
1919+ * require Electron's IPC to be wired up.
1720 */
18211922import { test, describe } from 'node:test';
···36393740function hasHardFailStub(name: string): boolean {
3841 if (!src) return true; // skip when file not compiled yet
3939- // Each stub should throw with '[tile-preload]' and 'Phase 4'
4242+ // Each stub should throw with '[tile-preload]' and a 'removed' marker.
4043 // The stub pattern looks like:
4141- // api.xxx = (..._args) => { throw new Error('[tile-preload] ... Phase 4 ...') }
4444+ // api.xxx = (..._args) => { throw new Error('[tile-preload] ... removed ...') }
4245 // or:
4343- // xxx: (..._args) => { throw new Error('[tile-preload] ... Phase 4 ...') }
4646+ // xxx: (..._args) => { throw new Error('[tile-preload] ... removed ...') }
4447 const stubRegex = new RegExp(
4545- `['"]?${name.replace('.', '\\.')}['"]?\\s*[:=]\\s*[^;{]*=>\\s*\\{[^}]*\\[tile-preload\\][^}]*Phase 4[^}]*\\}`,
4848+ `['"]?${name.replace('.', '\\.')}['"]?\\s*[:=]\\s*[^;{]*=>\\s*\\{[^}]*\\[tile-preload\\][^}]*removed[^}]*\\}`,
4649 'm',
4750 );
4848- // Fallback: look for the error string containing the name
5151+ // Fallback: look for the error string containing the name + 'removed'
4952 const errorRegex = new RegExp(
5050- `\\[tile-preload\\][^'"]*${name.replace('.', '\\.')}[^'"]*removed in Phase 4`,
5353+ `\\[tile-preload\\][^'"]*${name.replace('.', '\\.')}[^'"]*removed`,
5154 'm',
5255 );
5356 return stubRegex.test(src) || errorRegex.test(src);
5457}
55585656-// NOTE: api.publish / api.subscribe were INTENDED to be hard-fail stubs in
5757-// Phase 4, but several shared libraries in app/ (app/index.js, app/hud/
5858-// widgets/*, app/lib/tag-action-affordances.js, app/settings/settings.js)
5959-// still call them directly. They were restored as aliases to
6060-// api.pubsub.publish / api.pubsub.subscribe so those shared libs keep
6161-// working. Tracked in docs/tasks.md for a proper sweep later.
5959+// NOTE: api.publish / api.subscribe were INTENDED to be hard-fail stubs,
6060+// but several shared libraries in app/ (app/index.js, app/hud/widgets/*,
6161+// app/lib/tag-action-affordances.js, app/settings/settings.js) still call
6262+// them directly. They were restored as aliases to api.pubsub.publish /
6363+// api.pubsub.subscribe so those shared libs keep working. Tracked in
6464+// docs/tasks.md for a proper sweep later.
62656363-describe('Phase 4 hard-fail stubs — api.closeWindow', () => {
6464- test('api.closeWindow stub contains Phase 4 error', () => {
6666+describe('hard-fail stubs — api.closeWindow', () => {
6767+ test('api.closeWindow stub throws with removed marker', () => {
6568 assert.ok(
6669 hasHardFailStub('closeWindow'),
6767- 'api.closeWindow should have a hard-fail stub with Phase 4 error message',
7070+ 'api.closeWindow should have a hard-fail stub with a removed marker in the error message',
6871 );
6972 });
7073});
71747272-describe('Phase 4 hard-fail stubs — api.modes', () => {
7575+describe('hard-fail stubs — api.modes', () => {
7376 for (const method of ['getWindowMode', 'setMajorMode', 'listModes', 'getCommandContext', 'onModeChange']) {
7474- test(`api.modes.${method} stub contains Phase 4 error`, () => {
7777+ test(`api.modes.${method} stub throws with removed marker`, () => {
7578 assert.ok(
7679 hasHardFailStub(method),
7777- `api.modes.${method} should have a hard-fail stub with Phase 4 error message`,
8080+ `api.modes.${method} should have a hard-fail stub with a removed marker in the error message`,
7881 );
7982 });
8083 }
8184});
82858383-describe('Phase 4 hard-fail stubs — api.files', () => {
8686+describe('hard-fail stubs — api.files', () => {
8487 for (const method of ['save', 'open', 'readFromPath', 'writeToPath']) {
8585- test(`api.files.${method} stub contains Phase 4 error`, () => {
8888+ test(`api.files.${method} stub throws with removed marker`, () => {
8689 assert.ok(
8790 hasHardFailStub(method),
8888- `api.files.${method} should have a hard-fail stub with Phase 4 error message`,
9191+ `api.files.${method} should have a hard-fail stub with a removed marker in the error message`,
8992 );
9093 });
9194 }
9295});
93969494-describe('Phase 4 hard-fail stubs — api.settings.getKey/setKey', () => {
9595- test('api.settings.getKey stub contains Phase 4 error', () => {
9797+describe('hard-fail stubs — api.settings.getKey/setKey', () => {
9898+ test('api.settings.getKey stub throws with removed marker', () => {
9699 assert.ok(
97100 hasHardFailStub('getKey'),
9898- 'api.settings.getKey should have a hard-fail stub with Phase 4 error message',
101101+ 'api.settings.getKey should have a hard-fail stub with a removed marker in the error message',
99102 );
100103 });
101104102102- test('api.settings.setKey stub contains Phase 4 error', () => {
105105+ test('api.settings.setKey stub throws with removed marker', () => {
103106 assert.ok(
104107 hasHardFailStub('setKey'),
105105- 'api.settings.setKey should have a hard-fail stub with Phase 4 error message',
108108+ 'api.settings.setKey should have a hard-fail stub with a removed marker in the error message',
106109 );
107110 });
108111});
109112110110-// NOTE: Phase 4 initially replaced these datastore compat methods with
111111-// hard-fail stubs, but feature code was never migrated off them — the
112112-// stubs broke every tile UI. The methods were restored as thin routing
113113-// wrappers to the legacy `datastore-*` IPC channels. Tracked in
114114-// docs/tasks.md under "Phase 4 datastore restoration". This test block
115115-// is intentionally left out until we finish the strict migration and
116116-// can reintroduce the hard-fail stubs.
113113+// NOTE: the datastore compat methods were initially intended as hard-fail
114114+// stubs, but feature code was never migrated off them — the stubs broke
115115+// every tile UI. The methods were restored as thin routing wrappers to
116116+// the legacy `datastore-*` IPC channels. Tracked in docs/tasks.md under
117117+// "datastore restoration". This test block is intentionally left out
118118+// until we finish the strict migration and can reintroduce the hard-fail
119119+// stubs.
117120118118-describe('Phase 4 — strict surfaces still present', () => {
121121+describe('strict surfaces still present', () => {
119122 test('api.datastore.get strict surface still present', () => {
120123 if (!src) return;
121124 assert.ok(
+55-87
backend/electron/tile-preload.cts
···7373 return validationPromise;
7474}
75757676-// ─── Phase 4: v1-compat violation logger REMOVED ─────────────────────
7777-// The wrapCompat / wrapCompatObject helpers and the compatSeen set were
7878-// used during Phases 1-3 to log first-use of v1 shims. All shims have
7979-// now been replaced with hard-fail stubs or strict-only paths, so the
8080-// logger infrastructure is no longer needed.
7676+// V1 shims are gone; every legacy api.* surface is either a strict-only
7777+// path through capability-gated `tile:*` IPC or a hard-fail stub that
7878+// throws with a migration hint. No first-use violation logger is needed.
81798280// ─── API Builder ─────────────────────────────────────────────────────
8381···202200 // api.publish(topic, data) // v1-style
203201 // api.pubsub.publish(topic, data) // v2-style (kept for clarity)
204202 //
205205- // Scope (SYSTEM / SELF / GLOBAL) was removed in Phase 6 (see
203203+ // The legacy SYSTEM / SELF / GLOBAL scope param is gone (see
206204 // docs/pubsub-state-machine.md). Privilege is enforced by the
207205 // capability grant's topic allowlist in main.
208206···253251 };
254252 }
255253256256- // Phase 4 intent was to remove top-level api.publish / api.subscribe as
257257- // hard-fail stubs, but several shared libraries in app/ (app/index.js,
258258- // app/hud/widgets/*, app/lib/tag-action-affordances.js, app/settings/
259259- // settings.js) still call them directly. Restored as thin aliases to
260260- // api.pubsub.publish / api.pubsub.subscribe so those shared libs work
261261- // under tile-preload without touching every call site.
254254+ // Top-level api.publish / api.subscribe are kept as thin aliases for
255255+ // api.pubsub.publish / api.pubsub.subscribe — several shared libraries
256256+ // in app/ (app/index.js, app/hud/widgets/*, app/lib/
257257+ // tag-action-affordances.js, app/settings/settings.js) call them
258258+ // directly and the alias avoids touching every call site.
262259 api.publish = publishImpl;
263260 api.subscribe = subscribeImpl;
264261···537534 //
538535 // Strict-only: every call routes through `tile:window:*` and is
539536 // gated on the tile's `window` capability shape (or trustedBuiltin).
540540- // Legacy `window-*` IPC handlers were removed in v1-removal Phase
541541- // 3.7h. Tiles that call api.window.* must declare a `window`
542542- // capability in their manifest. `showSelf`/`hideSelf` and `resize`
543543- // (caller's own window only) remain ungated since the token itself
544544- // is the authority.
537537+ // Tiles that call api.window.* must declare a `window` capability
538538+ // in their manifest. `showSelf`/`hideSelf` and `resize` (caller's
539539+ // own window only) remain ungated since the token itself is the
540540+ // authority.
545541546542 api.window = {
547543 /**
···891887 //
892888 // Strict surfaces (get/set/query/extractPageContent) route through
893889 // `tile:datastore:*` which enforces per-table capability. The v1-compat
894894- // helper methods were removed in Phase 4 and replaced with hard-fail stubs.
890890+ // helper methods were replaced with hard-fail stubs.
895891896892 const datastoreStrict = {
897893 /**
···947943 };
948944949945 // Named datastore methods that features rely on. Each routes to the
950950- // corresponding legacy `datastore-*` IPC channel in ipc.ts. Phase 4
951951- // originally turned these into hard-fail stubs, but feature code was
952952- // never migrated off them (Phase 3 explicitly said "keep API name"),
953953- // so the stubs broke every tile UI. Restored as thin routing wrappers.
946946+ // corresponding legacy `datastore-*` IPC channel in ipc.ts. These are
947947+ // thin routing wrappers, not strict capability-gated paths.
954948 //
955949 // Follow-up (tracked in docs/tasks.md): build equivalent strict
956950 // `tile:datastore:*` handlers for the methods that don't have them
···12411235 },
1242123612431237 /**
12441244- * Phase 4: getKey REMOVED. Use api.settings.get(key) instead.
12381238+ * Removed: use api.settings.get(key) instead.
12451239 */
12461240 getKey: (..._args: unknown[]) => {
12471247- throw new Error('[tile-preload] api.settings.getKey removed in Phase 4; use api.settings.get');
12411241+ throw new Error('[tile-preload] api.settings.getKey removed; use api.settings.get');
12481242 },
1249124312501244 /**
12511251- * Phase 4: setKey REMOVED. Use api.settings.set(key, value) instead.
12451245+ * Removed: use api.settings.set(key, value) instead.
12521246 */
12531247 setKey: (..._args: unknown[]) => {
12541254- throw new Error('[tile-preload] api.settings.setKey removed in Phase 4; use api.settings.set');
12481248+ throw new Error('[tile-preload] api.settings.setKey removed; use api.settings.set');
12551249 },
1256125012571251 /**
12581258- * Cross-tile foreign-read: fetch another tile's stored setting
12591259- * value (read-only). Dual-path routing:
12601260- *
12611261- * - STRICT: when the tile's manifest declared
12621262- * `settings.readForeign` or `settingsForeign`, route through
12631263- * `tile:settings:get-foreign`. The main-process handler checks
12641264- * the allowlist and scopes the DB query to the named foreign
12651265- * tile.
12661266- *
12671267- * - V1-COMPAT: otherwise fall back to the legacy un-gated
12681268- * `feature-settings-get-key` channel. Preserves behaviour for
12691269- * manifests that haven't declared the new allowlist yet.
12701270- *
12711271- * The decision is made per-call because `grantedCapabilities` is
12721272- * populated asynchronously by `initialize()`.
12731273- */
12741274- /**
12751275- * Phase 4: getExtKey v1-compat fallback REMOVED. Strict path only.
12761276- * Tile manifest must declare settings.readForeign to use this.
12521252+ * Cross-tile foreign-read: fetch another tile's stored setting value
12531253+ * (read-only). Strict path only — the tile's manifest must declare
12541254+ * `settings.readForeign` (or top-level `settingsForeign`). Routes
12551255+ * through `tile:settings:get-foreign`; the main-process handler
12561256+ * checks the allowlist and scopes the DB query to the named foreign
12571257+ * tile.
12771258 *
12781278- * Returns a normalised `{success, data, value, error}` object so that
12791279- * callers using either the legacy `{success, data}` shape or the strict
12801280- * `{value, error}` shape both work correctly.
12591259+ * Returns a normalised `{success, data, value, error}` object so
12601260+ * that callers using either the legacy `{success, data}` shape or
12611261+ * the strict `{value, error}` shape both work correctly.
12811262 */
12821263 getExtKey: async (extId: unknown, key: unknown) => {
12831264 if (!extId || !key) {
12841265 return { success: false, data: undefined, value: undefined, error: 'extId and key are required' };
12851266 }
12861267 if (!hasSettingsForeignCapability()) {
12871287- throw new Error('[tile-preload] api.settings.getExtKey requires settings.readForeign capability in manifest (Phase 4: v1-compat fallback removed)');
12681268+ throw new Error('[tile-preload] api.settings.getExtKey requires settings.readForeign capability in manifest');
12881269 }
12891270 // Handler returns { success, data } (native shape) — normalise into the
12901271 // legacy { success, data, value } envelope so callers using either
···1325130613261307 // ── Shortcuts ─────────────────────────────────────────────────────
13271308 //
13281328- // Dual-path implementation:
13291329- //
13301330- // - STRICT: when the tile's manifest declared a `shortcuts` capability
13311331- // (`true` or `{ keys: [...] }`), route through `tile:shortcuts:*`.
13321332- // The main-process handlers validate the token + capability + keys
13331333- // allowlist. This is the path consuming features get automatically
13341334- // once their manifest declares `capabilities.shortcuts`.
13351335- //
13361336- // - V1-COMPAT: when no shortcuts capability was declared (or the token
13371337- // hasn't been validated yet), fall back to the legacy
13381338- // `registershortcut` / `unregistershortcut` IPC channels. Those
13391339- // channels remain available for the Phase 3/4 migration per the
13401340- // tile-preload trimming plan.
13091309+ // Strict-only: route through `tile:shortcuts:*`. The main-process
13101310+ // handlers validate the token + capability + keys allowlist. Tiles
13111311+ // that call api.shortcuts.* must declare a `shortcuts` capability
13121312+ // (`true` or `{ keys: [...] }`) in their manifest; missing capability
13131313+ // throws.
13411314 //
13421342- // The decision is made per-call (not at API-build time) because
13151315+ // The capability check is per-call (not at API-build time) because
13431316 // `grantedCapabilities` is populated asynchronously by `initialize()`.
1344131713451318 function rndm(): string {
···13981371 });
13991372 }
1400137314011401- // Phase 4: registerShortcutCompat / unregisterShortcutCompat REMOVED.
14021402-14031403-// Phase 4: v1-compat shortcuts fallback REMOVED. Strict path only.
14041404- // Tiles must declare `shortcuts` in their manifest capabilities.
13741374+ // Strict-only shortcuts surface. Tiles must declare `shortcuts` in
13751375+ // their manifest capabilities.
14051376 api.shortcuts = {
14061377 register: (shortcut: unknown, cb: unknown, options: unknown) => {
14071378 if (!hasShortcutsCapability()) {
14081408- throw new Error('[tile-preload] api.shortcuts.register requires shortcuts capability in manifest (Phase 4: v1-compat fallback removed)');
13791379+ throw new Error('[tile-preload] api.shortcuts.register requires shortcuts capability in manifest');
14091380 }
14101381 registerShortcutStrict(shortcut, cb, options);
14111382 },
14121383 unregister: (shortcut: unknown, options: unknown) => {
14131384 if (!hasShortcutsCapability()) {
14141414- throw new Error('[tile-preload] api.shortcuts.unregister requires shortcuts capability in manifest (Phase 4: v1-compat fallback removed)');
13851385+ throw new Error('[tile-preload] api.shortcuts.unregister requires shortcuts capability in manifest');
14151386 }
14161387 unregisterShortcutStrict(shortcut, options);
14171388 },
14181389 };
1419139014201420- // ── Files (Phase 4: v1-compat REMOVED) ───────────────────────────
13911391+ // ── Files (removed v1-compat) ────────────────────────────────────
14211392 // Use api.dialogs.save / api.dialogs.open for file dialogs,
14221393 // and api.filesystem.read / api.filesystem.write for path operations.
14231394 api.files = {
14241424- save: (..._args: unknown[]) => { throw new Error('[tile-preload] api.files.save removed in Phase 4; use api.dialogs.save'); },
14251425- open: (..._args: unknown[]) => { throw new Error('[tile-preload] api.files.open removed in Phase 4; use api.dialogs.open'); },
14261426- readFromPath: (..._args: unknown[]) => { throw new Error('[tile-preload] api.files.readFromPath removed in Phase 4; use api.filesystem.read'); },
14271427- writeToPath: (..._args: unknown[]) => { throw new Error('[tile-preload] api.files.writeToPath removed in Phase 4; use api.filesystem.write'); },
13951395+ save: (..._args: unknown[]) => { throw new Error('[tile-preload] api.files.save removed; use api.dialogs.save'); },
13961396+ open: (..._args: unknown[]) => { throw new Error('[tile-preload] api.files.open removed; use api.dialogs.open'); },
13971397+ readFromPath: (..._args: unknown[]) => { throw new Error('[tile-preload] api.files.readFromPath removed; use api.filesystem.read'); },
13981398+ writeToPath: (..._args: unknown[]) => { throw new Error('[tile-preload] api.files.writeToPath removed; use api.filesystem.write'); },
14281399 };
1429140014301430- // ── Modes (Phase 4: REMOVED — was unused per trimming plan §1 row 6) ──
14011401+ // ── Modes (removed — was unused) ─────────────────────────────────
14311402 // api.modes was confirmed zero-usage. Hard-fail stubs catch any survivor.
14321403 api.modes = {
14331433- getWindowMode: (..._args: unknown[]) => { throw new Error('[tile-preload] api.modes removed in Phase 4 (was unused); use api.context.*'); },
14341434- setMajorMode: (..._args: unknown[]) => { throw new Error('[tile-preload] api.modes removed in Phase 4 (was unused)'); },
14351435- listModes: (..._args: unknown[]) => { throw new Error('[tile-preload] api.modes removed in Phase 4 (was unused)'); },
14361436- getCommandContext: (..._args: unknown[]) => { throw new Error('[tile-preload] api.modes removed in Phase 4 (was unused)'); },
14371437- onModeChange: (..._args: unknown[]) => { throw new Error('[tile-preload] api.modes removed in Phase 4 (was unused)'); },
14041404+ getWindowMode: (..._args: unknown[]) => { throw new Error('[tile-preload] api.modes removed (was unused); use api.context.*'); },
14051405+ setMajorMode: (..._args: unknown[]) => { throw new Error('[tile-preload] api.modes removed (was unused)'); },
14061406+ listModes: (..._args: unknown[]) => { throw new Error('[tile-preload] api.modes removed (was unused)'); },
14071407+ getCommandContext: (..._args: unknown[]) => { throw new Error('[tile-preload] api.modes removed (was unused)'); },
14081408+ onModeChange: (..._args: unknown[]) => { throw new Error('[tile-preload] api.modes removed (was unused)'); },
14381409 };
1439141014401411 // ── Context ───────────────────────────────────────────────────────
···15271498 // through `tile:dialogs:*` when the tile declared a `dialogs`
15281499 // capability (`true` or `{ types: [...] }`). When the capability is
15291500 // not declared the strict handler rejects with `dialogs capability
15301530- // not granted`.
15311531- //
15321532- // v1-compat continues through `api.files.save` / `api.files.open`,
15331533- // which directly hit the un-gated `file-save-dialog` / `file-open-
15341534- // dialog` channels. The two surfaces coexist until Phase 4 removal.
15011501+ // not granted`. The v1 `api.files.save` / `api.files.open` surface
15021502+ // is a hard-fail stub directing callers here.
1535150315361504 api.dialogs = {
15371505 save: (content: unknown, options: unknown = {}) => {
···18061774 set: (mode: string) => ipcRenderer.invoke('tile:darkMode:set', { token: tileToken, mode }),
18071775 };
1808177618091809- // ── closeWindow (Phase 4: REMOVED — was unused per trimming plan §1 row 8) ──
17771777+ // ── closeWindow (removed — was unused) ───────────────────────────
18101778 // Use api.window.close() instead.
18111779 api.closeWindow = (..._args: unknown[]) => {
18121812- throw new Error('[tile-preload] api.closeWindow removed in Phase 4 (was unused); use api.window.close()');
17801780+ throw new Error('[tile-preload] api.closeWindow removed (was unused); use api.window.close()');
18131781 };
1814178218151783 // ── Screen (always available; read-only) ──────────────────────────