@signal9/era-ui 4.7.2 → 4.9.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 (48) hide show
  1. package/dist/ai/swarm/index.d.ts +2 -1
  2. package/dist/ai/swarm/index.js +2 -1
  3. package/dist/ai/swarm/swarm-actions.svelte +86 -0
  4. package/dist/ai/swarm/swarm-actions.svelte.d.ts +31 -0
  5. package/dist/ai/swarm/swarm-agent.svelte +9 -43
  6. package/dist/ai/swarm/swarm-artifact.svelte +2 -6
  7. package/dist/ai/swarm/swarm-artifacts.svelte +1 -10
  8. package/dist/ai/swarm/swarm-artifacts.svelte.d.ts +0 -2
  9. package/dist/ai/swarm/swarm-barriers.svelte +39 -0
  10. package/dist/ai/swarm/swarm-barriers.svelte.d.ts +13 -0
  11. package/dist/ai/swarm/swarm-counts.svelte +1 -4
  12. package/dist/ai/swarm/swarm-counts.svelte.d.ts +0 -2
  13. package/dist/ai/swarm/swarm-footer.svelte +20 -13
  14. package/dist/ai/swarm/swarm-header.svelte +3 -5
  15. package/dist/ai/swarm/swarm-node.svelte +8 -64
  16. package/dist/ai/swarm/swarm-role.svelte +2 -9
  17. package/dist/ai/swarm/swarm-sandbox.svelte +45 -0
  18. package/dist/ai/swarm/swarm-sandbox.svelte.d.ts +10 -0
  19. package/dist/ai/swarm/swarm-tree.svelte +22 -8
  20. package/dist/ai/swarm/swarm.d.ts +8 -0
  21. package/dist/ai/swarm/swarm.js +11 -2
  22. package/dist/ai/swarm/swarm.svelte.d.ts +0 -1
  23. package/dist/ai/swarm/swarm.svelte.js +10 -12
  24. package/dist/apps/llm-shell/llm-shell.svelte +2 -36
  25. package/dist/apps/llm-shell/llm-swarm.svelte +64 -0
  26. package/dist/apps/llm-shell/llm-swarm.svelte.d.ts +23 -0
  27. package/dist/apps/llm-shell/types.d.ts +2 -0
  28. package/dist/apps/notes/editor/bubble-toolbar.svelte +2 -1
  29. package/dist/apps/notes/notes.svelte +13 -10
  30. package/dist/dev/audit/audits/clipped-glyphs.js +33 -11
  31. package/dist/docs/llms-txt.js +1 -1
  32. package/dist/era-ui.css +1 -1
  33. package/dist/generated-docs/bar.md +3 -0
  34. package/dist/generated-docs/input.md +3 -1
  35. package/dist/generated-docs/llms-full.txt +15 -6
  36. package/dist/generated-docs/llms.txt +1 -1
  37. package/dist/generated-docs/manifest.json +5 -5
  38. package/dist/generated-docs/utilities.json +2 -2
  39. package/dist/generated-docs/utilities.md +8 -4
  40. package/dist/styles/density.css +42 -9
  41. package/dist/styles/index.css +8 -4
  42. package/dist/ui/bar/bar.svelte +37 -3
  43. package/dist/ui/bar/bar.svelte.d.ts +61 -1
  44. package/dist/ui/input/input.svelte +24 -4
  45. package/dist/ui/input/input.svelte.d.ts +6 -1
  46. package/dist/ui/input/variants.d.ts +99 -30
  47. package/dist/ui/input/variants.js +45 -19
  48. package/package.json +1 -1
@@ -7,10 +7,11 @@ export { default as Grid } from './swarm-grid.svelte';
7
7
  export { default as Agent } from './swarm-agent.svelte';
8
8
  export { default as Artifacts } from './swarm-artifacts.svelte';
9
9
  export { default as Artifact } from './swarm-artifact.svelte';
10
+ export { default as Barriers } from './swarm-barriers.svelte';
10
11
  export { default as Barrier } from './swarm-barrier.svelte';
11
12
  export { default as Footer } from './swarm-footer.svelte';
12
13
  export { default as Dot, SWARM_DOT } from './swarm-dot.svelte';
13
14
  export { default as Role, SWARM_ROLE } from './swarm-role.svelte';
14
15
  export { SwarmController } from './swarm.svelte.js';
