experiments in a post-browser web
10
fork

Configure Feed

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

fix(cmd): restore minimal cmd:register-batch publish + merge-preserve pattern in resident

+76 -58
+33 -32
app/cmd/background.js
··· 212 212 */ 213 213 const initCommandRegistry = () => { 214 214 // Handle batch command registrations (from preload batching) 215 + // 216 + // Merge-preserve pattern: a later publish with partial metadata (e.g. the 217 + // minimal {name, source} publish from tile-ipc.ts's tile:command:register) 218 + // must NOT erase rich fields that an earlier publish carried (accepts, 219 + // produces, description, params). Previously we overwrote unconditionally, 220 + // which caused metadata from the preload's cmd:register publish to be 221 + // erased when a tile also sent the minimal tile:command:register path. 215 222 api.pubsub.subscribe('cmd:register-batch', (msg) => { 216 223 if (!msg.commands || !Array.isArray(msg.commands)) return; 217 224 218 225 log('ext:cmd', 'cmd:register-batch received:', msg.commands.length, 'commands'); 219 226 220 227 for (const cmd of msg.commands) { 228 + const existing = commandRegistry.get(cmd.name) || {}; 221 229 const entry = { 222 230 name: cmd.name, 223 - description: cmd.description || '', 224 - source: cmd.source, 225 - // Scope: 'global' (app-wide), 'window' (target window), 'page' (page content) 226 - scope: cmd.scope || 'global', 227 - // Required major modes for command availability (empty = available in all modes) 228 - modes: cmd.modes || [], 229 - // Whether command has a canExecute guard 230 - hasCanExecute: cmd.hasCanExecute || false, 231 - // Connector metadata for chaining 232 - accepts: cmd.accepts || [], 233 - produces: cmd.produces || [], 234 - // Parameter definitions for completions 235 - params: cmd.params || [] 231 + description: cmd.description || existing.description || '', 232 + source: cmd.source || existing.source, 233 + scope: cmd.scope || existing.scope || 'global', 234 + modes: cmd.modes || existing.modes || [], 235 + hasCanExecute: cmd.hasCanExecute || existing.hasCanExecute || false, 236 + // Connector metadata for chaining — preserve richer existing entry 237 + // when the incoming publish lacks it (minimal publishes carry empty 238 + // arrays; we want the previously-registered full metadata to win). 239 + accepts: (cmd.accepts && cmd.accepts.length > 0) ? cmd.accepts : (existing.accepts || []), 240 + produces: (cmd.produces && cmd.produces.length > 0) ? cmd.produces : (existing.produces || []), 241 + params: (cmd.params && cmd.params.length > 0) ? cmd.params : (existing.params || []), 236 242 }; 237 - // Preserve noun routing metadata for proxy dispatch 238 - if (cmd._nounName) entry._nounName = cmd._nounName; 239 - if (cmd._nounCapability) entry._nounCapability = cmd._nounCapability; 243 + if (cmd._nounName || existing._nounName) entry._nounName = cmd._nounName || existing._nounName; 244 + if (cmd._nounCapability || existing._nounCapability) entry._nounCapability = cmd._nounCapability || existing._nounCapability; 240 245 commandRegistry.set(cmd.name, entry); 241 246 liveRegisteredCommands.add(cmd.name); 242 247 } 243 248 }, api.scopes.GLOBAL); 244 249 245 250 // Handle individual command registrations from extensions 251 + // Merge-preserve pattern: see cmd:register-batch above for rationale. 246 252 api.pubsub.subscribe('cmd:register', (msg) => { 247 253 log('ext:cmd', 'cmd:register received:', msg.name); 254 + const existing = commandRegistry.get(msg.name) || {}; 248 255 const entry = { 249 256 name: msg.name, 250 - description: msg.description || '', 251 - source: msg.source, 252 - // Scope: 'global' (app-wide), 'window' (target window), 'page' (page content) 253 - scope: msg.scope || 'global', 254 - // Required major modes for command availability 255 - modes: msg.modes || [], 256 - // Whether command has a canExecute guard 257 - hasCanExecute: msg.hasCanExecute || false, 258 - // Connector metadata for chaining 259 - accepts: msg.accepts || [], // MIME types this command accepts as input 260 - produces: msg.produces || [], // MIME types this command produces as output 261 - // Parameter definitions for completions 262 - params: msg.params || [] 257 + description: msg.description || existing.description || '', 258 + source: msg.source || existing.source, 259 + scope: msg.scope || existing.scope || 'global', 260 + modes: msg.modes || existing.modes || [], 261 + hasCanExecute: msg.hasCanExecute || existing.hasCanExecute || false, 262 + accepts: (msg.accepts && msg.accepts.length > 0) ? msg.accepts : (existing.accepts || []), 263 + produces: (msg.produces && msg.produces.length > 0) ? msg.produces : (existing.produces || []), 264 + params: (msg.params && msg.params.length > 0) ? msg.params : (existing.params || []), 263 265 }; 264 - // Preserve noun routing metadata for proxy dispatch 265 - if (msg._nounName) entry._nounName = msg._nounName; 266 - if (msg._nounCapability) entry._nounCapability = msg._nounCapability; 266 + if (msg._nounName || existing._nounName) entry._nounName = msg._nounName || existing._nounName; 267 + if (msg._nounCapability || existing._nounCapability) entry._nounCapability = msg._nounCapability || existing._nounCapability; 267 268 commandRegistry.set(msg.name, entry); 268 269 liveRegisteredCommands.add(msg.name); 269 270 }, api.scopes.GLOBAL);
+23 -6
backend/electron/tile-ipc.ts
··· 546 546 handleViolation(grant, 'commands', 'command:register', 'commands not granted', args.token); 547 547 return; 548 548 } 549 - // Capability gate + token-tile linkage only. Previously this also 550 - // published a minimal cmd:register-batch (name+source) — but that 551 - // overwrote the full-metadata entry from the preload's own cmd:register 552 - // publish, erasing accepts/produces/description. The preload is now the 553 - // single source of truth for cmd registry metadata (see tile-preload.cts 554 - // api.commands.register object-form publish). 549 + 550 + // Publish a minimal cmd:register-batch so downstream subscribers know 551 + // the tile claims ownership of this command name (used by the cmd 552 + // panel's query-backed registry for lazy/external tiles that don't 553 + // go through registerLazyTile at startup — e.g. `example`). 554 + // 555 + // This publish does NOT carry metadata (accepts/produces/description) 556 + // — those come from either (a) registerLazyTile at startup for built-in 557 + // tiles, or (b) the preload's own full cmd:register publish that 558 + // fires after this send. cmd:register-batch and cmd:register are 559 + // separate topics in the resident, and for a given command name the 560 + // full publish arrives second and overwrites the minimal. 561 + publish( 562 + `peek://${args.tileId}/background`, 563 + scopes.GLOBAL, 564 + 'cmd:register-batch', 565 + { 566 + commands: [{ 567 + name: args.name, 568 + source: `peek://${args.tileId}/background`, 569 + }], 570 + } 571 + ); 555 572 }); 556 573 557 574 ipcMain.on('tile:command:result', (_event, args: {
+20 -20
backend/electron/tile-preload.cts
··· 308 308 name, 309 309 }); 310 310 311 - // When called with the object form, also publish cmd:register so the 312 - // cmd panel picks up this command even though the manifest may only 313 - // have declared metadata (matches the v1 preload flow). 314 - if (typeof nameOrCommand === 'object') { 315 - ipcRenderer.send('tile:pubsub:publish', { 316 - token: tileToken, 311 + // Publish cmd:register so the resident (and live panel) picks up this 312 + // command. Emit for BOTH string-form (name+handler, no metadata) and 313 + // object-form (name+metadata) registrations — string-form entries still 314 + // need to appear in the registry so features-manager-style callers are 315 + // visible to the cmd panel, even if they carry no accepts/produces. 316 + ipcRenderer.send('tile:pubsub:publish', { 317 + token: tileToken, 318 + source: sourceAddress, 319 + scope: 3, // GLOBAL 320 + topic: 'cmd:register', 321 + data: { 322 + name, 323 + description, 317 324 source: sourceAddress, 318 - scope: 3, // GLOBAL 319 - topic: 'cmd:register', 320 - data: { 321 - name, 322 - description, 323 - source: sourceAddress, 324 - scope: scope || 'global', 325 - modes: modes || [], 326 - accepts: accepts || [], 327 - produces: produces || [], 328 - params: params || [], 329 - }, 330 - }); 331 - } 325 + scope: scope || 'global', 326 + modes: modes || [], 327 + accepts: accepts || [], 328 + produces: produces || [], 329 + params: params || [], 330 + }, 331 + }); 332 332 333 333 // Listen for command execution via pubsub relay (`pubsub:cmd:execute:{name}`). 334 334 // Subscribe so main process forwards the topic to us.