experiments in a post-browser web
10
fork

Configure Feed

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

fix(cmd): preserve noun routing metadata in command registry pipeline

_nounName and _nounCapability fields were being stripped at multiple points:
- cmd:register-batch handler overwrote noun commands without routing fields
- cmd:query-commands returned commands missing noun metadata
- Command cache save/restore dropped noun fields

Without these fields, noun proxy commands dispatched to cmd:execute:{name}
(which nobody handles) instead of noun:{capability}:{name}, causing the
command panel to hang at spinner for 30s on open groups/tags/etc.

Also fixes nouns.js to always publish valid result objects for void
capabilities like browse, and adds proper subscription cleanup.

+51 -19
+35 -17
extensions/cmd/background.js
··· 91 91 */ 92 92 const saveCommandCache = async (appVersion, extensionVersions) => { 93 93 try { 94 - const commands = Array.from(commandRegistry.values()).map(cmd => ({ 95 - name: cmd.name, 96 - description: cmd.description, 97 - source: cmd.source, 98 - scope: cmd.scope || 'global', 99 - modes: cmd.modes || [], 100 - hasCanExecute: cmd.hasCanExecute || false, 101 - accepts: cmd.accepts, 102 - produces: cmd.produces, 103 - params: cmd.params || [] 104 - })); 94 + const commands = Array.from(commandRegistry.values()).map(cmd => { 95 + const entry = { 96 + name: cmd.name, 97 + description: cmd.description, 98 + source: cmd.source, 99 + scope: cmd.scope || 'global', 100 + modes: cmd.modes || [], 101 + hasCanExecute: cmd.hasCanExecute || false, 102 + accepts: cmd.accepts, 103 + produces: cmd.produces, 104 + params: cmd.params || [] 105 + }; 106 + // Preserve noun routing metadata in cache 107 + if (cmd._nounName) entry._nounName = cmd._nounName; 108 + if (cmd._nounCapability) entry._nounCapability = cmd._nounCapability; 109 + return entry; 110 + }); 105 111 106 112 const cache = { 107 113 appVersion, ··· 197 203 log('ext:cmd', 'cmd:register-batch received:', msg.commands.length, 'commands'); 198 204 199 205 for (const cmd of msg.commands) { 200 - commandRegistry.set(cmd.name, { 206 + const entry = { 201 207 name: cmd.name, 202 208 description: cmd.description || '', 203 209 source: cmd.source, ··· 212 218 produces: cmd.produces || [], 213 219 // Parameter definitions for completions 214 220 params: cmd.params || [] 215 - }); 221 + }; 222 + // Preserve noun routing metadata for proxy dispatch 223 + if (cmd._nounName) entry._nounName = cmd._nounName; 224 + if (cmd._nounCapability) entry._nounCapability = cmd._nounCapability; 225 + commandRegistry.set(cmd.name, entry); 216 226 liveRegisteredCommands.add(cmd.name); 217 227 } 218 228 }, api.scopes.GLOBAL); ··· 220 230 // Handle individual command registrations from extensions 221 231 api.subscribe('cmd:register', (msg) => { 222 232 log('ext:cmd', 'cmd:register received:', msg.name); 223 - commandRegistry.set(msg.name, { 233 + const entry = { 224 234 name: msg.name, 225 235 description: msg.description || '', 226 236 source: msg.source, ··· 235 245 produces: msg.produces || [], // MIME types this command produces as output 236 246 // Parameter definitions for completions 237 247 params: msg.params || [] 238 - }); 248 + }; 249 + // Preserve noun routing metadata for proxy dispatch 250 + if (msg._nounName) entry._nounName = msg._nounName; 251 + if (msg._nounCapability) entry._nounCapability = msg._nounCapability; 252 + commandRegistry.set(msg.name, entry); 239 253 liveRegisteredCommands.add(msg.name); 240 254 }, api.scopes.GLOBAL); 241 255 ··· 448 462 if (cache && cache.commands) { 449 463 // Pre-populate from cache (will be updated by fresh registrations) 450 464 for (const cmd of cache.commands) { 451 - commandRegistry.set(cmd.name, { 465 + const entry = { 452 466 name: cmd.name, 453 467 description: cmd.description || '', 454 468 source: cmd.source, 455 469 accepts: cmd.accepts || [], 456 470 produces: cmd.produces || [], 457 471 params: cmd.params || [] 458 - }); 472 + }; 473 + // Restore noun routing metadata from cache 474 + if (cmd._nounName) entry._nounName = cmd._nounName; 475 + if (cmd._nounCapability) entry._nounCapability = cmd._nounCapability; 476 + commandRegistry.set(cmd.name, entry); 459 477 } 460 478 461 479 // Restore cached nouns and regenerate their commands
+14
extensions/cmd/commands.js
··· 109 109 base.execute = async (ctx) => { 110 110 return new Promise((resolve) => { 111 111 const resultTopic = `noun:result:${cmdData._nounName}:${Date.now()}`; 112 + let settled = false; 112 113 113 114 const unsubscribe = api.subscribe(resultTopic, (result) => { 115 + if (settled) return; 116 + settled = true; 117 + unsubscribe?.(); 114 118 resolve(result); 115 119 }, api.scopes.GLOBAL); 116 120 ··· 121 125 }, api.scopes.GLOBAL); 122 126 123 127 setTimeout(() => { 128 + if (settled) return; 129 + settled = true; 130 + unsubscribe?.(); 124 131 resolve(undefined); 125 132 }, 30000); 126 133 }); ··· 132 139 base.execute = async (ctx) => { 133 140 return new Promise((resolve) => { 134 141 const resultTopic = `cmd:execute:${cmdData.name}:result`; 142 + let settled = false; 135 143 136 144 const unsubscribe = api.subscribe(resultTopic, (result) => { 145 + if (settled) return; 146 + settled = true; 147 + unsubscribe?.(); 137 148 resolve(result); 138 149 }, api.scopes.GLOBAL); 139 150 ··· 144 155 }, api.scopes.GLOBAL); 145 156 146 157 setTimeout(() => { 158 + if (settled) return; 159 + settled = true; 160 + unsubscribe?.(); 147 161 resolve(undefined); 148 162 }, 30000); 149 163 });
+1 -1
extensions/cmd/nouns.js
··· 63 63 try { 64 64 const result = await handler(msg); 65 65 if (msg.expectResult && msg.resultTopic) { 66 - api.publish(msg.resultTopic, result, api.scopes.GLOBAL); 66 + api.publish(msg.resultTopic, result || { success: true }, api.scopes.GLOBAL); 67 67 } 68 68 } catch (err) { 69 69 console.error('[nouns] Error in', cap, nounDef.name, err);
+1 -1
extensions/tags/home.html
··· 136 136 <!-- Inline detail view (replaces card grid when an item is selected) --> 137 137 <div class="detail-view" style="display: none;"> 138 138 <div class="detail-header"> 139 - <img class="detail-favicon" src="" alt=""> 139 + <img class="detail-favicon" alt=""> 140 140 <div class="detail-header-info"> 141 141 <div class="detail-title"></div> 142 142 <div class="detail-url"></div>