15
- export { buildSwarmTree, flattenSwarmTree, summarizeSwarm, summarizeBarrier, swarmElapsed, formatSwarmAge, formatSwarmDuration, type SwarmAgent, type SwarmAgentStatus, type SwarmArtifact, type SwarmBarrier, type SwarmBarrierStatus, type SwarmConnection, type SwarmCounts, type SwarmEvent, type SwarmNode, type SwarmPlan, type SwarmPlanStatus, type SwarmRole, type SwarmSandbox, type SwarmSnapshot } from './swarm.js';
16
+ export { buildSwarmTree, isSwarmAgentActive, flattenSwarmTree, summarizeSwarm, summarizeBarrier, swarmElapsed, formatSwarmAge, formatSwarmDuration, type SwarmAgent, type SwarmAgentStatus, type SwarmArtifact, type SwarmBarrier, type SwarmBarrierStatus, type SwarmConnection, type SwarmCounts, type SwarmEvent, type SwarmNode, type SwarmPlan, type SwarmPlanStatus, type SwarmRole, type SwarmSandbox, type SwarmSnapshot } from './swarm.js';
16
17
  export type { SwarmContext } from './context.js';
@@ -7,9 +7,10 @@ export { default as Grid } from './swarm-grid.svelte';
7
7
  export { default as Agent } from './swarm-agent.svelte';
8
8
  export { default as Artifacts } from './swarm-artifacts.svelte';
9
9
  export { default as Artifact } from './swarm-artifact.svelte';
10
+ export { default as Barriers } from './swarm-barriers.svelte';
10
11
  export { default as Barrier } from './swarm-barrier.svelte';
11
12
  export { default as Footer } from './swarm-footer.svelte';
12
13
  export { default as Dot, SWARM_DOT } from './swarm-dot.svelte';
13
14
  export { default as Role, SWARM_ROLE } from './swarm-role.svelte';
14
15
  export { SwarmController } from './swarm.svelte.js';
