···22 * Migration: Extension Settings localStorage -> Datastore
33 *
44 * One-time migration to move extension settings from localStorage
55- * to the datastore extension_settings table for cross-origin access.
55+ * to the datastore feature_settings table for cross-origin access.
66 *
77 * Run this from core background context (peek://app/background.html)
88 */
···6262 const settings = JSON.parse(storedData);
6363 console.log(`[migration] Found settings for ${oldId}:`, Object.keys(settings));
64646565- // Write each key to extension_settings table via IPC
6565+ // Write each key to feature_settings table via IPC
6666 // Note: We can't use api.settings here because we're in core context, not extension context
6767 // So we use a direct datastore call
68686969 for (const [key, value] of Object.entries(settings)) {
7070 const rowId = `${newId}:${key}`;
7171- const result = await api.datastore.setRow('extension_settings', rowId, {
7272- extensionId: newId,
7171+ const result = await api.datastore.setRow('feature_settings', rowId, {
7272+ featureId: newId,
7373 key,
7474 value: JSON.stringify(value),
7575 updatedAt: Date.now()
+3-3
app/migrations/localstorage-to-datastore.js
···22 * Migration: Core/Feature localStorage -> Datastore
33 *
44 * One-time migration to move core settings and feature configurations
55- * from localStorage to the datastore extension_settings table.
55+ * from localStorage to the datastore feature_settings table.
66 * This enables syncing across backends (Electron, Tauri).
77 *
88 * Run this from core background context (peek://app/background.html)
···7070 console.log(`[migration] Found ${storageKey}, migrating to ${namespace}:${key}`);
71717272 const rowId = `${namespace}:${key}`;
7373- const result = await api.datastore.setRow('extension_settings', rowId, {
7474- extensionId: namespace,
7373+ const result = await api.datastore.setRow('feature_settings', rowId, {
7474+ featureId: namespace,
7575 key,
7676 value: JSON.stringify(value),
7777 updatedAt: Date.now()
···174174 CREATE INDEX IF NOT EXISTS idx_extensions_status ON extensions(status);
175175 CREATE INDEX IF NOT EXISTS idx_extensions_builtin ON extensions(builtin);
176176177177- CREATE TABLE IF NOT EXISTS extension_settings (
177177+ CREATE TABLE IF NOT EXISTS feature_settings (
178178 id TEXT PRIMARY KEY,
179179- extensionId TEXT NOT NULL,
179179+ featureId TEXT NOT NULL,
180180 key TEXT NOT NULL,
181181 value TEXT,
182182 updatedAt INTEGER
183183 );
184184- CREATE INDEX IF NOT EXISTS idx_extension_settings_extensionId ON extension_settings(extensionId);
185185- CREATE UNIQUE INDEX IF NOT EXISTS idx_extension_settings_unique ON extension_settings(extensionId, key);
184184+ CREATE INDEX IF NOT EXISTS idx_feature_settings_featureId ON feature_settings(featureId);
185185+ CREATE UNIQUE INDEX IF NOT EXISTS idx_feature_settings_unique ON feature_settings(featureId, key);
186186187187 CREATE TABLE IF NOT EXISTS migrations (
188188 id TEXT PRIMARY KEY,
···375375 migrateInvalidPersonEntities();
376376 migrateInvalidPlaceEntities();
377377 migrateTagNavigationHistory();
378378+ migrateExtensionSettingsToFeatureSettings();
378379 dropLegacyAddressTables();
379380380381 // Validate schema against canonical definition
···518519 }
519520520521 const tablesToMigrate = [
521521- 'addresses', 'visits', 'tags', 'address_tags', 'extension_settings',
522522+ 'addresses', 'visits', 'tags', 'address_tags', 'feature_settings',
522523 'extensions', 'content', 'blobs', 'scripts_data', 'feeds',
523524 ];
524525···21642165 }
21652166}
2166216721682168+// ==================== Rename extension_settings -> feature_settings ====================
21692169+21702170+function migrateExtensionSettingsToFeatureSettings(): void {
21712171+ if (!db) return;
21722172+21732173+ // Check if old table exists
21742174+ const oldTableExists = db.prepare(
21752175+ `SELECT name FROM sqlite_master WHERE type='table' AND name='extension_settings'`
21762176+ ).get();
21772177+21782178+ if (!oldTableExists) return;
21792179+21802180+ DEBUG && console.log('main', 'Migrating extension_settings -> feature_settings');
21812181+21822182+ try {
21832183+ // Copy data from old table to new table (created by CREATE TABLE IF NOT EXISTS above)
21842184+ db.exec(`
21852185+ INSERT OR IGNORE INTO feature_settings (id, featureId, key, value, updatedAt)
21862186+ SELECT id, extensionId, key, value, updatedAt FROM extension_settings;
21872187+ `);
21882188+ // Drop old table and its indexes
21892189+ db.exec(`DROP TABLE IF EXISTS extension_settings`);
21902190+ DEBUG && console.log('main', 'extension_settings -> feature_settings migration complete');
21912191+ } catch (error) {
21922192+ console.error('Failed to migrate extension_settings:', error);
21932193+ }
21942194+}
21952195+21672196// ==================== Context History Operations ====================
2168219721692198export interface ContextEntry {
···2421245024222451// ==================== Mode Helpers (Context-based replacements for modes.ts) ====================
2423245224242424-export type MajorModeId = 'page' | 'group' | 'default';
24532453+export type MajorModeId = 'page' | 'space' | 'default';
2425245424262455/**
24272456 * Detect the appropriate major mode based on URL
···24502479 const currentEntry = getContextEntry('mode', windowId);
2451248024522481 // Don't override group mode with page mode (group mode persists)
24532453- if (currentEntry?.value === 'group' && detectedMode === 'page') {
24542454- DEBUG && console.log('main', `Preserving group mode for window ${windowId} during navigation`);
24822482+ if (currentEntry?.value === 'space' && detectedMode === 'page') {
24832483+ DEBUG && console.log('main', `Preserving space mode for window ${windowId} during navigation`);
24552484 return;
24562485 }
24572486···25002529 if (!db) return;
2501253025022531 const row = db.prepare(`
25032503- SELECT value FROM extension_settings
25042504- WHERE extensionId = 'system' AND key = 'datastore_version'
25322532+ SELECT value FROM feature_settings
25332533+ WHERE featureId = 'system' AND key = 'datastore_version'
25052534 `).get() as { value: string } | undefined;
2506253525072536 if (row) {
···2530255925312560 // Write current version
25322561 db.prepare(`
25332533- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
25622562+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
25342563 VALUES (?, 'system', 'datastore_version', ?, ?)
25352564 `).run('system-datastore_version', JSON.stringify(DATASTORE_VERSION), Date.now());
25362565
+4-4
backend/electron/device.ts
···13131414/**
1515 * Get or generate device ID
1616- * Stored in extension_settings table for persistence
1616+ * Stored in feature_settings table for persistence
1717 */
1818export function getDeviceId(): string {
1919 if (deviceId) {
···2525 try {
2626 // Try to get existing device ID
2727 const row = db.prepare(`
2828- SELECT value FROM extension_settings
2929- WHERE extensionId = 'system' AND key = 'deviceId'
2828+ SELECT value FROM feature_settings
2929+ WHERE featureId = 'system' AND key = 'deviceId'
3030 `).get() as { value: string } | undefined;
31313232 if (row && row.value) {
···5050 try {
5151 const jsonValue = JSON.stringify(deviceId);
5252 db.prepare(`
5353- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
5353+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
5454 VALUES (?, 'system', 'deviceId', ?, ?)
5555 `).run('system-deviceId', jsonValue, Date.now());
5656 } catch (error) {
+2-4
backend/electron/entry.ts
···5454 registerAllHandlers,
5555 restoreSavedTheme,
5656 reopenLastClosedWindow,
5757- cleanupGroupScreenBorder,
5857 // Database
5958 getDb,
6059 // Config
···358357 _beforeQuitCleanupDone = true;
359358 DEBUG && console.log('[lifecycle] before-quit: running cleanup');
360359 try { stopAutosaveTimer(); } catch (e) { DEBUG && console.error('[lifecycle] stopAutosaveTimer error:', e); }
361361- try { cleanupGroupScreenBorder(); } catch (e) { DEBUG && console.error('[lifecycle] cleanupGroupScreenBorder error:', e); }
362360 try { cleanupDisplayWatcher(); } catch (e) { DEBUG && console.error('[lifecycle] cleanupDisplayWatcher error:', e); }
363361 try { saveSessionSnapshot('before-quit'); } catch (e) { DEBUG && console.error('[lifecycle] saveSessionSnapshot error:', e); }
364362 try { cleanupDevExtensions(); } catch (e) { DEBUG && console.error('[lifecycle] cleanupDevExtensions error:', e); }
···519517 try {
520518 const db = getDb();
521519 const snapshotRow = db.prepare(
522522- `SELECT value FROM extension_settings WHERE extensionId = 'session' AND key = 'snapshot'`
520520+ `SELECT value FROM feature_settings WHERE featureId = 'session' AND key = 'snapshot'`
523521 ).get() as { value: string } | undefined;
524522 if (snapshotRow?.value) {
525523 const snapshot = JSON.parse(snapshotRow.value);
···797795 try {
798796 const earlyDb = new Database(dbPath, { readonly: true, fileMustExist: true });
799797 const row = earlyDb.prepare(
800800- "SELECT value FROM extension_settings WHERE extensionId = 'core' AND key = 'darkMode'"
798798+ "SELECT value FROM feature_settings WHERE featureId = 'core' AND key = 'darkMode'"
801799 ).get() as { value: string } | undefined;
802800 if (row?.value) {
803801 // Value is JSON-encoded (stored via setThemeSetting which JSON.stringify's)
+1-1
backend/electron/extensions.ts
···167167 // Extensions may use 'enabled' for their own purposes (e.g., HUD overlay toggle),
168168 // so we use a distinct key that only the extension manager writes.
169169 const setting = db.prepare(
170170- 'SELECT * FROM extension_settings WHERE extensionId = ? AND key = ?'
170170+ 'SELECT * FROM feature_settings WHERE featureId = ? AND key = ?'
171171 ).get(extId, 'ext_disabled') as { value?: string } | undefined;
172172173173 if (setting) {
···326326 }
327327}
328328329329-function autoTagIfGroupMode(ev: Electron.IpcMainInvokeEvent, itemId: string): boolean {
330330- try {
331331- const callingWin = BrowserWindow.fromWebContents(ev.sender);
332332- const callerWinId = callingWin && !callingWin.isDestroyed() ? callingWin.id : null;
333333- const callerMode = callerWinId ? getContextEntry('mode', callerWinId) : null;
334334-335335- // Only tag if the calling window itself is in group mode.
336336- // Do NOT fall back to lastFocusedVisibleWindowId — that causes group leakage
337337- // where items from external apps, cmd palette, etc. get tagged with whatever
338338- // group happened to be active on the last focused window.
339339- if (callerMode && callerMode.value === 'group' && callerMode.metadata?.groupId) {
340340- const groupId = callerMode.metadata.groupId as string;
341341- tagItemAndPublish(itemId, groupId);
342342- console.log('[ipc] Auto-tagged item', itemId, 'with group', groupId, '(from caller)');
343343- return true;
344344- }
345345- } catch (e) {
346346- console.log('[ipc] autoTagIfGroupMode error:', e);
347347- }
348348- return false;
349349-}
350350-351351-// Group mode screen border — transparent overlay window showing a solid colored
352352-// border around the entire screen when any window is in group mode.
353353-let groupScreenBorderWin: BrowserWindow | null = null;
354354-let groupScreenBorderColor: string | null = null;
355355-// Shutdown flag — prevents updateGroupScreenBorder from recreating the overlay
356356-// after cleanupGroupScreenBorder destroys it during quit (window:closed events
357357-// fire during shutdown and would otherwise trigger recreation).
358358-let groupScreenBorderShuttingDown = false;
359359-360360-// Strong, vivid colors derived from iOS theme palette for group screen border.
361361-// Used when the group's own color is too pale or missing.
362362-const VIVID_GROUP_COLORS = [
363363- '#ff3b30', // red
364364- '#ff9500', // orange
365365- '#34c759', // green
366366- '#007aff', // blue
367367- '#af52de', // purple
368368- '#5ac8fa', // cyan
369369- '#ff2d55', // pink
370370- '#ff9f0a', // amber
371371-];
372372-373373-/**
374374- * Pick a vivid border color: use the group's color if it has enough saturation,
375375- * otherwise deterministically pick a vivid color from the palette based on the
376376- * group ID hash.
377377- */
378378-function resolveGroupBorderColor(color: string | undefined, groupId: string | undefined): string {
379379- if (color && color !== '#999' && color !== '#999999') {
380380- // Check if the color has enough saturation/chroma to be visually distinctive.
381381- // Parse hex and check if it's not too grey.
382382- const hex = color.replace('#', '');
383383- if (hex.length >= 6) {
384384- const r = parseInt(hex.substring(0, 2), 16);
385385- const g = parseInt(hex.substring(2, 4), 16);
386386- const b = parseInt(hex.substring(4, 6), 16);
387387- const max = Math.max(r, g, b);
388388- const min = Math.min(r, g, b);
389389- const chroma = max - min;
390390- // If chroma > 40 the color is vivid enough to use directly
391391- if (chroma > 40) return color;
392392- }
393393- }
394394- // Deterministic pick from vivid palette based on groupId
395395- if (groupId) {
396396- let hash = 0;
397397- for (let i = 0; i < groupId.length; i++) {
398398- hash = ((hash << 5) - hash + groupId.charCodeAt(i)) | 0;
399399- }
400400- return VIVID_GROUP_COLORS[Math.abs(hash) % VIVID_GROUP_COLORS.length];
401401- }
402402- return '#007aff';
403403-}
404404-405405-function showGroupScreenBorder(color: string, name: string): void {
406406- // Sanitize color for CSS injection
407407- const safeColor = /^[#a-zA-Z0-9(),%\s.]+$/.test(color) ? color : '#007aff';
408408- // Sanitize name for HTML injection
409409- const safeName = name.replace(/[<>"&'\\]/g, '');
410410-411411- if (groupScreenBorderWin && !groupScreenBorderWin.isDestroyed()) {
412412- // Update color and name if changed
413413- if (groupScreenBorderColor !== safeColor) {
414414- groupScreenBorderColor = safeColor;
415415- groupScreenBorderWin.webContents.executeJavaScript(
416416- `document.documentElement.style.setProperty('--group-color', '${safeColor}')`
417417- );
418418- }
419419- groupScreenBorderWin.webContents.executeJavaScript(
420420- `document.getElementById('group-label').textContent = ${JSON.stringify(safeName)}`
421421- );
422422- if (!groupScreenBorderWin.isVisible()) {
423423- groupScreenBorderWin.showInactive();
424424- }
425425- return;
426426- }
427427-428428- groupScreenBorderColor = safeColor;
429429- const primaryDisplay = screen.getPrimaryDisplay();
430430- const { x, y, width, height } = primaryDisplay.bounds;
431431-432432- groupScreenBorderWin = new BrowserWindow({
433433- x, y, width, height,
434434- frame: false,
435435- transparent: true,
436436- hasShadow: false,
437437- alwaysOnTop: true,
438438- focusable: false,
439439- skipTaskbar: true,
440440- roundedCorners: false,
441441- resizable: false,
442442- movable: false,
443443- webPreferences: {
444444- nodeIntegration: false,
445445- contextIsolation: true,
446446- }
447447- });
448448-449449- groupScreenBorderWin.setIgnoreMouseEvents(true);
450450- groupScreenBorderWin.setVisibleOnAllWorkspaces(true);
451451- groupScreenBorderWin.setAlwaysOnTop(true, 'screen-saver');
452452-453453- const html = `<!DOCTYPE html>
454454-<html style="--group-color: ${safeColor}">
455455-<head><style>
456456- * { margin: 0; padding: 0; }
457457- html, body { width: 100vw; height: 100vh; background: transparent; overflow: hidden; }
458458- html::after {
459459- content: '';
460460- position: fixed;
461461- top: 0; left: 0; right: 0; bottom: 0;
462462- border: 4px solid var(--group-color);
463463- border-radius: 10px;
464464- pointer-events: none;
465465- }
466466- #group-label {
467467- position: fixed;
468468- bottom: 8px;
469469- right: 12px;
470470- color: var(--group-color);
471471- font-family: -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
472472- font-size: 11px;
473473- font-weight: 600;
474474- letter-spacing: 0.5px;
475475- text-transform: uppercase;
476476- pointer-events: none;
477477- }
478478-</style></head>
479479-<body>
480480- <div id="group-label">${safeName}</div>
481481-</body>
482482-</html>`;
483483-484484- groupScreenBorderWin.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`);
485485- groupScreenBorderWin.showInactive();
486486-487487- groupScreenBorderWin.on('closed', () => {
488488- groupScreenBorderWin = null;
489489- groupScreenBorderColor = null;
490490- });
491491-492492- DEBUG && console.log('[group-screen-border] Created overlay with color:', safeColor, 'name:', safeName);
493493-}
494494-495495-function hideGroupScreenBorder(): void {
496496- if (groupScreenBorderWin && !groupScreenBorderWin.isDestroyed()) {
497497- groupScreenBorderWin.hide();
498498- DEBUG && console.log('[group-screen-border] Hidden');
499499- }
500500-}
501501-502502-/**
503503- * Destroy the group screen border overlay and clear any pending timers.
504504- * Called during app shutdown to ensure Electron can quit cleanly.
505505- */
506506-export function cleanupGroupScreenBorder(): void {
507507- groupScreenBorderShuttingDown = true;
508508- if (groupBorderHideTimer) {
509509- clearTimeout(groupBorderHideTimer);
510510- groupBorderHideTimer = null;
511511- }
512512- if (groupScreenBorderWin && !groupScreenBorderWin.isDestroyed()) {
513513- groupScreenBorderWin.destroy();
514514- DEBUG && console.log('[group-screen-border] Destroyed during shutdown');
515515- }
516516- groupScreenBorderWin = null;
517517- groupScreenBorderColor = null;
518518-}
519519-520520-// Debounce timer for hiding the screen border — prevents flicker during
521521-// navigation transitions where group mode is briefly absent.
522522-let groupBorderHideTimer: ReturnType<typeof setTimeout> | null = null;
523523-const GROUP_BORDER_HIDE_DELAY_MS = 600;
524524-525525-/**
526526- * Debounce the hide — a new group window may be about to gain focus.
527527- * Prevents flicker during window transitions.
528528- */
529529-function scheduleHideGroupScreenBorder(): void {
530530- if (!groupBorderHideTimer) {
531531- groupBorderHideTimer = setTimeout(() => {
532532- groupBorderHideTimer = null;
533533- hideGroupScreenBorder();
534534- }, GROUP_BORDER_HIDE_DELAY_MS);
535535- }
536536-}
537537-538538-/**
539539- * Check if the currently focused visible window is in group mode and show/hide the screen border.
540540- * The border is a per-focused-window indicator, NOT a global "any window has group mode" indicator.
541541- * Showing is immediate; hiding is debounced to avoid flicker during window transitions.
542542- *
543543- * Special case: modal windows (cmd palette, etc.) are "transparent" to the border — focusing
544544- * a modal does not hide the border, since it would flicker constantly during normal group work.
545545- */
546546-export function updateGroupScreenBorder(): void {
547547- if (groupScreenBorderShuttingDown) return;
548548-549549- // Don't show the border when Peek is not the active/foreground app.
550550- // Without this guard, background events (sync, context changes, window management)
551551- // can re-show the border overlay even though the user switched to another app.
552552- if (!getIzuiCoordinator().isAppFocused()) {
553553- scheduleHideGroupScreenBorder();
554554- return;
555555- }
556556-557557- if (!lastFocusedVisibleWindowId) {
558558- scheduleHideGroupScreenBorder();
559559- return;
560560- }
561561- const win = BrowserWindow.fromId(lastFocusedVisibleWindowId);
562562- if (!win || win.isDestroyed()) {
563563- scheduleHideGroupScreenBorder();
564564- return;
565565- }
566566-567567- // Skip modal/transient windows (cmd palette, etc.) — they're "transparent" to border state.
568568- // When a modal is focused, keep the border in its current state (don't show or hide).
569569- const winInfo = getWindowInfo(lastFocusedVisibleWindowId);
570570- if (winInfo?.params?.modal) return;
571571-572572- const entry = getContextEntry('mode', lastFocusedVisibleWindowId);
573573- if (entry && entry.value === 'group' && entry.metadata?.groupId) {
574574- // Cancel any pending hide — focused window is in group mode
575575- if (groupBorderHideTimer) {
576576- clearTimeout(groupBorderHideTimer);
577577- groupBorderHideTimer = null;
578578- }
579579- const rawColor = entry.metadata.color as string | undefined;
580580- const groupId = entry.metadata.groupId as string | undefined;
581581- const color = resolveGroupBorderColor(rawColor, groupId);
582582- const groupName = (entry.metadata.groupName as string) || 'group';
583583- showGroupScreenBorder(color, groupName);
584584- } else {
585585- scheduleHideGroupScreenBorder();
586586- }
587587-}
588329589330/**
590331 * Register datastore IPC handlers
···613354 }
614355615356 // Emit item:created event for consistency
357357+ const callingWinId1 = BrowserWindow.fromWebContents(ev.sender)?.id ?? null;
616358 publish('system', PubSubScopes.GLOBAL, 'item:created', {
617359 itemId: result.id,
618360 itemType: 'url',
619619- content: normalizedUri
361361+ content: normalizedUri,
362362+ windowId: callingWinId1
620363 });
621621-622622- // Auto-tag with group if calling window is in group mode
623623- autoTagIfGroupMode(ev, result.id);
624364625365 return { success: true, data: result, id: result.id };
626366 } catch (error) {
···765505 return { success: false, error: `Invalid table: ${tableName}` };
766506 }
767507768768- // When promoting a tag to a group, ensure it gets a vivid color
769769- let assignedVividColor = false;
770770- if (tableName === 'tags' && rowData) {
771771- let tagMeta: Record<string, unknown> | null = null;
772772- try {
773773- tagMeta = rowData.metadata
774774- ? (typeof rowData.metadata === 'string' ? JSON.parse(rowData.metadata) : rowData.metadata)
775775- : null;
776776- } catch { /* ignore parse errors */ }
777777-778778- if (tagMeta?.isGroup && (!rowData.color || rowData.color === '#999' || rowData.color === '#999999')) {
779779- const vividColor = resolveGroupBorderColor(rowData.color, rowId);
780780- rowData.color = vividColor;
781781- assignedVividColor = true;
782782- DEBUG && console.log('[ipc] Auto-assigned vivid color to group:', rowId, vividColor);
783783- }
784784- }
785785-786508 const result = setRow(tableName, rowId, rowData);
787787-788788- // Publish color-changed event so UI dots update immediately
789789- if (assignedVividColor) {
790790- publish('system', PubSubScopes.GLOBAL, 'tag:color-changed', {
791791- tagId: rowId,
792792- color: rowData.color
793793- });
794794- }
795509796510 return { success: true, data: result };
797511 } catch (error) {
···1005719 const result = addItem(data.type, data.options);
10067201007721 // Emit item:created event
722722+ const callingWinId2 = BrowserWindow.fromWebContents(ev.sender)?.id ?? null;
1008723 publish('system', PubSubScopes.GLOBAL, 'item:created', {
1009724 itemId: result.id,
1010725 itemType: data.type,
10111011- content: data.options?.content
726726+ content: data.options?.content,
727727+ windowId: callingWinId2
1012728 });
1013729 if (DEBUG) console.log('[ipc] item:created', result.id, data.type);
10141014-10151015- // Auto-tag with group if calling window is in group mode
10161016- autoTagIfGroupMode(ev, result.id);
10177301018731 return { success: true, data: result };
1019732 } catch (error) {
···1248961 try {
1249962 const result = trackNavigation(data.uri, data.options);
125096312511251- // Auto-tag with group if calling window is in group mode
12521252- autoTagIfGroupMode(ev, result.itemId);
12531253-1254964 return { success: true, data: result };
1255965 } catch (error) {
1256966 const message = error instanceof Error ? error.message : String(error);
···1534124415351245 // Remove from database
15361246 db.prepare('DELETE FROM extensions WHERE id = ?').run(extId);
15371537- db.prepare('DELETE FROM extension_settings WHERE extensionId = ?').run(extId);
12471247+ db.prepare('DELETE FROM feature_settings WHERE featureId = ?').run(extId);
1538124815391249 return { success: true, data: { id: extId } };
15401250 } catch (error) {
···17231433 }
17241434 });
1725143517261726- // Extension settings handlers
14361436+ // Feature settings handlers (renamed from extension-settings)
17271437 // Note: preload sends { extId } but we accept both extId and id for compatibility
17281728- ipcMain.handle('extension-settings-get', async (ev, data) => {
14381438+ ipcMain.handle('feature-settings-get', async (ev, data) => {
17291439 try {
17301440 const db = getDb();
17311441 const extId = data.extId || data.id;
17321442 const settings = db.prepare(
17331733- 'SELECT * FROM extension_settings WHERE extensionId = ?'
14431443+ 'SELECT * FROM feature_settings WHERE featureId = ?'
17341444 ).all(extId) as Array<{ key: string; value: string }>;
1735144517361446 const result: Record<string, unknown> = {};
···17481458 }
17491459 });
1750146017511751- ipcMain.handle('extension-settings-set', async (ev, data) => {
14611461+ ipcMain.handle('feature-settings-set', async (ev, data) => {
17521462 try {
17531463 const db = getDb();
17541464 const extId = data.extId || data.id;
···17571467 for (const [key, value] of Object.entries(settings)) {
17581468 const jsonValue = JSON.stringify(value);
17591469 db.prepare(`
17601760- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
14701470+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
17611471 VALUES (?, ?, ?, ?, ?)
17621472 `).run(`${extId}_${key}`, extId, key, jsonValue, Date.now());
17631473 }
···17691479 }
17701480 });
1771148117721772- ipcMain.handle('extension-settings-get-key', async (ev, data) => {
14821482+ ipcMain.handle('feature-settings-get-key', async (ev, data) => {
17731483 try {
17741484 const db = getDb();
17751485 const extId = data.extId || data.id;
17761486 const setting = db.prepare(
17771777- 'SELECT * FROM extension_settings WHERE extensionId = ? AND key = ?'
14871487+ 'SELECT * FROM feature_settings WHERE featureId = ? AND key = ?'
17781488 ).get(extId, data.key) as { value: string } | undefined;
1779148917801490 if (!setting) {
···17921502 }
17931503 });
1794150417951795- ipcMain.handle('extension-settings-set-key', async (ev, data) => {
15051505+ ipcMain.handle('feature-settings-set-key', async (ev, data) => {
17961506 try {
17971507 const db = getDb();
17981508 const extId = data.extId || data.id;
17991509 const jsonValue = JSON.stringify(data.value);
18001510 db.prepare(`
18011801- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
15111511+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
18021512 VALUES (?, ?, ?, ?, ?)
18031513 `).run(`${extId}_${data.key}`, extId, data.key, jsonValue, Date.now());
18041514···18301540 }
18311541 });
1832154218331833- ipcMain.handle('extension-settings-schema', async (ev, data) => {
15431543+ ipcMain.handle('feature-settings-schema', async (ev, data) => {
18341544 try {
18351545 const extPath = getExtensionPath(data.id);
18361546 if (!extPath) {
···18721582 try {
18731583 const db = getDb();
18741584 const row = db.prepare(
18751875- 'SELECT value FROM extension_settings WHERE extensionId = ? AND key = ?'
15851585+ 'SELECT value FROM feature_settings WHERE featureId = ? AND key = ?'
18761586 ).get(THEME_SETTINGS_KEY, key) as { value: string } | undefined;
18771587 if (!row?.value) return null;
18781588 // Parse JSON-encoded value, fall back to raw value for backwards compatibility
···18941604 const db = getDb();
18951605 const timestamp = Date.now();
18961606 db.prepare(`
18971897- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
16071607+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
18981608 VALUES (?, ?, ?, ?, ?)
18991609 `).run(`${THEME_SETTINGS_KEY}_${key}`, THEME_SETTINGS_KEY, key, JSON.stringify(value), timestamp);
19001610}
···24202130 options.height = entry.bounds.height;
24212131 }
2422213224232423- if (entry.groupMode) {
24242424- options.groupMode = entry.groupMode;
21332133+ if (entry.spaceMode) {
21342134+ options.spaceMode = entry.spaceMode;
24252135 }
2426213624272137 // Publish event for the background window to pick up and open
···28092519 }
28102520 }
2811252128122812- // Check for group mode inheritance from options
28132813- // If opener passed groupMode, inherit it instead of URL-based detection
28142814- if (options.groupMode) {
28152815- const { groupId, groupName, color } = options.groupMode;
28162816- // Set group mode via context API
28172817- addContextEntry('mode', 'group', {
25222522+ // Check for space mode inheritance from options
25232523+ // If opener passed spaceMode, inherit it instead of URL-based detection
25242524+ if (options.spaceMode) {
25252525+ const { spaceId, spaceName, color } = options.spaceMode;
25262526+ // Set space mode via context API
25272527+ addContextEntry('mode', 'space', {
28182528 windowId: win.id,
28192529 source: msg.source,
28202820- metadata: { groupId, groupName, color, url: modeUrl }
25302530+ metadata: { spaceId, spaceName, color, url: modeUrl }
28212531 });
28222822- DEBUG && console.log('Inherited group mode for window:', win.id, 'group:', groupName);
25322532+ DEBUG && console.log('Inherited space mode for window:', win.id, 'space:', spaceName);
28232533 } else {
28242824- // Check if opener is in group mode - inherit if so
28252825- let openerGroupMode = null;
25342534+ // Check if opener is in space mode - inherit if so
25352535+ let openerSpaceMode = null;
28262536 const callingWin = BrowserWindow.fromWebContents(ev.sender);
28272537 if (callingWin && !callingWin.isDestroyed()) {
28282538 const openerContext = getContextEntry('mode', callingWin.id);
28292829- if (openerContext && openerContext.value === 'group' && openerContext.metadata) {
28302830- openerGroupMode = openerContext;
25392539+ if (openerContext && openerContext.value === 'space' && openerContext.metadata) {
25402540+ openerSpaceMode = openerContext;
28312541 }
28322542 }
2833254328342834- if (openerGroupMode) {
28352835- // Inherit group mode from opener (window lineage)
28362836- addContextEntry('mode', 'group', {
25442544+ if (openerSpaceMode) {
25452545+ // Inherit space mode from opener (window lineage)
25462546+ addContextEntry('mode', 'space', {
28372547 windowId: win.id,
28382548 source: msg.source,
28392549 metadata: {
28402840- ...openerGroupMode.metadata,
25502550+ ...openerSpaceMode.metadata,
28412551 url: modeUrl,
28422842- inheritedFrom: openerGroupMode.windowId
25522552+ inheritedFrom: openerSpaceMode.windowId
28432553 }
28442554 });
28452845- DEBUG && console.log('Inherited group mode from opener:', openerGroupMode.metadata?.groupName);
25552555+ DEBUG && console.log('Inherited space mode from opener:', openerSpaceMode.metadata?.spaceName);
28462556 } else {
28472847- // No group mode inheritance — only explicit groupMode option or direct
28482848- // opener lineage propagates group mode. Do NOT fall back to
28492849- // lastFocusedVisibleWindowId, as that causes group leakage to unrelated
25572557+ // No space mode inheritance — only explicit spaceMode option or direct
25582558+ // opener lineage propagates space mode. Do NOT fall back to
25592559+ // lastFocusedVisibleWindowId, as that causes space leakage to unrelated
28502560 // windows (external app opens, peeks, slides, etc.)
28512561 const detectedMode = detectModeFromUrl(modeUrl);
28522562 addContextEntry('mode', detectedMode, {
···29582668 publish('system', PubSubScopes.GLOBAL, 'item:created', {
29592669 itemId: popupTrack.itemId,
29602670 itemType: 'url',
29612961- content: popupUrl
26712671+ content: popupUrl,
26722672+ windowId: win.id
29622673 });
29632674 }
29642675 } catch (e) {
29652676 DEBUG && console.log('Failed to track webview popup:', e);
29662677 }
2967267829682968- // Inherit group mode from parent window if applicable
29692969- let groupMode: { groupId: string; groupName: string; color: string } | undefined = undefined;
26792679+ // Inherit space mode from parent window if applicable
26802680+ let spaceMode: { spaceId: string; spaceName: string; color: string } | undefined = undefined;
29702681 const parentContext = getContextEntry('mode', win.id);
29712971- if (parentContext && parentContext.value === 'group' && parentContext.metadata) {
29722972- groupMode = {
29732973- groupId: parentContext.metadata.groupId as string,
29742974- groupName: parentContext.metadata.groupName as string,
26822682+ if (parentContext && parentContext.value === 'space' && parentContext.metadata) {
26832683+ spaceMode = {
26842684+ spaceId: parentContext.metadata.spaceId as string,
26852685+ spaceName: parentContext.metadata.spaceName as string,
29752686 color: parentContext.metadata.color as string,
29762687 };
29772977- // Auto-tag popup item with the group tag
29782978- if (popupItemId) {
29792979- try {
29802980- tagItemAndPublish(popupItemId, groupMode.groupId);
29812981- DEBUG && console.log('[webview-popup] Auto-tagged popup item', popupItemId, 'with group', groupMode.groupId);
29822982- } catch (e) {
29832983- DEBUG && console.log('[webview-popup] Failed to auto-tag popup:', e);
29842984- }
29852985- }
29862688 }
2987268929882690 // Get the source address from the parent window's registration
···30502752 parentWindowId: win.id,
30512753 role: 'child-content',
30522754 };
30533053- if (groupMode) {
30543054- popupParams.groupMode = groupMode;
27552755+ if (spaceMode) {
27562756+ popupParams.spaceMode = spaceMode;
30552757 }
30562758 registerWindow(popupWin.id, source, popupParams);
30572759 trackWindow(popupWin);
30582760 coordinator.pushWindow(popupWin.id);
3059276130602762 // Set mode context (inherit group mode or detect from URL)
30613061- if (groupMode) {
30623062- addContextEntry('mode', 'group', {
27632763+ if (spaceMode) {
27642764+ addContextEntry('mode', 'space', {
30632765 windowId: popupWin.id,
30642766 source,
30653065- metadata: { ...groupMode, url: popupUrl, inheritedFrom: win.id },
27672767+ metadata: { ...spaceMode, url: popupUrl, inheritedFrom: win.id },
30662768 });
30672769 } else {
30682770 const detectedMode = detectModeFromUrl(popupUrl);
···31332835 console.log(`[page-host:${win.id}] Loading: ${loadUrl.substring(0, 120)}`);
31342836 await win.loadURL(loadUrl);
31352837 console.log(`[page-host:${win.id}] loadURL resolved`);
31363136-31373137- // Update group screen border if this window entered group mode
31383138- updateGroupScreenBorder();
3139283831402839 // Background detection for non-canvas web pages (slides, modals, quick-views).
31412840 // Canvas pages get this via the webview dom-ready handler in page.js.
···32332932 publish('system', PubSubScopes.GLOBAL, 'item:created', {
32342933 itemId: trackResult.itemId,
32352934 itemType: 'url',
32363236- content: url
29352935+ content: url,
29362936+ windowId: win.id
32372937 });
32383238- }
32393239- // Auto-tag with group if this window is in group mode
32403240- const loadModeEntry = getContextEntry('mode', win.id);
32413241- console.log('[openWindow] trackWindowLoad auto-tag check:', { winId: win.id, mode: loadModeEntry?.value, groupId: loadModeEntry?.metadata?.groupId, itemId: trackResult.itemId });
32423242- if (loadModeEntry && loadModeEntry.value === 'group' && loadModeEntry.metadata?.groupId) {
32433243- tagItemAndPublish(trackResult.itemId, loadModeEntry.metadata.groupId as string);
32443244- console.log('[openWindow] Auto-tagged item', trackResult.itemId, 'with group', loadModeEntry.metadata.groupId);
32452938 }
32462939 } catch (e) {
32472940 DEBUG && console.log('Failed to track window load:', e);
···32672960 publish('system', PubSubScopes.GLOBAL, 'item:created', {
32682961 itemId: navTrack.itemId,
32692962 itemType: 'url',
32703270- content: navUrl
29632963+ content: navUrl,
29642964+ windowId: win.id
32712965 });
32723272- }
32733273- // Auto-tag with group if this window is in group mode
32743274- const navModeEntry = getContextEntry('mode', win.id);
32753275- if (navModeEntry && navModeEntry.value === 'group' && navModeEntry.metadata?.groupId) {
32763276- tagItemAndPublish(navTrack.itemId, navModeEntry.metadata.groupId as string);
32773277- DEBUG && console.log('[did-navigate] Auto-tagged item', navTrack.itemId, 'with group', navModeEntry.metadata.groupId);
32782966 }
32792967 } catch (e) {
32802968 DEBUG && console.log('Failed to track did-navigate:', e);
···34043092 if (!win || win.isDestroyed()) return { success: false };
34053093 win.setIgnoreMouseEvents(msg.ignore, msg.forward ? { forward: true } : undefined);
34063094 return { success: true };
30953095+ });
30963096+30973097+ // Set visible on all workspaces (macOS Spaces)
30983098+ ipcMain.handle('window-set-visible-on-all-workspaces', (ev, msg) => {
30993099+ const win = msg?.id ? BrowserWindow.fromId(msg.id) : BrowserWindow.fromWebContents(ev.sender);
31003100+ if (!win || win.isDestroyed()) return { success: false };
31013101+ win.setVisibleOnAllWorkspaces(msg.visible, msg.options || {});
31023102+ return { success: true };
31033103+ });
31043104+31053105+ // Get primary display info
31063106+ ipcMain.handle('screen-get-primary-display', () => {
31073107+ const display = screen.getPrimaryDisplay();
31083108+ return {
31093109+ success: true,
31103110+ data: {
31113111+ bounds: display.bounds,
31123112+ workArea: display.workArea,
31133113+ scaleFactor: display.scaleFactor,
31143114+ size: display.size,
31153115+ }
31163116+ };
34073117 });
3408311834093119 // Return full bounds (position + size) for any window
···50464756 * New code should use the context API (api.context) instead.
50474757 */
50484758export function registerModesHandlers(): void {
50495049- // Update group screen border when focus changes — border tracks the focused window's mode
50505050- subscribe(getSystemAddress(), PubSubScopes.GLOBAL, 'window:focused', () => {
50515051- updateGroupScreenBorder();
50525052- });
50535053-50545054- // Update group screen border when windows close (focused window may have closed)
50555055- subscribe(getSystemAddress(), PubSubScopes.GLOBAL, 'window:closed', () => {
50565056- updateGroupScreenBorder();
50575057- });
50585058-50595059- // Hide group screen border when app loses focus (no window is active)
50605060- subscribe(getSystemAddress(), PubSubScopes.GLOBAL, 'app:focus-changed', (msg: unknown) => {
50615061- const data = msg as { focused: boolean };
50625062- if (!data.focused) {
50635063- scheduleHideGroupScreenBorder();
50645064- } else {
50655065- updateGroupScreenBorder();
50665066- }
50675067- });
50685068-50695069- // Reset group mode when the group tag is removed from a page
50705070- subscribe(getSystemAddress(), PubSubScopes.GLOBAL, 'tag:item-removed', (msg: unknown) => {
50715071- const data = msg as { tagId: string; itemId: string };
50725072- if (!data.tagId || !data.itemId) return;
50735073-50745074- // Find all windows in group mode for this specific tag
50755075- const groupWindowIds = getWindowsMatchingContext('mode', (entry) => {
50765076- return entry.value === 'group' && entry.metadata?.groupId === data.tagId;
50775077- });
50785078- if (groupWindowIds.length === 0) return;
50795079-50805080- // Get the item to find its URL
50815081- const item = getItem(data.itemId);
50825082- if (!item) return;
50835083- const itemUrl = item.type === 'url' && item.content ? normalizeUrl(item.content) : null;
50845084- if (!itemUrl) return;
50855085-50865086- // Check each group window — if its URL matches the removed item, reset to page mode
50875087- for (const windowId of groupWindowIds) {
50885088- const entry = getContextEntry('mode', windowId);
50895089- const windowUrl = entry?.metadata?.url as string | undefined;
50905090- if (windowUrl && normalizeUrl(windowUrl) === itemUrl) {
50915091- const result = addContextEntry('mode', 'page', {
50925092- windowId,
50935093- source: 'tag-removed',
50945094- metadata: { url: windowUrl }
50955095- });
50965096- publish(getSystemAddress(), PubSubScopes.GLOBAL, 'context:changed', {
50975097- key: 'mode',
50985098- value: 'page',
50995099- metadata: { url: windowUrl },
51005100- windowId,
51015101- source: 'tag-removed',
51025102- entryId: result.id
51035103- });
51045104- publish(getSystemAddress(), PubSubScopes.GLOBAL, 'modes:changed', {
51055105- windowId,
51065106- major: 'page',
51075107- });
51085108- DEBUG && console.log('[modes] Reset window', windowId, 'from group to page after tag removal');
51095109- }
51105110- }
51115111- updateGroupScreenBorder();
51125112- });
51135113-51144759 // Mode definitions
51154760 const MODES = [
51164761 { id: 'default', name: 'Default', description: 'Standard browsing mode' },
51174762 { id: 'page', name: 'Page', description: 'Viewing a web page' },
51185118- { id: 'group', name: 'Group', description: 'Managing tab groups' },
47634763+ { id: 'space', name: 'Space', description: 'Working in a space' },
51194764 ];
5120476551214766 // Get mode state for a window (uses context API)
···52684913 windowId,
52694914 source
52704915 });
52715271-52725272- // Update group screen border when mode changes
52735273- if (data.key === 'mode') {
52745274- updateGroupScreenBorder();
52755275- }
5276491652774917 // Publish context change event for watchers
52784918 if (publish && PubSubScopes && getSystemAddress) {
···53394979 }
53404980 });
5341498153425342- // Get windows in a specific group (convenience method for mode='group' with groupId)
53435343- ipcMain.handle('context-windows-in-group', async (ev, data: { groupId: string }) => {
49824982+ // Get windows in a specific space (convenience method for mode='space' with spaceId)
49834983+ ipcMain.handle('context-windows-in-space', async (ev, data: { spaceId: string }) => {
53444984 try {
53454985 const windowIds = getWindowsMatchingContext('mode', (entry) => {
53465346- return entry.value === 'group' && entry.metadata?.groupId === data.groupId;
49864986+ return entry.value === 'space' && entry.metadata?.spaceId === data.spaceId;
53474987 });
53484988 return { success: true, data: windowIds };
53494989 } catch (error) {
···53655005 const CORE_EXT_ID = 'core';
53665006 const PREFS_KEY = 'prefs';
53675007 const row = db.prepare(
53685368- 'SELECT value FROM extension_settings WHERE extensionId = ? AND key = ?'
50085008+ 'SELECT value FROM feature_settings WHERE featureId = ? AND key = ?'
53695009 ).get(CORE_EXT_ID, PREFS_KEY) as { value: string } | undefined;
53705010 let prefs: Record<string, unknown> = {};
53715011 if (row?.value) {
···53745014 prefs.adBlockerEnabled = enabled;
53755015 const timestamp = Date.now();
53765016 db.prepare(`
53775377- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
50175017+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
53785018 VALUES (?, ?, ?, ?, ?)
53795019 `).run(`${CORE_EXT_ID}_${PREFS_KEY}`, CORE_EXT_ID, PREFS_KEY, JSON.stringify(prefs), timestamp);
53805020 } catch (error) {
···55265166 const CORE_EXT_ID = 'core';
55275167 const KEY = 'adblocker_allowlist';
55285168 const row = db.prepare(
55295529- 'SELECT value FROM extension_settings WHERE extensionId = ? AND key = ?'
51695169+ 'SELECT value FROM feature_settings WHERE featureId = ? AND key = ?'
55305170 ).get(CORE_EXT_ID, KEY) as { value: string } | undefined;
55315171 const allowlist: string[] = row?.value ? JSON.parse(row.value) : [];
55325172 return { success: true, data: allowlist };
···55435183 const CORE_EXT_ID = 'core';
55445184 const KEY = 'adblocker_allowlist';
55455185 const row = db.prepare(
55465546- 'SELECT value FROM extension_settings WHERE extensionId = ? AND key = ?'
51865186+ 'SELECT value FROM feature_settings WHERE featureId = ? AND key = ?'
55475187 ).get(CORE_EXT_ID, KEY) as { value: string } | undefined;
55485188 const allowlist: string[] = row?.value ? JSON.parse(row.value) : [];
55495189 return { success: true, data: allowlist.includes(data.hostname) };
···55605200 const CORE_EXT_ID = 'core';
55615201 const KEY = 'adblocker_allowlist';
55625202 const row = db.prepare(
55635563- 'SELECT value FROM extension_settings WHERE extensionId = ? AND key = ?'
52035203+ 'SELECT value FROM feature_settings WHERE featureId = ? AND key = ?'
55645204 ).get(CORE_EXT_ID, KEY) as { value: string } | undefined;
55655205 let allowlist: string[] = row?.value ? JSON.parse(row.value) : [];
55665206 if (!allowlist.includes(data.hostname)) {
···55685208 }
55695209 const timestamp = Date.now();
55705210 db.prepare(`
55715571- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
52115211+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
55725212 VALUES (?, ?, ?, ?, ?)
55735213 `).run(`${CORE_EXT_ID}_${KEY}`, CORE_EXT_ID, KEY, JSON.stringify(allowlist), timestamp);
55745214 return { success: true };
···55855225 const CORE_EXT_ID = 'core';
55865226 const KEY = 'adblocker_allowlist';
55875227 const row = db.prepare(
55885588- 'SELECT value FROM extension_settings WHERE extensionId = ? AND key = ?'
52285228+ 'SELECT value FROM feature_settings WHERE featureId = ? AND key = ?'
55895229 ).get(CORE_EXT_ID, KEY) as { value: string } | undefined;
55905230 let allowlist: string[] = row?.value ? JSON.parse(row.value) : [];
55915231 allowlist = allowlist.filter(h => h !== data.hostname);
55925232 const timestamp = Date.now();
55935233 db.prepare(`
55945594- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
52345234+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
55955235 VALUES (?, ?, ?, ?, ?)
55965236 `).run(`${CORE_EXT_ID}_${KEY}`, CORE_EXT_ID, KEY, JSON.stringify(allowlist), timestamp);
55975237 return { success: true };
···56865326 });
5687532756885328 // Save group workspace layouts on demand (called from groups extension)
56895689- ipcMain.handle('save-group-workspaces', async () => {
53295329+ ipcMain.handle('save-space-workspaces', async () => {
56905330 try {
56915331 const { saveGroupWorkspaces } = await import('./session.js');
56925332 saveGroupWorkspaces();
+9-9
backend/electron/main.ts
···74747575// Built-in extensions that load in consolidated mode (iframes)
7676// External extensions (including 'example') load in separate windows
7777-const CONSOLIDATED_EXTENSION_IDS = ['cmd', 'editor', 'groups', 'hud', 'lex', 'lists', 'peeks', 'search', 'slides', 'websearch', 'windows', 'page', 'files', 'pagestream', 'sheets', 'tags', 'feeds', 'helpdocs', 'entities', 'scripts', 'timers', 'wonderwall'];
7777+const CONSOLIDATED_EXTENSION_IDS = ['cmd', 'editor', 'groups', 'hud', 'lex', 'lists', 'peeks', 'search', 'slides', 'spaces', 'websearch', 'windows', 'page', 'files', 'pagestream', 'sheets', 'tags', 'feeds', 'helpdocs', 'entities', 'scripts', 'timers', 'wonderwall'];
78787979// Extensions that must load eagerly (not lazy) — needed at startup
8080const EAGER_EXTENSION_IDS = new Set(['cmd', 'hud', 'entities']);
···103103 url: string; // Original URL (not the peek:// rewritten one)
104104 source: string;
105105 bounds: { x: number; y: number; width: number; height: number } | null;
106106- groupMode: { groupId: string; groupName: string; color?: string } | null;
106106+ spaceMode: { spaceId: string; spaceName: string; color?: string } | null;
107107 timestamp: number;
108108}
109109···301301 const isContentRole = !role || role === 'content' || role === 'child-content' || role === 'workspace';
302302303303 if (isWebUrl && isContentRole && !isKeepLive && !isModal) {
304304- // Check for group mode context
305305- let groupMode: ClosedWindowEntry['groupMode'] = null;
304304+ // Check for space mode context
305305+ let spaceMode: ClosedWindowEntry['spaceMode'] = null;
306306 try {
307307 const modeEntry = getContextEntry('mode', windowId);
308308- if (modeEntry && modeEntry.value === 'group' && modeEntry.metadata) {
309309- groupMode = {
310310- groupId: modeEntry.metadata.groupId as string,
311311- groupName: modeEntry.metadata.groupName as string,
308308+ if (modeEntry && modeEntry.value === 'space' && modeEntry.metadata) {
309309+ spaceMode = {
310310+ spaceId: modeEntry.metadata.spaceId as string,
311311+ spaceName: modeEntry.metadata.spaceName as string,
312312 color: modeEntry.metadata.color as string | undefined,
313313 };
314314 }
···335335 url: address,
336336 source: windowData.source,
337337 bounds: saveBounds,
338338- groupMode,
338338+ spaceMode,
339339 timestamp: Date.now(),
340340 });
341341 }
+23-23
backend/electron/session.test.ts
···22 * Integration tests for session save/restore
33 *
44 * These tests use a REAL better-sqlite3 database with the same schema
55- * as the production extension_settings table. They test the actual SQL
55+ * as the production feature_settings table. They test the actual SQL
66 * operations, data structures, and logic flows from session.ts.
77 *
88 * Why these tests exist:
···143143function createTestDb(): InstanceType<typeof Database> {
144144 const db = new Database(':memory:');
145145 db.exec(`
146146- CREATE TABLE IF NOT EXISTS extension_settings (
146146+ CREATE TABLE IF NOT EXISTS feature_settings (
147147 id TEXT PRIMARY KEY,
148148- extensionId TEXT NOT NULL,
148148+ featureId TEXT NOT NULL,
149149 key TEXT NOT NULL,
150150 value TEXT,
151151 updatedAt INTEGER
152152 );
153153- CREATE UNIQUE INDEX IF NOT EXISTS idx_extension_settings_unique
154154- ON extension_settings(extensionId, key);
153153+ CREATE UNIQUE INDEX IF NOT EXISTS idx_feature_settings_unique
154154+ ON feature_settings(featureId, key);
155155 `);
156156 return db;
157157}
···162162 */
163163function saveSnapshot(db: InstanceType<typeof Database>, snapshot: SessionSnapshot, reason: string): void {
164164 db.prepare(`
165165- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
165165+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
166166 VALUES (?, 'session', 'snapshot', ?, ?)
167167 `).run('session-snapshot', JSON.stringify(snapshot), Date.now());
168168···170170 let existingMetadata: Partial<SessionMetadata> = {};
171171 try {
172172 const metaRow = db.prepare(
173173- `SELECT value FROM extension_settings WHERE extensionId = 'session' AND key = 'metadata'`
173173+ `SELECT value FROM feature_settings WHERE featureId = 'session' AND key = 'metadata'`
174174 ).get() as { value: string } | undefined;
175175 if (metaRow?.value) {
176176 existingMetadata = JSON.parse(metaRow.value);
···189189 restoreCount: existingMetadata.restoreCount,
190190 };
191191 db.prepare(`
192192- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
192192+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
193193 VALUES (?, 'session', 'metadata', ?, ?)
194194 `).run('session-metadata', JSON.stringify(metadata), Date.now());
195195}
···200200function readCrashState(db: InstanceType<typeof Database>): { cleanShutdown: boolean; crashCount: number } {
201201 try {
202202 const metaRow = db.prepare(
203203- `SELECT value FROM extension_settings WHERE extensionId = 'session' AND key = 'metadata'`
203203+ `SELECT value FROM feature_settings WHERE featureId = 'session' AND key = 'metadata'`
204204 ).get() as { value: string } | undefined;
205205 if (metaRow?.value) {
206206 const metadata = JSON.parse(metaRow.value);
···220220 */
221221function markDirty(db: InstanceType<typeof Database>): void {
222222 const metaRow = db.prepare(
223223- `SELECT value FROM extension_settings WHERE extensionId = 'session' AND key = 'metadata'`
223223+ `SELECT value FROM feature_settings WHERE featureId = 'session' AND key = 'metadata'`
224224 ).get() as { value: string } | undefined;
225225226226 let metadata: Partial<SessionMetadata> = {};
···237237 metadata.cleanShutdown = false;
238238239239 db.prepare(`
240240- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
240240+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
241241 VALUES (?, 'session', 'metadata', ?, ?)
242242 `).run('session-metadata', JSON.stringify(metadata), Date.now());
243243}
···247247 */
248248function readSnapshot(db: InstanceType<typeof Database>): SessionSnapshot | null {
249249 const row = db.prepare(
250250- `SELECT value FROM extension_settings WHERE extensionId = 'session' AND key = 'snapshot'`
250250+ `SELECT value FROM feature_settings WHERE featureId = 'session' AND key = 'snapshot'`
251251 ).get() as { value: string } | undefined;
252252 if (!row?.value) return null;
253253 return JSON.parse(row.value);
···258258 */
259259function getSnapshotInfo(db: InstanceType<typeof Database>): { windowCount: number; savedAt: number; reason: string } | null {
260260 const row = db.prepare(
261261- `SELECT value FROM extension_settings WHERE extensionId = 'session' AND key = 'snapshot'`
261261+ `SELECT value FROM feature_settings WHERE featureId = 'session' AND key = 'snapshot'`
262262 ).get() as { value: string } | undefined;
263263 if (!row?.value) return null;
264264···815815816816 // Verify by reading raw metadata from DB
817817 const row = db.prepare(
818818- `SELECT value FROM extension_settings WHERE extensionId = 'session' AND key = 'metadata'`
818818+ `SELECT value FROM feature_settings WHERE featureId = 'session' AND key = 'metadata'`
819819 ).get() as { value: string };
820820 const metadata = JSON.parse(row.value);
821821 assert.strictEqual(metadata.cleanShutdown, true,
···829829 markDirty(db);
830830831831 const row = db.prepare(
832832- `SELECT value FROM extension_settings WHERE extensionId = 'session' AND key = 'metadata'`
832832+ `SELECT value FROM feature_settings WHERE featureId = 'session' AND key = 'metadata'`
833833 ).get() as { value: string };
834834 const metadata = JSON.parse(row.value);
835835 assert.strictEqual(metadata.cleanShutdown, false);
···851851 saveSnapshot(db, snapshot, 'before-quit');
852852853853 const row = db.prepare(
854854- `SELECT value FROM extension_settings WHERE extensionId = 'session' AND key = 'metadata'`
854854+ `SELECT value FROM feature_settings WHERE featureId = 'session' AND key = 'metadata'`
855855 ).get() as { value: string };
856856 const metadata = JSON.parse(row.value);
857857 assert.strictEqual(metadata.crashCount, 2, 'Save should preserve crashCount from existing metadata');
···861861 it('save preserves restoreCount from existing metadata', () => {
862862 // Manually set some restore history
863863 db.prepare(`
864864- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
864864+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
865865 VALUES (?, 'session', 'metadata', ?, ?)
866866 `).run('session-metadata', JSON.stringify({
867867 cleanShutdown: true,
···879879 saveSnapshot(db, snapshot, 'before-quit');
880880881881 const row = db.prepare(
882882- `SELECT value FROM extension_settings WHERE extensionId = 'session' AND key = 'metadata'`
882882+ `SELECT value FROM feature_settings WHERE featureId = 'session' AND key = 'metadata'`
883883 ).get() as { value: string };
884884 const metadata = JSON.parse(row.value);
885885 assert.strictEqual(metadata.restoreCount, 5, 'Save should preserve restoreCount');
···1000100010011001 // Write snapshot directly (bypassing saveSnapshot to include invalid entries)
10021002 db.prepare(`
10031003- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
10031003+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
10041004 VALUES (?, 'session', 'snapshot', ?, ?)
10051005 `).run('session-snapshot', JSON.stringify(snapshot), Date.now());
10061006···10211021 };
1022102210231023 db.prepare(`
10241024- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
10241024+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
10251025 VALUES (?, 'session', 'snapshot', ?, ?)
10261026 `).run('session-snapshot', JSON.stringify(snapshot), Date.now());
10271027···10371037 };
1038103810391039 db.prepare(`
10401040- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
10401040+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
10411041 VALUES (?, 'session', 'snapshot', ?, ?)
10421042 `).run('session-snapshot', JSON.stringify(snapshot), Date.now());
10431043···11941194 };
1195119511961196 db.prepare(`
11971197- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
11971197+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
11981198 VALUES (?, 'session', 'snapshot', ?, ?)
11991199 `).run('session-snapshot', JSON.stringify(snapshot), Date.now());
12001200···1318131813191319 // Only one snapshot row should exist (INSERT OR REPLACE)
13201320 const count = db.prepare(
13211321- `SELECT COUNT(*) as count FROM extension_settings WHERE extensionId = 'session' AND key = 'snapshot'`
13211321+ `SELECT COUNT(*) as count FROM feature_settings WHERE featureId = 'session' AND key = 'snapshot'`
13221322 ).get() as { count: number };
13231323 assert.strictEqual(count.count, 1, 'Should have exactly one snapshot row');
13241324 });
+16-16
backend/electron/session.ts
···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 extension_settings
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.
1212 *
···248248 DEBUG && console.log(`[session] Captured ${windowDescriptors.length} window(s)`);
249249250250 try {
251251- // Write snapshot to extension_settings table
251251+ // Write snapshot to feature_settings table
252252 db.prepare(`
253253- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
253253+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
254254 VALUES (?, 'session', 'snapshot', ?, ?)
255255 `).run('session-snapshot', JSON.stringify(snapshot), Date.now());
256256···258258 let existingMetadata: Partial<SessionMetadata> = {};
259259 try {
260260 const metaRow = db.prepare(
261261- `SELECT value FROM extension_settings WHERE extensionId = 'session' AND key = 'metadata'`
261261+ `SELECT value FROM feature_settings WHERE featureId = 'session' AND key = 'metadata'`
262262 ).get() as { value: string } | undefined;
263263 if (metaRow?.value) {
264264 existingMetadata = JSON.parse(metaRow.value);
···280280 restoreCount: existingMetadata.restoreCount,
281281 };
282282 db.prepare(`
283283- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
283283+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
284284 VALUES (?, 'session', 'metadata', ?, ?)
285285 `).run('session-metadata', JSON.stringify(metadata), Date.now());
286286···310310311311 try {
312312 const metaRow = db.prepare(
313313- `SELECT value FROM extension_settings WHERE extensionId = 'session' AND key = 'metadata'`
313313+ `SELECT value FROM feature_settings WHERE featureId = 'session' AND key = 'metadata'`
314314 ).get() as { value: string } | undefined;
315315316316 let metadata: Partial<SessionMetadata> = {};
···328328 metadata.cleanShutdown = false;
329329330330 db.prepare(`
331331- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
331331+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
332332 VALUES (?, 'session', 'metadata', ?, ?)
333333 `).run('session-metadata', JSON.stringify(metadata), Date.now());
334334···405405/**
406406 * Restore session snapshot on startup.
407407 *
408408- * Reads the saved snapshot from extension_settings, validates it,
408408+ * Reads the saved snapshot from feature_settings, validates it,
409409 * and recreates windows using the background window's IPC bridge.
410410 *
411411 * Returns counts of restored/failed/total windows.
···419419 try {
420420 const db = getDb();
421421 const metaRow = db.prepare(
422422- `SELECT value FROM extension_settings WHERE extensionId = 'session' AND key = 'metadata'`
422422+ `SELECT value FROM feature_settings WHERE featureId = 'session' AND key = 'metadata'`
423423 ).get() as { value: string } | undefined;
424424425425 if (metaRow?.value) {
···475475 } else {
476476 // Fallback: read current metadata (manual restore path)
477477 const metaRow = db.prepare(
478478- `SELECT value FROM extension_settings WHERE extensionId = 'session' AND key = 'metadata'`
478478+ `SELECT value FROM feature_settings WHERE featureId = 'session' AND key = 'metadata'`
479479 ).get() as { value: string } | undefined;
480480 if (metaRow?.value) {
481481 const metadata: Partial<SessionMetadata> = JSON.parse(metaRow.value);
···503503 let snapshot: SessionSnapshot;
504504 try {
505505 const row = db.prepare(
506506- `SELECT value FROM extension_settings WHERE extensionId = 'session' AND key = 'snapshot'`
506506+ `SELECT value FROM feature_settings WHERE featureId = 'session' AND key = 'snapshot'`
507507 ).get() as { value: string } | undefined;
508508509509 if (!row?.value) {
···696696 // Update session metadata with restore info and reset crashCount
697697 try {
698698 const metaRow = db.prepare(
699699- `SELECT value FROM extension_settings WHERE extensionId = 'session' AND key = 'metadata'`
699699+ `SELECT value FROM feature_settings WHERE featureId = 'session' AND key = 'metadata'`
700700 ).get() as { value: string } | undefined;
701701702702 let metadata: Partial<SessionMetadata> = {};
···710710 metadata.crashCount = 0;
711711712712 db.prepare(`
713713- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
713713+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
714714 VALUES (?, 'session', 'metadata', ?, ?)
715715 `).run('session-metadata', JSON.stringify(metadata), Date.now());
716716 } catch {
···779779}
780780781781/**
782782- * Save per-group workspace snapshots to extension_settings.
782782+ * Save per-group workspace snapshots to feature_settings.
783783 *
784784 * Groups windows by their group mode context and writes a layout snapshot
785785 * for each group. Used by openGroup() to restore window positions.
···843843844844 // Write each group's workspace snapshot
845845 const upsert = db.prepare(`
846846- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
846846+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
847847 VALUES (?, 'group-workspaces', ?, ?, ?)
848848 `);
849849···870870 try {
871871 const db = getDb();
872872 const row = db.prepare(
873873- `SELECT value FROM extension_settings WHERE extensionId = 'session' AND key = 'snapshot'`
873873+ `SELECT value FROM feature_settings WHERE featureId = 'session' AND key = 'snapshot'`
874874 ).get() as { value: string } | undefined;
875875876876 if (!row?.value) return null;
+8-8
backend/electron/shortcuts.test.ts
···225225 afterEach(() => {
226226 // Cleanup shortcuts
227227 shortcuts.unregisterLocalShortcut('Ctrl+M', 'test-mode-page', { majorMode: 'page' });
228228- shortcuts.unregisterLocalShortcut('Ctrl+M', 'test-mode-group', { majorMode: 'group' });
228228+ shortcuts.unregisterLocalShortcut('Ctrl+M', 'test-mode-group', { majorMode: 'space' });
229229 shortcuts.unregisterLocalShortcut('Ctrl+M', 'test-mode-default');
230230 datastore.cleanupWindowContext(10001);
231231 });
···253253 it('should not trigger mode-conditional shortcut when mode does not match', () => {
254254 shortcuts.registerLocalShortcut('Ctrl+M', 'test-mode-group', () => {
255255 groupCallCount++;
256256- }, { majorMode: 'group' });
256256+ }, { majorMode: 'space' });
257257258258 const input: InputEvent = {
259259 type: 'keyDown',
···264264 code: 'KeyM'
265265 };
266266267267- // Window 10001 is in 'page' mode, not 'group'
267267+ // Window 10001 is in 'page' mode, not 'space'
268268 const handled = shortcuts.handleLocalShortcut(input, 10001);
269269 assert.strictEqual(handled, false);
270270 assert.strictEqual(groupCallCount, 0);
···278278279279 shortcuts.registerLocalShortcut('Ctrl+M', 'test-mode-group', () => {
280280 groupCallCount++;
281281- }, { majorMode: 'group' });
281281+ }, { majorMode: 'space' });
282282283283 const input: InputEvent = {
284284 type: 'keyDown',
···296296 assert.strictEqual(pageCallCount, 1);
297297 assert.strictEqual(groupCallCount, 0);
298298299299- // Switch to group mode - should trigger group handler
300300- datastore.addContextEntry('mode', 'group', { windowId: 10001 });
299299+ // Switch to space mode - should trigger space handler
300300+ datastore.addContextEntry('mode', 'space', { windowId: 10001 });
301301 handled = shortcuts.handleLocalShortcut(input, 10001);
302302 assert.strictEqual(handled, true);
303303 assert.strictEqual(pageCallCount, 1); // No change
···305305 });
306306307307 it('should fall back to non-conditional shortcut when no mode match', () => {
308308- // Register mode-conditional for 'group'
308308+ // Register mode-conditional for 'space'
309309 shortcuts.registerLocalShortcut('Ctrl+M', 'test-mode-group', () => {
310310 groupCallCount++;
311311- }, { majorMode: 'group' });
311311+ }, { majorMode: 'space' });
312312313313 // Register non-conditional fallback
314314 shortcuts.registerLocalShortcut('Ctrl+M', 'test-mode-default', () => {
+19-19
backend/electron/sync.ts
···4040// ==================== Settings Storage ====================
41414242// Note: Sync configuration is now stored per-profile in profiles.db
4343-// Legacy extension_settings storage is deprecated
4343+// Legacy feature_settings storage is deprecated
44444545/**
4646 * Get sync configuration for the active profile
···80808181/**
8282 * Get server URL from settings or environment
8383- * Priority: 1. User-configured (extension_settings), 2. Env var, 3. Default
8383+ * Priority: 1. User-configured (feature_settings), 2. Env var, 3. Default
8484 */
8585function getServerUrl(): string {
8686 const db = getDb();
87878888 try {
8989- // Try to get from extension_settings
8989+ // Try to get from feature_settings
9090 const row = db.prepare(`
9191- SELECT value FROM extension_settings
9292- WHERE extensionId = 'sync' AND key = 'serverUrl'
9191+ SELECT value FROM feature_settings
9292+ WHERE featureId = 'sync' AND key = 'serverUrl'
9393 `).get() as { value: string } | undefined;
94949595 if (row && row.value) {
9696- // Values in extension_settings are JSON-stringified
9696+ // Values in feature_settings are JSON-stringified
9797 try {
9898 return JSON.parse(row.value);
9999 } catch {
···115115function setServerUrl(url: string): void {
116116 const db = getDb();
117117118118- // Values in extension_settings must be JSON-stringified
118118+ // Values in feature_settings must be JSON-stringified
119119 const jsonValue = JSON.stringify(url);
120120121121 db.prepare(`
122122- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
122122+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
123123 VALUES (?, 'sync', 'serverUrl', ?, ?)
124124 `).run(`sync-serverUrl`, jsonValue, Date.now());
125125}
126126127127/**
128128- * Get autoSync setting from extension_settings
128128+ * Get autoSync setting from feature_settings
129129 */
130130function getAutoSync(): boolean {
131131 const db = getDb();
132132133133 try {
134134 const row = db.prepare(`
135135- SELECT value FROM extension_settings
136136- WHERE extensionId = 'sync' AND key = 'autoSync'
135135+ SELECT value FROM feature_settings
136136+ WHERE featureId = 'sync' AND key = 'autoSync'
137137 `).get() as { value: string } | undefined;
138138139139 if (row && row.value) {
···159159 const jsonValue = JSON.stringify(enabled);
160160161161 db.prepare(`
162162- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
162162+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
163163 VALUES (?, 'sync', 'autoSync', ?, ?)
164164 `).run(`sync-autoSync`, jsonValue, Date.now());
165165}
···173173 try {
174174 const activeProfile = getActiveProfile();
175175176176- // Update serverUrl if provided (stored globally in extension_settings)
176176+ // Update serverUrl if provided (stored globally in feature_settings)
177177 if (config.serverUrl !== undefined && config.serverUrl !== '') {
178178 setServerUrl(config.serverUrl);
179179 DEBUG && console.log(`[sync] Updated server URL: ${config.serverUrl}`);
···642642 let storedProfileId = '';
643643 try {
644644 const urlRow = db.prepare(`
645645- SELECT value FROM extension_settings
646646- WHERE extensionId = 'sync' AND key = 'lastSyncServerUrl'
645645+ SELECT value FROM feature_settings
646646+ WHERE featureId = 'sync' AND key = 'lastSyncServerUrl'
647647 `).get() as { value: string } | undefined;
648648 if (urlRow?.value) storedUrl = JSON.parse(urlRow.value);
649649 } catch { /* first sync */ }
650650651651 try {
652652 const pidRow = db.prepare(`
653653- SELECT value FROM extension_settings
654654- WHERE extensionId = 'sync' AND key = 'lastSyncProfileId'
653653+ SELECT value FROM feature_settings
654654+ WHERE featureId = 'sync' AND key = 'lastSyncProfileId'
655655 `).get() as { value: string } | undefined;
656656 if (pidRow?.value) storedProfileId = JSON.parse(pidRow.value);
657657 } catch { /* first sync */ }
···687687 const currentProfileId = profileConfig?.serverProfileId || '';
688688689689 db.prepare(`
690690- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
690690+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
691691 VALUES (?, 'sync', 'lastSyncServerUrl', ?, ?)
692692 `).run('sync-lastSyncServerUrl', JSON.stringify(serverUrl), Date.now());
693693694694 db.prepare(`
695695- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
695695+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
696696 VALUES (?, 'sync', 'lastSyncProfileId', ?, ?)
697697 `).run('sync-lastSyncProfileId', JSON.stringify(currentProfileId), Date.now());
698698}
+1-1
backend/extension/tests/bookmarks.test.js
···22222323 // Clear stores between tests
2424 const db = (await import('../datastore.js')).getRawDb();
2525- const storeNames = ['items', 'tags', 'item_tags', 'extension_settings'];
2525+ const storeNames = ['items', 'tags', 'item_tags', 'feature_settings'];
2626 for (const name of storeNames) {
2727 const tx = db.transaction(name, 'readwrite');
2828 tx.objectStore(name).clear();
+1-1
backend/extension/tests/history.test.js
···23232424 // Clear stores between tests
2525 const db = (await import('../datastore.js')).getRawDb();
2626- const storeNames = ['items', 'tags', 'item_tags', 'extension_settings'];
2626+ const storeNames = ['items', 'tags', 'item_tags', 'feature_settings'];
2727 for (const name of storeNames) {
2828 const tx = db.transaction(name, 'readwrite');
2929 tx.objectStore(name).clear();
+1-1
backend/extension/tests/tabs.test.js
···23232424 // Clear stores between tests
2525 const db = (await import('../datastore.js')).getRawDb();
2626- const storeNames = ['items', 'tags', 'item_tags', 'extension_settings'];
2626+ const storeNames = ['items', 'tags', 'item_tags', 'feature_settings'];
2727 for (const name of storeNames) {
2828 const tx = db.transaction(name, 'readwrite');
2929 tx.objectStore(name).clear();
+10-10
backend/tauri/src-tauri/src/datastore.rs
···186186 CREATE INDEX IF NOT EXISTS idx_extensions_status ON extensions(status);
187187 CREATE INDEX IF NOT EXISTS idx_extensions_builtin ON extensions(builtin);
188188189189- CREATE TABLE IF NOT EXISTS extension_settings (
189189+ CREATE TABLE IF NOT EXISTS feature_settings (
190190 id TEXT PRIMARY KEY,
191191- extensionId TEXT NOT NULL,
191191+ featureId TEXT NOT NULL,
192192 key TEXT NOT NULL,
193193 value TEXT,
194194 updatedAt INTEGER
195195 );
196196- CREATE INDEX IF NOT EXISTS idx_extension_settings_extensionId ON extension_settings(extensionId);
197197- CREATE UNIQUE INDEX IF NOT EXISTS idx_extension_settings_unique ON extension_settings(extensionId, key);
196196+ CREATE INDEX IF NOT EXISTS idx_feature_settings_featureId ON feature_settings(featureId);
197197+ CREATE UNIQUE INDEX IF NOT EXISTS idx_feature_settings_unique ON feature_settings(featureId, key);
198198199199 CREATE TABLE IF NOT EXISTS migrations (
200200 id TEXT PRIMARY KEY,
···647647 Ok(())
648648}
649649650650-/// Check and write datastore version to extension_settings.
650650+/// Check and write datastore version to feature_settings.
651651/// If mismatch detected, sets sync_disabled flag.
652652fn check_and_write_datastore_version(conn: &Connection) -> Result<()> {
653653 let existing: Option<String> = conn
654654 .query_row(
655655- "SELECT value FROM extension_settings WHERE extensionId = 'system' AND key = 'datastoreVersion'",
655655+ "SELECT value FROM feature_settings WHERE featureId = 'system' AND key = 'datastoreVersion'",
656656 [],
657657 |row| row.get(0),
658658 )
···674674 // Write current version
675675 let timestamp = now();
676676 conn.execute(
677677- "INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt) VALUES ('system-datastoreVersion', 'system', 'datastoreVersion', ?1, ?2)",
677677+ "INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt) VALUES ('system-datastoreVersion', 'system', 'datastoreVersion', ?1, ?2)",
678678 params![DATASTORE_VERSION.to_string(), timestamp],
679679 )?;
680680···12211221 "scripts_data",
12221222 "feeds",
12231223 "extensions",
12241224- "extension_settings",
12241224+ "feature_settings",
12251225 "migrations",
12261226 "items",
12271227 "item_tags",
···12851285 "scripts_data",
12861286 "feeds",
12871287 "extensions",
12881288- "extension_settings",
12881288+ "feature_settings",
12891289 "migrations",
12901290 "items",
12911291 "item_tags",
···13511351 "scripts_data",
13521352 "feeds",
13531353 "extensions",
13541354- "extension_settings",
13541354+ "feature_settings",
13551355 "migrations",
13561356 "items",
13571357 "item_tags",
+2-2
backend/tauri/src-tauri/src/extensions.rs
···139139 ext_id: &str,
140140 is_builtin: bool,
141141) -> bool {
142142- // Query extension_settings for enabled state
142142+ // Query feature_settings for enabled state
143143 let result: Result<Option<String>, _> = db.query_row(
144144- "SELECT value FROM extension_settings WHERE extensionId = ? AND key = 'enabled'",
144144+ "SELECT value FROM feature_settings WHERE featureId = ? AND key = 'enabled'",
145145 rusqlite::params![ext_id],
146146 |row| row.get(0),
147147 );
+9-9
backend/tauri/src-tauri/src/sync.rs
···1010//!
1111//! Sync configuration is per-profile:
1212//! - apiKey + serverProfileSlug stored in profiles.db (per-profile)
1313-//! - serverUrl + autoSync stored in extension_settings (global)
1313+//! - serverUrl + autoSync stored in feature_settings (global)
1414//!
1515//! Note: rusqlite::Connection is not Send/Sync, so all async functions that need
1616//! DB access accept Arc<Mutex<Connection>> and lock/unlock around await points.
···120120// ==================== Settings Storage ====================
121121122122// Note: Sync configuration is now stored per-profile in profiles.db
123123-// Legacy extension_settings storage is deprecated for apiKey/lastSyncTime
124124-// Only serverUrl and autoSync remain global in extension_settings
123123+// Legacy feature_settings storage is deprecated for apiKey/lastSyncTime
124124+// Only serverUrl and autoSync remain global in feature_settings
125125126126const DEFAULT_SERVER_URL: &str = "https://peek-node.up.railway.app";
127127···155155}
156156157157/// Save sync configuration
158158-/// Saves serverUrl (global in extension_settings), apiKey (per-profile in profiles.db)
158158+/// Saves serverUrl (global in feature_settings), apiKey (per-profile in profiles.db)
159159pub fn set_sync_config(
160160 datastore_conn: &Connection,
161161 profiles_conn: &Connection,
162162 config: &SyncConfig,
163163) -> rusqlite::Result<()> {
164164- // Update serverUrl if provided (stored globally in extension_settings)
164164+ // Update serverUrl if provided (stored globally in feature_settings)
165165 if !config.server_url.is_empty() {
166166 set_server_url(datastore_conn, &config.server_url)?;
167167 }
···233233 )
234234}
235235236236-/// Get a setting value from extension_settings
236236+/// Get a setting value from feature_settings
237237fn get_setting(conn: &Connection, extension_id: &str, key: &str) -> Option<String> {
238238 conn.query_row(
239239- "SELECT value FROM extension_settings WHERE extensionId = ?1 AND key = ?2",
239239+ "SELECT value FROM feature_settings WHERE featureId = ?1 AND key = ?2",
240240 params![extension_id, key],
241241 |row| row.get(0),
242242 )
243243 .ok()
244244}
245245246246-/// Set a setting value in extension_settings
246246+/// Set a setting value in feature_settings
247247fn set_setting(
248248 conn: &Connection,
249249 extension_id: &str,
···253253 let id = format!("{}-{}", extension_id, key);
254254 let timestamp = datastore::now();
255255 conn.execute(
256256- "INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt) VALUES (?1, ?2, ?3, ?4, ?5)",
256256+ "INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt) VALUES (?1, ?2, ?3, ?4, ?5)",
257257 params![id, extension_id, key, value, timestamp],
258258 )?;
259259 Ok(())
+2-2
backend/tauri/src-tauri/src/theme.rs
···145145/// Get theme setting from database
146146pub fn get_theme_setting(db: &Connection, key: &str) -> Option<String> {
147147 let result: Result<String, _> = db.query_row(
148148- "SELECT value FROM extension_settings WHERE extensionId = ? AND key = ?",
148148+ "SELECT value FROM feature_settings WHERE featureId = ? AND key = ?",
149149 [THEME_SETTINGS_KEY, key],
150150 |row| row.get(0),
151151 );
···169169 let timestamp = chrono::Utc::now().timestamp_millis();
170170171171 db.execute(
172172- "INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt) VALUES (?, ?, ?, ?, ?)",
172172+ "INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt) VALUES (?, ?, ?, ?, ?)",
173173 rusqlite::params![id, THEME_SETTINGS_KEY, key, json_value, timestamp],
174174 )?;
175175
+14-14
backend/tauri/src-tauri/tests/smoke.rs
···361361 println!("✓ Extension removed");
362362}
363363364364-/// Test extension_settings operations (used for extension prefs/items)
364364+/// Test feature_settings operations (used for extension prefs/items)
365365#[test]
366366-fn test_extension_settings() {
366366+fn test_feature_settings() {
367367 let temp_dir = TempDir::new().unwrap();
368368 let db_path = temp_dir.path().join("test.sqlite");
369369 let conn = datastore::init_database(&db_path).unwrap();
···377377 ]);
378378379379 conn.execute(
380380- "INSERT INTO extension_settings (extensionId, key, value, updatedAt) VALUES (?, ?, ?, ?)",
380380+ "INSERT INTO feature_settings (featureId, key, value, updatedAt) VALUES (?, ?, ?, ?)",
381381 rusqlite::params!["peeks", "items", peeks_items.to_string(), now],
382382 ).expect("Failed to save extension settings");
383383 println!("✓ Extension settings saved");
384384385385 // Retrieve settings
386386 let value: String = conn.query_row(
387387- "SELECT value FROM extension_settings WHERE extensionId = ? AND key = ?",
387387+ "SELECT value FROM feature_settings WHERE featureId = ? AND key = ?",
388388 rusqlite::params!["peeks", "items"],
389389 |row| row.get(0),
390390 ).expect("Failed to get extension settings");
···401401 ]);
402402403403 conn.execute(
404404- "UPDATE extension_settings SET value = ?, updatedAt = ? WHERE extensionId = ? AND key = ?",
404404+ "UPDATE feature_settings SET value = ?, updatedAt = ? WHERE featureId = ? AND key = ?",
405405 rusqlite::params![updated_items.to_string(), now, "peeks", "items"],
406406 ).expect("Failed to update extension settings");
407407408408 let value: String = conn.query_row(
409409- "SELECT value FROM extension_settings WHERE extensionId = ? AND key = ?",
409409+ "SELECT value FROM feature_settings WHERE featureId = ? AND key = ?",
410410 rusqlite::params!["peeks", "items"],
411411 |row| row.get(0),
412412 ).unwrap();
···420420 let slides_prefs = serde_json::json!({"defaultPosition": "right", "defaultSize": 350});
421421422422 conn.execute(
423423- "INSERT INTO extension_settings (extensionId, key, value, updatedAt) VALUES (?, ?, ?, ?)",
423423+ "INSERT INTO feature_settings (featureId, key, value, updatedAt) VALUES (?, ?, ?, ?)",
424424 rusqlite::params!["slides", "prefs", slides_prefs.to_string(), now],
425425 ).unwrap();
426426427427 // Query all settings for an extension
428428 let mut stmt = conn.prepare(
429429- "SELECT key, value FROM extension_settings WHERE extensionId = ?"
429429+ "SELECT key, value FROM feature_settings WHERE featureId = ?"
430430 ).unwrap();
431431432432 let settings: Vec<(String, String)> = stmt.query_map(
···462462 // Add extension settings
463463 let now = datastore::now();
464464 conn.execute(
465465- "INSERT INTO extension_settings (extensionId, key, value, updatedAt) VALUES (?, ?, ?, ?)",
465465+ "INSERT INTO feature_settings (featureId, key, value, updatedAt) VALUES (?, ?, ?, ?)",
466466 rusqlite::params!["test", "data", "\"persisted\"", now],
467467 ).unwrap();
468468···491491492492 // Verify extension settings persisted
493493 let value: String = conn.query_row(
494494- "SELECT value FROM extension_settings WHERE extensionId = ? AND key = ?",
494494+ "SELECT value FROM feature_settings WHERE featureId = ? AND key = ?",
495495 rusqlite::params!["test", "data"],
496496 |row| row.get(0),
497497 ).unwrap();
···657657 lastUsed INTEGER DEFAULT 0,
658658 frecencyScore INTEGER DEFAULT 0
659659 );
660660- CREATE TABLE IF NOT EXISTS extension_settings (
660660+ CREATE TABLE IF NOT EXISTS feature_settings (
661661 id TEXT PRIMARY KEY,
662662- extensionId TEXT NOT NULL,
662662+ featureId TEXT NOT NULL,
663663 key TEXT NOT NULL,
664664 value TEXT,
665665 updatedAt INTEGER
···736736737737 // Check that version was written
738738 let version: String = conn.query_row(
739739- "SELECT value FROM extension_settings WHERE extensionId = 'system' AND key = 'datastoreVersion'",
739739+ "SELECT value FROM feature_settings WHERE featureId = 'system' AND key = 'datastoreVersion'",
740740 [],
741741 |row| row.get(0),
742742- ).expect("Version should be written to extension_settings");
742742+ ).expect("Version should be written to feature_settings");
743743744744 assert_eq!(version, datastore::DATASTORE_VERSION.to_string());
745745 println!("✓ Datastore version written: {}", version);
···376376// replicate the version check/write logic inline.
377377//
378378// The code under test (in datastore.ts) does:
379379-// 1. Read datastore_version from extension_settings
379379+// 1. Read datastore_version from feature_settings
380380// 2. If stored > code: disable sync (downgrade detected)
381381// 3. If stored < code: update stored version (upgrade)
382382-// 4. Write current version to extension_settings
382382+// 4. Write current version to feature_settings
383383384384const CODE_DATASTORE_VERSION = 1; // Matches backend/version.ts
385385···387387 const db = new Database(dbPath);
388388 db.pragma('journal_mode = WAL');
389389390390- // Create the extension_settings table (same as desktop schema)
390390+ // Create the feature_settings table (same as desktop schema)
391391 db.exec(`
392392- CREATE TABLE IF NOT EXISTS extension_settings (
392392+ CREATE TABLE IF NOT EXISTS feature_settings (
393393 id TEXT PRIMARY KEY,
394394- extensionId TEXT NOT NULL,
394394+ featureId TEXT NOT NULL,
395395 key TEXT NOT NULL,
396396 value TEXT,
397397 updatedAt INTEGER
398398 );
399399- CREATE UNIQUE INDEX IF NOT EXISTS idx_extension_settings_unique
400400- ON extension_settings(extensionId, key);
399399+ CREATE UNIQUE INDEX IF NOT EXISTS idx_feature_settings_unique
400400+ ON feature_settings(featureId, key);
401401 `);
402402403403 return db;
···409409 */
410410function checkAndWriteDatastoreVersion(db, codeVersion = CODE_DATASTORE_VERSION) {
411411 const row = db.prepare(`
412412- SELECT value FROM extension_settings
413413- WHERE extensionId = 'system' AND key = 'datastore_version'
412412+ SELECT value FROM feature_settings
413413+ WHERE featureId = 'system' AND key = 'datastore_version'
414414 `).get();
415415416416 if (row) {
···429429430430 // Write current version
431431 db.prepare(`
432432- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
432432+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
433433 VALUES (?, 'system', 'datastore_version', ?, ?)
434434 `).run('system-datastore_version', JSON.stringify(codeVersion), Date.now());
435435436436 return { syncDisabled: false };
437437}
438438439439-// Test 9: Version is written to extension_settings after init
439439+// Test 9: Version is written to feature_settings after init
440440async function test9_initWritesVersion() {
441441 const tempDir = await mkdtemp(join(tmpdir(), 'peek-version-db-'));
442442 const dbPath = join(tempDir, 'test.db');
···446446447447 // Verify version was written
448448 const row = db.prepare(`
449449- SELECT value FROM extension_settings
450450- WHERE extensionId = 'system' AND key = 'datastore_version'
449449+ SELECT value FROM feature_settings
450450+ WHERE featureId = 'system' AND key = 'datastore_version'
451451 `).get();
452452453453- assert(row, 'datastore_version should be stored in extension_settings');
453453+ assert(row, 'datastore_version should be stored in feature_settings');
454454455455 let storedVersion;
456456 try {
···492492493493 // Simulate a newer version having written version 99
494494 db.prepare(`
495495- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
495495+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
496496 VALUES (?, 'system', 'datastore_version', ?, ?)
497497 `).run('system-datastore_version', JSON.stringify(99), Date.now());
498498···502502503503 // Verify the stored version was NOT overwritten
504504 const row = db.prepare(`
505505- SELECT value FROM extension_settings
506506- WHERE extensionId = 'system' AND key = 'datastore_version'
505505+ SELECT value FROM feature_settings
506506+ WHERE featureId = 'system' AND key = 'datastore_version'
507507 `).get();
508508 let storedVersion;
509509 try {
···526526527527 // Simulate an older version having written version 0
528528 db.prepare(`
529529- INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
529529+ INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt)
530530 VALUES (?, 'system', 'datastore_version', ?, ?)
531531 `).run('system-datastore_version', JSON.stringify(0), Date.now());
532532···536536537537 // Verify version was updated
538538 const row = db.prepare(`
539539- SELECT value FROM extension_settings
540540- WHERE extensionId = 'system' AND key = 'datastore_version'
539539+ SELECT value FROM feature_settings
540540+ WHERE featureId = 'system' AND key = 'datastore_version'
541541 `).get();
542542 let storedVersion;
543543 try {
+1-1
backend/types/api.ts
···4242 * Only one major mode can be active per window at a time.
4343 * Inspired by Emacs major modes.
4444 */
4545-export type MajorModeId = 'page' | 'group' | 'default';
4545+export type MajorModeId = 'page' | 'space' | 'default';
46464747/**
4848 * Mode metadata for display and behavior
···9797 }
98989999 try {
100100- await api.context.setMode('group', {
100100+ await api.context.setMode('space', {
101101 windowId,
102102 metadata: {
103103- groupId,
104104- groupName,
103103+ spaceId: groupId,
104104+ spaceName: groupName,
105105 color
106106 }
107107 });
108108- debug && console.log(`[ext:groups] Set group mode for window ${windowId}: ${groupName}`);
108108+ debug && console.log(`[ext:groups] Set space mode for window ${windowId}: ${groupName}`);
109109 } catch (err) {
110110 console.error('[ext:groups] Failed to set group mode:', err);
111111 }
···123123124124 try {
125125 // Get all windows in this group
126126- const result = await api.context.getWindowsInGroup(groupId);
126126+ const result = await api.context.getWindowsInSpace(groupId);
127127 if (!result.success || !result.data) return;
128128129129 const windowIds = result.data;
···151151 const targetWindowId = await api.window.getFocusedVisibleWindowId();
152152 if (targetWindowId) {
153153 const modeResult = await api.context.get('mode', targetWindowId);
154154- if (modeResult.success && modeResult.data?.value === 'group' && modeResult.data.metadata?.groupId) {
154154+ if (modeResult.success && modeResult.data?.value === 'space' && modeResult.data.metadata?.spaceId) {
155155 return {
156156- groupId: modeResult.data.metadata.groupId,
157157- groupName: modeResult.data.metadata.groupName || ''
156156+ groupId: modeResult.data.metadata.spaceId,
157157+ groupName: modeResult.data.metadata.spaceName || ''
158158 };
159159 }
160160 }
···188188189189 // Save workspace layouts before hiding
190190 try {
191191- if (api.session?.saveGroupWorkspaces) {
192192- await api.session.saveGroupWorkspaces();
191191+ if (api.session?.saveSpaceWorkspaces) {
192192+ await api.session.saveSpaceWorkspaces();
193193 }
194194 } catch (err) {
195195 debug && console.log('[ext:groups] Failed to save workspace before close:', err);
196196 }
197197198198 // Get all windows in this group
199199- const result = await api.context.getWindowsInGroup(groupId);
199199+ const result = await api.context.getWindowsInSpace(groupId);
200200 if (!result.success || !result.data || result.data.length === 0) {
201201 return { success: false, error: 'No windows found in group' };
202202 }
···316316317317/**
318318 * Get pinned item IDs for a group.
319319- * Stored in extension_settings as 'pins:<groupId>'.
319319+ * Stored in feature_settings as 'pins:<groupId>'.
320320 */
321321const getPinnedItems = async (groupId) => {
322322 try {
···521521522522 // Persist current window layouts for this group
523523 try {
524524- if (api.session?.saveGroupWorkspaces) {
525525- await api.session.saveGroupWorkspaces();
524524+ if (api.session?.saveSpaceWorkspaces) {
525525+ await api.session.saveSpaceWorkspaces();
526526 debug && console.log('[ext:groups] Group workspace layouts saved');
527527 }
528528 } catch (err) {
···596596 let savedBoundsMap = null;
597597 let sortedUrlItems = urlItems;
598598 try {
599599- const wsResult = await api.settings.getExtKey('group-workspaces', 'workspace:' + tag.id);
599599+ const wsResult = await api.settings.getExtKey('spaces', 'workspace:' + tag.id);
600600 if (wsResult.success && wsResult.data) {
601601 const snapshot = typeof wsResult.data === 'string' ? JSON.parse(wsResult.data) : wsResult.data;
602602 if (snapshot.version === 1 && Array.isArray(snapshot.windows)) {
···639639 role: 'content',
640640 trackingSource: 'cmd',
641641 trackingSourceId: `group:${groupName}`,
642642- // Pass group context for mode inheritance
643643- groupMode: {
644644- groupId: tag.id,
645645- groupName: tag.name,
642642+ // Pass space context for mode inheritance
643643+ spaceMode: {
644644+ spaceId: tag.id,
645645+ spaceName: tag.name,
646646 color: tag.color
647647 },
648648 ...boundsOpts
···742742743743 console.log('[ext:groups] Noun registered: groups');
744744745745- // ===== Standalone commands (close, switch, pin, unpin) =====
746746- // These use direct pubsub registration since the noun system
747747- // only supports query/browse/open/create capabilities.
748748-749749- // "close group" — hide all windows in the current group
750750- api.subscribe('cmd:execute:close group', async (msg) => {
751751- const result = await closeGroup();
752752- if (msg.expectResult && msg.resultTopic) {
753753- api.publish(msg.resultTopic, result || { success: true }, api.scopes.GLOBAL);
754754- }
755755- }, api.scopes.GLOBAL);
756756- api.publish('cmd:register', {
757757- name: 'close group',
758758- description: 'Hide all windows in the current group (restore later)',
759759- source: 'groups',
760760- scope: 'global',
761761- accepts: [],
762762- produces: [],
763763- params: []
764764- }, api.scopes.GLOBAL);
765765-766766- // "switch group <name>" — close current group, open target
767767- api.subscribe('cmd:execute:switch group', async (msg) => {
768768- const result = await switchGroup(msg.search?.trim());
769769- if (msg.expectResult && msg.resultTopic) {
770770- api.publish(msg.resultTopic, result || { success: true }, api.scopes.GLOBAL);
771771- }
772772- }, api.scopes.GLOBAL);
773773- api.publish('cmd:register', {
774774- name: 'switch group',
775775- description: 'Close current group and open another',
776776- source: 'groups',
777777- scope: 'global',
778778- accepts: [],
779779- produces: [],
780780- params: [{ name: 'name', type: 'tag', required: true, description: 'Target group name' }]
781781- }, api.scopes.GLOBAL);
782782-783783- // "restore group <name>" — show suspended group windows
784784- api.subscribe('cmd:execute:restore group', async (msg) => {
785785- const name = msg.search?.trim();
786786- if (!name) {
787787- if (msg.expectResult && msg.resultTopic) {
788788- api.publish(msg.resultTopic, { success: false, error: 'Usage: restore group <name>' }, api.scopes.GLOBAL);
789789- }
790790- return;
791791- }
792792- // Resolve group
793793- const tagsResult = await api.datastore.getTagsByFrecency();
794794- const tag = tagsResult.success ? tagsResult.data.find(t => t.name.toLowerCase() === name.toLowerCase()) : null;
795795- const result = tag ? await restoreGroup(tag.id, tag.name) : { success: false, error: 'Group not found' };
796796- if (msg.expectResult && msg.resultTopic) {
797797- api.publish(msg.resultTopic, result || { success: true }, api.scopes.GLOBAL);
798798- }
799799- }, api.scopes.GLOBAL);
800800- api.publish('cmd:register', {
801801- name: 'restore group',
802802- description: 'Restore a previously closed (suspended) group',
803803- source: 'groups',
804804- scope: 'global',
805805- accepts: [],
806806- produces: [],
807807- params: [{ name: 'name', type: 'tag', required: true, description: 'Group name to restore' }]
808808- }, api.scopes.GLOBAL);
745745+ // ===== Standalone commands (pin, unpin) =====
746746+ // Workspace commands (close, switch, restore) moved to spaces feature.
809747810748 // "pin <url>" — mark a URL as pinned in the current group
811749 api.subscribe('cmd:execute:pin', async (msg) => {
···841779 params: [{ name: 'url', type: 'string', required: true, description: 'URL to unpin' }]
842780 }, api.scopes.GLOBAL);
843781844844- console.log('[ext:groups] Standalone commands registered: close group, switch group, restore group, pin, unpin');
782782+ console.log('[ext:groups] Standalone commands registered: pin, unpin');
845783};
846784847785const uninitCommands = () => {
848786 unregisterNoun('groups');
849787 // Unregister standalone commands
850850- for (const name of ['close group', 'switch group', 'restore group', 'pin', 'unpin']) {
788788+ for (const name of ['pin', 'unpin']) {
851789 api.publish('cmd:unregister', { name }, api.scopes.GLOBAL);
852790 }
853791 console.log('[ext:groups] Noun and commands unregistered: groups');
+1-1
features/hud/background.js
···6161};
62626363/**
6464- * Ensure the HUD sheet config exists in extension_settings.
6464+ * Ensure the HUD sheet config exists in feature_settings.
6565 * Creates a default config if none exists.
6666 */
6767const CURRENT_CONFIG_VERSION = 2;
+2-2
features/hud/hud.js
···55 * Each widget is a webview pointing to an individual widget page
66 * (e.g., peek://ext/hud/widgets/mode.html).
77 *
88- * The layout config is loaded from extension_settings (created by background.js).
88+ * The layout config is loaded from feature_settings (created by background.js).
99 */
10101111const api = window.app;
···1616let sheetConfig = null;
17171818/**
1919- * Load HUD sheet config from extension_settings
1919+ * Load HUD sheet config from feature_settings
2020 */
2121const loadSheetConfig = async () => {
2222 const result = await api.settings.getKey(HUD_SHEET_KEY);
+1-1
features/sheets/background.js
···1515const sheetPageUrl = 'peek://sheets/sheet.html';
16161717/**
1818- * Get all sheet configs from extension_settings
1818+ * Get all sheet configs from feature_settings
1919 */
2020const listSheets = async () => {
2121 const result = await api.settings.get();
+2-2
features/sheets/sheet.js
···2121};
22222323/**
2424- * Load sheet config from extension_settings
2424+ * Load sheet config from feature_settings
2525 */
2626const loadSheetConfig = async () => {
2727 const key = `sheet:${sheetId}`;
···3333};
34343535/**
3636- * Save sheet config to extension_settings
3636+ * Save sheet config to feature_settings
3737 */
3838const saveSheetConfig = async () => {
3939 if (!sheetConfig) return;