@signal9/era-ui 34.2.9 → 34.3.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/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [34.3.0](https://github.com/sig-nine/era-ui/compare/v34.2.9...v34.3.0) (2026-08-25)
2
+
3
+ ### Features
4
+
5
+ * **ai:** generalize durable provider primitives ([808f9e3](https://github.com/sig-nine/era-ui/commit/808f9e3ff5d0446cc80d9e020c9fdf1259ce934e))
6
+
1
7
  ## [34.2.9](https://github.com/sig-nine/era-ui/compare/v34.2.8...v34.2.9) (2026-08-25)
2
8
 
3
9
  ### Bug Fixes
@@ -16,9 +16,9 @@
16
16
  >
17
17
  <Button
18
18
  icon
19
- size="control"
19
+ size="pill"
20
20
  aria-label="Scroll to bottom"
21
- class="rounded-full shadow-lg"
21
+ class="shadow-lg"
22
22
  onclick={() => stick.repin()}
23
23
  >
24
24
  <ArrowDown class="size-icon" />
@@ -13,10 +13,16 @@
13
13
 
14
14
  let {
15
15
  title = $bindable(''),
16
+ id,
16
17
  starred = $bindable(false),
17
18
  status,
19
+ childStatus,
20
+ role,
21
+ depth = 0,
22
+ rootActive = false,
18
23
  active = false,
19
24
  leading,
25
+ actions,
20
26
  onSelect,
21
27
  onRename,
22
28
  onStar,
@@ -25,16 +31,28 @@
25
31
  class: className,
26
32
  ...restProps
27
33
  }: Omit<HTMLAttributes<HTMLDivElement>, 'title'> & {
34
+ /** Stable consumer-owned identity, exposed as data-conversation-id. */
35
+ id?: string;
28
36
  /** Conversation label; double-click to rename in place. Bindable. */
29
37
  title?: string;
30
38
  /** Bindable star state; the rail's star button toggles it. */
31
39
  starred?: boolean;
32
40
  /** Drives the status dot tone; omit for no dot. */
33
41
  status?: ConversationStatus;
42
+ /** Separate delegated-child status mark. */
43
+ childStatus?: ConversationStatus;
44
+ /** Optional visible role label. */
45
+ role?: string;
46
+ /** Nesting depth; indentation follows the density axis. */
47
+ depth?: number;
48
+ /** Marks a root whose active child owns the current work. */
49
+ rootActive?: boolean;
34
50
  /** Highlighted as the open conversation. */
35
51
  active?: boolean;
36
52
  /** Overrides the leading icon (e.g. an expand chevron). */
37
53
  leading?: Snippet;
54
+ /** Consumer-owned actions, rendered ahead of built-in row actions. */
55
+ actions?: Snippet;
38
56
  /** Row activated (click / Enter on the title). */
39
57
  onSelect?: () => void;
40
58
  /** Fired after a non-empty rename commits, with the new title. */
@@ -127,7 +145,13 @@
127
145
  </script>
128
146
 
129
147
  <div
148
+ data-conversation-id={id}
130
149
  data-active={active || undefined}
150
+ data-root-active={rootActive || undefined}
151
+ data-depth={depth || undefined}
152
+ style:padding-inline-start={depth > 0
153
+ ? `calc(var(--era-inset-control) + ${Math.max(0, depth)} * var(--era-sp))`
154
+ : undefined}
131
155
  class={cn(
132
156
  // The trailing buttons are pill-tier icon buttons: h-pill box + h-icon glyph =
133
157
  // 2px of air around the glyph (the icon tier would hug it flush — density.css:
@@ -167,6 +191,20 @@
167
191
  aria-hidden="true"
168
192
  ></span>
169
193
  {/if}
194
+ {#if role}
195
+ <span
196
+ class="shrink-0 text-[0.625rem] era-text-trim-caps tracking-[0.12em] text-muted uppercase"
197
+ >
198
+ {role}
199
+ </span>
200
+ {/if}
201
+ {#if childStatus}
202
+ <span
203
+ class={cn('size-gutter shrink-0 rounded-full', DOT[childStatus].class)}
204
+ title={DOT[childStatus].label}
205
+ aria-label={`Child: ${DOT[childStatus].label}`}
206
+ ></span>
207
+ {/if}
170
208
 
171
209
  {#if editing}
172
210
  <!-- `bare`: the row is already the field's container, so the input brings no
@@ -201,6 +239,7 @@
201
239
  data-rail
202
240
  class="flex shrink-0 items-center gap-gutter opacity-0 group-hover/row:opacity-100 focus-within:opacity-100"
203
241
  >
242
+ {#if actions}{@render actions()}{/if}
204
243
  <Button
205
244
  icon
206
245
  size="pill"
@@ -2,16 +2,28 @@ import type { Snippet } from 'svelte';
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
3
  import type { ConversationStatus } from './conversations.svelte.js';
4
4
  type $$ComponentProps = Omit<HTMLAttributes<HTMLDivElement>, 'title'> & {
5
+ /** Stable consumer-owned identity, exposed as data-conversation-id. */
6
+ id?: string;
5
7
  /** Conversation label; double-click to rename in place. Bindable. */
6
8
  title?: string;
7
9
  /** Bindable star state; the rail's star button toggles it. */
8
10
  starred?: boolean;
9
11
  /** Drives the status dot tone; omit for no dot. */
10
12
  status?: ConversationStatus;
13
+ /** Separate delegated-child status mark. */
14
+ childStatus?: ConversationStatus;
15
+ /** Optional visible role label. */
16
+ role?: string;
17
+ /** Nesting depth; indentation follows the density axis. */
18
+ depth?: number;
19
+ /** Marks a root whose active child owns the current work. */
20
+ rootActive?: boolean;
11
21
  /** Highlighted as the open conversation. */
12
22
  active?: boolean;
13
23
  /** Overrides the leading icon (e.g. an expand chevron). */
14
24
  leading?: Snippet;
25
+ /** Consumer-owned actions, rendered ahead of built-in row actions. */
26
+ actions?: Snippet;
15
27
  /** Row activated (click / Enter on the title). */
16
28
  onSelect?: () => void;
17
29
  /** Fired after a non-empty rename commits, with the new title. */
@@ -10,6 +10,12 @@ export interface ConversationEntry {
10
10
  updatedAt: number;
11
11
  starred?: boolean;
12
12
  status?: ConversationStatus;
13
+ /** Optional parent identity for consumer-owned nested conversation trees. */
14
+ parentId?: string;
15
+ /** Reader-facing role label, for example "Research" or "Sub-agent". */
16
+ role?: string;
17
+ /** Distinct child execution state when a root row summarizes delegated work. */
18
+ childStatus?: ConversationStatus;
13
19
  }
14
20
  /** A rendered section: a bucket, its display label, and its member rows. */
15
21
  export interface ConversationGroup {
@@ -22,6 +22,7 @@ export * as Swarm from './swarm/index.js';
22
22
  export { Response, repairStreamingMarkdown } from './response/index.js';
23
23
  export { default as ContextGauge } from './context/context.svelte';
24
24
  export { default as ModelSelector } from './model-selector/model-selector.svelte';
25
+ export type { ModelGroup, ModelOption } from './model-selector/index.js';
25
26
  export { default as OpenInChat, providers as openInProviders } from './open-in-chat/open-in-chat.svelte';
26
27
  export { default as WebPreview } from './web-preview/web-preview.svelte';
27
28
  export * as ChainOfThought from './chain-of-thought/index.js';
@@ -33,7 +34,7 @@ export { CopyButton } from '../ui/copy-button/index.js';
33
34
  export { StickToBottom } from './stick-to-bottom.svelte.js';
34
35
  export type { PromptAttachment, PromptMessage, ChatStatus } from './prompt-input/index.js';
35
36
  export type { MessageRole, MessageVersion } from './message/index.js';
36
- export type { ToolState } from './tool/index.js';
37
+ export type { ToolState, ToolStatus } from './tool/index.js';
37
38
  export type { UsageBreakdown } from './message/index.js';
38
39
  export type { ConversationEntry, ConversationGroup, ConversationStatus, TimeBucket } from './conversations/index.js';
39
40
  export type { RunState, LogTone, RunTools } from './run-status/index.js';
@@ -1 +1,2 @@
1
1
  export { default as Root } from './model-selector.svelte';
2
+ export type { ModelGroup, ModelOption } from './model-selector.svelte';
@@ -1,3 +1,25 @@
1
+ <script lang="ts" module>
2
+ import type { Snippet } from 'svelte';
3
+
4
+ export interface ModelOption {
5
+ id: string;
6
+ name: string;
7
+ provider?: string;
8
+ /** Consumer-rendered local icon; preferred over any remote logo. */
9
+ icon?: Snippet;
10
+ /** Short, optional secondary detail such as context window or price tier. */
11
+ metadata?: string;
12
+ /** Optional remote logo for applications that explicitly permit it. */
13
+ logo?: string;
14
+ }
15
+
16
+ export interface ModelGroup {
17
+ id: string;
18
+ label: string;
19
+ models: ModelOption[];
20
+ }
21
+ </script>
22
+
1
23
  <script lang="ts">
2
24
  import * as Dialog from '../../ui/dialog/index.js';
3
25
  import * as Command from '../../ui/command/index.js';
@@ -6,18 +28,26 @@
6
28
 
7
29
  let {
8
30
  open = $bindable(false),
9
- models,
31
+ models = [],
32
+ groups,
10
33
  value = $bindable(''),
34
+ offline = false,
11
35
  onSelect,
12
36
  class: className
13
37
  }: {
14
38
  open?: boolean;
15
- /** `provider` keys models.dev logo slugs (e.g. `openai`, `anthropic`). */
16
- models: { id: string; name: string; provider?: string }[];
39
+ /** Flat provider-neutral options; used as one ungrouped list unless groups is supplied. */
40
+ models?: ModelOption[];
41
+ /** Consumer-owned provider or capability groups. */
42
+ groups?: ModelGroup[];
17
43
  value?: string;
18
- onSelect?: (id: string) => void;
44
+ /** Never loads remote logo URLs; local icon snippets continue to render. */
45
+ offline?: boolean;
46
+ onSelect?: (id: string, model: ModelOption) => void;
19
47
  class?: string;
20
48
  } = $props();
49
+
50
+ const visibleGroups = $derived(groups ?? [{ id: 'models', label: 'Models', models }]);
21
51
  </script>
22
52
 
23
53
  <Dialog.Root bind:open>
@@ -27,33 +57,37 @@
27
57
  <Command.List>
28
58
  <Command.Viewport>
29
59
  <Command.Empty>No models found.</Command.Empty>
30
- <Command.Group>
31
- <Command.GroupItems>
32
- {#each models as m (m.id)}
33
- <Command.Item
34
- value={m.id}
35
- keywords={[m.name, m.provider ?? '']}
36
- onSelect={() => {
37
- value = m.id;
38
- onSelect?.(m.id);
39
- open = false;
40
- }}
41
- >
42
- {#if m.provider}
43
- <img
44
- src="https://models.dev/logos/{m.provider}.svg"
45
- alt=""
46
- class="size-icon shrink-0 dark:invert"
47
- />
48
- {/if}
49
- <span class="truncate">{m.name}</span>
50
- {#if value === m.id}
51
- <span class="ml-auto text-body text-muted">current</span>
52
- {/if}
53
- </Command.Item>
54
- {/each}
55
- </Command.GroupItems>
56
- </Command.Group>
60
+ {#each visibleGroups as group (group.id)}
61
+ <Command.Group>
62
+ <Command.GroupHeading>{group.label}</Command.GroupHeading>
63
+ <Command.GroupItems>
64
+ {#each group.models as model (model.id)}
65
+ <Command.Item
66
+ value={model.id}
67
+ keywords={[model.name, model.provider ?? '', model.metadata ?? '']}
68
+ onSelect={() => {
69
+ value = model.id;
70
+ onSelect?.(model.id, model);
71
+ open = false;
72
+ }}
73
+ >
74
+ {#if model.icon}
75
+ {@render model.icon()}
76
+ {:else if model.logo && !offline}
77
+ <img src={model.logo} alt="" class="size-icon shrink-0 dark:invert" />
78
+ {/if}
79
+ <span class="min-w-0 flex-1 truncate">{model.name}</span>
80
+ {#if model.metadata}
81
+ <span class="shrink-0 text-body text-muted">{model.metadata}</span>
82
+ {/if}
83
+ {#if value === model.id}
84
+ <span class="shrink-0 text-body text-muted">current</span>
85
+ {/if}
86
+ </Command.Item>
87
+ {/each}
88
+ </Command.GroupItems>
89
+ </Command.Group>
90
+ {/each}
57
91
  </Command.Viewport>
58
92
  </Command.List>
59
93
  </Command.Root>
@@ -1,13 +1,30 @@
1
+ import type { Snippet } from 'svelte';
2
+ export interface ModelOption {
3
+ id: string;
4
+ name: string;
5
+ provider?: string;
6
+ /** Consumer-rendered local icon; preferred over any remote logo. */
7
+ icon?: Snippet;
8
+ /** Short, optional secondary detail such as context window or price tier. */
9
+ metadata?: string;
10
+ /** Optional remote logo for applications that explicitly permit it. */
11
+ logo?: string;
12
+ }
13
+ export interface ModelGroup {
14
+ id: string;
15
+ label: string;
16
+ models: ModelOption[];
17
+ }
1
18
  type $$ComponentProps = {
2
19
  open?: boolean;
3
- /** `provider` keys models.dev logo slugs (e.g. `openai`, `anthropic`). */
4
- models: {
5
- id: string;
6
- name: string;
7
- provider?: string;
8
- }[];
20
+ /** Flat provider-neutral options; used as one ungrouped list unless groups is supplied. */
21
+ models?: ModelOption[];
22
+ /** Consumer-owned provider or capability groups. */
23
+ groups?: ModelGroup[];
9
24
  value?: string;
10
- onSelect?: (id: string) => void;
25
+ /** Never loads remote logo URLs; local icon snippets continue to render. */
26
+ offline?: boolean;
27
+ onSelect?: (id: string, model: ModelOption) => void;
11
28
  class?: string;
12
29
  };
13
30
  declare const ModelSelector: import("svelte").Component<$$ComponentProps, {}, "value" | "open">;
@@ -1,2 +1,2 @@
1
1
  export { default as Root } from './queue.svelte';
2
- export { default as Item } from './queue-item.svelte';
2
+ export { default as Item, type QueueStatus, type QueueTone } from './queue-item.svelte';
@@ -1,26 +1,83 @@
1
+ <script lang="ts" module>
2
+ export type QueueStatus = 'queued' | 'running' | 'complete' | 'cancelled' | 'error';
3
+ export type QueueTone = 'default' | 'accent' | 'success' | 'warning' | 'destructive' | 'info';
4
+ </script>
5
+
1
6
  <script lang="ts">
2
7
  import type { Snippet } from 'svelte';
8
+ import type { HTMLAttributes } from 'svelte/elements';
9
+ import X from '@lucide/svelte/icons/x';
10
+ import { Button } from '../../ui/button/index.js';
3
11
  import { cn } from '../../utils/index.js';
4
12
 
5
13
  let {
6
- completed = false,
14
+ id,
15
+ status = 'queued',
16
+ tone,
17
+ label,
18
+ cancelLabel = 'Cancel queued work',
19
+ leading,
20
+ trailing,
7
21
  children,
8
- class: className
9
- }: { completed?: boolean; children?: Snippet; class?: string } = $props();
22
+ onCancel,
23
+ class: className,
24
+ ...restProps
25
+ }: Omit<HTMLAttributes<HTMLLIElement>, 'id'> & {
26
+ id: string;
27
+ status?: QueueStatus;
28
+ tone?: QueueTone;
29
+ label?: string;
30
+ cancelLabel?: string;
31
+ leading?: Snippet;
32
+ trailing?: Snippet;
33
+ children?: Snippet;
34
+ onCancel?: (id: string) => void;
35
+ } = $props();
36
+
37
+ const visual = $derived(
38
+ tone ??
39
+ (
40
+ {
41
+ queued: 'warning',
42
+ running: 'accent',
43
+ complete: 'success',
44
+ cancelled: 'default',
45
+ error: 'destructive'
46
+ } as const
47
+ )[status]
48
+ );
49
+ const cancellable = $derived(status === 'queued' || status === 'running');
10
50
  </script>
11
51
 
12
52
  <li
53
+ data-queue-id={id}
54
+ data-status={status}
55
+ aria-label={label}
13
56
  class={cn(
14
- 'flex min-w-0 items-center gap-gutter rounded-control px-inset-control py-gutter text-body hover:bg-highlight',
15
- completed ? 'text-muted line-through' : 'text-fg',
57
+ 'flex min-w-0 items-center gap-gutter rounded-control px-inset-control py-gutter text-body hover:bg-highlight hover:shadow-highlight',
58
+ status === 'complete' || status === 'cancelled' ? 'text-muted line-through' : 'text-fg',
16
59
  className
17
60
  )}
61
+ {...restProps}
18
62
  >
63
+ {#if leading}{@render leading()}{/if}
19
64
  <span
20
65
  class={cn(
21
66
  'size-2 shrink-0 rounded-full',
22
- completed ? 'bg-success' : 'border border-divider bg-transparent'
67
+ visual === 'accent' && 'animate-pulse bg-primary',
68
+ visual === 'success' && 'bg-success',
69
+ visual === 'warning' && 'bg-warning',
70
+ visual === 'destructive' && 'bg-destructive',
71
+ visual === 'info' && 'bg-info',
72
+ visual === 'default' && 'border border-divider bg-transparent'
23
73
  )}
74
+ aria-hidden="true"
24
75
  ></span>
25
76
  <span class="min-w-0 flex-1 truncate">{@render children?.()}</span>
77
+ {#if trailing}{@render trailing()}{/if}
78
+ {#if cancellable && onCancel}
79
+ <Button icon size="pill" aria-label={cancelLabel} onclick={() => onCancel?.(id)}>
80
+ <X class="size-icon" />
81
+ </Button>
82
+ {/if}
26
83
  </li>
@@ -1,8 +1,17 @@
1
+ export type QueueStatus = 'queued' | 'running' | 'complete' | 'cancelled' | 'error';
2
+ export type QueueTone = 'default' | 'accent' | 'success' | 'warning' | 'destructive' | 'info';
1
3
  import type { Snippet } from 'svelte';
2
- type $$ComponentProps = {
3
- completed?: boolean;
4
+ import type { HTMLAttributes } from 'svelte/elements';
5
+ type $$ComponentProps = Omit<HTMLAttributes<HTMLLIElement>, 'id'> & {
6
+ id: string;
7
+ status?: QueueStatus;
8
+ tone?: QueueTone;
9
+ label?: string;
10
+ cancelLabel?: string;
11
+ leading?: Snippet;
12
+ trailing?: Snippet;
4
13
  children?: Snippet;
5
- class?: string;
14
+ onCancel?: (id: string) => void;
6
15
  };
7
16
  declare const QueueItem: import("svelte").Component<$$ComponentProps, {}, "">;
8
17
  type QueueItem = ReturnType<typeof QueueItem>;
@@ -1,14 +1,38 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
3
4
  import { cn } from '../../utils/index.js';
4
- let { children, class: className }: { children?: Snippet; class?: string } = $props();
5
+
6
+ let {
7
+ full = false,
8
+ fullLabel = 'Queue is full',
9
+ label = 'Queued work',
10
+ fullContent,
11
+ children,
12
+ class: className,
13
+ ...restProps
14
+ }: HTMLAttributes<HTMLUListElement> & {
15
+ full?: boolean;
16
+ fullLabel?: string;
17
+ label?: string;
18
+ fullContent?: Snippet;
19
+ children?: Snippet;
20
+ } = $props();
5
21
  </script>
6
22
 
7
- <ul
8
- class={cn(
9
- 'flex max-h-48 flex-col gap-gutter overflow-y-auto rounded-bar bg-well p-panel shadow-sm',
10
- className
11
- )}
12
- >
13
- {@render children?.()}
14
- </ul>
23
+ <div data-queue-full={full || undefined} class="flex min-w-0 flex-col gap-gutter">
24
+ {#if full}
25
+ {#if fullContent}
26
+ {@render fullContent()}
27
+ {:else}
28
+ <p role="status" class="px-inset-control text-body text-warning">{fullLabel}</p>
29
+ {/if}
30
+ {/if}
31
+ <ul
32
+ aria-label={label}
33
+ class={cn('flex max-h-48 flex-col gap-gutter rounded-bar bg-well p-panel shadow-sm', className)}
34
+ {...restProps}
35
+ >
36
+ {@render children?.()}
37
+ </ul>
38
+ </div>
@@ -1,7 +1,11 @@
1
1
  import type { Snippet } from 'svelte';
2
- type $$ComponentProps = {
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ type $$ComponentProps = HTMLAttributes<HTMLUListElement> & {
4
+ full?: boolean;
5
+ fullLabel?: string;
6
+ label?: string;
7
+ fullContent?: Snippet;
3
8
  children?: Snippet;
4
- class?: string;
5
9
  };
6
10
  declare const Queue: import("svelte").Component<$$ComponentProps, {}, "">;
7
11
  type Queue = ReturnType<typeof Queue>;
@@ -1,4 +1,4 @@
1
- export { default as Root, type ToolState } from './tool.svelte';
1
+ export { default as Root, toolStepStatus, type ToolState, type ToolStatus } from './tool.svelte';
2
2
  export { default as Header } from './tool-header.svelte';
3
3
  export { default as Input } from './tool-input.svelte';
4
4
  export { default as Output } from './tool-output.svelte';
@@ -1,4 +1,4 @@
1
- export { default as Root } from './tool.svelte';
1
+ export { default as Root, toolStepStatus } from './tool.svelte';
2
2
  export { default as Header } from './tool-header.svelte';
3
3
  export { default as Input } from './tool-input.svelte';
4
4
  export { default as Output } from './tool-output.svelte';
@@ -2,7 +2,7 @@
2
2
  import type { Component, Snippet } from 'svelte';
3
3
  import Wrench from '@lucide/svelte/icons/wrench';
4
4
  import { cn } from '../../utils/index.js';
5
- import type { ToolState } from './tool.svelte';
5
+ import { toolStepStatus, type ToolState } from './tool.svelte';
6
6
 
7
7
  let {
8
8
  state: toolState = 'input-streaming',
@@ -25,17 +25,7 @@
25
25
  class?: string;
26
26
  } = $props();
27
27
 
28
- // Same mapping tool.svelte uses to drive Step status.
29
- const status = $derived(
30
- (
31
- {
32
- 'input-streaming': 'pending',
33
- 'input-available': 'running',
34
- 'output-available': 'done',
35
- 'output-error': 'error'
36
- } as const
37
- )[toolState]
38
- );
28
+ const status = $derived(toolStepStatus(toolState));
39
29
  </script>
40
30
 
41
31
  <!-- Compact horizontal tool call: a quiet pill that brightens on hover and
@@ -1,5 +1,5 @@
1
1
  import type { Component, Snippet } from 'svelte';
2
- import type { ToolState } from './tool.svelte';
2
+ import { type ToolState } from './tool.svelte';
3
3
  type $$ComponentProps = {
4
4
  /** AI-SDK tool lifecycle — drives the pulse and the failure dot. */
5
5
  state?: ToolState;
@@ -1,37 +1,67 @@
1
1
  <script lang="ts" module>
2
- /** AI-SDK tool part lifecycle. */
2
+ export type ToolStatus =
3
+ 'pending' | 'running' | 'complete' | 'error' | 'approval-required' | 'cancelled';
3
4
  export type ToolState =
4
- 'input-streaming' | 'input-available' | 'output-available' | 'output-error';
5
+ ToolStatus | 'input-streaming' | 'input-available' | 'output-available' | 'output-error';
6
+ export function toolStepStatus(state: ToolState): 'pending' | 'running' | 'done' | 'error' {
7
+ return (
8
+ {
9
+ pending: 'pending',
10
+ running: 'running',
11
+ complete: 'done',
12
+ error: 'error',
13
+ 'approval-required': 'pending',
14
+ cancelled: 'done',
15
+ 'input-streaming': 'pending',
16
+ 'input-available': 'running',
17
+ 'output-available': 'done',
18
+ 'output-error': 'error'
19
+ } as const
20
+ )[state];
21
+ }
5
22
  </script>
6
23
 
7
24
  <script lang="ts">
8
25
  import type { Snippet } from 'svelte';
9
26
  import * as Step from '../../ui/step/index.js';
10
-
11
27
  let {
12
- state: toolState = 'input-streaming',
28
+ status,
29
+ state = 'pending',
13
30
  open = $bindable(false),
31
+ header,
32
+ input,
33
+ progress,
34
+ approval,
35
+ result,
36
+ error,
37
+ actions,
14
38
  children,
15
39
  class: className
16
40
  }: {
41
+ status?: ToolStatus;
17
42
  state?: ToolState;
18
43
  open?: boolean;
44
+ header?: Snippet;
45
+ input?: Snippet;
46
+ progress?: Snippet;
47
+ approval?: Snippet;
48
+ result?: Snippet;
49
+ error?: Snippet;
50
+ actions?: Snippet;
19
51
  children?: Snippet;
20
52
  class?: string;
21
53
  } = $props();
22
-
23
- const status = $derived(
24
- (
25
- {
26
- 'input-streaming': 'pending',
27
- 'input-available': 'running',
28
- 'output-available': 'done',
29
- 'output-error': 'error'
30
- } as const
31
- )[toolState]
32
- );
54
+ const resolved = $derived(status ?? state);
55
+ const stepStatus = $derived(toolStepStatus(resolved));
33
56
  </script>
34
57
 
35
- <Step.Root {status} bind:open class={className}>
58
+ <Step.Root status={stepStatus} bind:open class={className} data-tool-status={resolved}>
59
+ {@render header?.()}
60
+ {@render input?.()}
61
+ {@render progress?.()}
62
+ {@render approval?.()}
63
+ {@render result?.()}
64
+ {@render error?.()}
65
+ {@render actions?.()}
36
66
  {@render children?.()}
37
67
  </Step.Root>