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 3.7f — migrate darkMode to strict tile:darkMode:* handlers

Move darkMode:get / darkMode:set from legacy ipc.ts handlers to strict
tile:darkMode:* handlers in tile-ipc.ts. Behavior preserved exactly
(same broadcast on change, same restartRequired tier2 logic).

- ipc.ts: export setDarkModeSetting, drop legacy darkMode:* handlers
(registerDarkModeSettingHandlers() now a no-op kept for callsite)
- tile-ipc.ts: add tile:darkMode:get/set behind trustedBuiltin grant,
import getDarkModeSetting/setDarkModeSetting/applyDarkModeSetting
from ipc.js
- tile-preload.cts: api.darkMode.{get,set} now invoke strict channels
with token

First of 5 namespace migrations in Phase 3.7f. Build green, unit tests
588/588.

+66 -47
+5 -44
backend/electron/ipc.ts
··· 590 590 /** 591 591 * Set the dark mode setting in datastore 592 592 */ 593 - function setDarkModeSetting(value: string): void { 593 + export function setDarkModeSetting(value: string): void { 594 594 setThemeSetting(DARK_MODE_KEY, value); 595 595 } 596 596 ··· 605 605 606 606 /** 607 607 * Register dark mode setting IPC handlers 608 + * 609 + * Strict `tile:darkMode:*` handlers live in tile-ipc.ts (Phase 3.7f). 610 + * Legacy `darkMode:*` ipc handlers were removed there too. 608 611 */ 609 612 function registerDarkModeSettingHandlers(): void { 610 - // Get current dark mode setting 611 - ipcMain.handle('darkMode:get', () => { 612 - const mode = getDarkModeSetting(); 613 - return { 614 - success: true, 615 - data: { 616 - mode, 617 - // Tier 2 (WebContentsForceDark) requires app restart to take effect 618 - tier2Active: app.commandLine.hasSwitch('enable-features') && 619 - (app.commandLine.getSwitchValue('enable-features') || '').includes('WebContentsForceDark'), 620 - } 621 - }; 622 - }); 623 - 624 - // Set dark mode setting 625 - ipcMain.handle('darkMode:set', (_ev, mode: string) => { 626 - if (!['off', 'system', 'force'].includes(mode)) { 627 - return { success: false, error: 'Invalid dark mode value. Must be "off", "system", or "force".' }; 628 - } 629 - 630 - const previousMode = getDarkModeSetting(); 631 - setDarkModeSetting(mode); 632 - 633 - // Apply Tier 1 immediately (nativeTheme) 634 - applyDarkModeSetting(mode); 635 - 636 - // Check if Tier 2 change requires restart 637 - const tier2Changed = (previousMode === 'force') !== (mode === 'force'); 638 - 639 - // Broadcast change to all windows 640 - const windows = BrowserWindow.getAllWindows(); 641 - for (const win of windows) { 642 - win.webContents.send('darkMode:changed', { mode }); 643 - } 644 - 645 - return { 646 - success: true, 647 - data: { 648 - mode, 649 - restartRequired: tier2Changed, 650 - } 651 - }; 652 - }); 613 + // No-op — tile:darkMode:get / tile:darkMode:set live in tile-ipc.ts. 653 614 } 654 615 655 616 /**
+59 -1
backend/electron/tile-ipc.ts
··· 86 86 import { installFromBundle } from './feature-installer.js'; 87 87 import { resolveCapabilities, validateTileManifest, detectManifestVersion } from './tile-manifest.js'; 88 88 import type { CapabilityGrant, TileCapabilities } from './tile-manifest.js'; 89 - import { invokeWindowOpen, popupToOpener, reopenLastClosedWindow, getLastFocusedVisibleWindowId } from './ipc.js'; 89 + import { invokeWindowOpen, popupToOpener, reopenLastClosedWindow, getLastFocusedVisibleWindowId, getDarkModeSetting, setDarkModeSetting, applyDarkModeSetting } from './ipc.js'; 90 90 import { 91 91 getRunningExtensions, 92 92 getAllRegisteredExtensions, 93 93 reloadExtension, 94 94 getWindowInfo, 95 + validateThemeCSS, 95 96 } from './main.js'; 96 97 import { 97 98 registerGlobalShortcut, ··· 6742 6743 return { success: false, error: 'trustedBuiltin required for tile:app:get-prefs' }; 6743 6744 } 6744 6745 return getPrefs(); 6746 + }); 6747 + 6748 + // ── tile:darkMode:* (Phase 3.7f) ───────────────────────────────────── 6749 + // 6750 + // Strict counterparts of the legacy `darkMode:get` / `darkMode:set` 6751 + // channels in ipc.ts (deleted in Phase 3.7f). Settings UI consumes 6752 + // these via api.darkMode.{get,set} in tile-preload. trustedBuiltin 6753 + // only — dark mode is global app state. 6754 + 6755 + registerTileIpc('tile:darkMode:get', { mode: 'handle' }, (event, args: { 6756 + token: string; 6757 + }, _grant) => { 6758 + if (!args?.token) return { success: false, error: 'Invalid token' }; 6759 + const grant = _grant; 6760 + if (!grant.trustedBuiltin) { 6761 + handleViolation(grant, 'darkMode', 'tile:darkMode:get', 'trustedBuiltin required', args.token); 6762 + return { success: false, error: 'trustedBuiltin required for tile:darkMode:get' }; 6763 + } 6764 + const mode = getDarkModeSetting(); 6765 + return { 6766 + success: true, 6767 + data: { 6768 + mode, 6769 + // Tier 2 (WebContentsForceDark) requires app restart to take effect. 6770 + tier2Active: app.commandLine.hasSwitch('enable-features') && 6771 + (app.commandLine.getSwitchValue('enable-features') || '').includes('WebContentsForceDark'), 6772 + }, 6773 + }; 6774 + }); 6775 + 6776 + registerTileIpc('tile:darkMode:set', { mode: 'handle' }, (event, args: { 6777 + token: string; 6778 + mode: string; 6779 + }, _grant) => { 6780 + if (!args?.token) return { success: false, error: 'Invalid token' }; 6781 + const grant = _grant; 6782 + if (!grant.trustedBuiltin) { 6783 + handleViolation(grant, 'darkMode', 'tile:darkMode:set', 'trustedBuiltin required', args.token); 6784 + return { success: false, error: 'trustedBuiltin required for tile:darkMode:set' }; 6785 + } 6786 + const mode = args.mode; 6787 + if (!['off', 'system', 'force'].includes(mode)) { 6788 + return { success: false, error: 'Invalid dark mode value. Must be "off", "system", or "force".' }; 6789 + } 6790 + 6791 + const previousMode = getDarkModeSetting(); 6792 + setDarkModeSetting(mode); 6793 + applyDarkModeSetting(mode); 6794 + 6795 + const tier2Changed = (previousMode === 'force') !== (mode === 'force'); 6796 + 6797 + // Broadcast change to all windows. 6798 + for (const win of BrowserWindow.getAllWindows()) { 6799 + win.webContents.send('darkMode:changed', { mode }); 6800 + } 6801 + 6802 + return { success: true, data: { mode, restartRequired: tier2Changed } }; 6745 6803 }); 6746 6804 6747 6805 // ── tile:nav:* strict shims (Phase 3.11b-nav-session) ────────────────
+2 -2
backend/electron/tile-preload.cts
··· 1886 1886 // 1887 1887 // Thin wrappers over `darkMode:*` ipcMain handlers. 1888 1888 api.darkMode = { 1889 - get: () => ipcRenderer.invoke('darkMode:get'), 1890 - set: (mode: string) => ipcRenderer.invoke('darkMode:set', mode), 1889 + get: () => ipcRenderer.invoke('tile:darkMode:get', { token: tileToken }), 1890 + set: (mode: string) => ipcRenderer.invoke('tile:darkMode:set', { token: tileToken, mode }), 1891 1891 }; 1892 1892 1893 1893 // ── closeWindow (Phase 4: REMOVED — was unused per trimming plan §1 row 8) ──