experiments in a post-browser web
10
fork

Configure Feed

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

docs: update widgets/HUD research with current state (2026-02-09)

Updates research doc to reflect current codebase:

Current State Section Added:
- IZUI: OS-level app focus, IzuiSession model, new APIs
- Context/Modes: First-class context keys with history tracking
- Window Management: Current API surface and options
- Pubsub: Available system topics

HUD Integration Points:
- api.context.watch('mode') for mode tracking
- api.izui.getState() for IZUI display
- api.window.getFocusedVisibleWindowId() for active window
- Mode metadata includes groupId/groupName

Implementation Details:
- Updated window setup code with current options
- Reactive update patterns using context API
- Example code for mode/state watching

Status: Basic HUD in progress (agent implementing)

Based on exploration agent findings reviewing:
- backend/electron/izui-state.ts
- preload.js context/modes APIs
- backend/electron/windows.ts
- Extension infrastructure

+689 -2
+1 -1
backend/electron/main.ts
··· 50 50 51 51 // Built-in extensions that load in consolidated mode (iframes) 52 52 // External extensions (including 'example') load in separate windows 53 - const CONSOLIDATED_EXTENSION_IDS = ['cmd', 'editor', 'groups', 'peeks', 'slides', 'windows']; 53 + const CONSOLIDATED_EXTENSION_IDS = ['cmd', 'editor', 'groups', 'hud', 'peeks', 'slides', 'windows']; 54 54 55 55 // Dev extensions loaded via --load-extension CLI flag 56 56 // These are transient (not persisted) and always have devtools open
+10
extensions/hud/background.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <title>HUD Extension Background</title> 6 + </head> 7 + <body> 8 + <script type="module" src="./background.js"></script> 9 + </body> 10 + </html>
+202
extensions/hud/background.js
··· 1 + /** 2 + * HUD Extension Background Script 3 + * 4 + * Always-on-top overlay showing current mode, IZUI state, and window context. 5 + * Runs in isolated extension process (peek://ext/hud/background.html) 6 + */ 7 + 8 + const api = window.app; 9 + const debug = api.debug; 10 + 11 + console.log('[ext:hud] background init'); 12 + 13 + const HUD_ADDRESS = 'peek://hud/hud.html'; 14 + const STORAGE_KEY = 'hud_enabled'; 15 + 16 + // Track HUD state 17 + let hudEnabled = false; 18 + let hudWindowId = null; 19 + 20 + /** 21 + * Load HUD enabled state from localStorage 22 + */ 23 + const loadState = () => { 24 + const stored = localStorage.getItem(STORAGE_KEY); 25 + hudEnabled = stored === 'true'; 26 + console.log('[ext:hud] Loaded state - enabled:', hudEnabled); 27 + return hudEnabled; 28 + }; 29 + 30 + /** 31 + * Save HUD enabled state to localStorage 32 + */ 33 + const saveState = (enabled) => { 34 + hudEnabled = enabled; 35 + localStorage.setItem(STORAGE_KEY, String(enabled)); 36 + console.log('[ext:hud] Saved state - enabled:', enabled); 37 + }; 38 + 39 + /** 40 + * Open the HUD window 41 + */ 42 + const openHud = async () => { 43 + if (hudWindowId) { 44 + // Already open, just show it 45 + const exists = await api.window.exists(hudWindowId); 46 + if (exists.success && exists.data) { 47 + await api.window.show(hudWindowId); 48 + console.log('[ext:hud] HUD already open, showing'); 49 + return; 50 + } 51 + hudWindowId = null; 52 + } 53 + 54 + const params = { 55 + key: HUD_ADDRESS, 56 + width: 300, 57 + height: 200, 58 + x: 20, 59 + y: 20, 60 + transparent: true, 61 + alwaysOnTop: true, 62 + skipTaskbar: true, 63 + resizable: false, 64 + frame: false, 65 + focusable: false, 66 + escapeMode: 'ignore', 67 + title: 'HUD' 68 + }; 69 + 70 + try { 71 + const result = await api.window.open(HUD_ADDRESS, params); 72 + if (result.success) { 73 + hudWindowId = result.id; 74 + console.log('[ext:hud] HUD opened:', hudWindowId); 75 + 76 + // Make sure it's always on top with floating level 77 + await api.invoke('window-set-always-on-top', { 78 + id: hudWindowId, 79 + value: true, 80 + level: 'floating' 81 + }); 82 + } else { 83 + console.error('[ext:hud] Failed to open HUD:', result.error); 84 + } 85 + } catch (error) { 86 + console.error('[ext:hud] Error opening HUD:', error); 87 + } 88 + }; 89 + 90 + /** 91 + * Close the HUD window 92 + */ 93 + const closeHud = async () => { 94 + if (hudWindowId) { 95 + await api.window.close(hudWindowId); 96 + hudWindowId = null; 97 + console.log('[ext:hud] HUD closed'); 98 + } 99 + }; 100 + 101 + /** 102 + * Toggle HUD on/off 103 + */ 104 + const toggleHud = async () => { 105 + const newState = !hudEnabled; 106 + saveState(newState); 107 + 108 + if (newState) { 109 + await openHud(); 110 + return { output: 'HUD enabled', mimeType: 'text/plain' }; 111 + } else { 112 + await closeHud(); 113 + return { output: 'HUD disabled', mimeType: 'text/plain' }; 114 + } 115 + }; 116 + 117 + // ===== Command definitions ===== 118 + 119 + const commandDefinitions = [ 120 + { 121 + name: 'hud', 122 + description: 'Toggle HUD overlay on/off', 123 + execute: async (ctx) => { 124 + console.log('[ext:hud] Toggle command executed'); 125 + return await toggleHud(); 126 + } 127 + } 128 + ]; 129 + 130 + // ===== Registration ===== 131 + 132 + let registeredShortcut = null; 133 + let registeredCommands = []; 134 + 135 + const initShortcut = () => { 136 + const shortcut = 'Command+Shift+H'; 137 + api.shortcuts.register(shortcut, async () => { 138 + await toggleHud(); 139 + }, { global: true }); 140 + registeredShortcut = shortcut; 141 + console.log('[ext:hud] Registered shortcut:', shortcut); 142 + }; 143 + 144 + const initCommands = async () => { 145 + commandDefinitions.forEach(cmd => { 146 + api.commands.register(cmd); 147 + registeredCommands.push(cmd.name); 148 + }); 149 + console.log('[ext:hud] Registered commands:', registeredCommands); 150 + }; 151 + 152 + const uninitCommands = () => { 153 + registeredCommands.forEach(name => { 154 + api.commands.unregister(name); 155 + }); 156 + registeredCommands = []; 157 + console.log('[ext:hud] Unregistered commands'); 158 + }; 159 + 160 + const init = async () => { 161 + console.log('[ext:hud] init'); 162 + 163 + // Load persisted state 164 + loadState(); 165 + 166 + // Register shortcut 167 + initShortcut(); 168 + 169 + // Wait for cmd:ready before registering commands 170 + api.subscribe('cmd:ready', () => { 171 + initCommands(); 172 + }, api.scopes.GLOBAL); 173 + 174 + // Query in case cmd is already ready 175 + api.publish('cmd:query', {}, api.scopes.GLOBAL); 176 + 177 + // Open HUD if it was enabled 178 + if (hudEnabled) { 179 + await openHud(); 180 + } 181 + 182 + console.log('[ext:hud] Initialized, enabled:', hudEnabled); 183 + }; 184 + 185 + const uninit = () => { 186 + console.log('[ext:hud] uninit'); 187 + if (registeredShortcut) { 188 + api.shortcuts.unregister(registeredShortcut, { global: true }); 189 + registeredShortcut = null; 190 + } 191 + uninitCommands(); 192 + closeHud(); 193 + }; 194 + 195 + export default { 196 + id: 'hud', 197 + labels: { 198 + name: 'HUD' 199 + }, 200 + init, 201 + uninit 202 + };
+34
extensions/hud/hud.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>HUD</title> 7 + <link rel="stylesheet" href="./styles.css"> 8 + </head> 9 + <body> 10 + <div id="hud-container"> 11 + <div class="hud-section"> 12 + <div class="hud-label">Mode</div> 13 + <div id="mode-value" class="hud-value">-</div> 14 + </div> 15 + 16 + <div class="hud-section"> 17 + <div class="hud-label">IZUI State</div> 18 + <div id="izui-value" class="hud-value">-</div> 19 + </div> 20 + 21 + <div class="hud-section"> 22 + <div class="hud-label">Active Window</div> 23 + <div id="window-title" class="hud-value hud-truncate">-</div> 24 + </div> 25 + 26 + <div class="hud-section"> 27 + <div class="hud-label">Stats</div> 28 + <div id="stats-value" class="hud-value hud-small">-</div> 29 + </div> 30 + </div> 31 + 32 + <script type="module" src="./hud.js"></script> 33 + </body> 34 + </html>
+224
extensions/hud/hud.js
··· 1 + /** 2 + * HUD Display Script 3 + * 4 + * Reactive display showing current mode, IZUI state, and window context. 5 + */ 6 + 7 + const api = window.app; 8 + const debug = api.debug; 9 + 10 + console.log('[hud] Display script loaded'); 11 + 12 + // DOM elements 13 + const modeValue = document.getElementById('mode-value'); 14 + const izuiValue = document.getElementById('izui-value'); 15 + const windowTitle = document.getElementById('window-title'); 16 + const statsValue = document.getElementById('stats-value'); 17 + 18 + // Current state 19 + let currentMode = 'default'; 20 + let currentModeMetadata = {}; 21 + let currentIzuiState = 'idle'; 22 + let currentWindowInfo = null; 23 + let windowCount = 0; 24 + 25 + /** 26 + * Update mode display 27 + */ 28 + const updateModeDisplay = () => { 29 + if (!modeValue) return; 30 + 31 + // Remove old mode classes 32 + modeValue.classList.remove('mode-default', 'mode-page', 'mode-group', 'mode-settings'); 33 + 34 + // Format mode display 35 + let displayText = currentMode; 36 + if (currentMode === 'group' && currentModeMetadata.groupName) { 37 + displayText = `group: ${currentModeMetadata.groupName}`; 38 + } 39 + 40 + modeValue.textContent = displayText; 41 + modeValue.classList.add(`mode-${currentMode}`); 42 + 43 + debug && console.log('[hud] Updated mode display:', displayText); 44 + }; 45 + 46 + /** 47 + * Update IZUI state display 48 + */ 49 + const updateIzuiDisplay = () => { 50 + if (!izuiValue) return; 51 + 52 + // Remove old state classes 53 + izuiValue.classList.remove('izui-idle', 'izui-transient', 'izui-active', 'izui-overlay'); 54 + 55 + izuiValue.textContent = currentIzuiState; 56 + izuiValue.classList.add(`izui-${currentIzuiState}`); 57 + 58 + debug && console.log('[hud] Updated IZUI display:', currentIzuiState); 59 + }; 60 + 61 + /** 62 + * Update active window display 63 + */ 64 + const updateWindowDisplay = () => { 65 + if (!windowTitle) return; 66 + 67 + if (currentWindowInfo && currentWindowInfo.title) { 68 + let displayText = currentWindowInfo.title; 69 + 70 + // Add URL if available (for web pages) 71 + if (currentWindowInfo.url && 72 + !currentWindowInfo.url.startsWith('peek://') && 73 + currentWindowInfo.url !== currentWindowInfo.title) { 74 + displayText = currentWindowInfo.title; 75 + } 76 + 77 + windowTitle.textContent = displayText; 78 + } else { 79 + windowTitle.textContent = 'No active window'; 80 + } 81 + 82 + debug && console.log('[hud] Updated window display:', currentWindowInfo?.title); 83 + }; 84 + 85 + /** 86 + * Update stats display 87 + */ 88 + const updateStatsDisplay = () => { 89 + if (!statsValue) return; 90 + 91 + const parts = []; 92 + parts.push(`${windowCount} window${windowCount !== 1 ? 's' : ''}`); 93 + 94 + statsValue.textContent = parts.join(' • '); 95 + 96 + debug && console.log('[hud] Updated stats display:', windowCount); 97 + }; 98 + 99 + /** 100 + * Refresh all window info 101 + */ 102 + const refreshWindowInfo = async () => { 103 + try { 104 + // Get list of windows 105 + const windowsResult = await api.window.list(); 106 + if (windowsResult.success && windowsResult.data) { 107 + windowCount = windowsResult.data.length; 108 + 109 + // Find focused window (excluding HUD itself) 110 + const focusedWindow = windowsResult.data.find(w => 111 + w.focused && !w.url?.includes('hud/hud.html') 112 + ); 113 + 114 + if (focusedWindow) { 115 + currentWindowInfo = { 116 + title: focusedWindow.label || 'Untitled', 117 + url: focusedWindow.url || focusedWindow.source 118 + }; 119 + } else { 120 + currentWindowInfo = null; 121 + } 122 + 123 + updateWindowDisplay(); 124 + updateStatsDisplay(); 125 + } 126 + } catch (error) { 127 + console.error('[hud] Error refreshing window info:', error); 128 + } 129 + }; 130 + 131 + /** 132 + * Refresh IZUI state 133 + */ 134 + const refreshIzuiState = async () => { 135 + try { 136 + const state = await api.izui.getState(); 137 + if (state) { 138 + currentIzuiState = state; 139 + updateIzuiDisplay(); 140 + } 141 + } catch (error) { 142 + console.error('[hud] Error getting IZUI state:', error); 143 + } 144 + }; 145 + 146 + /** 147 + * Refresh mode 148 + */ 149 + const refreshMode = async () => { 150 + try { 151 + const result = await api.context.get('mode'); 152 + if (result.success && result.data) { 153 + currentMode = result.data.value || 'default'; 154 + currentModeMetadata = result.data.metadata || {}; 155 + updateModeDisplay(); 156 + } 157 + } catch (error) { 158 + console.error('[hud] Error getting mode:', error); 159 + } 160 + }; 161 + 162 + /** 163 + * Refresh all data 164 + */ 165 + const refreshAll = async () => { 166 + await Promise.all([ 167 + refreshMode(), 168 + refreshIzuiState(), 169 + refreshWindowInfo() 170 + ]); 171 + }; 172 + 173 + /** 174 + * Initialize HUD 175 + */ 176 + const init = async () => { 177 + console.log('[hud] Initializing display'); 178 + 179 + // Initial data fetch 180 + await refreshAll(); 181 + 182 + // Watch for mode changes 183 + api.context.watchMode((mode, entry) => { 184 + if (mode) { 185 + currentMode = mode; 186 + currentModeMetadata = entry?.metadata || {}; 187 + updateModeDisplay(); 188 + } 189 + }); 190 + 191 + // Subscribe to IZUI state changes 192 + api.subscribe('izui:state-changed', (msg) => { 193 + if (msg.state) { 194 + currentIzuiState = msg.state; 195 + updateIzuiDisplay(); 196 + } 197 + }, api.scopes.GLOBAL); 198 + 199 + // Subscribe to window focus changes 200 + api.subscribe('window:focused', () => { 201 + refreshWindowInfo(); 202 + }, api.scopes.GLOBAL); 203 + 204 + // Subscribe to window open/close events 205 + api.subscribe('window:opened', () => { 206 + refreshWindowInfo(); 207 + }, api.scopes.GLOBAL); 208 + 209 + api.subscribe('window:closed', () => { 210 + refreshWindowInfo(); 211 + }, api.scopes.GLOBAL); 212 + 213 + // Periodic refresh (fallback for events we might miss) 214 + setInterval(() => { 215 + refreshAll(); 216 + }, 5000); 217 + 218 + console.log('[hud] Display initialized'); 219 + }; 220 + 221 + // Start initialization 222 + init().catch(error => { 223 + console.error('[hud] Initialization error:', error); 224 + });
+9
extensions/hud/manifest.json
··· 1 + { 2 + "id": "hud", 3 + "shortname": "hud", 4 + "name": "HUD", 5 + "description": "Always-on-top heads-up display showing mode, IZUI state, and window info", 6 + "version": "1.0.0", 7 + "background": "background.html", 8 + "builtin": true 9 + }
+103
extensions/hud/styles.css
··· 1 + * { 2 + margin: 0; 3 + padding: 0; 4 + box-sizing: border-box; 5 + } 6 + 7 + html, body { 8 + width: 100%; 9 + height: 100%; 10 + overflow: hidden; 11 + background: transparent; 12 + -webkit-app-region: drag; 13 + } 14 + 15 + #hud-container { 16 + width: 100%; 17 + height: 100%; 18 + padding: 12px; 19 + background: rgba(0, 0, 0, 0.85); 20 + backdrop-filter: blur(10px); 21 + -webkit-backdrop-filter: blur(10px); 22 + border-radius: 8px; 23 + border: 1px solid rgba(255, 255, 255, 0.1); 24 + color: #fff; 25 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 26 + font-size: 12px; 27 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); 28 + -webkit-app-region: no-drag; 29 + } 30 + 31 + .hud-section { 32 + margin-bottom: 10px; 33 + padding-bottom: 8px; 34 + border-bottom: 1px solid rgba(255, 255, 255, 0.1); 35 + } 36 + 37 + .hud-section:last-child { 38 + margin-bottom: 0; 39 + padding-bottom: 0; 40 + border-bottom: none; 41 + } 42 + 43 + .hud-label { 44 + font-size: 10px; 45 + font-weight: 600; 46 + text-transform: uppercase; 47 + letter-spacing: 0.5px; 48 + color: rgba(255, 255, 255, 0.6); 49 + margin-bottom: 4px; 50 + } 51 + 52 + .hud-value { 53 + font-size: 13px; 54 + font-weight: 500; 55 + color: #fff; 56 + line-height: 1.4; 57 + } 58 + 59 + .hud-truncate { 60 + white-space: nowrap; 61 + overflow: hidden; 62 + text-overflow: ellipsis; 63 + max-width: 100%; 64 + } 65 + 66 + .hud-small { 67 + font-size: 11px; 68 + line-height: 1.5; 69 + } 70 + 71 + /* Mode-specific colors */ 72 + .mode-default { 73 + color: #60a5fa; 74 + } 75 + 76 + .mode-page { 77 + color: #34d399; 78 + } 79 + 80 + .mode-group { 81 + color: #f472b6; 82 + } 83 + 84 + .mode-settings { 85 + color: #fbbf24; 86 + } 87 + 88 + /* IZUI state colors */ 89 + .izui-idle { 90 + color: #9ca3af; 91 + } 92 + 93 + .izui-transient { 94 + color: #60a5fa; 95 + } 96 + 97 + .izui-active { 98 + color: #34d399; 99 + } 100 + 101 + .izui-overlay { 102 + color: #f472b6; 103 + }
+106 -1
notes/research-widgets-hud.md
··· 2 2 3 3 Research note capturing prior session plans for the Widgets system and HUD. 4 4 5 + **Last Updated:** 2026-02-09 6 + **Status:** Basic HUD in progress, full widget system not yet started 7 + 8 + --- 9 + 10 + ## Current State (2026-02-09) 11 + 12 + ### Available Infrastructure for HUD/Widgets 13 + 14 + **IZUI State System** (`backend/electron/izui-state.ts`): 15 + - States: `idle`, `transient`, `active`, `overlay` 16 + - Uses OS-level app focus (not window-specific) 17 + - New `IzuiSession` model tracks window stack and focus 18 + - Frontend API: 19 + - `api.izui.isTransient()` - Re-evaluates on each call (fixes keepLive stale state) 20 + - `api.izui.getEffectiveMode()` - Returns 'default' for transient, 'active' for focused 21 + - `api.izui.getState()` - Current state string 22 + - `api.izui.closeSelf()` - Request window closure 23 + 24 + **Context & Modes API** (`preload.js`): 25 + - General-purpose key-value store with history tracking 26 + - Modes built on context as first-class keys 27 + - Available operations: 28 + - `api.context.get(key, windowId)` - Get current value 29 + - `api.context.set(key, value, options)` - Set with metadata 30 + - `api.context.watch(key, callback)` - Subscribe to changes 31 + - `api.context.watchMode(callback)` - Watch mode changes specifically 32 + - `api.context.setMode(mode, options)` - Set mode with metadata (groupId, groupName, etc.) 33 + - `api.context.getHistory(key, options)` - Query history with timestamps 34 + - `api.context.getWindowsWithValue(key, value)` - Find windows by context 35 + - `api.context.getWindowsInGroup(groupId)` - Group queries 36 + 37 + **Window Management** (`preload.js`): 38 + - `api.window.open(url, options)` - Opens window 39 + - `api.window.list(options)` - List windows 40 + - `api.window.getFocusedVisibleWindowId()` - Active window 41 + - Window options support: `parentWindowId`, `modal`, `keepLive`, `overlayHiddenWindows`, `escapeMode` 42 + - Backend auto-handles parent centering 43 + 44 + **Pubsub System**: 45 + - `api.publish(topic, msg, scope)` - Broadcast 46 + - `api.subscribe(topic, callback, scope)` - Listen 47 + - System topics: `context:changed`, `modes:changed`, `window:closed` 48 + 49 + ### HUD Integration Points 50 + 51 + **For Basic HUD:** 52 + 1. **Mode Tracking**: `api.context.watch('mode', callback)` - Real-time mode changes 53 + 2. **IZUI State**: `api.izui.getState()` - Current state display 54 + 3. **Active Window**: `api.window.getFocusedVisibleWindowId()` - Which window is active 55 + 4. **Mode Metadata**: Context entries include metadata (groupId, groupName for group mode) 56 + 5. **Pubsub Updates**: Subscribe to system events for real-time refresh 57 + 58 + **Window Configuration for HUD:** 59 + - Always-on-top: Use standard BrowserWindow `alwaysOnTop: true` 60 + - Non-focusable: May need `focusable: false` or `skipTaskbar: true` 61 + - Transparent: `transparent: true`, `frame: false` 62 + 63 + --- 64 + 5 65 ## Widgets System 6 66 7 67 Full widget infrastructure for Peek. Widgets are the building blocks for HUDs, dashboards, page metadata panels, command previews, and observability displays. ··· 61 121 - Updates reactively from IZUI state changes and system events 62 122 - Full version: composed of Widgets, so each panel is a widget instance in a HUD sheet 63 123 124 + ### Implementation Details (Updated) 125 + 126 + **Data Sources:** 127 + - Mode: `api.context.watch('mode', callback)` with metadata (groupId, groupName) 128 + - IZUI state: `api.izui.getState()` returns current state string 129 + - Active window: `api.window.getFocusedVisibleWindowId()` + `api.window.list()` for details 130 + - Window count: `api.window.list().length` 131 + - Extension info: Via pubsub or extension registry if exposed 132 + 133 + **Window Setup:** 134 + ```javascript 135 + api.window.open('peek://ext/hud/hud.html', { 136 + alwaysOnTop: true, 137 + transparent: true, 138 + frame: false, 139 + skipTaskbar: true, // May make non-focusable 140 + width: 200, 141 + height: 100, 142 + x: 0, // Screen corner 143 + y: 0 144 + }); 145 + ``` 146 + 147 + **Reactive Updates:** 148 + ```javascript 149 + // Watch mode changes 150 + api.context.watch('mode', (mode, entry) => { 151 + updateDisplay({ 152 + mode: mode, 153 + groupName: entry?.metadata?.groupName 154 + }); 155 + }); 156 + 157 + // Watch IZUI state 158 + api.subscribe('izui:state-changed', (msg) => { 159 + updateDisplay({ izuiState: msg.state }); 160 + }); 161 + 162 + // Periodic refresh for window info 163 + setInterval(() => { 164 + const activeId = api.window.getFocusedVisibleWindowId(); 165 + // Update active window display 166 + }, 1000); 167 + ``` 168 + 64 169 ### Status 65 170 66 - - Basic HUD: completed (standalone implementation) 171 + - Basic HUD: **In Progress** (agent implementing now) 67 172 - Full HUD (widget-based): blocked on Widgets system