experiments in a post-browser web
10
fork

Configure Feed

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

feat(cmd): restore mode indicator from orphaned work (xsvtrksy)

Restores mode indicator to command bar:
- Mode indicator shows current mode (page/group/settings)
- Click to cycle through modes
- Auto-hides when in default mode
- Auto-detects page mode for http/https URLs

Originally developed in orphaned commits ysvntsoz..xsvtrksy (Jan 31)

+166 -96
+19 -89
backend/electron/main.ts
··· 17 17 import { scopes, publish, subscribe, setExtensionBroadcaster, getSystemAddress } from './pubsub.js'; 18 18 import { APP_DEF_WIDTH, APP_DEF_HEIGHT, WEB_CORE_ADDRESS, getPreloadPath, isTestProfile, isDevProfile, isHeadless, getProfile, DEBUG } from './config.js'; 19 19 import { addEscHandler, winDevtoolsConfig, closeOrHideWindow, getSystemThemeBackgroundColor, getPrefs } from './windows.js'; 20 + import { updateModeForNavigation } from './modes.js'; 20 21 21 22 // Configuration 22 23 export interface AppConfig { ··· 48 49 49 50 // Built-in extensions that load in consolidated mode (iframes) 50 51 // External extensions (including 'example') load in separate windows 51 - const CONSOLIDATED_EXTENSION_IDS = ['cmd', 'editor', 'groups', 'peeks', 'slides', 'windows']; 52 + const CONSOLIDATED_EXTENSION_IDS = ['cmd', 'editor', 'groups', 'peeks', 'slides', 'windows', 'overlay']; 52 53 53 54 // Dev extensions loaded via --load-extension CLI flag 54 55 // These are transient (not persisted) and always have devtools open ··· 771 772 let backgroundWindow: BrowserWindow | null = null; 772 773 773 774 /** 774 - * Helper to create a window for a URL (used when routing web pages through peek://page) 775 - */ 776 - async function createWindowForUrl(url: string, features: string, source: string): Promise<void> { 777 - const preloadPath = getPreloadPath(); 778 - 779 - // Parse features string 780 - const featuresMap: Record<string, unknown> = {}; 781 - if (features) { 782 - features.split(',') 783 - .map(entry => entry.split('=')) 784 - .forEach(([key, value]) => { 785 - let parsedValue: unknown = value; 786 - if (value === 'true') parsedValue = true; 787 - else if (value === 'false') parsedValue = false; 788 - else if (!isNaN(Number(value)) && value?.trim() !== '') { 789 - parsedValue = parseInt(value, 10); 790 - } 791 - featuresMap[key] = parsedValue; 792 - }); 793 - } 794 - 795 - // Determine frame setting 796 - let frameDefault = false; 797 - if (featuresMap.frame === undefined) { 798 - const prefs = getPrefs(); 799 - frameDefault = prefs.hideTitleBar === false; 800 - } 801 - 802 - const winOptions: Electron.BrowserWindowConstructorOptions = { 803 - frame: frameDefault, 804 - ...(featuresMap as Electron.BrowserWindowConstructorOptions), 805 - width: parseInt(String(featuresMap.width)) || APP_DEF_WIDTH, 806 - height: parseInt(String(featuresMap.height)) || APP_DEF_HEIGHT, 807 - show: isHeadless() ? false : featuresMap.show !== false, 808 - backgroundColor: featuresMap.transparent ? undefined : getSystemThemeBackgroundColor(), 809 - webPreferences: { 810 - preload: preloadPath, 811 - webviewTag: true 812 - } 813 - }; 814 - 815 - const win = new BrowserWindow(winOptions); 816 - 817 - try { 818 - await win.loadURL(url); 819 - registerWindow(win.id, source, { ...featuresMap, address: url }); 820 - addEscHandler(win); 821 - winDevtoolsConfig(win); 822 - 823 - if (!isHeadless()) { 824 - win.show(); 825 - } 826 - } catch (err) { 827 - console.error('Failed to create window for URL:', url, err); 828 - if (!win.isDestroyed()) { 829 - win.close(); 830 - } 831 - } 832 - } 833 - 834 - /** 835 775 * Create the core background window 836 776 */ 837 777 export function createBackgroundWindow(): BrowserWindow { ··· 870 810 win.webContents.setWindowOpenHandler((details) => { 871 811 DEBUG && console.log('Background window opening child window:', details.url); 872 812 873 - // Route http/https URLs through peek://page container 874 - // We need to deny and manually create the window since we can't change the URL here 875 - const isWebPage = details.url.startsWith('http://') || details.url.startsWith('https://'); 876 - if (isWebPage) { 877 - DEBUG && console.log('Routing web URL through peek://app/page'); 878 - // Create window asynchronously with routed URL 879 - const routedUrl = `peek://app/page/index.html?url=${encodeURIComponent(details.url)}`; 880 - createWindowForUrl(routedUrl, details.features, WEB_CORE_ADDRESS); 881 - return { action: 'deny' as const }; 882 - } 883 - 884 813 // Parse window features into options 885 814 const featuresMap: Record<string, unknown> = {}; 886 815 if (details.features) { ··· 932 861 // Don't set backgroundColor for transparent windows - it would show through 933 862 backgroundColor: featuresMap.transparent ? undefined : getSystemThemeBackgroundColor(), 934 863 webPreferences: { 935 - preload: preloadPath, 936 - webviewTag: true // Enable webview for peek://page container 864 + preload: preloadPath 937 865 } 938 866 }; 939 867 ··· 963 891 modal: featuresMap.modal 964 892 }); 965 893 966 - // Track this load in history (skip internal peek:// URLs) 967 - if (!details.url.startsWith('peek://')) { 968 - try { 969 - trackWindowLoad(details.url, { 970 - source: (featuresMap.trackingSource as string) || 'background', 971 - sourceId: (featuresMap.trackingSourceId as string) || '', 972 - windowType: featuresMap.modal ? 'modal' : 'main', 973 - title: newWin.getTitle() || '', 974 - }); 975 - } catch (e) { 976 - DEBUG && console.log('Failed to track background child window load:', e); 977 - } 894 + // Track this load in history 895 + try { 896 + trackWindowLoad(details.url, { 897 + source: (featuresMap.trackingSource as string) || 'background', 898 + sourceId: (featuresMap.trackingSourceId as string) || '', 899 + windowType: featuresMap.modal ? 'modal' : 'main', 900 + title: newWin.getTitle() || '', 901 + }); 902 + } catch (e) { 903 + DEBUG && console.log('Failed to track background child window load:', e); 978 904 } 979 905 980 - // Track in-page navigation within this window (skip peek:// URLs) 906 + // Set initial mode based on URL 907 + updateModeForNavigation(newWin.id, details.url); 908 + 909 + // Track in-page navigation within this window 981 910 newWin.webContents.on('did-navigate', (_event: Electron.Event, navUrl: string) => { 982 911 if (navUrl === details.url) return; 983 - if (navUrl.startsWith('peek://')) return; 912 + // Update mode for the new URL 913 + updateModeForNavigation(newWin.id, navUrl); 984 914 try { 985 915 trackWindowLoad(navUrl, { 986 916 source: 'navigation',
+58 -1
extensions/cmd/panel.html
··· 38 38 39 39 /* Command display wrapper */ 40 40 .command-display { 41 - width: 100%; 41 + flex: 1; 42 + min-width: 0; 42 43 min-height: 28px; 43 44 position: relative; 44 45 display: flex; ··· 387 388 background: rgba(255, 255, 255, 0.1); 388 389 border-radius: 3px; 389 390 } 391 + 392 + /* Mode indicator - tag button style */ 393 + .mode-indicator { 394 + display: flex; 395 + align-items: center; 396 + padding: 4px 10px; 397 + background: rgba(255, 255, 255, 0.15); 398 + border: 1px solid rgba(255, 255, 255, 0.2); 399 + border-radius: 12px; 400 + font-size: 11px; 401 + font-weight: 500; 402 + color: rgba(255, 255, 255, 0.9); 403 + cursor: pointer; 404 + transition: all 0.15s ease; 405 + margin-right: 10px; 406 + flex-shrink: 0; 407 + -webkit-app-region: no-drag; 408 + user-select: none; 409 + } 410 + 411 + .mode-indicator:hover { 412 + background: rgba(255, 255, 255, 0.25); 413 + border-color: rgba(255, 255, 255, 0.3); 414 + } 415 + 416 + .mode-indicator:active { 417 + background: rgba(255, 255, 255, 0.3); 418 + transform: scale(0.98); 419 + } 420 + 421 + .mode-indicator .mode-label { 422 + text-transform: capitalize; 423 + } 424 + 425 + /* Mode-specific colors */ 426 + .mode-indicator[data-mode="page"] { 427 + background: rgba(102, 153, 204, 0.4); 428 + border-color: rgba(102, 153, 204, 0.6); 429 + } 430 + 431 + .mode-indicator[data-mode="group"] { 432 + background: rgba(153, 102, 204, 0.4); 433 + border-color: rgba(153, 102, 204, 0.6); 434 + } 435 + 436 + .mode-indicator[data-mode="settings"] { 437 + background: rgba(204, 153, 102, 0.4); 438 + border-color: rgba(204, 153, 102, 0.6); 439 + } 440 + 441 + .mode-indicator[data-mode="default"] { 442 + display: none; 443 + } 390 444 </style> 391 445 </head> 392 446 <body> ··· 405 459 </div> 406 460 407 461 <div class="center-wrapper"> 462 + <div id="mode-indicator" class="mode-indicator" title="Click to cycle modes"> 463 + <span class="mode-label">default</span> 464 + </div> 408 465 <div class="command-display"> 409 466 <input id="command-input" type="text" autofocus spellcheck="false" placeholder="Type a command..." /> 410 467 <div id="command-text"></div>
+85 -2
extensions/cmd/panel.js
··· 99 99 // Loaded asynchronously when panel opens 100 100 let commandContext = null; 101 101 102 + // Major modes for cycling 103 + const MAJOR_MODES = ['default', 'page', 'group', 'settings']; 104 + 105 + // Current mode for UI display 106 + let currentMode = 'default'; 107 + 102 108 /** 103 109 * Load the current command context (target window, mode state) 104 110 * Called when panel becomes visible ··· 109 115 if (result.success) { 110 116 commandContext = result.data; 111 117 log('cmd:panel', 'Loaded command context:', commandContext); 118 + // Update mode from context 119 + if (commandContext?.mode?.major) { 120 + currentMode = commandContext.mode.major; 121 + updateModeIndicator(); 122 + } 112 123 } 113 124 } catch (err) { 114 125 log.error('cmd:panel', 'Failed to load command context:', err); 115 126 commandContext = null; 116 127 } 117 128 }; 129 + 130 + // ===== Mode Indicator Functions ===== 131 + 132 + /** 133 + * Update the mode indicator UI 134 + */ 135 + function updateModeIndicator() { 136 + const indicator = document.getElementById('mode-indicator'); 137 + if (!indicator) return; 138 + 139 + const label = indicator.querySelector('.mode-label'); 140 + indicator.setAttribute('data-mode', currentMode); 141 + label.textContent = currentMode; 142 + 143 + log('cmd:panel', 'Mode indicator updated:', currentMode); 144 + } 145 + 146 + /** 147 + * Cycle to the next major mode 148 + */ 149 + async function cycleMode() { 150 + const currentIndex = MAJOR_MODES.indexOf(currentMode); 151 + const nextIndex = (currentIndex + 1) % MAJOR_MODES.length; 152 + const nextMode = MAJOR_MODES[nextIndex]; 153 + 154 + log('cmd:panel', 'Cycling mode from', currentMode, 'to', nextMode); 155 + 156 + // Set the mode via the API 157 + const result = await api.modes.setMajorMode(nextMode); 158 + if (result.success) { 159 + currentMode = nextMode; 160 + updateModeIndicator(); 161 + // Reload command context with new mode 162 + await loadCommandContext(); 163 + } else { 164 + log.error('cmd:panel', 'Failed to set mode:', result.error); 165 + } 166 + } 167 + 168 + /** 169 + * Initialize the mode indicator 170 + */ 171 + async function initModeIndicator() { 172 + // Use commandContext which has the target window's mode (not the cmd panel's mode) 173 + // commandContext is loaded by loadCommandContext() before this is called 174 + if (commandContext?.mode?.major) { 175 + currentMode = commandContext.mode.major; 176 + } 177 + updateModeIndicator(); 178 + 179 + // Set up click handler for cycling 180 + const indicator = document.getElementById('mode-indicator'); 181 + if (indicator) { 182 + indicator.addEventListener('click', (e) => { 183 + e.preventDefault(); 184 + e.stopPropagation(); 185 + cycleMode(); 186 + }); 187 + } 188 + 189 + // Subscribe to mode changes 190 + api.modes.onModeChange((modeState, windowId) => { 191 + log('cmd:panel', 'Mode changed:', modeState, 'for window:', windowId); 192 + currentMode = modeState.major || 'default'; 193 + updateModeIndicator(); 194 + }); 195 + 196 + log('cmd:panel', 'Mode indicator initialized with mode:', currentMode); 197 + } 118 198 119 199 /** 120 200 * Check if a command is available in the current context ··· 242 322 } 243 323 }); 244 324 245 - // Load command context on initial render 246 - loadCommandContext(); 325 + // Load command context on initial render (must complete before mode indicator init) 326 + await loadCommandContext(); 327 + 328 + // Initialize mode indicator (uses commandContext loaded above) 329 + initModeIndicator(); 247 330 248 331 // Chain cancel button handler 249 332 const chainCancelBtn = document.getElementById('chain-cancel');
+1 -1
schema/generated/sqlite-full.sql
··· 1 1 -- Generated by schema/codegen.js 2 2 -- Schema version: 1 3 - -- Generated: 2026-02-04T14:18:35.215Z 3 + -- Generated: 2026-02-04T15:12:50.182Z 4 4 -- DO NOT EDIT - regenerate with: yarn schema:codegen 5 5 6 6 -- Unified content storage - URLs, text notes, tagsets, and images
+1 -1
schema/generated/sqlite-sync.sql
··· 1 1 -- Generated by schema/codegen.js 2 2 -- Schema version: 1 3 - -- Generated: 2026-02-04T14:18:35.216Z 3 + -- Generated: 2026-02-04T15:12:50.183Z 4 4 -- DO NOT EDIT - regenerate with: yarn schema:codegen 5 5 6 6 -- Unified content storage - URLs, text notes, tagsets, and images
+1 -1
schema/generated/types.rs
··· 1 1 // Generated by schema/codegen.js 2 2 // Schema version: 1 3 - // Generated: 2026-02-04T14:18:35.216Z 3 + // Generated: 2026-02-04T15:12:50.183Z 4 4 // DO NOT EDIT - regenerate with: yarn schema:codegen 5 5 6 6 use serde::{Deserialize, Serialize};
+1 -1
schema/generated/types.ts
··· 1 1 /** 2 2 * Generated by schema/codegen.js 3 3 * Schema version: 1 4 - * Generated: 2026-02-04T14:18:35.216Z 4 + * Generated: 2026-02-04T15:12:50.183Z 5 5 * DO NOT EDIT - regenerate with: yarn schema:codegen 6 6 */ 7 7