@tangle-network/ui 11.0.0 → 11.2.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 +30 -0
- package/dist/{artifact-pane-DvJyPWV4.d.ts → artifact-pane-DpuWcAiF.d.ts} +10 -1
- package/dist/chat.d.ts +8 -2
- package/dist/chat.js +5 -5
- package/dist/{chunk-52Y3FMFI.js → chunk-3M7BPSSW.js} +1 -1
- package/dist/chunk-4ORVXSMU.js +507 -0
- package/dist/{chunk-LELGOLFV.js → chunk-GGDYWMFF.js} +15 -7
- package/dist/{chunk-CCM2KRQH.js → chunk-HW4ROISY.js} +1 -1
- package/dist/{chunk-KBOD47AW.js → chunk-J3YZIQNI.js} +2 -2
- package/dist/{chunk-CSAIKY36.js → chunk-LTEKI7YJ.js} +12 -7
- package/dist/{chunk-KHO7DYXH.js → chunk-OTZWQJ5W.js} +107 -72
- package/dist/{chunk-FJBTCTZM.js → chunk-P5F43A65.js} +1 -1
- package/dist/{chunk-SZ44QDA6.js → chunk-RADUPP44.js} +12 -5
- package/dist/{chunk-HHYUPAOJ.js → chunk-SRAKCVPO.js} +49 -81
- package/dist/{document-editor-pane-DyDEX_Zm.d.ts → document-editor-pane-C3cYEDWZ.d.ts} +2 -2
- package/dist/{document-editor-pane-WCTA3ZOE.js → document-editor-pane-OQNGM4RJ.js} +3 -3
- package/dist/editor.d.ts +3 -3
- package/dist/editor.js +3 -3
- package/dist/files.d.ts +5 -3
- package/dist/files.js +3 -3
- package/dist/hooks.js +4 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +9 -9
- package/dist/markdown.js +1 -1
- package/dist/openui.js +2 -2
- package/dist/primitives.d.ts +1 -1
- package/dist/primitives.js +1 -1
- package/dist/run.d.ts +4 -3
- package/dist/run.js +5 -5
- package/dist/sdk-hooks.js +4 -4
- package/package.json +1 -1
- package/src/chat/agent-timeline.stories.tsx +11 -0
- package/src/chat/agent-timeline.test.tsx +34 -0
- package/src/chat/agent-timeline.tsx +65 -12
- package/src/chat/chat-container.tsx +5 -0
- package/src/editor/document-editor-pane.stories.tsx +74 -0
- package/src/editor/document-editor-pane.tsx +10 -3
- package/src/files/file-artifact-pane.tsx +14 -2
- package/src/files/file-preview.stories.tsx +22 -1
- package/src/files/file-preview.tsx +2 -2
- package/src/markdown/markdown.stories.tsx +55 -1
- package/src/markdown/markdown.test.tsx +23 -0
- package/src/markdown/markdown.tsx +1 -1
- package/src/primitives/artifact-pane.tsx +52 -26
- package/src/run/expandable-content.test.tsx +52 -0
- package/src/run/expandable-content.tsx +73 -0
- package/src/run/expanded-tool-detail.tsx +17 -12
- package/src/run/inline-thinking-item.stories.tsx +30 -1
- package/src/run/inline-thinking-item.tsx +34 -70
- package/src/run/inline-tool-item.tsx +30 -116
- package/src/run/run-row-shell.test.tsx +38 -0
- package/src/run/run-row-shell.tsx +191 -0
- package/dist/chunk-INMWNFAS.js +0 -369
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { ExpandableContent } from "./expandable-content";
|
|
5
|
+
|
|
6
|
+
describe("ExpandableContent", () => {
|
|
7
|
+
let scrollHeightSpy: ReturnType<typeof vi.spyOn> | undefined;
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
scrollHeightSpy?.mockRestore();
|
|
11
|
+
scrollHeightSpy = undefined;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("renders no toggle when content fits within the cap", () => {
|
|
15
|
+
render(
|
|
16
|
+
<ExpandableContent collapsedMaxPx={180}>
|
|
17
|
+
<div>short</div>
|
|
18
|
+
</ExpandableContent>,
|
|
19
|
+
);
|
|
20
|
+
expect(
|
|
21
|
+
screen.queryByRole("button", { name: /show more/i }),
|
|
22
|
+
).not.toBeInTheDocument();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("when content overflows", () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
scrollHeightSpy = vi
|
|
28
|
+
.spyOn(HTMLElement.prototype, "scrollHeight", "get")
|
|
29
|
+
.mockReturnValue(999);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("clamps with a Show more toggle that expands and collapses", async () => {
|
|
33
|
+
const user = userEvent.setup();
|
|
34
|
+
render(
|
|
35
|
+
<ExpandableContent collapsedMaxPx={180}>
|
|
36
|
+
<div>tall content</div>
|
|
37
|
+
</ExpandableContent>,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const showMore = screen.getByRole("button", { name: /show more/i });
|
|
41
|
+
await user.click(showMore);
|
|
42
|
+
expect(
|
|
43
|
+
screen.getByRole("button", { name: /show less/i }),
|
|
44
|
+
).toBeInTheDocument();
|
|
45
|
+
|
|
46
|
+
await user.click(screen.getByRole("button", { name: /show less/i }));
|
|
47
|
+
expect(
|
|
48
|
+
screen.getByRole("button", { name: /show more/i }),
|
|
49
|
+
).toBeInTheDocument();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
export interface ExpandableContentProps {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
/** Height (px) the content is clamped to while collapsed. */
|
|
7
|
+
collapsedMaxPx?: number;
|
|
8
|
+
/**
|
|
9
|
+
* Gradient from-color for the bottom fade, so it blends with the surface
|
|
10
|
+
* behind the content (e.g. `from-muted`, `from-card`, `from-[var(--surface-danger-bg)]`).
|
|
11
|
+
*/
|
|
12
|
+
fadeClassName?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Clamps tall content to a fixed height with a bottom fade and a
|
|
18
|
+
* "Show more / Show less" toggle. Content shorter than the cap renders
|
|
19
|
+
* as-is with no toggle.
|
|
20
|
+
*/
|
|
21
|
+
export function ExpandableContent({
|
|
22
|
+
children,
|
|
23
|
+
collapsedMaxPx = 180,
|
|
24
|
+
fadeClassName = "from-muted",
|
|
25
|
+
className,
|
|
26
|
+
}: ExpandableContentProps) {
|
|
27
|
+
const [expanded, setExpanded] = useState(false);
|
|
28
|
+
const [overflowing, setOverflowing] = useState(false);
|
|
29
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const el = contentRef.current;
|
|
33
|
+
if (!el) return;
|
|
34
|
+
const measure = () =>
|
|
35
|
+
setOverflowing(el.scrollHeight > collapsedMaxPx + 4);
|
|
36
|
+
measure();
|
|
37
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
38
|
+
const observer = new ResizeObserver(measure);
|
|
39
|
+
observer.observe(el);
|
|
40
|
+
return () => observer.disconnect();
|
|
41
|
+
}, [collapsedMaxPx]);
|
|
42
|
+
|
|
43
|
+
const clamped = overflowing && !expanded;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className={className}>
|
|
47
|
+
<div
|
|
48
|
+
ref={contentRef}
|
|
49
|
+
className="relative overflow-hidden"
|
|
50
|
+
style={clamped ? { maxHeight: collapsedMaxPx } : undefined}
|
|
51
|
+
>
|
|
52
|
+
{children}
|
|
53
|
+
{clamped ? (
|
|
54
|
+
<div
|
|
55
|
+
className={cn(
|
|
56
|
+
"pointer-events-none absolute inset-x-0 bottom-0 h-10 bg-gradient-to-t to-transparent",
|
|
57
|
+
fadeClassName,
|
|
58
|
+
)}
|
|
59
|
+
/>
|
|
60
|
+
) : null}
|
|
61
|
+
</div>
|
|
62
|
+
{overflowing ? (
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
onClick={() => setExpanded((value) => !value)}
|
|
66
|
+
className="mt-1 text-[11px] font-medium text-muted-foreground transition-colors hover:text-foreground"
|
|
67
|
+
>
|
|
68
|
+
{expanded ? "Show less" : "Show more"}
|
|
69
|
+
</button>
|
|
70
|
+
) : null}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -17,6 +17,7 @@ import { WebSearchPreview } from "../tool-previews/web-search-preview";
|
|
|
17
17
|
import { QuestionPreview } from "../tool-previews/question-preview";
|
|
18
18
|
import { DiffPreview } from "../tool-previews/diff-preview";
|
|
19
19
|
import { cn } from "../lib/utils";
|
|
20
|
+
import { ExpandableContent } from "./expandable-content";
|
|
20
21
|
|
|
21
22
|
export interface ExpandedToolDetailProps {
|
|
22
23
|
part: ToolPart;
|
|
@@ -146,15 +147,17 @@ export const ExpandedToolDetail = memo(({ part }: ExpandedToolDetailProps) => {
|
|
|
146
147
|
Output
|
|
147
148
|
</span>
|
|
148
149
|
</div>
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
150
|
+
<ExpandableContent fadeClassName="from-card">
|
|
151
|
+
<CodeBlock
|
|
152
|
+
code={
|
|
153
|
+
outputStr.length > 2000
|
|
154
|
+
? outputStr.slice(0, 2000) + "\n...(truncated)"
|
|
155
|
+
: outputStr
|
|
156
|
+
}
|
|
157
|
+
language="json"
|
|
158
|
+
className="rounded-none border-0"
|
|
159
|
+
/>
|
|
160
|
+
</ExpandableContent>
|
|
158
161
|
</div>
|
|
159
162
|
)}
|
|
160
163
|
|
|
@@ -167,9 +170,11 @@ export const ExpandedToolDetail = memo(({ part }: ExpandedToolDetailProps) => {
|
|
|
167
170
|
Error
|
|
168
171
|
</span>
|
|
169
172
|
</div>
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
+
<ExpandableContent fadeClassName="from-[var(--surface-danger-bg)]">
|
|
174
|
+
<pre className="bg-[var(--surface-danger-bg)] p-3 text-xs font-mono whitespace-pre-wrap break-all text-[var(--surface-danger-text)]">
|
|
175
|
+
{error}
|
|
176
|
+
</pre>
|
|
177
|
+
</ExpandableContent>
|
|
173
178
|
</div>
|
|
174
179
|
)}
|
|
175
180
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react'
|
|
2
2
|
import { InlineThinkingItem } from './inline-thinking-item'
|
|
3
|
-
import
|
|
3
|
+
import { InlineToolItem } from './inline-tool-item'
|
|
4
|
+
import type { ReasoningPart, ToolPart } from '../types/parts'
|
|
4
5
|
|
|
5
6
|
const NOW = Date.now()
|
|
6
7
|
|
|
@@ -122,6 +123,34 @@ export const EmptyText: Story = {
|
|
|
122
123
|
),
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
const bashTool: ToolPart = {
|
|
127
|
+
type: 'tool',
|
|
128
|
+
id: 'tool-shell-demo',
|
|
129
|
+
tool: 'bash',
|
|
130
|
+
state: {
|
|
131
|
+
status: 'completed',
|
|
132
|
+
input: { command: 'npx tsc --noEmit' },
|
|
133
|
+
output: '✓ No type errors',
|
|
134
|
+
time: { start: NOW - 2040, end: NOW - 200 },
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Reasoning and tool rows adjacent — they must read as one family (shared
|
|
139
|
+
// badge geometry, trailing status/duration, chevron), not two designs.
|
|
140
|
+
export const UnifiedWithToolRow: Story = {
|
|
141
|
+
name: 'Unified shell (reasoning + tool)',
|
|
142
|
+
render: () => (
|
|
143
|
+
<div className="p-6 max-w-2xl space-y-2">
|
|
144
|
+
<InlineThinkingItem
|
|
145
|
+
part={thinkingComplete}
|
|
146
|
+
defaultOpen={false}
|
|
147
|
+
autoCollapse={false}
|
|
148
|
+
/>
|
|
149
|
+
<InlineToolItem part={bashTool} />
|
|
150
|
+
</div>
|
|
151
|
+
),
|
|
152
|
+
}
|
|
153
|
+
|
|
125
154
|
export const AllStates: Story = {
|
|
126
155
|
render: () => (
|
|
127
156
|
<div className="p-6 max-w-2xl space-y-3">
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { memo, useEffect, useRef, useState } from "react";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import { cn } from "../lib/utils";
|
|
5
|
-
import { formatDuration, truncateText } from "../utils/format";
|
|
2
|
+
import { Brain } from "lucide-react";
|
|
3
|
+
import { truncateText } from "../utils/format";
|
|
6
4
|
import type { ReasoningPart } from "../types/parts";
|
|
7
5
|
import { Markdown } from "../markdown/markdown";
|
|
8
|
-
import {
|
|
6
|
+
import { RunRowShell } from "./run-row-shell";
|
|
7
|
+
import { ExpandableContent } from "./expandable-content";
|
|
9
8
|
|
|
10
9
|
export interface InlineThinkingItemProps {
|
|
11
10
|
part: ReasoningPart;
|
|
@@ -28,7 +27,8 @@ export const InlineThinkingItem = memo(
|
|
|
28
27
|
|
|
29
28
|
const startTime = part.time?.start;
|
|
30
29
|
const endTime = part.time?.end;
|
|
31
|
-
const durationMs =
|
|
30
|
+
const durationMs =
|
|
31
|
+
startTime != null && endTime != null ? endTime - startTime : undefined;
|
|
32
32
|
const isActive = startTime != null && endTime == null;
|
|
33
33
|
const preview = part.text ? truncateText(part.text, 120) : undefined;
|
|
34
34
|
|
|
@@ -50,70 +50,34 @@ export const InlineThinkingItem = memo(
|
|
|
50
50
|
}, [autoCollapse, durationMs, isActive]);
|
|
51
51
|
|
|
52
52
|
return (
|
|
53
|
-
<
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
<div className="flex shrink-0 items-center gap-2">
|
|
83
|
-
{isActive && startTime ? <LiveDuration startTime={startTime} /> : null}
|
|
84
|
-
{!isActive && durationMs != null ? (
|
|
85
|
-
<span className="shrink-0 text-xs tabular-nums text-muted-foreground">
|
|
86
|
-
{formatDuration(durationMs)}
|
|
87
|
-
</span>
|
|
88
|
-
) : null}
|
|
89
|
-
<ChevronRight
|
|
90
|
-
className={cn(
|
|
91
|
-
"h-3 w-3 text-muted-foreground transition-transform shrink-0",
|
|
92
|
-
open && "rotate-90",
|
|
93
|
-
)}
|
|
94
|
-
/>
|
|
95
|
-
</div>
|
|
96
|
-
</button>
|
|
97
|
-
</Collapsible.Trigger>
|
|
98
|
-
|
|
99
|
-
<Collapsible.Content className="overflow-hidden data-[state=open]:animate-slideDown data-[state=closed]:animate-slideUp">
|
|
100
|
-
{part.text ? (
|
|
101
|
-
<div
|
|
102
|
-
className={cn(
|
|
103
|
-
"max-h-60 overflow-y-auto border-t border-border bg-muted px-3 py-3 text-sm leading-relaxed text-muted-foreground",
|
|
104
|
-
contentClassName,
|
|
105
|
-
)}
|
|
106
|
-
>
|
|
107
|
-
<Markdown>{part.text}</Markdown>
|
|
108
|
-
</div>
|
|
109
|
-
) : (
|
|
110
|
-
<div className="border-t border-border bg-muted px-3 py-2.5 text-xs text-muted-foreground">
|
|
111
|
-
No reasoning text provided.
|
|
112
|
-
</div>
|
|
113
|
-
)}
|
|
114
|
-
</Collapsible.Content>
|
|
115
|
-
</div>
|
|
116
|
-
</Collapsible.Root>
|
|
53
|
+
<RunRowShell
|
|
54
|
+
icon={<Brain className="h-3.5 w-3.5" />}
|
|
55
|
+
title={isActive ? "Thinking…" : "Reasoning"}
|
|
56
|
+
description={preview}
|
|
57
|
+
status={isActive ? "running" : "idle"}
|
|
58
|
+
startTime={startTime}
|
|
59
|
+
durationMs={durationMs}
|
|
60
|
+
open={open}
|
|
61
|
+
onOpenChange={setOpen}
|
|
62
|
+
className={className}
|
|
63
|
+
>
|
|
64
|
+
{part.text ? (
|
|
65
|
+
<ExpandableContent
|
|
66
|
+
fadeClassName="from-[var(--md3-surface-container)]"
|
|
67
|
+
className={
|
|
68
|
+
contentClassName ? `px-3 py-3 ${contentClassName}` : "px-3 py-3"
|
|
69
|
+
}
|
|
70
|
+
>
|
|
71
|
+
<div className="text-sm leading-relaxed text-foreground">
|
|
72
|
+
<Markdown>{part.text}</Markdown>
|
|
73
|
+
</div>
|
|
74
|
+
</ExpandableContent>
|
|
75
|
+
) : (
|
|
76
|
+
<div className="px-3 py-2.5 text-xs text-muted-foreground">
|
|
77
|
+
No reasoning text provided.
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
</RunRowShell>
|
|
117
81
|
);
|
|
118
82
|
},
|
|
119
83
|
);
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import { memo,
|
|
2
|
-
import * as Collapsible from "@radix-ui/react-collapsible";
|
|
1
|
+
import { memo, type ComponentType, type ReactNode } from "react";
|
|
3
2
|
import {
|
|
4
|
-
Loader2,
|
|
5
|
-
CheckCircle2,
|
|
6
|
-
AlertCircle,
|
|
7
|
-
ChevronDown,
|
|
8
|
-
ChevronRight,
|
|
9
3
|
Terminal,
|
|
10
4
|
FileEdit,
|
|
11
5
|
FileSearch,
|
|
@@ -23,12 +17,11 @@ import {
|
|
|
23
17
|
getToolErrorText,
|
|
24
18
|
getToolCategory,
|
|
25
19
|
} from "../utils/tool-display";
|
|
26
|
-
import { formatDuration } from "../utils/format";
|
|
27
20
|
import type { ToolPart } from "../types/parts";
|
|
28
21
|
import type { ToolCategory } from "../types/run";
|
|
29
22
|
import type { CustomToolRenderer } from "../types/tool-display";
|
|
30
23
|
import { ExpandedToolDetail } from "./expanded-tool-detail";
|
|
31
|
-
import {
|
|
24
|
+
import { RunRowShell, type RunRowStatus } from "./run-row-shell";
|
|
32
25
|
|
|
33
26
|
/** Map tool category to a lucide-react icon component. */
|
|
34
27
|
const TOOL_CATEGORY_ICON_MAP: Record<
|
|
@@ -59,10 +52,18 @@ export interface InlineToolItemProps {
|
|
|
59
52
|
description?: string;
|
|
60
53
|
}
|
|
61
54
|
|
|
55
|
+
/** Map the tool part status onto the shared row status vocabulary. */
|
|
56
|
+
function toRowStatus(status: ToolPart["state"]["status"]): RunRowStatus {
|
|
57
|
+
if (status === "pending" || status === "running") return "running";
|
|
58
|
+
if (status === "error") return "error";
|
|
59
|
+
return "success";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
62
|
/**
|
|
63
|
-
* Compact single-line tool call display
|
|
64
|
-
*
|
|
65
|
-
*
|
|
63
|
+
* Compact single-line tool call display. Shows a category icon, title,
|
|
64
|
+
* description, duration and status, and expands on click to show
|
|
65
|
+
* ExpandedToolDetail. Rendered through the shared RunRowShell so it stays
|
|
66
|
+
* consistent with the reasoning row.
|
|
66
67
|
*/
|
|
67
68
|
export const InlineToolItem = memo(
|
|
68
69
|
({
|
|
@@ -75,123 +76,36 @@ export const InlineToolItem = memo(
|
|
|
75
76
|
title: titleOverride,
|
|
76
77
|
description: descriptionOverride,
|
|
77
78
|
}: InlineToolItemProps) => {
|
|
78
|
-
const [open, setOpen] = useState(false);
|
|
79
79
|
const meta = getToolDisplayMetadata(part);
|
|
80
80
|
const title = titleOverride ?? meta.title;
|
|
81
81
|
const description = descriptionOverride ?? meta.description;
|
|
82
|
-
const { status } = part.state;
|
|
83
82
|
const errorText = getToolErrorText(part);
|
|
84
83
|
|
|
85
|
-
const isRunning = status === "pending" || status === "running";
|
|
86
|
-
const isError = status === "error";
|
|
87
|
-
const isComplete = status === "completed";
|
|
88
|
-
|
|
89
|
-
// Duration
|
|
90
84
|
const startTime = part.state.time?.start;
|
|
91
85
|
const endTime = part.state.time?.end;
|
|
92
86
|
const durationMs =
|
|
93
|
-
startTime && endTime ? endTime - startTime : undefined;
|
|
87
|
+
startTime != null && endTime != null ? endTime - startTime : undefined;
|
|
94
88
|
|
|
95
|
-
// Determine the default icon based on tool category
|
|
96
89
|
const category = getToolCategory(part.tool);
|
|
97
90
|
const DefaultIcon = TOOL_CATEGORY_ICON_MAP[category] ?? Settings;
|
|
98
|
-
const shapeClass = {
|
|
99
|
-
single: "rounded-[var(--radius-lg)]",
|
|
100
|
-
first: "rounded-t-[var(--radius-lg)] rounded-b-[var(--radius-sm)]",
|
|
101
|
-
middle: "rounded-[var(--radius-sm)]",
|
|
102
|
-
last: "rounded-t-[var(--radius-sm)] rounded-b-[var(--radius-lg)]",
|
|
103
|
-
}[groupPosition];
|
|
104
91
|
|
|
105
92
|
return (
|
|
106
|
-
<
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
isComplete && "text-[var(--surface-success-text)]",
|
|
123
|
-
isError && "text-[var(--surface-danger-text)]",
|
|
124
|
-
!isRunning && !isComplete && !isError && "text-muted-foreground",
|
|
125
|
-
)}>
|
|
126
|
-
{isRunning ? (
|
|
127
|
-
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
128
|
-
) : isComplete ? (
|
|
129
|
-
<CheckCircle2 className="h-3.5 w-3.5" />
|
|
130
|
-
) : isError ? (
|
|
131
|
-
<AlertCircle className="h-3.5 w-3.5" />
|
|
132
|
-
) : (
|
|
133
|
-
<DefaultIcon className="h-3.5 w-3.5" />
|
|
134
|
-
)}
|
|
135
|
-
</div>
|
|
136
|
-
|
|
137
|
-
<span className="truncate text-xs font-medium text-foreground">
|
|
138
|
-
{title}
|
|
139
|
-
</span>
|
|
140
|
-
{description ? (
|
|
141
|
-
<span className="hidden truncate text-xs font-mono text-muted-foreground sm:inline">
|
|
142
|
-
{description}
|
|
143
|
-
</span>
|
|
144
|
-
) : null}
|
|
145
|
-
|
|
146
|
-
<div className="ml-auto flex shrink-0 items-center gap-1.5">
|
|
147
|
-
{isRunning && startTime ? <LiveDuration startTime={startTime} /> : null}
|
|
148
|
-
{!isRunning && durationMs != null ? (
|
|
149
|
-
<span className="text-[10px] font-mono tabular-nums text-muted-foreground">
|
|
150
|
-
{formatDuration(durationMs)}
|
|
151
|
-
</span>
|
|
152
|
-
) : null}
|
|
153
|
-
{isError ? (
|
|
154
|
-
<span className="text-[10px] font-medium text-[var(--surface-danger-text)]">
|
|
155
|
-
failed
|
|
156
|
-
</span>
|
|
157
|
-
) : null}
|
|
158
|
-
{isRunning ? (
|
|
159
|
-
<span className="text-[10px] font-medium text-[var(--accent-text)]">
|
|
160
|
-
running
|
|
161
|
-
</span>
|
|
162
|
-
) : null}
|
|
163
|
-
{open ? (
|
|
164
|
-
<ChevronDown className="h-3 w-3 text-muted-foreground" />
|
|
165
|
-
) : (
|
|
166
|
-
<ChevronRight className="h-3 w-3 text-muted-foreground" />
|
|
167
|
-
)}
|
|
168
|
-
</div>
|
|
169
|
-
</div>
|
|
170
|
-
|
|
171
|
-
{errorText && !open ? (
|
|
172
|
-
<div className="border-t border-border px-3 py-2 text-xs text-red-200">
|
|
173
|
-
{errorText}
|
|
174
|
-
</div>
|
|
175
|
-
) : null}
|
|
176
|
-
</button>
|
|
177
|
-
</Collapsible.Trigger>
|
|
178
|
-
|
|
179
|
-
{actions ? (
|
|
180
|
-
<div
|
|
181
|
-
className="flex shrink-0 flex-wrap items-center justify-end gap-1.5 pt-1"
|
|
182
|
-
onClick={(event) => event.stopPropagation()}
|
|
183
|
-
>
|
|
184
|
-
{actions}
|
|
185
|
-
</div>
|
|
186
|
-
) : null}
|
|
187
|
-
</div>
|
|
188
|
-
|
|
189
|
-
<Collapsible.Content className="overflow-hidden data-[state=open]:animate-slideDown data-[state=closed]:animate-slideUp">
|
|
190
|
-
<div className={cn("mt-1.5 ml-2.5 border-l-2 border-primary/15 pl-3.5", contentClassName)}>
|
|
191
|
-
{renderToolDetail?.(part) ?? <ExpandedToolDetail part={part} />}
|
|
192
|
-
</div>
|
|
193
|
-
</Collapsible.Content>
|
|
194
|
-
</Collapsible.Root>
|
|
93
|
+
<RunRowShell
|
|
94
|
+
icon={<DefaultIcon className="h-3.5 w-3.5" />}
|
|
95
|
+
title={title}
|
|
96
|
+
description={description}
|
|
97
|
+
descriptionMono
|
|
98
|
+
status={toRowStatus(part.state.status)}
|
|
99
|
+
startTime={startTime}
|
|
100
|
+
durationMs={durationMs}
|
|
101
|
+
groupPosition={groupPosition}
|
|
102
|
+
collapsedError={errorText ?? undefined}
|
|
103
|
+
actions={actions}
|
|
104
|
+
className={className}
|
|
105
|
+
contentClassName={cn("p-2", contentClassName)}
|
|
106
|
+
>
|
|
107
|
+
{renderToolDetail?.(part) ?? <ExpandedToolDetail part={part} />}
|
|
108
|
+
</RunRowShell>
|
|
195
109
|
);
|
|
196
110
|
},
|
|
197
111
|
);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { RunRowShell } from "./run-row-shell";
|
|
4
|
+
|
|
5
|
+
function renderShell(status: "running" | "success" | "error" | "idle") {
|
|
6
|
+
return render(
|
|
7
|
+
<RunRowShell
|
|
8
|
+
icon={<span data-testid="lead-icon">icon</span>}
|
|
9
|
+
title="Ran command"
|
|
10
|
+
status={status}
|
|
11
|
+
>
|
|
12
|
+
<div>body</div>
|
|
13
|
+
</RunRowShell>,
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("RunRowShell", () => {
|
|
18
|
+
it("keeps the semantic lead icon visible in every status (no hijack)", () => {
|
|
19
|
+
for (const status of ["running", "success", "error", "idle"] as const) {
|
|
20
|
+
const { unmount } = renderShell(status);
|
|
21
|
+
expect(screen.getByTestId("lead-icon")).toBeInTheDocument();
|
|
22
|
+
expect(screen.getByText("Ran command")).toBeInTheDocument();
|
|
23
|
+
unmount();
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("shows a spinner while running", () => {
|
|
28
|
+
const { container } = renderShell("running");
|
|
29
|
+
expect(container.querySelector(".animate-spin")).not.toBeNull();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("is not expandable and has no chevron when no children are provided", () => {
|
|
33
|
+
render(<RunRowShell icon={<span>i</span>} title="Reasoning" />);
|
|
34
|
+
// The trigger button is disabled when there is no expandable body.
|
|
35
|
+
const button = screen.getByRole("button", { name: /reasoning/i });
|
|
36
|
+
expect(button).toBeDisabled();
|
|
37
|
+
});
|
|
38
|
+
});
|