experiments in a post-browser web
10
fork

Configure Feed

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

feat(extension): add command bar popup with tag, note, search

Add a simple command bar popup to the browser extension with three
basic commands:

- tag: Add tags to the current page (comma-separated)
- note: Save a text note about the current page
- search: Search saved items by title, URL, or content

The popup UI follows the same patterns as the desktop cmd panel:
- Dark/light mode support via prefers-color-scheme
- Keyboard navigation (arrows, tab, enter, escape)
- Results list with icons and descriptions

Backend handlers in background.js:
- handleSaveTags: Creates/updates URL item and applies tags
- handleSaveNote: Creates text item with optional source URL metadata
- handleSearchItems: Filters items by title/content/URL match

+886 -1
+136
backend/extension/background.js
··· 109 109 return true; 110 110 } 111 111 112 + if (message.type === 'save-tags') { 113 + handleSaveTags(message.data).then(sendResponse); 114 + return true; 115 + } 116 + 117 + if (message.type === 'save-note') { 118 + handleSaveNote(message.data).then(sendResponse); 119 + return true; 120 + } 121 + 122 + if (message.type === 'search-items') { 123 + handleSearchItems(message.data).then(sendResponse); 124 + return true; 125 + } 126 + 112 127 if (message.type === 'bookmark-sync-toggle') { 113 128 handleBookmarkSyncToggle(message.enabled).then(sendResponse); 114 129 return true; ··· 219 234 return await getHistoryStats(); 220 235 } catch (error) { 221 236 return { historyItems: 0, imported: 0, synced: 0 }; 237 + } 238 + } 239 + 240 + // ==================== Command Bar Handlers ==================== 241 + 242 + async function handleSaveTags(data) { 243 + const { url, title, tags } = data; 244 + 245 + if (!url || !tags || tags.length === 0) { 246 + return { success: false, error: 'URL and tags are required' }; 247 + } 248 + 249 + try { 250 + await openDatabase(); 251 + 252 + // Import from datastore 253 + const { addItem, getOrCreateTag, tagItem, queryItems } = await import('./datastore.js'); 254 + 255 + // Check if this URL already exists 256 + const existing = await queryItems({ type: 'url', url }); 257 + let itemId; 258 + 259 + if (existing.success && existing.data && existing.data.length > 0) { 260 + itemId = existing.data[0].id; 261 + } else { 262 + // Create new URL item 263 + const result = await addItem('url', { url, title }); 264 + if (!result.success) { 265 + return { success: false, error: result.error || 'Failed to create item' }; 266 + } 267 + itemId = result.data.id; 268 + } 269 + 270 + // Add each tag 271 + for (const tagName of tags) { 272 + const tagResult = await getOrCreateTag(tagName); 273 + if (tagResult.success && tagResult.data) { 274 + await tagItem(itemId, tagResult.data.tag.id); 275 + } 276 + } 277 + 278 + return { success: true, data: { itemId, tags } }; 279 + } catch (error) { 280 + console.error('[peek:bg] Save tags error:', error); 281 + return { success: false, error: error.message }; 282 + } 283 + } 284 + 285 + async function handleSaveNote(data) { 286 + const { url, title, note } = data; 287 + 288 + if (!note) { 289 + return { success: false, error: 'Note content is required' }; 290 + } 291 + 292 + try { 293 + await openDatabase(); 294 + 295 + const { addItem } = await import('./datastore.js'); 296 + 297 + // Create a text item for the note 298 + // Include URL as metadata if provided 299 + const metadata = url ? { sourceUrl: url, sourceTitle: title } : {}; 300 + 301 + const result = await addItem('text', { 302 + content: note, 303 + title: title ? `Note: ${title}` : 'Note', 304 + metadata 305 + }); 306 + 307 + if (!result.success) { 308 + return { success: false, error: result.error || 'Failed to save note' }; 309 + } 310 + 311 + return { success: true, data: { id: result.data.id } }; 312 + } catch (error) { 313 + console.error('[peek:bg] Save note error:', error); 314 + return { success: false, error: error.message }; 315 + } 316 + } 317 + 318 + async function handleSearchItems(data) { 319 + const { query } = data; 320 + 321 + if (!query) { 322 + return { success: false, error: 'Search query is required' }; 323 + } 324 + 325 + try { 326 + await openDatabase(); 327 + 328 + const { queryItems } = await import('./datastore.js'); 329 + 330 + // Search items - the queryItems function should support text search 331 + // For now, we'll do a simple filter on title/content 332 + const allItems = await queryItems({}); 333 + 334 + if (!allItems.success) { 335 + return { success: false, error: allItems.error }; 336 + } 337 + 338 + const lowerQuery = query.toLowerCase(); 339 + const results = (allItems.data || []).filter(item => { 340 + const title = (item.title || '').toLowerCase(); 341 + const content = (item.content || '').toLowerCase(); 342 + const url = (item.url || '').toLowerCase(); 343 + return title.includes(lowerQuery) || 344 + content.includes(lowerQuery) || 345 + url.includes(lowerQuery); 346 + }).slice(0, 20).map(item => ({ 347 + id: item.id, 348 + type: item.type, 349 + title: item.title, 350 + url: item.url, 351 + snippet: item.content ? item.content.substring(0, 100) : null 352 + })); 353 + 354 + return { success: true, data: results }; 355 + } catch (error) { 356 + console.error('[peek:bg] Search error:', error); 357 + return { success: false, error: error.message }; 222 358 } 223 359 } 224 360
+2 -1
backend/extension/manifest.json
··· 9 9 "type": "module" 10 10 }, 11 11 "action": { 12 - "default_title": "Peek" 12 + "default_title": "Peek", 13 + "default_popup": "popup.html" 13 14 }, 14 15 "permissions": ["storage", "alarms", "bookmarks", "tabs", "tabGroups", "history"], 15 16 "options_page": "options.html",
+301
backend/extension/popup.css
··· 1 + * { 2 + box-sizing: border-box; 3 + margin: 0; 4 + padding: 0; 5 + } 6 + 7 + body { 8 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 9 + font-size: 14px; 10 + line-height: 1.5; 11 + color: #333; 12 + background: #fff; 13 + width: 320px; 14 + min-height: 100px; 15 + } 16 + 17 + /* Command bar input */ 18 + .command-bar { 19 + position: relative; 20 + padding: 12px; 21 + border-bottom: 1px solid #eee; 22 + } 23 + 24 + #command-input { 25 + width: 100%; 26 + padding: 10px 12px; 27 + border: 1px solid #ddd; 28 + border-radius: 6px; 29 + font-size: 15px; 30 + font-family: inherit; 31 + background: #fafafa; 32 + } 33 + 34 + #command-input:focus { 35 + outline: none; 36 + border-color: #4a90d9; 37 + background: #fff; 38 + box-shadow: 0 0 0 2px rgba(74, 144, 217, 0.15); 39 + } 40 + 41 + .command-hint { 42 + position: absolute; 43 + top: 22px; 44 + left: 24px; 45 + right: 24px; 46 + pointer-events: none; 47 + color: #999; 48 + font-size: 15px; 49 + white-space: nowrap; 50 + overflow: hidden; 51 + } 52 + 53 + /* Results list */ 54 + .results { 55 + max-height: 200px; 56 + overflow-y: auto; 57 + } 58 + 59 + .results:empty { 60 + display: none; 61 + } 62 + 63 + .result-item { 64 + padding: 10px 12px; 65 + cursor: pointer; 66 + display: flex; 67 + align-items: center; 68 + gap: 10px; 69 + border-bottom: 1px solid #f0f0f0; 70 + } 71 + 72 + .result-item:last-child { 73 + border-bottom: none; 74 + } 75 + 76 + .result-item:hover { 77 + background: #f5f5f5; 78 + } 79 + 80 + .result-item.selected { 81 + background: #e8f0fa; 82 + } 83 + 84 + .result-icon { 85 + width: 20px; 86 + height: 20px; 87 + display: flex; 88 + align-items: center; 89 + justify-content: center; 90 + color: #666; 91 + font-size: 14px; 92 + } 93 + 94 + .result-text { 95 + flex: 1; 96 + min-width: 0; 97 + } 98 + 99 + .result-title { 100 + font-weight: 500; 101 + white-space: nowrap; 102 + overflow: hidden; 103 + text-overflow: ellipsis; 104 + } 105 + 106 + .result-description { 107 + font-size: 12px; 108 + color: #888; 109 + white-space: nowrap; 110 + overflow: hidden; 111 + text-overflow: ellipsis; 112 + } 113 + 114 + /* Input area for note/tag entry */ 115 + .input-area { 116 + padding: 12px; 117 + border-top: 1px solid #eee; 118 + } 119 + 120 + .input-label { 121 + font-weight: 500; 122 + margin-bottom: 8px; 123 + color: #333; 124 + } 125 + 126 + #input-text { 127 + width: 100%; 128 + padding: 8px 10px; 129 + border: 1px solid #ddd; 130 + border-radius: 4px; 131 + font-size: 14px; 132 + font-family: inherit; 133 + resize: vertical; 134 + min-height: 60px; 135 + } 136 + 137 + #input-text:focus { 138 + outline: none; 139 + border-color: #4a90d9; 140 + box-shadow: 0 0 0 2px rgba(74, 144, 217, 0.15); 141 + } 142 + 143 + .input-actions { 144 + display: flex; 145 + justify-content: flex-end; 146 + gap: 8px; 147 + margin-top: 10px; 148 + } 149 + 150 + /* Buttons */ 151 + .btn { 152 + padding: 6px 14px; 153 + border: 1px solid #ccc; 154 + border-radius: 4px; 155 + background: #fff; 156 + font-size: 13px; 157 + font-family: inherit; 158 + cursor: pointer; 159 + } 160 + 161 + .btn:hover { 162 + background: #f5f5f5; 163 + } 164 + 165 + .btn.primary { 166 + background: #4a90d9; 167 + color: #fff; 168 + border-color: #4a90d9; 169 + } 170 + 171 + .btn.primary:hover { 172 + background: #3a7bc8; 173 + } 174 + 175 + /* Status message */ 176 + .status { 177 + padding: 10px 12px; 178 + font-size: 13px; 179 + color: #666; 180 + text-align: center; 181 + } 182 + 183 + .status:empty { 184 + display: none; 185 + } 186 + 187 + .status.error { 188 + color: #c0392b; 189 + background: #fdf2f2; 190 + } 191 + 192 + .status.success { 193 + color: #27ae60; 194 + background: #f0fdf4; 195 + } 196 + 197 + /* Help text at bottom */ 198 + .help-text { 199 + padding: 10px 12px; 200 + font-size: 11px; 201 + color: #999; 202 + text-align: center; 203 + border-top: 1px solid #f0f0f0; 204 + background: #fafafa; 205 + } 206 + 207 + .shortcut { 208 + display: inline-block; 209 + background: #e8e8e8; 210 + padding: 2px 6px; 211 + border-radius: 3px; 212 + font-family: monospace; 213 + font-size: 10px; 214 + margin-right: 2px; 215 + } 216 + 217 + /* Dark mode */ 218 + @media (prefers-color-scheme: dark) { 219 + body { 220 + background: #1e1e1e; 221 + color: #ddd; 222 + } 223 + 224 + .command-bar { 225 + border-bottom-color: #333; 226 + } 227 + 228 + #command-input { 229 + background: #2a2a2a; 230 + border-color: #444; 231 + color: #ddd; 232 + } 233 + 234 + #command-input:focus { 235 + background: #333; 236 + border-color: #4a90d9; 237 + } 238 + 239 + #command-input::placeholder { 240 + color: #777; 241 + } 242 + 243 + .command-hint { 244 + color: #666; 245 + } 246 + 247 + .result-item { 248 + border-bottom-color: #333; 249 + } 250 + 251 + .result-item:hover { 252 + background: #2a2a2a; 253 + } 254 + 255 + .result-item.selected { 256 + background: #2a3a4a; 257 + } 258 + 259 + .result-description { 260 + color: #777; 261 + } 262 + 263 + .input-area { 264 + border-top-color: #333; 265 + } 266 + 267 + #input-text { 268 + background: #2a2a2a; 269 + border-color: #444; 270 + color: #ddd; 271 + } 272 + 273 + .btn { 274 + background: #333; 275 + border-color: #555; 276 + color: #ddd; 277 + } 278 + 279 + .btn:hover { 280 + background: #444; 281 + } 282 + 283 + .help-text { 284 + background: #1a1a1a; 285 + border-top-color: #333; 286 + color: #777; 287 + } 288 + 289 + .shortcut { 290 + background: #333; 291 + color: #aaa; 292 + } 293 + 294 + .status.error { 295 + background: #3a2020; 296 + } 297 + 298 + .status.success { 299 + background: #1a3020; 300 + } 301 + }
+42
backend/extension/popup.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>Peek</title> 7 + <link rel="stylesheet" href="popup.css"> 8 + </head> 9 + <body> 10 + <div class="command-bar"> 11 + <input 12 + type="text" 13 + id="command-input" 14 + placeholder="Type a command..." 15 + autofocus 16 + spellcheck="false" 17 + > 18 + <div id="command-hint" class="command-hint"></div> 19 + </div> 20 + 21 + <div id="results" class="results"></div> 22 + 23 + <div id="input-area" class="input-area" hidden> 24 + <div id="input-label" class="input-label"></div> 25 + <textarea id="input-text" rows="3" placeholder=""></textarea> 26 + <div class="input-actions"> 27 + <button id="input-cancel" class="btn">Cancel</button> 28 + <button id="input-submit" class="btn primary">Save</button> 29 + </div> 30 + </div> 31 + 32 + <div id="status" class="status"></div> 33 + 34 + <div class="help-text"> 35 + <span class="shortcut">tag</span> Tag this page 36 + <span class="shortcut">note</span> Save a note 37 + <span class="shortcut">search</span> Search items 38 + </div> 39 + 40 + <script type="module" src="popup.js"></script> 41 + </body> 42 + </html>
+405
backend/extension/popup.js
··· 1 + /** 2 + * Peek Browser Extension - Command Bar Popup 3 + * 4 + * Simple command bar for quick actions: 5 + * - tag: Add tags to the current page 6 + * - note: Save a text note about the page 7 + * - search: Search saved items 8 + */ 9 + 10 + // Available commands 11 + const COMMANDS = { 12 + tag: { 13 + name: 'tag', 14 + title: 'Tag this page', 15 + description: 'Add tags to the current page', 16 + icon: '#', 17 + action: 'input', 18 + inputLabel: 'Enter tags (comma-separated)', 19 + inputPlaceholder: 'work, reference, read-later' 20 + }, 21 + note: { 22 + name: 'note', 23 + title: 'Save a note', 24 + description: 'Write a note about this page', 25 + icon: '📝', 26 + action: 'input', 27 + inputLabel: 'Enter your note', 28 + inputPlaceholder: 'Add a note about this page...' 29 + }, 30 + search: { 31 + name: 'search', 32 + title: 'Search items', 33 + description: 'Search your saved items', 34 + icon: '🔍', 35 + action: 'search' 36 + } 37 + }; 38 + 39 + // State 40 + let state = { 41 + typed: '', 42 + matches: [], 43 + selectedIndex: 0, 44 + currentCommand: null, 45 + currentTab: null 46 + }; 47 + 48 + // DOM elements 49 + const commandInput = document.getElementById('command-input'); 50 + const commandHint = document.getElementById('command-hint'); 51 + const resultsEl = document.getElementById('results'); 52 + const inputArea = document.getElementById('input-area'); 53 + const inputLabel = document.getElementById('input-label'); 54 + const inputText = document.getElementById('input-text'); 55 + const inputCancel = document.getElementById('input-cancel'); 56 + const inputSubmit = document.getElementById('input-submit'); 57 + const statusEl = document.getElementById('status'); 58 + 59 + // Initialize 60 + async function init() { 61 + // Get current tab info 62 + try { 63 + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); 64 + state.currentTab = tab; 65 + } catch (err) { 66 + console.error('Failed to get current tab:', err); 67 + } 68 + 69 + // Set up event listeners 70 + commandInput.addEventListener('input', onInput); 71 + commandInput.addEventListener('keydown', onKeyDown); 72 + inputCancel.addEventListener('click', cancelInput); 73 + inputSubmit.addEventListener('click', submitInput); 74 + inputText.addEventListener('keydown', onInputTextKeyDown); 75 + 76 + // Focus the command input 77 + commandInput.focus(); 78 + 79 + // Show all commands initially 80 + updateMatches(''); 81 + renderResults(); 82 + } 83 + 84 + // Handle input changes 85 + function onInput(e) { 86 + state.typed = e.target.value; 87 + state.selectedIndex = 0; 88 + updateMatches(state.typed); 89 + renderResults(); 90 + updateHint(); 91 + } 92 + 93 + // Handle keyboard navigation 94 + function onKeyDown(e) { 95 + if (e.key === 'ArrowDown') { 96 + e.preventDefault(); 97 + if (state.selectedIndex < state.matches.length - 1) { 98 + state.selectedIndex++; 99 + renderResults(); 100 + } 101 + } else if (e.key === 'ArrowUp') { 102 + e.preventDefault(); 103 + if (state.selectedIndex > 0) { 104 + state.selectedIndex--; 105 + renderResults(); 106 + } 107 + } else if (e.key === 'Enter') { 108 + e.preventDefault(); 109 + executeSelected(); 110 + } else if (e.key === 'Escape') { 111 + e.preventDefault(); 112 + window.close(); 113 + } else if (e.key === 'Tab') { 114 + e.preventDefault(); 115 + // Autocomplete the selected command 116 + if (state.matches.length > 0) { 117 + const selected = state.matches[state.selectedIndex]; 118 + commandInput.value = selected.name + ' '; 119 + state.typed = commandInput.value; 120 + updateMatches(state.typed); 121 + renderResults(); 122 + } 123 + } 124 + } 125 + 126 + // Handle Enter in input text area 127 + function onInputTextKeyDown(e) { 128 + if (e.key === 'Escape') { 129 + e.preventDefault(); 130 + cancelInput(); 131 + } else if (e.key === 'Enter' && e.metaKey) { 132 + e.preventDefault(); 133 + submitInput(); 134 + } 135 + } 136 + 137 + // Find matching commands 138 + function updateMatches(text) { 139 + const query = text.trim().toLowerCase(); 140 + 141 + if (!query) { 142 + // Show all commands 143 + state.matches = Object.values(COMMANDS); 144 + return; 145 + } 146 + 147 + // Check if text starts with a command name (for search queries) 148 + const parts = query.split(' '); 149 + const cmdName = parts[0]; 150 + 151 + if (COMMANDS[cmdName] && parts.length > 1) { 152 + // User typed a full command plus query 153 + state.matches = [COMMANDS[cmdName]]; 154 + return; 155 + } 156 + 157 + // Filter commands by prefix/substring match 158 + state.matches = Object.values(COMMANDS).filter(cmd => { 159 + return cmd.name.includes(query) || 160 + cmd.title.toLowerCase().includes(query); 161 + }); 162 + } 163 + 164 + // Update hint text 165 + function updateHint() { 166 + if (state.matches.length > 0 && state.typed) { 167 + const match = state.matches[state.selectedIndex]; 168 + if (match.name.startsWith(state.typed.toLowerCase())) { 169 + commandHint.textContent = match.name; 170 + } else { 171 + commandHint.textContent = ''; 172 + } 173 + } else { 174 + commandHint.textContent = ''; 175 + } 176 + } 177 + 178 + // Render results list 179 + function renderResults() { 180 + resultsEl.innerHTML = ''; 181 + 182 + state.matches.forEach((cmd, index) => { 183 + const item = document.createElement('div'); 184 + item.className = 'result-item' + (index === state.selectedIndex ? ' selected' : ''); 185 + item.innerHTML = ` 186 + <div class="result-icon">${cmd.icon}</div> 187 + <div class="result-text"> 188 + <div class="result-title">${cmd.title}</div> 189 + <div class="result-description">${cmd.description}</div> 190 + </div> 191 + `; 192 + item.addEventListener('click', () => { 193 + state.selectedIndex = index; 194 + executeSelected(); 195 + }); 196 + resultsEl.appendChild(item); 197 + }); 198 + } 199 + 200 + // Execute the selected command 201 + function executeSelected() { 202 + if (state.matches.length === 0) return; 203 + 204 + const cmd = state.matches[state.selectedIndex]; 205 + state.currentCommand = cmd; 206 + 207 + if (cmd.action === 'input') { 208 + showInputArea(cmd); 209 + } else if (cmd.action === 'search') { 210 + performSearch(); 211 + } 212 + } 213 + 214 + // Show input area for tag/note entry 215 + function showInputArea(cmd) { 216 + inputLabel.textContent = cmd.inputLabel; 217 + inputText.placeholder = cmd.inputPlaceholder; 218 + inputText.value = ''; 219 + inputArea.hidden = false; 220 + resultsEl.innerHTML = ''; 221 + commandInput.disabled = true; 222 + inputText.focus(); 223 + } 224 + 225 + // Cancel input and go back 226 + function cancelInput() { 227 + inputArea.hidden = true; 228 + commandInput.disabled = false; 229 + commandInput.value = ''; 230 + state.typed = ''; 231 + state.currentCommand = null; 232 + updateMatches(''); 233 + renderResults(); 234 + commandInput.focus(); 235 + } 236 + 237 + // Submit the input 238 + async function submitInput() { 239 + const value = inputText.value.trim(); 240 + if (!value) { 241 + showStatus('Please enter a value', 'error'); 242 + return; 243 + } 244 + 245 + const cmd = state.currentCommand; 246 + 247 + if (cmd.name === 'tag') { 248 + await saveTag(value); 249 + } else if (cmd.name === 'note') { 250 + await saveNote(value); 251 + } 252 + } 253 + 254 + // Save tags for current page 255 + async function saveTag(tagString) { 256 + const tags = tagString.split(',').map(t => t.trim()).filter(t => t); 257 + 258 + if (tags.length === 0) { 259 + showStatus('Please enter at least one tag', 'error'); 260 + return; 261 + } 262 + 263 + try { 264 + // Send message to background script 265 + const response = await chrome.runtime.sendMessage({ 266 + type: 'save-tags', 267 + data: { 268 + url: state.currentTab?.url, 269 + title: state.currentTab?.title, 270 + tags: tags 271 + } 272 + }); 273 + 274 + if (response?.success) { 275 + showStatus(`Tagged with: ${tags.join(', ')}`, 'success'); 276 + setTimeout(() => window.close(), 1000); 277 + } else { 278 + showStatus(response?.error || 'Failed to save tags', 'error'); 279 + } 280 + } catch (err) { 281 + console.error('Failed to save tags:', err); 282 + showStatus('Failed to save tags', 'error'); 283 + } 284 + } 285 + 286 + // Save note for current page 287 + async function saveNote(noteText) { 288 + try { 289 + const response = await chrome.runtime.sendMessage({ 290 + type: 'save-note', 291 + data: { 292 + url: state.currentTab?.url, 293 + title: state.currentTab?.title, 294 + note: noteText 295 + } 296 + }); 297 + 298 + if (response?.success) { 299 + showStatus('Note saved', 'success'); 300 + setTimeout(() => window.close(), 1000); 301 + } else { 302 + showStatus(response?.error || 'Failed to save note', 'error'); 303 + } 304 + } catch (err) { 305 + console.error('Failed to save note:', err); 306 + showStatus('Failed to save note', 'error'); 307 + } 308 + } 309 + 310 + // Perform search 311 + async function performSearch() { 312 + const query = state.typed.replace(/^search\s*/i, '').trim(); 313 + 314 + if (!query) { 315 + // Show search input 316 + showInputArea({ 317 + inputLabel: 'Search your items', 318 + inputPlaceholder: 'Enter search query...', 319 + name: 'search-query' 320 + }); 321 + state.currentCommand = { name: 'search-query' }; 322 + return; 323 + } 324 + 325 + try { 326 + const response = await chrome.runtime.sendMessage({ 327 + type: 'search-items', 328 + data: { query } 329 + }); 330 + 331 + if (response?.success && response.data?.length > 0) { 332 + renderSearchResults(response.data); 333 + } else if (response?.success) { 334 + showStatus('No results found', ''); 335 + } else { 336 + showStatus(response?.error || 'Search failed', 'error'); 337 + } 338 + } catch (err) { 339 + console.error('Search failed:', err); 340 + showStatus('Search failed', 'error'); 341 + } 342 + } 343 + 344 + // Render search results 345 + function renderSearchResults(items) { 346 + resultsEl.innerHTML = ''; 347 + 348 + items.forEach(item => { 349 + const el = document.createElement('div'); 350 + el.className = 'result-item'; 351 + el.innerHTML = ` 352 + <div class="result-icon">${item.type === 'note' ? '📝' : '🔗'}</div> 353 + <div class="result-text"> 354 + <div class="result-title">${escapeHtml(item.title || item.url)}</div> 355 + <div class="result-description">${escapeHtml(item.snippet || item.url || '')}</div> 356 + </div> 357 + `; 358 + el.addEventListener('click', () => { 359 + if (item.url) { 360 + chrome.tabs.create({ url: item.url }); 361 + window.close(); 362 + } 363 + }); 364 + resultsEl.appendChild(el); 365 + }); 366 + } 367 + 368 + // Show status message 369 + function showStatus(message, type) { 370 + statusEl.textContent = message; 371 + statusEl.className = 'status' + (type ? ' ' + type : ''); 372 + 373 + if (type === 'success') { 374 + inputArea.hidden = true; 375 + } 376 + } 377 + 378 + // Escape HTML to prevent XSS 379 + function escapeHtml(str) { 380 + if (!str) return ''; 381 + return str 382 + .replace(/&/g, '&amp;') 383 + .replace(/</g, '&lt;') 384 + .replace(/>/g, '&gt;') 385 + .replace(/"/g, '&quot;'); 386 + } 387 + 388 + // Handle search-query submit differently 389 + const originalSubmitInput = submitInput; 390 + submitInput = async function() { 391 + if (state.currentCommand?.name === 'search-query') { 392 + const query = inputText.value.trim(); 393 + if (query) { 394 + commandInput.value = 'search ' + query; 395 + state.typed = commandInput.value; 396 + inputArea.hidden = true; 397 + await performSearch(); 398 + } 399 + return; 400 + } 401 + await originalSubmitInput(); 402 + }; 403 + 404 + // Initialize when DOM is ready 405 + init();