experiments in a post-browser web
10
fork

Configure Feed

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

feat(cmd): add item completer for search-as-you-type note suggestions in edit command

+78 -5
+1
extensions/cmd/commands/edit.js
··· 64 64 // Accept search text, produce item (note object) 65 65 accepts: ['text'], 66 66 produces: ['item'], 67 + params: [{ type: 'item', itemType: 'text' }], 67 68 68 69 async execute(ctx) { 69 70 const query = ctx.search || '';
+39
extensions/cmd/completers.js
··· 98 98 } 99 99 100 100 /** 101 + * Item completer - searches notes/items with search-as-you-type 102 + * @param {string} partial - Current search text 103 + * @param {Set<string>} exclude - Not used for items 104 + * @param {Object} paramDef - The param definition {type, itemType} 105 + * @returns {Promise<Array<{title, subtitle, value, _item}>>} 106 + */ 107 + export async function completeItem(partial, exclude, paramDef) { 108 + const queryOpts = { 109 + type: paramDef.itemType || 'text', 110 + limit: 20, 111 + sortBy: 'updated' 112 + }; 113 + 114 + const searchText = (partial || '').trim(); 115 + if (searchText) { 116 + queryOpts.search = searchText; 117 + } 118 + 119 + const result = await api.datastore.queryItems(queryOpts); 120 + if (!result.success || !result.data) return []; 121 + 122 + return result.data.map(item => { 123 + const content = item.content || ''; 124 + const firstLine = content.split('\n')[0].replace(/^#+ /, '').trim() || 'Untitled'; 125 + const title = firstLine.length > 60 ? firstLine.substring(0, 60) + '...' : firstLine; 126 + const date = item.updatedAt ? new Date(item.updatedAt).toLocaleDateString() : ''; 127 + 128 + return { 129 + title, 130 + subtitle: date, 131 + value: item.id, 132 + _item: item 133 + }; 134 + }); 135 + } 136 + 137 + /** 101 138 * Get suggestions for a param definition 102 139 * @param {Object} paramDef - {type, multiple, prefix, values} 103 140 * @param {string} partial - Current partial token ··· 110 147 return completeTag(partial, exclude, paramDef); 111 148 case 'enum': 112 149 return completeEnum(partial, exclude, paramDef); 150 + case 'item': 151 + return completeItem(partial, exclude, paramDef); 113 152 default: 114 153 return []; 115 154 }
+38 -5
extensions/cmd/panel.js
··· 659 659 return; 660 660 } 661 661 662 + // Enter key in param mode with item-type param — accept selected suggestion 663 + if (e.key === 'Enter' && !hasModifier(e) && state.paramMode && state.paramSuggestions.length > 0) { 664 + const cmd = state.commands[state.paramCommand]; 665 + const paramDef = cmd && cmd.params && cmd.params[0]; 666 + if (paramDef && paramDef.type === 'item') { 667 + e.preventDefault(); 668 + const idx = state.paramIndex >= 0 ? state.paramIndex : 0; 669 + acceptParamSuggestion(idx); 670 + return; 671 + } 672 + } 673 + 662 674 // Enter key - execute command (but not if in output selection mode - handled above) 663 675 if (e.key === 'Enter' && !hasModifier(e) && !state.outputSelectionMode) { 664 676 // Check if the typed text is a URL - if so, open it directly ··· 2139 2151 const generation = ++state.paramGeneration; 2140 2152 const { partial, completedTokens } = getParamTokens(); 2141 2153 2154 + // For item-type params, pass the full rest-of-input as the search query 2155 + // (user is typing a search phrase, not discrete tokens) 2156 + let searchPartial = partial; 2157 + if (paramDef.type === 'item') { 2158 + searchPartial = [...completedTokens, partial].filter(Boolean).join(' '); 2159 + } 2160 + 2142 2161 // Build exclusion set from completed tokens 2143 2162 const prefix = paramDef.prefix || ''; 2144 2163 const exclude = new Set( ··· 2150 2169 }) 2151 2170 ); 2152 2171 2153 - const suggestions = await getSuggestions(paramDef, partial, exclude); 2172 + const suggestions = await getSuggestions(paramDef, searchPartial, exclude); 2154 2173 2155 2174 // Discard stale results 2156 2175 if (generation !== state.paramGeneration) return; ··· 2170 2189 if (index < 0 || index >= state.paramSuggestions.length) return; 2171 2190 2172 2191 const suggestion = state.paramSuggestions[index]; 2192 + 2193 + // Item-type param: open editor directly instead of inserting text 2194 + const cmd = state.commands[state.paramCommand]; 2195 + const paramDef = cmd && cmd.params && cmd.params[0]; 2196 + if (paramDef && paramDef.type === 'item' && suggestion._item) { 2197 + api.publish('editor:open', { itemId: suggestion._item.id }, api.scopes.GLOBAL); 2198 + setTimeout(shutdown, 100); 2199 + return; 2200 + } 2201 + 2173 2202 const commandInput = document.getElementById('command-input'); 2174 2203 const commandName = state.paramCommand; 2175 2204 ··· 2331 2360 const suggestion = state.paramSuggestions[idx]; 2332 2361 if (suggestion) { 2333 2362 const { partial } = getParamTokens(); 2334 - // Only show ghost if suggestion value starts with the partial 2335 - if (partial && suggestion.value.toLowerCase().startsWith(partial.toLowerCase())) { 2336 - const ghostText = suggestion.value.substring(partial.length); 2363 + // For item-type params, show title (human-readable) instead of value (ID) 2364 + const cmd = state.commands[state.paramCommand]; 2365 + const paramDef = cmd && cmd.params && cmd.params[0]; 2366 + const ghostSource = (paramDef && paramDef.type === 'item') ? suggestion.title : suggestion.value; 2367 + // Only show ghost if suggestion starts with the partial 2368 + if (partial && ghostSource.toLowerCase().startsWith(partial.toLowerCase())) { 2369 + const ghostText = ghostSource.substring(partial.length); 2337 2370 if (ghostText) { 2338 2371 const ghostSpan = document.createElement('span'); 2339 2372 ghostSpan.textContent = ghostText; ··· 2342 2375 } else if (!partial) { 2343 2376 // No partial typed — show full suggestion as ghost 2344 2377 const ghostSpan = document.createElement('span'); 2345 - ghostSpan.textContent = suggestion.value; 2378 + ghostSpan.textContent = ghostSource; 2346 2379 commandText.appendChild(ghostSpan); 2347 2380 } 2348 2381 }