@signal9/era-ui 34.4.0 → 34.5.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 +17 -0
- package/dist/ai/context/conversation-capacity.svelte +89 -0
- package/dist/ai/context/conversation-capacity.svelte.d.ts +15 -0
- package/dist/ai/context/index.d.ts +1 -0
- package/dist/ai/context/index.js +1 -0
- package/dist/ai/index.d.ts +1 -0
- package/dist/ai/index.js +1 -0
- package/dist/ai/model-selector/index.d.ts +7 -0
- package/dist/ai/model-selector/index.js +7 -0
- package/dist/ai/model-selector/model-selector.svelte +28 -0
- package/dist/ai/model-selector/model-selector.svelte.d.ts +4 -0
- package/dist/ai/run-status/run-status.svelte +45 -1
- package/dist/ai/run-status/run-status.svelte.d.ts +1 -1
- package/dist/ai/tool/index.d.ts +1 -0
- package/dist/ai/tool/index.js +1 -0
- package/dist/ai/tool/tool-header.svelte +8 -1
- package/dist/ai/tool/tool-header.svelte.d.ts +2 -0
- package/dist/ai/tool/tool-inspector.svelte +236 -0
- package/dist/ai/tool/tool-inspector.svelte.d.ts +61 -0
- package/dist/ai/tool/tool.svelte +25 -7
- package/dist/ai/tool/tool.svelte.d.ts +1 -0
- package/dist/era-ui.css +1 -1
- package/dist/styles/prose.css +1 -0
- package/dist/styles/surfaces/bevel.css +5 -5
- package/dist/ui/step/step-root.svelte +4 -1
- package/package.json +1 -1
- package/skills/era-ui/references/releases/34.md +17 -0
- package/skills/era-ui/references/releases/index.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
## [34.5.0](https://github.com/sig-nine/era-ui/compare/v34.4.1...v34.5.0) (2026-08-28)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* **ai:** add compact tool inspector ([1fe91ec](https://github.com/sig-nine/era-ui/commit/1fe91ec0eaaf121ad16bc75de4ebe893e222b988))
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
* **ai:** render inspector input details ([2400f80](https://github.com/sig-nine/era-ui/commit/2400f806ed9aabed901bc2625751096453f168b5))
|
|
10
|
+
* **notes:** focus editor from trailing surface ([d4de399](https://github.com/sig-nine/era-ui/commit/d4de399d03b1d85db55b99aed1d5ffe7e0d5fcea))
|
|
11
|
+
|
|
12
|
+
## [34.4.1](https://github.com/sig-nine/era-ui/compare/v34.4.0...v34.4.1) (2026-08-26)
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* **bevel:** unify compact keycap travel ([7ca2e28](https://github.com/sig-nine/era-ui/commit/7ca2e2874a6bbe6e6f6a0aff811462bdab40aad6))
|
|
17
|
+
|
|
1
18
|
## [34.4.0](https://github.com/sig-nine/era-ui/compare/v34.3.0...v34.4.0) (2026-08-25)
|
|
2
19
|
|
|
3
20
|
### Features
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type ConversationCapacityPhase =
|
|
3
|
+
'quiet' | 'preparing' | 'compacting' | 'complete' | 'failed';
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
8
|
+
import LoaderCircle from '@lucide/svelte/icons/loader-circle';
|
|
9
|
+
import CircleCheck from '@lucide/svelte/icons/circle-check';
|
|
10
|
+
import CircleAlert from '@lucide/svelte/icons/circle-alert';
|
|
11
|
+
import * as Collapsible from '../../ui/collapsible/index.js';
|
|
12
|
+
import { cn } from '../../utils/index.js';
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
phase = 'quiet',
|
|
16
|
+
used,
|
|
17
|
+
total,
|
|
18
|
+
retained,
|
|
19
|
+
condensedToolResults,
|
|
20
|
+
setAsideTurns,
|
|
21
|
+
open = $bindable(false),
|
|
22
|
+
threshold = 0.7,
|
|
23
|
+
class: className
|
|
24
|
+
}: HTMLAttributes<HTMLDivElement> & {
|
|
25
|
+
phase?: ConversationCapacityPhase;
|
|
26
|
+
used?: number;
|
|
27
|
+
total?: number;
|
|
28
|
+
retained?: number;
|
|
29
|
+
condensedToolResults?: number;
|
|
30
|
+
setAsideTurns?: number;
|
|
31
|
+
open?: boolean;
|
|
32
|
+
threshold?: number;
|
|
33
|
+
} = $props();
|
|
34
|
+
const ratio = $derived(total && used !== undefined ? Math.max(0, Math.min(1, used / total)) : 0);
|
|
35
|
+
const hidden = $derived(phase === 'quiet' && ratio < threshold);
|
|
36
|
+
const label = $derived(
|
|
37
|
+
phase === 'preparing' || phase === 'compacting'
|
|
38
|
+
? 'Making room for this conversation…'
|
|
39
|
+
: phase === 'complete'
|
|
40
|
+
? 'Conversation space refreshed.'
|
|
41
|
+
: phase === 'failed'
|
|
42
|
+
? 'Conversation space could not be refreshed. You can continue safely.'
|
|
43
|
+
: 'Conversation capacity'
|
|
44
|
+
);
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
{#if !hidden}
|
|
48
|
+
<Collapsible.Root bind:open class={cn('min-w-0', className)}>
|
|
49
|
+
<Collapsible.Trigger
|
|
50
|
+
class="flex h-control w-full items-center gap-gutter px-inset-control text-left text-body"
|
|
51
|
+
>
|
|
52
|
+
{#if phase === 'preparing' || phase === 'compacting'}<LoaderCircle
|
|
53
|
+
class="size-icon shrink-0 animate-spin text-primary"
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
/>
|
|
56
|
+
{:else if phase === 'complete'}<CircleCheck
|
|
57
|
+
class="size-icon shrink-0 text-success"
|
|
58
|
+
aria-hidden="true"
|
|
59
|
+
/>
|
|
60
|
+
{:else if phase === 'failed'}<CircleAlert
|
|
61
|
+
class="size-icon shrink-0 text-warning"
|
|
62
|
+
aria-hidden="true"
|
|
63
|
+
/>{/if}
|
|
64
|
+
<span class="min-w-0 flex-1 truncate">{label}</span>
|
|
65
|
+
{#if total && used !== undefined}<span class="shrink-0 text-muted tabular-nums"
|
|
66
|
+
>{Math.round(ratio * 100)}%</span
|
|
67
|
+
>{/if}
|
|
68
|
+
</Collapsible.Trigger>
|
|
69
|
+
{#if total && used !== undefined}<div
|
|
70
|
+
class="mx-inset-control h-1 overflow-hidden rounded-full bg-well"
|
|
71
|
+
>
|
|
72
|
+
<div
|
|
73
|
+
class="h-full bg-primary duration-base motion-safe:transition-[width]"
|
|
74
|
+
style={`width: ${ratio * 100}%`}
|
|
75
|
+
></div>
|
|
76
|
+
</div>{/if}
|
|
77
|
+
<Collapsible.Content class="overflow-hidden"
|
|
78
|
+
><div
|
|
79
|
+
class="flex flex-wrap gap-x-panel gap-y-gutter px-inset-control py-gutter text-body text-muted"
|
|
80
|
+
>
|
|
81
|
+
{#if retained !== undefined}<span>{retained} retained</span
|
|
82
|
+
>{/if}{#if condensedToolResults !== undefined}<span
|
|
83
|
+
>{condensedToolResults} older tool results condensed</span
|
|
84
|
+
>{/if}{#if setAsideTurns !== undefined}<span>{setAsideTurns} earlier turns set aside</span
|
|
85
|
+
>{/if}
|
|
86
|
+
</div></Collapsible.Content
|
|
87
|
+
>
|
|
88
|
+
</Collapsible.Root>
|
|
89
|
+
{/if}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type ConversationCapacityPhase = 'quiet' | 'preparing' | 'compacting' | 'complete' | 'failed';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
type $$ComponentProps = HTMLAttributes<HTMLDivElement> & {
|
|
4
|
+
phase?: ConversationCapacityPhase;
|
|
5
|
+
used?: number;
|
|
6
|
+
total?: number;
|
|
7
|
+
retained?: number;
|
|
8
|
+
condensedToolResults?: number;
|
|
9
|
+
setAsideTurns?: number;
|
|
10
|
+
open?: boolean;
|
|
11
|
+
threshold?: number;
|
|
12
|
+
};
|
|
13
|
+
declare const ConversationCapacity: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
14
|
+
type ConversationCapacity = ReturnType<typeof ConversationCapacity>;
|
|
15
|
+
export default ConversationCapacity;
|
package/dist/ai/context/index.js
CHANGED
package/dist/ai/index.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export * as Swarm from './swarm/index.js';
|
|
|
22
22
|
export * as CodeMode from './code-mode/index.js';
|
|
23
23
|
export { Response, repairStreamingMarkdown } from './response/index.js';
|
|
24
24
|
export { default as ContextGauge } from './context/context.svelte';
|
|
25
|
+
export * as Context from './context/index.js';
|
|
25
26
|
export { default as ModelSelector } from './model-selector/model-selector.svelte';
|
|
26
27
|
export type { ModelGroup, ModelOption } from './model-selector/index.js';
|
|
27
28
|
export { default as OpenInChat, providers as openInProviders } from './open-in-chat/open-in-chat.svelte';
|
package/dist/ai/index.js
CHANGED
|
@@ -27,6 +27,7 @@ export * as Swarm from './swarm/index.js';
|
|
|
27
27
|
export * as CodeMode from './code-mode/index.js';
|
|
28
28
|
export { Response, repairStreamingMarkdown } from './response/index.js';
|
|
29
29
|
export { default as ContextGauge } from './context/context.svelte';
|
|
30
|
+
export * as Context from './context/index.js';
|
|
30
31
|
export { default as ModelSelector } from './model-selector/model-selector.svelte';
|
|
31
32
|
export { default as OpenInChat, providers as openInProviders } from './open-in-chat/open-in-chat.svelte';
|
|
32
33
|
export { default as WebPreview } from './web-preview/web-preview.svelte';
|
|
@@ -1,2 +1,9 @@
|
|
|
1
|
+
import * as Dialog from '../../ui/dialog/index.js';
|
|
2
|
+
import * as Command from '../../ui/command/index.js';
|
|
1
3
|
export { default as Root } from './model-selector.svelte';
|
|
4
|
+
/** Composable dialog parts for applications that need a custom picker trigger or layout. */
|
|
5
|
+
export declare const Trigger: typeof Dialog.Trigger;
|
|
6
|
+
export declare const Content: typeof Dialog.Content;
|
|
7
|
+
export declare const Group: typeof Command.Group;
|
|
8
|
+
export declare const Item: typeof Command.Item;
|
|
2
9
|
export type { ModelGroup, ModelOption } from './model-selector.svelte';
|
|
@@ -1 +1,8 @@
|
|
|
1
|
+
import * as Dialog from '../../ui/dialog/index.js';
|
|
2
|
+
import * as Command from '../../ui/command/index.js';
|
|
1
3
|
export { default as Root } from './model-selector.svelte';
|
|
4
|
+
/** Composable dialog parts for applications that need a custom picker trigger or layout. */
|
|
5
|
+
export const Trigger = Dialog.Trigger;
|
|
6
|
+
export const Content = Dialog.Content;
|
|
7
|
+
export const Group = Command.Group;
|
|
8
|
+
export const Item = Command.Item;
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import * as Dialog from '../../ui/dialog/index.js';
|
|
25
25
|
import * as Command from '../../ui/command/index.js';
|
|
26
26
|
import Search from '@lucide/svelte/icons/search';
|
|
27
|
+
import ChevronDown from '@lucide/svelte/icons/chevron-down';
|
|
27
28
|
import { cn } from '../../utils/index.js';
|
|
28
29
|
|
|
29
30
|
let {
|
|
@@ -32,6 +33,8 @@
|
|
|
32
33
|
groups,
|
|
33
34
|
value = $bindable(''),
|
|
34
35
|
offline = false,
|
|
36
|
+
shortLabel = false,
|
|
37
|
+
showTrigger = true,
|
|
35
38
|
onSelect,
|
|
36
39
|
class: className
|
|
37
40
|
}: {
|
|
@@ -43,14 +46,39 @@
|
|
|
43
46
|
value?: string;
|
|
44
47
|
/** Never loads remote logo URLs; local icon snippets continue to render. */
|
|
45
48
|
offline?: boolean;
|
|
49
|
+
/** Use the provider/name only label when the picker sits in a narrow toolbar. */
|
|
50
|
+
shortLabel?: boolean;
|
|
51
|
+
/** Set false when composing a custom Dialog trigger around this picker. */
|
|
52
|
+
showTrigger?: boolean;
|
|
46
53
|
onSelect?: (id: string, model: ModelOption) => void;
|
|
47
54
|
class?: string;
|
|
48
55
|
} = $props();
|
|
49
56
|
|
|
50
57
|
const visibleGroups = $derived(groups ?? [{ id: 'models', label: 'Models', models }]);
|
|
58
|
+
const selected = $derived(
|
|
59
|
+
visibleGroups.flatMap((group) => group.models).find((model) => model.id === value)
|
|
60
|
+
);
|
|
51
61
|
</script>
|
|
52
62
|
|
|
53
63
|
<Dialog.Root bind:open>
|
|
64
|
+
{#if showTrigger}
|
|
65
|
+
<Dialog.Trigger
|
|
66
|
+
size="control"
|
|
67
|
+
disabled={offline || !selected}
|
|
68
|
+
aria-label={selected ? `Choose model, current ${selected.name}` : 'Choose model'}
|
|
69
|
+
aria-haspopup="dialog"
|
|
70
|
+
>
|
|
71
|
+
{#if selected?.icon}{@render selected.icon()}{/if}
|
|
72
|
+
<span class="min-w-0 truncate"
|
|
73
|
+
>{shortLabel
|
|
74
|
+
? (selected?.name ?? 'Model')
|
|
75
|
+
: selected?.provider
|
|
76
|
+
? `${selected.provider} · ${selected.name}`
|
|
77
|
+
: (selected?.name ?? 'Choose model')}</span
|
|
78
|
+
>
|
|
79
|
+
<ChevronDown class="size-icon shrink-0 text-muted" aria-hidden="true" />
|
|
80
|
+
</Dialog.Trigger>
|
|
81
|
+
{/if}
|
|
54
82
|
<Dialog.Content class={cn('max-w-md p-gutter', className)}>
|
|
55
83
|
<Command.Root bare>
|
|
56
84
|
<Command.Input icon={Search} placeholder="Search models…" autofocus />
|
|
@@ -24,6 +24,10 @@ type $$ComponentProps = {
|
|
|
24
24
|
value?: string;
|
|
25
25
|
/** Never loads remote logo URLs; local icon snippets continue to render. */
|
|
26
26
|
offline?: boolean;
|
|
27
|
+
/** Use the provider/name only label when the picker sits in a narrow toolbar. */
|
|
28
|
+
shortLabel?: boolean;
|
|
29
|
+
/** Set false when composing a custom Dialog trigger around this picker. */
|
|
30
|
+
showTrigger?: boolean;
|
|
27
31
|
onSelect?: (id: string, model: ModelOption) => void;
|
|
28
32
|
class?: string;
|
|
29
33
|
};
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
2
|
/** Aggregate run lifecycle. `running` pulses; the three active states
|
|
3
3
|
* (queued/running/awaiting) surface the Cancel affordance. */
|
|
4
|
-
export type RunState =
|
|
4
|
+
export type RunState =
|
|
5
|
+
| 'idle'
|
|
6
|
+
| 'queued'
|
|
7
|
+
| 'running'
|
|
8
|
+
| 'thinking'
|
|
9
|
+
| 'tool'
|
|
10
|
+
| 'browser'
|
|
11
|
+
| 'compacting'
|
|
12
|
+
| 'speaking'
|
|
13
|
+
| 'cancelling'
|
|
14
|
+
| 'awaiting'
|
|
15
|
+
| 'done'
|
|
16
|
+
| 'cancelled'
|
|
17
|
+
| 'error';
|
|
5
18
|
|
|
6
19
|
/** Tone for the latest-log line — reuses the era semantic palette. */
|
|
7
20
|
export type LogTone = 'default' | 'muted' | 'success' | 'warning' | 'destructive';
|
|
@@ -25,6 +38,30 @@
|
|
|
25
38
|
idle: { label: 'idle', dot: 'bg-muted', text: 'text-muted', active: false, pulse: false },
|
|
26
39
|
queued: { label: 'queued', dot: 'bg-muted', text: 'text-muted', active: true, pulse: false },
|
|
27
40
|
running: { label: 'running', dot: 'bg-primary', text: 'text-fg', active: true, pulse: true },
|
|
41
|
+
thinking: { label: 'thinking', dot: 'bg-primary', text: 'text-fg', active: true, pulse: true },
|
|
42
|
+
tool: { label: 'using a tool', dot: 'bg-primary', text: 'text-fg', active: true, pulse: true },
|
|
43
|
+
browser: {
|
|
44
|
+
label: 'working in browser',
|
|
45
|
+
dot: 'bg-primary',
|
|
46
|
+
text: 'text-fg',
|
|
47
|
+
active: true,
|
|
48
|
+
pulse: true
|
|
49
|
+
},
|
|
50
|
+
compacting: {
|
|
51
|
+
label: 'making room',
|
|
52
|
+
dot: 'bg-primary',
|
|
53
|
+
text: 'text-fg',
|
|
54
|
+
active: true,
|
|
55
|
+
pulse: true
|
|
56
|
+
},
|
|
57
|
+
speaking: { label: 'speaking', dot: 'bg-primary', text: 'text-fg', active: true, pulse: true },
|
|
58
|
+
cancelling: {
|
|
59
|
+
label: 'stopping',
|
|
60
|
+
dot: 'bg-muted',
|
|
61
|
+
text: 'text-muted',
|
|
62
|
+
active: true,
|
|
63
|
+
pulse: true
|
|
64
|
+
},
|
|
28
65
|
awaiting: {
|
|
29
66
|
label: 'awaiting',
|
|
30
67
|
dot: 'bg-warning',
|
|
@@ -33,6 +70,13 @@
|
|
|
33
70
|
pulse: false
|
|
34
71
|
},
|
|
35
72
|
done: { label: 'done', dot: 'bg-success', text: 'text-success', active: false, pulse: false },
|
|
73
|
+
cancelled: {
|
|
74
|
+
label: 'stopped',
|
|
75
|
+
dot: 'bg-muted',
|
|
76
|
+
text: 'text-muted',
|
|
77
|
+
active: false,
|
|
78
|
+
pulse: false
|
|
79
|
+
},
|
|
36
80
|
error: {
|
|
37
81
|
label: 'error',
|
|
38
82
|
dot: 'bg-destructive',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Aggregate run lifecycle. `running` pulses; the three active states
|
|
2
2
|
* (queued/running/awaiting) surface the Cancel affordance. */
|
|
3
|
-
export type RunState = 'idle' | 'queued' | 'running' | 'awaiting' | 'done' | 'error';
|
|
3
|
+
export type RunState = 'idle' | 'queued' | 'running' | 'thinking' | 'tool' | 'browser' | 'compacting' | 'speaking' | 'cancelling' | 'awaiting' | 'done' | 'cancelled' | 'error';
|
|
4
4
|
/** Tone for the latest-log line — reuses the era semantic palette. */
|
|
5
5
|
export type LogTone = 'default' | 'muted' | 'success' | 'warning' | 'destructive';
|
|
6
6
|
export interface RunTools {
|
package/dist/ai/tool/index.d.ts
CHANGED
|
@@ -5,4 +5,5 @@ export { default as Output } from './tool-output.svelte';
|
|
|
5
5
|
export { default as Chip } from './tool-chip.svelte';
|
|
6
6
|
export { default as Chips } from './tool-chips.svelte';
|
|
7
7
|
export { default as Exec } from './tool-exec.svelte';
|
|
8
|
+
export { default as Inspector, toolValue, type ToolMetric, type ToolPresentation } from './tool-inspector.svelte';
|
|
8
9
|
export { Content, Section } from '../../ui/step/index.js';
|
package/dist/ai/tool/index.js
CHANGED
|
@@ -5,4 +5,5 @@ export { default as Output } from './tool-output.svelte';
|
|
|
5
5
|
export { default as Chip } from './tool-chip.svelte';
|
|
6
6
|
export { default as Chips } from './tool-chips.svelte';
|
|
7
7
|
export { default as Exec } from './tool-exec.svelte';
|
|
8
|
+
export { default as Inspector, toolValue } from './tool-inspector.svelte';
|
|
8
9
|
export { Content, Section } from '../../ui/step/index.js';
|
|
@@ -5,16 +5,23 @@
|
|
|
5
5
|
|
|
6
6
|
let {
|
|
7
7
|
type,
|
|
8
|
+
icon,
|
|
8
9
|
children
|
|
9
10
|
}: {
|
|
10
11
|
/** The tool name, e.g. `search_flights`. */
|
|
11
12
|
type: string;
|
|
13
|
+
/** Optional local category icon; avoids any remote asset dependency. */
|
|
14
|
+
icon?: Snippet;
|
|
12
15
|
children?: Snippet;
|
|
13
16
|
} = $props();
|
|
14
17
|
</script>
|
|
15
18
|
|
|
16
19
|
<Step.Summary>
|
|
17
|
-
|
|
20
|
+
{#if icon}
|
|
21
|
+
{@render icon()}
|
|
22
|
+
{:else}
|
|
23
|
+
<Wrench class="size-icon shrink-0" aria-hidden="true" />
|
|
24
|
+
{/if}
|
|
18
25
|
<span class="truncate text-fg">{type}</span>
|
|
19
26
|
{@render children?.()}
|
|
20
27
|
</Step.Summary>
|
|
@@ -2,6 +2,8 @@ import type { Snippet } from 'svelte';
|
|
|
2
2
|
type $$ComponentProps = {
|
|
3
3
|
/** The tool name, e.g. `search_flights`. */
|
|
4
4
|
type: string;
|
|
5
|
+
/** Optional local category icon; avoids any remote asset dependency. */
|
|
6
|
+
icon?: Snippet;
|
|
5
7
|
children?: Snippet;
|
|
6
8
|
};
|
|
7
9
|
declare const ToolHeader: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export interface ToolMetric {
|
|
3
|
+
label: string;
|
|
4
|
+
value: string | number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Provider-neutral semantic presentation. Applications own all fetching and actions. */
|
|
8
|
+
export type ToolPresentation =
|
|
9
|
+
| { kind: 'generic' }
|
|
10
|
+
| { kind: 'code'; execution: import('../code-mode/types.js').CodeModeExecution }
|
|
11
|
+
| { kind: 'shell'; command?: string; stdout?: string; stderr?: string; exitCode?: number }
|
|
12
|
+
| { kind: 'table'; columns: string[]; rows: unknown[][]; maxRows?: number }
|
|
13
|
+
| { kind: 'chart'; description?: string }
|
|
14
|
+
| { kind: 'app'; label?: string; description?: string }
|
|
15
|
+
| {
|
|
16
|
+
kind: 'artifact';
|
|
17
|
+
name: string;
|
|
18
|
+
mimeType: string;
|
|
19
|
+
size?: string;
|
|
20
|
+
available?: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function toolValue(
|
|
24
|
+
value: unknown,
|
|
25
|
+
limit = 12_000
|
|
26
|
+
): {
|
|
27
|
+
value: unknown;
|
|
28
|
+
raw: string;
|
|
29
|
+
truncated: boolean;
|
|
30
|
+
encodedError?: string;
|
|
31
|
+
} {
|
|
32
|
+
let resolved = value;
|
|
33
|
+
if (typeof value === 'string') {
|
|
34
|
+
try {
|
|
35
|
+
resolved = JSON.parse(value);
|
|
36
|
+
} catch {
|
|
37
|
+
// A plain string remains a useful, valid raw result.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const encoded =
|
|
41
|
+
resolved && typeof resolved === 'object' && !Array.isArray(resolved)
|
|
42
|
+
? (resolved as Record<string, unknown>)
|
|
43
|
+
: undefined;
|
|
44
|
+
const encodedError =
|
|
45
|
+
encoded && (encoded.success === false || encoded.isError === true)
|
|
46
|
+
? String(encoded.error ?? encoded.message ?? 'Tool reported an error')
|
|
47
|
+
: undefined;
|
|
48
|
+
const raw =
|
|
49
|
+
typeof resolved === 'string' ? resolved : (JSON.stringify(resolved, null, 2) ?? 'null');
|
|
50
|
+
return {
|
|
51
|
+
value: resolved,
|
|
52
|
+
raw: raw.length > limit ? `${raw.slice(0, limit)}\n… truncated` : raw,
|
|
53
|
+
truncated: raw.length > limit,
|
|
54
|
+
encodedError
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<script lang="ts">
|
|
60
|
+
import type { Snippet } from 'svelte';
|
|
61
|
+
import * as Tabs from '../../ui/tabs/index.js';
|
|
62
|
+
import * as Table from '../../ui/table/index.js';
|
|
63
|
+
import { CodeBlock } from '../../ui/code-block/index.js';
|
|
64
|
+
import { CopyButton } from '../../ui/copy-button/index.js';
|
|
65
|
+
import { cn } from '../../utils/index.js';
|
|
66
|
+
import * as CodeMode from '../code-mode/index.js';
|
|
67
|
+
import * as StructuredValue from '../structured-value/index.js';
|
|
68
|
+
import Root from './tool.svelte';
|
|
69
|
+
import Header from './tool-header.svelte';
|
|
70
|
+
|
|
71
|
+
let {
|
|
72
|
+
name,
|
|
73
|
+
status = 'pending',
|
|
74
|
+
input,
|
|
75
|
+
output,
|
|
76
|
+
error,
|
|
77
|
+
durationMs,
|
|
78
|
+
metrics = [],
|
|
79
|
+
presentation = { kind: 'generic' },
|
|
80
|
+
icon,
|
|
81
|
+
chart,
|
|
82
|
+
actions,
|
|
83
|
+
open = $bindable(false),
|
|
84
|
+
class: className
|
|
85
|
+
}: {
|
|
86
|
+
name: string;
|
|
87
|
+
status?: import('./tool.svelte').ToolStatus;
|
|
88
|
+
input?: unknown;
|
|
89
|
+
output?: unknown;
|
|
90
|
+
error?: string;
|
|
91
|
+
durationMs?: number;
|
|
92
|
+
metrics?: ToolMetric[];
|
|
93
|
+
presentation?: ToolPresentation;
|
|
94
|
+
icon?: Snippet;
|
|
95
|
+
/** A trusted application chart; the raw result always remains available. */
|
|
96
|
+
chart?: Snippet<[unknown]>;
|
|
97
|
+
actions?: Snippet;
|
|
98
|
+
open?: boolean;
|
|
99
|
+
class?: string;
|
|
100
|
+
} = $props();
|
|
101
|
+
|
|
102
|
+
let rawView = $state<'structured' | 'raw'>('structured');
|
|
103
|
+
let search = $state('');
|
|
104
|
+
const resultPayload = $derived(toolValue(output));
|
|
105
|
+
const reportedError = $derived(error ?? resultPayload.encodedError);
|
|
106
|
+
const visibleRaw = $derived(
|
|
107
|
+
search
|
|
108
|
+
? resultPayload.raw
|
|
109
|
+
.split('\n')
|
|
110
|
+
.filter((line) => line.toLowerCase().includes(search.toLowerCase()))
|
|
111
|
+
.join('\n')
|
|
112
|
+
: resultPayload.raw
|
|
113
|
+
);
|
|
114
|
+
const tableRows = $derived(
|
|
115
|
+
presentation.kind === 'table' ? presentation.rows.slice(0, presentation.maxRows ?? 100) : []
|
|
116
|
+
);
|
|
117
|
+
const active = $derived(
|
|
118
|
+
status === 'pending' || status === 'running' || status === 'approval-required'
|
|
119
|
+
);
|
|
120
|
+
</script>
|
|
121
|
+
|
|
122
|
+
{#snippet inputDetail()}
|
|
123
|
+
<StructuredValue.Root value={toolValue(input).value} />
|
|
124
|
+
{/snippet}
|
|
125
|
+
|
|
126
|
+
<Root
|
|
127
|
+
bind:open
|
|
128
|
+
{status}
|
|
129
|
+
input={input === undefined ? undefined : inputDetail}
|
|
130
|
+
class={cn('min-w-0', className)}
|
|
131
|
+
>
|
|
132
|
+
{#snippet header()}
|
|
133
|
+
<Header type={name} {icon}>
|
|
134
|
+
{#if active}<span
|
|
135
|
+
class="ml-auto size-1.5 shrink-0 rounded-full bg-primary motion-safe:animate-pulse"
|
|
136
|
+
aria-label="active"
|
|
137
|
+
></span>{/if}
|
|
138
|
+
{#if durationMs !== undefined}<span class="ml-auto shrink-0 text-muted tabular-nums"
|
|
139
|
+
>{durationMs}ms</span
|
|
140
|
+
>{/if}
|
|
141
|
+
{#each metrics.slice(0, 2) as metric (metric.label)}
|
|
142
|
+
<span class="shrink-0 text-muted tabular-nums">{metric.value} {metric.label}</span>
|
|
143
|
+
{/each}
|
|
144
|
+
</Header>
|
|
145
|
+
{/snippet}
|
|
146
|
+
{#snippet result()}
|
|
147
|
+
{#if presentation.kind === 'code'}
|
|
148
|
+
<CodeMode.Root {...presentation.execution} />
|
|
149
|
+
{:else if presentation.kind === 'shell'}
|
|
150
|
+
<div class="flex flex-col gap-panel">
|
|
151
|
+
{#if presentation.command}<CodeBlock code={`$ ${presentation.command}`} />{/if}
|
|
152
|
+
{#if presentation.stdout}<CodeBlock code={presentation.stdout} />{/if}
|
|
153
|
+
{#if presentation.stderr}<p class="text-body whitespace-pre-wrap text-destructive">
|
|
154
|
+
{presentation.stderr}
|
|
155
|
+
</p>{/if}
|
|
156
|
+
{#if presentation.exitCode !== undefined}<p class="text-body text-muted">
|
|
157
|
+
exit code {presentation.exitCode}
|
|
158
|
+
</p>{/if}
|
|
159
|
+
</div>
|
|
160
|
+
{:else if presentation.kind === 'table'}
|
|
161
|
+
<div class="flex flex-col gap-gutter">
|
|
162
|
+
<p class="text-body text-muted">
|
|
163
|
+
{presentation.rows.length} rows · {presentation.columns.length} columns
|
|
164
|
+
</p>
|
|
165
|
+
<div class="overflow-auto">
|
|
166
|
+
<Table.Root
|
|
167
|
+
><Table.Head
|
|
168
|
+
><Table.Row
|
|
169
|
+
>{#each presentation.columns as column (column)}<Table.HeadCell
|
|
170
|
+
>{column}</Table.HeadCell
|
|
171
|
+
>{/each}</Table.Row
|
|
172
|
+
></Table.Head
|
|
173
|
+
><Table.Body
|
|
174
|
+
>{#each tableRows as row, i (i)}<Table.Row
|
|
175
|
+
>{#each row as cell, j (j)}<Table.Cell>{String(cell ?? '')}</Table.Cell
|
|
176
|
+
>{/each}</Table.Row
|
|
177
|
+
>{/each}</Table.Body
|
|
178
|
+
></Table.Root
|
|
179
|
+
>
|
|
180
|
+
</div>
|
|
181
|
+
{#if tableRows.length < presentation.rows.length}<p class="text-body text-muted">
|
|
182
|
+
Showing the first {tableRows.length} rows.
|
|
183
|
+
</p>{/if}
|
|
184
|
+
</div>
|
|
185
|
+
{:else if presentation.kind === 'chart' && chart}
|
|
186
|
+
{@render chart(resultPayload.value)}
|
|
187
|
+
{:else if presentation.kind === 'app'}
|
|
188
|
+
<p class="text-body text-muted">
|
|
189
|
+
{presentation.description ?? `${presentation.label ?? name} opened a visual destination.`}
|
|
190
|
+
</p>
|
|
191
|
+
{:else if presentation.kind === 'artifact'}
|
|
192
|
+
<div class="flex flex-wrap items-center gap-gutter text-body">
|
|
193
|
+
<strong class="text-fg">{presentation.name}</strong><span class="text-muted"
|
|
194
|
+
>{presentation.mimeType}</span
|
|
195
|
+
>{#if presentation.size}<span class="text-muted">{presentation.size}</span>{/if}<span
|
|
196
|
+
class={presentation.available === false ? 'text-warning' : 'text-success'}
|
|
197
|
+
>{presentation.available === false ? 'unavailable' : 'available'}</span
|
|
198
|
+
>{@render actions?.()}
|
|
199
|
+
</div>
|
|
200
|
+
{:else if output !== undefined}
|
|
201
|
+
<Tabs.Root bind:value={rawView}>
|
|
202
|
+
<Tabs.List
|
|
203
|
+
><Tabs.Trigger value="structured">structured</Tabs.Trigger><Tabs.Trigger value="raw"
|
|
204
|
+
>raw</Tabs.Trigger
|
|
205
|
+
></Tabs.List
|
|
206
|
+
>
|
|
207
|
+
<Tabs.Content value="structured" class="pt-panel"
|
|
208
|
+
><StructuredValue.Root value={resultPayload.value} /></Tabs.Content
|
|
209
|
+
>
|
|
210
|
+
<Tabs.Content value="raw" class="flex flex-col gap-gutter pt-panel"
|
|
211
|
+
><div class="flex gap-gutter">
|
|
212
|
+
<input
|
|
213
|
+
aria-label="Search raw output"
|
|
214
|
+
bind:value={search}
|
|
215
|
+
placeholder="Search"
|
|
216
|
+
class="min-w-0 flex-1 bg-well px-inset-control text-body outline-none"
|
|
217
|
+
/><CopyButton text={resultPayload.raw} label="Copy raw output" />
|
|
218
|
+
</div>
|
|
219
|
+
<CodeBlock
|
|
220
|
+
code={visibleRaw || 'No matching lines'}
|
|
221
|
+
class="whitespace-pre-wrap"
|
|
222
|
+
/></Tabs.Content
|
|
223
|
+
>
|
|
224
|
+
</Tabs.Root>
|
|
225
|
+
{/if}
|
|
226
|
+
{#if resultPayload.truncated}<p class="mt-gutter text-body text-muted">
|
|
227
|
+
Raw output is bounded to keep this transcript responsive.
|
|
228
|
+
</p>{/if}
|
|
229
|
+
{#if reportedError}<p
|
|
230
|
+
role="alert"
|
|
231
|
+
class="mt-gutter text-body whitespace-pre-wrap text-destructive"
|
|
232
|
+
>
|
|
233
|
+
{reportedError}
|
|
234
|
+
</p>{/if}
|
|
235
|
+
{/snippet}
|
|
236
|
+
</Root>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface ToolMetric {
|
|
2
|
+
label: string;
|
|
3
|
+
value: string | number;
|
|
4
|
+
}
|
|
5
|
+
/** Provider-neutral semantic presentation. Applications own all fetching and actions. */
|
|
6
|
+
export type ToolPresentation = {
|
|
7
|
+
kind: 'generic';
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'code';
|
|
10
|
+
execution: import('../code-mode/types.js').CodeModeExecution;
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'shell';
|
|
13
|
+
command?: string;
|
|
14
|
+
stdout?: string;
|
|
15
|
+
stderr?: string;
|
|
16
|
+
exitCode?: number;
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'table';
|
|
19
|
+
columns: string[];
|
|
20
|
+
rows: unknown[][];
|
|
21
|
+
maxRows?: number;
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'chart';
|
|
24
|
+
description?: string;
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'app';
|
|
27
|
+
label?: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
} | {
|
|
30
|
+
kind: 'artifact';
|
|
31
|
+
name: string;
|
|
32
|
+
mimeType: string;
|
|
33
|
+
size?: string;
|
|
34
|
+
available?: boolean;
|
|
35
|
+
};
|
|
36
|
+
export declare function toolValue(value: unknown, limit?: number): {
|
|
37
|
+
value: unknown;
|
|
38
|
+
raw: string;
|
|
39
|
+
truncated: boolean;
|
|
40
|
+
encodedError?: string;
|
|
41
|
+
};
|
|
42
|
+
import type { Snippet } from 'svelte';
|
|
43
|
+
type $$ComponentProps = {
|
|
44
|
+
name: string;
|
|
45
|
+
status?: import('./tool.svelte').ToolStatus;
|
|
46
|
+
input?: unknown;
|
|
47
|
+
output?: unknown;
|
|
48
|
+
error?: string;
|
|
49
|
+
durationMs?: number;
|
|
50
|
+
metrics?: ToolMetric[];
|
|
51
|
+
presentation?: ToolPresentation;
|
|
52
|
+
icon?: Snippet;
|
|
53
|
+
/** A trusted application chart; the raw result always remains available. */
|
|
54
|
+
chart?: Snippet<[unknown]>;
|
|
55
|
+
actions?: Snippet;
|
|
56
|
+
open?: boolean;
|
|
57
|
+
class?: string;
|
|
58
|
+
};
|
|
59
|
+
declare const ToolInspector: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
60
|
+
type ToolInspector = ReturnType<typeof ToolInspector>;
|
|
61
|
+
export default ToolInspector;
|