@signal9/era-ui 4.6.1 → 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.
Files changed (53) hide show
  1. package/dist/ai/error-panel/error-panel.svelte +1 -30
  2. package/dist/ai/index.d.ts +1 -0
  3. package/dist/ai/index.js +1 -0
  4. package/dist/ai/runtime-feed/runtime-feed-item.svelte +4 -1
  5. package/dist/ai/swarm/context.d.ts +26 -0
  6. package/dist/ai/swarm/context.js +14 -0
  7. package/dist/ai/swarm/index.d.ts +16 -0
  8. package/dist/ai/swarm/index.js +15 -0
  9. package/dist/ai/swarm/swarm-agent.svelte +132 -0
  10. package/dist/ai/swarm/swarm-agent.svelte.d.ts +16 -0
  11. package/dist/ai/swarm/swarm-artifact.svelte +45 -0
  12. package/dist/ai/swarm/swarm-artifact.svelte.d.ts +8 -0
  13. package/dist/ai/swarm/swarm-artifacts.svelte +42 -0
  14. package/dist/ai/swarm/swarm-artifacts.svelte.d.ts +15 -0
  15. package/dist/ai/swarm/swarm-barrier.svelte +63 -0
  16. package/dist/ai/swarm/swarm-barrier.svelte.d.ts +10 -0
  17. package/dist/ai/swarm/swarm-counts.svelte +52 -0
  18. package/dist/ai/swarm/swarm-counts.svelte.d.ts +13 -0
  19. package/dist/ai/swarm/swarm-dot.svelte +52 -0
  20. package/dist/ai/swarm/swarm-dot.svelte.d.ts +23 -0
  21. package/dist/ai/swarm/swarm-footer.svelte +63 -0
  22. package/dist/ai/swarm/swarm-footer.svelte.d.ts +16 -0
  23. package/dist/ai/swarm/swarm-grid.svelte +51 -0
  24. package/dist/ai/swarm/swarm-grid.svelte.d.ts +14 -0
  25. package/dist/ai/swarm/swarm-header.svelte +64 -0
  26. package/dist/ai/swarm/swarm-header.svelte.d.ts +14 -0
  27. package/dist/ai/swarm/swarm-node.svelte +258 -0
  28. package/dist/ai/swarm/swarm-node.svelte.d.ts +32 -0
  29. package/dist/ai/swarm/swarm-role.svelte +71 -0
  30. package/dist/ai/swarm/swarm-role.svelte.d.ts +17 -0
  31. package/dist/ai/swarm/swarm-root.svelte +55 -0
  32. package/dist/ai/swarm/swarm-root.svelte.d.ts +16 -0
  33. package/dist/ai/swarm/swarm-tree.svelte +201 -0
  34. package/dist/ai/swarm/swarm-tree.svelte.d.ts +25 -0
  35. package/dist/ai/swarm/swarm.d.ts +218 -0
  36. package/dist/ai/swarm/swarm.js +187 -0
  37. package/dist/ai/swarm/swarm.svelte.d.ts +59 -0
  38. package/dist/ai/swarm/swarm.svelte.js +125 -0
  39. package/dist/apps/llm-shell/index.d.ts +1 -1
  40. package/dist/apps/llm-shell/llm-shell.svelte +65 -1
  41. package/dist/apps/llm-shell/llm-shell.svelte.d.ts +7 -0
  42. package/dist/apps/llm-shell/types.d.ts +22 -0
  43. package/dist/era-ui.css +1 -1
  44. package/dist/generated-docs/llm-shell.md +1 -1
  45. package/dist/generated-docs/llms-full.txt +36 -1
  46. package/dist/generated-docs/llms.txt +1 -0
  47. package/dist/generated-docs/manifest.json +14 -3
  48. package/dist/generated-docs/utilities.json +10 -0
  49. package/dist/generated-docs/utilities.md +34 -0
  50. package/dist/styles/index.css +47 -0
  51. package/dist/styles/themes.css +27 -15
  52. package/dist/ui/step/step-content.svelte +5 -34
  53. package/package.json +1 -1
