@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.
- 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/era-ui.css +1 -1
- package/dist/generated-docs/llm-shell.md +1 -1
- package/dist/generated-docs/llms-full.txt +36 -1
- package/dist/generated-docs/llms.txt +1 -0
- package/dist/generated-docs/manifest.json +14 -3
- package/dist/generated-docs/utilities.json +10 -0
- package/dist/generated-docs/utilities.md +34 -0
- package/dist/styles/index.css +47 -0
- package/dist/styles/themes.css +27 -15
- package/dist/ui/step/step-content.svelte +5 -34
- 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;
|