@signal9/era-ui 4.6.0 → 4.7.0
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/ai/error-panel/error-panel.svelte +1 -30
- package/dist/ai/index.d.ts +1 -0
- package/dist/ai/index.js +1 -0
- package/dist/ai/runtime-feed/runtime-feed-item.svelte +4 -1
- package/dist/ai/swarm/context.d.ts +26 -0
- package/dist/ai/swarm/context.js +14 -0
- package/dist/ai/swarm/index.d.ts +16 -0
- package/dist/ai/swarm/index.js +15 -0
- package/dist/ai/swarm/swarm-agent.svelte +132 -0
- package/dist/ai/swarm/swarm-agent.svelte.d.ts +16 -0
- package/dist/ai/swarm/swarm-artifact.svelte +45 -0
- package/dist/ai/swarm/swarm-artifact.svelte.d.ts +8 -0
- package/dist/ai/swarm/swarm-artifacts.svelte +42 -0
- package/dist/ai/swarm/swarm-artifacts.svelte.d.ts +15 -0
- package/dist/ai/swarm/swarm-barrier.svelte +63 -0
- package/dist/ai/swarm/swarm-barrier.svelte.d.ts +10 -0
- package/dist/ai/swarm/swarm-counts.svelte +52 -0
- package/dist/ai/swarm/swarm-counts.svelte.d.ts +13 -0
- package/dist/ai/swarm/swarm-dot.svelte +52 -0
- package/dist/ai/swarm/swarm-dot.svelte.d.ts +23 -0
- package/dist/ai/swarm/swarm-footer.svelte +63 -0
- package/dist/ai/swarm/swarm-footer.svelte.d.ts +16 -0
- package/dist/ai/swarm/swarm-grid.svelte +51 -0
- package/dist/ai/swarm/swarm-grid.svelte.d.ts +14 -0
- package/dist/ai/swarm/swarm-header.svelte +64 -0
- package/dist/ai/swarm/swarm-header.svelte.d.ts +14 -0
- package/dist/ai/swarm/swarm-node.svelte +258 -0
- package/dist/ai/swarm/swarm-node.svelte.d.ts +32 -0
- package/dist/ai/swarm/swarm-role.svelte +71 -0
- package/dist/ai/swarm/swarm-role.svelte.d.ts +17 -0
- package/dist/ai/swarm/swarm-root.svelte +55 -0
- package/dist/ai/swarm/swarm-root.svelte.d.ts +16 -0
- package/dist/ai/swarm/swarm-tree.svelte +201 -0
- package/dist/ai/swarm/swarm-tree.svelte.d.ts +25 -0
- package/dist/ai/swarm/swarm.d.ts +218 -0
- package/dist/ai/swarm/swarm.js +187 -0
- package/dist/ai/swarm/swarm.svelte.d.ts +59 -0
- package/dist/ai/swarm/swarm.svelte.js +125 -0
- package/dist/apps/llm-shell/index.d.ts +1 -1
- package/dist/apps/llm-shell/llm-shell.svelte +65 -1
- package/dist/apps/llm-shell/llm-shell.svelte.d.ts +7 -0
- package/dist/apps/llm-shell/types.d.ts +22 -0
- package/dist/dev/audit/audits/clipped-glyphs.js +7 -0
- package/dist/era-ui.css +1 -1
- package/dist/generated-docs/llm-shell.md +1 -1
- package/dist/generated-docs/llms-full.txt +53 -11
- package/dist/generated-docs/llms.txt +1 -0
- package/dist/generated-docs/manifest.json +16 -5
- package/dist/generated-docs/utilities.json +12 -2
- package/dist/generated-docs/utilities.md +51 -10
- package/dist/styles/index.css +63 -9
- package/dist/styles/themes.css +27 -15
- package/dist/ui/step/step-content.svelte +5 -34
- package/package.json +1 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live half of the swarm model: one reconnectable, reactive view of a run.
|
|
3
|
+
*
|
|
4
|
+
* The shapes and the pure projections live next door in `swarm.ts` — everything
|
|
5
|
+
* here is state, so this is the only file in the family that needs runes (and
|
|
6
|
+
* the only one a host has to construct). See that module for the model itself
|
|
7
|
+
* and for why the sequence number is the whole resumption contract.
|
|
8
|
+
*/
|
|
9
|
+
import { buildSwarmTree, summarizeSwarm } from './swarm.js';
|
|
10
|
+
/**
|
|
11
|
+
* Merge an agent patch over what is already known.
|
|
12
|
+
*
|
|
13
|
+
* Keys the patch omits keep their previous value — the wire carries patches,
|
|
14
|
+
* not replacements. `sandbox` is the case that proves the rule: a status change
|
|
15
|
+
* event that omitted the VM would blank the sandbox chip on every transition,
|
|
16
|
+
* so "absent" has to mean "unchanged" (pass `null` to say "has none").
|
|
17
|
+
*/
|
|
18
|
+
function mergeAgent(prev, next) {
|
|
19
|
+
if (!prev)
|
|
20
|
+
return next;
|
|
21
|
+
const patch = Object.fromEntries(Object.entries(next).filter(([, value]) => value !== undefined));
|
|
22
|
+
return { ...prev, ...patch };
|
|
23
|
+
}
|
|
24
|
+
function upsert(list, item) {
|
|
25
|
+
const index = list.findIndex((existing) => existing.id === item.id);
|
|
26
|
+
if (index < 0)
|
|
27
|
+
return [...list, item];
|
|
28
|
+
const next = [...list];
|
|
29
|
+
next[index] = { ...next[index], ...item };
|
|
30
|
+
return next;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A reconnectable view of one swarm.
|
|
34
|
+
*
|
|
35
|
+
* Backend-free by design: the host does the fetching and the socket handling and
|
|
36
|
+
* hands the results here. A typical wiring is
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* const swarm = new SwarmController();
|
|
40
|
+
* swarm.connection = 'loading';
|
|
41
|
+
* swarm.snapshot(await api.swarmSnapshot(id)); // sets the watermark
|
|
42
|
+
* swarm.connection = 'live';
|
|
43
|
+
* for await (const event of api.swarmStream(id, swarm.seq)) swarm.apply(event);
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* and the retry path is the same three lines — re-snapshot (never rewinding the
|
|
47
|
+
* watermark), then subscribe from `seq` again. Everything the components need
|
|
48
|
+
* (`tree`, `counts`) is derived, so nothing has to be recomputed by hand.
|
|
49
|
+
*/
|
|
50
|
+
export class SwarmController {
|
|
51
|
+
agents = $state([]);
|
|
52
|
+
plan = $state(null);
|
|
53
|
+
barriers = $state([]);
|
|
54
|
+
artifacts = $state([]);
|
|
55
|
+
/** Stream health, for `Swarm.Footer`. The host sets it; nothing here does. */
|
|
56
|
+
connection = $state('idle');
|
|
57
|
+
/** Last transport error, shown beside the watermark. */
|
|
58
|
+
error = $state(null);
|
|
59
|
+
/** The watermark: the last APPLIED event seq, and the resume point. */
|
|
60
|
+
seq = $state(-1);
|
|
61
|
+
constructor(snapshot) {
|
|
62
|
+
if (snapshot)
|
|
63
|
+
this.snapshot(snapshot);
|
|
64
|
+
}
|
|
65
|
+
tree = $derived(buildSwarmTree(this.agents));
|
|
66
|
+
counts = $derived(summarizeSwarm(this.agents));
|
|
67
|
+
/**
|
|
68
|
+
* Merge a snapshot in.
|
|
69
|
+
*
|
|
70
|
+
* The watermark only ever moves FORWARD: a refresh taken while live events
|
|
71
|
+
* are already flowing is older than the view it is refreshing, and rewinding
|
|
72
|
+
* to its seq would replay every event since.
|
|
73
|
+
*/
|
|
74
|
+
snapshot(snapshot) {
|
|
75
|
+
if (snapshot.plan !== undefined)
|
|
76
|
+
this.plan = snapshot.plan;
|
|
77
|
+
for (const agent of snapshot.agents ?? [])
|
|
78
|
+
this.#mergeAgent(agent);
|
|
79
|
+
for (const barrier of snapshot.barriers ?? [])
|
|
80
|
+
this.barriers = upsert(this.barriers, barrier);
|
|
81
|
+
for (const artifact of snapshot.artifacts ?? [])
|
|
82
|
+
this.artifacts = upsert(this.artifacts, artifact);
|
|
83
|
+
if (snapshot.seq != null && snapshot.seq > this.seq)
|
|
84
|
+
this.seq = snapshot.seq;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Apply one event. Returns false when it was already applied — a backfill
|
|
88
|
+
* that overlaps what the snapshot already covered is a no-op, not a
|
|
89
|
+
* double-apply, which is what lets the resume path be careless about overlap.
|
|
90
|
+
*/
|
|
91
|
+
apply(event) {
|
|
92
|
+
if (event.seq <= this.seq)
|
|
93
|
+
return false;
|
|
94
|
+
if (event.agent)
|
|
95
|
+
this.#mergeAgent(event.agent);
|
|
96
|
+
if (event.plan)
|
|
97
|
+
this.plan = event.plan;
|
|
98
|
+
if (event.barrier)
|
|
99
|
+
this.barriers = upsert(this.barriers, event.barrier);
|
|
100
|
+
if (event.artifact)
|
|
101
|
+
this.artifacts = upsert(this.artifacts, event.artifact);
|
|
102
|
+
this.seq = event.seq;
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
/** Drop everything — switching to a different swarm, not reconnecting to this one. */
|
|
106
|
+
reset() {
|
|
107
|
+
this.agents = [];
|
|
108
|
+
this.plan = null;
|
|
109
|
+
this.barriers = [];
|
|
110
|
+
this.artifacts = [];
|
|
111
|
+
this.connection = 'idle';
|
|
112
|
+
this.error = null;
|
|
113
|
+
this.seq = -1;
|
|
114
|
+
}
|
|
115
|
+
#mergeAgent(agent) {
|
|
116
|
+
const index = this.agents.findIndex((existing) => existing.id === agent.id);
|
|
117
|
+
if (index < 0) {
|
|
118
|
+
this.agents = [...this.agents, agent];
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const next = [...this.agents];
|
|
122
|
+
next[index] = mergeAgent(next[index], agent);
|
|
123
|
+
this.agents = next;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { default as LlmShell } from './llm-shell.svelte';
|
|
2
|
-
export type { LlmBlock, LlmConversation, LlmMessage, LlmToolChip, LlmUsage } from './types.js';
|
|
2
|
+
export type { LlmBlock, LlmConversation, LlmMessage, LlmSwarm, LlmToolChip, LlmUsage } from './types.js';
|
|
@@ -45,6 +45,8 @@
|
|
|
45
45
|
onNewChat,
|
|
46
46
|
onDelete,
|
|
47
47
|
onStop,
|
|
48
|
+
onOpenAgent,
|
|
49
|
+
onCancelAgent,
|
|
48
50
|
empty,
|
|
49
51
|
class: className
|
|
50
52
|
}: {
|
|
@@ -62,6 +64,13 @@
|
|
|
62
64
|
onNewChat?: () => void;
|
|
63
65
|
onDelete?: (id: string) => void;
|
|
64
66
|
onStop?: () => void;
|
|
67
|
+
/**
|
|
68
|
+
* Open a swarm agent's own transcript. Without it a swarm renders read-only
|
|
69
|
+
* — the button appears on agents that carry a `conversationId`.
|
|
70
|
+
*/
|
|
71
|
+
onOpenAgent?: (agent: AI.Swarm.SwarmAgent) => void;
|
|
72
|
+
/** Cancel a swarm agent. Offered only while the agent has work left to stop. */
|
|
73
|
+
onCancelAgent?: (agent: AI.Swarm.SwarmAgent) => void;
|
|
65
74
|
/** Replaces the built-in empty state for a conversation with no messages. */
|
|
66
75
|
empty?: Snippet;
|
|
67
76
|
class?: string;
|
|
@@ -70,6 +79,18 @@
|
|
|
70
79
|
const active = $derived(conversations.find((c) => c.id === activeId) ?? conversations[0]);
|
|
71
80
|
const groups = $derived(AI.Conversations.bucketConversations(conversations, now));
|
|
72
81
|
|
|
82
|
+
// Swarm agents still in flight, across every swarm block in the open thread.
|
|
83
|
+
// The run strip is about THIS conversation's work, and a delegated run is the
|
|
84
|
+
// part of it least visible in the transcript — it happens inside one block.
|
|
85
|
+
const swarmActive = $derived(
|
|
86
|
+
AI.Swarm.summarizeSwarm(
|
|
87
|
+
(active?.messages ?? [])
|
|
88
|
+
.flatMap((message) => message.blocks ?? [])
|
|
89
|
+
.filter((block) => block.kind === 'swarm')
|
|
90
|
+
.flatMap((block) => block.swarm.agents)
|
|
91
|
+
).active
|
|
92
|
+
);
|
|
93
|
+
|
|
73
94
|
// One open chip per row is plenty — the expansion area is shared, so tracking
|
|
74
95
|
// more than the current index would render two bodies into one slot.
|
|
75
96
|
let openChip = $state<string | null>(null);
|
|
@@ -122,9 +143,15 @@
|
|
|
122
143
|
total: 1,
|
|
123
144
|
running: runStatus === 'running' ? 0 : 1
|
|
124
145
|
}}
|
|
146
|
+
agents={swarmActive || undefined}
|
|
125
147
|
latest={{
|
|
126
148
|
tone: 'muted',
|
|
127
|
-
text:
|
|
149
|
+
text:
|
|
150
|
+
swarmActive > 0
|
|
151
|
+
? `${swarmActive} agents working…`
|
|
152
|
+
: runStatus === 'running'
|
|
153
|
+
? 'streaming response…'
|
|
154
|
+
: 'thinking…'
|
|
128
155
|
}}
|
|
129
156
|
oncancel={() => onStop?.()}
|
|
130
157
|
/>
|
|
@@ -249,6 +276,43 @@
|
|
|
249
276
|
/>
|
|
250
277
|
</AI.Tool.Content>
|
|
251
278
|
</AI.Tool.Root>
|
|
279
|
+
{:else if block.kind === 'swarm'}
|
|
280
|
+
<!-- A delegated run, inline in the turn that started it. The tree keeps the
|
|
281
|
+
delegation shape (who asked whom, what is still open); `view="grid"`
|
|
282
|
+
turns the same agents into the console layout for a wide pane.
|
|
283
|
+
Everything reads off block.swarm, so a live SwarmController streams
|
|
284
|
+
straight into it — the app never copies swarm state around. -->
|
|
285
|
+
{@const swarm = block.swarm}
|
|
286
|
+
{@const artifactsFor = (agent: AI.Swarm.SwarmAgent) =>
|
|
287
|
+
(swarm.artifacts ?? []).filter((a) => a.agentId === agent.id)}
|
|
288
|
+
<AI.Swarm.Root {now} onOpen={onOpenAgent} onCancel={onCancelAgent}>
|
|
289
|
+
<AI.Swarm.Header title={swarm.plan?.title} status={swarm.plan?.status}>
|
|
290
|
+
<AI.Swarm.Counts agents={swarm.agents} />
|
|
291
|
+
</AI.Swarm.Header>
|
|
292
|
+
|
|
293
|
+
{#if block.view === 'grid'}
|
|
294
|
+
<AI.Swarm.Grid agents={swarm.agents} />
|
|
295
|
+
{:else}
|
|
296
|
+
<AI.Swarm.Tree agents={swarm.agents} expandable={(agent) => artifactsFor(agent).length > 0}>
|
|
297
|
+
{#snippet detail(agent)}
|
|
298
|
+
<AI.Swarm.Artifacts artifacts={artifactsFor(agent)} />
|
|
299
|
+
{/snippet}
|
|
300
|
+
</AI.Swarm.Tree>
|
|
301
|
+
{/if}
|
|
302
|
+
|
|
303
|
+
{#if (swarm.barriers ?? []).length > 0}
|
|
304
|
+
<!-- Fan-in gates last: they are what the branches above are waiting on. -->
|
|
305
|
+
<div
|
|
306
|
+
class="flex flex-col gap-(--era-pad-sm) border-t border-divider-faded p-(--era-pad-sm)"
|
|
307
|
+
>
|
|
308
|
+
{#each swarm.barriers ?? [] as barrier (barrier.id)}
|
|
309
|
+
<AI.Swarm.Barrier {barrier} agents={swarm.agents} />
|
|
310
|
+
{/each}
|
|
311
|
+
</div>
|
|
312
|
+
{/if}
|
|
313
|
+
|
|
314
|
+
<AI.Swarm.Footer connection={swarm.connection} seq={swarm.seq} />
|
|
315
|
+
</AI.Swarm.Root>
|
|
252
316
|
{:else if block.kind === 'approval'}
|
|
253
317
|
<AI.Confirmation.Root state={approvalState}>
|
|
254
318
|
Approve <span class="font-mono text-fg">{block.toolName}</span>? Review the arguments before
|
|
@@ -35,6 +35,13 @@ type $$ComponentProps = {
|
|
|
35
35
|
onNewChat?: () => void;
|
|
36
36
|
onDelete?: (id: string) => void;
|
|
37
37
|
onStop?: () => void;
|
|
38
|
+
/**
|
|
39
|
+
* Open a swarm agent's own transcript. Without it a swarm renders read-only
|
|
40
|
+
* — the button appears on agents that carry a `conversationId`.
|
|
41
|
+
*/
|
|
42
|
+
onOpenAgent?: (agent: AI.Swarm.SwarmAgent) => void;
|
|
43
|
+
/** Cancel a swarm agent. Offered only while the agent has work left to stop. */
|
|
44
|
+
onCancelAgent?: (agent: AI.Swarm.SwarmAgent) => void;
|
|
38
45
|
/** Replaces the built-in empty state for a conversation with no messages. */
|
|
39
46
|
empty?: Snippet;
|
|
40
47
|
class?: string;
|
|
@@ -3,6 +3,7 @@ import type { IconProps } from '@lucide/svelte';
|
|
|
3
3
|
import type { ChatStatus, ConversationStatus, RunState, ToolState, UsageBreakdown } from '../../ai/index.js';
|
|
4
4
|
import type { ApprovalController } from '../../ai/confirmation/index.js';
|
|
5
5
|
import type { RuntimeFeedItemData } from '../../ai/runtime-feed/index.js';
|
|
6
|
+
import type { SwarmAgent, SwarmArtifact, SwarmBarrier, SwarmConnection, SwarmPlan } from '../../ai/swarm/index.js';
|
|
6
7
|
/** Per-turn cost footer under a finished answer. */
|
|
7
8
|
export type LlmUsage = {
|
|
8
9
|
tokensIn: number;
|
|
@@ -20,6 +21,22 @@ export type LlmToolChip = {
|
|
|
20
21
|
input?: unknown;
|
|
21
22
|
output?: unknown;
|
|
22
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* Where a swarm block reads its state.
|
|
26
|
+
*
|
|
27
|
+
* Deliberately structural rather than a class: a `SwarmController` satisfies it
|
|
28
|
+
* as-is (so a live, streaming swarm is `{ kind: 'swarm', swarm: controller }`
|
|
29
|
+
* and stays reactive), and a host with static state passes a plain object with
|
|
30
|
+
* the same fields. Neither has to know about the other.
|
|
31
|
+
*/
|
|
32
|
+
export type LlmSwarm = {
|
|
33
|
+
agents: SwarmAgent[];
|
|
34
|
+
plan?: SwarmPlan | null;
|
|
35
|
+
barriers?: SwarmBarrier[];
|
|
36
|
+
artifacts?: SwarmArtifact[];
|
|
37
|
+
connection?: SwarmConnection;
|
|
38
|
+
seq?: number;
|
|
39
|
+
};
|
|
23
40
|
/**
|
|
24
41
|
* Everything an assistant turn can contain.
|
|
25
42
|
*
|
|
@@ -49,6 +66,11 @@ export type LlmBlock = {
|
|
|
49
66
|
stdout?: string;
|
|
50
67
|
stderr?: string;
|
|
51
68
|
exitCode?: number;
|
|
69
|
+
} | {
|
|
70
|
+
kind: 'swarm';
|
|
71
|
+
swarm: LlmSwarm;
|
|
72
|
+
/** `tree` (default) keeps the delegation shape; `grid` is the console view. */
|
|
73
|
+
view?: 'tree' | 'grid';
|
|
52
74
|
} | {
|
|
53
75
|
kind: 'approval';
|
|
54
76
|
toolName: string;
|
|
@@ -36,6 +36,13 @@ export const clippedGlyphs = {
|
|
|
36
36
|
const s = getComputedStyle(el);
|
|
37
37
|
if (!/hidden|clip|auto|scroll/.test(s.overflowX + s.overflowY))
|
|
38
38
|
return null;
|
|
39
|
+
// `normal` IS the ink box: the UA sizes the line from the font's own
|
|
40
|
+
// ascent + descent, so the box cannot be shorter than what the font paints,
|
|
41
|
+
// whatever ratio that works out to (a serif at 14px lands at 16px = 1.14em,
|
|
42
|
+
// under the ratio below and perfectly safe). Only an AUTHORED line-height
|
|
43
|
+
// can starve the box, so only an authored one is measured.
|
|
44
|
+
if (s.lineHeight === 'normal')
|
|
45
|
+
return null;
|
|
39
46
|
const fontSize = parseFloat(s.fontSize) || 14;
|
|
40
47
|
const h = el.getBoundingClientRect().height;
|
|
41
48
|
if (h === 0)
|