@fungi.computer/whistle 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,694 @@
1
+ /** An owning handler's explicit failure code and optional structured details. */
2
+ export class WhistleActionError extends Error {
3
+ code;
4
+ details;
5
+ constructor(
6
+ /** Stable failure identity supplied by the handler owner. */
7
+ code, message,
8
+ /** Optional JSON context safe to return to the caller. */
9
+ details) {
10
+ super(message);
11
+ this.code = code;
12
+ this.details = details;
13
+ this.name = "WhistleActionError";
14
+ }
15
+ }
16
+ /** A valid contribution was attempted after the runtime closed. */
17
+ export class WhistleClosedError extends Error {
18
+ /** Construct the provisional error for a valid post-close contribution. */
19
+ constructor() {
20
+ super("Whistle runtime is closed");
21
+ this.name = "WhistleClosedError";
22
+ }
23
+ }
24
+ export { createWhistleKeymapAdapter, } from "./keymap.js";
25
+ /**
26
+ * The projection for one focused scope: global rows plus the focused
27
+ * scope's rows. A focused slash alias or binding shadows the same global one.
28
+ * Agent rows follow the same scope filter; the complete runtime remains whole.
29
+ *
30
+ * @param snapshot - A complete runtime snapshot.
31
+ * @param scope - The focused scope, or `null` when nothing scoped is focused.
32
+ * @returns A frozen snapshot of globals plus the selected scope.
33
+ */
34
+ export function scopeSnapshot(snapshot, scope) {
35
+ const active = (rows) => rows.filter((row) => row.scope === undefined || row.scope === scope);
36
+ const shadowed = (rows, key) => {
37
+ const focused = new Set(rows.filter((row) => row.scope === scope).map(key));
38
+ return Object.freeze(active(rows).filter((row) => row.scope !== undefined || !focused.has(key(row))));
39
+ };
40
+ return Object.freeze({
41
+ bindings: shadowed(snapshot.bindings, (row) => row.binding),
42
+ palette: Object.freeze(active(snapshot.palette)),
43
+ slash: shadowed(snapshot.slash, (row) => row.alias),
44
+ help: Object.freeze(active(snapshot.help)),
45
+ menu: Object.freeze(active(snapshot.menu)),
46
+ agent: Object.freeze(active(snapshot.agent)),
47
+ });
48
+ }
49
+ /** Construct the provisional semantic runtime shell. */
50
+ export function createWhistle() {
51
+ let closed = false;
52
+ let currentSnapshot = emptySnapshot;
53
+ let liveContributions = [];
54
+ let previousAvailability = [];
55
+ let graphChanged = false;
56
+ let presenters = [];
57
+ const listeners = new Set();
58
+ const publish = () => {
59
+ for (const listener of listeners)
60
+ listener();
61
+ };
62
+ const snapshot = () => {
63
+ if (closed)
64
+ return emptySnapshot;
65
+ const availability = liveContributions.flatMap((live) => live.contribution.commands.map(readAvailability));
66
+ if (graphChanged ||
67
+ availability.some((value, index) => !sameAvailability(value, previousAvailability[index]))) {
68
+ currentSnapshot = snapshotFor(liveContributions, availability);
69
+ previousAvailability = availability;
70
+ graphChanged = false;
71
+ }
72
+ return currentSnapshot;
73
+ };
74
+ return Object.freeze({
75
+ close: () => {
76
+ if (closed)
77
+ return undefined;
78
+ closed = true;
79
+ liveContributions = [];
80
+ presenters = [];
81
+ previousAvailability = [];
82
+ currentSnapshot = emptySnapshot;
83
+ publish();
84
+ listeners.clear();
85
+ return undefined;
86
+ },
87
+ contribute: (contribution) => {
88
+ validateContribution(contribution);
89
+ if (closed)
90
+ throw new WhistleClosedError();
91
+ validateConflicts(contribution, liveContributions);
92
+ validateCapacity(contribution, liveContributions);
93
+ const generation = Symbol("whistle-contribution");
94
+ if (!isEmptyContribution(contribution)) {
95
+ liveContributions = [
96
+ ...liveContributions,
97
+ { contribution: copyContribution(contribution), generation },
98
+ ];
99
+ graphChanged = true;
100
+ publish();
101
+ }
102
+ let active = true;
103
+ return makeLease((commands) => {
104
+ if (!active || closed)
105
+ return;
106
+ const replacement = {
107
+ ...contribution,
108
+ commands,
109
+ };
110
+ validateContribution(replacement);
111
+ const ownIndex = liveContributions.findIndex((candidate) => candidate.generation === generation);
112
+ const others = liveContributions.filter((candidate) => candidate.generation !== generation);
113
+ validateConflicts(replacement, others);
114
+ validateCapacity(replacement, others);
115
+ const copied = copyContribution(replacement);
116
+ if (isEmptyContribution(replacement)) {
117
+ if (ownIndex < 0)
118
+ return;
119
+ liveContributions = others;
120
+ }
121
+ else if (ownIndex < 0) {
122
+ liveContributions = [
123
+ ...liveContributions,
124
+ { contribution: copied, generation },
125
+ ];
126
+ }
127
+ else {
128
+ liveContributions = liveContributions.map((candidate) => candidate.generation === generation
129
+ ? { contribution: copied, generation }
130
+ : candidate);
131
+ }
132
+ graphChanged = true;
133
+ publish();
134
+ }, () => {
135
+ if (!active)
136
+ return;
137
+ active = false;
138
+ const live = liveContributions.find((candidate) => candidate.generation === generation);
139
+ if (live === undefined)
140
+ return;
141
+ liveContributions = liveContributions.filter((candidate) => candidate.generation !== generation);
142
+ graphChanged = true;
143
+ publish();
144
+ });
145
+ },
146
+ execute: async function execute(commandId, input) {
147
+ if (commandId.trim().length === 0)
148
+ return { status: "invalid" };
149
+ if (closed)
150
+ return { status: "closed" };
151
+ const command = findCommand(commandId, liveContributions);
152
+ if (command === undefined)
153
+ return { status: "missing" };
154
+ let session;
155
+ try {
156
+ const availability = readAvailability(command);
157
+ if (availability.status === "unavailable")
158
+ return availability;
159
+ const presenter = presenters.at(-1)?.presenter;
160
+ if (input.interaction === undefined &&
161
+ presenter !== undefined &&
162
+ humanOrigins.has(input.origin))
163
+ session = presenter({
164
+ commandId,
165
+ title: command.title,
166
+ retry: () => {
167
+ void execute(commandId, input);
168
+ },
169
+ });
170
+ const executionInput = {
171
+ ...input,
172
+ signal: input.signal ?? new AbortController().signal,
173
+ };
174
+ if (session !== undefined) {
175
+ executionInput.interaction = session.interaction;
176
+ executionInput.signal = AbortSignal.any([
177
+ executionInput.signal,
178
+ session.interaction.signal,
179
+ ]);
180
+ }
181
+ const result = await command.handler(executionInput);
182
+ session?.settle({ status: "handled" });
183
+ return result === undefined
184
+ ? { status: "handled" }
185
+ : { status: "handled", result };
186
+ }
187
+ catch (error) {
188
+ const failure = failureFor(error instanceof Error
189
+ ? error
190
+ : new Error("Command handler threw a non-Error value"));
191
+ session?.settle(session.interaction.signal.aborted
192
+ ? { status: "cancelled" }
193
+ : { status: "failed", message: failure.message });
194
+ return { status: "failed", error: failure };
195
+ }
196
+ },
197
+ present: (presenter) => {
198
+ const entry = { presenter };
199
+ if (!closed)
200
+ presenters = [...presenters, entry];
201
+ return () => {
202
+ presenters = presenters.filter((candidate) => candidate !== entry);
203
+ };
204
+ },
205
+ snapshot,
206
+ subscribe: (listener) => {
207
+ if (closed)
208
+ return () => undefined;
209
+ listeners.add(listener);
210
+ return () => {
211
+ listeners.delete(listener);
212
+ };
213
+ },
214
+ });
215
+ }
216
+ /** Origins where a person is present to answer an interaction. */
217
+ const humanOrigins = new Set([
218
+ "browser",
219
+ "keyboard",
220
+ "menu",
221
+ "palette",
222
+ "slash",
223
+ "terminal",
224
+ ]);
225
+ const maximumBindingsOrAliases = 16;
226
+ const maximumSlashWords = 32;
227
+ const maximumLiveContributions = 64;
228
+ const maximumLiveCommands = 256;
229
+ function findCommand(commandId, liveContributions) {
230
+ for (const live of liveContributions) {
231
+ for (const command of live.contribution.commands) {
232
+ if (`${live.contribution.namespace}:${command.id}` === commandId)
233
+ return command;
234
+ }
235
+ }
236
+ return undefined;
237
+ }
238
+ function isEmptyContribution(contribution) {
239
+ return contribution.commands.length === 0;
240
+ }
241
+ function validateContribution(contribution) {
242
+ validateMeaningful(contribution.sourceId, "source ID");
243
+ validateMeaningful(contribution.namespace, "namespace", true);
244
+ if (contribution.scope !== undefined)
245
+ validateMeaningful(contribution.scope, "scope");
246
+ for (const command of contribution.commands)
247
+ validateCommand(command);
248
+ }
249
+ function validateConflicts(contribution, liveContributions) {
250
+ const namespaceOwner = liveContributions.find((live) => live.contribution.namespace === contribution.namespace &&
251
+ live.contribution.sourceId !== contribution.sourceId);
252
+ if (namespaceOwner !== undefined)
253
+ reject("Namespace is already owned by another source");
254
+ const commandIds = new Set();
255
+ const slashAliases = new Set();
256
+ for (const live of liveContributions) {
257
+ const sameScope = live.contribution.scope === contribution.scope;
258
+ for (const command of live.contribution.commands) {
259
+ commandIds.add(`${live.contribution.namespace}:${command.id}`);
260
+ if (sameScope)
261
+ for (const alias of command.projections?.slash?.aliases ?? [])
262
+ slashAliases.add(alias);
263
+ }
264
+ }
265
+ for (const command of contribution.commands) {
266
+ const commandId = `${contribution.namespace}:${command.id}`;
267
+ if (commandIds.has(commandId))
268
+ reject("Canonical command ID is already registered");
269
+ commandIds.add(commandId);
270
+ for (const alias of command.projections?.slash?.aliases ?? []) {
271
+ if (slashAliases.has(alias))
272
+ reject("Slash alias is already registered");
273
+ slashAliases.add(alias);
274
+ }
275
+ }
276
+ }
277
+ function validateCapacity(contribution, liveContributions) {
278
+ if (liveContributions.length >= maximumLiveContributions)
279
+ reject("Live contribution capacity exceeded");
280
+ const liveCommandCount = liveContributions.reduce((count, live) => count + live.contribution.commands.length, 0);
281
+ if (liveCommandCount + contribution.commands.length > maximumLiveCommands)
282
+ reject("Live command capacity exceeded");
283
+ }
284
+ function copyContribution(contribution) {
285
+ const copy = {
286
+ sourceId: contribution.sourceId,
287
+ namespace: contribution.namespace,
288
+ commands: contribution.commands.map(copyCommand),
289
+ };
290
+ return contribution.scope === undefined
291
+ ? copy
292
+ : { ...copy, scope: contribution.scope };
293
+ }
294
+ function copyCommand(command) {
295
+ const copy = {
296
+ id: command.id,
297
+ title: command.title,
298
+ category: command.category,
299
+ handler: command.handler,
300
+ };
301
+ if (command.description !== undefined)
302
+ copy.description = command.description;
303
+ if (command.action !== undefined)
304
+ copy.action = copyAction(command.action);
305
+ if (command.availability !== undefined)
306
+ copy.availability = command.availability;
307
+ if (command.projections !== undefined)
308
+ copy.projections = copyProjections(command.projections);
309
+ return copy;
310
+ }
311
+ function copyAction(action) {
312
+ const copy = {};
313
+ if (action.inputSchema !== undefined)
314
+ copy.inputSchema = copyJsonObject(action.inputSchema);
315
+ if (action.outputSchema !== undefined)
316
+ copy.outputSchema = copyJsonObject(action.outputSchema);
317
+ if (action.presentation !== undefined)
318
+ copy.presentation = copyPresentation(action.presentation);
319
+ return Object.freeze(copy);
320
+ }
321
+ function copyJsonObject(value) {
322
+ return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, nested]) => [
323
+ key,
324
+ copyJsonValue(nested),
325
+ ])));
326
+ }
327
+ function copyJsonValue(value) {
328
+ if (Array.isArray(value))
329
+ return Object.freeze(value.map(copyJsonValue));
330
+ if (value !== null && value instanceof Object) {
331
+ return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, nested]) => [
332
+ key,
333
+ copyJsonValue(nested),
334
+ ])));
335
+ }
336
+ return value;
337
+ }
338
+ function copyPresentation(presentation) {
339
+ switch (presentation.type) {
340
+ case "action":
341
+ return Object.freeze({ type: "action" });
342
+ case "custom":
343
+ return Object.freeze({
344
+ type: "custom",
345
+ data: copyJsonObject(presentation.data),
346
+ });
347
+ case "choice":
348
+ return Object.freeze({
349
+ type: "choice",
350
+ options: Object.freeze(presentation.options.map((option) => Object.freeze({
351
+ ...option,
352
+ value: copyJsonValue(option.value),
353
+ }))),
354
+ });
355
+ case "form":
356
+ return Object.freeze({ ...presentation });
357
+ case "confirmation":
358
+ return Object.freeze({ ...presentation });
359
+ }
360
+ }
361
+ function copyProjections(projections) {
362
+ const copy = {};
363
+ if (projections.bindings !== undefined)
364
+ copy.bindings = copyBindings(projections.bindings);
365
+ if (projections.agent !== undefined)
366
+ copy.agent = copyAgent(projections.agent);
367
+ if (projections.help !== undefined)
368
+ copy.help = copyOrdered(projections.help.order);
369
+ if (projections.menu !== undefined)
370
+ copy.menu = copyOrdered(projections.menu.order);
371
+ if (projections.palette !== undefined)
372
+ copy.palette = copyOrdered(projections.palette.order);
373
+ if (projections.slash !== undefined)
374
+ copy.slash = copySlash(projections.slash);
375
+ return copy;
376
+ }
377
+ function copyBindings(projection) {
378
+ const copy = {
379
+ values: [...projection.values],
380
+ };
381
+ if (projection.order !== undefined)
382
+ copy.order = projection.order;
383
+ return copy;
384
+ }
385
+ function copyAgent(projection) {
386
+ const copy = {};
387
+ if (projection.keybind !== undefined)
388
+ copy.keybind = projection.keybind;
389
+ if (projection.order !== undefined)
390
+ copy.order = projection.order;
391
+ return copy;
392
+ }
393
+ function copyOrdered(order) {
394
+ const copy = {};
395
+ if (order !== undefined)
396
+ copy.order = order;
397
+ return copy;
398
+ }
399
+ function copySlash(projection) {
400
+ const copy = {
401
+ aliases: [...projection.aliases],
402
+ };
403
+ if (projection.order !== undefined)
404
+ copy.order = projection.order;
405
+ if (projection.arguments !== undefined)
406
+ copy.arguments = copySlashArguments(projection.arguments);
407
+ return copy;
408
+ }
409
+ function copySlashArguments(grammar) {
410
+ const copy = { hint: grammar.hint };
411
+ if (grammar.words !== undefined)
412
+ copy.words = Object.freeze(grammar.words.map((word) => Object.freeze({ ...word, arguments: copyJsonObject(word.arguments) })));
413
+ if (grammar.text !== undefined)
414
+ copy.text = Object.freeze({
415
+ ...grammar.text,
416
+ arguments: copyJsonObject(grammar.text.arguments),
417
+ });
418
+ return Object.freeze(copy);
419
+ }
420
+ function validateCommand(command) {
421
+ validateMeaningful(command.id, "command ID", true);
422
+ validateMeaningful(command.title, "title");
423
+ validateMeaningful(command.category, "category");
424
+ validateAction(command.action);
425
+ validateProjections(command.projections);
426
+ }
427
+ function validateAction(action) {
428
+ if (action?.presentation === undefined)
429
+ return;
430
+ switch (action.presentation.type) {
431
+ case "action":
432
+ case "custom":
433
+ return;
434
+ case "choice":
435
+ for (const option of action.presentation.options) {
436
+ validateMeaningful(option.label, "choice label");
437
+ if (option.description !== undefined)
438
+ validateMeaningful(option.description, "choice description");
439
+ }
440
+ return;
441
+ case "form":
442
+ if (action.presentation.submitLabel !== undefined)
443
+ validateMeaningful(action.presentation.submitLabel, "submit label");
444
+ return;
445
+ case "confirmation":
446
+ validateMeaningful(action.presentation.message, "confirmation message");
447
+ if (action.presentation.confirmLabel !== undefined)
448
+ validateMeaningful(action.presentation.confirmLabel, "confirmation label");
449
+ if (action.presentation.cancelLabel !== undefined)
450
+ validateMeaningful(action.presentation.cancelLabel, "cancellation label");
451
+ }
452
+ }
453
+ function validateProjections(projections) {
454
+ if (projections === undefined)
455
+ return;
456
+ for (const projection of [
457
+ projections.bindings,
458
+ projections.palette,
459
+ projections.slash,
460
+ projections.help,
461
+ projections.menu,
462
+ projections.agent,
463
+ ]) {
464
+ if (projection !== undefined)
465
+ validateOrder(projection.order);
466
+ }
467
+ if (projections.bindings !== undefined)
468
+ validateValues(projections.bindings.values, "binding");
469
+ if (projections.slash !== undefined) {
470
+ validateValues(projections.slash.aliases, "slash alias");
471
+ for (const alias of projections.slash.aliases)
472
+ if (/\s/u.test(alias))
473
+ reject("slash alias must not contain whitespace");
474
+ validateSlashArguments(projections.slash.arguments);
475
+ }
476
+ if (projections.agent?.keybind !== undefined)
477
+ validateMeaningful(projections.agent.keybind, "agent keybind");
478
+ }
479
+ function validateSlashArguments(grammar) {
480
+ if (grammar === undefined)
481
+ return;
482
+ validateMeaningful(grammar.hint, "argument hint");
483
+ if (grammar.words === undefined && grammar.text === undefined)
484
+ reject("slash arguments need words or text");
485
+ const words = new Set();
486
+ for (const word of grammar.words ?? []) {
487
+ validateMeaningful(word.word, "argument word");
488
+ if (/\s/u.test(word.word))
489
+ reject("argument word must not contain whitespace");
490
+ if (words.has(word.word))
491
+ reject("argument word is repeated");
492
+ words.add(word.word);
493
+ if (word.description !== undefined)
494
+ validateMeaningful(word.description, "argument word description");
495
+ }
496
+ if (words.size > maximumSlashWords)
497
+ reject("argument word count exceeds 32");
498
+ if (grammar.text !== undefined) {
499
+ validateMeaningful(grammar.text.field, "argument text field");
500
+ if (grammar.text.requestIdField !== undefined)
501
+ validateMeaningful(grammar.text.requestIdField, "request id field");
502
+ }
503
+ }
504
+ function validateValues(values, label) {
505
+ if (values.length > maximumBindingsOrAliases)
506
+ reject(`${label} count exceeds 16`);
507
+ for (const value of values)
508
+ validateMeaningful(value, label);
509
+ }
510
+ function validateMeaningful(value, label, colonFree = false) {
511
+ if (value.trim().length === 0)
512
+ reject(`${label} must not be blank`);
513
+ if (colonFree && value.includes(":"))
514
+ reject(`${label} must not contain a colon`);
515
+ }
516
+ function validateOrder(value) {
517
+ if (value !== undefined && !Number.isSafeInteger(value))
518
+ reject("order must be a safe integer");
519
+ }
520
+ function makeLease(update = () => undefined, dispose = () => undefined) {
521
+ return Object.freeze({ update, dispose });
522
+ }
523
+ function reject(message) {
524
+ throw new Error(message);
525
+ }
526
+ const emptySnapshot = Object.freeze({
527
+ bindings: Object.freeze([]),
528
+ palette: Object.freeze([]),
529
+ slash: Object.freeze([]),
530
+ help: Object.freeze([]),
531
+ menu: Object.freeze([]),
532
+ agent: Object.freeze([]),
533
+ });
534
+ const available = Object.freeze({ status: "available" });
535
+ function readAvailability(command) {
536
+ const value = command.availability?.() ?? available;
537
+ return value.status === "available"
538
+ ? available
539
+ : Object.freeze({ status: "unavailable", reason: value.reason });
540
+ }
541
+ function sameAvailability(left, right) {
542
+ if (left.status === "available")
543
+ return right?.status === "available";
544
+ return right?.status === "unavailable" && left.reason === right.reason;
545
+ }
546
+ function failureFor(error) {
547
+ if (error instanceof WhistleActionError) {
548
+ const failure = {
549
+ code: error.code,
550
+ message: error.message,
551
+ };
552
+ return Object.freeze(error.details === undefined
553
+ ? failure
554
+ : { ...failure, details: copyJsonValue(error.details) });
555
+ }
556
+ return Object.freeze({
557
+ code: "command_failed",
558
+ message: error.message,
559
+ });
560
+ }
561
+ function snapshotFor(contributions, availability) {
562
+ const bindings = [];
563
+ const palette = [];
564
+ const slash = [];
565
+ const help = [];
566
+ const menu = [];
567
+ const agent = [];
568
+ let commandIndex = 0;
569
+ for (const live of contributions) {
570
+ const contribution = live.contribution;
571
+ for (const command of contribution.commands) {
572
+ const projections = command.projections;
573
+ const commandAvailability = availability[commandIndex++] ?? available;
574
+ if (projections?.bindings !== undefined) {
575
+ for (const binding of projections.bindings.values)
576
+ bindings.push({
577
+ ...projectionRow(contribution, command, projections.bindings.order, commandAvailability),
578
+ binding,
579
+ });
580
+ }
581
+ if (projections?.palette !== undefined)
582
+ palette.push({
583
+ ...projectionRow(contribution, command, projections.palette.order, commandAvailability),
584
+ });
585
+ if (projections?.slash !== undefined) {
586
+ for (const alias of projections.slash.aliases) {
587
+ const row = {
588
+ ...projectionRow(contribution, command, projections.slash.order, commandAvailability),
589
+ alias,
590
+ };
591
+ slash.push(projections.slash.arguments === undefined
592
+ ? row
593
+ : { ...row, arguments: projections.slash.arguments });
594
+ }
595
+ }
596
+ if (projections?.help !== undefined)
597
+ help.push({
598
+ ...projectionRow(contribution, command, projections.help.order, commandAvailability),
599
+ });
600
+ if (projections?.menu !== undefined)
601
+ menu.push({
602
+ ...projectionRow(contribution, command, projections.menu.order, commandAvailability),
603
+ });
604
+ if (projections?.agent !== undefined)
605
+ agent.push(agentRow(contribution, command, projections.agent, commandAvailability));
606
+ }
607
+ }
608
+ sortBindings(bindings);
609
+ sortAliases(slash);
610
+ sortRows(palette);
611
+ sortRows(help);
612
+ sortRows(menu);
613
+ sortRows(agent);
614
+ return Object.freeze({
615
+ bindings: freezeRows(bindings),
616
+ palette: freezeRows(palette),
617
+ slash: freezeRows(slash),
618
+ help: freezeRows(help),
619
+ menu: freezeRows(menu),
620
+ agent: freezeRows(agent),
621
+ });
622
+ }
623
+ const utf8Encoder = new TextEncoder();
624
+ function compareUtf8(left, right) {
625
+ const leftBytes = utf8Encoder.encode(left);
626
+ const rightBytes = utf8Encoder.encode(right);
627
+ const length = Math.min(leftBytes.length, rightBytes.length);
628
+ for (let index = 0; index < length; index += 1) {
629
+ const leftByte = leftBytes[index] ?? 0;
630
+ const rightByte = rightBytes[index] ?? 0;
631
+ if (leftByte !== rightByte)
632
+ return leftByte < rightByte ? -1 : 1;
633
+ }
634
+ return leftBytes.length < rightBytes.length
635
+ ? -1
636
+ : leftBytes.length > rightBytes.length
637
+ ? 1
638
+ : 0;
639
+ }
640
+ function sortRows(rows) {
641
+ rows.sort((left, right) => left.order - right.order || compareUtf8(left.commandId, right.commandId));
642
+ }
643
+ function sortBindings(rows) {
644
+ rows.sort((left, right) => left.order - right.order ||
645
+ compareUtf8(left.commandId, right.commandId) ||
646
+ compareUtf8(left.binding, right.binding));
647
+ }
648
+ function sortAliases(rows) {
649
+ rows.sort((left, right) => left.order - right.order ||
650
+ compareUtf8(left.commandId, right.commandId) ||
651
+ compareUtf8(left.alias, right.alias));
652
+ }
653
+ function freezeRows(rows) {
654
+ return Object.freeze(rows.map((row) => Object.freeze(row)));
655
+ }
656
+ function projectionRow(contribution, command, order, availability) {
657
+ const row = unscopedRow(contribution, command, order, availability);
658
+ return contribution.scope === undefined
659
+ ? row
660
+ : { ...row, scope: contribution.scope };
661
+ }
662
+ function unscopedRow(contribution, command, order, availability) {
663
+ const row = {
664
+ commandId: `${contribution.namespace}:${command.id}`,
665
+ sourceId: contribution.sourceId,
666
+ title: command.title,
667
+ category: command.category,
668
+ order: order ?? 0,
669
+ availability,
670
+ };
671
+ const described = command.description === undefined
672
+ ? row
673
+ : { ...row, description: command.description };
674
+ return command.action === undefined
675
+ ? described
676
+ : { ...described, action: command.action };
677
+ }
678
+ function agentRow(contribution, command, projection, availability) {
679
+ const row = projectionRow(contribution, command, projection.order, availability);
680
+ const semanticRow = row.action === undefined
681
+ ? row
682
+ : { ...row, action: semanticAction(row.action) };
683
+ return projection.keybind === undefined
684
+ ? semanticRow
685
+ : { ...semanticRow, keybind: projection.keybind };
686
+ }
687
+ function semanticAction(action) {
688
+ const semantic = {};
689
+ if (action.inputSchema !== undefined)
690
+ semantic.inputSchema = action.inputSchema;
691
+ if (action.outputSchema !== undefined)
692
+ semantic.outputSchema = action.outputSchema;
693
+ return Object.freeze(semantic);
694
+ }