experiments in a post-browser web
10
fork

Configure Feed

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

refactor: declarative extensions, peek-card slot fix, HUD focus, test reliability

- Refactor editor, hud, localsearch, websearch to declarative manifest commands/shortcuts
- Fix peek-card empty slot sections (header/body/footer) taking space via slotchange detection
- Hide alwaysOnTop windows (HUD) when app loses OS focus, show on regain
- Fix declarative publish-type commands hanging cmd panel (sync-now regression)
- Fix groups promote-to-group button (excluded runtime props from DB row)
- Increase HUD background opacity
- Fix cmd panel test race condition with deterministic waitForPanelCommandsLoaded
- Console.log cleanup across extensions

+495 -597
+21 -4
app/components/peek-card.js
··· 169 169 color: var(--theme-text, #333); 170 170 } 171 171 172 - .header:empty { 172 + .header.slot-empty { 173 173 display: none; 174 174 } 175 175 ··· 181 181 line-height: var(--peek-leading-normal); 182 182 } 183 183 184 - .body:empty { 184 + .body.slot-empty { 185 185 display: none; 186 186 } 187 187 ··· 200 200 color: var(--theme-text-muted, #999); 201 201 } 202 202 203 - .footer:empty { 203 + .footer.slot-empty { 204 204 display: none; 205 205 } 206 206 ··· 214 214 line-height: 0; 215 215 } 216 216 217 - .media:empty { 217 + .media.slot-empty { 218 218 display: none; 219 219 } 220 220 ··· 266 266 </div> 267 267 </div> 268 268 `; 269 + } 270 + 271 + firstUpdated() { 272 + // Hide sections whose slots have no assigned content 273 + for (const slot of this.shadowRoot.querySelectorAll('slot')) { 274 + this._updateSlotEmpty(slot); 275 + slot.addEventListener('slotchange', () => this._updateSlotEmpty(slot)); 276 + } 277 + } 278 + 279 + _updateSlotEmpty(slot) { 280 + const wrapper = slot.parentElement; 281 + if (!wrapper) return; 282 + const hasContent = slot.assignedNodes({ flatten: true }).some( 283 + n => n.nodeType === Node.ELEMENT_NODE || (n.nodeType === Node.TEXT_NODE && n.textContent.trim()) 284 + ); 285 + wrapper.classList.toggle('slot-empty', !hasContent); 269 286 } 270 287 271 288 _handleClick(e) {
-7
app/datastore/history.js
··· 28 28 } 29 29 30 30 const data = result.data; 31 - console.log('Tracked navigation:', { 32 - visitId: data.visitId, 33 - itemId: data.itemId, 34 - uri, 35 - source: options.source 36 - }); 37 - 38 31 return { visitId: data.visitId, itemId: data.itemId, addressId: data.itemId }; 39 32 } catch (error) { 40 33 console.error('Error tracking navigation:', error);
+1 -1
app/page/page.js
··· 19 19 20 20 import api from '../api.js'; 21 21 22 - const DEBUG = true; 22 + const DEBUG = false; 23 23 24 24 // Cache our window ID for filtering GLOBAL pubsub messages 25 25 let myWindowId = null;
-6
app/settings/settings.js
··· 6 6 const DEBUG = api.debug; 7 7 const clear = false; 8 8 9 - console.log('loading', 'settings'); 10 - 11 9 // Helper: Create text/number input 12 10 const createInput = (label, value, onChange, options = {}) => { 13 11 const group = document.createElement('div'); ··· 3234 3232 window.addEventListener('load', init); 3235 3233 3236 3234 // No IZUI init needed — backend handles focus restoration and ESC policy via roles. 3237 - 3238 - window.addEventListener('blur', () => { 3239 - console.log('core settings blur'); 3240 - }); 3241 3235 3242 3236 // Listen for navigation requests from commands 3243 3237 api.subscribe('settings:navigate', (msg) => {
-7
backend/electron/ipc.ts
··· 3041 3041 }); 3042 3042 3043 3043 ipcMain.handle('window-close', async (_ev, msg) => { 3044 - // Always log for ESC debugging 3045 - console.log('[ipc:window-close] called with:', msg); 3046 - 3047 3044 try { 3048 3045 if (!msg.id) { 3049 - console.log('[ipc:window-close] no id provided'); 3050 3046 return { success: false, error: 'Window ID is required' }; 3051 3047 } 3052 3048 3053 3049 const win = BrowserWindow.fromId(msg.id); 3054 3050 if (!win) { 3055 - console.log('[ipc:window-close] window not found:', msg.id); 3056 3051 return { success: false, error: 'Window not found' }; 3057 3052 } 3058 3053 3059 - console.log('[ipc:window-close] calling win.close() for window:', msg.id); 3060 3054 win.close(); 3061 - console.log('[ipc:window-close] win.close() completed for window:', msg.id); 3062 3055 return { success: true }; 3063 3056 } catch (error) { 3064 3057 console.error('[ipc:window-close] Failed to close window:', error);
+33 -3
backend/electron/main.ts
··· 64 64 const CONSOLIDATED_EXTENSION_IDS = ['cmd', 'editor', 'groups', 'hud', 'localsearch', 'peeks', 'slides', 'websearch', 'windows', 'page', 'files', 'pagestream', 'sheets', 'tags', 'feeds', 'helpdocs', 'entities', 'scripts']; 65 65 66 66 // Extensions that must load eagerly (not lazy) — needed at startup 67 - const EAGER_EXTENSION_IDS = new Set(['cmd', 'editor', 'groups', 'hud', 'localsearch', 'pagestream', 'windows']); 67 + const EAGER_EXTENSION_IDS = new Set(['cmd', 'hud']); 68 68 69 69 // Track which lazy extensions have been loaded 70 70 const lazyExtensionLoaded = new Set<string>(); ··· 180 180 (app as any).on('did-become-active', () => { 181 181 getIzuiCoordinator().setAppFocused(true); 182 182 publish(getSystemAddress(), scopes.GLOBAL, 'app:focus-changed', { focused: true }); 183 + // Show alwaysOnTop overlay windows (e.g. HUD) when app regains focus 184 + for (const win of BrowserWindow.getAllWindows()) { 185 + if (!win.isDestroyed() && win.isAlwaysOnTop() && (win as any).__hudHidden) { 186 + win.show(); 187 + (win as any).__hudHidden = false; 188 + } 189 + } 183 190 }); 184 191 (app as any).on('did-resign-active', () => { 185 192 getIzuiCoordinator().setAppFocused(false); 186 193 publish(getSystemAddress(), scopes.GLOBAL, 'app:focus-changed', { focused: false }); 194 + // Hide alwaysOnTop overlay windows (e.g. HUD) when app loses focus 195 + for (const win of BrowserWindow.getAllWindows()) { 196 + if (!win.isDestroyed() && win.isAlwaysOnTop() && win.isVisible()) { 197 + win.hide(); 198 + (win as any).__hudHidden = true; 199 + } 200 + } 187 201 }); 188 202 } else { 189 203 // Non-macOS fallback: use browser-window-focus/blur with focused-window check ··· 412 426 subscribe(source, scopes.GLOBAL, `cmd:execute:${cmd.name}`, (msg: unknown) => { 413 427 DEBUG && console.log(`[ext:declarative] Executing command: ${cmd.name}`); 414 428 executeDeclarativeAction(cmd, msg as Record<string, unknown>); 429 + // Signal completion so the cmd panel proxy resolves immediately 430 + // (publish-type actions are fire-and-forget — don't block the UI) 431 + publish(source, scopes.GLOBAL, `cmd:execute:${cmd.name}:result`, { success: true }); 415 432 }); 416 433 417 434 // Register the command with the cmd extension's registry ··· 442 459 443 460 const callback = () => { 444 461 DEBUG && console.log(`[ext:declarative] Shortcut triggered: ${shortcut.keys} -> ${shortcut.command}`); 445 - executeDeclarativeAction(cmd, {}); 462 + if (cmd.action.type === 'execute') { 463 + // For execute-type commands, publish to cmd:execute so lazy stubs can intercept 464 + publish(`peek://ext/${extId}/`, scopes.GLOBAL, `cmd:execute:${cmd.name}`, {}); 465 + } else { 466 + executeDeclarativeAction(cmd, {}); 467 + } 446 468 }; 447 469 448 470 if (shortcut.global) { ··· 606 628 607 629 for (const extId of registeredExtIds) { 608 630 if (!isBuiltinExtensionEnabled(extId)) continue; 609 - if (EAGER_EXTENSION_IDS.has(extId)) continue; // Eager extensions handle their own commands 610 631 if (declarativeExtensions.has(extId)) continue; // Already declarative (background: false) 611 632 612 633 const extPath = getExtensionPath(extId); ··· 620 641 const commands = manifest.commands || []; 621 642 const shortcuts = manifest.shortcuts || []; 622 643 if (commands.length === 0 && shortcuts.length === 0) continue; 644 + 645 + // Eager extensions: register manifest shortcuts only (they handle their own commands) 646 + if (EAGER_EXTENSION_IDS.has(extId)) { 647 + DEBUG && console.log(`[ext:eager] Registering manifest shortcuts for ${extId}: ${shortcuts.length} shortcuts`); 648 + for (const shortcut of shortcuts) { 649 + registerDeclarativeShortcut(extId, shortcut, commands); 650 + } 651 + continue; 652 + } 623 653 624 654 DEBUG && console.log(`[ext:lazy] Registering lazy commands for ${extId}: ${commands.length} commands, ${shortcuts.length} shortcuts`); 625 655
+7 -7
backend/electron/windows.ts
··· 81 81 export function askRendererToHandleEscape(bw: BrowserWindow): Promise<{ handled: boolean; close?: boolean; timeout?: boolean }> { 82 82 return new Promise((resolve) => { 83 83 const responseChannel = `escape-response-${bw.id}-${Date.now()}`; 84 - console.log(`[esc] Asking renderer to handle escape, channel: ${responseChannel}`); 84 + DEBUG && console.log(`[esc] Asking renderer to handle escape, channel: ${responseChannel}`); 85 85 86 86 const timeout = setTimeout(() => { 87 - console.log(`[esc] Renderer timeout (500ms) - no response on ${responseChannel}`); 87 + DEBUG && console.log(`[esc] Renderer timeout (500ms) - no response on ${responseChannel}`); 88 88 ipcMain.removeAllListeners(responseChannel); 89 89 resolve({ handled: false, timeout: true }); 90 90 }, 500); 91 91 92 92 ipcMain.once(responseChannel, (_event, response) => { 93 - console.log(`[esc] Renderer responded on ${responseChannel}:`, response); 93 + DEBUG && console.log(`[esc] Renderer responded on ${responseChannel}:`, response); 94 94 clearTimeout(timeout); 95 95 resolve(response || { handled: false }); 96 96 }); ··· 129 129 130 130 const params = entry.params; 131 131 const role = (params.role as string) || 'utility'; 132 - console.log(`[esc] ESC keyDown - window ${bw.id}, source: ${source}, role: ${role}, url: ${params.address}`); 132 + DEBUG && console.log(`[esc] ESC keyDown - window ${bw.id}, source: ${source}, role: ${role}, url: ${params.address}`); 133 133 134 134 // escapeMode: 'ignore' is a hard override — let ESC pass through entirely 135 135 if (params.escapeMode === 'ignore') { ··· 145 145 const coordinator = getIzuiCoordinator(); 146 146 const sessionState = coordinator.getState(); 147 147 const action = escPolicy(sessionState, role); 148 - console.log(`[esc] escPolicy(${sessionState}, ${role}) -> ${action}`); 148 + DEBUG && console.log(`[esc] escPolicy(${sessionState}, ${role}) -> ${action}`); 149 149 150 150 if (action === 'close' || action === 'close-and-restore') { 151 151 closeOrHideWindow(bw.id); ··· 190 190 // Prevents double-fires from key propagation or OS focus changes 191 191 const now = Date.now(); 192 192 if (now - lastEscTime < 200) { 193 - console.log(`[esc] Debounced ESC keyDown on window ${bw.id} source:${source} (${now - lastEscTime}ms since last)`); 193 + DEBUG && console.log(`[esc] Debounced ESC keyDown on window ${bw.id} source:${source} (${now - lastEscTime}ms since last)`); 194 194 return; 195 195 } 196 196 lastEscTime = now; ··· 213 213 // showing youtube.com) is intercepted and routed through the host window's 214 214 // escape handling (which talks to the host renderer's preload onEscape callback). 215 215 bw.webContents.on('did-attach-webview', (_event, guestWebContents) => { 216 - console.log(`[esc] Webview guest attached to window ${bw.id}, adding ESC listener`); 216 + DEBUG && console.log(`[esc] Webview guest attached to window ${bw.id}, adding ESC listener`); 217 217 attachEscListener(guestWebContents, 'webview'); 218 218 }); 219 219 }
+3
extensions/cmd/panel.js
··· 369 369 state.commands = e.detail; 370 370 }); 371 371 372 + // Expose state for testing — allows waitForFunction to check command loading 373 + window._cmdState = state; 374 + 372 375 async function render() { 373 376 // Get elements 374 377 const commandInput = document.getElementById('command-input');
+6 -13
extensions/editor/background.html
··· 14 14 const api = hasPeekAPI ? window.app : null; 15 15 const extId = extension.id; 16 16 17 - console.log(`[ext:${extId}] background.html loaded`); 18 - console.log(`[ext:${extId}] Peek API available:`, hasPeekAPI); 19 - 20 17 if (hasPeekAPI) { 21 18 // Running as a Peek extension - full functionality 19 + 20 + // Initialize extension BEFORE signaling ready 21 + // (lazy loading waits for ext:ready, so handlers must be registered first) 22 + if (extension.init) { 23 + await extension.init(); 24 + } 22 25 23 26 // Signal ready to main process 24 27 api.publish('ext:ready', { ··· 30 33 } 31 34 }, api.scopes.SYSTEM); 32 35 33 - // Initialize extension 34 - if (extension.init) { 35 - console.log(`[ext:${extId}] calling init()`); 36 - extension.init(); 37 - } 38 - 39 36 // Handle shutdown request from main process 40 37 api.subscribe('app:shutdown', () => { 41 - console.log(`[ext:${extId}] received shutdown`); 42 38 if (extension.uninit) { 43 39 extension.uninit(); 44 40 } ··· 46 42 47 43 // Handle extension-specific shutdown 48 44 api.subscribe(`ext:${extId}:shutdown`, () => { 49 - console.log(`[ext:${extId}] received extension shutdown`); 50 45 if (extension.uninit) { 51 46 extension.uninit(); 52 47 } ··· 54 49 55 50 } else { 56 51 // Running as a regular website - limited functionality 57 - console.log(`[ext:${extId}] Running in standalone mode (no Peek API)`); 58 - 59 52 if (extension.init) { 60 53 extension.init(); 61 54 }
+20 -33
extensions/editor/background.js
··· 7 7 * - Optional vim mode 8 8 * - Resizable panels 9 9 * - Pubsub integration 10 + * 11 + * Commands and shortcuts are declared in manifest.json. 12 + * This extension loads lazily on first command invocation. 10 13 */ 11 14 12 15 import { registerNoun, unregisterNoun } from 'peek://ext/cmd/nouns.js'; ··· 51 54 name: 'Editor' 52 55 }, 53 56 54 - /** 55 - * Register commands - called when cmd extension is ready 56 - */ 57 - registerCommands() { 57 + init() { 58 + if (!hasPeekAPI) return; 59 + 60 + // Register "editor" command handler — manifest declares the command, 61 + // lazy stub registers metadata, this sets up the execute handler 62 + api.commands.register({ 63 + name: 'editor', 64 + description: 'Open markdown editor', 65 + execute: () => openEditor() 66 + }); 67 + 68 + // Register noun for browse/list commands (not in manifest) 58 69 registerNoun({ 59 70 name: 'editor', 60 71 singular: 'editor', 61 72 description: 'Markdown editor', 62 - skipBare: true, // No bare "editor" command 73 + skipBare: true, 63 74 browse: async () => { openEditor(); }, 64 75 }); 65 76 66 - console.log('[editor] Commands registered'); 67 - }, 68 - 69 - init() { 70 - console.log('[editor] init - Peek API available:', hasPeekAPI); 71 - 72 - if (!hasPeekAPI) { 73 - console.log('[editor] Running without Peek API - limited functionality'); 74 - return; 75 - } 76 - 77 - // Register commands (cmd loads first with its subscribers ready via 100ms head start) 78 - this.registerCommands(); 79 - 80 - // Register global shortcut Option+E (works when Peek is not focused) 81 - api.shortcuts.register('Option+e', () => openEditor(), { global: true }); 82 - 83 - // Register local shortcut Cmd+E (works when a Peek window is focused) 84 - api.shortcuts.register('CommandOrControl+E', () => openEditor()); 77 + // Shortcuts declared in manifest.json — no imperative registration needed 85 78 86 79 // Subscribe to editor:open — open editor with optional content or itemId 87 80 api.subscribe('editor:open', (msg) => { ··· 99 92 }, api.scopes.GLOBAL); 100 93 101 94 // Subscribe to editor:add — open a new blank editor document 102 - api.subscribe('editor:add', (msg) => { 103 - console.log('[editor] editor:add received:', msg); 104 - openEditor(); // No params = blank document 95 + api.subscribe('editor:add', () => { 96 + openEditor(); 105 97 }, api.scopes.GLOBAL); 106 - 107 - console.log('[editor] Extension loaded'); 108 98 }, 109 99 110 100 uninit() { 111 - console.log('[editor] Cleaning up...'); 112 - 113 101 if (hasPeekAPI) { 114 102 unregisterNoun('editor'); 115 - api.shortcuts.unregister('Option+e', { global: true }); 116 - api.shortcuts.unregister('CommandOrControl+E'); 103 + // Shortcuts are managed by manifest — no imperative unregister needed 117 104 } 118 105 } 119 106 };
+19 -1
extensions/editor/manifest.json
··· 6 6 "version": "1.0.0", 7 7 "background": "background.html", 8 8 "builtin": true, 9 - "settingsSchema": "./settings-schema.json" 9 + "settingsSchema": "./settings-schema.json", 10 + "commands": [ 11 + { 12 + "name": "editor", 13 + "description": "Open markdown editor", 14 + "action": { "type": "execute" } 15 + } 16 + ], 17 + "shortcuts": [ 18 + { 19 + "keys": "Option+e", 20 + "command": "editor", 21 + "global": true 22 + }, 23 + { 24 + "keys": "CommandOrControl+E", 25 + "command": "editor" 26 + } 27 + ] 10 28 }
+3 -24
extensions/entities/background.js
··· 135 135 * @returns {Promise<Array<Object>>} Processed entities 136 136 */ 137 137 async function extractEntities(url, title = '') { 138 - console.log('[entities] Extracting from:', url); 139 - 140 138 const content = await getPageContent(url); 141 - if (!content) { 142 - console.log('[entities] No content available for:', url); 143 - return []; 144 - } 139 + if (!content) return []; 145 140 146 141 const html = content.html || ''; 147 142 const text = content.text || ''; ··· 170 165 ...regexEntities, 171 166 ]; 172 167 173 - console.log(`[entities] Raw extractions: ${allRaw.length} (JSON-LD: ${structuredDataEntities.length}, microformats: ${microformatEntities.length}, regex: ${regexEntities.length})`); 174 - 175 168 if (allRaw.length === 0) return []; 176 169 177 170 // Process through matcher (dedup + store) ··· 179 172 url, 180 173 title: pageTitle 181 174 }, CONFIDENCE_THRESHOLD); 182 - 183 - console.log(`[entities] Stored: ${processed.length} entities (${processed.filter(e => e.isNew).length} new)`); 184 175 185 176 return processed; 186 177 } ··· 261 252 async function extractCurrentPage() { 262 253 // Get the focused window 263 254 const windowList = await api.window.list({ includeInternal: false }); 264 - if (!windowList.success || !windowList.windows.length) { 265 - console.log('[entities] No active window to extract from'); 266 - return; 267 - } 255 + if (!windowList.success || !windowList.windows.length) return; 268 256 269 257 // Use the last focused visible window (not just first in list) 270 258 let activeWindow = windowList.windows[0]; ··· 275 263 } 276 264 const url = activeWindow.url; 277 265 278 - if (!url || !url.startsWith('http')) { 279 - console.log('[entities] Active window has no extractable URL:', url); 280 - return; 281 - } 266 + if (!url || !url.startsWith('http')) return; 282 267 283 268 // Clear cooldown for manual extraction 284 269 extractionCache.delete(url); 285 270 286 271 const entities = await extractEntities(url, activeWindow.title || ''); 287 - console.log(`[entities] Manual extraction: ${entities.length} entities from ${url}`); 288 272 289 273 return entities; 290 274 } ··· 360 344 } 361 345 }); 362 346 363 - console.log('[entities] Commands registered'); 364 347 }, 365 348 366 349 init() { 367 - console.log('[entities] Initializing...'); 368 - 369 350 // Register commands (cmd loads first with its subscribers ready via 100ms head start) 370 351 this.registerCommands(); 371 352 ··· 440 421 } 441 422 }, api.scopes.GLOBAL); 442 423 443 - console.log('[entities] Extension loaded'); 444 424 }, 445 425 446 426 uninit() { 447 - console.log('[entities] Cleaning up...'); 448 427 449 428 // Clear pending extraction timers 450 429 for (const timer of pendingExtractions.values()) {
-19
extensions/feeds/background.js
··· 110 110 if (existing.success) { 111 111 const alreadySubscribed = existing.data.find(f => f.content === url); 112 112 if (alreadySubscribed) { 113 - console.log('[feeds] Already subscribed to:', url); 114 113 return { success: false, error: 'Already subscribed to this feed' }; 115 114 } 116 115 } ··· 136 135 }); 137 136 138 137 if (result.success) { 139 - console.log('[feeds] Subscribed to:', title); 140 138 // Poll immediately 141 139 await pollFeed(result.data.id, url); 142 140 return { success: true, data: { id: result.data.id, title, url } }; ··· 184 182 * Poll a single feed for new entries 185 183 */ 186 184 async function pollFeed(feedId, feedUrl) { 187 - console.log('[feeds] Polling:', feedUrl); 188 - 189 185 try { 190 186 const response = await fetch(feedUrl); 191 187 if (!response.ok) { ··· 234 230 }) 235 231 }); 236 232 237 - console.log(`[feeds] Polled ${feedUrl}: ${newCount} new entries`); 238 233 return { success: true, newCount }; 239 234 } catch (error) { 240 235 console.error('[feeds] Poll error:', feedUrl, error); ··· 260 255 */ 261 256 async function pollAllFeeds() { 262 257 const feeds = await getFeeds(); 263 - console.log(`[feeds] Polling ${feeds.length} feeds...`); 264 - 265 258 for (const feed of feeds) { 266 259 await pollFeed(feed.id, feed.url); 267 260 } ··· 370 363 execute: pollAllFeeds 371 364 }); 372 365 373 - console.log('[feeds] Commands registered'); 374 366 }, 375 367 376 368 init() { 377 - console.log('[feeds] Initializing...'); 378 - 379 369 // Register commands (cmd loads first with its subscribers ready via 100ms head start) 380 370 this.registerCommands(); 381 371 ··· 391 381 // React to feed item changes from other sources 392 382 api.subscribe('item:created', async (msg) => { 393 383 if (msg.itemType === 'feed') { 394 - console.log('[feeds] New feed added externally, polling immediately'); 395 384 const item = await api.datastore.getItem(msg.itemId); 396 385 if (item.success && item.data) { 397 386 await pollFeed(msg.itemId, item.data.content); ··· 399 388 } 400 389 }, api.scopes.GLOBAL); 401 390 402 - api.subscribe('item:deleted', (msg) => { 403 - if (msg.itemType === 'feed') { 404 - console.log('[feeds] Feed removed externally:', msg.itemId); 405 - } 406 - }, api.scopes.GLOBAL); 407 - 408 - console.log('[feeds] Extension loaded'); 409 391 }, 410 392 411 393 uninit() { 412 - console.log('[feeds] Cleaning up...'); 413 394 414 395 if (pollTimer) { 415 396 clearInterval(pollTimer);
-29
extensions/files/background.js
··· 12 12 const id = 'files'; 13 13 const labels = { name: 'Files' }; 14 14 15 - console.log('[ext:files] background', labels.name); 16 - 17 15 // ===== CSV Command ===== 18 16 19 17 /** ··· 147 145 produces: ['text/plain', 'text/markdown', 'text/html', 'text/csv', 'application/json'], 148 146 149 147 execute: async (ctx) => { 150 - console.log('[open file] execute:', ctx); 151 - 152 148 try { 153 149 const result = await api.files.open(); 154 150 155 151 if (!result.success) { 156 152 if (result.canceled) { 157 - console.log('[open file] User cancelled file picker'); 158 153 return { success: true, message: 'File picker cancelled' }; 159 154 } 160 155 return { success: false, error: result.error || 'Failed to open file' }; 161 156 } 162 157 163 158 const file = result.data; 164 - console.log('[open file] Opened:', file.name, file.mimeType, file.size, 'bytes'); 165 159 166 160 // If the file is not text, we can't chain it 167 161 if (!file.isText || file.content === null) { ··· 209 203 produces: ['text/csv'], 210 204 211 205 execute: async (ctx) => { 212 - console.log('[csv] execute:', ctx); 213 - 214 206 // Check if we have input data from chain 215 207 if (!ctx.input) { 216 - console.log('[csv] No input data'); 217 208 return { 218 209 success: false, 219 210 error: 'No input data. Use this command in a chain after a command that produces JSON.' ··· 249 240 // Convert to CSV 250 241 const csvOutput = jsonToCsv(data); 251 242 252 - console.log('[csv] Converted', data.length, 'items to CSV'); 253 - 254 243 return { 255 244 success: true, 256 245 output: { ··· 272 261 produces: [], // End of chain - doesn't produce output 273 262 274 263 execute: async (ctx) => { 275 - console.log('[save] execute:', ctx); 276 - 277 264 // Check if we have input data from chain 278 265 if (!ctx.input) { 279 - console.log('[save] No input data'); 280 266 return { 281 267 success: false, 282 268 error: 'No input data. Use this command in a chain after a command that produces output.' ··· 310 296 const result = await api.files.save(content, { filename, mimeType }); 311 297 312 298 if (result.canceled) { 313 - console.log('[save] User cancelled save dialog'); 314 299 return { success: true, message: 'Save cancelled' }; 315 300 } 316 301 317 302 if (!result.success) { 318 303 return { success: false, error: result.error || 'Failed to save file' }; 319 304 } 320 - 321 - console.log('[save] Saved to:', result.path); 322 305 return { 323 306 success: true, 324 307 message: `Saved to ${result.path}` ··· 338 321 produces: ['text/markdown'], 339 322 340 323 execute: async (ctx) => { 341 - console.log('[markdown] execute:', ctx); 342 - 343 324 if (!ctx.input) { 344 325 return { 345 326 success: false, ··· 495 476 }); 496 477 497 478 const markdownOutput = lines.join('\n'); 498 - console.log('[markdown] Generated markdown document,', markdownOutput.length, 'chars'); 499 479 500 480 return { 501 481 success: true, ··· 521 501 produces: ['item'], // Produces item for potential editor routing 522 502 523 503 execute: async (ctx) => { 524 - console.log('[save as note] execute:', ctx); 525 - 526 504 if (!ctx.input) { 527 505 return { 528 506 success: false, ··· 558 536 559 537 // Notify editor of the new item 560 538 api.publish('editor:changed', { action: 'add', itemId }, api.scopes.GLOBAL); 561 - 562 - console.log('[save as note] Created note:', itemId); 563 539 564 540 return { 565 541 success: true, ··· 587 563 api.commands.register(cmd); 588 564 registeredCommands.push(cmd.name); 589 565 }); 590 - console.log('[ext:files] Registered commands:', registeredCommands); 591 566 }; 592 567 593 568 const uninitCommands = () => { ··· 595 570 api.commands.unregister(name); 596 571 }); 597 572 registeredCommands = []; 598 - console.log('[ext:files] Unregistered commands'); 599 573 }; 600 574 601 575 const init = async () => { 602 - console.log('[ext:files] init'); 603 - 604 576 // Register commands (cmd loads first with its subscribers ready via 100ms head start) 605 577 initCommands(); 606 578 }; 607 579 608 580 const uninit = () => { 609 - console.log('[ext:files] uninit'); 610 581 uninitCommands(); 611 582 }; 612 583
+17 -9
extensions/groups/home.js
··· 106 106 meta.isGroup = true; 107 107 const metadataStr = JSON.stringify(meta); 108 108 try { 109 - await api.datastore.setRow('tags', tag.id, { 110 - ...tag, 111 - metadata: metadataStr 112 - }); 109 + // Only include valid tag table columns — exclude runtime properties like 110 + // addressCount which would cause the SQL INSERT to fail 111 + const tagRow = { 112 + id: tag.id, 113 + name: tag.name, 114 + slug: tag.slug, 115 + color: tag.color, 116 + parentId: tag.parentId, 117 + description: tag.description, 118 + metadata: metadataStr, 119 + createdAt: tag.createdAt, 120 + updatedAt: tag.updatedAt, 121 + frequency: tag.frequency, 122 + lastUsed: tag.lastUsed, 123 + frecencyScore: tag.frecencyScore 124 + }; 125 + await api.datastore.setRow('tags', tag.id, tagRow); 113 126 tag.metadata = metadataStr; 114 127 debug && console.log('[groups] Promoted tag to group:', tag.name); 115 128 return true; ··· 125 138 * Returns { handled: false } if at root (groups list) and window should close 126 139 */ 127 140 const handleEscape = () => { 128 - console.log('[groups:esc] handleEscape called, view:', state.view, 'searchQuery:', state.searchQuery); 129 - 130 141 // If search has content, clear it first 131 142 const searchInput = document.querySelector('peek-input.search-input'); 132 143 if (state.searchQuery) { 133 144 state.searchQuery = ''; 134 145 searchInput.value = ''; 135 146 renderCurrentView(); 136 - console.log('[groups:esc] Cleared search, returning handled: true'); 137 147 return { handled: true }; 138 148 } 139 149 ··· 145 155 console.error('[groups] Error navigating back to groups:', err); 146 156 }); 147 157 }, 0); 148 - console.log('[groups:esc] VIEW_ADDRESSES -> navigating to groups, returning handled: true'); 149 158 return { handled: true }; 150 159 } 151 160 // At root (groups list) - let window close 152 - console.log('[groups:esc] At root (VIEW_GROUPS), returning handled: false'); 153 161 return { handled: false }; 154 162 }; 155 163
+44 -1
extensions/groups/manifest.json
··· 6 6 "version": "1.0.0", 7 7 "background": "background.html", 8 8 "builtin": true, 9 - "settingsSchema": "./settings-schema.json" 9 + "settingsSchema": "./settings-schema.json", 10 + "commands": [ 11 + { 12 + "name": "groups", 13 + "description": "Saved tab groups", 14 + "action": { "type": "execute" } 15 + }, 16 + { 17 + "name": "close group", 18 + "description": "Hide all windows in the current group (restore later)", 19 + "action": { "type": "execute" } 20 + }, 21 + { 22 + "name": "switch group", 23 + "description": "Close current group and open another", 24 + "action": { "type": "execute" } 25 + }, 26 + { 27 + "name": "restore group", 28 + "description": "Restore a previously closed (suspended) group", 29 + "action": { "type": "execute" } 30 + }, 31 + { 32 + "name": "pin", 33 + "description": "Pin a URL in the current group (always opens with group)", 34 + "action": { "type": "execute" } 35 + }, 36 + { 37 + "name": "unpin", 38 + "description": "Unpin a URL from the current group", 39 + "action": { "type": "execute" } 40 + } 41 + ], 42 + "shortcuts": [ 43 + { 44 + "keys": "Option+g", 45 + "command": "groups", 46 + "global": true 47 + }, 48 + { 49 + "keys": "CommandOrControl+G", 50 + "command": "groups" 51 + } 52 + ] 10 53 }
+5 -9
extensions/hud/background.html
··· 12 12 const api = window.app; 13 13 const extId = extension.id; 14 14 15 - console.log(`[ext:${extId}] background.html loaded`); 15 + // Initialize extension BEFORE signaling ready 16 + // (command handlers must be registered before ext:ready so lazy stubs can re-publish) 17 + if (extension.init) { 18 + await extension.init(); 19 + } 16 20 17 21 // Signal ready to main process 18 22 api.publish('ext:ready', { ··· 24 28 } 25 29 }, api.scopes.SYSTEM); 26 30 27 - // Initialize extension 28 - if (extension.init) { 29 - console.log(`[ext:${extId}] calling init()`); 30 - extension.init(); 31 - } 32 - 33 31 // Handle shutdown request from main process 34 32 api.subscribe('app:shutdown', () => { 35 - console.log(`[ext:${extId}] received shutdown`); 36 33 if (extension.uninit) { 37 34 extension.uninit(); 38 35 } ··· 40 37 41 38 // Handle extension-specific shutdown 42 39 api.subscribe(`ext:${extId}:shutdown`, () => { 43 - console.log(`[ext:${extId}] received extension shutdown`); 44 40 if (extension.uninit) { 45 41 extension.uninit(); 46 42 }
+19 -93
extensions/hud/background.js
··· 5 5 * Uses the widget sheet system — each piece of HUD info is an individual 6 6 * widget page rendered via webview in a peek-grid freeform layout. 7 7 * 8 + * Commands and shortcuts are declared in manifest.json. 9 + * This extension loads eagerly (needs startup state restoration). 10 + * 8 11 * Runs in isolated extension process (peek://ext/hud/background.html) 9 12 */ 10 13 11 14 const api = window.app; 12 15 const debug = api.debug; 13 - 14 - console.log('[ext:hud] background init'); 15 16 16 17 const HUD_ADDRESS = 'peek://ext/hud/hud.html'; 17 18 const STORAGE_KEY = 'hud_enabled'; ··· 68 69 const ensureSheetConfig = async () => { 69 70 const result = await api.settings.getKey(HUD_SHEET_KEY); 70 71 if (result.success && result.data) { 71 - // Upgrade config if version is outdated 72 72 if (result.data.version && result.data.version >= CURRENT_CONFIG_VERSION) { 73 - debug && console.log('[ext:hud] HUD sheet config already exists (v' + result.data.version + ')'); 74 73 return result.data; 75 74 } 76 - console.log('[ext:hud] Upgrading HUD sheet config from v' + (result.data.version || 1) + ' to v' + CURRENT_CONFIG_VERSION); 77 75 } 78 76 79 77 // Create default config (or upgrade) 80 78 const config = buildDefaultSheetConfig(); 81 79 await api.settings.setKey(HUD_SHEET_KEY, config); 82 - console.log('[ext:hud] Created default HUD sheet config v' + CURRENT_CONFIG_VERSION + ' with', config.items.length, 'widgets'); 83 80 return config; 84 81 }; 85 82 ··· 95 92 try { 96 93 const stored = localStorage.getItem(STORAGE_KEY); 97 94 hudEnabled = stored === 'true'; 98 - console.log('[ext:hud] Loaded state from localStorage - enabled:', hudEnabled); 99 95 } catch (err) { 100 - console.log('[ext:hud] Failed to load state from localStorage:', err); 96 + // localStorage unavailable 101 97 } 102 98 return hudEnabled; 103 99 }; ··· 109 105 hudEnabled = enabled; 110 106 try { 111 107 localStorage.setItem(STORAGE_KEY, String(enabled)); 112 - console.log('[ext:hud] Saved state to localStorage - enabled:', enabled); 113 108 } catch (err) { 114 - console.error('[ext:hud] Failed to save state to localStorage:', err); 109 + console.error('[ext:hud] Failed to save state:', err); 115 110 } 116 111 }; 117 112 ··· 137 132 */ 138 133 const openHud = async () => { 139 134 if (hudWindowId) { 140 - // Already open, just show it 141 135 const exists = await api.window.exists(hudWindowId); 142 136 if (exists.success && exists.data) { 143 137 await api.window.show(hudWindowId); 144 - console.log('[ext:hud] HUD already open, showing'); 145 138 return; 146 139 } 147 140 hudWindowId = null; 148 141 } 149 142 150 - // Ensure sheet config exists before opening 151 143 const config = await ensureSheetConfig(); 152 144 const size = getHudWindowSize(config); 153 145 ··· 174 166 const result = await api.window.open(HUD_ADDRESS, params); 175 167 if (result.success) { 176 168 hudWindowId = result.id; 177 - console.log('[ext:hud] HUD opened:', hudWindowId); 178 169 } else { 179 170 console.error('[ext:hud] Failed to open HUD:', result.error); 180 171 } ··· 191 182 const exists = await api.window.exists(hudWindowId); 192 183 if (exists.success && exists.data) { 193 184 await api.window.show(hudWindowId); 194 - console.log('[ext:hud] HUD shown (app focused)'); 195 185 } 196 186 } 197 187 }; ··· 204 194 const exists = await api.window.exists(hudWindowId); 205 195 if (exists.success && exists.data) { 206 196 await api.window.hide(hudWindowId); 207 - console.log('[ext:hud] HUD hidden (app blurred)'); 208 197 } 209 198 } 210 199 }; ··· 216 205 if (hudWindowId) { 217 206 await api.window.close(hudWindowId); 218 207 hudWindowId = null; 219 - console.log('[ext:hud] HUD closed'); 220 208 } 221 209 }; 222 210 ··· 236 224 } 237 225 }; 238 226 239 - // ===== Command definitions ===== 240 - 241 - const commandDefinitions = [ 242 - { 243 - name: 'hud', 244 - description: 'Toggle HUD overlay on/off', 245 - execute: async (ctx) => { 246 - console.log('[ext:hud] Toggle command executed'); 247 - return await toggleHud(); 248 - } 249 - } 250 - ]; 251 - 252 - // ===== Registration ===== 253 - 254 - let registeredShortcut = null; 255 - let registeredCommands = []; 256 - 257 - const LOCAL_HUD_SHORTCUT = 'CommandOrControl+H'; 258 - 259 - const initShortcut = () => { 260 - const shortcut = 'Option+H'; 261 - api.shortcuts.register(shortcut, async () => { 262 - await toggleHud(); 263 - }, { global: true }); 264 - registeredShortcut = shortcut; 265 - 266 - // Local shortcut (Cmd+H) — works when a Peek window is focused 267 - api.shortcuts.register(LOCAL_HUD_SHORTCUT, async () => { 268 - await toggleHud(); 269 - }); 270 - 271 - console.log('[ext:hud] Registered shortcuts:', shortcut, '(global),', LOCAL_HUD_SHORTCUT, '(local)'); 272 - }; 273 - 274 - const initCommands = async () => { 275 - commandDefinitions.forEach(cmd => { 276 - api.commands.register(cmd); 277 - registeredCommands.push(cmd.name); 278 - }); 279 - console.log('[ext:hud] Registered commands:', registeredCommands); 280 - }; 281 - 282 - const uninitCommands = () => { 283 - registeredCommands.forEach(name => { 284 - api.commands.unregister(name); 285 - }); 286 - registeredCommands = []; 287 - console.log('[ext:hud] Unregistered commands'); 288 - }; 227 + // ===== Lifecycle ===== 289 228 290 229 const init = async () => { 291 - console.log('[ext:hud] init'); 292 - 293 230 // Load persisted state 294 231 await loadState(); 295 232 296 233 // Ensure HUD sheet config exists (create default if needed) 297 234 await ensureSheetConfig(); 298 235 299 - // Register shortcut 300 - initShortcut(); 236 + // Register command handler — manifest declares the command, 237 + // shortcuts are registered from manifest by main.ts for eager extensions 238 + api.commands.register({ 239 + name: 'hud', 240 + description: 'Toggle HUD overlay on/off', 241 + execute: async () => await toggleHud() 242 + }); 301 243 302 - // Register commands (cmd loads first with its subscribers ready via 100ms head start) 303 - initCommands(); 244 + // Shortcuts declared in manifest.json — no imperative registration needed 304 245 305 - // Subscribe to app focus changes — hide HUD when app loses focus, show when regained 306 - api.subscribe('app:focus-changed', async (msg) => { 246 + // App focus hide/show is handled in the main process (did-resign-active / did-become-active) 247 + // for reliability — alwaysOnTop windows are hidden/shown directly without pubsub round-trip. 248 + api.subscribe('app:focus-changed', (msg) => { 307 249 appFocused = !!msg.focused; 308 - console.log('[ext:hud] App focus changed:', appFocused); 309 - if (hudEnabled && hudWindowId) { 310 - if (appFocused) { 311 - await showHud(); 312 - } else { 313 - await hideHud(); 314 - } 315 - } 316 250 }, api.scopes.GLOBAL); 317 251 318 - // Open HUD if it was enabled 252 + // Open HUD if it was enabled in a previous session 319 253 if (hudEnabled) { 320 254 await openHud(); 321 255 } 322 - 323 - console.log('[ext:hud] Initialized, enabled:', hudEnabled); 324 256 }; 325 257 326 258 const uninit = () => { 327 - console.log('[ext:hud] uninit'); 328 - if (registeredShortcut) { 329 - api.shortcuts.unregister(registeredShortcut, { global: true }); 330 - registeredShortcut = null; 331 - } 332 - api.shortcuts.unregister(LOCAL_HUD_SHORTCUT); 333 - uninitCommands(); 259 + // Shortcuts are managed by manifest — no imperative unregister needed 334 260 closeHud(); 335 261 }; 336 262
+19 -1
extensions/hud/manifest.json
··· 5 5 "description": "Always-on-top heads-up display showing mode, IZUI state, and window info", 6 6 "version": "1.0.0", 7 7 "background": "background.html", 8 - "builtin": true 8 + "builtin": true, 9 + "commands": [ 10 + { 11 + "name": "hud", 12 + "description": "Toggle HUD overlay on/off", 13 + "action": { "type": "execute" } 14 + } 15 + ], 16 + "shortcuts": [ 17 + { 18 + "keys": "Option+H", 19 + "command": "hud", 20 + "global": true 21 + }, 22 + { 23 + "keys": "CommandOrControl+H", 24 + "command": "hud" 25 + } 26 + ] 9 27 }
+1 -1
extensions/hud/styles.css
··· 25 25 #hud-container { 26 26 width: 100%; 27 27 padding: 10px; 28 - background: rgba(0, 0, 0, 0.65); 28 + background: rgba(0, 0, 0, 0.81); 29 29 backdrop-filter: blur(12px); 30 30 -webkit-backdrop-filter: blur(12px); 31 31 border-radius: 8px;
-8
extensions/lex/chain-form.js
··· 114 114 if (resultPublished) return; 115 115 resultPublished = true; 116 116 117 - console.log('[chain-form] Publishing result, sessionId:', sessionId); 118 - 119 117 api.publish('chain-popup:result', { 120 118 sessionId, 121 119 data: data || null, ··· 214 212 } else { 215 213 throw new Error('No form data to submit'); 216 214 } 217 - 218 - console.log('[chain-form] Creating record:', currentNsid, record); 219 215 220 216 // Create record directly via atproto 221 217 const result = await xrpcPost(currentSession, 'com.atproto.repo.createRecord', { ··· 309 305 return; 310 306 } 311 307 312 - console.log('[chain-form] Waiting for content, sessionId:', sessionId); 313 - 314 308 let received = false; 315 309 316 310 // Subscribe for content from the cmd panel ··· 318 312 if (msg.sessionId !== sessionId) return; 319 313 if (received) return; 320 314 received = true; 321 - 322 - console.log('[chain-form] Received content:', msg.data); 323 315 324 316 const nsid = msg.data?.nsid; 325 317 if (!nsid) {
+5 -9
extensions/localsearch/background.html
··· 12 12 const api = window.app; 13 13 const extId = extension.id; 14 14 15 - console.log(`[ext:${extId}] background.html loaded`); 15 + // Initialize extension BEFORE signaling ready 16 + // (lazy loading waits for ext:ready, so handlers must be registered first) 17 + if (extension.init) { 18 + await extension.init(); 19 + } 16 20 17 21 // Signal ready to main process 18 22 api.publish('ext:ready', { ··· 24 28 } 25 29 }, api.scopes.SYSTEM); 26 30 27 - // Initialize extension 28 - if (extension.init) { 29 - console.log(`[ext:${extId}] calling init()`); 30 - extension.init(); 31 - } 32 - 33 31 // Handle shutdown request from main process 34 32 api.subscribe('app:shutdown', () => { 35 - console.log(`[ext:${extId}] received shutdown`); 36 33 if (extension.uninit) { 37 34 extension.uninit(); 38 35 } ··· 40 37 41 38 // Handle extension-specific shutdown 42 39 api.subscribe(`ext:${extId}:shutdown`, () => { 43 - console.log(`[ext:${extId}] received extension shutdown`); 44 40 if (extension.uninit) { 45 41 extension.uninit(); 46 42 }
+38 -63
extensions/localsearch/background.js
··· 3 3 * 4 4 * Search across local items, tags, and history. 5 5 * 6 + * Commands and shortcuts are declared in manifest.json. 7 + * This extension loads lazily on first command invocation. 8 + * The shortcut key is configurable — manifest provides the default (Option+F), 9 + * and if the user customizes it, the extension registers the custom key on load. 10 + * 6 11 * Runs in isolated extension process (peek://ext/localsearch/background.html) 7 12 * Uses api.settings for datastore-backed settings storage 8 13 */ ··· 12 17 const api = window.app; 13 18 const debug = api.debug; 14 19 15 - console.log('[ext:localsearch] background', labels.name); 16 - 17 20 // Extension content is served from peek://ext/localsearch/ 18 21 const address = 'peek://ext/localsearch/home.html'; 19 22 ··· 22 25 prefs: defaults.prefs 23 26 }; 24 27 25 - // Track registered shortcut for cleanup 28 + // Track registered shortcut for cleanup (only used for custom shortcut keys) 26 29 let registeredShortcut = null; 27 - let registeredCommands = []; 28 30 29 31 /** 30 32 * Load settings from datastore ··· 56 58 if (isOpeningSearch) return; 57 59 isOpeningSearch = true; 58 60 try { 59 - const height = 600; 60 - const width = 700; 61 - 62 61 const params = { 63 - // IZUI role 64 62 role: 'workspace', 65 - 66 63 key: address, 67 - height, 68 - width, 64 + height: 600, 65 + width: 700, 69 66 trackingSource: 'cmd', 70 67 trackingSourceId: 'search' 71 68 }; 72 69 73 - const window = await api.window.open(address, params); 74 - debug && console.log('[ext:localsearch] Search window opened:', window); 70 + await api.window.open(address, params); 75 71 } catch (error) { 76 72 console.error('[ext:localsearch] Failed to open search window:', error); 77 73 } finally { ··· 79 75 } 80 76 }; 81 77 82 - // ===== Command definitions ===== 78 + // ===== Registration ===== 83 79 84 - const commandDefinitions = [ 85 - { 86 - name: 'localsearch', 87 - description: 'Search local items, tags, and history', 88 - execute: async (ctx) => { 89 - console.log('[ext:localsearch] Opening search window'); 80 + /** 81 + * Register a custom shortcut key (only if different from manifest default) 82 + */ 83 + const initShortcut = (shortcut) => { 84 + // Manifest declares Option+f as default — only register imperatively 85 + // if the user has customized the shortcut key 86 + if (shortcut && shortcut !== 'Option+f') { 87 + api.shortcuts.register(shortcut, () => { 90 88 openSearchWindow(); 91 - } 89 + }, { global: true }); 90 + registeredShortcut = shortcut; 92 91 } 93 - ]; 94 - 95 - // ===== Registration ===== 96 - 97 - const initShortcut = (shortcut) => { 98 - api.shortcuts.register(shortcut, () => { 99 - openSearchWindow(); 100 - }, { global: true }); 101 - registeredShortcut = shortcut; 102 - }; 103 - 104 - const initCommands = () => { 105 - commandDefinitions.forEach(cmd => { 106 - api.commands.register(cmd); 107 - registeredCommands.push(cmd.name); 108 - }); 109 - // Note: Do NOT call api.commands.flush() here — the 16ms debounce in preload 110 - // ensures cmd's subscriber has time to be ready before the batch is sent. 111 - // Flushing immediately can race with cmd's initialization. 112 - console.log('[ext:localsearch] Registered commands:', registeredCommands); 113 - }; 114 - 115 - const uninitCommands = () => { 116 - registeredCommands.forEach(name => { 117 - api.commands.unregister(name); 118 - }); 119 - registeredCommands = []; 120 - console.log('[ext:localsearch] Unregistered commands'); 121 92 }; 122 93 123 94 const init = async () => { 124 - console.log('[ext:localsearch] init'); 125 - 126 95 // Load settings from datastore 127 96 currentSettings = await loadSettings(); 128 97 129 - initShortcut(currentSettings.prefs.shortcutKey); 98 + // Register command handler — manifest declares the command, 99 + // lazy stub registers metadata, this sets up the execute handler 100 + api.commands.register({ 101 + name: 'localsearch', 102 + description: 'Search local items, tags, and history', 103 + execute: async () => { openSearchWindow(); } 104 + }); 130 105 131 - // Register commands (cmd loads first with its subscribers ready via 100ms head start) 132 - initCommands(); 106 + // Register custom shortcut if user has configured one 107 + initShortcut(currentSettings.prefs.shortcutKey); 133 108 134 109 // Listen for settings changes to hot-reload (GLOBAL scope for cross-process) 135 110 api.subscribe('localsearch:settings-changed', async () => { 136 - console.log('[ext:localsearch] settings changed, reinitializing'); 137 - uninit(); 111 + debug && console.log('[ext:localsearch] settings changed, reinitializing'); 112 + uninitShortcut(); 138 113 currentSettings = await loadSettings(); 139 114 initShortcut(currentSettings.prefs.shortcutKey); 140 - initCommands(); 141 115 }, api.scopes.GLOBAL); 142 116 143 117 // Listen for settings updates from Settings UI 144 118 api.subscribe('localsearch:settings-update', async (msg) => { 145 - console.log('[ext:localsearch] settings-update received:', msg); 119 + debug && console.log('[ext:localsearch] settings-update received:', msg); 146 120 147 121 try { 148 122 if (msg.data) { ··· 158 132 159 133 await saveSettings(currentSettings); 160 134 161 - uninit(); 135 + uninitShortcut(); 162 136 initShortcut(currentSettings.prefs.shortcutKey); 163 - initCommands(); 164 137 165 138 api.publish('localsearch:settings-changed', currentSettings, api.scopes.GLOBAL); 166 139 } catch (err) { ··· 169 142 }, api.scopes.GLOBAL); 170 143 }; 171 144 172 - const uninit = () => { 173 - console.log('[ext:localsearch] uninit'); 145 + const uninitShortcut = () => { 174 146 if (registeredShortcut) { 175 147 api.shortcuts.unregister(registeredShortcut, { global: true }); 176 148 registeredShortcut = null; 177 149 } 178 - uninitCommands(); 150 + }; 151 + 152 + const uninit = () => { 153 + uninitShortcut(); 179 154 }; 180 155 181 156 export default {
+15 -1
extensions/localsearch/manifest.json
··· 5 5 "description": "Search across local items, tags, and history", 6 6 "version": "1.0.0", 7 7 "background": "background.html", 8 - "builtin": false 8 + "builtin": false, 9 + "commands": [ 10 + { 11 + "name": "localsearch", 12 + "description": "Search local items, tags, and history", 13 + "action": { "type": "execute" } 14 + } 15 + ], 16 + "shortcuts": [ 17 + { 18 + "keys": "Option+f", 19 + "command": "localsearch", 20 + "global": true 21 + } 22 + ] 9 23 }
+2 -19
extensions/page/background.js
··· 14 14 description: 'Page and window commands' 15 15 }; 16 16 17 - console.log('[ext:page] background', labels.name); 18 - 19 17 // ===== URL Validation ===== 20 18 21 19 /** ··· 66 64 name: 'open', 67 65 description: 'Open a URL in a new window', 68 66 execute: async (ctx) => { 69 - console.log('[ext:page] open command', ctx); 70 - 71 67 const parts = ctx.typed.split(' '); 72 68 parts.shift(); 73 69 74 70 const address = parts.shift(); 75 71 76 72 if (!address) { 77 - console.log('[ext:page] No address provided'); 78 73 return { error: 'No address provided' }; 79 74 } 80 75 81 76 // Check if the input is a valid URL and get the normalized version 82 77 const urlResult = getValidURL(address); 83 78 if (!urlResult.valid) { 84 - console.log('[ext:page] Invalid URL:', address); 85 79 return { error: 'Invalid URL. Must be a valid URL starting with http://, https://, or other valid protocol.' }; 86 80 } 87 81 88 82 // Use the normalized URL (with protocol added if needed) 89 83 const normalizedAddress = urlResult.url; 90 - console.log('[ext:page] Using normalized URL:', normalizedAddress); 91 84 92 85 try { 93 - const result = await api.window.open(normalizedAddress, { 86 + await api.window.open(normalizedAddress, { 94 87 width: 800, 95 88 height: 600, 96 89 trackingSource: 'cmd', 97 90 trackingSourceId: 'open' 98 91 }); 99 - console.log('[ext:page] Window opened:', result); 100 92 101 93 return { 102 94 command: 'open', ··· 116 108 name: 'modal', 117 109 description: 'Open a URL in a modal window that hides on blur or escape', 118 110 execute: async (ctx) => { 119 - console.log('[ext:page] modal command', ctx); 120 - 121 111 const parts = ctx.typed.split(' '); 122 112 parts.shift(); 123 113 124 114 const address = parts.shift(); 125 115 126 116 if (!address) { 127 - console.log('[ext:page] No address provided'); 128 117 return { error: 'No address provided' }; 129 118 } 130 119 131 120 try { 132 - const result = await api.window.openModal(address, { 121 + await api.window.openModal(address, { 133 122 width: 700, 134 123 height: 500 135 124 }); 136 - console.log('[ext:page] Modal window opened:', result); 137 125 } catch (error) { 138 126 console.error('[ext:page] Failed to open modal window:', error); 139 127 return { error: 'Failed to open modal window: ' + error.message }; ··· 156 144 api.commands.register(cmd); 157 145 registeredCommands.push(cmd.name); 158 146 }); 159 - console.log('[ext:page] Registered commands:', registeredCommands); 160 147 }; 161 148 162 149 const uninitCommands = () => { ··· 164 151 api.commands.unregister(name); 165 152 }); 166 153 registeredCommands = []; 167 - console.log('[ext:page] Unregistered commands'); 168 154 }; 169 155 170 156 const init = async () => { 171 - console.log('[ext:page] init'); 172 - 173 157 // Register commands (cmd loads first with its subscribers ready via 100ms head start) 174 158 initCommands(); 175 159 }; 176 160 177 161 const uninit = () => { 178 - console.log('[ext:page] uninit'); 179 162 uninitCommands(); 180 163 }; 181 164
+2 -9
extensions/pagestream/background.js
··· 13 13 const api = window.app; 14 14 const debug = api.debug; 15 15 16 - console.log('[ext:pagestream] background', labels.name); 17 - 18 16 // Extension content is served from peek://ext/pagestream/ 19 17 const address = 'peek://ext/pagestream/home.html'; 20 18 ··· 116 114 skipBare: false, 117 115 browse: async () => { openPagestreamWindow(); }, 118 116 }); 119 - console.log('[ext:pagestream] Noun registered: pagestream'); 120 117 }; 121 118 122 119 const uninitCommands = () => { 123 120 unregisterNoun('pagestream'); 124 - console.log('[ext:pagestream] Noun unregistered: pagestream'); 125 121 }; 126 122 127 123 const init = async () => { 128 - console.log('[ext:pagestream] init'); 129 - 130 124 // Load settings from datastore 131 125 currentSettings = await loadSettings(); 132 126 ··· 146 140 147 141 // Listen for settings changes to hot-reload 148 142 api.subscribe('pagestream:settings-changed', async () => { 149 - console.log('[ext:pagestream] settings changed, reinitializing'); 143 + debug && console.log('[ext:pagestream] settings changed, reinitializing'); 150 144 uninit(); 151 145 currentSettings = await loadSettings(); 152 146 initShortcut(currentSettings.prefs.shortcutKey); ··· 155 149 156 150 // Listen for settings updates from Settings UI 157 151 api.subscribe('pagestream:settings-update', async (msg) => { 158 - console.log('[ext:pagestream] settings-update received:', msg); 152 + debug && console.log('[ext:pagestream] settings-update received'); 159 153 160 154 try { 161 155 if (msg.data) { ··· 183 177 }; 184 178 185 179 const uninit = () => { 186 - console.log('[ext:pagestream] uninit'); 187 180 if (registeredShortcut) { 188 181 api.shortcuts.unregister(registeredShortcut, { global: true }); 189 182 registeredShortcut = null;
+15 -1
extensions/pagestream/manifest.json
··· 5 5 "description": "Vertical, chat-like navigational interface for web history", 6 6 "version": "1.0.0", 7 7 "background": "background.html", 8 - "builtin": true 8 + "builtin": true, 9 + "commands": [ 10 + { 11 + "name": "pagestream", 12 + "description": "Open browsing history stream", 13 + "action": { "type": "execute" } 14 + } 15 + ], 16 + "shortcuts": [ 17 + { 18 + "keys": "Option+p", 19 + "command": "pagestream", 20 + "global": true 21 + } 22 + ] 9 23 }
-10
extensions/scripts/background.js
··· 51 51 } 52 52 } 53 53 54 - console.log('[ext:scripts] background loaded'); 55 - 56 54 // Default settings 57 55 const defaults = { 58 56 scripts: [] ··· 96 94 * Open scripts manager UI 97 95 */ 98 96 const openScriptsManager = (params = {}) => { 99 - console.log('[ext:scripts] Opening scripts manager', params); 100 - 101 97 let url = 'peek://scripts/manager.html'; 102 98 if (params.scriptId) { 103 99 url += `?scriptId=${params.scriptId}`; ··· 297 293 produces: 'application/json' 298 294 }); 299 295 300 - console.log('[ext:scripts] Noun registered: scripts'); 301 296 }; 302 297 303 298 /** 304 299 * Initialize the extension 305 300 */ 306 301 const init = async () => { 307 - console.log('[ext:scripts] init'); 308 - 309 302 // Load scripts from datastore 310 303 currentSettings = await loadSettings(); 311 - console.log('[ext:scripts] Loaded', currentSettings.scripts.length, 'scripts'); 312 304 313 305 // Register commands (cmd loads first with its subscribers ready via 100ms head start) 314 306 registerCommands(); ··· 348 340 api.publish('scripts:execute:response', result, api.scopes.GLOBAL); 349 341 }, api.scopes.GLOBAL); 350 342 351 - console.log('[ext:scripts] Extension loaded'); 352 343 }; 353 344 354 345 /** 355 346 * Cleanup 356 347 */ 357 348 const uninit = () => { 358 - console.log('[ext:scripts] Cleaning up...'); 359 349 unregisterNoun('scripts'); 360 350 api.shortcuts.unregister('Command+Shift+S'); 361 351 };
-9
extensions/sheets/background.js
··· 11 11 const api = window.app; 12 12 const debug = api.debug; 13 13 14 - console.log('[ext:sheets] background', labels.name); 15 - 16 14 // Extension content is served from peek://sheets/ (hybrid mode) 17 15 const sheetPageUrl = 'peek://sheets/sheet.html'; 18 16 ··· 78 76 execute: async (ctx) => { 79 77 const sheets = await listSheets(); 80 78 if (sheets.length === 0) { 81 - console.log('[ext:sheets] No sheets found'); 82 79 return { success: true, message: 'No sheets found' }; 83 80 } 84 81 ··· 101 98 description: 'Create a new widget sheet', 102 99 execute: async (ctx) => { 103 100 const name = ctx?.args || 'Untitled Sheet'; 104 - console.log('[ext:sheets] Creating new sheet:', name); 105 101 const { sheetId } = await createSheet(name); 106 102 openSheet(sheetId, name); 107 103 } ··· 112 108 execute: async (ctx) => { 113 109 const sheets = await listSheets(); 114 110 if (sheets.length === 0) { 115 - console.log('[ext:sheets] No sheets to open'); 116 111 return { success: true, message: 'No sheets found' }; 117 112 } 118 113 ··· 154 149 api.commands.register(cmd); 155 150 registeredCommands.push(cmd.name); 156 151 }); 157 - console.log('[ext:sheets] Registered commands:', registeredCommands); 158 152 }; 159 153 160 154 const uninitCommands = () => { ··· 162 156 api.commands.unregister(name); 163 157 }); 164 158 registeredCommands = []; 165 - console.log('[ext:sheets] Unregistered commands'); 166 159 }; 167 160 168 161 const init = async () => { 169 - console.log('[ext:sheets] init'); 170 162 initCommands(); 171 163 }; 172 164 173 165 const uninit = () => { 174 - console.log('[ext:sheets] uninit'); 175 166 uninitCommands(); 176 167 }; 177 168
+2 -7
extensions/sync/background.js
··· 14 14 description: 'Sync commands and functionality' 15 15 }; 16 16 17 - console.log('[ext:sync] background', labels.name); 17 + const debug = api.debug; 18 18 19 19 // ===== Command definitions ===== 20 20 ··· 23 23 name: 'Sync now', 24 24 description: 'Trigger manual sync with server', 25 25 execute: () => { 26 - console.log('[sync] Triggering manual sync...'); 27 - 28 26 if (!api?.sync?.syncAll) { 29 27 console.error('[sync] Sync API not available'); 30 28 return { success: false, error: 'Sync API not available' }; ··· 32 30 33 31 // Fire and forget — don't block the cmd palette 34 32 api.sync.syncAll().then(result => { 35 - if (result.success) { 36 - const { pulled, pushed, conflicts } = result.data || {}; 37 - console.log('[sync] Sync completed:', result.data); 38 - } else { 33 + if (!result.success) { 39 34 console.error('[sync] Sync failed:', result.error); 40 35 } 41 36 }).catch(err => {
+7 -104
extensions/tags/background.js
··· 36 36 * List all tags (console output) 37 37 */ 38 38 async function listTags() { 39 - if (!hasPeekAPI) { 40 - console.log('Peek API not available'); 41 - return; 42 - } 39 + if (!hasPeekAPI) return; 43 40 44 41 const result = await api.datastore.getTagsByFrecency(); 45 - if (!result.success) { 46 - console.log('Failed to get tags'); 47 - return { success: false }; 48 - } 49 - 50 - if (result.data.length === 0) { 51 - console.log('No tags yet'); 52 - } else { 53 - console.log('All tags (by frecency):'); 54 - result.data.slice(0, 20).forEach(t => { 55 - console.log(' -', t.name, `(used ${t.frequency}x, frecency: ${t.frecencyScore?.toFixed(1) || 0})`); 56 - }); 57 - } 42 + if (!result.success) return { success: false }; 58 43 return { success: true, tags: result.data }; 59 44 } 60 45 ··· 104 89 for (const rawName of tagNames) { 105 90 const tagName = normalizeTagName(rawName); 106 91 // Get or create the tag 107 - api.log('[tag] Getting/creating tag:', tagName); 108 92 const tagResult = await api.datastore.getOrCreateTag(tagName); 109 - api.log('[tag] getOrCreateTag result:', JSON.stringify(tagResult)); 110 93 if (!tagResult.success) { 111 94 console.error('Failed to get/create tag:', tagName, tagResult.error); 112 95 continue; 113 96 } 114 97 115 98 const tag = tagResult.data.tag; 116 - api.log('[tag] Tag id:', tag.id, 'name:', tag.name); 117 99 118 100 // Link tag to item 119 - api.log('[tag] Linking tag', tag.id, 'to item', itemId); 120 101 const linkResult = await api.datastore.tagItem(itemId, tag.id); 121 - api.log('[tag] tagItem result:', JSON.stringify(linkResult)); 122 102 if (!linkResult.success) { 123 103 console.error('Failed to link tag:', tagName, linkResult.error); 124 104 continue; ··· 149 129 const tagName = normalizeTagName(rawName); 150 130 // Find the tag by name 151 131 const tag = tagsResult.data.find(t => t.name.toLowerCase() === tagName.toLowerCase()); 152 - if (!tag) { 153 - console.log('Tag not found on item:', tagName); 154 - continue; 155 - } 132 + if (!tag) continue; 156 133 157 134 // Unlink tag from item 158 135 const unlinkResult = await api.datastore.untagItem(itemId, tag.id); ··· 188 165 */ 189 166 async function executeTag(ctx) { 190 167 // Get active window 191 - api.log('tag command execute, ctx:', ctx); 192 168 const activeWindow = await getActiveWindow(); 193 - api.log('tag command: activeWindow =', activeWindow); 194 169 if (!activeWindow) { 195 - api.log('No active window found'); 196 170 return { success: false, error: 'No active window' }; 197 171 } 198 172 199 173 const url = activeWindow.url; 200 - api.log('Tagging URL:', url); 201 174 202 175 // Find item in datastore 203 176 let item = await findItemByUrl(url); 204 177 205 178 // If no item exists, create one 206 179 if (!item) { 207 - api.log('[tag] Creating new item for URL:', url); 208 180 const addResult = await api.datastore.addItem('url', { 209 181 content: url, 210 182 metadata: JSON.stringify({ title: activeWindow.title || '' }) 211 183 }); 212 - api.log('[tag] addItem result:', JSON.stringify(addResult)); 213 184 if (!addResult.success) { 214 185 console.error('Failed to create item:', addResult.error); 215 186 return { success: false, error: 'Failed to create item' }; 216 187 } 217 188 item = { id: addResult.data.id }; 218 - api.log('[tag] Created item with id:', item.id); 219 - } else { 220 - api.log('[tag] Found existing item:', item.id, 'for URL:', url); 221 189 } 222 190 223 191 // No args - show current tags 224 192 if (!ctx.search) { 225 193 const tags = await getTagsForItem(item.id); 226 - if (tags.length === 0) { 227 - console.log('No tags for:', url); 228 - } else { 229 - console.log('Tags for', url + ':'); 230 - tags.forEach(t => console.log(' -', t.name, `(frecency: ${t.frecencyScore?.toFixed(1) || 0})`)); 231 - } 232 194 return { success: true, tags }; 233 195 } 234 196 ··· 246 208 const tagsToProcess = removeMode ? args.slice(1) : args; 247 209 248 210 if (tagsToProcess.length === 0) { 249 - console.log('No tags specified'); 250 211 return { success: false, error: 'No tags specified' }; 251 212 } 252 213 ··· 254 215 if (removeMode) { 255 216 const results = await removeTagsFromItem(item.id, tagsToProcess); 256 217 const removed = results.filter(r => r.removed).map(r => r.tag.name); 257 - if (removed.length > 0) { 258 - console.log('Removed tags:', removed.join(', '), 'from', url); 259 - } 260 218 return { success: true, removed }; 261 219 } else { 262 - api.log('Adding tags to item:', item.id, 'tags:', tagsToProcess); 263 220 const results = await addTagsToItem(item.id, tagsToProcess); 264 - api.log('addTagsToItem results:', results); 265 221 const added = results.filter(r => !r.alreadyExists).map(r => r.tag.name); 266 222 const existing = results.filter(r => r.alreadyExists).map(r => r.tag.name); 267 - if (added.length > 0) { 268 - console.log('Added tags:', added.join(', '), 'to', url); 269 - } 270 - if (existing.length > 0) { 271 - console.log('Already tagged:', existing.join(', ')); 272 - } 273 223 return { success: true, added, existing }; 274 224 } 275 225 } ··· 281 231 // If search term provided, show all tags matching 282 232 if (ctx.search) { 283 233 const result = await api.datastore.getTagsByFrecency(); 284 - if (!result.success) { 285 - console.log('Failed to get tags'); 286 - return { success: false }; 287 - } 234 + if (!result.success) return { success: false }; 288 235 289 236 const filter = ctx.search.toLowerCase(); 290 237 const filtered = result.data.filter(t => t.name.toLowerCase().includes(filter)); 291 - 292 - if (filtered.length === 0) { 293 - console.log('No tags matching:', ctx.search); 294 - } else { 295 - console.log('Tags matching "' + ctx.search + '":'); 296 - filtered.forEach(t => { 297 - console.log(' -', t.name, `(used ${t.frequency}x, frecency: ${t.frecencyScore?.toFixed(1) || 0})`); 298 - }); 299 - } 300 238 return { success: true, tags: filtered }; 301 239 } 302 240 ··· 305 243 if (!activeWindow) { 306 244 // No active window - show all tags by frecency 307 245 const result = await api.datastore.getTagsByFrecency(); 308 - if (!result.success) { 309 - console.log('Failed to get tags'); 310 - return { success: false }; 311 - } 312 - 313 - if (result.data.length === 0) { 314 - console.log('No tags yet'); 315 - } else { 316 - console.log('All tags (by frecency):'); 317 - result.data.slice(0, 20).forEach(t => { 318 - console.log(' -', t.name, `(used ${t.frequency}x, frecency: ${t.frecencyScore?.toFixed(1) || 0})`); 319 - }); 320 - } 246 + if (!result.success) return { success: false }; 321 247 return { success: true, tags: result.data }; 322 248 } 323 249 324 250 const item = await findItemByUrl(activeWindow.url); 325 - if (!item) { 326 - console.log('No tags for:', activeWindow.url); 327 - return { success: true, tags: [] }; 328 - } 251 + if (!item) return { success: true, tags: [] }; 329 252 330 253 const tags = await getTagsForItem(item.id); 331 - 332 - if (tags.length === 0) { 333 - console.log('No tags for:', activeWindow.url); 334 - } else { 335 - console.log('Tags for', activeWindow.url + ':'); 336 - tags.forEach(t => console.log(' -', t.name)); 337 - } 338 - 339 254 return { success: true, tags }; 340 255 } 341 256 ··· 344 259 */ 345 260 async function executeUntag(ctx) { 346 261 if (!ctx.search) { 347 - console.log('Usage: untag <tag1> [tag2] ...'); 348 262 return { success: false, error: 'No tags specified' }; 349 263 } 350 264 ··· 409 323 if (ctx.search) { 410 324 try { 411 325 const { id, tags } = await createTagset(ctx.search); 412 - console.log(`Tagset created with ID: ${id}`); 413 - console.log(`Tags: ${tags.join(', ')}`); 414 326 api.publish('editor:changed', { action: 'add', itemId: id }, api.scopes.GLOBAL); 415 327 return { success: true, message: `Tagset created with tags: ${tags.join(', ')}` }; 416 328 } catch (error) { ··· 497 409 execute: executeTagset 498 410 }); 499 411 500 - console.log('[tags] Commands registered'); 501 412 }, 502 413 503 414 init() { 504 - console.log('[tags] init - Peek API available:', hasPeekAPI); 505 - 506 - if (!hasPeekAPI) { 507 - console.log('[tags] Running without Peek API - limited functionality'); 508 - return; 509 - } 415 + if (!hasPeekAPI) return; 510 416 511 417 // Register commands (cmd loads first with its subscribers ready via 100ms head start) 512 418 this.registerCommands(); ··· 516 422 517 423 // Register local shortcut Cmd+T (works when a Peek window is focused) 518 424 api.shortcuts.register('CommandOrControl+T', openTags); 519 - 520 - console.log('[tags] Extension loaded'); 521 425 }, 522 426 523 427 uninit() { 524 - console.log('[tags] Cleaning up...'); 525 428 526 429 if (hasPeekAPI) { 527 430 unregisterNoun('tags');
+1 -22
extensions/tags/home.js
··· 162 162 */ 163 163 const loadData = async () => { 164 164 // Load all items (unified: addresses, texts, tagsets, images) 165 - console.log('[tags] Loading items...'); 166 165 const itemsResult = await api.datastore.queryItems({}); 167 - console.log('[tags] queryItems result:', itemsResult.success, 'count:', itemsResult.data?.length); 168 166 if (itemsResult.success) { 169 167 state.items = itemsResult.data; 170 - console.log('[tags] Loaded items:', state.items.length); 171 - // Log first few items for debugging 172 - state.items.slice(0, 3).forEach((item, i) => { 173 - console.log(`[tags] Item ${i}:`, item.id, item.type, item.content?.substring(0, 50)); 174 - }); 175 168 } else { 176 169 console.error('[tags] Failed to load items:', itemsResult.error); 177 170 state.items = []; 178 171 } 179 172 180 173 // Load tags for each item 181 - console.log('[tags] Loading tags for', state.items.length, 'items...'); 182 174 for (const item of state.items) { 183 175 const tagsResult = await api.datastore.getItemTags(item.id); 184 176 if (tagsResult.success) { 185 177 state.itemTags.set(item.id, tagsResult.data); 186 - if (tagsResult.data.length > 0) { 187 - console.log('[tags] Item', item.id, 'has tags:', tagsResult.data.map(t => t.name).join(', ')); 188 - } 189 178 } 190 179 } 191 180 ··· 232 221 * 2. ESC in tags list -> closes window 233 222 */ 234 223 const handleEscape = () => { 235 - console.log('[tags:esc] handleEscape called, view:', state.currentView, 'searchQuery:', state.searchQuery); 236 - 237 224 // If in detail view with embedded editor and vim mode is enabled, 238 225 // check if vim is in insert mode. If so, exit insert mode instead 239 226 // of navigating away. The preload intercepts ESC via before-input-event ··· 244 231 if (cm && cm.state && cm.state.vim) { 245 232 const vimState = cm.state.vim; 246 233 if (vimState.insertMode || vimState.visualMode) { 247 - console.log('[tags:esc] Vim in insert/visual mode, sending <Esc> to vim'); 248 234 Vim.handleKey(cm, '<Esc>'); 249 235 return { handled: true }; 250 236 } 251 - // In normal mode, fall through to exit detail view 252 - console.log('[tags:esc] Vim in normal mode, will exit detail view'); 253 237 } 254 238 } catch (e) { 255 - console.log('[tags:esc] Could not check vim state:', e); 239 + // vim state unavailable, fall through 256 240 } 257 241 } 258 242 ··· 267 251 setTimeout(() => { 268 252 showList(); 269 253 }, 0); 270 - console.log('[tags:esc] VIEW_DETAIL -> navigating to list, returning handled: true'); 271 254 return { handled: true }; 272 255 } 273 256 ··· 276 259 state.searchQuery = ''; 277 260 searchInput.value = ''; 278 261 render(); 279 - console.log('[tags:esc] Cleared search, returning handled: true'); 280 262 return { handled: true }; 281 263 } 282 264 283 265 // If tag filter is active, clear it 284 266 if (state.activeTags.length > 0) { 285 267 clearTagFilter(); 286 - console.log('[tags:esc] Cleared tag filter, returning handled: true'); 287 268 return { handled: true }; 288 269 } 289 270 ··· 292 273 state.activeFilter = 'all'; 293 274 state.selectedIndex = 0; 294 275 render(); 295 - console.log('[tags:esc] Cleared type filter, returning handled: true'); 296 276 return { handled: true }; 297 277 } 298 278 299 279 // Nothing to clear, let window close 300 - console.log('[tags:esc] At root (VIEW_LIST), returning handled: false'); 301 280 return { handled: false }; 302 281 }; 303 282
+5 -9
extensions/websearch/background.html
··· 12 12 const api = window.app; 13 13 const extId = extension.id; 14 14 15 - console.log(`[ext:${extId}] background.html loaded`); 15 + // Initialize extension BEFORE signaling ready 16 + // (lazy loading waits for ext:ready, so handlers must be registered first) 17 + if (extension.init) { 18 + await extension.init(); 19 + } 16 20 17 21 // Signal ready to main process 18 22 api.publish('ext:ready', { ··· 24 28 } 25 29 }, api.scopes.SYSTEM); 26 30 27 - // Initialize extension 28 - if (extension.init) { 29 - console.log(`[ext:${extId}] calling init()`); 30 - extension.init(); 31 - } 32 - 33 31 // Handle shutdown request from main process 34 32 api.subscribe('app:shutdown', () => { 35 - console.log(`[ext:${extId}] received shutdown`); 36 33 if (extension.uninit) { 37 34 extension.uninit(); 38 35 } ··· 40 37 41 38 // Handle extension-specific shutdown 42 39 api.subscribe(`ext:${extId}:shutdown`, () => { 43 - console.log(`[ext:${extId}] received extension shutdown`); 44 40 if (extension.uninit) { 45 41 extension.uninit(); 46 42 }
+8 -10
extensions/websearch/background.js
··· 8 8 * - Per-engine commands (/google, /ddg, /bing, /wiki, /kagi) 9 9 * - A general "web search" command with engine selection 10 10 * 11 + * Commands are declared in manifest.json. 12 + * This extension loads lazily on first command invocation. 13 + * 11 14 * Runs in isolated extension process (peek://ext/websearch/background.html) 12 15 * Uses api.settings for datastore-backed settings storage 13 16 */ ··· 16 19 17 20 const api = window.app; 18 21 const debug = api.debug; 19 - 20 - console.log('[ext:websearch] background', labels.name); 21 22 22 23 // ===== BroadcastChannel for intra-extension messaging ===== 23 24 // Falls back to IPC pubsub if BroadcastChannel is unavailable (e.g., custom protocol origins) ··· 447 448 engine: { id: engine.id, name: engine.name, searchUrl: engine.searchUrl } 448 449 }, api.scopes.GLOBAL); 449 450 450 - console.log('[ext:websearch] Discovered search engine:', engine.name, 'from', pageUrl); 451 + debug && console.log('[ext:websearch] Discovered search engine:', engine.name, 'from', pageUrl); 451 452 return engine; 452 453 } catch (err) { 453 454 debug && console.log('[ext:websearch] OpenSearch discovery failed:', err.message); ··· 559 560 // Register engine commands 560 561 registerEngineCommands(); 561 562 562 - console.log('[ext:websearch] Registered commands:', [...registeredCommands, ...engineCommands]); 563 + debug && console.log('[ext:websearch] Registered commands:', [...registeredCommands, ...engineCommands]); 563 564 }; 564 565 565 566 /** ··· 573 574 574 575 unregisterEngineCommands(); 575 576 576 - console.log('[ext:websearch] Unregistered commands'); 577 + debug && console.log('[ext:websearch] Unregistered commands'); 577 578 }; 578 579 579 580 // ===== Event Handlers ===== ··· 687 688 // ===== Lifecycle ===== 688 689 689 690 const init = async () => { 690 - console.log('[ext:websearch] init'); 691 - 692 691 // Load settings from datastore 693 692 currentSettings = await loadSettings(); 694 693 ··· 718 717 719 718 // Listen for settings changes to hot-reload 720 719 api.subscribe('websearch:settings-changed', async () => { 721 - console.log('[ext:websearch] settings changed, reinitializing'); 720 + debug && console.log('[ext:websearch] settings changed, reinitializing'); 722 721 uninit(); 723 722 currentSettings = await loadSettings(); 724 723 await loadEngines(); ··· 727 726 728 727 // Listen for settings updates from Settings UI 729 728 api.subscribe('websearch:settings-update', async (msg) => { 730 - console.log('[ext:websearch] settings-update received:', msg); 729 + debug && console.log('[ext:websearch] settings-update received:', msg); 731 730 732 731 try { 733 732 if (msg.data) { ··· 760 759 }; 761 760 762 761 const uninit = () => { 763 - console.log('[ext:websearch] uninit'); 764 762 uninitCommands(); 765 763 766 764 // Cancel any in-flight suggestion request
+13 -1
extensions/websearch/manifest.json
··· 6 6 "version": "1.0.0", 7 7 "background": "background.html", 8 8 "builtin": true, 9 - "settingsSchema": "./settings-schema.json" 9 + "settingsSchema": "./settings-schema.json", 10 + "commands": [ 11 + { 12 + "name": "web search", 13 + "description": "Search the web with your default search engine", 14 + "action": { "type": "execute" } 15 + }, 16 + { 17 + "name": "open web search", 18 + "description": "Open the web search window", 19 + "action": { "type": "execute" } 20 + } 21 + ] 10 22 }
+35 -1
extensions/windows/manifest.json
··· 5 5 "description": "Full-screen window switcher with transparent overlay", 6 6 "version": "1.0.0", 7 7 "background": "background.html", 8 - "builtin": true 8 + "builtin": true, 9 + "commands": [ 10 + { 11 + "name": "windows", 12 + "description": "Show all windows in full-screen overlay", 13 + "action": { "type": "execute" } 14 + }, 15 + { 16 + "name": "center window", 17 + "description": "Center the active window on its display", 18 + "action": { "type": "execute" } 19 + }, 20 + { 21 + "name": "center all windows", 22 + "description": "Center all windows on their respective displays", 23 + "action": { "type": "execute" } 24 + }, 25 + { 26 + "name": "maximize window", 27 + "description": "Maximize the active window", 28 + "action": { "type": "execute" } 29 + }, 30 + { 31 + "name": "fullscreen", 32 + "description": "Toggle fullscreen for the active window", 33 + "action": { "type": "execute" } 34 + } 35 + ], 36 + "shortcuts": [ 37 + { 38 + "keys": "Option+w", 39 + "command": "windows", 40 + "global": true 41 + } 42 + ] 9 43 }
+4 -9
tests/desktop/localsearch.spec.ts
··· 24 24 // Note: Do NOT call closeSharedApp() - tests share the app instance 25 25 26 26 test.describe('Local Search Extension @desktop', () => { 27 - test('localsearch extension loads', async () => { 28 - // Verify the localsearch extension loaded by checking the extension list 29 - const isLoaded = await sharedBgWindow.evaluate(async () => { 30 - const api = (window as any).app; 31 - const result = await api.extensions.list(); 32 - if (!result.success || !result.data) return false; 33 - return result.data.some((e: any) => e.id === 'localsearch' && e.status === 'running'); 34 - }); 35 - expect(isLoaded).toBe(true); 27 + test('localsearch command is available via lazy stub', async () => { 28 + // Localsearch is lazy-loaded — the manifest command is registered by the lazy stub 29 + // before the extension itself loads 30 + await waitForCommand(sharedBgWindow, 'localsearch', 10000); 36 31 }); 37 32 38 33 test('localsearch command is registered', async () => {
+20 -13
tests/desktop/pagestream.spec.ts
··· 113 113 await sleep(500); 114 114 }); 115 115 116 - test('pagestream extension is loaded and running', async () => { 117 - const extensionRunning = await sharedBgWindow.evaluate(async () => { 118 - const result = await (window as any).app.extensions.list(); 119 - if (!result.success) return false; 120 - return result.data.some( 121 - (e: { id: string; status: string }) => e.id === 'pagestream' && e.status === 'running' 122 - ); 116 + test('pagestream command is available via lazy stub', async () => { 117 + // Pagestream is lazy-loaded — the manifest command is registered by the lazy stub 118 + const hasCommand = await sharedBgWindow.evaluate(async () => { 119 + const start = Date.now(); 120 + while (Date.now() - start < 10000) { 121 + const result = await (window as any).app.commands.list(); 122 + if (result.success && result.data) { 123 + if (result.data.some((c: { name: string }) => c.name === 'pagestream')) { 124 + return true; 125 + } 126 + } 127 + await new Promise(r => setTimeout(r, 200)); 128 + } 129 + return false; 123 130 }); 124 - expect(extensionRunning).toBe(true); 131 + expect(hasCommand).toBe(true); 125 132 }); 126 133 127 - test('pagestream commands are registered', async () => { 128 - // Wait for commands to be registered (cmd:ready may fire after extension init) 129 - const hasCommands = await sharedBgWindow.evaluate(async () => { 134 + test('pagestream command is registered via manifest', async () => { 135 + // Lazy stub registers the command from manifest — check for base command 136 + const hasCommand = await sharedBgWindow.evaluate(async () => { 130 137 const start = Date.now(); 131 138 while (Date.now() - start < 5000) { 132 139 const result = await (window as any).app.commands.list(); 133 140 if (result.success && result.data) { 134 141 const names = result.data.map((c: { name: string }) => c.name); 135 - if (names.includes('open pagestream') && names.includes('pagestream')) { 142 + if (names.includes('pagestream')) { 136 143 return true; 137 144 } 138 145 } ··· 140 147 } 141 148 return false; 142 149 }); 143 - expect(hasCommands).toBe(true); 150 + expect(hasCommand).toBe(true); 144 151 }); 145 152 146 153 test('pagestream window opens and shows stream', async () => {
+30 -14
tests/desktop/smoke.spec.ts
··· 14 14 import path from 'path'; 15 15 import { fileURLToPath } from 'url'; 16 16 import { spawn } from 'child_process'; 17 - import { waitForCommandResults, waitForWindowCount, waitForVisible, waitForClass, waitForResultsWithContent, waitForSelectionChange, sleep, waitForWindow, waitForExtensionsReady, queryCommandsWithRetry, waitForAppReady, waitForCommand } from '../helpers/window-utils'; 17 + import { waitForCommandResults, waitForWindowCount, waitForVisible, waitForClass, waitForResultsWithContent, waitForSelectionChange, sleep, waitForWindow, waitForExtensionsReady, queryCommandsWithRetry, waitForAppReady, waitForCommand, waitForPanelCommandsLoaded } from '../helpers/window-utils'; 18 18 19 19 const __filename = fileURLToPath(import.meta.url); 20 20 const __dirname = path.dirname(__filename); ··· 112 112 const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 113 113 expect(cmdWindow).toBeTruthy(); 114 114 115 - // Wait for input to be ready 115 + // Wait for input to be ready and commands to be loaded 116 116 await cmdWindow.waitForSelector('input', { timeout: 5000 }); 117 + await waitForPanelCommandsLoaded(cmdWindow, 10000); 117 118 118 - // Wait for commands to be loaded - type a built-in command first to verify 119 + // Type a built-in command first to verify 119 120 // Built-in commands (like 'settings') load faster than extension commands 120 121 await cmdWindow.fill('input', 'settings'); 121 122 // Press ArrowDown to show results (panel requires this to display dropdown) ··· 2698 2699 const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 2699 2700 expect(cmdWindow).toBeTruthy(); 2700 2701 2701 - // Wait for input to be ready 2702 + // Wait for input to be ready and commands to be loaded 2702 2703 await cmdWindow.waitForSelector('input', { timeout: 5000 }); 2704 + await waitForPanelCommandsLoaded(cmdWindow); 2703 2705 2704 2706 // Type 'lists' command 2705 2707 await cmdWindow.fill('input', 'lists'); ··· 2756 2758 const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 2757 2759 expect(cmdWindow).toBeTruthy(); 2758 2760 2759 - // Execute lists command 2761 + // Wait for input to be ready and commands to be loaded 2760 2762 await cmdWindow.waitForSelector('input', { timeout: 5000 }); 2763 + await waitForPanelCommandsLoaded(cmdWindow); 2764 + 2765 + // Execute lists command 2761 2766 await cmdWindow.fill('input', 'lists'); 2762 2767 await cmdWindow.press('input', 'ArrowDown'); 2763 2768 await waitForClass(cmdWindow, '#results', 'visible'); ··· 2811 2816 2812 2817 const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 2813 2818 expect(cmdWindow).toBeTruthy(); 2819 + 2820 + // Wait for input to be ready and commands to be loaded 2821 + await cmdWindow.waitForSelector('input', { timeout: 5000 }); 2822 + await waitForPanelCommandsLoaded(cmdWindow); 2814 2823 2815 2824 // Execute lists command — enters chain mode directly (save accepts */*) 2816 - await cmdWindow.waitForSelector('input', { timeout: 5000 }); 2817 2825 await cmdWindow.fill('input', 'lists'); 2818 2826 await cmdWindow.press('input', 'ArrowDown'); 2819 2827 await waitForClass(cmdWindow, '#results', 'visible'); ··· 2870 2878 const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 2871 2879 expect(cmdWindow).toBeTruthy(); 2872 2880 2881 + // Wait for input to be ready and commands to be loaded 2882 + await cmdWindow.waitForSelector('input', { timeout: 5000 }); 2883 + await waitForPanelCommandsLoaded(cmdWindow); 2884 + 2873 2885 // Execute lists and select item to enter chain mode 2874 - await cmdWindow.waitForSelector('input', { timeout: 5000 }); 2875 2886 await cmdWindow.fill('input', 'lists'); 2876 2887 await cmdWindow.press('input', 'ArrowDown'); 2877 2888 await waitForClass(cmdWindow, '#results', 'visible'); ··· 2922 2933 const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 2923 2934 expect(cmdWindow).toBeTruthy(); 2924 2935 2936 + // Wait for input to be ready and commands to be loaded 2937 + await cmdWindow.waitForSelector('input', { timeout: 5000 }); 2938 + await waitForPanelCommandsLoaded(cmdWindow); 2939 + 2925 2940 // Execute lists command 2926 - await cmdWindow.waitForSelector('input', { timeout: 5000 }); 2927 2941 await cmdWindow.fill('input', 'lists'); 2928 2942 await cmdWindow.press('input', 'ArrowDown'); 2929 2943 await waitForClass(cmdWindow, '#results', 'visible'); ··· 3395 3409 // Wait for #extensions container to exist (it may be hidden, so use 'attached' state) 3396 3410 await hostWindow!.waitForSelector('#extensions', { timeout: 15000, state: 'attached' }); 3397 3411 3398 - // Wait for at least 5 iframes to load (cmd, groups, peeks, slides, windows) 3412 + // Wait for at least 4 eager iframes to load (cmd, groups, hud, websearch) 3413 + // Lazy extensions (editor, localsearch, windows, pagestream) load on first use 3399 3414 await hostWindow!.waitForFunction( 3400 3415 () => { 3401 3416 const container = document.getElementById('extensions'); 3402 3417 const iframes = container ? container.querySelectorAll('iframe') : []; 3403 - return iframes.length >= 5; 3418 + return iframes.length >= 4; 3404 3419 }, 3405 3420 { timeout: 15000 } 3406 3421 ); ··· 3415 3430 }; 3416 3431 }); 3417 3432 3418 - // Should have iframes for built-in extensions (6: cmd, groups, peeks, slides, windows, settings) 3419 - expect(iframeData.count).toBeGreaterThanOrEqual(5); 3433 + // Eager extensions: cmd (EAGER_EXTENSION_IDS), hud (EAGER_EXTENSION_IDS), 3434 + // plus extensions without manifest commands (peeks, slides, page, files, etc.) 3435 + // Lazy extensions with manifest commands (editor, groups, localsearch, pagestream, 3436 + // websearch, windows) load on first use 3437 + expect(iframeData.count).toBeGreaterThanOrEqual(4); 3420 3438 expect(iframeData.srcs.some(s => s.includes('peek://cmd/'))).toBe(true); 3421 - expect(iframeData.srcs.some(s => s.includes('peek://groups/'))).toBe(true); 3422 3439 expect(iframeData.srcs.some(s => s.includes('peek://peeks/'))).toBe(true); 3423 3440 expect(iframeData.srcs.some(s => s.includes('peek://slides/'))).toBe(true); 3424 - expect(iframeData.srcs.some(s => s.includes('peek://windows/'))).toBe(true); 3425 3441 }); 3426 3442 3427 3443 test('example extension loads as separate window (external)', async () => {
+56 -10
tests/desktop/websearch.spec.ts
··· 69 69 70 70 test.describe('Web Search Extension @desktop', () => { 71 71 72 - test('websearch extension is loaded and running', async () => { 73 - const extensionRunning = await sharedBgWindow.evaluate(async () => { 74 - const result = await (window as any).app.extensions.list(); 75 - if (!result.success) return false; 76 - return result.data.some( 77 - (e: { id: string; status: string }) => e.id === 'websearch' && e.status === 'running' 78 - ); 72 + test('websearch manifest commands are registered via lazy stub', async () => { 73 + // Before extension loads, manifest commands should be available via lazy stubs 74 + const hasManifestCommands = await sharedBgWindow.evaluate(async () => { 75 + const start = Date.now(); 76 + while (Date.now() - start < 5000) { 77 + const result = await (window as any).app.commands.list(); 78 + if (result.success && result.data) { 79 + const names = result.data.map((c: { name: string }) => c.name); 80 + if (names.includes('web search') && names.includes('open web search')) { 81 + return true; 82 + } 83 + } 84 + await new Promise(r => setTimeout(r, 200)); 85 + } 86 + return false; 79 87 }); 80 - expect(extensionRunning).toBe(true); 88 + expect(hasManifestCommands).toBe(true); 81 89 }); 82 90 83 - test('websearch commands are registered', async () => { 84 - // Wait for commands to be registered (cmd:ready may fire after extension init) 91 + test('websearch extension loads on first command invocation', async () => { 92 + // Trigger lazy loading by publishing cmd:execute for the command 93 + const extensionLoaded = await sharedBgWindow.evaluate(async () => { 94 + const api = (window as any).app; 95 + // Trigger the lazy stub — this loads the extension 96 + api.publish('cmd:execute:open web search', {}, api.scopes.GLOBAL); 97 + 98 + // Wait for the extension to be running 99 + const start = Date.now(); 100 + while (Date.now() - start < 15000) { 101 + const result = await api.extensions.list(); 102 + if (result.success && result.data) { 103 + if (result.data.some( 104 + (e: { id: string; status: string }) => e.id === 'websearch' && e.status === 'running' 105 + )) return true; 106 + } 107 + await new Promise(r => setTimeout(r, 200)); 108 + } 109 + return false; 110 + }); 111 + expect(extensionLoaded).toBe(true); 112 + 113 + // Close any window that was opened by the command 114 + await sleep(500); 115 + const windows = sharedApp.windows(); 116 + for (const w of windows) { 117 + if (w.url().includes('websearch/home.html')) { 118 + const id = await sharedBgWindow.evaluate(async (url: string) => { 119 + const result = await (window as any).app.window.list({ includeInternal: false }); 120 + if (!result.success) return null; 121 + const win = result.windows.find((w: any) => w.url?.includes('websearch/home.html')); 122 + return win?.id || null; 123 + }, w.url()); 124 + if (id) await closeWindow(sharedBgWindow, id); 125 + } 126 + } 127 + }); 128 + 129 + test('websearch commands are registered after loading', async () => { 130 + // After lazy loading, both manifest and per-engine commands should be available 85 131 const hasCommands = await sharedBgWindow.evaluate(async () => { 86 132 const start = Date.now(); 87 133 while (Date.now() - start < 5000) {
+19
tests/helpers/window-utils.ts
··· 391 391 } 392 392 return commands as CommandInfo[]; 393 393 } 394 + 395 + /** 396 + * Wait for cmd panel commands to be loaded. 397 + * The panel exposes window._cmdState; this waits until commands are populated. 398 + * Must be called on a cmd panel window (not the background window). 399 + */ 400 + export async function waitForPanelCommandsLoaded( 401 + cmdWindow: Page, 402 + timeout = 5000 403 + ): Promise<void> { 404 + await cmdWindow.waitForFunction( 405 + () => { 406 + const state = (window as any)._cmdState; 407 + return state && Object.keys(state.commands).length > 0; 408 + }, 409 + undefined, 410 + { timeout } 411 + ); 412 + }