experiments in a post-browser web
10
fork

Configure Feed

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

fix(cmd): param mode Tab fills text, Enter executes with correct item identity

+315 -8
+14
extensions/cmd/commands/edit.js
··· 67 67 params: [{ type: 'item', itemType: 'text' }], 68 68 69 69 async execute(ctx) { 70 + // If a specific item was selected from autocomplete, use it directly 71 + // (avoids re-searching by text which could match the wrong item) 72 + if (ctx.selectedItem) { 73 + const note = ctx.selectedItem; 74 + return { 75 + success: true, 76 + output: { 77 + data: note, 78 + mimeType: 'item', 79 + title: getNoteTitle(note) 80 + } 81 + }; 82 + } 83 + 70 84 const query = ctx.search || ''; 71 85 72 86 // Search for matching notes
+74 -8
extensions/cmd/panel.js
··· 828 828 return; 829 829 } 830 830 831 - // Tab key - in param mode, accept top suggestion 831 + // Tab key - in param mode, fill suggestion text (do NOT execute) 832 832 if (e.key === 'Tab' && state.paramMode && state.paramSuggestions.length > 0) { 833 833 const idx = state.paramIndex >= 0 ? state.paramIndex : 0; 834 - acceptParamSuggestion(idx); 834 + fillParamSuggestion(idx); 835 835 return; 836 836 } 837 837 ··· 1674 1674 /** 1675 1675 * Executes a command 1676 1676 */ 1677 - async function execute(name, typed) { 1677 + async function execute(name, typed, extra = {}) { 1678 1678 log('cmd:panel', 'execute() called with:', name, typed); 1679 1679 if (!state.commands[name]) { 1680 1680 log.error('cmd:panel', 'Command not found:', name); ··· 1684 1684 1685 1685 log('cmd:panel', 'executing cmd', name, typed); 1686 1686 const context = buildExecutionContext(name, typed); 1687 + if (extra.selectedItem) { 1688 + context.selectedItem = extra.selectedItem; 1689 + } 1687 1690 log('cmd:panel', 'execution context', context); 1688 1691 1689 1692 // Delay showing execution state - only show if command takes > 150ms ··· 2193 2196 } 2194 2197 2195 2198 /** 2196 - * Accept a param suggestion at the given index 2199 + * Fill a param suggestion into the input without executing (Tab behavior). 2200 + * For item-type params, fills the title (human-readable) into the input. 2201 + * For other params, fills the suggestion value. 2202 + * Tab should ONLY complete/fill text — never execute. 2203 + * @param {number} index - Index in paramSuggestions to fill 2204 + */ 2205 + function fillParamSuggestion(index) { 2206 + if (index < 0 || index >= state.paramSuggestions.length) return; 2207 + 2208 + const suggestion = state.paramSuggestions[index]; 2209 + const commandInput = document.getElementById('command-input'); 2210 + const commandName = state.paramCommand; 2211 + 2212 + // For item-type params, use title (human-readable); otherwise use value 2213 + const cmd = state.commands[state.paramCommand]; 2214 + const paramDef = cmd && cmd.params && cmd.params[0]; 2215 + const fillText = (paramDef && paramDef.type === 'item' && suggestion.title) 2216 + ? suggestion.title 2217 + : suggestion.value; 2218 + 2219 + // Get current text after command name 2220 + const typed = state.typed || ''; 2221 + const lowerTyped = typed.toLowerCase(); 2222 + const lowerName = commandName.toLowerCase(); 2223 + let beforeParams = commandName + ' '; 2224 + 2225 + // Get text after command name 2226 + let rest = ''; 2227 + if (lowerTyped.startsWith(lowerName + ' ')) { 2228 + rest = typed.slice(commandName.length + 1); 2229 + } 2230 + 2231 + // Split tokens, replace the last partial with suggestion text 2232 + const tokens = rest.split(/\s+/).filter(t => t.length > 0); 2233 + const endsWithSpace = rest.endsWith(' ') || rest === ''; 2234 + 2235 + if (!endsWithSpace && tokens.length > 0) { 2236 + // Replace the partial (last token) with suggestion text 2237 + tokens[tokens.length - 1] = fillText; 2238 + } else { 2239 + // No partial — append the suggestion 2240 + tokens.push(fillText); 2241 + } 2242 + 2243 + // Rebuild input: command + completed tokens + trailing space 2244 + const newValue = beforeParams + tokens.join(' ') + ' '; 2245 + commandInput.value = newValue; 2246 + state.typed = newValue; 2247 + 2248 + // Select this suggestion in the list for visual feedback 2249 + state.paramIndex = index; 2250 + 2251 + // Cursor to end 2252 + setTimeout(() => { 2253 + commandInput.setSelectionRange(newValue.length, newValue.length); 2254 + }, 0); 2255 + 2256 + // Refresh suggestions for the next token 2257 + updateParamSuggestions(); 2258 + } 2259 + 2260 + /** 2261 + * Accept a param suggestion at the given index — executes the command. 2262 + * Called by Enter key and click handlers. 2197 2263 * @param {number} index - Index in paramSuggestions to accept 2198 2264 */ 2199 2265 function acceptParamSuggestion(index) { ··· 2202 2268 const suggestion = state.paramSuggestions[index]; 2203 2269 2204 2270 // Item-type param for connector commands (those with produces): 2205 - // Route through execute() so the command's output handling works properly 2206 - // (enters output selection mode, routes to editor, supports chaining, etc.) 2271 + // Route through execute() with the selected item so the command can use it 2272 + // directly instead of re-searching by text (which could match the wrong item). 2207 2273 const cmd = state.commands[state.paramCommand]; 2208 2274 const paramDef = cmd && cmd.params && cmd.params[0]; 2209 2275 if (paramDef && paramDef.type === 'item' && suggestion._item) { 2210 2276 const commandName = state.paramCommand; 2211 - // Build typed string with the suggestion title as search text 2212 2277 const typed = commandName + ' ' + (suggestion.title || suggestion.value || ''); 2278 + const selectedItem = suggestion._item; 2213 2279 exitParamMode(); 2214 - execute(commandName, typed); 2280 + execute(commandName, typed, { selectedItem }); 2215 2281 return; 2216 2282 } 2217 2283
+227
tests/desktop/smoke.spec.ts
··· 3135 3135 }); 3136 3136 3137 3137 // ============================================================================ 3138 + // Edit Command Param Mode Tests (uses shared app) 3139 + // ============================================================================ 3140 + 3141 + test.describe('Edit Command Param Mode @desktop', () => { 3142 + let bgWindow: Page; 3143 + 3144 + test.beforeAll(async () => { 3145 + bgWindow = sharedBgWindow; 3146 + }); 3147 + 3148 + test('Tab in param mode fills text, does NOT execute', async () => { 3149 + // Create a test note so param mode has suggestions 3150 + const createResult = await bgWindow.evaluate(async () => { 3151 + return await (window as any).app.datastore.addItem({ 3152 + type: 'text', 3153 + content: '# Tab Test Note\nThis is a note for testing Tab in param mode.' 3154 + }); 3155 + }); 3156 + expect(createResult.success).toBe(true); 3157 + const noteId = createResult.data.id; 3158 + 3159 + // Open cmd panel 3160 + const openResult = await bgWindow.evaluate(async () => { 3161 + return await (window as any).app.window.open('peek://ext/cmd/panel.html', { 3162 + modal: true, 3163 + width: 600, 3164 + height: 400, 3165 + frame: false, 3166 + transparent: true, 3167 + alwaysOnTop: true, 3168 + center: true 3169 + }); 3170 + }); 3171 + expect(openResult.success).toBe(true); 3172 + 3173 + const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 3174 + expect(cmdWindow).toBeTruthy(); 3175 + 3176 + await cmdWindow.waitForSelector('input', { timeout: 5000 }); 3177 + await waitForPanelCommandsLoaded(cmdWindow); 3178 + 3179 + // Type 'edit' and Tab to enter param mode 3180 + await cmdWindow.fill('input', 'edit'); 3181 + await cmdWindow.keyboard.press('ArrowDown'); 3182 + await waitForCommandResults(cmdWindow, 1, 10000); 3183 + await cmdWindow.keyboard.press('Tab'); 3184 + 3185 + // Verify param mode is active 3186 + const paramModeActive = await cmdWindow.evaluate(() => { 3187 + return (window as any)._cmdState.paramMode === true; 3188 + }); 3189 + expect(paramModeActive).toBe(true); 3190 + 3191 + // Wait for param suggestions to load (items query) 3192 + await cmdWindow.waitForFunction(() => { 3193 + return (window as any)._cmdState.paramSuggestions.length > 0; 3194 + }, { timeout: 10000 }); 3195 + 3196 + // Press Tab on a suggestion - should fill text, NOT execute 3197 + await cmdWindow.keyboard.press('Tab'); 3198 + 3199 + // Verify param mode is still active (Tab fills, doesn't execute) 3200 + const stillParamMode = await cmdWindow.evaluate(() => { 3201 + return (window as any)._cmdState.paramMode === true; 3202 + }); 3203 + expect(stillParamMode).toBe(true); 3204 + 3205 + // Verify input text was updated with the suggestion value 3206 + const inputValue = await cmdWindow.$eval('input', (el: HTMLInputElement) => el.value); 3207 + expect(inputValue.startsWith('edit ')).toBe(true); 3208 + expect(inputValue.length).toBeGreaterThan('edit '.length); 3209 + 3210 + // Close the cmd window 3211 + if (openResult.id) { 3212 + await bgWindow.evaluate(async (id: number) => { 3213 + return await (window as any).app.window.close(id); 3214 + }, openResult.id); 3215 + } 3216 + 3217 + // Clean up test note 3218 + await bgWindow.evaluate(async (id: string) => { 3219 + return await (window as any).app.datastore.deleteItem(id); 3220 + }, noteId); 3221 + }); 3222 + 3223 + test('Enter in param mode executes with correct itemId', async () => { 3224 + // Create a test note 3225 + const createResult = await bgWindow.evaluate(async () => { 3226 + return await (window as any).app.datastore.addItem({ 3227 + type: 'text', 3228 + content: '# Enter Test Note\nThis is a note for testing Enter in param mode.' 3229 + }); 3230 + }); 3231 + expect(createResult.success).toBe(true); 3232 + const noteId = createResult.data.id; 3233 + 3234 + // Set up a listener for editor:open events BEFORE opening cmd panel 3235 + await bgWindow.evaluate(async () => { 3236 + (window as any).__editorOpenEvents = []; 3237 + (window as any).app.subscribe('editor:open', (data: any) => { 3238 + (window as any).__editorOpenEvents.push(data); 3239 + }, { scope: 'global' }); 3240 + }); 3241 + 3242 + // Open cmd panel 3243 + const openResult = await bgWindow.evaluate(async () => { 3244 + return await (window as any).app.window.open('peek://ext/cmd/panel.html', { 3245 + modal: true, 3246 + width: 600, 3247 + height: 400, 3248 + frame: false, 3249 + transparent: true, 3250 + alwaysOnTop: true, 3251 + center: true 3252 + }); 3253 + }); 3254 + expect(openResult.success).toBe(true); 3255 + 3256 + const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 3257 + expect(cmdWindow).toBeTruthy(); 3258 + 3259 + await cmdWindow.waitForSelector('input', { timeout: 5000 }); 3260 + await waitForPanelCommandsLoaded(cmdWindow); 3261 + 3262 + // Type 'edit' and Tab to enter param mode 3263 + await cmdWindow.fill('input', 'edit'); 3264 + await cmdWindow.keyboard.press('ArrowDown'); 3265 + await waitForCommandResults(cmdWindow, 1, 10000); 3266 + await cmdWindow.keyboard.press('Tab'); 3267 + 3268 + // Wait for param suggestions to load 3269 + await cmdWindow.waitForFunction(() => { 3270 + return (window as any)._cmdState.paramSuggestions.length > 0; 3271 + }, { timeout: 10000 }); 3272 + 3273 + // Find the index of our test note in suggestions 3274 + const testNoteIndex = await cmdWindow.evaluate((targetId: string) => { 3275 + const suggestions = (window as any)._cmdState.paramSuggestions; 3276 + return suggestions.findIndex((s: any) => s._item && s._item.id === targetId); 3277 + }, noteId); 3278 + 3279 + // Navigate to the test note if needed 3280 + if (testNoteIndex > 0) { 3281 + for (let i = 0; i < testNoteIndex; i++) { 3282 + await cmdWindow.keyboard.press('ArrowDown'); 3283 + } 3284 + } 3285 + 3286 + // Press Enter to execute with the selected item 3287 + await cmdWindow.keyboard.press('Enter'); 3288 + 3289 + // Wait for editor:open event to be published 3290 + await bgWindow.waitForFunction(() => { 3291 + return (window as any).__editorOpenEvents && (window as any).__editorOpenEvents.length > 0; 3292 + }, { timeout: 10000 }); 3293 + 3294 + // Verify editor:open was published with the correct itemId 3295 + const editorEvents = await bgWindow.evaluate(() => { 3296 + return (window as any).__editorOpenEvents; 3297 + }); 3298 + expect(editorEvents.length).toBeGreaterThan(0); 3299 + const lastEvent = editorEvents[editorEvents.length - 1]; 3300 + expect(lastEvent.itemId).toBe(noteId); 3301 + 3302 + // Close cmd window if still open 3303 + if (openResult.id) { 3304 + await bgWindow.evaluate(async (id: number) => { 3305 + try { return await (window as any).app.window.close(id); } catch(e) { /* may already be closed */ } 3306 + }, openResult.id); 3307 + } 3308 + 3309 + // Clean up 3310 + await bgWindow.evaluate(async (id: string) => { 3311 + delete (window as any).__editorOpenEvents; 3312 + return await (window as any).app.datastore.deleteItem(id); 3313 + }, noteId); 3314 + }); 3315 + 3316 + test('Tab in command mode completes name, does not execute', async () => { 3317 + // Open cmd panel 3318 + const openResult = await bgWindow.evaluate(async () => { 3319 + return await (window as any).app.window.open('peek://ext/cmd/panel.html', { 3320 + modal: true, 3321 + width: 600, 3322 + height: 400, 3323 + frame: false, 3324 + transparent: true, 3325 + alwaysOnTop: true, 3326 + center: true 3327 + }); 3328 + }); 3329 + expect(openResult.success).toBe(true); 3330 + 3331 + const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 3332 + expect(cmdWindow).toBeTruthy(); 3333 + 3334 + await cmdWindow.waitForSelector('input', { timeout: 5000 }); 3335 + await waitForPanelCommandsLoaded(cmdWindow); 3336 + 3337 + // Type partial command name 'edi' 3338 + await cmdWindow.fill('input', 'edi'); 3339 + await cmdWindow.keyboard.press('ArrowDown'); 3340 + await waitForCommandResults(cmdWindow, 1, 10000); 3341 + 3342 + // Press Tab - should complete command name, not execute 3343 + await cmdWindow.keyboard.press('Tab'); 3344 + 3345 + // Verify input is now 'edit' (completed from 'edi') 3346 + const inputValue = await cmdWindow.$eval('input', (el: HTMLInputElement) => el.value); 3347 + expect(inputValue.toLowerCase().startsWith('edit')).toBe(true); 3348 + 3349 + // Verify the panel is still open and responsive (no command was executed) 3350 + const panelStillOpen = await cmdWindow.evaluate(() => { 3351 + return document.getElementById('command-input') !== null; 3352 + }); 3353 + expect(panelStillOpen).toBe(true); 3354 + 3355 + // Close the cmd window 3356 + if (openResult.id) { 3357 + await bgWindow.evaluate(async (id: number) => { 3358 + return await (window as any).app.window.close(id); 3359 + }, openResult.id); 3360 + } 3361 + }); 3362 + }); 3363 + 3364 + // ============================================================================ 3138 3365 // Theme Tests (uses shared app) 3139 3366 // ============================================================================ 3140 3367