@loomweaver/cli 0.7.6 → 0.7.7
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/main.mjs +724 -122
- package/package.json +1 -1
package/dist/main.mjs
CHANGED
|
@@ -53,6 +53,32 @@ function ensurePostcssPlugin(existing, amendment) {
|
|
|
53
53
|
declined: []
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
|
+
function ensureDependency(manifest2, amendment) {
|
|
57
|
+
const root = asObject(manifest2) ?? {};
|
|
58
|
+
const dependencies = asObject(root["dependencies"]);
|
|
59
|
+
if (dependencies === void 0 && root["dependencies"] !== void 0) {
|
|
60
|
+
return {
|
|
61
|
+
value: root,
|
|
62
|
+
added: [],
|
|
63
|
+
declined: ['package.json: "dependencies" is not an object']
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const recorded = dependencies?.[amendment.name] ?? asObject(root["devDependencies"])?.[amendment.name];
|
|
67
|
+
if (recorded !== void 0) {
|
|
68
|
+
return { value: root, added: [], declined: [] };
|
|
69
|
+
}
|
|
70
|
+
const next = { ...dependencies, [amendment.name]: amendment.version };
|
|
71
|
+
return {
|
|
72
|
+
value: {
|
|
73
|
+
...root,
|
|
74
|
+
dependencies: Object.fromEntries(
|
|
75
|
+
Object.entries(next).toSorted(([a], [b]) => a.localeCompare(b))
|
|
76
|
+
)
|
|
77
|
+
},
|
|
78
|
+
added: [`dependencies: ${amendment.name}@${amendment.version}`],
|
|
79
|
+
declined: []
|
|
80
|
+
};
|
|
81
|
+
}
|
|
56
82
|
function ensureBuildTarget(target, amendment, projectRoot) {
|
|
57
83
|
const next = { ...asObject(target) };
|
|
58
84
|
const added = [];
|
|
@@ -263,6 +289,9 @@ function describeAmendment(amendment) {
|
|
|
263
289
|
if (amendment.kind === "postcss") {
|
|
264
290
|
return `Write ${amendment.file} beside your package.json, naming ${amendment.plugin}. Without it the stylesheet is read as plain CSS: no utility class is emitted, the workbench renders unstyled, and the build still reports success.`;
|
|
265
291
|
}
|
|
292
|
+
if (amendment.kind === "package") {
|
|
293
|
+
return `Add ${amendment.name}@${amendment.version} to your project's dependencies and install it. Without it the generated files import a package that is not there, so the very first build fails.`;
|
|
294
|
+
}
|
|
266
295
|
if (amendment.kind === "stylesheet-source") {
|
|
267
296
|
return `Add an @source entry for '${amendment.sourceRoot}' to the application's entry stylesheet, resolved from that stylesheet. Without it none of that code's utilities are emitted.`;
|
|
268
297
|
}
|
|
@@ -274,7 +303,9 @@ function describeAmendment(amendment) {
|
|
|
274
303
|
return [
|
|
275
304
|
...amendment.styles.length > 0 ? [`name ${amendment.styles.join(", ")} in styles`] : [],
|
|
276
305
|
...amendment.assets.length > 0 ? [
|
|
277
|
-
`add assets for ${amendment.assets.map((asset) => asset.input).join(
|
|
306
|
+
`add assets for ${amendment.assets.map((asset) => asset.input).join(
|
|
307
|
+
", "
|
|
308
|
+
)} (the shell fetches its own strings at runtime, so without that glob every label in the chrome renders as its raw translation key)`
|
|
278
309
|
] : [],
|
|
279
310
|
...amendment.serviceWorker ? [
|
|
280
311
|
`set serviceWorker to ${amendment.serviceWorker} in the production configuration (provideShell registers a worker that 404s otherwise)`
|
|
@@ -373,6 +404,589 @@ function validateCapabilities(capabilities, known) {
|
|
|
373
404
|
return findings;
|
|
374
405
|
}
|
|
375
406
|
|
|
407
|
+
// ../devkit/src/recipes/angular-weaver/agent-panel.ts
|
|
408
|
+
function panelFile(w) {
|
|
409
|
+
return `import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
|
|
410
|
+
import { EventType, type BaseEvent, type Tool } from '@ag-ui/core';
|
|
411
|
+
import { ${w.propertyName}Agent } from './${w.id}-agent';
|
|
412
|
+
import { askAgent } from './${w.id}-agent-source';
|
|
413
|
+
|
|
414
|
+
interface Line {
|
|
415
|
+
readonly kind: 'you' | 'agent' | 'call' | 'result' | 'note';
|
|
416
|
+
readonly text: string;
|
|
417
|
+
readonly args?: string;
|
|
418
|
+
readonly failed?: boolean;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
@Component({
|
|
422
|
+
selector: '${w.prefix}-${w.id}-agent-panel',
|
|
423
|
+
templateUrl: './${w.id}-agent-panel.html',
|
|
424
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
425
|
+
})
|
|
426
|
+
export class ${w.className}AgentPanel {
|
|
427
|
+
protected readonly offered = signal<readonly Tool[]>([]);
|
|
428
|
+
|
|
429
|
+
protected readonly lines = signal<readonly Line[]>([]);
|
|
430
|
+
|
|
431
|
+
protected readonly busy = signal(false);
|
|
432
|
+
|
|
433
|
+
private runs = 0;
|
|
434
|
+
|
|
435
|
+
constructor() {
|
|
436
|
+
this.offered.set(${w.propertyName}Agent()?.list() ?? []);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
protected async ask(name: string): Promise<void> {
|
|
440
|
+
const tools = ${w.propertyName}Agent();
|
|
441
|
+
if (!tools || this.busy()) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
this.busy.set(true);
|
|
445
|
+
try {
|
|
446
|
+
// Ask for the list again, every run. What a plugin may reach changes as plugins load and the
|
|
447
|
+
// session changes, and a list kept from earlier offers actions that are no longer there.
|
|
448
|
+
const offered = tools.list();
|
|
449
|
+
this.offered.set(offered);
|
|
450
|
+
this.push({ kind: 'you', text: \`Run \${name}\` });
|
|
451
|
+
this.push({ kind: 'note', text: \`\${offered.length} tool(s) offered right now\` });
|
|
452
|
+
|
|
453
|
+
const request = {
|
|
454
|
+
runId: \`run-\${++this.runs}\`,
|
|
455
|
+
prompt: name,
|
|
456
|
+
tools: offered,
|
|
457
|
+
};
|
|
458
|
+
for await (const event of askAgent(request)) {
|
|
459
|
+
this.draw(event);
|
|
460
|
+
// Every event goes over, unfiltered. The adapter decides which ones matter; a filter here is
|
|
461
|
+
// how a call ends up half-assembled.
|
|
462
|
+
const message = await tools.receive(event);
|
|
463
|
+
if (message) {
|
|
464
|
+
this.push({
|
|
465
|
+
kind: 'result',
|
|
466
|
+
text: message.error ?? message.content,
|
|
467
|
+
failed: Boolean(message.error),
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
// A run that ends without its closing event leaves a call open; flush() answers it.
|
|
472
|
+
const last = await tools.flush();
|
|
473
|
+
if (last) {
|
|
474
|
+
this.push({
|
|
475
|
+
kind: 'result',
|
|
476
|
+
text: last.error ?? last.content,
|
|
477
|
+
failed: Boolean(last.error),
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
} finally {
|
|
481
|
+
this.busy.set(false);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
private draw(event: BaseEvent): void {
|
|
486
|
+
const raw = event as unknown as Record<string, unknown>;
|
|
487
|
+
switch (event.type) {
|
|
488
|
+
case EventType.TEXT_MESSAGE_START:
|
|
489
|
+
this.push({ kind: 'agent', text: '' });
|
|
490
|
+
break;
|
|
491
|
+
case EventType.TEXT_MESSAGE_CONTENT:
|
|
492
|
+
this.grow('text', String(raw['delta'] ?? ''));
|
|
493
|
+
break;
|
|
494
|
+
case EventType.TOOL_CALL_START:
|
|
495
|
+
this.push({
|
|
496
|
+
kind: 'call',
|
|
497
|
+
text: String(raw['toolCallName'] ?? ''),
|
|
498
|
+
args: '',
|
|
499
|
+
});
|
|
500
|
+
break;
|
|
501
|
+
case EventType.TOOL_CALL_ARGS:
|
|
502
|
+
this.grow('args', String(raw['delta'] ?? ''));
|
|
503
|
+
break;
|
|
504
|
+
default:
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private push(line: Line): void {
|
|
510
|
+
this.lines.update((all) => [...all, line]);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private grow(field: 'text' | 'args', delta: string): void {
|
|
514
|
+
this.lines.update((all) => {
|
|
515
|
+
const last = all[all.length - 1];
|
|
516
|
+
return [
|
|
517
|
+
...all.slice(0, -1),
|
|
518
|
+
{ ...last, [field]: \`\${last[field] ?? ''}\${delta}\` },
|
|
519
|
+
];
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
`;
|
|
524
|
+
}
|
|
525
|
+
function panelTemplateFile(w) {
|
|
526
|
+
return `<div class="flex h-full flex-col gap-3">
|
|
527
|
+
<p
|
|
528
|
+
class="shrink-0 rounded-md border border-border bg-surface-raised px-3 py-2 text-xs text-content-muted"
|
|
529
|
+
>
|
|
530
|
+
This is a stand-in, not an assistant. It speaks the protocol so you can watch the whole path;
|
|
531
|
+
replace <code class="text-content">${w.id}-agent-source.ts</code> with your own transport.
|
|
532
|
+
</p>
|
|
533
|
+
|
|
534
|
+
<div class="min-h-0 flex-1 overflow-auto">
|
|
535
|
+
<ul class="flex flex-col gap-2.5">
|
|
536
|
+
@for (line of lines(); track $index) {
|
|
537
|
+
@if (line.kind === 'you') {
|
|
538
|
+
<li class="max-w-full self-end rounded-lg bg-brand-fill px-3 py-2 text-sm text-on-brand">
|
|
539
|
+
{{ line.text }}
|
|
540
|
+
</li>
|
|
541
|
+
} @else if (line.kind === 'agent') {
|
|
542
|
+
<li
|
|
543
|
+
class="max-w-full self-start rounded-lg bg-surface-raised px-3 py-2 text-sm text-content"
|
|
544
|
+
>
|
|
545
|
+
{{ line.text }}
|
|
546
|
+
</li>
|
|
547
|
+
} @else if (line.kind === 'call') {
|
|
548
|
+
<li
|
|
549
|
+
class="rounded-md border border-border px-3 py-2 font-mono text-xs break-all text-content-muted"
|
|
550
|
+
>
|
|
551
|
+
{{ line.text }}<span class="text-content-faint">({{ line.args }})</span>
|
|
552
|
+
</li>
|
|
553
|
+
} @else if (line.kind === 'result') {
|
|
554
|
+
<li
|
|
555
|
+
class="px-1 text-xs"
|
|
556
|
+
[class.text-negative]="line.failed"
|
|
557
|
+
[class.text-content-muted]="!line.failed"
|
|
558
|
+
>
|
|
559
|
+
{{ line.text }}
|
|
560
|
+
</li>
|
|
561
|
+
} @else {
|
|
562
|
+
<li class="px-1 text-xs text-content-faint">{{ line.text }}</li>
|
|
563
|
+
}
|
|
564
|
+
} @empty {
|
|
565
|
+
<li class="px-1 py-6 text-center text-sm text-content-muted">
|
|
566
|
+
Pick something below and watch the call go through.
|
|
567
|
+
</li>
|
|
568
|
+
}
|
|
569
|
+
</ul>
|
|
570
|
+
</div>
|
|
571
|
+
|
|
572
|
+
<div class="flex shrink-0 flex-col gap-2 border-t border-border pt-3">
|
|
573
|
+
<span class="px-1 text-xs font-medium text-content-muted">
|
|
574
|
+
Offered right now ({{ offered().length }})
|
|
575
|
+
</span>
|
|
576
|
+
@for (tool of offered(); track tool.name) {
|
|
577
|
+
<button
|
|
578
|
+
type="button"
|
|
579
|
+
class="lw-btn lw-btn--default lw-btn--sm h-auto justify-start py-2 text-left whitespace-normal"
|
|
580
|
+
[disabled]="busy()"
|
|
581
|
+
(click)="ask(tool.name)"
|
|
582
|
+
>
|
|
583
|
+
{{ tool.description }}
|
|
584
|
+
</button>
|
|
585
|
+
} @empty {
|
|
586
|
+
<span class="px-1 text-xs text-content-faint">
|
|
587
|
+
Nothing is offered. A command reaches this list only when it is registered with
|
|
588
|
+
<code class="text-content">callable: true</code>, and reaching another plugin's commands
|
|
589
|
+
needs the <code class="text-content">automation</code> capability granted.
|
|
590
|
+
</span>
|
|
591
|
+
}
|
|
592
|
+
</div>
|
|
593
|
+
</div>
|
|
594
|
+
`;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ../devkit/src/recipes/angular-weaver/agent-stand-in.ts
|
|
598
|
+
function standInFile(w) {
|
|
599
|
+
return `import { EventType, type BaseEvent, type Tool } from '@ag-ui/core';
|
|
600
|
+
|
|
601
|
+
// WHAT THIS IS: a stand-in for an agent, and not an agent. It speaks the AG-UI protocol and nothing
|
|
602
|
+
// else \u2014 no model, no network, no judgement \u2014 so that the whole path from the offered tools through
|
|
603
|
+
// a call to its outcome runs on the first serve, before you have connected anything.
|
|
604
|
+
//
|
|
605
|
+
// WHAT REPLACES IT: this file, and only this file. Point askAgent at your own endpoint and yield the
|
|
606
|
+
// events it streams back. The panel and the connection beside it stay exactly as they are.
|
|
607
|
+
|
|
608
|
+
const PACE = 40;
|
|
609
|
+
|
|
610
|
+
export interface AgentRequest {
|
|
611
|
+
readonly runId: string;
|
|
612
|
+
readonly prompt: string;
|
|
613
|
+
/** What the workbench offers right now \u2014 the same list a real agent would be handed. */
|
|
614
|
+
readonly tools: readonly Tool[];
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export async function* askAgent(
|
|
618
|
+
request: AgentRequest,
|
|
619
|
+
): AsyncGenerator<BaseEvent> {
|
|
620
|
+
const picked = request.tools.find((tool) =>
|
|
621
|
+
request.prompt.includes(tool.name),
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
yield event(EventType.RUN_STARTED, {
|
|
625
|
+
threadId: '${w.id}-stand-in',
|
|
626
|
+
runId: request.runId,
|
|
627
|
+
});
|
|
628
|
+
yield* speak(
|
|
629
|
+
\`\${request.runId}.says\`,
|
|
630
|
+
picked
|
|
631
|
+
? \`You asked for "\${request.prompt}". I can see \${request.tools.length} tool(s) and \${picked.name} is one of them, so I will call it.\`
|
|
632
|
+
: \`You asked for "\${request.prompt}", and nothing among the \${request.tools.length} tool(s) I was offered matches it, so I will not call anything.\`,
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
if (picked) {
|
|
636
|
+
const toolCallId = \`\${request.runId}.call\`;
|
|
637
|
+
yield event(EventType.TOOL_CALL_START, {
|
|
638
|
+
toolCallId,
|
|
639
|
+
toolCallName: picked.name,
|
|
640
|
+
});
|
|
641
|
+
// Arguments arrive in pieces, exactly as they do from a real model. The adapter assembles them;
|
|
642
|
+
// nothing downstream ever sees a half-written call. A real agent fills them from the JSON Schema
|
|
643
|
+
// the workbench described in picked.parameters; a stand-in has nothing to fill them from, so it
|
|
644
|
+
// sends none and lets the command apply its own defaults.
|
|
645
|
+
for (const piece of chunks(JSON.stringify({}), 4)) {
|
|
646
|
+
yield event(EventType.TOOL_CALL_ARGS, { toolCallId, delta: piece });
|
|
647
|
+
await pause();
|
|
648
|
+
}
|
|
649
|
+
yield event(EventType.TOOL_CALL_END, { toolCallId });
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
yield event(EventType.RUN_FINISHED, {
|
|
653
|
+
threadId: '${w.id}-stand-in',
|
|
654
|
+
runId: request.runId,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async function* speak(
|
|
659
|
+
messageId: string,
|
|
660
|
+
text: string,
|
|
661
|
+
): AsyncGenerator<BaseEvent> {
|
|
662
|
+
yield event(EventType.TEXT_MESSAGE_START, { messageId, role: 'assistant' });
|
|
663
|
+
for (const piece of chunks(text, 10)) {
|
|
664
|
+
yield event(EventType.TEXT_MESSAGE_CONTENT, { messageId, delta: piece });
|
|
665
|
+
await pause();
|
|
666
|
+
}
|
|
667
|
+
yield event(EventType.TEXT_MESSAGE_END, { messageId });
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function chunks(text: string, size: number): readonly string[] {
|
|
671
|
+
const pieces: string[] = [];
|
|
672
|
+
for (let at = 0; at < text.length; at += size) {
|
|
673
|
+
pieces.push(text.slice(at, at + size));
|
|
674
|
+
}
|
|
675
|
+
return pieces;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function event(type: EventType, fields: Record<string, unknown>): BaseEvent {
|
|
679
|
+
return { type, ...fields } as unknown as BaseEvent;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function pause(): Promise<void> {
|
|
683
|
+
return new Promise((done) => setTimeout(done, PACE));
|
|
684
|
+
}
|
|
685
|
+
`;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ../devkit/src/recipes/angular-weaver/agent-files.ts
|
|
689
|
+
var AG_UI_ADAPTER_VERSION = "0.7.7";
|
|
690
|
+
var AG_UI_PROTOCOL_VERSION = "0.0.x";
|
|
691
|
+
function connectionFile(w) {
|
|
692
|
+
return `import { signal } from '@angular/core';
|
|
693
|
+
import {
|
|
694
|
+
commandTools,
|
|
695
|
+
type CommandTools,
|
|
696
|
+
type PendingToolCall,
|
|
697
|
+
type ToolDecision,
|
|
698
|
+
} from '@loomweaver/ag-ui';
|
|
699
|
+
import type { PluginContext } from '@loomweaver/plugin-sdk';
|
|
700
|
+
|
|
701
|
+
// Commands an agent may not run on its own word. Each one asks the person at the keyboard first, and
|
|
702
|
+
// declining stops it: the workbench never sees the call. This weaver's own command is listed as an
|
|
703
|
+
// example \u2014 replace it with the ones that actually cost something.
|
|
704
|
+
const CONSEQUENTIAL = new Set(['${w.id}.hello']);
|
|
705
|
+
|
|
706
|
+
// A factory, not a module-level connection: everything a run needs lives in the closure, so a second
|
|
707
|
+
// one never shares state with the first.
|
|
708
|
+
export function ${w.propertyName}Connection(ctx: PluginContext): CommandTools {
|
|
709
|
+
return commandTools(ctx, { before: (call) => decide(ctx, call) });
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// The one connection this plugin activates, published for its panel. Set in activate(), cleared in
|
|
713
|
+
// deactivate(), so the panel renders an honest empty state either side of that.
|
|
714
|
+
export const ${w.propertyName}Agent = signal<CommandTools | null>(null);
|
|
715
|
+
|
|
716
|
+
async function decide(
|
|
717
|
+
ctx: PluginContext,
|
|
718
|
+
call: PendingToolCall,
|
|
719
|
+
): Promise<ToolDecision> {
|
|
720
|
+
if (!CONSEQUENTIAL.has(call.commandId)) {
|
|
721
|
+
return { decision: 'run' };
|
|
722
|
+
}
|
|
723
|
+
const yes = await ctx.ui.confirm({
|
|
724
|
+
title: '${w.id}.agent.confirm.title',
|
|
725
|
+
message: '${w.id}.agent.confirm.message',
|
|
726
|
+
confirmLabel: '${w.id}.agent.confirm.yes',
|
|
727
|
+
cancelLabel: '${w.id}.agent.confirm.no',
|
|
728
|
+
tone: 'warning',
|
|
729
|
+
});
|
|
730
|
+
// A decision can only narrow. Letting a call through does not make it reachable: the workbench
|
|
731
|
+
// still refuses whatever it always refused.
|
|
732
|
+
return yes
|
|
733
|
+
? { decision: 'run' }
|
|
734
|
+
: { decision: 'decline', reason: 'the person at the keyboard said no.' };
|
|
735
|
+
}
|
|
736
|
+
`;
|
|
737
|
+
}
|
|
738
|
+
function specFile(w) {
|
|
739
|
+
return `import { EventType, type BaseEvent } from '@ag-ui/core';
|
|
740
|
+
import type { CommandArguments, PluginContext } from '@loomweaver/plugin-sdk';
|
|
741
|
+
import { ${w.propertyName}Connection } from './${w.id}-agent';
|
|
742
|
+
|
|
743
|
+
interface Asked {
|
|
744
|
+
readonly id: string;
|
|
745
|
+
readonly args?: CommandArguments;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function contextThat(confirms: boolean, ran: Asked[]): PluginContext {
|
|
749
|
+
return {
|
|
750
|
+
invocableCommands: () => [
|
|
751
|
+
{ id: '${w.id}.hello', title: '${w.name} action', description: 'Shows a short message.' },
|
|
752
|
+
],
|
|
753
|
+
invokeCommand: (id: string, args?: CommandArguments) => {
|
|
754
|
+
ran.push({ id, args });
|
|
755
|
+
return Promise.resolve({ outcome: 'answered', value: 'it ran' });
|
|
756
|
+
},
|
|
757
|
+
ui: { confirm: () => Promise.resolve(confirms) },
|
|
758
|
+
} as unknown as PluginContext;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function event(type: EventType, fields: Record<string, unknown>): BaseEvent {
|
|
762
|
+
return { type, ...fields } as unknown as BaseEvent;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
describe('${w.propertyName}Connection', () => {
|
|
766
|
+
it('offers what the workbench offers', () => {
|
|
767
|
+
const tools = ${w.propertyName}Connection(contextThat(true, []));
|
|
768
|
+
expect(tools.list().map((tool) => tool.name)).toEqual(['${w.id}.hello']);
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
it('assembles a call from its events and answers with the outcome', async () => {
|
|
772
|
+
const ran: Asked[] = [];
|
|
773
|
+
const tools = ${w.propertyName}Connection(contextThat(true, ran));
|
|
774
|
+
|
|
775
|
+
expect(
|
|
776
|
+
await tools.receive(
|
|
777
|
+
event(EventType.TOOL_CALL_START, {
|
|
778
|
+
toolCallId: 'c1',
|
|
779
|
+
toolCallName: '${w.id}.hello',
|
|
780
|
+
}),
|
|
781
|
+
),
|
|
782
|
+
).toBeNull();
|
|
783
|
+
await tools.receive(
|
|
784
|
+
event(EventType.TOOL_CALL_ARGS, { toolCallId: 'c1', delta: '{"who"' }),
|
|
785
|
+
);
|
|
786
|
+
await tools.receive(
|
|
787
|
+
event(EventType.TOOL_CALL_ARGS, { toolCallId: 'c1', delta: ':"you"}' }),
|
|
788
|
+
);
|
|
789
|
+
const answer = await tools.receive(
|
|
790
|
+
event(EventType.TOOL_CALL_END, { toolCallId: 'c1' }),
|
|
791
|
+
);
|
|
792
|
+
|
|
793
|
+
expect(ran).toEqual([{ id: '${w.id}.hello', args: { who: 'you' } }]);
|
|
794
|
+
expect(answer?.content).toBe('it ran');
|
|
795
|
+
expect(answer?.error).toBeUndefined();
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
it('never reaches the workbench when a consequential call is declined', async () => {
|
|
799
|
+
const ran: Asked[] = [];
|
|
800
|
+
const tools = ${w.propertyName}Connection(contextThat(false, ran));
|
|
801
|
+
|
|
802
|
+
await tools.receive(
|
|
803
|
+
event(EventType.TOOL_CALL_START, {
|
|
804
|
+
toolCallId: 'c2',
|
|
805
|
+
toolCallName: '${w.id}.hello',
|
|
806
|
+
}),
|
|
807
|
+
);
|
|
808
|
+
await tools.receive(
|
|
809
|
+
event(EventType.TOOL_CALL_ARGS, { toolCallId: 'c2', delta: '{}' }),
|
|
810
|
+
);
|
|
811
|
+
const answer = await tools.receive(
|
|
812
|
+
event(EventType.TOOL_CALL_END, { toolCallId: 'c2' }),
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
expect(ran).toEqual([]);
|
|
816
|
+
expect(answer?.error).toContain('did not run');
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
`;
|
|
820
|
+
}
|
|
821
|
+
function agentSurfaceBlock(w) {
|
|
822
|
+
return [
|
|
823
|
+
" ctx.registerSurface({",
|
|
824
|
+
` id: '${w.id}.agent',`,
|
|
825
|
+
` title: '${w.id}.agent.title',`,
|
|
826
|
+
` icon: '${w.id}',`,
|
|
827
|
+
" docks: ['right-panel'],",
|
|
828
|
+
` component: ${w.className}AgentPanel,`,
|
|
829
|
+
" });"
|
|
830
|
+
].join("\n");
|
|
831
|
+
}
|
|
832
|
+
function agentFiles(w) {
|
|
833
|
+
const files = {
|
|
834
|
+
[`src/lib/agent/${w.id}-agent.ts`]: connectionFile(w),
|
|
835
|
+
[`src/lib/agent/${w.id}-agent-source.ts`]: standInFile(w),
|
|
836
|
+
[`src/lib/agent/${w.id}-agent-panel.ts`]: panelFile(w),
|
|
837
|
+
[`src/lib/agent/${w.id}-agent-panel.html`]: panelTemplateFile(w)
|
|
838
|
+
};
|
|
839
|
+
if (w.features.spec) {
|
|
840
|
+
files[`src/lib/agent/${w.id}-agent.spec.ts`] = specFile(w);
|
|
841
|
+
}
|
|
842
|
+
return files;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// ../devkit/src/recipes/angular-weaver/weaver-terms.ts
|
|
846
|
+
var CONTAINER_EXAMPLE_ID = "example";
|
|
847
|
+
function capabilityItems(capabilities) {
|
|
848
|
+
return capabilities.map((capability) => `'${capability}'`).join(", ");
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// ../devkit/src/recipes/angular-weaver/weaver-readme.ts
|
|
852
|
+
function surfaceNotes(w) {
|
|
853
|
+
const railNote = "Rail and bar items reference region ids (`primary`, `status`) that must exist in your layout.";
|
|
854
|
+
if (w.features.container) {
|
|
855
|
+
return [
|
|
856
|
+
`The surface is a **container**: it is routable at \`/${w.id}/:id\`, and its tab holds a`,
|
|
857
|
+
"nested pane tree of child surfaces. The host draws the inner tabs, splits and drag targets; this",
|
|
858
|
+
"weaver only declares which children it offers.",
|
|
859
|
+
"",
|
|
860
|
+
`- \`children\` is what the inner "new tab" picker lists \u2014 the host access-gates it for you.`,
|
|
861
|
+
"- `initial` is what a freshly opened container tab starts with.",
|
|
862
|
+
`- The children declare \`docks: []\`. That is the container-only convention: they are never seeded`,
|
|
863
|
+
" into a sidebar, they exist solely inside this container.",
|
|
864
|
+
`- Each child reads the container's \`:id\` from an injected \`ActivatedRoute\` \u2014 the host supplies a`,
|
|
865
|
+
" synthetic one, so a child needs no knowledge of where it is mounted. Two open container tabs are",
|
|
866
|
+
" two independent trees, each scoped to its own id.",
|
|
867
|
+
`- The inner tree is **sealed**: a child cannot be dragged out, and nothing can be dragged in. It`,
|
|
868
|
+
" travels with the tab, including into a sidebar or a pop-out window.",
|
|
869
|
+
"",
|
|
870
|
+
`The rail item opens the fixed id \`${CONTAINER_EXAMPLE_ID}\`. Replace that with whatever the user`,
|
|
871
|
+
"actually picked \u2014 a document, a run, a project.",
|
|
872
|
+
"",
|
|
873
|
+
railNote
|
|
874
|
+
];
|
|
875
|
+
}
|
|
876
|
+
if (w.features.instanceable) {
|
|
877
|
+
return [
|
|
878
|
+
`The surface is **docked** into the \`primary\` region and marked \`instanceable\`, so the host shows a`,
|
|
879
|
+
"switcher for saving, naming, renaming and deleting several configurations of it, each with its own",
|
|
880
|
+
"`VIEW_STATE` blob.",
|
|
881
|
+
"",
|
|
882
|
+
"It is deliberately **not** routable. Named instances exist only for a docked surface \u2014 a routable",
|
|
883
|
+
"one holds the URL pane instead, and the host drops `instanceable` on that path. The rail item",
|
|
884
|
+
"therefore reveals the surface (`ctx.revealSurface`) rather than navigating to a URL, which focuses",
|
|
885
|
+
"it wherever the user has since moved it.",
|
|
886
|
+
"",
|
|
887
|
+
"The generated view already uses that blob for its sort order, because a hidden surface is destroyed",
|
|
888
|
+
"as soon as it is clean: state kept in a component field survives neither a tab switch nor",
|
|
889
|
+
"a collapsed sidebar, and never survived a reload. The rule is *evictable = reload-safe* \u2014 anything",
|
|
890
|
+
"that must not be lost goes through `VIEW_STATE`, and `set()` replaces the whole blob, so spread it.",
|
|
891
|
+
"",
|
|
892
|
+
railNote
|
|
893
|
+
];
|
|
894
|
+
}
|
|
895
|
+
return [
|
|
896
|
+
`The surface is routable at \`/${w.id}\`; ${railNote.charAt(0).toLowerCase()}${railNote.slice(1)}`,
|
|
897
|
+
"",
|
|
898
|
+
"A routable surface has **no `VIEW_STATE` handle** \u2014 injecting the token there throws. It owns a URL,",
|
|
899
|
+
"so anything shareable (a filter, the active sub-tab) belongs in route params or `subRoutes`, where it",
|
|
900
|
+
"survives a deep link too; unsaved edits are `DirtySurface`, and an instance that is expensive to",
|
|
901
|
+
"rebuild declares `retain: 'always'`. Generate with `--instanceable` for the docked, `VIEW_STATE`",
|
|
902
|
+
"flavour instead."
|
|
903
|
+
];
|
|
904
|
+
}
|
|
905
|
+
function agentNotes(w) {
|
|
906
|
+
return [
|
|
907
|
+
"",
|
|
908
|
+
"## The agent connection",
|
|
909
|
+
"",
|
|
910
|
+
`\`src/lib/agent/\` holds three files and one of them is meant to be thrown away.`,
|
|
911
|
+
"",
|
|
912
|
+
`- \`${w.id}-agent.ts\` is the connection: the workbench's own commands offered as tools, and a`,
|
|
913
|
+
" seam where this weaver decides about a call before it runs. Nothing is registered twice \u2014 the",
|
|
914
|
+
" list comes from the workbench, already narrowed by everything that would refuse the call.",
|
|
915
|
+
`- \`${w.id}-agent-panel.ts\` shows what is offered, the call as it streams, and the outcome.`,
|
|
916
|
+
`- \`${w.id}-agent-source.ts\` is a **stand-in**, not an assistant: it produces the protocol's own`,
|
|
917
|
+
" events so the whole path runs before you have connected anything. Replace that one file with",
|
|
918
|
+
" your transport and nothing else changes. No transport, credential or model is generated for",
|
|
919
|
+
" you, because none of them can be guessed.",
|
|
920
|
+
"",
|
|
921
|
+
"Three things are easy to get wrong and invisible when they are, so the generated code does them",
|
|
922
|
+
"rather than explaining them: the offered list is asked for again every run, every event is handed",
|
|
923
|
+
"over unfiltered, and a decision before a call can only narrow what the workbench would have",
|
|
924
|
+
"allowed anyway.",
|
|
925
|
+
"",
|
|
926
|
+
`The generated weaver needs two packages your project may not carry yet:`,
|
|
927
|
+
"",
|
|
928
|
+
"```bash",
|
|
929
|
+
`npm i @loomweaver/ag-ui @ag-ui/core`,
|
|
930
|
+
"```",
|
|
931
|
+
"",
|
|
932
|
+
"The Nx generator and the CLI record them for you; the MCP route names them instead."
|
|
933
|
+
];
|
|
934
|
+
}
|
|
935
|
+
function readmeFile(w) {
|
|
936
|
+
return [
|
|
937
|
+
`# ${w.name} weaver`,
|
|
938
|
+
"",
|
|
939
|
+
`A LoomWeaver weaver (a domain plugin bundle). It consumes only the public \`@loomweaver/plugin-sdk\` contract.`,
|
|
940
|
+
"",
|
|
941
|
+
"## Wire it into a distribution",
|
|
942
|
+
"",
|
|
943
|
+
`1. Add the plugin to \`providePlugins\` in \`src/app/app.config.ts\`. It is **variadic** and`,
|
|
944
|
+
` returns an array, so spread it:`,
|
|
945
|
+
"",
|
|
946
|
+
" ```ts",
|
|
947
|
+
` import { ${w.propertyName}Plugin } from '${w.importPath}'; // Nx: the workspace alias; without one, a relative path to this library's src/index.ts`,
|
|
948
|
+
` ...providePlugins(${w.propertyName}Plugin),`,
|
|
949
|
+
" ```",
|
|
950
|
+
"",
|
|
951
|
+
`2. Grant its capabilities (default-deny) via \`provideCapabilityGrants\`:`,
|
|
952
|
+
"",
|
|
953
|
+
" ```ts",
|
|
954
|
+
` provideCapabilityGrants({ '${w.id}': [${capabilityItems(w.capabilities)}] });`,
|
|
955
|
+
" ```",
|
|
956
|
+
"",
|
|
957
|
+
`3. Compose its translations with \`provideTranslationNamespaces('${w.id}')\` \u2014 and serve the`,
|
|
958
|
+
` bundle by adding an assets glob to your application's build target, so the loader can fetch`,
|
|
959
|
+
` \`/i18n/${w.id}/<lang>.json\` (the Nx generator adds this glob for you):`,
|
|
960
|
+
"",
|
|
961
|
+
" ```json",
|
|
962
|
+
` { "glob": "**/*.json", "input": "<path to this library>/src/lib/i18n", "output": "i18n/${w.id}" }`,
|
|
963
|
+
" ```",
|
|
964
|
+
"",
|
|
965
|
+
`4. If your application compiles the shell's theme with Tailwind, name this library as a source`,
|
|
966
|
+
` for it, so the utility classes in these templates are emitted. Tailwind also detects sources`,
|
|
967
|
+
` by itself, but that depends on where it resolves the project root and on \`.gitignore\`, and`,
|
|
968
|
+
` what the scaffold names covers the application alone (the Nx generator adds this line for`,
|
|
969
|
+
` you). Applications scaffolded with \`--styles precompiled\` run no Tailwind and need nothing:`,
|
|
970
|
+
"",
|
|
971
|
+
" ```css",
|
|
972
|
+
` @source '<path from that stylesheet to this library>/src';`,
|
|
973
|
+
" ```",
|
|
974
|
+
"",
|
|
975
|
+
...surfaceNotes(w),
|
|
976
|
+
...w.features.agent ? agentNotes(w) : [],
|
|
977
|
+
"",
|
|
978
|
+
"## After scaffolding",
|
|
979
|
+
"",
|
|
980
|
+
"- `src/lib/i18n/de.json` starts as a copy of the English strings \u2014 translate it.",
|
|
981
|
+
`- A scaffolded command defaults its shortcut to \`mod+shift+<first letter of the id>\` \u2014 two weavers whose ids share a first letter collide; pass \`--shortcut\` or edit the command.`,
|
|
982
|
+
"- The project is generated **untagged**: Nx tags belong to your `depConstraints`, and inventing",
|
|
983
|
+
" one would fail a lint policy you never opted this project into. If your workspace enforces",
|
|
984
|
+
" module boundaries, give it tags your constraints allow \u2014 `--tags` at generation time, or",
|
|
985
|
+
" `tags` in `project.json` afterwards.",
|
|
986
|
+
""
|
|
987
|
+
].join("\n");
|
|
988
|
+
}
|
|
989
|
+
|
|
376
990
|
// ../devkit/src/recipes/angular-weaver/weaver-i18n.ts
|
|
377
991
|
function i18nBundle(w) {
|
|
378
992
|
const bundle = { title: w.name };
|
|
@@ -385,6 +999,17 @@ function i18nBundle(w) {
|
|
|
385
999
|
bundle["actionDescription"] = `Shows a short ${w.name} message.`;
|
|
386
1000
|
}
|
|
387
1001
|
if (w.features.about) bundle["about"] = `About ${w.name}`;
|
|
1002
|
+
if (w.features.agent) {
|
|
1003
|
+
bundle["agent"] = {
|
|
1004
|
+
title: `${w.name} assistant`,
|
|
1005
|
+
confirm: {
|
|
1006
|
+
title: "Run this command?",
|
|
1007
|
+
message: "An agent asked to run a command that was marked consequential.",
|
|
1008
|
+
yes: "Run it",
|
|
1009
|
+
no: "Not now"
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
388
1013
|
if (w.features.settings)
|
|
389
1014
|
bundle["settings"] = { title: w.name, enabled: "Enabled", note: "Note" };
|
|
390
1015
|
return bundle;
|
|
@@ -428,9 +1053,7 @@ var PLATFORM_BOUND_CHORD_TOKENS = /* @__PURE__ */ new Set([
|
|
|
428
1053
|
]);
|
|
429
1054
|
function assertPlatformNeutralChord(shortcut) {
|
|
430
1055
|
const tokens = shortcut.toLowerCase().split("+").map((token) => token.trim());
|
|
431
|
-
const bound = tokens.find(
|
|
432
|
-
(token) => PLATFORM_BOUND_CHORD_TOKENS.has(token)
|
|
433
|
-
);
|
|
1056
|
+
const bound = tokens.find((token) => PLATFORM_BOUND_CHORD_TOKENS.has(token));
|
|
434
1057
|
if (bound) {
|
|
435
1058
|
throw new Error(
|
|
436
1059
|
`Shortcut "${shortcut}" binds the platform-specific "${bound}" key. Use the neutral 'mod' token (e.g. 'mod+shift+k') \u2014 the host renders it as \u2318 on macOS and Ctrl elsewhere.`
|
|
@@ -451,8 +1074,9 @@ function resolveFeatures(id, input) {
|
|
|
451
1074
|
'A surface cannot be both a container and instanceable: a container tab holds its own ":id" and is therefore routable, while named instances exist only for a docked, non-routable surface. Pick one.'
|
|
452
1075
|
);
|
|
453
1076
|
}
|
|
1077
|
+
const agent = Boolean(input?.agent);
|
|
454
1078
|
return {
|
|
455
|
-
command: Boolean(input?.command) || menuSlot !== void 0 || barItem || hasShortcut,
|
|
1079
|
+
command: Boolean(input?.command) || menuSlot !== void 0 || barItem || hasShortcut || agent,
|
|
456
1080
|
menuSlot,
|
|
457
1081
|
settings: Boolean(input?.settings),
|
|
458
1082
|
access: input?.access ? accessLiteral(input.access) : void 0,
|
|
@@ -461,6 +1085,7 @@ function resolveFeatures(id, input) {
|
|
|
461
1085
|
about: Boolean(input?.about),
|
|
462
1086
|
instanceable,
|
|
463
1087
|
container,
|
|
1088
|
+
agent,
|
|
464
1089
|
spec: input?.spec !== false
|
|
465
1090
|
};
|
|
466
1091
|
}
|
|
@@ -471,6 +1096,10 @@ function deriveCapabilities(features) {
|
|
|
471
1096
|
set.add("ui");
|
|
472
1097
|
set.add("host");
|
|
473
1098
|
}
|
|
1099
|
+
if (features.agent) {
|
|
1100
|
+
set.add("ui");
|
|
1101
|
+
set.add("automation");
|
|
1102
|
+
}
|
|
474
1103
|
return KNOWN_CAPABILITIES.filter((capability) => set.has(capability));
|
|
475
1104
|
}
|
|
476
1105
|
function resolveWeaverInput(input) {
|
|
@@ -492,10 +1121,6 @@ function resolveWeaverInput(input) {
|
|
|
492
1121
|
importPath: input.importPath?.trim() || `@loomweaver/${input.id}-weaver`
|
|
493
1122
|
};
|
|
494
1123
|
}
|
|
495
|
-
function capabilityItems(capabilities) {
|
|
496
|
-
return capabilities.map((capability) => `'${capability}'`).join(", ");
|
|
497
|
-
}
|
|
498
|
-
var CONTAINER_EXAMPLE_ID = "example";
|
|
499
1124
|
function containerChildIds(w) {
|
|
500
1125
|
return [`${w.id}.canvas`, `${w.id}.details`];
|
|
501
1126
|
}
|
|
@@ -659,6 +1284,12 @@ function pluginFile(w) {
|
|
|
659
1284
|
`import { ${w.className}AboutDialog } from '../dialogs/${w.id}-about-dialog';`
|
|
660
1285
|
);
|
|
661
1286
|
}
|
|
1287
|
+
if (w.features.agent) {
|
|
1288
|
+
imports.push(
|
|
1289
|
+
`import { ${w.propertyName}Agent, ${w.propertyName}Connection } from '../agent/${w.id}-agent';`,
|
|
1290
|
+
`import { ${w.className}AgentPanel } from '../agent/${w.id}-agent-panel';`
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
662
1293
|
if (w.features.settings) {
|
|
663
1294
|
imports.unshift("import { signal } from '@angular/core';");
|
|
664
1295
|
}
|
|
@@ -671,6 +1302,11 @@ function pluginFile(w) {
|
|
|
671
1302
|
);
|
|
672
1303
|
}
|
|
673
1304
|
const body = [` ctx.contributeIcons({ '${w.id}': icon });`];
|
|
1305
|
+
if (w.features.agent) {
|
|
1306
|
+
body.push(
|
|
1307
|
+
` ${w.propertyName}Agent.set(${w.propertyName}Connection(ctx));`
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
674
1310
|
if (w.features.command) body.push(commandBlock(w));
|
|
675
1311
|
if (w.features.about) body.push(aboutCommandBlock(w));
|
|
676
1312
|
body.push(surfaceBlock(w), railBlock(w));
|
|
@@ -678,6 +1314,11 @@ function pluginFile(w) {
|
|
|
678
1314
|
if (w.features.barItem) body.push(barItemBlock(w));
|
|
679
1315
|
if (w.features.menuSlot) body.push(menuBlock(w));
|
|
680
1316
|
if (w.features.settings) body.push(settingsBlock(w));
|
|
1317
|
+
if (w.features.agent) body.push(agentSurfaceBlock(w));
|
|
1318
|
+
const deactivate = w.features.agent ? `
|
|
1319
|
+
deactivate() {
|
|
1320
|
+
${w.propertyName}Agent.set(null);
|
|
1321
|
+
},` : "";
|
|
681
1322
|
return `${imports.join("\n")}
|
|
682
1323
|
|
|
683
1324
|
${consts.join("\n")}
|
|
@@ -690,7 +1331,7 @@ export const ${w.propertyName}Plugin: Plugin = {
|
|
|
690
1331
|
},
|
|
691
1332
|
activate(ctx) {
|
|
692
1333
|
${body.join("\n")}
|
|
693
|
-
}
|
|
1334
|
+
},${deactivate}
|
|
694
1335
|
};
|
|
695
1336
|
`;
|
|
696
1337
|
}
|
|
@@ -806,7 +1447,7 @@ function childViewTemplateFile(w, heading) {
|
|
|
806
1447
|
</div>
|
|
807
1448
|
`;
|
|
808
1449
|
}
|
|
809
|
-
function
|
|
1450
|
+
function specFile2(w) {
|
|
810
1451
|
return `import { ${w.propertyName}Plugin } from './${w.id}.plugin';
|
|
811
1452
|
|
|
812
1453
|
describe('${w.propertyName}Plugin', () => {
|
|
@@ -817,112 +1458,6 @@ describe('${w.propertyName}Plugin', () => {
|
|
|
817
1458
|
});
|
|
818
1459
|
`;
|
|
819
1460
|
}
|
|
820
|
-
function surfaceNotes(w) {
|
|
821
|
-
const railNote = "Rail and bar items reference region ids (`primary`, `status`) that must exist in your layout.";
|
|
822
|
-
if (w.features.container) {
|
|
823
|
-
return [
|
|
824
|
-
`The surface is a **container**: it is routable at \`/${w.id}/:id\`, and its tab holds a`,
|
|
825
|
-
"nested pane tree of child surfaces. The host draws the inner tabs, splits and drag targets; this",
|
|
826
|
-
"weaver only declares which children it offers.",
|
|
827
|
-
"",
|
|
828
|
-
`- \`children\` is what the inner "new tab" picker lists \u2014 the host access-gates it for you.`,
|
|
829
|
-
"- `initial` is what a freshly opened container tab starts with.",
|
|
830
|
-
`- The children declare \`docks: []\`. That is the container-only convention: they are never seeded`,
|
|
831
|
-
" into a sidebar, they exist solely inside this container.",
|
|
832
|
-
`- Each child reads the container's \`:id\` from an injected \`ActivatedRoute\` \u2014 the host supplies a`,
|
|
833
|
-
" synthetic one, so a child needs no knowledge of where it is mounted. Two open container tabs are",
|
|
834
|
-
" two independent trees, each scoped to its own id.",
|
|
835
|
-
`- The inner tree is **sealed**: a child cannot be dragged out, and nothing can be dragged in. It`,
|
|
836
|
-
" travels with the tab, including into a sidebar or a pop-out window.",
|
|
837
|
-
"",
|
|
838
|
-
`The rail item opens the fixed id \`${CONTAINER_EXAMPLE_ID}\`. Replace that with whatever the user`,
|
|
839
|
-
"actually picked \u2014 a document, a run, a project.",
|
|
840
|
-
"",
|
|
841
|
-
railNote
|
|
842
|
-
];
|
|
843
|
-
}
|
|
844
|
-
if (w.features.instanceable) {
|
|
845
|
-
return [
|
|
846
|
-
`The surface is **docked** into the \`primary\` region and marked \`instanceable\`, so the host shows a`,
|
|
847
|
-
"switcher for saving, naming, renaming and deleting several configurations of it, each with its own",
|
|
848
|
-
"`VIEW_STATE` blob.",
|
|
849
|
-
"",
|
|
850
|
-
"It is deliberately **not** routable. Named instances exist only for a docked surface \u2014 a routable",
|
|
851
|
-
"one holds the URL pane instead, and the host drops `instanceable` on that path. The rail item",
|
|
852
|
-
"therefore reveals the surface (`ctx.revealSurface`) rather than navigating to a URL, which focuses",
|
|
853
|
-
"it wherever the user has since moved it.",
|
|
854
|
-
"",
|
|
855
|
-
"The generated view already uses that blob for its sort order, because a hidden surface is destroyed",
|
|
856
|
-
"as soon as it is clean: state kept in a component field survives neither a tab switch nor",
|
|
857
|
-
"a collapsed sidebar, and never survived a reload. The rule is *evictable = reload-safe* \u2014 anything",
|
|
858
|
-
"that must not be lost goes through `VIEW_STATE`, and `set()` replaces the whole blob, so spread it.",
|
|
859
|
-
"",
|
|
860
|
-
railNote
|
|
861
|
-
];
|
|
862
|
-
}
|
|
863
|
-
return [
|
|
864
|
-
`The surface is routable at \`/${w.id}\`; ${railNote.charAt(0).toLowerCase()}${railNote.slice(1)}`,
|
|
865
|
-
"",
|
|
866
|
-
"A routable surface has **no `VIEW_STATE` handle** \u2014 injecting the token there throws. It owns a URL,",
|
|
867
|
-
"so anything shareable (a filter, the active sub-tab) belongs in route params or `subRoutes`, where it",
|
|
868
|
-
"survives a deep link too; unsaved edits are `DirtySurface`, and an instance that is expensive to",
|
|
869
|
-
"rebuild declares `retain: 'always'`. Generate with `--instanceable` for the docked, `VIEW_STATE`",
|
|
870
|
-
"flavour instead."
|
|
871
|
-
];
|
|
872
|
-
}
|
|
873
|
-
function readmeFile(w) {
|
|
874
|
-
return [
|
|
875
|
-
`# ${w.name} weaver`,
|
|
876
|
-
"",
|
|
877
|
-
`A LoomWeaver weaver (a domain plugin bundle). It consumes only the public \`@loomweaver/plugin-sdk\` contract.`,
|
|
878
|
-
"",
|
|
879
|
-
"## Wire it into a distribution",
|
|
880
|
-
"",
|
|
881
|
-
`1. Add the plugin to \`providePlugins\` in \`src/app/app.config.ts\`. It is **variadic** and`,
|
|
882
|
-
` returns an array, so spread it:`,
|
|
883
|
-
"",
|
|
884
|
-
" ```ts",
|
|
885
|
-
` import { ${w.propertyName}Plugin } from '${w.importPath}'; // Nx: the workspace alias; without one, a relative path to this library's src/index.ts`,
|
|
886
|
-
` ...providePlugins(${w.propertyName}Plugin),`,
|
|
887
|
-
" ```",
|
|
888
|
-
"",
|
|
889
|
-
`2. Grant its capabilities (default-deny) via \`provideCapabilityGrants\`:`,
|
|
890
|
-
"",
|
|
891
|
-
" ```ts",
|
|
892
|
-
` provideCapabilityGrants({ '${w.id}': [${capabilityItems(w.capabilities)}] });`,
|
|
893
|
-
" ```",
|
|
894
|
-
"",
|
|
895
|
-
`3. Compose its translations with \`provideTranslationNamespaces('${w.id}')\` \u2014 and serve the`,
|
|
896
|
-
` bundle by adding an assets glob to your application's build target, so the loader can fetch`,
|
|
897
|
-
` \`/i18n/${w.id}/<lang>.json\` (the Nx generator adds this glob for you):`,
|
|
898
|
-
"",
|
|
899
|
-
" ```json",
|
|
900
|
-
` { "glob": "**/*.json", "input": "<path to this library>/src/lib/i18n", "output": "i18n/${w.id}" }`,
|
|
901
|
-
" ```",
|
|
902
|
-
"",
|
|
903
|
-
`4. If your application compiles the shell's theme with Tailwind, name this library as a source`,
|
|
904
|
-
` for it, so the utility classes in these templates are emitted. Tailwind also detects sources`,
|
|
905
|
-
` by itself, but that depends on where it resolves the project root and on \`.gitignore\`, and`,
|
|
906
|
-
` what the scaffold names covers the application alone (the Nx generator adds this line for`,
|
|
907
|
-
` you). Applications scaffolded with \`--styles precompiled\` run no Tailwind and need nothing:`,
|
|
908
|
-
"",
|
|
909
|
-
" ```css",
|
|
910
|
-
` @source '<path from that stylesheet to this library>/src';`,
|
|
911
|
-
" ```",
|
|
912
|
-
"",
|
|
913
|
-
...surfaceNotes(w),
|
|
914
|
-
"",
|
|
915
|
-
"## After scaffolding",
|
|
916
|
-
"",
|
|
917
|
-
"- `src/lib/i18n/de.json` starts as a copy of the English strings \u2014 translate it.",
|
|
918
|
-
`- A scaffolded command defaults its shortcut to \`mod+shift+<first letter of the id>\` \u2014 two weavers whose ids share a first letter collide; pass \`--shortcut\` or edit the command.`,
|
|
919
|
-
"- The project is generated **untagged**: Nx tags belong to your `depConstraints`, and inventing",
|
|
920
|
-
" one would fail a lint policy you never opted this project into. If your workspace enforces",
|
|
921
|
-
" module boundaries, give it tags your constraints allow \u2014 `--tags` at generation time, or",
|
|
922
|
-
" `tags` in `project.json` afterwards.",
|
|
923
|
-
""
|
|
924
|
-
].join("\n");
|
|
925
|
-
}
|
|
926
1461
|
var angularWeaver = {
|
|
927
1462
|
id: "angular-weaver",
|
|
928
1463
|
build(input) {
|
|
@@ -961,8 +1496,11 @@ var angularWeaver = {
|
|
|
961
1496
|
files[`src/lib/dialogs/${w.id}-about-dialog.ts`] = aboutDialogFile(w);
|
|
962
1497
|
files[`src/lib/dialogs/${w.id}-about-dialog.html`] = aboutDialogTemplateFile(w);
|
|
963
1498
|
}
|
|
1499
|
+
if (w.features.agent) {
|
|
1500
|
+
Object.assign(files, agentFiles(w));
|
|
1501
|
+
}
|
|
964
1502
|
if (w.features.spec) {
|
|
965
|
-
files[`src/lib/plugin/${w.id}.plugin.spec.ts`] =
|
|
1503
|
+
files[`src/lib/plugin/${w.id}.plugin.spec.ts`] = specFile2(w);
|
|
966
1504
|
}
|
|
967
1505
|
return files;
|
|
968
1506
|
}
|
|
@@ -1892,11 +2430,24 @@ var layout = {
|
|
|
1892
2430
|
// ../devkit/src/recipes/angular-weaver/amendments.ts
|
|
1893
2431
|
function weaverAmendments(input, where) {
|
|
1894
2432
|
const w = resolveWeaverInput(input);
|
|
2433
|
+
const packages = w.features.agent ? [
|
|
2434
|
+
{
|
|
2435
|
+
kind: "package",
|
|
2436
|
+
name: "@loomweaver/ag-ui",
|
|
2437
|
+
version: `^${AG_UI_ADAPTER_VERSION}`
|
|
2438
|
+
},
|
|
2439
|
+
{
|
|
2440
|
+
kind: "package",
|
|
2441
|
+
name: "@ag-ui/core",
|
|
2442
|
+
version: AG_UI_PROTOCOL_VERSION
|
|
2443
|
+
}
|
|
2444
|
+
] : [];
|
|
1895
2445
|
const directory = normalizeProjectRoot(where ?? "");
|
|
1896
2446
|
if (!directory) {
|
|
1897
|
-
return
|
|
2447
|
+
return packages;
|
|
1898
2448
|
}
|
|
1899
2449
|
return [
|
|
2450
|
+
...packages,
|
|
1900
2451
|
{
|
|
1901
2452
|
kind: "build-target",
|
|
1902
2453
|
styles: [],
|
|
@@ -1936,6 +2487,7 @@ function weaverInput(values) {
|
|
|
1936
2487
|
about: bool(values, "about"),
|
|
1937
2488
|
instanceable: bool(values, "instanceable"),
|
|
1938
2489
|
container: bool(values, "container"),
|
|
2490
|
+
agent: bool(values, "agent"),
|
|
1939
2491
|
access: str(values, "access"),
|
|
1940
2492
|
spec: bool(values, "spec")
|
|
1941
2493
|
}
|
|
@@ -2062,6 +2614,12 @@ var SCAFFOLDS = [
|
|
|
2062
2614
|
description: "Make the surface a container: a routable tab at '<id>/:id' holding a nested pane tree of child surfaces, which the host draws. Not combinable with --instanceable.",
|
|
2063
2615
|
default: false
|
|
2064
2616
|
},
|
|
2617
|
+
{
|
|
2618
|
+
name: "agent",
|
|
2619
|
+
type: "boolean",
|
|
2620
|
+
description: "Also scaffold the connection that lets an AG-UI agent run the commands this workbench offers: a docked panel, the seam where a call is decided before it runs, and a local stand-in that speaks the protocol so the whole path works on the first serve. Implies --command.",
|
|
2621
|
+
default: false
|
|
2622
|
+
},
|
|
2065
2623
|
{
|
|
2066
2624
|
name: "access",
|
|
2067
2625
|
type: "string",
|
|
@@ -2734,13 +3292,16 @@ var Amender = class {
|
|
|
2734
3292
|
planned = [];
|
|
2735
3293
|
remaining = [];
|
|
2736
3294
|
configAdded = [];
|
|
3295
|
+
manifestAdded = [];
|
|
2737
3296
|
config;
|
|
3297
|
+
manifest;
|
|
2738
3298
|
project;
|
|
2739
3299
|
plan(amendments2) {
|
|
2740
3300
|
for (const amendment of amendments2) {
|
|
2741
3301
|
this.planOne(amendment);
|
|
2742
3302
|
}
|
|
2743
3303
|
this.flushConfig();
|
|
3304
|
+
this.flushManifest();
|
|
2744
3305
|
return { amendments: this.planned, remaining: this.remaining };
|
|
2745
3306
|
}
|
|
2746
3307
|
planOne(amendment) {
|
|
@@ -2748,6 +3309,10 @@ var Amender = class {
|
|
|
2748
3309
|
this.planPostcss(amendment);
|
|
2749
3310
|
return;
|
|
2750
3311
|
}
|
|
3312
|
+
if (amendment.kind === "package") {
|
|
3313
|
+
this.planPackage(amendment);
|
|
3314
|
+
return;
|
|
3315
|
+
}
|
|
2751
3316
|
if (this.workspace.kind !== "angular") {
|
|
2752
3317
|
this.remaining.push(this.nonAngularNote(amendment));
|
|
2753
3318
|
return;
|
|
@@ -2791,6 +3356,39 @@ var Amender = class {
|
|
|
2791
3356
|
`
|
|
2792
3357
|
});
|
|
2793
3358
|
}
|
|
3359
|
+
planPackage(amendment) {
|
|
3360
|
+
const file = resolve2(this.workspace.root, "package.json");
|
|
3361
|
+
if (!existsSync2(file)) {
|
|
3362
|
+
this.remaining.push(describeAmendment(amendment));
|
|
3363
|
+
return;
|
|
3364
|
+
}
|
|
3365
|
+
const manifest2 = this.manifest ?? readJsonFile(file);
|
|
3366
|
+
const result = ensureDependency(manifest2, amendment);
|
|
3367
|
+
this.remaining.push(...result.declined);
|
|
3368
|
+
if (result.added.length === 0) {
|
|
3369
|
+
return;
|
|
3370
|
+
}
|
|
3371
|
+
this.manifest = result.value;
|
|
3372
|
+
this.manifestAdded.push(...result.added);
|
|
3373
|
+
}
|
|
3374
|
+
flushManifest() {
|
|
3375
|
+
if (this.manifestAdded.length === 0 || !this.manifest) {
|
|
3376
|
+
return;
|
|
3377
|
+
}
|
|
3378
|
+
const file = resolve2(this.workspace.root, "package.json");
|
|
3379
|
+
this.planned.push({
|
|
3380
|
+
file,
|
|
3381
|
+
display: this.displayName(file),
|
|
3382
|
+
added: this.manifestAdded,
|
|
3383
|
+
content: `${JSON.stringify(this.manifest, null, 2)}
|
|
3384
|
+
`
|
|
3385
|
+
});
|
|
3386
|
+
this.remaining.push(
|
|
3387
|
+
`Install what was just recorded in ${this.displayName(file)} (${this.manifestAdded.map((entry) => entry.replace("dependencies: ", "")).join(
|
|
3388
|
+
", "
|
|
3389
|
+
)}) \u2014 recording it is not installing it, and the build fails until you do.`
|
|
3390
|
+
);
|
|
3391
|
+
}
|
|
2794
3392
|
planBuildTarget(amendment, project) {
|
|
2795
3393
|
const target = this.buildTarget(project.name);
|
|
2796
3394
|
if (!target) {
|
|
@@ -2860,7 +3458,9 @@ var Amender = class {
|
|
|
2860
3458
|
this.planned.push({
|
|
2861
3459
|
file: root,
|
|
2862
3460
|
display: this.displayName(root),
|
|
2863
|
-
added: [
|
|
3461
|
+
added: [
|
|
3462
|
+
`${amendment.symbol}, its translations and its capability grants`
|
|
3463
|
+
],
|
|
2864
3464
|
content: result.source
|
|
2865
3465
|
});
|
|
2866
3466
|
}
|
|
@@ -2915,7 +3515,9 @@ var Amender = class {
|
|
|
2915
3515
|
return void 0;
|
|
2916
3516
|
}
|
|
2917
3517
|
entryStylesheet(project) {
|
|
2918
|
-
const styles = asObject3(
|
|
3518
|
+
const styles = asObject3(
|
|
3519
|
+
asObject3(this.buildTarget(project.name)?.value)?.["options"]
|
|
3520
|
+
)?.["styles"];
|
|
2919
3521
|
if (!Array.isArray(styles)) {
|
|
2920
3522
|
return void 0;
|
|
2921
3523
|
}
|
|
@@ -3077,7 +3679,7 @@ function replaceSymlinkEntry(absolute) {
|
|
|
3077
3679
|
}
|
|
3078
3680
|
|
|
3079
3681
|
// src/lib/run.ts
|
|
3080
|
-
var VERSION = "0.7.
|
|
3682
|
+
var VERSION = "0.7.7";
|
|
3081
3683
|
function help() {
|
|
3082
3684
|
const commands = SCAFFOLDS.map((s) => ` ${s.name.padEnd(16)}${s.summary}`);
|
|
3083
3685
|
return [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loomweaver/cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.7",
|
|
4
4
|
"description": "LoomWeaver scaffolding CLI: generates weavers, distributions and integrations into any project — no Nx workspace, no LoomWeaver checkout and no AI assistant required.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"loomweaver",
|