@@ -0,0 +1,63 @@
1
+ <script lang="ts" module>
2
+ import type { SwarmConnection } from './swarm.js';
3
+
4
+ /**
5
+ * Stream health, which is NOT swarm health — the two are shown apart on
6
+ * purpose (the header has the plan's dot, this has the socket's). A swarm can
7
+ * be running perfectly behind a dead connection, and a console that paints
8
+ * one dot for both tells you the run stalled when only your view did.
9
+ */
10
+ const CONNECTION: Record<SwarmConnection, { class: string; dot: string; label: string }> = {
11
+ idle: { class: 'text-muted', dot: 'bg-muted', label: 'idle' },
12
+ loading: { class: 'text-muted', dot: 'bg-muted animate-pulse', label: 'loading' },
13
+ live: { class: 'text-success', dot: 'bg-success', label: 'live' },
14
+ reconnecting: { class: 'text-warning', dot: 'bg-warning animate-pulse', label: 'reconnecting' },
15
+ error: { class: 'text-destructive', dot: 'bg-destructive', label: 'disconnected' }
16
+ };
17
+ </script>
18
+
19
+ <script lang="ts">
20
+ import type { HTMLAttributes } from 'svelte/elements';
21
+ import { cn } from '../../utils/index.js';
22
+
23
+ let {
24
+ connection = 'idle',
25
+ seq,
26
+ error,
27
+ class: className,
28
+ ...restProps
29
+ }: HTMLAttributes<HTMLDivElement> & {
30
+ connection?: SwarmConnection;
31
+ /**
32
+ * The applied event watermark. Showing it is not debug output: it is the
33
+ * resume point, and seeing it advance is what tells you a quiet swarm is
34
+ * still connected rather than stuck.
35
+ */
36
+ seq?: number;
37
+ /** Last transport error. Shown beside the watermark, never in place of it. */
38
+ error?: string | null;
39
+ } = $props();
40
+
41
+ const tone = $derived(CONNECTION[connection]);
42
+ </script>
43
+
44
+ <div
45
+ class={cn(
46
+ 'flex h-(--era-h-md) shrink-0 items-center gap-(--era-gap) border-t border-divider-faded px-(--era-inset-md) font-mono text-body era-text-trim',
47
+ className
48
+ )}
49
+ {...restProps}
50
+ >
51
+ <span class={cn('flex shrink-0 items-center gap-(--era-gap)', tone.class)}>
52
+ <span class={cn('size-2 shrink-0 rounded-full', tone.dot)} aria-hidden="true"></span>
53
+ {tone.label}
54
+ </span>
55
+ {#if error}
56
+ <span class="min-w-0 flex-1 truncate text-destructive" title={error}>{error}</span>
57
+ {:else}
58
+ <span class="flex-1"></span>
59
+ {/if}
60
+ {#if seq != null && seq >= 0}
61
+ <span class="shrink-0 text-muted tabular-nums">seq {seq}</span>
62
+ {/if}
63
+ </div>
@@ -0,0 +1,16 @@
1
+ import type { SwarmConnection } from './swarm.js';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ type $$ComponentProps = HTMLAttributes<HTMLDivElement> & {
4
+ connection?: SwarmConnection;
5
+ /**
6
+ * The applied event watermark. Showing it is not debug output: it is the
7
+ * resume point, and seeing it advance is what tells you a quiet swarm is
8
+ * still connected rather than stuck.
9
+ */
10
+ seq?: number;
11
+ /** Last transport error. Shown beside the watermark, never in place of it. */
12
+ error?: string | null;
13
+ };
14
+ declare const SwarmFooter: import("svelte").Component<$$ComponentProps, {}, "">;
15
+ type SwarmFooter = ReturnType<typeof SwarmFooter>;
16
+ export default SwarmFooter;
@@ -0,0 +1,51 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import { cn } from '../../utils/index.js';
5
+ import Agent from './swarm-agent.svelte';
6
+ import { buildSwarmTree, flattenSwarmTree, type SwarmAgent } from './swarm.js';
7
+
8
+ let {
9
+ agents = [],
10
+ children,
11
+ empty,
12
+ class: className,
13
+ ...restProps
14
+ }: Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
15
+ /** The swarm's agents, flat. Cards are ordered shallowest-first. */
16
+ agents?: SwarmAgent[];
17
+ /** Extra card body, forwarded to every `Swarm.Agent`. */
18
+ children?: Snippet<[SwarmAgent]>;
19
+ /** Replaces the built-in "no agents yet" line. */
20
+ empty?: Snippet;
21
+ } = $props();
22
+
23
+ // Depth-first from the roots: the grid loses the tree's shape, so the order
24
+ // is the only thing left that says who delegated whom — a parent immediately
25
+ // followed by its branch reads far better than arrival order, where a
26
+ // late-spawned child lands twenty cards from its parent. The `d2` tag on each
27
+ // card carries the rest.
28
+ const nodes = $derived(flattenSwarmTree(buildSwarmTree(agents)));
29
+ </script>
30
+
31
+ <!-- The console view: every agent at once, one card each.
32
+ `auto-fill` + a density-derived minimum means the column count follows the
33
+ container's actual width — no breakpoints to keep in sync, and the cards
34
+ grow with the density axis instead of staying dense-sized in touch mode. -->
35
+ <div
36
+ class={cn(
37
+ 'grid min-w-0 grid-cols-[repeat(auto-fill,minmax(calc(var(--era-h-lg)*6),1fr))] gap-(--era-gap) p-(--era-gap)',
38
+ className
39
+ )}
40
+ {...restProps}
41
+ >
42
+ {#if nodes.length === 0}
43
+ <div class="px-(--era-inset-md) py-(--era-gap) text-body text-muted">
44
+ {#if empty}{@render empty()}{:else}No agents yet.{/if}
45
+ </div>
46
+ {:else}
47
+ {#each nodes as node (node.agent.id)}
48
+ <Agent agent={node.agent} depth={node.depth} {children} />
49
+ {/each}
50
+ {/if}
51
+ </div>
@@ -0,0 +1,14 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { type SwarmAgent } from './swarm.js';
4
+ type $$ComponentProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
5
+ /** The swarm's agents, flat. Cards are ordered shallowest-first. */
6
+ agents?: SwarmAgent[];
7
+ /** Extra card body, forwarded to every `Swarm.Agent`. */
8
+ children?: Snippet<[SwarmAgent]>;
9
+ /** Replaces the built-in "no agents yet" line. */
10
+ empty?: Snippet;
11
+ };
12
+ declare const SwarmGrid: import("svelte").Component<$$ComponentProps, {}, "">;
13
+ type SwarmGrid = ReturnType<typeof SwarmGrid>;
14
+ export default SwarmGrid;
@@ -0,0 +1,64 @@
1
+ <script lang="ts" module>
2
+ import type { SwarmAgentStatus, SwarmPlanStatus } from './swarm.js';
3
+
4
+ /**
5
+ * The plan's lifecycle borrows the agent dot's vocabulary — one tone system
6
+ * for the whole family, so "this is still working" looks identical whether it
7
+ * is one agent or the plan over all of them.
8
+ */
9
+ const PLAN_DOT: Record<SwarmPlanStatus, { status: SwarmAgentStatus; label: string }> = {
10
+ active: { status: 'running', label: 'Active' },
11
+ done: { status: 'done', label: 'Done' },
12
+ failed: { status: 'failed', label: 'Failed' },
13
+ cancelled: { status: 'cancelled', label: 'Cancelled' }
14
+ };
15
+ </script>
16
+
17
+ <script lang="ts">
18
+ import type { Snippet } from 'svelte';
19
+ import type { HTMLAttributes } from 'svelte/elements';
20
+ import { cn } from '../../utils/index.js';
21
+ import Dot from './swarm-dot.svelte';
22
+
23
+ let {
24
+ title,
25
+ status,
26
+ children,
27
+ class: className,
28
+ ...restProps
29
+ }: HTMLAttributes<HTMLDivElement> & {
30
+ /** The plan's title — what the swarm was spun up to do. */
31
+ title?: string;
32
+ /** Plan lifecycle; drives the leading dot. Omit for no dot. */
33
+ status?: SwarmPlanStatus;
34
+ /** Trailing content — `Swarm.Counts`, a cancel button, a view switch. */
35
+ children?: Snippet;
36
+ } = $props();
37
+
38
+ const plan = $derived(status ? PLAN_DOT[status] : null);
39
+ </script>
40
+
41
+ <!-- The swarm's own line: what it is doing, and how much of it is left. The
42
+ title truncates and the trailing group does not — on a narrow pane you lose
43
+ the end of a sentence you can hover for, never the count of what failed. -->
44
+ <div
45
+ class={cn(
46
+ 'flex h-(--era-h-md) shrink-0 items-center gap-(--era-gap) px-(--era-inset-md) text-body era-text-trim',
47
+ className
48
+ )}
49
+ {...restProps}
50
+ >
51
+ {#if plan}
52
+ <Dot status={plan.status} label={plan.label} />
53
+ {/if}
54
+ {#if title}
55
+ <span class="min-w-0 flex-1 truncate text-bright">{title}</span>
56
+ {:else}
57
+ <span class="flex-1"></span>
58
+ {/if}
59
+ {#if children}
60
+ <div class="flex shrink-0 items-center gap-(--era-gap)">
61
+ {@render children()}
62
+ </div>
63
+ {/if}
64
+ </div>
@@ -0,0 +1,14 @@
1
+ import type { SwarmPlanStatus } from './swarm.js';
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ type $$ComponentProps = HTMLAttributes<HTMLDivElement> & {
5
+ /** The plan's title — what the swarm was spun up to do. */
6
+ title?: string;
7
+ /** Plan lifecycle; drives the leading dot. Omit for no dot. */
8
+ status?: SwarmPlanStatus;
9
+ /** Trailing content — `Swarm.Counts`, a cancel button, a view switch. */
10
+ children?: Snippet;
11
+ };
12
+ declare const SwarmHeader: import("svelte").Component<$$ComponentProps, {}, "">;
13
+ type SwarmHeader = ReturnType<typeof SwarmHeader>;
14
+ export default SwarmHeader;
@@ -0,0 +1,258 @@
1
+ <script lang="ts" module>
2
+ /** How a sandbox's health tints the VM glyph on a row. */
3
+ const SANDBOX_TONE: Record<string, string> = {
4
+ running: 'text-success',
5
+ paused: 'text-warning',
6
+ terminated: 'text-muted'
7
+ };
8
+ </script>
9
+
10
+ <script lang="ts">
11
+ import type { Snippet } from 'svelte';
12
+ import { Collapsible } from 'bits-ui';
13
+ import ChevronRight from '@lucide/svelte/icons/chevron-right';
14
+ import Box from '@lucide/svelte/icons/box';
15
+ import X from '@lucide/svelte/icons/x';
16
+ import PanelRightOpen from '@lucide/svelte/icons/panel-right-open';
17
+ import { Button } from '../../ui/button/index.js';
18
+ import { cn } from '../../utils/index.js';
19
+ import { getSwarmContext } from './context.js';
20
+ import Dot from './swarm-dot.svelte';
21
+ import Role from './swarm-role.svelte';
22
+ import { formatSwarmDuration, swarmElapsed, type SwarmAgent } from './swarm.js';
23
+
24
+ let {
25
+ agent,
26
+ level = 1,
27
+ open = $bindable(true),
28
+ active = false,
29
+ expandable,
30
+ detail,
31
+ meta,
32
+ children,
33
+ onOpen,
34
+ onCancel,
35
+ onToggle,
36
+ class: className,
37
+ ...restProps
38
+ }: Omit<Collapsible.RootProps, 'children' | 'child' | 'open'> & {
39
+ agent: SwarmAgent;
40
+ /** Depth in the tree, 1-based — announced as aria-level. `Swarm.Tree` sets it. */
41
+ level?: number;
42
+ /** Expanded: shows `detail` and the child group. Bindable. */
43
+ open?: boolean;
44
+ /** The tree's focused row — holds the seated highlight. */
45
+ active?: boolean;
46
+ /**
47
+ * Force the chevron on or off. Defaults to "has children or a detail
48
+ * snippet" — override it when the detail snippet renders nothing for THIS
49
+ * agent (a worker with no artifacts yet), which the node cannot see.
50
+ */
51
+ expandable?: boolean;
52
+ /** Expanded body for this agent: its artifacts, transcript, output. */
53
+ detail?: Snippet<[SwarmAgent]>;
54
+ /** Trailing row content — a count, a chip, anything the host tracks. */
55
+ meta?: Snippet<[SwarmAgent]>;
56
+ /** Nested `Swarm.Node`s. Their presence is what makes the row expandable. */
57
+ children?: Snippet;
58
+ /** Overrides the Root's handler for this row. */
59
+ onOpen?: (agent: SwarmAgent) => void;
60
+ onCancel?: (agent: SwarmAgent) => void;
61
+ /** Fired after the row expands or collapses. */
62
+ onToggle?: (open: boolean) => void;
63
+ } = $props();
64
+
65
+ const ctx = getSwarmContext();
66
+ const openAgent = $derived(onOpen ?? ctx.onOpen);
67
+ const cancelAgent = $derived(onCancel ?? ctx.onCancel);
68
+
69
+ const canExpand = $derived(expandable ?? (children != null || detail != null));
70
+ const running = $derived(agent.status === 'running');
71
+ // Cancel is offered for work that has not landed — a done or failed agent has
72
+ // nothing left to stop, and showing a dead button on every finished row is
73
+ // how a long run turns into a wall of noise.
74
+ const cancellable = $derived(
75
+ cancelAgent != null &&
76
+ (agent.status === 'running' || agent.status === 'queued' || agent.status === 'blocked')
77
+ );
78
+ // No transcript, no button: `conversationId` is the host's way of saying this
79
+ // agent has something to open, and a row full of buttons that open nothing is
80
+ // worse than a row without them.
81
+ const openable = $derived(openAgent != null && agent.conversationId != null);
82
+ const hasActions = $derived(cancellable || openable);
83
+
84
+ const elapsed = $derived(swarmElapsed(agent, ctx.now));
85
+
86
+ function toggle() {
87
+ if (!canExpand) return;
88
+ open = !open;
89
+ onToggle?.(open);
90
+ }
91
+
92
+ /**
93
+ * Activation only. Navigation (arrows, Home/End) belongs to `Swarm.Tree`,
94
+ * which is the thing that knows the visible row order — and the target check
95
+ * is what keeps a child's keystroke from also toggling every ancestor it
96
+ * bubbles through.
97
+ */
98
+ function onkeydown(event: KeyboardEvent) {
99
+ if (event.target !== event.currentTarget) return;
100
+ if (event.key === 'Enter') {
101
+ event.preventDefault();
102
+ if (openAgent) openAgent(agent);
103
+ else toggle();
104
+ } else if (event.key === ' ') {
105
+ event.preventDefault();
106
+ toggle();
107
+ }
108
+ }
109
+
110
+ /**
111
+ * The click lands on the treeitem, not on the row inside it: the treeitem is
112
+ * the element with the role, the keyboard handler and the focus, and putting
113
+ * the pointer on the same element is what keeps those from drifting apart.
114
+ * The closest() check is the tree equivalent of the keydown target check — a
115
+ * click on a nested row bubbles through every ancestor, and without it
116
+ * opening a leaf would collapse its whole lineage on the way up.
117
+ */
118
+ function onclick(event: MouseEvent) {
119
+ const row = (event.target as HTMLElement | null)?.closest?.('[data-agent-id]');
120
+ if (row !== event.currentTarget) return;
121
+ // Safari does not focus a tabindex=-1 element on click; the tree's roving
122
+ // focus depends on it, so ask for it explicitly.
123
+ (event.currentTarget as HTMLElement).focus();
124
+ toggle();
125
+ }
126
+ </script>
127
+
128
+ <!-- One agent in the delegation tree.
129
+ The treeitem is the Collapsible root, so `data-state` (and therefore the
130
+ chevron's rotation and the body's height sweep) comes from the same element
131
+ that carries aria-expanded — one source of truth for "is this row open",
132
+ which is what stops the arrow from ever disagreeing with the body. -->
133
+ <Collapsible.Root
134
+ bind:open
135
+ role="treeitem"
136
+ aria-level={level}
137
+ aria-expanded={canExpand ? open : undefined}
138
+ data-agent-id={agent.id}
139
+ data-active={active || undefined}
140
+ tabindex={-1}
141
+ {onkeydown}
142
+ {onclick}
143
+ class={cn('group/node min-w-0 outline-none', className)}
144
+ {...restProps}
145
+ >
146
+ <!-- The row. No transition on the highlight: this is a list row, and every
147
+ list in the library (select, menu, command, conversations) takes its
148
+ highlight instantly so a fast pass down the tree doesn't smear. -->
149
+ <div
150
+ class={cn(
151
+ 'flex h-(--era-h-md) items-center gap-(--era-gap) rounded-(--era-rd-md) pl-(--era-inset-md) text-body era-text-trim',
152
+ hasActions ? 'pr-(--era-xxs-inset-md)' : 'pr-(--era-inset-md)',
153
+ canExpand && 'cursor-pointer',
154
+ active
155
+ ? 'bg-hover text-bright'
156
+ : 'text-fg hover:bg-(--era-highlight) hover:shadow-(--era-highlight-shadow)',
157
+ 'group-focus-visible/node:outline group-focus-visible/node:-outline-offset-1 group-focus-visible/node:outline-primary'
158
+ )}
159
+ >
160
+ {#if canExpand}
161
+ <ChevronRight
162
+ class="size-(--era-h-xs) shrink-0 text-muted transition-transform duration-(--era-duration) ease-(--era-ease) group-data-[state=open]/node:rotate-90"
163
+ aria-hidden="true"
164
+ />
165
+ {:else}
166
+ <!-- Keeps every row's dot on the same column, expandable or not. -->
167
+ <span class="size-(--era-h-xs) shrink-0" aria-hidden="true"></span>
168
+ {/if}
169
+
170
+ <Dot status={agent.status} />
171
+ <Role role={agent.role ?? 'worker'} label={agent.label} />
172
+
173
+ {#if agent.key}
174
+ <span class="shrink-0 font-mono text-body text-muted">#{agent.key}</span>
175
+ {/if}
176
+
177
+ <!-- The task shimmers while the agent works — the same in-progress
178
+ treatment a streaming label gets everywhere else in the suite, and it
179
+ freezes to plain text at instant motion. -->
180
+ <span class={cn('min-w-0 flex-1 truncate', running && 'era-shimmer')}>{agent.task}</span>
181
+
182
+ {#if agent.result}
183
+ <span class="hidden max-w-[45%] min-w-0 truncate text-muted @md:block" title={agent.result}>
184
+ {agent.result}
185
+ </span>
186
+ {/if}
187
+
188
+ {@render meta?.(agent)}
189
+
190
+ {#if agent.sandbox}
191
+ <span
192
+ class={cn('flex shrink-0 items-center', SANDBOX_TONE[agent.sandbox.status] ?? 'text-muted')}
193
+ title="sandbox {agent.sandbox.id ?? ''} ({agent.sandbox.status})"
194
+ >
195
+ <Box class="size-(--era-h-xs)" aria-hidden="true" />
196
+ </span>
197
+ {/if}
198
+
199
+ {#if elapsed != null}
200
+ <span class="shrink-0 font-mono text-body text-muted tabular-nums">
201
+ {formatSwarmDuration(elapsed)}
202
+ </span>
203
+ {/if}
204
+
205
+ {#if cancellable}
206
+ <Button
207
+ icon
208
+ size="xxs"
209
+ tone="destructive"
210
+ aria-label="Cancel {agent.task}"
211
+ onclick={(event: MouseEvent) => {
212
+ event.stopPropagation();
213
+ cancelAgent?.(agent);
214
+ }}
215
+ >
216
+ <X class="size-(--era-h-xs)" />
217
+ </Button>
218
+ {/if}
219
+
220
+ {#if openable}
221
+ <Button
222
+ icon
223
+ size="xxs"
224
+ aria-label="Open {agent.task}"
225
+ onclick={(event: MouseEvent) => {
226
+ event.stopPropagation();
227
+ openAgent?.(agent);
228
+ }}
229
+ >
230
+ <PanelRightOpen class="size-(--era-h-xs)" />
231
+ </Button>
232
+ {/if}
233
+ </div>
234
+
235
+ {#if canExpand}
236
+ <Collapsible.Content class="era-collapse">
237
+ {#if detail}
238
+ <!-- Indented to where the row's text starts (chevron + gap), so the
239
+ body reads as this agent's output rather than a sibling row. -->
240
+ <div
241
+ class="min-w-0 pr-(--era-inset-md) pb-(--era-gap) pl-[calc(var(--era-inset-md)+var(--era-h-xs)+var(--era-gap))]"
242
+ >
243
+ {@render detail(agent)}
244
+ </div>
245
+ {/if}
246
+ {#if children}
247
+ <!-- The rail lands on the chevron's centre line, so the connector runs
248
+ straight down from the parent's own arrow into its children. -->
249
+ <div
250
+ role="group"
251
+ class="ml-[calc(var(--era-inset-md)+var(--era-h-xs)/2)] border-l border-divider-faded"
252
+ >
253
+ {@render children()}
254
+ </div>
255
+ {/if}
256
+ </Collapsible.Content>
257
+ {/if}
258
+ </Collapsible.Root>
@@ -0,0 +1,32 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { Collapsible } from 'bits-ui';
3
+ import { type SwarmAgent } from './swarm.js';
4
+ type $$ComponentProps = Omit<Collapsible.RootProps, 'children' | 'child' | 'open'> & {
5
+ agent: SwarmAgent;
6
+ /** Depth in the tree, 1-based — announced as aria-level. `Swarm.Tree` sets it. */
7
+ level?: number;
8
+ /** Expanded: shows `detail` and the child group. Bindable. */
9
+ open?: boolean;
10
+ /** The tree's focused row — holds the seated highlight. */
11
+ active?: boolean;
12
+ /**
13
+ * Force the chevron on or off. Defaults to "has children or a detail
14
+ * snippet" — override it when the detail snippet renders nothing for THIS
15
+ * agent (a worker with no artifacts yet), which the node cannot see.
16
+ */
17
+ expandable?: boolean;
18
+ /** Expanded body for this agent: its artifacts, transcript, output. */
19
+ detail?: Snippet<[SwarmAgent]>;
20
+ /** Trailing row content — a count, a chip, anything the host tracks. */
21
+ meta?: Snippet<[SwarmAgent]>;
22
+ /** Nested `Swarm.Node`s. Their presence is what makes the row expandable. */
23
+ children?: Snippet;
24
+ /** Overrides the Root's handler for this row. */
25
+ onOpen?: (agent: SwarmAgent) => void;
26
+ onCancel?: (agent: SwarmAgent) => void;
27
+ /** Fired after the row expands or collapses. */
28
+ onToggle?: (open: boolean) => void;
29
+ };
30
+ declare const SwarmNode: import("svelte").Component<$$ComponentProps, {}, "open">;
31
+ type SwarmNode = ReturnType<typeof SwarmNode>;
32
+ export default SwarmNode;
@@ -0,0 +1,71 @@
1
+ <script lang="ts" module>
2
+ import type { Component } from 'svelte';
3
+ import type { IconProps } from '@lucide/svelte';
4
+ import Network from '@lucide/svelte/icons/network';
5
+ import Bot from '@lucide/svelte/icons/bot';
6
+ import Eye from '@lucide/svelte/icons/eye';
7
+ import ShieldCheck from '@lucide/svelte/icons/shield-check';
8
+ import GitMerge from '@lucide/svelte/icons/git-merge';
9
+ import type { SwarmRole } from './swarm.js';
10
+
11
+ /**
12
+ * Role → glyph. A role is a CATEGORY, and categories are read faster as
13
+ * shapes than as colour — which also leaves the palette free to mean
14
+ * lifecycle and nothing else (see swarm-dot). The orchestrator's fan-out
15
+ * glyph is the one that has to be recognisable at a glance in a big tree:
16
+ * it is the node whose children are the rest of the swarm.
17
+ */
18
+ export const SWARM_ROLE: Record<SwarmRole, { icon: Component<IconProps>; label: string }> = {
19
+ orchestrator: { icon: Network, label: 'orchestrator' },
20
+ worker: { icon: Bot, label: 'worker' },
21
+ reviewer: { icon: Eye, label: 'reviewer' },
22
+ verifier: { icon: ShieldCheck, label: 'verifier' },
23
+ aggregator: { icon: GitMerge, label: 'aggregator' }
24
+ };
25
+ </script>
26
+
27
+ <script lang="ts">
28
+ import type { HTMLAttributes } from 'svelte/elements';
29
+ import { cn } from '../../utils/index.js';
30
+
31
+ let {
32
+ role = 'worker',
33
+ label,
34
+ short = false,
35
+ class: className,
36
+ ...restProps
37
+ }: HTMLAttributes<HTMLSpanElement> & {
38
+ role?: SwarmRole;
39
+ /** Specialization shown after the role ("api", "docs") — the agent's `label`. */
40
+ label?: string;
41
+ /** Glyph only, with the role kept as the hover/announced name. For dense rows. */
42
+ short?: boolean;
43
+ } = $props();
44
+
45
+ const spec = $derived(SWARM_ROLE[role]);
46
+ const Icon = $derived(spec.icon);
47
+ const name = $derived(label ? `${spec.label} · ${label}` : spec.label);
48
+ </script>
49
+
50
+ <!-- The role tag: glyph plus an ALL-CAPS name, quiet by default so the task
51
+ text stays the loudest thing in the row. era-text-trim-caps (not the
52
+ mixed-case trim) because the string is caps-only — the x-height band is the
53
+ wrong box for it and would seat the tag high. The orchestrator reads at
54
+ full foreground: in a tree of forty workers it is the one row you look for. -->
55
+ <span
56
+ data-role={role}
57
+ title={name}
58
+ class={cn(
59
+ 'inline-flex shrink-0 items-center gap-(--era-gap) font-mono text-[0.625rem] era-text-trim-caps tracking-[0.12em] uppercase',
60
+ role === 'orchestrator' ? 'text-fg' : 'text-muted',
61
+ className
62
+ )}
63
+ {...restProps}
64
+ >
65
+ <Icon class="size-(--era-h-xs) shrink-0" aria-hidden="true" />
66
+ {#if short}
67
+ <span class="sr-only">{name}</span>
68
+ {:else}
69
+ {name}
70
+ {/if}
71
+ </span>
@@ -0,0 +1,17 @@
1
+ import type { Component } from 'svelte';
2
+ import type { IconProps } from '@lucide/svelte';
3
+ import type { SwarmRole } from './swarm.js';
4
+ /**
5
+ * Role → glyph. A role is a CATEGORY, and categories are read faster as
6
+ * shapes than as colour — which also leaves the palette free to mean
7
+ * lifecycle and nothing else (see swarm-dot). The orchestrator's fan-out
8
+ * glyph is the one that has to be recognisable at a glance in a big tree:
9
+ * it is the node whose children are the rest of the swarm.
10
+ */
11
+ export declare const SWARM_ROLE: Record<SwarmRole, {
12
+ icon: Component<IconProps>;
13
+ label: string;
14
+ }>;
15
+ declare const SwarmRole: any;
16
+ type SwarmRole = ReturnType<typeof SwarmRole>;
17
+ export default SwarmRole;
@@ -0,0 +1,55 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import { cn } from '../../utils/index.js';
5
+ import { setSwarmContext } from './context.js';
6
+ import type { SwarmAgent } from './swarm.js';
7
+
8
+ let {
9
+ ref = $bindable(null),
10
+ now = Date.now(),
11
+ onOpen,
12
+ onCancel,
13
+ children,
14
+ class: className,
15
+ ...restProps
16
+ }: HTMLAttributes<HTMLDivElement> & {
17
+ ref?: HTMLDivElement | null;
18
+ /** Epoch ms for every elapsed readout. Tick it in `$state` for live counters. */
19
+ now?: number;
20
+ /** Open an agent's transcript. Reaches every node without prop drilling. */
21
+ onOpen?: (agent: SwarmAgent) => void;
22
+ /** Cancel an agent. Nodes show the affordance only while one is still active. */
23
+ onCancel?: (agent: SwarmAgent) => void;
24
+ children?: Snippet;
25
+ } = $props();
26
+
27
+ // Getters, not values: the context is read during child render, so a plain
28
+ // snapshot would freeze `now` at mount and every counter would stop.
29
+ setSwarmContext({
30
+ get now() {
31
+ return now;
32
+ },
33
+ get onOpen() {
34
+ return onOpen;
35
+ },
36
+ get onCancel() {
37
+ return onCancel;
38
+ }
39
+ });
40
+ </script>
41
+
42
+ <!-- The swarm panel: one surface holding a header, a tree or a card grid, and
43
+ the stream footer. Level (bg-surface) rather than elevated, because it
44
+ usually sits INSIDE a message — the thread is the elevated thing, and a
45
+ swarm reads as a region of it, not a card floating above it. -->
46
+ <div
47
+ bind:this={ref}
48
+ class={cn(
49
+ 'flex min-w-0 flex-col rounded-(--era-rd-lg) bg-(--era-surface-bg) shadow-(--era-shadow)',
50
+ className
51
+ )}
52
+ {...restProps}
53
+ >
54
+ {@render children?.()}
55
+ </div>
@@ -0,0 +1,16 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import type { SwarmAgent } from './swarm.js';
4
+ type $$ComponentProps = HTMLAttributes<HTMLDivElement> & {
5
+ ref?: HTMLDivElement | null;
6
+ /** Epoch ms for every elapsed readout. Tick it in `$state` for live counters. */
7
+ now?: number;
8
+ /** Open an agent's transcript. Reaches every node without prop drilling. */
9
+ onOpen?: (agent: SwarmAgent) => void;
10
+ /** Cancel an agent. Nodes show the affordance only while one is still active. */
11
+ onCancel?: (agent: SwarmAgent) => void;
12
+ children?: Snippet;
13
+ };
14
+ declare const SwarmRoot: import("svelte").Component<$$ComponentProps, {}, "ref">;
15
+ type SwarmRoot = ReturnType<typeof SwarmRoot>;
16
+ export default SwarmRoot;