experiments in a post-browser web
10
fork

Configure Feed

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

add history search cmd as default action

+91 -21
+5 -2
app/cmd/commands/index.js
··· 6 6 import modalCommand from './modal.js'; 7 7 import groupsModule from './groups.js'; 8 8 import noteModule from './note.js'; 9 + import historyModule from './history.js'; 9 10 10 11 // Source commands (commented out as they need browser extension APIs) 11 12 // These modules contain command sources that dynamically generate commands ··· 25 26 debugCommand, 26 27 modalCommand, 27 28 ...groupsModule.commands, 28 - ...noteModule.commands 29 + ...noteModule.commands, 30 + ...historyModule.commands 29 31 ]; 30 32 31 33 // Inactive commands - these require browser extension APIs and are not loaded ··· 47 49 48 50 // Source commands - these are modules that generate multiple commands dynamically 49 51 const sources = [ 50 - groupsModule 52 + groupsModule, 53 + historyModule 51 54 ]; 52 55 53 56 /**
+1 -1
app/cmd/index.js
··· 47 47 resizable: false, 48 48 fullscreenable: false, 49 49 50 - openDevTools: true, 50 + openDevTools: debug, 51 51 detachedDevTools: true, 52 52 }; 53 53
+85 -18
app/cmd/panel.js
··· 11 11 const store = openStore(id, defaults, clear /* clear storage */); 12 12 const api = window.app; 13 13 14 + // Storage keys for persistent adaptive matching 15 + const STORAGE_KEY_FEEDBACK = 'adaptiveFeedback'; 16 + const STORAGE_KEY_COUNTS = 'matchCounts'; 17 + 18 + // Load persisted adaptive data 19 + const loadAdaptiveData = () => { 20 + const feedback = store.get(STORAGE_KEY_FEEDBACK) || {}; 21 + const counts = store.get(STORAGE_KEY_COUNTS) || {}; 22 + return { feedback, counts }; 23 + }; 24 + 25 + // Save adaptive data to storage 26 + const saveAdaptiveData = (feedback, counts) => { 27 + store.set(STORAGE_KEY_FEEDBACK, feedback); 28 + store.set(STORAGE_KEY_COUNTS, counts); 29 + }; 30 + 31 + // Initialize with persisted data 32 + const persistedData = loadAdaptiveData(); 33 + 14 34 let state = { 15 35 commands: [], // array of command names 16 36 matches: [], // array of commands matching the typed text 17 37 matchIndex: 0, // index of selected match 18 - matchCounts: {}, // match counts - selectedcommand:numberofselections 19 - matchFeedback: {}, // adaptive matching - partiallytypedandselected:fullname 38 + matchCounts: persistedData.counts, // match counts - selectedcommand:numberofselections 39 + adaptiveFeedback: persistedData.feedback, // adaptive matching - typed -> { command: count, ... } 20 40 typed: '', // text typed by user so far, if any 21 41 lastExecuted: '' // text last typed by user when last they hit return 22 42 }; ··· 115 135 // Preserve any parameters when executing 116 136 const typedText = commandInput.value; 117 137 118 - // Store command name for history and feedback 138 + // Store command name for history and adaptive feedback 119 139 const commandPart = typedText.split(' ')[0]; 120 140 state.lastExecuted = name; 121 141 updateMatchCount(name); 122 - updateMatchFeedback(commandPart, name); 142 + updateAdaptiveFeedback(commandPart, name); 123 143 124 144 // Execute with full typed text 125 145 execute(name, typedText); ··· 248 268 } 249 269 } 250 270 251 - // Sort by match count 271 + // Sort by adaptive score first, then by match count (frecency) 272 + // Adaptive score takes priority - commands you've selected for this input pattern 273 + // will float to the top based on reinforcement learning 252 274 matches.sort(function(a, b) { 275 + // First compare adaptive scores for this typed string 276 + const aAdaptive = getAdaptiveScore(commandPart, a); 277 + const bAdaptive = getAdaptiveScore(commandPart, b); 278 + 279 + // If there's a significant difference in adaptive scores, use that 280 + if (Math.abs(aAdaptive - bAdaptive) > 0.01) { 281 + return bAdaptive - aAdaptive; 282 + } 283 + 284 + // Otherwise fall back to match count (frecency) 253 285 const aCount = state.matchCounts[a] || 0; 254 286 const bCount = state.matchCounts[b] || 0; 255 287 return bCount - aCount; 256 288 }); 257 289 258 - // Insert adaptive feedback at the top if present 259 - if (state.matchFeedback[text]) { 260 - // Check if it's already in the list 261 - const feedbackIndex = matches.indexOf(state.matchFeedback[text]); 262 - if (feedbackIndex !== -1) { 263 - // Move to the beginning 264 - matches.splice(feedbackIndex, 1); 290 + return matches; 291 + } 292 + 293 + /** 294 + * Updates the adaptive feedback for a typed string -> command selection 295 + * Uses asymptotic scoring: score = count / (count + k) 296 + * This creates ever-strengthening reinforcement based on user decisions 297 + */ 298 + function updateAdaptiveFeedback(typed, name) { 299 + // Initialize feedback for this typed string if needed 300 + if (!state.adaptiveFeedback[typed]) { 301 + state.adaptiveFeedback[typed] = {}; 302 + } 303 + 304 + // Increment the count for this typed -> command pair 305 + if (!state.adaptiveFeedback[typed][name]) { 306 + state.adaptiveFeedback[typed][name] = 0; 307 + } 308 + state.adaptiveFeedback[typed][name]++; 309 + 310 + // Also record feedback for all prefixes of the typed string 311 + // This helps with single-character matching 312 + for (let i = 1; i < typed.length; i++) { 313 + const prefix = typed.substring(0, i); 314 + if (!state.adaptiveFeedback[prefix]) { 315 + state.adaptiveFeedback[prefix] = {}; 316 + } 317 + if (!state.adaptiveFeedback[prefix][name]) { 318 + state.adaptiveFeedback[prefix][name] = 0; 265 319 } 266 - matches.unshift(state.matchFeedback[text]); 320 + // Give partial credit to prefixes (half weight) 321 + state.adaptiveFeedback[prefix][name] += 0.5; 267 322 } 268 323 269 - return matches; 324 + // Persist to storage 325 + saveAdaptiveData(state.adaptiveFeedback, state.matchCounts); 270 326 } 271 327 272 328 /** 273 - * Updates the match feedback for adaptive suggestions 329 + * Gets the adaptive score for a command given the typed string 330 + * Uses asymptotic formula: score = count / (count + k) 331 + * Returns 0-1 where higher is better 274 332 */ 275 - function updateMatchFeedback(typed, name) { 276 - state.matchFeedback[typed] = name; 333 + function getAdaptiveScore(typed, name) { 334 + const k = 3; // Tuning constant - higher = slower convergence 335 + const feedback = state.adaptiveFeedback[typed]; 336 + if (!feedback || !feedback[name]) { 337 + return 0; 338 + } 339 + const count = feedback[name]; 340 + return count / (count + k); 277 341 } 278 342 279 343 /** ··· 284 348 state.matchCounts[name] = 0; 285 349 } 286 350 state.matchCounts[name]++; 351 + 352 + // Persist to storage 353 + saveAdaptiveData(state.adaptiveFeedback, state.matchCounts); 287 354 } 288 355 289 356 /** ··· 405 472 execute(match, state.typed); 406 473 state.lastExecuted = match; 407 474 updateMatchCount(match); 408 - updateMatchFeedback(state.typed, match); 475 + updateAdaptiveFeedback(state.typed.split(' ')[0], match); 409 476 document.getElementById('command-input').value = ''; 410 477 state.typed = ''; 411 478 updateCommandUI();