@tangle-network/ui 10.0.0 → 11.1.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 +22 -0
- package/dist/chat.d.ts +8 -2
- package/dist/chat.js +3 -3
- package/dist/chunk-4ORVXSMU.js +507 -0
- package/dist/{chunk-CCM2KRQH.js → chunk-EWIWYC2I.js} +1 -1
- package/dist/{chunk-KBOD47AW.js → chunk-F22SQTBP.js} +1 -1
- package/dist/{chunk-KHO7DYXH.js → chunk-FIFE5HCW.js} +105 -70
- package/dist/{chunk-HHYUPAOJ.js → chunk-PX5KV5HI.js} +47 -79
- package/dist/hooks.js +3 -3
- package/dist/index.js +5 -5
- package/dist/run.d.ts +4 -3
- package/dist/run.js +3 -3
- package/dist/sdk-hooks.js +3 -3
- package/package.json +2 -2
- 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/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
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { useState, type ReactNode } from "react";
|
|
2
|
+
import * as Collapsible from "@radix-ui/react-collapsible";
|
|
3
|
+
import { Loader2, ChevronRight } from "lucide-react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
import { formatDuration } from "../utils/format";
|
|
6
|
+
import { LiveDuration } from "./run-item-primitives";
|
|
7
|
+
|
|
8
|
+
export type RunRowStatus = "running" | "success" | "error" | "idle";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Trailing status indicator shared by every run row: a spinner while running,
|
|
12
|
+
* a green/red dot on terminal states, nothing when idle. Status is shown here
|
|
13
|
+
* — it never replaces the row's semantic lead icon.
|
|
14
|
+
*/
|
|
15
|
+
export function RunRowStatusDot({ status }: { status: RunRowStatus }) {
|
|
16
|
+
if (status === "running") {
|
|
17
|
+
return <Loader2 className="h-3 w-3 shrink-0 animate-spin text-[var(--accent-text)]" />;
|
|
18
|
+
}
|
|
19
|
+
if (status === "success") {
|
|
20
|
+
return (
|
|
21
|
+
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--surface-success-text)]" />
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
if (status === "error") {
|
|
25
|
+
return (
|
|
26
|
+
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--surface-danger-text)]" />
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RunRowShellProps {
|
|
33
|
+
/** Semantic lead glyph — always visible; never replaced by status. */
|
|
34
|
+
icon: ReactNode;
|
|
35
|
+
title: string;
|
|
36
|
+
/** Secondary inline text (tool path/command, or a reasoning preview). */
|
|
37
|
+
description?: string;
|
|
38
|
+
/** Render the description in mono (tools) vs prose (reasoning). */
|
|
39
|
+
descriptionMono?: boolean;
|
|
40
|
+
status?: RunRowStatus;
|
|
41
|
+
/** Live-ticks a duration while running. */
|
|
42
|
+
startTime?: number;
|
|
43
|
+
/** Static duration shown once the row is no longer running. */
|
|
44
|
+
durationMs?: number;
|
|
45
|
+
/** Radius shaping when rows are stacked into a group. */
|
|
46
|
+
groupPosition?: "single" | "first" | "middle" | "last";
|
|
47
|
+
/** Message shown below the header while collapsed (tool errors). */
|
|
48
|
+
collapsedError?: string;
|
|
49
|
+
actions?: ReactNode;
|
|
50
|
+
/** Controlled open state. Omit for internal (uncontrolled) state. */
|
|
51
|
+
open?: boolean;
|
|
52
|
+
onOpenChange?: (open: boolean) => void;
|
|
53
|
+
defaultOpen?: boolean;
|
|
54
|
+
className?: string;
|
|
55
|
+
contentClassName?: string;
|
|
56
|
+
/** Expanded body. When absent the row is not expandable. */
|
|
57
|
+
children?: ReactNode;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const SHAPE_CLASS: Record<
|
|
61
|
+
NonNullable<RunRowShellProps["groupPosition"]>,
|
|
62
|
+
string
|
|
63
|
+
> = {
|
|
64
|
+
single: "rounded-[var(--radius-lg)]",
|
|
65
|
+
first: "rounded-t-[var(--radius-lg)] rounded-b-[var(--radius-sm)]",
|
|
66
|
+
middle: "rounded-[var(--radius-sm)]",
|
|
67
|
+
last: "rounded-t-[var(--radius-sm)] rounded-b-[var(--radius-lg)]",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Shared shell for the agent activity rows (reasoning + tool). Owns the
|
|
72
|
+
* bordered container, the lead icon badge, title/description, the trailing
|
|
73
|
+
* meta cluster (duration + status + chevron) and the collapsible body, so
|
|
74
|
+
* both row kinds read as one family instead of two bespoke layouts.
|
|
75
|
+
*/
|
|
76
|
+
export function RunRowShell({
|
|
77
|
+
icon,
|
|
78
|
+
title,
|
|
79
|
+
description,
|
|
80
|
+
descriptionMono = false,
|
|
81
|
+
status = "idle",
|
|
82
|
+
startTime,
|
|
83
|
+
durationMs,
|
|
84
|
+
groupPosition = "single",
|
|
85
|
+
collapsedError,
|
|
86
|
+
actions,
|
|
87
|
+
open: openProp,
|
|
88
|
+
onOpenChange,
|
|
89
|
+
defaultOpen = false,
|
|
90
|
+
className,
|
|
91
|
+
contentClassName,
|
|
92
|
+
children,
|
|
93
|
+
}: RunRowShellProps) {
|
|
94
|
+
const [openState, setOpenState] = useState(defaultOpen);
|
|
95
|
+
const isControlled = openProp !== undefined;
|
|
96
|
+
const open = isControlled ? openProp : openState;
|
|
97
|
+
const setOpen = (next: boolean) => {
|
|
98
|
+
if (!isControlled) setOpenState(next);
|
|
99
|
+
onOpenChange?.(next);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const isRunning = status === "running";
|
|
103
|
+
const expandable = children != null;
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<Collapsible.Root open={open} onOpenChange={setOpen}>
|
|
107
|
+
<div className="flex items-start gap-2">
|
|
108
|
+
<div
|
|
109
|
+
className={cn(
|
|
110
|
+
"min-w-0 flex-1 overflow-hidden border transition-colors",
|
|
111
|
+
"border-[var(--border-subtle)] bg-[var(--md3-surface-container)]",
|
|
112
|
+
expandable && "hover:border-border",
|
|
113
|
+
open && "border-border",
|
|
114
|
+
SHAPE_CLASS[groupPosition],
|
|
115
|
+
className,
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
118
|
+
<Collapsible.Trigger asChild disabled={!expandable}>
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
className={cn(
|
|
122
|
+
"flex w-full items-center gap-2.5 px-3 py-2 text-left",
|
|
123
|
+
expandable ? "cursor-pointer" : "cursor-default",
|
|
124
|
+
)}
|
|
125
|
+
>
|
|
126
|
+
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-[var(--radius-sm)] border border-border bg-muted text-muted-foreground">
|
|
127
|
+
{icon}
|
|
128
|
+
</span>
|
|
129
|
+
|
|
130
|
+
<span className="shrink-0 whitespace-nowrap text-xs font-medium text-foreground">
|
|
131
|
+
{title}
|
|
132
|
+
</span>
|
|
133
|
+
{description ? (
|
|
134
|
+
<span
|
|
135
|
+
className={cn(
|
|
136
|
+
"hidden min-w-0 flex-1 truncate text-xs text-muted-foreground sm:inline",
|
|
137
|
+
descriptionMono && "font-mono",
|
|
138
|
+
)}
|
|
139
|
+
>
|
|
140
|
+
{description}
|
|
141
|
+
</span>
|
|
142
|
+
) : null}
|
|
143
|
+
|
|
144
|
+
<div className="ml-auto flex shrink-0 items-center gap-1.5">
|
|
145
|
+
{isRunning && startTime != null ? (
|
|
146
|
+
<LiveDuration startTime={startTime} />
|
|
147
|
+
) : durationMs != null ? (
|
|
148
|
+
<span className="text-[10px] font-mono tabular-nums text-muted-foreground">
|
|
149
|
+
{formatDuration(durationMs)}
|
|
150
|
+
</span>
|
|
151
|
+
) : null}
|
|
152
|
+
<RunRowStatusDot status={status} />
|
|
153
|
+
{expandable ? (
|
|
154
|
+
<ChevronRight
|
|
155
|
+
className={cn(
|
|
156
|
+
"h-3 w-3 shrink-0 text-muted-foreground transition-transform",
|
|
157
|
+
open && "rotate-90",
|
|
158
|
+
)}
|
|
159
|
+
/>
|
|
160
|
+
) : null}
|
|
161
|
+
</div>
|
|
162
|
+
</button>
|
|
163
|
+
</Collapsible.Trigger>
|
|
164
|
+
|
|
165
|
+
{collapsedError && !open ? (
|
|
166
|
+
<div className="border-t border-border px-3 py-2 text-xs text-[var(--surface-danger-text)]">
|
|
167
|
+
{collapsedError}
|
|
168
|
+
</div>
|
|
169
|
+
) : null}
|
|
170
|
+
|
|
171
|
+
{expandable ? (
|
|
172
|
+
<Collapsible.Content className="overflow-hidden data-[state=open]:animate-slideDown data-[state=closed]:animate-slideUp">
|
|
173
|
+
<div className={cn("border-t border-border", contentClassName)}>
|
|
174
|
+
{children}
|
|
175
|
+
</div>
|
|
176
|
+
</Collapsible.Content>
|
|
177
|
+
) : null}
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
{actions ? (
|
|
181
|
+
<div
|
|
182
|
+
className="flex shrink-0 flex-wrap items-center justify-end gap-1.5 pt-1"
|
|
183
|
+
onClick={(event) => event.stopPropagation()}
|
|
184
|
+
>
|
|
185
|
+
{actions}
|
|
186
|
+
</div>
|
|
187
|
+
) : null}
|
|
188
|
+
</div>
|
|
189
|
+
</Collapsible.Root>
|
|
190
|
+
);
|
|
191
|
+
}
|