15
- export { buildSwarmTree, flattenSwarmTree, summarizeSwarm, summarizeBarrier, swarmElapsed, formatSwarmAge, formatSwarmDuration } from './swarm.js';
16
+ export { buildSwarmTree, isSwarmAgentActive, flattenSwarmTree, summarizeSwarm, summarizeBarrier, swarmElapsed, formatSwarmAge, formatSwarmDuration } from './swarm.js';
@@ -0,0 +1,86 @@
1
+ <script lang="ts" module>
2
+ import { isSwarmAgentActive, type SwarmAgent } from './swarm.js';
3
+ import type { SwarmContext } from './context.js';
4
+
5
+ /**
6
+ * Which affordances an agent actually gets, resolved once.
7
+ *
8
+ * Both the row and the card need the ANSWER before they render — the row pays
9
+ * a different trailing inset when it holds buttons — so the decision is a
10
+ * function they call, not something each of them re-derives. It used to be
11
+ * re-derived, and the two copies had already drifted on which sandbox states
12
+ * counted as cancellable.
13
+ *
14
+ * `null` means "no affordance": no handler, or nothing for it to act on (an
15
+ * agent with no transcript has nowhere to open, a landed agent has nothing
16
+ * left to stop).
17
+ */
18
+ export function swarmActions(
19
+ agent: SwarmAgent,
20
+ ctx: SwarmContext,
21
+ overrides?: {
22
+ onOpen?: (agent: SwarmAgent) => void;
23
+ onCancel?: (agent: SwarmAgent) => void;
24
+ }
25
+ ): { open: ((agent: SwarmAgent) => void) | null; cancel: ((agent: SwarmAgent) => void) | null } {
26
+ const open = overrides?.onOpen ?? ctx.onOpen;
27
+ const cancel = overrides?.onCancel ?? ctx.onCancel;
28
+ return {
29
+ open: open && agent.conversationId != null ? open : null,
30
+ cancel: cancel && isSwarmAgentActive(agent) ? cancel : null
31
+ };
32
+ }
33
+ </script>
34
+
35
+ <script lang="ts">
36
+ import X from '@lucide/svelte/icons/x';
37
+ import PanelRightOpen from '@lucide/svelte/icons/panel-right-open';
38
+ import { Button } from '../../ui/button/index.js';
39
+
40
+ let {
41
+ agent,
42
+ open = null,
43
+ cancel = null
44
+ }: {
45
+ agent: SwarmAgent;
46
+ /** Resolved handlers from `swarmActions` — `null` renders no button. */
47
+ open?: ((agent: SwarmAgent) => void) | null;
48
+ cancel?: ((agent: SwarmAgent) => void) | null;
49
+ } = $props();
50
+ </script>
51
+
52
+ <!-- The two things you can do to one agent, wherever it is drawn.
53
+ stopPropagation unconditionally: in the tree these sit inside a treeitem
54
+ whose own click toggles the row, and a button that also collapsed its
55
+ parent would be a trap. On a card there is nothing above to stop, so one
56
+ behaviour covers both.
57
+ The glyphs carry no size class — Button's icon variant sizes them from the
58
+ tier via [&_svg], and that selector outranks a class here (see
59
+ button/variants.ts), so one would be inert. -->
60
+ {#if cancel}
61
+ <Button
62
+ icon
63
+ size="xxs"
64
+ tone="destructive"
65
+ aria-label="Cancel {agent.task}"
66
+ onclick={(event: MouseEvent) => {
67
+ event.stopPropagation();
68
+ cancel?.(agent);
69
+ }}
70
+ >
71
+ <X />
72
+ </Button>
73
+ {/if}
74
+ {#if open}
75
+ <Button
76
+ icon
77
+ size="xxs"
78
+ aria-label="Open {agent.task}"
79
+ onclick={(event: MouseEvent) => {
80
+ event.stopPropagation();
81
+ open?.(agent);
82
+ }}
83
+ >
84
+ <PanelRightOpen />
85
+ </Button>
86
+ {/if}
@@ -0,0 +1,31 @@
1
+ import { type SwarmAgent } from './swarm.js';
2
+ import type { SwarmContext } from './context.js';
3
+ /**
4
+ * Which affordances an agent actually gets, resolved once.
5
+ *
6
+ * Both the row and the card need the ANSWER before they render — the row pays
7
+ * a different trailing inset when it holds buttons — so the decision is a
8
+ * function they call, not something each of them re-derives. It used to be
9
+ * re-derived, and the two copies had already drifted on which sandbox states
10
+ * counted as cancellable.
11
+ *
12
+ * `null` means "no affordance": no handler, or nothing for it to act on (an
13
+ * agent with no transcript has nowhere to open, a landed agent has nothing
14
+ * left to stop).
15
+ */
16
+ export declare function swarmActions(agent: SwarmAgent, ctx: SwarmContext, overrides?: {
17
+ onOpen?: (agent: SwarmAgent) => void;
18
+ onCancel?: (agent: SwarmAgent) => void;
19
+ }): {
20
+ open: ((agent: SwarmAgent) => void) | null;
21
+ cancel: ((agent: SwarmAgent) => void) | null;
22
+ };
23
+ type $$ComponentProps = {
24
+ agent: SwarmAgent;
25
+ /** Resolved handlers from `swarmActions` — `null` renders no button. */
26
+ open?: ((agent: SwarmAgent) => void) | null;
27
+ cancel?: ((agent: SwarmAgent) => void) | null;
28
+ };
29
+ declare const SwarmActions: import("svelte").Component<$$ComponentProps, {}, "">;
30
+ type SwarmActions = ReturnType<typeof SwarmActions>;
31
+ export default SwarmActions;
@@ -1,15 +1,13 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
- import Box from '@lucide/svelte/icons/box';
5
- import X from '@lucide/svelte/icons/x';
6
- import PanelRightOpen from '@lucide/svelte/icons/panel-right-open';
7
- import { Button } from '../../ui/button/index.js';
8
4
  import { Card } from '../../ui/card/index.js';
9
5
  import { cn } from '../../utils/index.js';
10
6
  import { getSwarmContext } from './context.js';
11
7
  import Dot from './swarm-dot.svelte';
12
8
  import Role from './swarm-role.svelte';
9
+ import Sandbox from './swarm-sandbox.svelte';
10
+ import Actions, { swarmActions } from './swarm-actions.svelte';
13
11
  import { formatSwarmDuration, swarmElapsed, type SwarmAgent } from './swarm.js';
14
12
 
15
13
  let {
@@ -32,21 +30,17 @@
32
30
  } = $props();
33
31
 
34
32
  const ctx = getSwarmContext();
35
- const openAgent = $derived(onOpen ?? ctx.onOpen);
36
- const cancelAgent = $derived(onCancel ?? ctx.onCancel);
37
- const cancellable = $derived(
38
- cancelAgent != null &&
39
- (agent.status === 'running' || agent.status === 'queued' || agent.status === 'blocked')
40
- );
41
- // See `Swarm.Node`: the open affordance is gated on the agent actually having
42
- // a transcript to open.
43
- const openable = $derived(openAgent != null && agent.conversationId != null);
33
+ const actions = $derived(swarmActions(agent, ctx, { onOpen, onCancel }));
44
34
  const elapsed = $derived(swarmElapsed(agent, ctx.now));
45
35
  </script>
46
36
 
47
37
  <!-- One agent as a card, for the console grid — the same agent the tree renders
48
38
  as a row, sized for scanning forty of them at once.
49
39
 
40
+ The row and the card are two drawings of one agent, so everything that
41
+ decides WHAT to draw — which affordances it gets, how its VM reads — is
42
+ shared with the row rather than restated here.
43
+
50
44
  A well, not an elevated card: the grid sits INSIDE the swarm panel, and on
51
45
  the flat surface `elevated` resolves to the same fill the panel already has
52
46
  — forty invisible boxes. The recessed tier is the one that reads as content
@@ -77,22 +71,7 @@
77
71
  {#if depth != null}
78
72
  <span class="shrink-0 font-mono text-[0.625rem] text-muted tabular-nums">d{depth}</span>
79
73
  {/if}
80
- {#if cancellable}
81
- <Button
82
- icon
83
- size="xxs"
84
- tone="destructive"
85
- aria-label="Cancel {agent.task}"
86
- onclick={() => cancelAgent?.(agent)}
87
- >
88
- <X class="size-(--era-h-xs)" />
89
- </Button>
90
- {/if}
91
- {#if openable}
92
- <Button icon size="xxs" aria-label="Open {agent.task}" onclick={() => openAgent?.(agent)}>
93
- <PanelRightOpen class="size-(--era-h-xs)" />
94
- </Button>
95
- {/if}
74
+ <Actions {agent} open={actions.open} cancel={actions.cancel} />
96
75
  </div>
97
76
 
98
77
  <!-- Two lines of task, one of result: enough to tell two workers apart,
@@ -108,20 +87,7 @@
108
87
  {#if agent.sandbox || elapsed != null}
109
88
  <div class="flex min-w-0 items-center gap-(--era-gap) text-body era-text-trim text-muted">
110
89
  {#if agent.sandbox}
111
- <span
112
- class="flex min-w-0 items-center gap-(--era-gap) font-mono"
113
- title="sandbox {agent.sandbox.id ?? ''}"
114
- >
115
- <Box
116
- class={cn(
117
- 'size-(--era-h-xs) shrink-0',
118
- agent.sandbox.status === 'running' && 'text-success',
119
- agent.sandbox.status === 'paused' && 'text-warning'
120
- )}
121
- aria-hidden="true"
122
- />
123
- <span class="truncate">{agent.sandbox.status}</span>
124
- </span>
90
+ <Sandbox sandbox={agent.sandbox} label />
125
91
  {/if}
126
92
  <span class="flex-1"></span>
127
93
  {#if elapsed != null}
@@ -29,14 +29,10 @@
29
29
  {...restProps}
30
30
  >
31
31
  <div class="flex min-w-0 items-center gap-(--era-gap) text-body era-text-trim">
32
- <span class="shrink-0 font-mono text-[0.625rem] tracking-[0.12em] text-muted uppercase">
32
+ <span class="shrink-0 font-mono text-[0.625rem] tracking-[0.15em] text-muted uppercase">
33
33
  {artifact.kind}
34
34
  </span>
35
- {#if artifact.title}
36
- <span class="min-w-0 flex-1 truncate text-bright">{artifact.title}</span>
37
- {:else}
38
- <span class="flex-1"></span>
39
- {/if}
35
+ <span class="min-w-0 flex-1 truncate text-bright">{artifact.title ?? ''}</span>
40
36
  {#if age}
41
37
  <span class="shrink-0 font-mono text-body text-muted tabular-nums">{age}</span>
42
38
  {/if}
@@ -9,7 +9,6 @@
9
9
  let {
10
10
  artifacts = [],
11
11
  label = 'artifacts',
12
- limit,
13
12
  children,
14
13
  class: className,
15
14
  ...restProps
@@ -17,14 +16,9 @@
17
16
  artifacts?: SwarmArtifact[];
18
17
  /** Section heading. Set to `undefined` for a bare list. */
19
18
  label?: string;
20
- /** Show at most this many, newest first by position. Omit for all of them. */
21
- limit?: number;
22
19
  /** Replaces the default `Swarm.Artifact` rendering for each item. */
23
20
  children?: Snippet<[SwarmArtifact]>;
24
21
  } = $props();
25
-
26
- const shown = $derived(limit == null ? artifacts : artifacts.slice(0, limit));
27
- const hidden = $derived(artifacts.length - shown.length);
28
22
  </script>
29
23
 
30
24
  <!-- Renders nothing at all when there is nothing to show: an empty "ARTIFACTS"
@@ -32,11 +26,8 @@
32
26
  that are mostly empty until a branch lands. -->
33
27
  {#if artifacts.length > 0}
34
28
  <Step.Section {label} class={cn('min-w-0', className)} {...restProps}>
35
- {#each shown as artifact (artifact.id)}
29
+ {#each artifacts as artifact (artifact.id)}
36
30
  {#if children}{@render children(artifact)}{:else}<Artifact {artifact} />{/if}
37
31
  {/each}
38
- {#if hidden > 0}
39
- <span class="text-body text-muted">+{hidden} more</span>
40
- {/if}
41
32
  </Step.Section>
42
33
  {/if}
@@ -5,8 +5,6 @@ type $$ComponentProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
5
5
  artifacts?: SwarmArtifact[];
6
6
  /** Section heading. Set to `undefined` for a bare list. */
7
7
  label?: string;
8
- /** Show at most this many, newest first by position. Omit for all of them. */
9
- limit?: number;
10
8
  /** Replaces the default `Swarm.Artifact` rendering for each item. */
11
9
  children?: Snippet<[SwarmArtifact]>;
12
10
  };
@@ -0,0 +1,39 @@
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 Barrier from './swarm-barrier.svelte';
6
+ import type { SwarmAgent, SwarmBarrier } from './swarm.js';
7
+
8
+ let {
9
+ barriers = [],
10
+ agents = [],
11
+ children,
12
+ class: className,
13
+ ...restProps
14
+ }: Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
15
+ barriers?: SwarmBarrier[];
16
+ /** The swarm's agents — each barrier's progress is derived from its members. */
17
+ agents?: SwarmAgent[];
18
+ /** Replaces the default `Swarm.Barrier` rendering for each gate. */
19
+ children?: Snippet<[SwarmBarrier]>;
20
+ } = $props();
21
+ </script>
22
+
23
+ <!-- The plural container, so a host does not have to know that the fan-in
24
+ section is divided off from the tree above it (Artifacts already had one;
25
+ the asymmetry meant every host re-invented this chrome). Renders nothing
26
+ when a swarm declares no gates, which most do not. -->
27
+ {#if barriers.length > 0}
28
+ <div
29
+ class={cn(
30
+ 'flex min-w-0 flex-col gap-(--era-pad-sm) border-t border-divider-faded p-(--era-pad-sm)',
31
+ className
32
+ )}
33
+ {...restProps}
34
+ >
35
+ {#each barriers as barrier (barrier.id)}
36
+ {#if children}{@render children(barrier)}{:else}<Barrier {barrier} {agents} />{/if}
37
+ {/each}
38
+ </div>
39
+ {/if}
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import type { SwarmAgent, SwarmBarrier } from './swarm.js';
4
+ type $$ComponentProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
5
+ barriers?: SwarmBarrier[];
6
+ /** The swarm's agents — each barrier's progress is derived from its members. */
7
+ agents?: SwarmAgent[];
8
+ /** Replaces the default `Swarm.Barrier` rendering for each gate. */
9
+ children?: Snippet<[SwarmBarrier]>;
10
+ };
11
+ declare const SwarmBarriers: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type SwarmBarriers = ReturnType<typeof SwarmBarriers>;
13
+ export default SwarmBarriers;
@@ -8,7 +8,6 @@
8
8
  let {
9
9
  agents = [],
10
10
  counts,
11
- sandboxes = true,
12
11
  class: className,
13
12
  ...restProps
14
13
  }: HTMLAttributes<HTMLDivElement> & {
@@ -16,8 +15,6 @@
16
15
  agents?: SwarmAgent[];
17
16
  /** Pre-computed counts (a controller's `counts`), used instead of `agents`. */
18
17
  counts?: SwarmCounts;
19
- /** Show the live-VM tally. Off for swarms that do not run sandboxes. */
20
- sandboxes?: boolean;
21
18
  } = $props();
22
19
 
23
20
  const tally = $derived(counts ?? summarizeSwarm(agents));
@@ -43,7 +40,7 @@
43
40
  {#if tally.done > 0}<Badge tone="success">{tally.done} done</Badge>{/if}
44
41
  {#if tally.failed > 0}<Badge tone="destructive">{tally.failed} failed</Badge>{/if}
45
42
  {/if}
46
- {#if sandboxes && tally.sandboxes > 0}
43
+ {#if tally.sandboxes > 0}
47
44
  <Badge>
48
45
  <Box class="size-(--era-h-xs)" aria-hidden="true" />
49
46
  {tally.sandboxes}
@@ -5,8 +5,6 @@ type $$ComponentProps = HTMLAttributes<HTMLDivElement> & {
5
5
  agents?: SwarmAgent[];
6
6
  /** Pre-computed counts (a controller's `counts`), used instead of `agents`. */
7
7
  counts?: SwarmCounts;
8
- /** Show the live-VM tally. Off for swarms that do not run sandboxes. */
9
- sandboxes?: boolean;
10
8
  };
11
9
  declare const SwarmCounts: import("svelte").Component<$$ComponentProps, {}, "">;
12
10
  type SwarmCounts = ReturnType<typeof SwarmCounts>;
@@ -1,24 +1,31 @@
1
1
  <script lang="ts" module>
2
- import type { SwarmConnection } from './swarm.js';
2
+ import type { SwarmAgentStatus, SwarmConnection } from './swarm.js';
3
3
 
4
4
  /**
5
5
  * Stream health, which is NOT swarm health — the two are shown apart on
6
6
  * purpose (the header has the plan's dot, this has the socket's). A swarm can
7
7
  * be running perfectly behind a dead connection, and a console that paints
8
8
  * one dot for both tells you the run stalled when only your view did.
9
+ *
10
+ * Borrows the agent dot's vocabulary the same way the header does, so one tone
11
+ * system covers every state in the family and the dot itself is drawn once.
9
12
  */
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' }
13
+ const CONNECTION: Record<
14
+ SwarmConnection,
15
+ { status: SwarmAgentStatus; class: string; label: string; pulse?: boolean }
16
+ > = {
17
+ idle: { status: 'queued', class: 'text-muted', label: 'idle' },
18
+ loading: { status: 'queued', class: 'text-muted', label: 'loading', pulse: true },
19
+ live: { status: 'done', class: 'text-success', label: 'live' },
20
+ reconnecting: { status: 'blocked', class: 'text-warning', label: 'reconnecting', pulse: true },
21
+ error: { status: 'failed', class: 'text-destructive', label: 'disconnected' }
16
22
  };
17
23
  </script>
18
24
 
19
25
  <script lang="ts">
20
26
  import type { HTMLAttributes } from 'svelte/elements';
21
27
  import { cn } from '../../utils/index.js';
28
+ import Dot from './swarm-dot.svelte';
22
29
 
23
30
  let {
24
31
  connection = 'idle',
@@ -49,14 +56,14 @@
49
56
  {...restProps}
50
57
  >
51
58
  <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>
59
+ <Dot status={tone.status} label={tone.label} class={tone.pulse ? 'animate-pulse' : undefined} />
53
60
  {tone.label}
54
61
  </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}
62
+ <!-- Always rendered, empty when there is nothing to say: the span is also the
63
+ spacer that pushes the watermark to the trailing edge. -->
64
+ <span class="min-w-0 flex-1 truncate text-destructive" title={error ?? undefined}>
65
+ {error ?? ''}
66
+ </span>
60
67
  {#if seq != null && seq >= 0}
61
68
  <span class="shrink-0 text-muted tabular-nums">seq {seq}</span>
62
69
  {/if}
@@ -51,11 +51,9 @@
51
51
  {#if plan}
52
52
  <Dot status={plan.status} label={plan.label} />
53
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}
54
+ <!-- Always rendered: it is the title AND the spacer that pushes the trailing
55
+ group to the edge, so there is no empty-title branch to keep in sync. -->
56
+ <span class="min-w-0 flex-1 truncate text-bright">{title ?? ''}</span>
59
57
  {#if children}
60
58
  <div class="flex shrink-0 items-center gap-(--era-gap)">
61
59
  {@render children()}
@@ -1,24 +1,13 @@
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
1
  <script lang="ts">
11
2
  import type { Snippet } from 'svelte';
12
3
  import { Collapsible } from 'bits-ui';
13
4
  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
5
  import { cn } from '../../utils/index.js';
19
6
  import { getSwarmContext } from './context.js';
20
7
  import Dot from './swarm-dot.svelte';
21
8
  import Role from './swarm-role.svelte';
9
+ import Sandbox from './swarm-sandbox.svelte';
10
+ import Actions, { swarmActions } from './swarm-actions.svelte';
22
11
  import { formatSwarmDuration, swarmElapsed, type SwarmAgent } from './swarm.js';
23
12
 
24
13
  let {
@@ -63,24 +52,11 @@
63
52
  } = $props();
64
53
 
65
54
  const ctx = getSwarmContext();
66
- const openAgent = $derived(onOpen ?? ctx.onOpen);
67
- const cancelAgent = $derived(onCancel ?? ctx.onCancel);
55
+ const actions = $derived(swarmActions(agent, ctx, { onOpen, onCancel }));
56
+ const hasActions = $derived(actions.open != null || actions.cancel != null);
68
57
 
69
58
  const canExpand = $derived(expandable ?? (children != null || detail != null));
70
59
  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
60
  const elapsed = $derived(swarmElapsed(agent, ctx.now));
85
61
 
86
62
  function toggle() {
@@ -99,7 +75,7 @@
99
75
  if (event.target !== event.currentTarget) return;
100
76
  if (event.key === 'Enter') {
101
77
  event.preventDefault();
102
- if (openAgent) openAgent(agent);
78
+ if (actions.open) actions.open(agent);
103
79
  else toggle();
104
80
  } else if (event.key === ' ') {
105
81
  event.preventDefault();
@@ -137,7 +113,7 @@
137
113
  aria-expanded={canExpand ? open : undefined}
138
114
  data-agent-id={agent.id}
139
115
  data-active={active || undefined}
140
- tabindex={-1}
116
+ tabindex={active ? 0 : -1}
141
117
  {onkeydown}
142
118
  {onclick}
143
119
  class={cn('group/node min-w-0 outline-none', className)}
@@ -188,12 +164,7 @@
188
164
  {@render meta?.(agent)}
189
165
 
190
166
  {#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>
167
+ <Sandbox sandbox={agent.sandbox} />
197
168
  {/if}
198
169
 
199
170
  {#if elapsed != null}
@@ -202,34 +173,7 @@
202
173
  </span>
203
174
  {/if}
204
175
 
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}
176
+ <Actions {agent} open={actions.open} cancel={actions.cancel} />
233
177
  </div>
234
178
 
235
179
  {#if canExpand}
@@ -31,15 +31,12 @@
31
31
  let {
32
32
  role = 'worker',
33
33
  label,
34
- short = false,
35
34
  class: className,
36
35
  ...restProps
37
36
  }: HTMLAttributes<HTMLSpanElement> & {
38
37
  role?: SwarmRole;
39
38
  /** Specialization shown after the role ("api", "docs") — the agent's `label`. */
40
39
  label?: string;
41
- /** Glyph only, with the role kept as the hover/announced name. For dense rows. */
42
- short?: boolean;
43
40
  } = $props();
44
41
 
45
42
  const spec = $derived(SWARM_ROLE[role]);
@@ -56,16 +53,12 @@
56
53
  data-role={role}
57
54
  title={name}
58
55
  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',
56
+ 'inline-flex shrink-0 items-center gap-(--era-gap) font-mono text-[0.625rem] era-text-trim-caps tracking-[0.15em] uppercase',
60
57
  role === 'orchestrator' ? 'text-fg' : 'text-muted',
61
58
  className
62
59
  )}
63
60
  {...restProps}
64
61
  >
65
62
  <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}
63
+ {name}
71
64
  </span>