experiments in a post-browser web
10
fork

Configure Feed

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

refactor(cmd): use connector pattern for edit and note commands

Commands now return structured output with mimeType instead of directly
publishing editor events. Panel layer handles routing based on mimeType:
- 'item' mimeType: auto-opens editor for existing items
- 'new-item' mimeType: opens empty editor for new items

This makes commands composable and testable by separating the command
logic from UI routing concerns.

+103 -38
+23 -30
extensions/cmd/commands/edit.js
··· 1 1 /** 2 2 * Edit command - opens notes in the editor 3 3 * 4 + * Uses the connector pattern: 5 + * - Returns { output: { data, mimeType: 'item' } } for selected notes 6 + * - Panel layer handles routing to editor based on mimeType 7 + * 4 8 * Usage: 5 9 * cmd:edit {search} - search for notes and open in editor 6 - * - Shows matching notes in dropdown 7 - * - On selection: opens editor with note content 10 + * - Shows matching notes in dropdown (via output selection mode) 11 + * - On selection: panel auto-opens editor for 'item' mimeType 8 12 */ 9 13 import api from 'peek://app/api.js'; 10 14 ··· 41 45 }; 42 46 43 47 /** 44 - * Format note for dropdown display 48 + * Get title from note content 45 49 * @param {Object} note - Note item 46 - * @returns {Object} Formatted for dropdown 50 + * @returns {string} Title extracted from first line 47 51 */ 48 - const formatNoteForDropdown = (note) => { 52 + const getNoteTitle = (note) => { 49 53 const content = note.content || ''; 50 - // First line as title, rest as preview 51 54 const lines = content.split('\n'); 52 55 const title = lines[0].replace(/^#+ /, '').trim() || 'Untitled'; 53 - const preview = lines.slice(1).join(' ').trim().substring(0, 100); 54 - 55 - return { 56 - id: note.id, 57 - title: title.substring(0, 50) + (title.length > 50 ? '...' : ''), 58 - description: preview + (preview.length === 100 ? '...' : ''), 59 - content: content 60 - }; 56 + return title.substring(0, 50) + (title.length > 50 ? '...' : ''); 61 57 }; 62 58 63 59 // Commands ··· 65 61 { 66 62 name: 'edit', 67 63 description: 'Edit a note', 68 - // Accept search text, produce selection 64 + // Accept search text, produce item (note object) 69 65 accepts: ['text'], 70 66 produces: ['item'], 71 67 ··· 82 78 }; 83 79 } 84 80 85 - // If exactly one match and query is specific, open directly 81 + // If exactly one match and query is specific, return it directly 86 82 if (notes.length === 1 && query.trim()) { 87 83 const note = notes[0]; 88 - api.publish('editor:open', { itemId: note.id }, api.scopes.GLOBAL); 89 84 return { 90 85 success: true, 91 - message: 'Opening note', 92 - data: note 86 + output: { 87 + data: note, 88 + mimeType: 'item', 89 + title: getNoteTitle(note) 90 + } 93 91 }; 94 92 } 95 93 96 - // Return dropdown items for selection 97 - const dropdownItems = notes.map(formatNoteForDropdown); 98 - 94 + // Return array of notes - panel will enter output selection mode 95 + // User picks one, then panel handles the 'item' mimeType routing 99 96 return { 100 97 success: true, 101 - dropdown: { 102 - items: dropdownItems, 103 - placeholder: 'Select a note to edit', 104 - onSelect: (selected) => { 105 - // This is called when user selects from dropdown 106 - api.publish('editor:open', { itemId: selected.id }, api.scopes.GLOBAL); 107 - return { success: true, message: 'Opening note' }; 108 - } 98 + output: { 99 + data: notes, 100 + mimeType: 'item', 101 + title: `${notes.length} notes found` 109 102 } 110 103 }; 111 104 }
+29 -4
extensions/cmd/commands/note.js
··· 82 82 { 83 83 name: 'note', 84 84 description: 'Save a note or URL (auto-detects type)', 85 + // Produces item when saving content, or signals new-item for empty editor 86 + produces: ['item'], 87 + 85 88 async execute(ctx) { 86 89 if (ctx.search) { 90 + // Save the provided content 87 91 try { 88 92 const { itemId, type } = await saveItem(ctx.search); 89 93 console.log(`Saved ${type} item:`, itemId); 90 - api.publish('editor:changed', { action: 'add', itemId }, api.scopes.GLOBAL); 91 - return { success: true, message: `${type === 'url' ? 'URL' : 'Note'} saved` }; 94 + 95 + // Return the saved item through the connector pattern 96 + // Panel will handle editor:changed notification and routing 97 + return { 98 + success: true, 99 + message: `${type === 'url' ? 'URL' : 'Note'} saved`, 100 + output: { 101 + data: { id: itemId, type, content: ctx.search }, 102 + mimeType: 'item', 103 + title: type === 'url' ? 'Saved URL' : 'Saved note', 104 + // Flag to indicate this is a newly created item (for editor:changed) 105 + isNew: true 106 + } 107 + }; 92 108 } catch (error) { 93 109 console.error('Failed to save:', error); 94 110 return { success: false, message: error.message }; 95 111 } 96 112 } else { 97 - api.publish('editor:add', { type: 'text' }, api.scopes.GLOBAL); 98 - return { success: true, message: 'Opening editor' }; 113 + // No content provided - signal to open empty editor 114 + // Return special output that panel interprets as "open new editor" 115 + return { 116 + success: true, 117 + message: 'Opening editor', 118 + output: { 119 + data: { type: 'text' }, 120 + mimeType: 'new-item', 121 + title: 'New note' 122 + } 123 + }; 99 124 } 100 125 } 101 126 },
+51 -4
extensions/cmd/panel.js
··· 766 766 } 767 767 768 768 /** 769 - * Select the currently highlighted output item and enter chain mode 769 + * Select the currently highlighted output item 770 + * Routes to editor for 'item' mimeType, otherwise enters chain mode 770 771 */ 771 772 function selectOutputItem() { 772 773 if (!state.outputSelectionMode || state.outputItems.length === 0) return; ··· 774 775 const selectedItem = state.outputItems[state.outputItemIndex]; 775 776 log('cmd:panel', 'Selected output item:', state.outputItemIndex, selectedItem); 776 777 777 - // Exit output selection mode 778 + // Capture state before exiting selection mode 778 779 const mimeType = state.outputMimeType; 779 780 const sourceCommand = state.outputSourceCommand; 780 781 exitOutputSelectionMode(); 781 782 782 - // Enter chain mode with the selected item 783 + // Handle 'item' mimeType - route to editor instead of chain mode 784 + if (mimeType === 'item' && selectedItem.id) { 785 + log('cmd:panel', 'Opening editor for selected item:', selectedItem.id); 786 + api.publish('editor:open', { itemId: selectedItem.id }, api.scopes.GLOBAL); 787 + setTimeout(shutdown, 100); 788 + return; 789 + } 790 + 791 + // For other mimeTypes, enter chain mode with the selected item 783 792 enterChainMode({ 784 793 data: selectedItem, 785 794 mimeType: mimeType, ··· 1165 1174 // Check if command produced chainable output 1166 1175 if (result && result.output && result.output.data && result.output.mimeType) { 1167 1176 const outputData = result.output.data; 1177 + const outputMimeType = result.output.mimeType; 1178 + 1179 + // Handle special mimeTypes that have automatic routing (not chaining) 1180 + // 'item' - open existing item in editor 1181 + // 'new-item' - open empty editor for new item 1182 + if (outputMimeType === 'item' || outputMimeType === 'new-item') { 1183 + // If output is an array of items, enter selection mode 1184 + if (Array.isArray(outputData) && outputData.length > 0) { 1185 + enterOutputSelectionMode(outputData, outputMimeType, name); 1186 + 1187 + const commandInput = document.getElementById('command-input'); 1188 + if (commandInput) { 1189 + commandInput.value = ''; 1190 + commandInput.focus(); 1191 + } 1192 + return; 1193 + } 1194 + 1195 + // Single item - route to editor 1196 + if (outputMimeType === 'new-item') { 1197 + // Open empty editor for new item 1198 + log('cmd:panel', 'Opening editor for new item:', outputData); 1199 + api.publish('editor:add', { type: outputData.type || 'text' }, api.scopes.GLOBAL); 1200 + } else if (outputData.id) { 1201 + // Open existing item in editor 1202 + log('cmd:panel', 'Opening editor for item:', outputData.id); 1203 + api.publish('editor:open', { itemId: outputData.id }, api.scopes.GLOBAL); 1204 + 1205 + // If this is a newly created item, also notify of the change 1206 + if (result.output.isNew) { 1207 + api.publish('editor:changed', { action: 'add', itemId: outputData.id }, api.scopes.GLOBAL); 1208 + } 1209 + } 1210 + 1211 + // Close panel after routing to editor 1212 + setTimeout(shutdown, 100); 1213 + return; 1214 + } 1168 1215 1169 1216 // If output is an array, enter output selection mode first 1170 1217 // User picks an item, then we enter chain mode with that item 1171 1218 if (Array.isArray(outputData) && outputData.length > 0) { 1172 1219 // Enter output selection mode to let user pick an item 1173 - enterOutputSelectionMode(outputData, result.output.mimeType, name); 1220 + enterOutputSelectionMode(outputData, outputMimeType, name); 1174 1221 1175 1222 // Clear input for selection 1176 1223 const commandInput = document.getElementById('command-input');