@springbrand/message-panel 0.1.3-alpha.1 → 0.1.3-alpha.3
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/cloud-os/README.md +71 -0
- package/cloud-os/chat/activity-indicator.tsx +29 -0
- package/cloud-os/chat/cloud-os-chat-messages.tsx +632 -0
- package/cloud-os/chat/markdown-message.tsx +78 -0
- package/cloud-os/chat/rich-blocks.tsx +672 -0
- package/cloud-os/chat/tool-presentation.ts +584 -0
- package/cloud-os/chat/tool-rows.tsx +216 -0
- package/cloud-os/chat/transcript-model.ts +672 -0
- package/cloud-os/composer/cloud-os-chat-input.tsx +541 -0
- package/cloud-os/index.ts +107 -0
- package/cloud-os/internal/cn.ts +6 -0
- package/cloud-os/internal/theme-context.tsx +16 -0
- package/cloud-os/layout/cloud-os-root.tsx +34 -0
- package/cloud-os/layout/cloud-os-workspace-split.tsx +242 -0
- package/cloud-os/primitives/dropdown-menu.tsx +82 -0
- package/cloud-os/primitives/tooltip.tsx +68 -0
- package/cloud-os/primitives/workshop-controls.tsx +114 -0
- package/cloud-os/styles/cloud-os.css +573 -0
- package/cloud-os/workspace/cloud-os-workspace-panel.tsx +272 -0
- package/demo/camel-chat-showcase.tsx +13 -917
- package/demo/chat-scenarios.ts +926 -0
- package/demo/cloud-os-chat-showcase.tsx +471 -0
- package/demo/index.ts +2 -0
- package/package.json +16 -4
- package/src/camel/camel-chat-messages.tsx +41 -18
- package/src/camel/camel-prompt-input.tsx +2 -1
- package/src/chat-summary-panel.tsx +1 -1
- package/src/composer/chat-composer.tsx +2 -1
- package/src/composer/composer.tsx +2 -1
- package/src/composer/index.ts +1 -0
- package/src/composer/key-rules.ts +12 -0
- package/src/message.tsx +0 -8
- package/src/styles/index.css +4 -1
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
import type { UIMessage } from "ai";
|
|
2
|
+
import {
|
|
3
|
+
ArrowDown,
|
|
4
|
+
Brain,
|
|
5
|
+
CaretRight,
|
|
6
|
+
Check,
|
|
7
|
+
Copy,
|
|
8
|
+
File as FileIcon,
|
|
9
|
+
Terminal,
|
|
10
|
+
} from "@phosphor-icons/react";
|
|
11
|
+
import {
|
|
12
|
+
Fragment,
|
|
13
|
+
useCallback,
|
|
14
|
+
useEffect,
|
|
15
|
+
useMemo,
|
|
16
|
+
useRef,
|
|
17
|
+
useState,
|
|
18
|
+
type ReactNode,
|
|
19
|
+
} from "react";
|
|
20
|
+
import { Tooltip } from "../primitives/tooltip";
|
|
21
|
+
import { WorkshopIconButton } from "../primitives/workshop-controls";
|
|
22
|
+
import {
|
|
23
|
+
MarkdownMessage,
|
|
24
|
+
type CloudOsUrlResolver,
|
|
25
|
+
} from "./markdown-message";
|
|
26
|
+
import {
|
|
27
|
+
ApprovalBlock,
|
|
28
|
+
AskUserBlock,
|
|
29
|
+
ErrorBlock,
|
|
30
|
+
ParallelBlock,
|
|
31
|
+
PlanBlock,
|
|
32
|
+
ScheduleBlock,
|
|
33
|
+
SubAgentsBlock,
|
|
34
|
+
SuggestionsBlock,
|
|
35
|
+
type ApprovalDecision,
|
|
36
|
+
} from "./rich-blocks";
|
|
37
|
+
import { ThinkingTraceRow, ToolGroupRow } from "./tool-rows";
|
|
38
|
+
import { CloudOsActivityIndicator } from "./activity-indicator";
|
|
39
|
+
import {
|
|
40
|
+
buildCloudOsEntries,
|
|
41
|
+
deriveTurnActivity,
|
|
42
|
+
formatClockTime,
|
|
43
|
+
formatFullTimestamp,
|
|
44
|
+
rhythmTopClass,
|
|
45
|
+
type CloudOsEntry,
|
|
46
|
+
} from "./transcript-model";
|
|
47
|
+
|
|
48
|
+
export type CloudOsChatStatus = "ready" | "submitted" | "streaming" | "error";
|
|
49
|
+
|
|
50
|
+
export interface CloudOsChatMessagesProps {
|
|
51
|
+
messages: readonly UIMessage[];
|
|
52
|
+
status: CloudOsChatStatus;
|
|
53
|
+
isHydrating?: boolean;
|
|
54
|
+
isRecovering?: boolean;
|
|
55
|
+
recoveryStatusLabel?: string;
|
|
56
|
+
/** 有 pending steer 时,活动占位说明「正在等当前步骤收尾」。 */
|
|
57
|
+
hasPendingSteer?: boolean;
|
|
58
|
+
/** 首帧就带着待发消息:此时不显示空会话欢迎语。 */
|
|
59
|
+
startedWithPending?: boolean;
|
|
60
|
+
error?: unknown;
|
|
61
|
+
showThinkingTraces?: boolean;
|
|
62
|
+
/** 把某个 part 映射成待授权 id;返回 undefined 表示不在此处渲染授权。 */
|
|
63
|
+
approvalIdOf?: (part: unknown) => string | undefined;
|
|
64
|
+
approvalsDisabled?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* 用宿主自己的卡片渲染某条待授权。返回 undefined 就退回内置的通用授权行。
|
|
67
|
+
*
|
|
68
|
+
* 存在的理由:授权行只认得工具名和入参,说不出「这次要花多少钱、买的是什么」。
|
|
69
|
+
* 需要在决定之前把这些摆到人面前的宿主,得自己画那张卡。
|
|
70
|
+
*/
|
|
71
|
+
renderApproval?: (approval: {
|
|
72
|
+
approvalId: string;
|
|
73
|
+
toolName: string;
|
|
74
|
+
input: Record<string, unknown>;
|
|
75
|
+
disabled: boolean;
|
|
76
|
+
}) => ReactNode;
|
|
77
|
+
/** 把消息里的 `sandbox:/workspace/...` 之类地址翻译成可访问 URL。 */
|
|
78
|
+
resolveUrl?: CloudOsUrlResolver;
|
|
79
|
+
/** 消息区宽度是否收敛到 920px 居中(GadgetEditor 里叫 constrainChatWidth)。 */
|
|
80
|
+
constrainWidth?: boolean;
|
|
81
|
+
emptyTitle?: string;
|
|
82
|
+
onRetry?: () => void;
|
|
83
|
+
onApprove?: (approvalId: string, decision: ApprovalDecision) => void;
|
|
84
|
+
onSuggestion?: (text: string) => void;
|
|
85
|
+
/**
|
|
86
|
+
* 把交互卡片上的操作回写成那次工具调用的结果(缺省 → 卡片不可操作)。
|
|
87
|
+
* 卡片不再知道「答案怎么变成消息」—— 它只认自己的 toolCallId。
|
|
88
|
+
*/
|
|
89
|
+
onRespond?: (toolCallId: string, response: unknown) => Promise<boolean>;
|
|
90
|
+
/** 点击定时任务卡片:宿主决定跳到哪(缺省 → 卡片不可点)。 */
|
|
91
|
+
onOpenSchedule?: (scheduleId: string) => void;
|
|
92
|
+
onCopy?: (text: string) => void;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function useCopyAction(): {
|
|
96
|
+
copied: string | null;
|
|
97
|
+
copy: (text: string) => void;
|
|
98
|
+
} {
|
|
99
|
+
const [copied, setCopied] = useState<string | null>(null);
|
|
100
|
+
const timerRef = useRef<number | null>(null);
|
|
101
|
+
useEffect(
|
|
102
|
+
() => () => {
|
|
103
|
+
if (timerRef.current !== null) window.clearTimeout(timerRef.current);
|
|
104
|
+
},
|
|
105
|
+
[],
|
|
106
|
+
);
|
|
107
|
+
const copy = useCallback((text: string) => {
|
|
108
|
+
void navigator.clipboard.writeText(text).then(() => {
|
|
109
|
+
setCopied(text);
|
|
110
|
+
if (timerRef.current !== null) window.clearTimeout(timerRef.current);
|
|
111
|
+
timerRef.current = window.setTimeout(() => setCopied(null), 1500);
|
|
112
|
+
});
|
|
113
|
+
}, []);
|
|
114
|
+
return { copied, copy };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function UserBubble({
|
|
118
|
+
entry,
|
|
119
|
+
onCopy,
|
|
120
|
+
copied,
|
|
121
|
+
resolveUrl,
|
|
122
|
+
}: {
|
|
123
|
+
entry: Extract<CloudOsEntry, { type: "user" }>;
|
|
124
|
+
onCopy: (text: string) => void;
|
|
125
|
+
copied: boolean;
|
|
126
|
+
resolveUrl?: CloudOsUrlResolver;
|
|
127
|
+
}) {
|
|
128
|
+
return (
|
|
129
|
+
<div className="group/message relative flex flex-col items-end">
|
|
130
|
+
{entry.attachments.length > 0 && (
|
|
131
|
+
<div className="mb-1.5 flex max-w-[min(680px,78%)] flex-wrap justify-end gap-2">
|
|
132
|
+
{entry.attachments.map((attachment, index) =>
|
|
133
|
+
attachment.mediaType?.startsWith("image/") ? (
|
|
134
|
+
<img
|
|
135
|
+
key={`${attachment.url}:${index}`}
|
|
136
|
+
src={attachment.url}
|
|
137
|
+
alt={attachment.filename ?? "Attachment"}
|
|
138
|
+
className="themed-thumbnail-shadow max-h-52 max-w-64 rounded-xl border border-kumo-line object-cover"
|
|
139
|
+
/>
|
|
140
|
+
) : (
|
|
141
|
+
<span
|
|
142
|
+
key={`${attachment.url}:${index}`}
|
|
143
|
+
className="inline-flex items-center gap-1.5 rounded-lg border border-kumo-line bg-kumo-elevated px-2.5 py-1.5 text-[12px] leading-4 text-kumo-subtle"
|
|
144
|
+
>
|
|
145
|
+
<FileIcon size={13} className="text-kumo-inactive" />
|
|
146
|
+
{attachment.filename ?? "Attachment"}
|
|
147
|
+
</span>
|
|
148
|
+
),
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
{entry.text && (
|
|
153
|
+
<div className="themed-user-bubble-shadow cos-markdown w-fit max-w-[min(680px,78%)] rounded-[24px] rounded-br-lg border border-transparent bg-kumo-bubble-user px-4 py-2.5 text-[14px] leading-[22px] tracking-[-0.25px] text-kumo-default">
|
|
154
|
+
{/* pre-wrap 让用户消息里的单个换行渲染成硬换行 */}
|
|
155
|
+
<div className="whitespace-pre-wrap">
|
|
156
|
+
<MarkdownMessage message={entry.text} resolveUrl={resolveUrl} />
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
<div className="mt-0.5 flex items-center justify-end gap-2 pr-1 text-[11px] leading-4 text-kumo-inactive opacity-0 transition-opacity duration-150 ease-out group-hover/message:opacity-100 group-focus-within/message:opacity-100">
|
|
161
|
+
{entry.authorName && <span className="font-medium">{entry.authorName}</span>}
|
|
162
|
+
{entry.timestamp !== undefined && (
|
|
163
|
+
<Tooltip content={formatFullTimestamp(entry.timestamp)} asChild>
|
|
164
|
+
<span className="font-mono">{formatClockTime(entry.timestamp)}</span>
|
|
165
|
+
</Tooltip>
|
|
166
|
+
)}
|
|
167
|
+
{entry.text && (
|
|
168
|
+
<Tooltip content={copied ? "Copied!" : "Copy message"} asChild>
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
onClick={() => onCopy(entry.text)}
|
|
172
|
+
className="flex cursor-pointer items-center rounded-md p-1 text-kumo-inactive transition-[color,transform] duration-150 ease-out hover:text-kumo-default focus-visible:text-kumo-default focus-visible:outline-none active:scale-[0.96]"
|
|
173
|
+
aria-label={copied ? "Copied!" : "Copy message"}
|
|
174
|
+
>
|
|
175
|
+
{copied ? <Check size={13} weight="bold" /> : <Copy size={13} />}
|
|
176
|
+
</button>
|
|
177
|
+
</Tooltip>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function CloudOsChatMessages({
|
|
185
|
+
messages,
|
|
186
|
+
status,
|
|
187
|
+
isHydrating = false,
|
|
188
|
+
isRecovering = false,
|
|
189
|
+
recoveryStatusLabel,
|
|
190
|
+
hasPendingSteer = false,
|
|
191
|
+
startedWithPending = false,
|
|
192
|
+
error,
|
|
193
|
+
showThinkingTraces = true,
|
|
194
|
+
approvalIdOf,
|
|
195
|
+
approvalsDisabled = false,
|
|
196
|
+
renderApproval,
|
|
197
|
+
resolveUrl,
|
|
198
|
+
constrainWidth = true,
|
|
199
|
+
emptyTitle = "What are we working on?",
|
|
200
|
+
onRetry,
|
|
201
|
+
onApprove,
|
|
202
|
+
onSuggestion,
|
|
203
|
+
onRespond,
|
|
204
|
+
onOpenSchedule,
|
|
205
|
+
onCopy,
|
|
206
|
+
}: CloudOsChatMessagesProps) {
|
|
207
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
208
|
+
const endRef = useRef<HTMLDivElement>(null);
|
|
209
|
+
const [showScrollButton, setShowScrollButton] = useState(false);
|
|
210
|
+
const [expandedToolCalls, setExpandedToolCalls] = useState<ReadonlySet<string>>(
|
|
211
|
+
() => new Set(),
|
|
212
|
+
);
|
|
213
|
+
const [expandedCompactions, setExpandedCompactions] = useState<
|
|
214
|
+
ReadonlySet<string>
|
|
215
|
+
>(() => new Set());
|
|
216
|
+
const { copied, copy } = useCopyAction();
|
|
217
|
+
const isActive = status === "submitted" || status === "streaming";
|
|
218
|
+
|
|
219
|
+
const entries = useMemo(
|
|
220
|
+
() =>
|
|
221
|
+
buildCloudOsEntries({
|
|
222
|
+
messages,
|
|
223
|
+
isActive,
|
|
224
|
+
showThinkingTraces,
|
|
225
|
+
approvalIdOf,
|
|
226
|
+
}),
|
|
227
|
+
[approvalIdOf, isActive, messages, showThinkingTraces],
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const entryTopClasses = useMemo(
|
|
231
|
+
() =>
|
|
232
|
+
entries.map((entry, index) =>
|
|
233
|
+
rhythmTopClass(index > 0 ? entries[index - 1] : null, entry),
|
|
234
|
+
),
|
|
235
|
+
[entries],
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const toggleToolCall = useCallback((key: string) => {
|
|
239
|
+
setExpandedToolCalls((current) => {
|
|
240
|
+
const next = new Set(current);
|
|
241
|
+
if (next.has(key)) next.delete(key);
|
|
242
|
+
else next.add(key);
|
|
243
|
+
return next;
|
|
244
|
+
});
|
|
245
|
+
}, []);
|
|
246
|
+
|
|
247
|
+
const toggleCompaction = useCallback((key: string) => {
|
|
248
|
+
setExpandedCompactions((current) => {
|
|
249
|
+
const next = new Set(current);
|
|
250
|
+
if (next.has(key)) next.delete(key);
|
|
251
|
+
else next.add(key);
|
|
252
|
+
return next;
|
|
253
|
+
});
|
|
254
|
+
}, []);
|
|
255
|
+
|
|
256
|
+
const syncScrollButton = useCallback(() => {
|
|
257
|
+
const element = scrollRef.current;
|
|
258
|
+
if (!element) return;
|
|
259
|
+
const distance =
|
|
260
|
+
element.scrollHeight - element.scrollTop - element.clientHeight;
|
|
261
|
+
setShowScrollButton(distance > 120);
|
|
262
|
+
}, []);
|
|
263
|
+
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
const element = scrollRef.current;
|
|
266
|
+
if (!element || showScrollButton) return;
|
|
267
|
+
element.scrollTo({ top: element.scrollHeight });
|
|
268
|
+
}, [isRecovering, messages, status, showScrollButton]);
|
|
269
|
+
|
|
270
|
+
// 判据是「尾巴有没有活口」,不是原文件的「本回合有没有出过内容」——
|
|
271
|
+
// 后者会让「答完 ask_user 之后到模型下一个 chunk」这段完全没有指示,
|
|
272
|
+
// 而卡片此时写着 Submitted,读起来就是卡死了。见 deriveTurnActivity。
|
|
273
|
+
const activity = deriveTurnActivity(entries, { isActive, hasPendingSteer });
|
|
274
|
+
const copyText = (text: string) => {
|
|
275
|
+
copy(text);
|
|
276
|
+
onCopy?.(text);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<div
|
|
281
|
+
className="relative flex min-h-0 flex-1 flex-col overflow-hidden bg-kumo-base"
|
|
282
|
+
data-cloud-os-transcript=""
|
|
283
|
+
data-status={status}
|
|
284
|
+
aria-busy={isActive || isHydrating}
|
|
285
|
+
>
|
|
286
|
+
<div
|
|
287
|
+
ref={scrollRef}
|
|
288
|
+
onScroll={syncScrollButton}
|
|
289
|
+
tabIndex={0}
|
|
290
|
+
role="region"
|
|
291
|
+
aria-label="Chat messages"
|
|
292
|
+
className="cos-chat-panel flex-1 overflow-y-auto"
|
|
293
|
+
>
|
|
294
|
+
<div
|
|
295
|
+
className={`flex flex-col px-6 pb-8 pt-8 ${
|
|
296
|
+
constrainWidth ? "mx-auto w-full max-w-[920px]" : ""
|
|
297
|
+
}`}
|
|
298
|
+
>
|
|
299
|
+
{entries.length === 0 && !startedWithPending && !isHydrating && (
|
|
300
|
+
<div className="flex min-h-56 items-center justify-center text-center text-[24px] leading-8 font-semibold tracking-[-0.4px] text-kumo-default">
|
|
301
|
+
{emptyTitle}
|
|
302
|
+
</div>
|
|
303
|
+
)}
|
|
304
|
+
|
|
305
|
+
{entries.map((entry, entryIndex) => {
|
|
306
|
+
const topClass = entryTopClasses[entryIndex] ?? "";
|
|
307
|
+
|
|
308
|
+
if (entry.type === "user") {
|
|
309
|
+
return (
|
|
310
|
+
<div key={entry.key} className={topClass}>
|
|
311
|
+
<UserBubble
|
|
312
|
+
entry={entry}
|
|
313
|
+
onCopy={copyText}
|
|
314
|
+
copied={copied === entry.text}
|
|
315
|
+
resolveUrl={resolveUrl}
|
|
316
|
+
/>
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (entry.type === "slashCommand") {
|
|
322
|
+
return (
|
|
323
|
+
<div key={entry.key} className={`${topClass} flex flex-col items-end`}>
|
|
324
|
+
<div className="themed-user-bubble-shadow w-fit max-w-[min(680px,78%)] rounded-[24px] rounded-br-lg border border-transparent bg-kumo-bubble-user px-4 py-2.5 text-[14px] leading-[22px] tracking-[-0.25px] text-kumo-brand">
|
|
325
|
+
<span className="whitespace-pre-wrap font-mono">{entry.text}</span>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (entry.type === "localCommandOutput") {
|
|
332
|
+
return (
|
|
333
|
+
<div
|
|
334
|
+
key={entry.key}
|
|
335
|
+
className={`${topClass} max-w-[860px] text-[14px] leading-5 tracking-[-0.25px] text-kumo-subtle`}
|
|
336
|
+
>
|
|
337
|
+
<div className="flex items-center gap-3 px-1.5 py-1">
|
|
338
|
+
<span
|
|
339
|
+
className="flex h-5 w-5 flex-shrink-0 items-center justify-center text-kumo-inactive"
|
|
340
|
+
aria-hidden="true"
|
|
341
|
+
>
|
|
342
|
+
<Terminal size={16} />
|
|
343
|
+
</span>
|
|
344
|
+
<span className="min-w-0 truncate font-mono text-[13px]">
|
|
345
|
+
{entry.text}
|
|
346
|
+
</span>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (entry.type === "notice") {
|
|
353
|
+
return (
|
|
354
|
+
<div
|
|
355
|
+
key={entry.key}
|
|
356
|
+
className={`${topClass} max-w-[860px] text-[13px] leading-[19px] tracking-[-0.25px] text-kumo-inactive`}
|
|
357
|
+
>
|
|
358
|
+
<div className="px-1.5 py-1">{entry.text}</div>
|
|
359
|
+
</div>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (entry.type === "compactionBoundary") {
|
|
364
|
+
const expanded = expandedCompactions.has(entry.key);
|
|
365
|
+
return (
|
|
366
|
+
<div key={entry.key} className={`${topClass} mb-4 max-w-[860px]`}>
|
|
367
|
+
<div
|
|
368
|
+
className="flex items-center gap-3"
|
|
369
|
+
role="separator"
|
|
370
|
+
aria-label="Context compacted"
|
|
371
|
+
>
|
|
372
|
+
<span className="h-px flex-1 bg-kumo-line" aria-hidden="true" />
|
|
373
|
+
<button
|
|
374
|
+
type="button"
|
|
375
|
+
onClick={() => toggleCompaction(entry.key)}
|
|
376
|
+
aria-expanded={expanded}
|
|
377
|
+
className="flex flex-shrink-0 cursor-pointer items-center gap-1.5 rounded-md px-1 py-0.5 text-[11px] leading-4 font-medium tracking-[0.6px] text-kumo-inactive uppercase transition-colors duration-150 ease-out hover:text-kumo-default focus-visible:text-kumo-default focus-visible:outline-none"
|
|
378
|
+
>
|
|
379
|
+
<Brain size={13} aria-hidden="true" />
|
|
380
|
+
Context compacted
|
|
381
|
+
<CaretRight
|
|
382
|
+
size={11}
|
|
383
|
+
weight="bold"
|
|
384
|
+
className={`transition-transform duration-150 ease-out ${expanded ? "rotate-90" : ""}`}
|
|
385
|
+
/>
|
|
386
|
+
</button>
|
|
387
|
+
<span className="h-px flex-1 bg-kumo-line" aria-hidden="true" />
|
|
388
|
+
</div>
|
|
389
|
+
{expanded && (
|
|
390
|
+
<div className="themed-surface-inset cos-markdown mt-3 rounded-2xl border border-kumo-line/70 bg-kumo-elevated/45 p-3.5 text-[13px] leading-[19px]">
|
|
391
|
+
<MarkdownMessage
|
|
392
|
+
message={entry.summary}
|
|
393
|
+
resolveUrl={resolveUrl}
|
|
394
|
+
/>
|
|
395
|
+
</div>
|
|
396
|
+
)}
|
|
397
|
+
</div>
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const hasNarrative = entry.blocks.some(
|
|
402
|
+
(block) => block.kind === "text",
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
return (
|
|
406
|
+
<div
|
|
407
|
+
key={entry.key}
|
|
408
|
+
className={`${topClass} min-w-0 w-full max-w-[860px] space-y-2`}
|
|
409
|
+
>
|
|
410
|
+
<div className="group/agentMessage relative space-y-1.5">
|
|
411
|
+
{entry.blocks.map((block) => {
|
|
412
|
+
switch (block.kind) {
|
|
413
|
+
case "reasoning":
|
|
414
|
+
return (
|
|
415
|
+
<ThinkingTraceRow key={block.key} reasoning={block.text} />
|
|
416
|
+
);
|
|
417
|
+
case "text":
|
|
418
|
+
return (
|
|
419
|
+
<div
|
|
420
|
+
key={block.key}
|
|
421
|
+
className="cos-markdown text-[14px] leading-[22px] tracking-[-0.25px] text-kumo-default"
|
|
422
|
+
>
|
|
423
|
+
<MarkdownMessage
|
|
424
|
+
message={block.text}
|
|
425
|
+
resolveUrl={resolveUrl}
|
|
426
|
+
/>
|
|
427
|
+
</div>
|
|
428
|
+
);
|
|
429
|
+
case "toolGroup":
|
|
430
|
+
return (
|
|
431
|
+
<ToolGroupRow
|
|
432
|
+
key={block.key}
|
|
433
|
+
group={block.group}
|
|
434
|
+
open={expandedToolCalls.has(block.group.key)}
|
|
435
|
+
expandedKeys={expandedToolCalls}
|
|
436
|
+
onToggle={toggleToolCall}
|
|
437
|
+
/>
|
|
438
|
+
);
|
|
439
|
+
case "plan":
|
|
440
|
+
return (
|
|
441
|
+
<PlanBlock
|
|
442
|
+
key={block.key}
|
|
443
|
+
steps={block.steps}
|
|
444
|
+
running={block.running}
|
|
445
|
+
/>
|
|
446
|
+
);
|
|
447
|
+
case "askUser":
|
|
448
|
+
return (
|
|
449
|
+
<AskUserBlock
|
|
450
|
+
key={block.key}
|
|
451
|
+
call={block.call}
|
|
452
|
+
onRespond={onRespond}
|
|
453
|
+
/>
|
|
454
|
+
);
|
|
455
|
+
case "suggestions":
|
|
456
|
+
return (
|
|
457
|
+
<SuggestionsBlock
|
|
458
|
+
key={block.key}
|
|
459
|
+
items={block.items}
|
|
460
|
+
onSuggestion={(text) => onSuggestion?.(text)}
|
|
461
|
+
/>
|
|
462
|
+
);
|
|
463
|
+
case "schedule":
|
|
464
|
+
return (
|
|
465
|
+
<ScheduleBlock
|
|
466
|
+
key={block.key}
|
|
467
|
+
call={block.call}
|
|
468
|
+
onOpen={onOpenSchedule}
|
|
469
|
+
/>
|
|
470
|
+
);
|
|
471
|
+
case "approval": {
|
|
472
|
+
const custom = renderApproval?.({
|
|
473
|
+
approvalId: block.approvalId,
|
|
474
|
+
toolName: block.call.toolName,
|
|
475
|
+
input: block.call.input,
|
|
476
|
+
disabled: approvalsDisabled,
|
|
477
|
+
});
|
|
478
|
+
if (custom !== undefined && custom !== null) {
|
|
479
|
+
return (
|
|
480
|
+
<Fragment key={block.key}>{custom}</Fragment>
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
return (
|
|
484
|
+
<ApprovalBlock
|
|
485
|
+
key={block.key}
|
|
486
|
+
call={block.call}
|
|
487
|
+
approvalId={block.approvalId}
|
|
488
|
+
disabled={approvalsDisabled}
|
|
489
|
+
onApprove={(id, decision) => onApprove?.(id, decision)}
|
|
490
|
+
/>
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
case "parallel":
|
|
494
|
+
return (
|
|
495
|
+
<ParallelBlock
|
|
496
|
+
key={block.key}
|
|
497
|
+
label={block.label}
|
|
498
|
+
tools={block.tools}
|
|
499
|
+
/>
|
|
500
|
+
);
|
|
501
|
+
case "subagents":
|
|
502
|
+
return (
|
|
503
|
+
<SubAgentsBlock key={block.key} agents={block.agents} />
|
|
504
|
+
);
|
|
505
|
+
case "image":
|
|
506
|
+
return (
|
|
507
|
+
<figure key={block.key} className="my-1">
|
|
508
|
+
<img
|
|
509
|
+
src={resolveUrl?.(block.src) ?? block.src}
|
|
510
|
+
alt={block.alt}
|
|
511
|
+
className="max-h-[28rem] max-w-full rounded-xl border border-kumo-line object-contain"
|
|
512
|
+
/>
|
|
513
|
+
</figure>
|
|
514
|
+
);
|
|
515
|
+
case "error":
|
|
516
|
+
return (
|
|
517
|
+
<ErrorBlock
|
|
518
|
+
key={block.key}
|
|
519
|
+
title={block.title}
|
|
520
|
+
body={block.body}
|
|
521
|
+
/>
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
})}
|
|
525
|
+
|
|
526
|
+
{entry.terminal?.kind === "error" && (
|
|
527
|
+
<ErrorBlock
|
|
528
|
+
title={entry.terminal.title}
|
|
529
|
+
body={entry.terminal.body}
|
|
530
|
+
onRetry={onRetry}
|
|
531
|
+
/>
|
|
532
|
+
)}
|
|
533
|
+
|
|
534
|
+
{entry.terminal?.kind === "interrupted" && (
|
|
535
|
+
<div
|
|
536
|
+
className="px-1.5 py-1 text-[13px] leading-[19px] text-kumo-inactive"
|
|
537
|
+
role="status"
|
|
538
|
+
>
|
|
539
|
+
{entry.terminal.title}
|
|
540
|
+
</div>
|
|
541
|
+
)}
|
|
542
|
+
|
|
543
|
+
{hasNarrative && entry.timestamp !== undefined && (
|
|
544
|
+
<div className="mt-0.5 -ml-1 flex items-center gap-1 opacity-0 transition-opacity duration-150 ease-out group-hover/agentMessage:opacity-100 group-focus-within/agentMessage:opacity-100">
|
|
545
|
+
<Tooltip
|
|
546
|
+
content={copied === entry.copyText ? "Copied!" : "Copy message"}
|
|
547
|
+
asChild
|
|
548
|
+
>
|
|
549
|
+
<button
|
|
550
|
+
type="button"
|
|
551
|
+
onClick={() => copyText(entry.copyText)}
|
|
552
|
+
className="flex cursor-pointer items-center rounded-md p-1 text-kumo-inactive transition-[color,transform] duration-150 ease-out hover:text-kumo-default focus-visible:text-kumo-default focus-visible:outline-none active:scale-[0.96]"
|
|
553
|
+
aria-label="Copy message"
|
|
554
|
+
>
|
|
555
|
+
{copied === entry.copyText ? (
|
|
556
|
+
<Check size={15} weight="bold" />
|
|
557
|
+
) : (
|
|
558
|
+
<Copy size={15} />
|
|
559
|
+
)}
|
|
560
|
+
</button>
|
|
561
|
+
</Tooltip>
|
|
562
|
+
<Tooltip content={formatFullTimestamp(entry.timestamp)} asChild>
|
|
563
|
+
<span className="px-1 font-mono text-[11px] leading-4 text-kumo-inactive">
|
|
564
|
+
{formatClockTime(entry.timestamp)}
|
|
565
|
+
</span>
|
|
566
|
+
</Tooltip>
|
|
567
|
+
</div>
|
|
568
|
+
)}
|
|
569
|
+
</div>
|
|
570
|
+
</div>
|
|
571
|
+
);
|
|
572
|
+
})}
|
|
573
|
+
|
|
574
|
+
{isRecovering && (
|
|
575
|
+
<div className="mt-4 flex max-w-[860px] items-center gap-3 px-1.5 py-1 text-[14px] leading-5 tracking-[-0.25px]">
|
|
576
|
+
<span className="cos-thinking-shimmer">
|
|
577
|
+
{recoveryStatusLabel ?? "Reconnecting…"}
|
|
578
|
+
</span>
|
|
579
|
+
</div>
|
|
580
|
+
)}
|
|
581
|
+
|
|
582
|
+
{activity && (
|
|
583
|
+
<div className="mt-5 inline-flex px-1.5 py-1 text-[14px] leading-5 tracking-[-0.25px]">
|
|
584
|
+
<CloudOsActivityIndicator {...activity} />
|
|
585
|
+
</div>
|
|
586
|
+
)}
|
|
587
|
+
|
|
588
|
+
{error != null && (
|
|
589
|
+
<div className="mt-4">
|
|
590
|
+
<ErrorBlock
|
|
591
|
+
title="Error"
|
|
592
|
+
body={
|
|
593
|
+
error instanceof Error
|
|
594
|
+
? error.message
|
|
595
|
+
: typeof error === "string"
|
|
596
|
+
? error
|
|
597
|
+
: "Something went wrong"
|
|
598
|
+
}
|
|
599
|
+
onRetry={onRetry}
|
|
600
|
+
/>
|
|
601
|
+
</div>
|
|
602
|
+
)}
|
|
603
|
+
|
|
604
|
+
<div ref={endRef} />
|
|
605
|
+
</div>
|
|
606
|
+
</div>
|
|
607
|
+
|
|
608
|
+
<WorkshopIconButton
|
|
609
|
+
aria-label="Scroll to bottom"
|
|
610
|
+
onClick={() => endRef.current?.scrollIntoView({ behavior: "smooth" })}
|
|
611
|
+
className={`themed-floating-shadow absolute bottom-3 left-1/2 z-20 -translate-x-1/2 !rounded-full border border-kumo-line bg-kumo-base transition-all duration-200 ${
|
|
612
|
+
showScrollButton
|
|
613
|
+
? "translate-y-0 opacity-100"
|
|
614
|
+
: "pointer-events-none translate-y-2 opacity-0"
|
|
615
|
+
}`}
|
|
616
|
+
>
|
|
617
|
+
<ArrowDown size={15} />
|
|
618
|
+
</WorkshopIconButton>
|
|
619
|
+
|
|
620
|
+
{isHydrating && (
|
|
621
|
+
<div className="absolute inset-0 z-30 grid place-items-center bg-kumo-base/90">
|
|
622
|
+
<div className="flex flex-col items-center gap-3">
|
|
623
|
+
<div className="h-6 w-6 animate-spin rounded-full border-2 border-kumo-brand border-t-transparent" />
|
|
624
|
+
<p className="m-0 text-[13px] leading-[18px] text-kumo-subtle">
|
|
625
|
+
Loading conversation…
|
|
626
|
+
</p>
|
|
627
|
+
</div>
|
|
628
|
+
</div>
|
|
629
|
+
)}
|
|
630
|
+
</div>
|
|
631
|
+
);
|
|
632
|
+
}
|