@tangle-network/ui 1.0.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 +12 -0
- package/LICENSE +21 -0
- package/README.md +33 -0
- package/dist/active-sessions-store-CeOmXgv5.d.ts +85 -0
- package/dist/artifact-pane-DvJyPWV4.d.ts +24 -0
- package/dist/auth.d.ts +74 -0
- package/dist/auth.js +15 -0
- package/dist/button-CMQuQEW_.d.ts +17 -0
- package/dist/chat.d.ts +232 -0
- package/dist/chat.js +30 -0
- package/dist/chunk-2NFQRQOD.js +1009 -0
- package/dist/chunk-2VH6PUXD.js +186 -0
- package/dist/chunk-34A66VBG.js +214 -0
- package/dist/chunk-3OI2QKFD.js +0 -0
- package/dist/chunk-4CLN43XT.js +45 -0
- package/dist/chunk-54SQQMMM.js +156 -0
- package/dist/chunk-5Z5ZYMOJ.js +0 -0
- package/dist/chunk-66BNMOVT.js +167 -0
- package/dist/chunk-6BGQA4BQ.js +0 -0
- package/dist/chunk-7UO2ZMRQ.js +133 -0
- package/dist/chunk-BX6AQMUS.js +183 -0
- package/dist/chunk-CD53GZOM.js +59 -0
- package/dist/chunk-CSAIKY36.js +54 -0
- package/dist/chunk-EEE55AVS.js +1201 -0
- package/dist/chunk-GYPQXTJU.js +230 -0
- package/dist/chunk-HFL6R6IF.js +37 -0
- package/dist/chunk-HJKCSXCH.js +737 -0
- package/dist/chunk-LISXUB4D.js +1222 -0
- package/dist/chunk-LQS34IGP.js +0 -0
- package/dist/chunk-MKTSMWVD.js +109 -0
- package/dist/chunk-NKDZ7GZE.js +192 -0
- package/dist/chunk-OEX7NZE3.js +321 -0
- package/dist/chunk-Q56BYXQF.js +61 -0
- package/dist/chunk-Q7EIIWTC.js +0 -0
- package/dist/chunk-REJESC5U.js +117 -0
- package/dist/chunk-RQGKSCEZ.js +0 -0
- package/dist/chunk-RQHJBTEU.js +10 -0
- package/dist/chunk-TMFOPHHN.js +299 -0
- package/dist/chunk-XGKULLYE.js +40 -0
- package/dist/chunk-XIHMJ7ZQ.js +614 -0
- package/dist/chunk-YJ2G3XO5.js +1048 -0
- package/dist/chunk-YNN4O57I.js +754 -0
- package/dist/code-block-DjXf8eOG.d.ts +19 -0
- package/dist/document-editor-pane-A5LT5H4N.js +12 -0
- package/dist/document-editor-pane-DyDEX_Zm.d.ts +124 -0
- package/dist/editor.d.ts +120 -0
- package/dist/editor.js +34 -0
- package/dist/files.d.ts +175 -0
- package/dist/files.js +20 -0
- package/dist/hooks.d.ts +56 -0
- package/dist/hooks.js +41 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +446 -0
- package/dist/markdown.d.ts +15 -0
- package/dist/markdown.js +14 -0
- package/dist/message-BHWbxBtT.d.ts +15 -0
- package/dist/openui.d.ts +115 -0
- package/dist/openui.js +12 -0
- package/dist/parts-dj7AcUg0.d.ts +36 -0
- package/dist/primitives.d.ts +332 -0
- package/dist/primitives.js +191 -0
- package/dist/run-PfLmDAox.d.ts +41 -0
- package/dist/run.d.ts +69 -0
- package/dist/run.js +36 -0
- package/dist/sdk-hooks.d.ts +285 -0
- package/dist/sdk-hooks.js +31 -0
- package/dist/stores.d.ts +17 -0
- package/dist/stores.js +76 -0
- package/dist/tool-call-feed-Bs3MyQMT.d.ts +68 -0
- package/dist/tool-display-z4JcDmMQ.d.ts +32 -0
- package/dist/tool-previews.d.ts +48 -0
- package/dist/tool-previews.js +21 -0
- package/dist/types.d.ts +19 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +45 -0
- package/dist/utils.js +32 -0
- package/package.json +193 -0
- package/src/auth/auth.tsx +228 -0
- package/src/auth/index.ts +13 -0
- package/src/auth/login-layout.tsx +46 -0
- package/src/chat/agent-timeline.stories.tsx +429 -0
- package/src/chat/agent-timeline.tsx +360 -0
- package/src/chat/chat-container.tsx +486 -0
- package/src/chat/chat-input.stories.tsx +142 -0
- package/src/chat/chat-input.tsx +389 -0
- package/src/chat/chat-message.stories.tsx +237 -0
- package/src/chat/chat-message.tsx +129 -0
- package/src/chat/index.ts +18 -0
- package/src/chat/message-list.stories.tsx +336 -0
- package/src/chat/message-list.tsx +79 -0
- package/src/chat/thinking-indicator.stories.tsx +56 -0
- package/src/chat/thinking-indicator.tsx +30 -0
- package/src/chat/user-message.stories.tsx +92 -0
- package/src/chat/user-message.tsx +43 -0
- package/src/editor/document-editor-pane.tsx +351 -0
- package/src/editor/editor-provider.tsx +428 -0
- package/src/editor/editor-toolbar.tsx +130 -0
- package/src/editor/index.ts +31 -0
- package/src/editor/markdown-conversion.ts +21 -0
- package/src/editor/markdown-document-editor.tsx +137 -0
- package/src/editor/tiptap-editor.tsx +331 -0
- package/src/editor/use-editor.ts +221 -0
- package/src/files/file-artifact-pane.tsx +183 -0
- package/src/files/file-preview.tsx +342 -0
- package/src/files/file-tabs.tsx +71 -0
- package/src/files/file-tree.tsx +258 -0
- package/src/files/index.ts +17 -0
- package/src/files/rich-file-tree.stories.tsx +104 -0
- package/src/files/rich-file-tree.test.tsx +42 -0
- package/src/files/rich-file-tree.tsx +232 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/use-auth.ts +153 -0
- package/src/hooks/use-auto-scroll.ts +59 -0
- package/src/hooks/use-dropdown-menu.ts +40 -0
- package/src/hooks/use-live-time.test.tsx +40 -0
- package/src/hooks/use-live-time.ts +27 -0
- package/src/hooks/use-realtime-session.ts +319 -0
- package/src/hooks/use-run-collapse-state.ts +25 -0
- package/src/hooks/use-run-groups.ts +111 -0
- package/src/hooks/use-sdk-session.ts +575 -0
- package/src/hooks/use-sse-stream.ts +475 -0
- package/src/hooks/use-tool-call-stream.ts +96 -0
- package/src/index.ts +14 -0
- package/src/lib/utils.ts +6 -0
- package/src/markdown/code-block.tsx +198 -0
- package/src/markdown/index.ts +2 -0
- package/src/markdown/markdown.stories.tsx +190 -0
- package/src/markdown/markdown.tsx +62 -0
- package/src/openui/index.ts +20 -0
- package/src/openui/openui-artifact-renderer.tsx +542 -0
- package/src/primitives/artifact-pane.tsx +91 -0
- package/src/primitives/avatar.stories.tsx +95 -0
- package/src/primitives/avatar.tsx +47 -0
- package/src/primitives/badge.stories.tsx +57 -0
- package/src/primitives/badge.tsx +97 -0
- package/src/primitives/button.stories.tsx +48 -0
- package/src/primitives/button.tsx +115 -0
- package/src/primitives/card.stories.tsx +53 -0
- package/src/primitives/card.tsx +98 -0
- package/src/primitives/code-block.stories.tsx +115 -0
- package/src/primitives/code-block.tsx +22 -0
- package/src/primitives/design-tokens.stories.tsx +162 -0
- package/src/primitives/dialog.stories.tsx +176 -0
- package/src/primitives/dialog.tsx +137 -0
- package/src/primitives/drop-zone.stories.tsx +123 -0
- package/src/primitives/drop-zone.tsx +131 -0
- package/src/primitives/dropdown-menu.stories.tsx +122 -0
- package/src/primitives/dropdown-menu.tsx +214 -0
- package/src/primitives/empty-state.stories.tsx +81 -0
- package/src/primitives/empty-state.tsx +40 -0
- package/src/primitives/index.ts +118 -0
- package/src/primitives/input.stories.tsx +113 -0
- package/src/primitives/input.tsx +136 -0
- package/src/primitives/label.stories.tsx +84 -0
- package/src/primitives/label.tsx +24 -0
- package/src/primitives/progress.stories.tsx +93 -0
- package/src/primitives/progress.tsx +50 -0
- package/src/primitives/segmented-control.test.tsx +328 -0
- package/src/primitives/segmented-control.tsx +154 -0
- package/src/primitives/select.stories.tsx +164 -0
- package/src/primitives/select.tsx +158 -0
- package/src/primitives/sidebar-drop-zone.stories.tsx +100 -0
- package/src/primitives/sidebar-drop-zone.tsx +149 -0
- package/src/primitives/skeleton.stories.tsx +79 -0
- package/src/primitives/skeleton.tsx +55 -0
- package/src/primitives/stat-card.stories.tsx +137 -0
- package/src/primitives/stat-card.tsx +97 -0
- package/src/primitives/switch.stories.tsx +85 -0
- package/src/primitives/switch.tsx +28 -0
- package/src/primitives/table.stories.tsx +170 -0
- package/src/primitives/table.tsx +116 -0
- package/src/primitives/tabs.stories.tsx +180 -0
- package/src/primitives/tabs.tsx +71 -0
- package/src/primitives/terminal-display.stories.tsx +191 -0
- package/src/primitives/terminal-display.tsx +189 -0
- package/src/primitives/theme-toggle.stories.tsx +32 -0
- package/src/primitives/theme-toggle.tsx +96 -0
- package/src/primitives/toast.stories.tsx +155 -0
- package/src/primitives/toast.tsx +190 -0
- package/src/primitives/upload-progress.stories.tsx +120 -0
- package/src/primitives/upload-progress.tsx +110 -0
- package/src/run/expanded-tool-detail.stories.tsx +182 -0
- package/src/run/expanded-tool-detail.tsx +186 -0
- package/src/run/index.ts +13 -0
- package/src/run/inline-thinking-item.stories.tsx +136 -0
- package/src/run/inline-thinking-item.tsx +120 -0
- package/src/run/inline-tool-item.stories.tsx +222 -0
- package/src/run/inline-tool-item.tsx +190 -0
- package/src/run/run-group.stories.tsx +322 -0
- package/src/run/run-group.tsx +569 -0
- package/src/run/run-item-primitives.tsx +17 -0
- package/src/run/tool-call-feed.stories.tsx +294 -0
- package/src/run/tool-call-feed.tsx +192 -0
- package/src/run/tool-call-step.stories.tsx +198 -0
- package/src/run/tool-call-step.tsx +240 -0
- package/src/sdk-hooks.ts +38 -0
- package/src/stores/active-sessions-store.ts +455 -0
- package/src/stores/chat-store.ts +43 -0
- package/src/stores/index.ts +2 -0
- package/src/tool-previews/command-preview.tsx +116 -0
- package/src/tool-previews/diff-preview.tsx +85 -0
- package/src/tool-previews/glob-results-preview.tsx +98 -0
- package/src/tool-previews/grep-results-preview.tsx +157 -0
- package/src/tool-previews/index.ts +22 -0
- package/src/tool-previews/preview-primitives.tsx +84 -0
- package/src/tool-previews/question-preview.tsx +101 -0
- package/src/tool-previews/web-search-preview.tsx +117 -0
- package/src/tool-previews/write-file-preview.tsx +80 -0
- package/src/types/branding.ts +11 -0
- package/src/types/index.ts +5 -0
- package/src/types/message.ts +13 -0
- package/src/types/parts.ts +51 -0
- package/src/types/run.ts +56 -0
- package/src/types/tool-display.ts +41 -0
- package/src/utils/copy-text.ts +30 -0
- package/src/utils/format.test.ts +43 -0
- package/src/utils/format.ts +56 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/time-ago.ts +9 -0
- package/src/utils/tool-display.ts +238 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import { memo, useMemo, type ComponentType, type ReactNode } from "react";
|
|
2
|
+
import * as Collapsible from "@radix-ui/react-collapsible";
|
|
3
|
+
import {
|
|
4
|
+
Bot,
|
|
5
|
+
Loader2,
|
|
6
|
+
ChevronDown,
|
|
7
|
+
ChevronRight,
|
|
8
|
+
Terminal,
|
|
9
|
+
FileEdit,
|
|
10
|
+
FileSearch,
|
|
11
|
+
Search,
|
|
12
|
+
PencilLine,
|
|
13
|
+
Globe,
|
|
14
|
+
ClipboardList,
|
|
15
|
+
Settings,
|
|
16
|
+
Sparkles,
|
|
17
|
+
type LucideProps,
|
|
18
|
+
} from "lucide-react";
|
|
19
|
+
import { cn } from "../lib/utils";
|
|
20
|
+
import { formatDuration } from "../utils/format";
|
|
21
|
+
import type { Run, ToolCategory } from "../types/run";
|
|
22
|
+
import type { SessionPart, ToolPart, ReasoningPart } from "../types/parts";
|
|
23
|
+
import type { AgentBranding } from "../types/branding";
|
|
24
|
+
import type { CustomToolRenderer } from "../types/tool-display";
|
|
25
|
+
import { InlineToolItem } from "./inline-tool-item";
|
|
26
|
+
import { InlineThinkingItem } from "./inline-thinking-item";
|
|
27
|
+
import { Markdown } from "../markdown/markdown";
|
|
28
|
+
import {
|
|
29
|
+
OpenUIArtifactRenderer,
|
|
30
|
+
type OpenUIAction,
|
|
31
|
+
type OpenUIComponentNode,
|
|
32
|
+
} from "../openui/openui-artifact-renderer";
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Default branding
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
const DEFAULT_BRANDING: AgentBranding = {
|
|
39
|
+
label: "Agent",
|
|
40
|
+
accentClass: "text-primary",
|
|
41
|
+
bgClass: "bg-[var(--accent-surface-soft)]",
|
|
42
|
+
containerBgClass: "bg-muted",
|
|
43
|
+
borderClass: "border-border",
|
|
44
|
+
iconClass: "",
|
|
45
|
+
textClass: "text-primary",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const ASSISTANT_SHELL =
|
|
49
|
+
"min-w-0 flex-1 space-y-3 rounded-[26px] border border-[var(--border-subtle)] bg-[color:color-mix(in_srgb,var(--bg-card)_94%,transparent)] px-5 py-4 shadow-[0_1px_2px_rgba(15,23,42,0.04)]";
|
|
50
|
+
|
|
51
|
+
function AssistantShell({
|
|
52
|
+
branding,
|
|
53
|
+
isStreaming,
|
|
54
|
+
children,
|
|
55
|
+
}: {
|
|
56
|
+
branding: AgentBranding;
|
|
57
|
+
isStreaming: boolean;
|
|
58
|
+
children: ReactNode;
|
|
59
|
+
}) {
|
|
60
|
+
return (
|
|
61
|
+
<div className="flex gap-3">
|
|
62
|
+
<div className="mt-1 flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[var(--brand-primary)] text-white">
|
|
63
|
+
<Bot className="h-4 w-4" />
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div className={ASSISTANT_SHELL}>
|
|
67
|
+
<div className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.14em] text-[var(--text-muted)]">
|
|
68
|
+
<span>{branding.label}</span>
|
|
69
|
+
{isStreaming ? (
|
|
70
|
+
<span className="inline-flex items-center gap-1.5 text-[var(--text-muted)]">
|
|
71
|
+
<Loader2 className="h-3 w-3 animate-spin" />
|
|
72
|
+
Thinking
|
|
73
|
+
</span>
|
|
74
|
+
) : null}
|
|
75
|
+
</div>
|
|
76
|
+
{children}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Category icon mapping
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
const CATEGORY_ICON_MAP: Record<ToolCategory, ComponentType<LucideProps>> = {
|
|
87
|
+
command: Terminal,
|
|
88
|
+
write: FileEdit,
|
|
89
|
+
read: FileSearch,
|
|
90
|
+
search: Search,
|
|
91
|
+
edit: PencilLine,
|
|
92
|
+
task: Bot,
|
|
93
|
+
web: Globe,
|
|
94
|
+
todo: ClipboardList,
|
|
95
|
+
other: Settings,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const CATEGORY_ORDER: ToolCategory[] = [
|
|
99
|
+
"command",
|
|
100
|
+
"write",
|
|
101
|
+
"edit",
|
|
102
|
+
"read",
|
|
103
|
+
"search",
|
|
104
|
+
"web",
|
|
105
|
+
"task",
|
|
106
|
+
"todo",
|
|
107
|
+
"other",
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Props
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
export interface RunGroupProps {
|
|
115
|
+
run: Run;
|
|
116
|
+
partMap: Record<string, SessionPart[]>;
|
|
117
|
+
collapsed: boolean;
|
|
118
|
+
onToggle: () => void;
|
|
119
|
+
branding?: AgentBranding;
|
|
120
|
+
renderToolDetail?: CustomToolRenderer;
|
|
121
|
+
headerActions?: ReactNode;
|
|
122
|
+
renderToolActions?: (
|
|
123
|
+
part: ToolPart,
|
|
124
|
+
options: {
|
|
125
|
+
run: Run;
|
|
126
|
+
messageId: string;
|
|
127
|
+
partIndex: number;
|
|
128
|
+
},
|
|
129
|
+
) => ReactNode;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const OPENUI_NODE_TYPES = new Set([
|
|
133
|
+
"heading",
|
|
134
|
+
"text",
|
|
135
|
+
"badge",
|
|
136
|
+
"stat",
|
|
137
|
+
"key_value",
|
|
138
|
+
"code",
|
|
139
|
+
"markdown",
|
|
140
|
+
"table",
|
|
141
|
+
"actions",
|
|
142
|
+
"separator",
|
|
143
|
+
"stack",
|
|
144
|
+
"grid",
|
|
145
|
+
"card",
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
function isOpenUINode(value: unknown): value is OpenUIComponentNode {
|
|
149
|
+
return (
|
|
150
|
+
typeof value === "object" &&
|
|
151
|
+
value !== null &&
|
|
152
|
+
"type" in value &&
|
|
153
|
+
typeof (value as Record<string, unknown>).type === "string" &&
|
|
154
|
+
OPENUI_NODE_TYPES.has((value as Record<string, unknown>).type as string)
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function extractOpenUISchema(output: unknown): OpenUIComponentNode[] | null {
|
|
159
|
+
if (output == null) return null;
|
|
160
|
+
if (isOpenUINode(output)) return [output];
|
|
161
|
+
if (Array.isArray(output) && output.length > 0 && output.every(isOpenUINode)) {
|
|
162
|
+
return output as OpenUIComponentNode[];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (typeof output === "object" && !Array.isArray(output)) {
|
|
166
|
+
const obj = output as Record<string, unknown>;
|
|
167
|
+
for (const key of ["openui", "schema", "ui"]) {
|
|
168
|
+
if (obj[key] == null) continue;
|
|
169
|
+
const inner = obj[key];
|
|
170
|
+
if (typeof inner === "string") {
|
|
171
|
+
try {
|
|
172
|
+
const parsed = JSON.parse(inner);
|
|
173
|
+
return extractOpenUISchema(parsed);
|
|
174
|
+
} catch {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const nested = extractOpenUISchema(inner);
|
|
179
|
+
if (nested) return nested;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (typeof output === "string") {
|
|
184
|
+
try {
|
|
185
|
+
return extractOpenUISchema(JSON.parse(output));
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function getOpenUISummary(output: unknown) {
|
|
195
|
+
if (!output || typeof output !== "object" || Array.isArray(output)) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const summary = (output as Record<string, unknown>).summary;
|
|
199
|
+
return typeof summary === "string" && summary.trim() ? summary.trim() : null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function isOpenUITool(part: ToolPart) {
|
|
203
|
+
const normalized = part.tool.toLowerCase().replace(/^tool:/, "");
|
|
204
|
+
return normalized.includes("openui");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// Stat badges
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
function CategoryBadges({ categories }: { categories: Set<ToolCategory> }) {
|
|
212
|
+
const sorted = useMemo(
|
|
213
|
+
() => CATEGORY_ORDER.filter((category) => categories.has(category)),
|
|
214
|
+
[categories],
|
|
215
|
+
);
|
|
216
|
+
if (sorted.length === 0) return null;
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<div className="flex items-center gap-1">
|
|
220
|
+
{sorted.map((cat) => {
|
|
221
|
+
const Icon = CATEGORY_ICON_MAP[cat] ?? Settings;
|
|
222
|
+
return (
|
|
223
|
+
<span
|
|
224
|
+
key={cat}
|
|
225
|
+
title={cat}
|
|
226
|
+
className="flex h-5 w-5 items-center justify-center rounded border border-border text-muted-foreground"
|
|
227
|
+
>
|
|
228
|
+
<Icon className="h-3 w-3" />
|
|
229
|
+
</span>
|
|
230
|
+
);
|
|
231
|
+
})}
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function renderSummary(run: Run) {
|
|
237
|
+
const parts: string[] = [];
|
|
238
|
+
|
|
239
|
+
if (run.stats.toolCount > 0) {
|
|
240
|
+
parts.push(`${run.stats.toolCount} tool${run.stats.toolCount === 1 ? "" : "s"}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (run.stats.textPartCount > 0) {
|
|
244
|
+
parts.push(`${run.stats.textPartCount} response${run.stats.textPartCount === 1 ? "" : "s"}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (run.stats.thinkingDurationMs > 0) {
|
|
248
|
+
parts.push(`${formatDuration(run.stats.thinkingDurationMs)} thinking`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return parts.join(", ");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function getToolGroupPosition(
|
|
255
|
+
currentIndex: number,
|
|
256
|
+
parts: Array<{ part: SessionPart; msgId: string; index: number }>,
|
|
257
|
+
) {
|
|
258
|
+
const previous = parts[currentIndex - 1]?.part;
|
|
259
|
+
const next = parts[currentIndex + 1]?.part;
|
|
260
|
+
const previousIsTool = previous?.type === "tool";
|
|
261
|
+
const nextIsTool = next?.type === "tool";
|
|
262
|
+
|
|
263
|
+
if (previousIsTool && nextIsTool) return "middle" as const;
|
|
264
|
+
if (previousIsTool) return "last" as const;
|
|
265
|
+
if (nextIsTool) return "first" as const;
|
|
266
|
+
return "single" as const;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// Component
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Collapsible container for a consecutive group of assistant messages (a "run").
|
|
275
|
+
* Shows a summary header with stats and renders tool/thinking/text parts.
|
|
276
|
+
*/
|
|
277
|
+
export const RunGroup = memo(
|
|
278
|
+
({
|
|
279
|
+
run,
|
|
280
|
+
partMap,
|
|
281
|
+
collapsed,
|
|
282
|
+
onToggle,
|
|
283
|
+
branding = DEFAULT_BRANDING,
|
|
284
|
+
renderToolDetail,
|
|
285
|
+
headerActions,
|
|
286
|
+
renderToolActions,
|
|
287
|
+
}: RunGroupProps) => {
|
|
288
|
+
// Flatten all parts from all messages in this run
|
|
289
|
+
const allParts = useMemo(() => {
|
|
290
|
+
const parts: Array<{
|
|
291
|
+
part: SessionPart;
|
|
292
|
+
msgId: string;
|
|
293
|
+
index: number;
|
|
294
|
+
}> = [];
|
|
295
|
+
for (const msg of run.messages) {
|
|
296
|
+
const msgParts = partMap[msg.id] ?? [];
|
|
297
|
+
msgParts.forEach((part, index) => {
|
|
298
|
+
parts.push({ part, msgId: msg.id, index });
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
return parts;
|
|
302
|
+
}, [run.messages, partMap]);
|
|
303
|
+
|
|
304
|
+
const { stats, isStreaming } = run;
|
|
305
|
+
const hasRenderableParts = allParts.some(({ part }) => {
|
|
306
|
+
if (part.type === "tool" || part.type === "reasoning") {
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return part.type === "text" && !part.synthetic && part.text.trim().length > 0;
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
if (!hasRenderableParts) {
|
|
314
|
+
if (!isStreaming) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<AssistantShell branding={branding} isStreaming={true}>
|
|
320
|
+
<div className="flex items-center gap-2 px-0.5 py-0.5 text-sm text-[var(--text-muted)]">
|
|
321
|
+
<span className="flex gap-[5px]">
|
|
322
|
+
<span className="h-2 w-2 animate-bounce rounded-full bg-[var(--brand-glow)]" style={{ animationDelay: "0ms" }} />
|
|
323
|
+
<span className="h-2 w-2 animate-bounce rounded-full bg-[var(--brand-glow)]" style={{ animationDelay: "150ms" }} />
|
|
324
|
+
<span className="h-2 w-2 animate-bounce rounded-full bg-[var(--brand-glow)]" style={{ animationDelay: "300ms" }} />
|
|
325
|
+
</span>
|
|
326
|
+
</div>
|
|
327
|
+
</AssistantShell>
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const showTraceChrome = allParts.some(({ part }) => {
|
|
332
|
+
if (part.type === "reasoning") {
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (part.type === "tool") {
|
|
337
|
+
return !isOpenUITool(part as ToolPart);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return false;
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
if (!showTraceChrome) {
|
|
344
|
+
return (
|
|
345
|
+
<AssistantShell branding={branding} isStreaming={isStreaming}>
|
|
346
|
+
{allParts.map(({ part, msgId, index }) => {
|
|
347
|
+
const key = `${msgId}-${index}`;
|
|
348
|
+
|
|
349
|
+
if (part.type === "tool" && isOpenUITool(part as ToolPart)) {
|
|
350
|
+
const toolPart = part as ToolPart;
|
|
351
|
+
const schema = extractOpenUISchema(toolPart.state.output);
|
|
352
|
+
const summary = getOpenUISummary(toolPart.state.output);
|
|
353
|
+
|
|
354
|
+
if (toolPart.state.status === "completed" && schema) {
|
|
355
|
+
return (
|
|
356
|
+
<div
|
|
357
|
+
key={key}
|
|
358
|
+
className="overflow-hidden rounded-[22px] border border-[var(--border-subtle)] bg-[var(--bg-root)]"
|
|
359
|
+
>
|
|
360
|
+
{summary ? (
|
|
361
|
+
<div className="border-b border-[var(--border-subtle)] px-4 py-3 text-sm leading-6 text-foreground">
|
|
362
|
+
{summary}
|
|
363
|
+
</div>
|
|
364
|
+
) : null}
|
|
365
|
+
<div className="p-4">
|
|
366
|
+
<OpenUIArtifactRenderer schema={schema} />
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (toolPart.state.status === "running") {
|
|
373
|
+
return (
|
|
374
|
+
<div
|
|
375
|
+
key={key}
|
|
376
|
+
className="flex items-center gap-2 rounded-[18px] border border-[var(--border-subtle)] bg-[var(--bg-root)] px-4 py-3 text-sm text-muted-foreground"
|
|
377
|
+
>
|
|
378
|
+
<Loader2 className="h-4 w-4 animate-spin text-primary" />
|
|
379
|
+
Building view…
|
|
380
|
+
</div>
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (part.type === "text" && !part.synthetic && part.text.trim()) {
|
|
386
|
+
return (
|
|
387
|
+
<div key={key} className="px-0.5">
|
|
388
|
+
<Markdown className="tangle-prose text-[15px] leading-7 text-[var(--text-primary)]">
|
|
389
|
+
{part.text}
|
|
390
|
+
</Markdown>
|
|
391
|
+
</div>
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return null;
|
|
396
|
+
})}
|
|
397
|
+
</AssistantShell>
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return (
|
|
402
|
+
<Collapsible.Root open={!collapsed} onOpenChange={() => onToggle()}>
|
|
403
|
+
<div className="rounded-[28px] border border-[var(--border-subtle)] bg-[var(--bg-card)] shadow-none">
|
|
404
|
+
{/* Header */}
|
|
405
|
+
<div className="flex items-start gap-3 px-4 py-3.5">
|
|
406
|
+
<Collapsible.Trigger asChild>
|
|
407
|
+
<button
|
|
408
|
+
className={cn(
|
|
409
|
+
"w-full rounded-[20px] px-0 py-0 text-left transition-colors",
|
|
410
|
+
"bg-transparent hover:bg-transparent",
|
|
411
|
+
)}
|
|
412
|
+
>
|
|
413
|
+
<div className="flex items-center gap-2.5">
|
|
414
|
+
<div
|
|
415
|
+
className={cn(
|
|
416
|
+
"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[var(--brand-primary)] text-white",
|
|
417
|
+
)}
|
|
418
|
+
>
|
|
419
|
+
<Bot className="h-4 w-4" />
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
<span className={cn("text-sm font-semibold", branding.textClass)}>
|
|
423
|
+
{branding.label}
|
|
424
|
+
</span>
|
|
425
|
+
|
|
426
|
+
{renderSummary(run) ? (
|
|
427
|
+
<span className="text-[11px] text-muted-foreground">{renderSummary(run)}</span>
|
|
428
|
+
) : null}
|
|
429
|
+
{collapsed && run.summaryText ? (
|
|
430
|
+
<span className="min-w-0 truncate text-[11px] text-foreground/70">
|
|
431
|
+
{run.summaryText}
|
|
432
|
+
</span>
|
|
433
|
+
) : null}
|
|
434
|
+
|
|
435
|
+
<div className="ml-auto flex shrink-0 items-center gap-1.5">
|
|
436
|
+
<CategoryBadges categories={stats.toolCategories} />
|
|
437
|
+
|
|
438
|
+
{isStreaming ? (
|
|
439
|
+
<span className="inline-flex items-center gap-1 rounded-full border border-[var(--border-accent)] bg-[var(--accent-surface-soft)] px-2 py-px text-[10px] font-semibold uppercase text-[var(--accent-text)]">
|
|
440
|
+
<Loader2 className="h-2.5 w-2.5 animate-spin" />
|
|
441
|
+
Running
|
|
442
|
+
</span>
|
|
443
|
+
) : (
|
|
444
|
+
<span className="inline-flex items-center gap-1 rounded-full border border-border px-2 py-px text-[10px] font-semibold uppercase text-muted-foreground">
|
|
445
|
+
<Sparkles className="h-2.5 w-2.5" />
|
|
446
|
+
Done
|
|
447
|
+
</span>
|
|
448
|
+
)}
|
|
449
|
+
|
|
450
|
+
{!collapsed ? (
|
|
451
|
+
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
|
|
452
|
+
) : (
|
|
453
|
+
<ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />
|
|
454
|
+
)}
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
</button>
|
|
458
|
+
</Collapsible.Trigger>
|
|
459
|
+
|
|
460
|
+
{headerActions ? (
|
|
461
|
+
<div className="flex shrink-0 flex-wrap items-center justify-end gap-1.5 pt-1">
|
|
462
|
+
{headerActions}
|
|
463
|
+
</div>
|
|
464
|
+
) : null}
|
|
465
|
+
</div>
|
|
466
|
+
|
|
467
|
+
{/* Summary text when collapsed */}
|
|
468
|
+
{collapsed && run.summaryText && (
|
|
469
|
+
<div className="px-4 pb-4 text-sm leading-6 text-muted-foreground line-clamp-2">
|
|
470
|
+
{run.summaryText}
|
|
471
|
+
</div>
|
|
472
|
+
)}
|
|
473
|
+
|
|
474
|
+
{/* Expanded content */}
|
|
475
|
+
<Collapsible.Content className="overflow-hidden data-[state=open]:animate-slideDown data-[state=closed]:animate-slideUp">
|
|
476
|
+
<div className={cn("space-y-3 border-t border-[var(--border-subtle)] px-4 pb-4 pt-3")}>
|
|
477
|
+
{allParts.map(({ part, msgId, index }, partIndex) => {
|
|
478
|
+
const key = `${msgId}-${index}`;
|
|
479
|
+
|
|
480
|
+
if (part.type === "tool") {
|
|
481
|
+
if (isOpenUITool(part as ToolPart)) {
|
|
482
|
+
const toolPart = part as ToolPart;
|
|
483
|
+
const schema = extractOpenUISchema(toolPart.state.output);
|
|
484
|
+
const summary = getOpenUISummary(toolPart.state.output);
|
|
485
|
+
|
|
486
|
+
if (toolPart.state.status === "completed" && schema) {
|
|
487
|
+
return (
|
|
488
|
+
<div
|
|
489
|
+
key={key}
|
|
490
|
+
className="overflow-hidden rounded-[24px] border border-[var(--border-subtle)] bg-[var(--bg-card)]"
|
|
491
|
+
>
|
|
492
|
+
{summary ? (
|
|
493
|
+
<div className="border-b border-[var(--border-subtle)] px-4 py-3">
|
|
494
|
+
<div className="text-[11px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
|
495
|
+
View
|
|
496
|
+
</div>
|
|
497
|
+
<div className="mt-1 text-sm leading-6 text-foreground">{summary}</div>
|
|
498
|
+
</div>
|
|
499
|
+
) : null}
|
|
500
|
+
<div className="p-4">
|
|
501
|
+
<OpenUIArtifactRenderer schema={schema} />
|
|
502
|
+
</div>
|
|
503
|
+
</div>
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (toolPart.state.status === "running") {
|
|
508
|
+
return (
|
|
509
|
+
<div
|
|
510
|
+
key={key}
|
|
511
|
+
className="flex items-center gap-3 rounded-[20px] border border-[var(--border-subtle)] bg-[var(--bg-card)] px-4 py-3 text-sm text-muted-foreground"
|
|
512
|
+
>
|
|
513
|
+
<Loader2 className="h-4 w-4 animate-spin text-primary" />
|
|
514
|
+
Building view…
|
|
515
|
+
</div>
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return (
|
|
521
|
+
<InlineToolItem
|
|
522
|
+
key={key}
|
|
523
|
+
part={part as ToolPart}
|
|
524
|
+
renderToolDetail={renderToolDetail}
|
|
525
|
+
groupPosition={getToolGroupPosition(partIndex, allParts)}
|
|
526
|
+
actions={renderToolActions?.(part as ToolPart, {
|
|
527
|
+
run,
|
|
528
|
+
messageId: msgId,
|
|
529
|
+
partIndex: index,
|
|
530
|
+
})}
|
|
531
|
+
/>
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (part.type === "reasoning") {
|
|
536
|
+
return (
|
|
537
|
+
<InlineThinkingItem
|
|
538
|
+
key={key}
|
|
539
|
+
part={part as ReasoningPart}
|
|
540
|
+
defaultOpen={isStreaming}
|
|
541
|
+
/>
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (
|
|
546
|
+
part.type === "text" &&
|
|
547
|
+
!part.synthetic &&
|
|
548
|
+
part.text.trim()
|
|
549
|
+
) {
|
|
550
|
+
return (
|
|
551
|
+
<div
|
|
552
|
+
key={key}
|
|
553
|
+
className="px-1 py-1"
|
|
554
|
+
>
|
|
555
|
+
<Markdown className="tangle-prose text-[15px] leading-7">{part.text}</Markdown>
|
|
556
|
+
</div>
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return null;
|
|
561
|
+
})}
|
|
562
|
+
</div>
|
|
563
|
+
</Collapsible.Content>
|
|
564
|
+
</div>
|
|
565
|
+
</Collapsible.Root>
|
|
566
|
+
);
|
|
567
|
+
},
|
|
568
|
+
);
|
|
569
|
+
RunGroup.displayName = "RunGroup";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { formatDuration } from "../utils/format";
|
|
3
|
+
|
|
4
|
+
export function LiveDuration({ startTime }: { startTime: number }) {
|
|
5
|
+
const [elapsed, setElapsed] = useState(Date.now() - startTime);
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const id = setInterval(() => setElapsed(Date.now() - startTime), 100);
|
|
9
|
+
return () => clearInterval(id);
|
|
10
|
+
}, [startTime]);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<span className="text-xs font-mono text-neutral-400 dark:text-neutral-500 tabular-nums">
|
|
14
|
+
{formatDuration(elapsed)}
|
|
15
|
+
</span>
|
|
16
|
+
);
|
|
17
|
+
}
|