@springbrand/message-panel 0.1.3-alpha.3 → 0.1.3-alpha.30
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 +6 -6
- package/cloud-os/assets/followup-arrow.svg +3 -0
- package/cloud-os/assets/loading-corner.svg +3 -0
- package/cloud-os/assets/loading-mark.svg +16 -0
- package/cloud-os/assets/loading-spark.svg +3 -0
- package/cloud-os/assets/model-selected.svg +5 -0
- package/cloud-os/assets/thinking-spark.svg +9 -0
- package/cloud-os/capability-chip.tsx +35 -0
- package/cloud-os/chat/activity-indicator.tsx +2 -2
- package/cloud-os/chat/cloud-os-chat-messages.tsx +374 -63
- package/cloud-os/chat/image-generation.tsx +76 -0
- package/cloud-os/chat/loading-state.tsx +25 -0
- package/cloud-os/chat/markdown-message.tsx +144 -41
- package/cloud-os/chat/rich-blocks.tsx +548 -246
- package/cloud-os/chat/tool-presentation.ts +35 -16
- package/cloud-os/chat/tool-rows.tsx +330 -85
- package/cloud-os/chat/transcript-model.ts +428 -56
- package/cloud-os/composer/cloud-os-chat-input.tsx +228 -142
- package/cloud-os/composer/cloud-os-model-select.tsx +105 -0
- package/cloud-os/file-view.tsx +441 -0
- package/cloud-os/index.ts +35 -3
- package/cloud-os/layout/cloud-os-workspace-split.tsx +58 -15
- package/cloud-os/primitives/workshop-controls.tsx +2 -2
- package/cloud-os/styles/cloud-os.css +100 -20
- package/demo/camel-chat-showcase.tsx +21 -13
- package/demo/chat-scenarios.ts +223 -43
- package/demo/cloud-os-chat-showcase.tsx +47 -14
- package/demo/fixtures.ts +4 -5
- package/demo/message-panel-gallery.tsx +16 -2
- package/package.json +3 -4
- package/src/camel/camel-chat-messages.tsx +78 -78
- package/src/camel/camel-tool-presentation.tsx +19 -15
- package/src/camel/camel-turn.ts +1 -17
- package/src/composer/composer-attachments.tsx +1 -1
- package/src/composer/composer-trigger-popover.tsx +1 -1
- package/src/composer/composer.tsx +2 -2
- package/src/contracts.ts +0 -2
- package/src/message-panel.tsx +1 -24
- package/src/parts/index.tsx +103 -64
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
CaretRight,
|
|
6
6
|
Check,
|
|
7
7
|
Copy,
|
|
8
|
-
File as FileIcon,
|
|
9
8
|
Terminal,
|
|
10
9
|
} from "@phosphor-icons/react";
|
|
11
10
|
import {
|
|
@@ -21,6 +20,7 @@ import { Tooltip } from "../primitives/tooltip";
|
|
|
21
20
|
import { WorkshopIconButton } from "../primitives/workshop-controls";
|
|
22
21
|
import {
|
|
23
22
|
MarkdownMessage,
|
|
23
|
+
type CloudOsArtifactResolver,
|
|
24
24
|
type CloudOsUrlResolver,
|
|
25
25
|
} from "./markdown-message";
|
|
26
26
|
import {
|
|
@@ -34,15 +34,28 @@ import {
|
|
|
34
34
|
SuggestionsBlock,
|
|
35
35
|
type ApprovalDecision,
|
|
36
36
|
} from "./rich-blocks";
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
ToolGroupRow,
|
|
39
|
+
WorkDescriptionRow,
|
|
40
|
+
WorkTraceDisclosure,
|
|
41
|
+
} from "./tool-rows";
|
|
38
42
|
import { CloudOsActivityIndicator } from "./activity-indicator";
|
|
43
|
+
import { ProductGeneration } from "./image-generation";
|
|
44
|
+
import { GenerationLoader } from "./loading-state";
|
|
45
|
+
import { CapabilityChip } from "../capability-chip";
|
|
46
|
+
import {
|
|
47
|
+
renderFileView,
|
|
48
|
+
type FileRenderer,
|
|
49
|
+
} from "../file-view";
|
|
39
50
|
import {
|
|
40
51
|
buildCloudOsEntries,
|
|
41
52
|
deriveTurnActivity,
|
|
42
53
|
formatClockTime,
|
|
43
54
|
formatFullTimestamp,
|
|
55
|
+
partitionAssistantBlocks,
|
|
44
56
|
rhythmTopClass,
|
|
45
57
|
type CloudOsEntry,
|
|
58
|
+
type CloudOsActionPresentation,
|
|
46
59
|
} from "./transcript-model";
|
|
47
60
|
|
|
48
61
|
export type CloudOsChatStatus = "ready" | "submitted" | "streaming" | "error";
|
|
@@ -50,9 +63,11 @@ export type CloudOsChatStatus = "ready" | "submitted" | "streaming" | "error";
|
|
|
50
63
|
export interface CloudOsChatMessagesProps {
|
|
51
64
|
messages: readonly UIMessage[];
|
|
52
65
|
status: CloudOsChatStatus;
|
|
53
|
-
|
|
66
|
+
/** 权威 Turn 生命周期;提供时替代本地 status 推断。 */
|
|
67
|
+
turnActive?: boolean;
|
|
54
68
|
isRecovering?: boolean;
|
|
55
69
|
recoveryStatusLabel?: string;
|
|
70
|
+
awaitingApproval?: boolean;
|
|
56
71
|
/** 有 pending steer 时,活动占位说明「正在等当前步骤收尾」。 */
|
|
57
72
|
hasPendingSteer?: boolean;
|
|
58
73
|
/** 首帧就带着待发消息:此时不显示空会话欢迎语。 */
|
|
@@ -74,8 +89,14 @@ export interface CloudOsChatMessagesProps {
|
|
|
74
89
|
input: Record<string, unknown>;
|
|
75
90
|
disabled: boolean;
|
|
76
91
|
}) => ReactNode;
|
|
92
|
+
/** 接管消息里的文件视图;未提供时使用包内 FileView。 */
|
|
93
|
+
renderFile?: FileRenderer;
|
|
77
94
|
/** 把消息里的 `sandbox:/workspace/...` 之类地址翻译成可访问 URL。 */
|
|
78
95
|
resolveUrl?: CloudOsUrlResolver;
|
|
96
|
+
/** 把图片 URL 关联到宿主的持久产物。 */
|
|
97
|
+
resolveArtifactId?: CloudOsArtifactResolver;
|
|
98
|
+
/** 打开宿主拥有的产物视图。 */
|
|
99
|
+
onOpenArtifact?: (artifactId: string) => void;
|
|
79
100
|
/** 消息区宽度是否收敛到 920px 居中(GadgetEditor 里叫 constrainChatWidth)。 */
|
|
80
101
|
constrainWidth?: boolean;
|
|
81
102
|
emptyTitle?: string;
|
|
@@ -92,6 +113,18 @@ export interface CloudOsChatMessagesProps {
|
|
|
92
113
|
onCopy?: (text: string) => void;
|
|
93
114
|
}
|
|
94
115
|
|
|
116
|
+
export function collapseEntriesWithNewNarrative(
|
|
117
|
+
expanded: ReadonlySet<string>,
|
|
118
|
+
previousNarrativeKeys: ReadonlySet<string>,
|
|
119
|
+
narrativeKeys: ReadonlySet<string>,
|
|
120
|
+
): ReadonlySet<string> {
|
|
121
|
+
const next = new Set(expanded);
|
|
122
|
+
for (const key of narrativeKeys) {
|
|
123
|
+
if (!previousNarrativeKeys.has(key)) next.delete(key);
|
|
124
|
+
}
|
|
125
|
+
return next.size === expanded.size ? expanded : next;
|
|
126
|
+
}
|
|
127
|
+
|
|
95
128
|
function useCopyAction(): {
|
|
96
129
|
copied: string | null;
|
|
97
130
|
copy: (text: string) => void;
|
|
@@ -118,43 +151,63 @@ function UserBubble({
|
|
|
118
151
|
entry,
|
|
119
152
|
onCopy,
|
|
120
153
|
copied,
|
|
154
|
+
renderFile,
|
|
121
155
|
resolveUrl,
|
|
156
|
+
resolveArtifactId,
|
|
157
|
+
onOpenArtifact,
|
|
122
158
|
}: {
|
|
123
159
|
entry: Extract<CloudOsEntry, { type: "user" }>;
|
|
124
160
|
onCopy: (text: string) => void;
|
|
125
161
|
copied: boolean;
|
|
162
|
+
renderFile?: FileRenderer;
|
|
126
163
|
resolveUrl?: CloudOsUrlResolver;
|
|
164
|
+
resolveArtifactId?: CloudOsArtifactResolver;
|
|
165
|
+
onOpenArtifact?: (artifactId: string) => void;
|
|
127
166
|
}) {
|
|
128
167
|
return (
|
|
129
168
|
<div className="group/message relative flex flex-col items-end">
|
|
169
|
+
{entry.capabilities.length > 0 && (
|
|
170
|
+
<div className="mb-1.5 flex max-w-[min(680px,78%)] flex-wrap justify-end gap-1.5">
|
|
171
|
+
{entry.capabilities.map((capability) => (
|
|
172
|
+
<CapabilityChip
|
|
173
|
+
key={`${capability.kind}:${capability.name}`}
|
|
174
|
+
kind={capability.kind}
|
|
175
|
+
label={capability.label}
|
|
176
|
+
/>
|
|
177
|
+
))}
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
130
180
|
{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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
181
|
+
<div className="mb-1.5 flex max-w-[min(680px,78%)] flex-wrap items-start justify-end gap-2">
|
|
182
|
+
{entry.attachments.map((attachment, index) => {
|
|
183
|
+
const view = renderFileView(renderFile, {
|
|
184
|
+
file: {
|
|
185
|
+
...attachment,
|
|
186
|
+
url: resolveUrl?.(attachment.url) ?? attachment.url,
|
|
187
|
+
},
|
|
188
|
+
placement: "user-message",
|
|
189
|
+
});
|
|
190
|
+
return attachment.mediaType?.startsWith("image/") ? (
|
|
191
|
+
<Fragment key={`${attachment.url}:${index}`}>{view}</Fragment>
|
|
140
192
|
) : (
|
|
141
193
|
<span
|
|
142
194
|
key={`${attachment.url}:${index}`}
|
|
143
|
-
className="
|
|
195
|
+
className="flex basis-full justify-end"
|
|
144
196
|
>
|
|
145
|
-
|
|
146
|
-
{attachment.filename ?? "Attachment"}
|
|
197
|
+
{view}
|
|
147
198
|
</span>
|
|
148
|
-
)
|
|
149
|
-
)}
|
|
199
|
+
);
|
|
200
|
+
})}
|
|
150
201
|
</div>
|
|
151
202
|
)}
|
|
152
203
|
{entry.text && (
|
|
153
|
-
<div className="themed-user-bubble-shadow cos-markdown w-fit max-w-[min(680px,78%)] rounded-[
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
204
|
+
<div className="themed-user-bubble-shadow cos-markdown cos-user-markdown w-fit max-w-[min(680px,78%)] rounded-[12px] border border-transparent bg-kumo-bubble-user px-4 py-2.5 text-[14px] leading-[22px] tracking-[-0.25px] text-kumo-default">
|
|
205
|
+
<MarkdownMessage
|
|
206
|
+
message={entry.text}
|
|
207
|
+
resolveUrl={resolveUrl}
|
|
208
|
+
resolveArtifactId={resolveArtifactId}
|
|
209
|
+
onOpenArtifact={onOpenArtifact}
|
|
210
|
+
/>
|
|
158
211
|
</div>
|
|
159
212
|
)}
|
|
160
213
|
<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">
|
|
@@ -181,12 +234,111 @@ function UserBubble({
|
|
|
181
234
|
);
|
|
182
235
|
}
|
|
183
236
|
|
|
237
|
+
function ActionPresentationBlock({
|
|
238
|
+
presentation,
|
|
239
|
+
failureCount = 1,
|
|
240
|
+
partial = false,
|
|
241
|
+
renderFile,
|
|
242
|
+
resolveUrl,
|
|
243
|
+
resolveArtifactId,
|
|
244
|
+
onOpenArtifact,
|
|
245
|
+
}: {
|
|
246
|
+
presentation: CloudOsActionPresentation;
|
|
247
|
+
failureCount?: number;
|
|
248
|
+
partial?: boolean;
|
|
249
|
+
renderFile?: FileRenderer;
|
|
250
|
+
resolveUrl?: CloudOsUrlResolver;
|
|
251
|
+
resolveArtifactId?: CloudOsArtifactResolver;
|
|
252
|
+
onOpenArtifact?: (artifactId: string) => void;
|
|
253
|
+
}) {
|
|
254
|
+
const { state } = presentation;
|
|
255
|
+
if (state === "ready") {
|
|
256
|
+
return (
|
|
257
|
+
<div
|
|
258
|
+
data-action-presentation={state}
|
|
259
|
+
role="status"
|
|
260
|
+
aria-label="Completed"
|
|
261
|
+
className="space-y-2"
|
|
262
|
+
>
|
|
263
|
+
{presentation.content.length === 0
|
|
264
|
+
? <p className="m-0 text-[14px] leading-5 text-kumo-subtle">Completed</p>
|
|
265
|
+
: presentation.content.map((file, index) => {
|
|
266
|
+
const url = resolveUrl?.(file.url) ?? file.url;
|
|
267
|
+
const artifactId = resolveArtifactId?.(url);
|
|
268
|
+
return (
|
|
269
|
+
<Fragment key={`${file.url}:${index}`}>
|
|
270
|
+
{renderFileView(renderFile, {
|
|
271
|
+
file: {
|
|
272
|
+
...file,
|
|
273
|
+
filename: "Generated file",
|
|
274
|
+
url,
|
|
275
|
+
},
|
|
276
|
+
placement: "assistant-message",
|
|
277
|
+
variant: "product",
|
|
278
|
+
onOpen: file.mediaType?.startsWith("image/") && artifactId && onOpenArtifact
|
|
279
|
+
? () => onOpenArtifact(artifactId)
|
|
280
|
+
: undefined,
|
|
281
|
+
})}
|
|
282
|
+
</Fragment>
|
|
283
|
+
);
|
|
284
|
+
})}
|
|
285
|
+
</div>
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const product = presentation.produces;
|
|
290
|
+
const productName = product?.type ?? "other";
|
|
291
|
+
const isFileProduct = productName !== "data" && productName !== "other";
|
|
292
|
+
const insufficientCredits = state === "failed" &&
|
|
293
|
+
presentation.failureCode === "insufficient_credits";
|
|
294
|
+
const body = state === "running"
|
|
295
|
+
? isFileProduct
|
|
296
|
+
? `Generating ${productName}${product?.mediaType ? ` (${product.mediaType})` : ""}`
|
|
297
|
+
: "Generating"
|
|
298
|
+
: state === "outcome_unknown"
|
|
299
|
+
? "The provider may have completed this action. Check the status before trying again."
|
|
300
|
+
: insufficientCredits
|
|
301
|
+
? `${failureCount} API call${failureCount === 1 ? " was" : "s were"} not started because there weren't enough Credits.`
|
|
302
|
+
: undefined;
|
|
303
|
+
if (state === "running") {
|
|
304
|
+
return (
|
|
305
|
+
<div
|
|
306
|
+
data-action-presentation={state}
|
|
307
|
+
data-product-type={productName}
|
|
308
|
+
role="status"
|
|
309
|
+
aria-label={body}
|
|
310
|
+
>
|
|
311
|
+
{isFileProduct
|
|
312
|
+
? <ProductGeneration type={productName} mediaType={product?.mediaType} />
|
|
313
|
+
: <GenerationLoader />}
|
|
314
|
+
</div>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
return (
|
|
318
|
+
<div
|
|
319
|
+
data-action-presentation={state}
|
|
320
|
+
role={state === "failed" ? "alert" : "status"}
|
|
321
|
+
className="inline-flex max-w-72 items-start rounded-xl border border-kumo-line bg-kumo-elevated px-3 py-2 text-[13px] leading-5"
|
|
322
|
+
>
|
|
323
|
+
<span>
|
|
324
|
+
<strong className="block font-medium text-kumo-default">
|
|
325
|
+
{insufficientCredits
|
|
326
|
+
? partial ? "Partially completed" : "Not completed"
|
|
327
|
+
: state === "failed" ? "Action failed" : "Status unavailable"}
|
|
328
|
+
</strong>
|
|
329
|
+
{body && <span className="block text-kumo-subtle">{body}</span>}
|
|
330
|
+
</span>
|
|
331
|
+
</div>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
184
335
|
export function CloudOsChatMessages({
|
|
185
336
|
messages,
|
|
186
337
|
status,
|
|
187
|
-
|
|
338
|
+
turnActive,
|
|
188
339
|
isRecovering = false,
|
|
189
340
|
recoveryStatusLabel,
|
|
341
|
+
awaitingApproval = false,
|
|
190
342
|
hasPendingSteer = false,
|
|
191
343
|
startedWithPending = false,
|
|
192
344
|
error,
|
|
@@ -194,7 +346,9 @@ export function CloudOsChatMessages({
|
|
|
194
346
|
approvalIdOf,
|
|
195
347
|
approvalsDisabled = false,
|
|
196
348
|
renderApproval,
|
|
349
|
+
renderFile,
|
|
197
350
|
resolveUrl,
|
|
351
|
+
resolveArtifactId,
|
|
198
352
|
constrainWidth = true,
|
|
199
353
|
emptyTitle = "What are we working on?",
|
|
200
354
|
onRetry,
|
|
@@ -202,6 +356,7 @@ export function CloudOsChatMessages({
|
|
|
202
356
|
onSuggestion,
|
|
203
357
|
onRespond,
|
|
204
358
|
onOpenSchedule,
|
|
359
|
+
onOpenArtifact,
|
|
205
360
|
onCopy,
|
|
206
361
|
}: CloudOsChatMessagesProps) {
|
|
207
362
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
@@ -210,11 +365,15 @@ export function CloudOsChatMessages({
|
|
|
210
365
|
const [expandedToolCalls, setExpandedToolCalls] = useState<ReadonlySet<string>>(
|
|
211
366
|
() => new Set(),
|
|
212
367
|
);
|
|
368
|
+
const [expandedWorkEntries, setExpandedWorkEntries] = useState<
|
|
369
|
+
ReadonlySet<string>
|
|
370
|
+
>(() => new Set());
|
|
213
371
|
const [expandedCompactions, setExpandedCompactions] = useState<
|
|
214
372
|
ReadonlySet<string>
|
|
215
373
|
>(() => new Set());
|
|
216
374
|
const { copied, copy } = useCopyAction();
|
|
217
|
-
const isActive =
|
|
375
|
+
const isActive =
|
|
376
|
+
turnActive ?? (status === "submitted" || status === "streaming");
|
|
218
377
|
|
|
219
378
|
const entries = useMemo(
|
|
220
379
|
() =>
|
|
@@ -226,6 +385,31 @@ export function CloudOsChatMessages({
|
|
|
226
385
|
}),
|
|
227
386
|
[approvalIdOf, isActive, messages, showThinkingTraces],
|
|
228
387
|
);
|
|
388
|
+
const narrativeEntryKeys = useMemo(
|
|
389
|
+
() =>
|
|
390
|
+
new Set(
|
|
391
|
+
entries.flatMap((entry) =>
|
|
392
|
+
entry.type === "assistant" &&
|
|
393
|
+
partitionAssistantBlocks(entry.blocks).content.some(
|
|
394
|
+
(block) => block.kind === "text",
|
|
395
|
+
)
|
|
396
|
+
? [entry.key]
|
|
397
|
+
: [],
|
|
398
|
+
),
|
|
399
|
+
),
|
|
400
|
+
[entries],
|
|
401
|
+
);
|
|
402
|
+
const previousNarrativeEntryKeys = useRef<ReadonlySet<string>>(
|
|
403
|
+
narrativeEntryKeys,
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
useEffect(() => {
|
|
407
|
+
const previous = previousNarrativeEntryKeys.current;
|
|
408
|
+
previousNarrativeEntryKeys.current = narrativeEntryKeys;
|
|
409
|
+
setExpandedWorkEntries((expanded) =>
|
|
410
|
+
collapseEntriesWithNewNarrative(expanded, previous, narrativeEntryKeys),
|
|
411
|
+
);
|
|
412
|
+
}, [narrativeEntryKeys]);
|
|
229
413
|
|
|
230
414
|
const entryTopClasses = useMemo(
|
|
231
415
|
() =>
|
|
@@ -244,6 +428,15 @@ export function CloudOsChatMessages({
|
|
|
244
428
|
});
|
|
245
429
|
}, []);
|
|
246
430
|
|
|
431
|
+
const toggleWorkEntry = useCallback((key: string) => {
|
|
432
|
+
setExpandedWorkEntries((current) => {
|
|
433
|
+
const next = new Set(current);
|
|
434
|
+
if (next.has(key)) next.delete(key);
|
|
435
|
+
else next.add(key);
|
|
436
|
+
return next;
|
|
437
|
+
});
|
|
438
|
+
}, []);
|
|
439
|
+
|
|
247
440
|
const toggleCompaction = useCallback((key: string) => {
|
|
248
441
|
setExpandedCompactions((current) => {
|
|
249
442
|
const next = new Set(current);
|
|
@@ -267,10 +460,13 @@ export function CloudOsChatMessages({
|
|
|
267
460
|
element.scrollTo({ top: element.scrollHeight });
|
|
268
461
|
}, [isRecovering, messages, status, showScrollButton]);
|
|
269
462
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
463
|
+
const activity = deriveTurnActivity(entries, {
|
|
464
|
+
isActive,
|
|
465
|
+
isRecovering,
|
|
466
|
+
recoveryStatusLabel,
|
|
467
|
+
awaitingApproval,
|
|
468
|
+
hasPendingSteer,
|
|
469
|
+
});
|
|
274
470
|
const copyText = (text: string) => {
|
|
275
471
|
copy(text);
|
|
276
472
|
onCopy?.(text);
|
|
@@ -281,7 +477,7 @@ export function CloudOsChatMessages({
|
|
|
281
477
|
className="relative flex min-h-0 flex-1 flex-col overflow-hidden bg-kumo-base"
|
|
282
478
|
data-cloud-os-transcript=""
|
|
283
479
|
data-status={status}
|
|
284
|
-
aria-busy={isActive ||
|
|
480
|
+
aria-busy={isActive || isRecovering}
|
|
285
481
|
>
|
|
286
482
|
<div
|
|
287
483
|
ref={scrollRef}
|
|
@@ -296,7 +492,7 @@ export function CloudOsChatMessages({
|
|
|
296
492
|
constrainWidth ? "mx-auto w-full max-w-[920px]" : ""
|
|
297
493
|
}`}
|
|
298
494
|
>
|
|
299
|
-
{entries.length === 0 && !startedWithPending &&
|
|
495
|
+
{entries.length === 0 && !startedWithPending && (
|
|
300
496
|
<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
497
|
{emptyTitle}
|
|
302
498
|
</div>
|
|
@@ -312,7 +508,10 @@ export function CloudOsChatMessages({
|
|
|
312
508
|
entry={entry}
|
|
313
509
|
onCopy={copyText}
|
|
314
510
|
copied={copied === entry.text}
|
|
511
|
+
renderFile={renderFile}
|
|
315
512
|
resolveUrl={resolveUrl}
|
|
513
|
+
resolveArtifactId={resolveArtifactId}
|
|
514
|
+
onOpenArtifact={onOpenArtifact}
|
|
316
515
|
/>
|
|
317
516
|
</div>
|
|
318
517
|
);
|
|
@@ -321,7 +520,7 @@ export function CloudOsChatMessages({
|
|
|
321
520
|
if (entry.type === "slashCommand") {
|
|
322
521
|
return (
|
|
323
522
|
<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-[
|
|
523
|
+
<div className="themed-user-bubble-shadow w-fit max-w-[min(680px,78%)] rounded-[12px] border border-transparent bg-kumo-bubble-user px-4 py-2.5 text-[14px] leading-[22px] tracking-[-0.25px] text-kumo-brand">
|
|
325
524
|
<span className="whitespace-pre-wrap font-mono">{entry.text}</span>
|
|
326
525
|
</div>
|
|
327
526
|
</div>
|
|
@@ -387,10 +586,12 @@ export function CloudOsChatMessages({
|
|
|
387
586
|
<span className="h-px flex-1 bg-kumo-line" aria-hidden="true" />
|
|
388
587
|
</div>
|
|
389
588
|
{expanded && (
|
|
390
|
-
<div className="
|
|
589
|
+
<div className="cos-markdown mt-3 rounded-2xl border border-kumo-line/70 bg-kumo-elevated/45 p-3.5 text-[13px] leading-[19px]">
|
|
391
590
|
<MarkdownMessage
|
|
392
591
|
message={entry.summary}
|
|
393
592
|
resolveUrl={resolveUrl}
|
|
593
|
+
resolveArtifactId={resolveArtifactId}
|
|
594
|
+
onOpenArtifact={onOpenArtifact}
|
|
394
595
|
/>
|
|
395
596
|
</div>
|
|
396
597
|
)}
|
|
@@ -398,23 +599,107 @@ export function CloudOsChatMessages({
|
|
|
398
599
|
);
|
|
399
600
|
}
|
|
400
601
|
|
|
401
|
-
const
|
|
602
|
+
const { work: workBlocks, content: contentBlocks } =
|
|
603
|
+
partitionAssistantBlocks(entry.blocks);
|
|
604
|
+
const workGroups = workBlocks.flatMap((block) =>
|
|
605
|
+
block.kind === "toolGroup" ? [block.group] : [],
|
|
606
|
+
);
|
|
607
|
+
const hasNarrative = contentBlocks.some(
|
|
402
608
|
(block) => block.kind === "text",
|
|
403
609
|
);
|
|
610
|
+
const insufficientFailures = contentBlocks.filter(
|
|
611
|
+
(block) => block.kind === "actionPresentation" &&
|
|
612
|
+
block.presentation.state === "failed" &&
|
|
613
|
+
block.presentation.failureCode === "insufficient_credits",
|
|
614
|
+
);
|
|
615
|
+
const firstInsufficientFailure = insufficientFailures[0];
|
|
616
|
+
const hasCompletedAction = contentBlocks.some(
|
|
617
|
+
(block) => block.kind === "actionPresentation" &&
|
|
618
|
+
block.presentation.state === "ready",
|
|
619
|
+
);
|
|
404
620
|
|
|
405
621
|
return (
|
|
406
622
|
<div
|
|
407
623
|
key={entry.key}
|
|
408
|
-
className={`${topClass} min-w-0 w-full max-w-[860px]
|
|
624
|
+
className={`${topClass} min-w-0 w-full max-w-[860px]`}
|
|
409
625
|
>
|
|
410
|
-
<div className="group/agentMessage relative space-y-
|
|
411
|
-
{
|
|
626
|
+
<div className="group/agentMessage relative space-y-4">
|
|
627
|
+
{workBlocks.length > 0 && (
|
|
628
|
+
<WorkTraceDisclosure
|
|
629
|
+
durationMs={entry.durationMs}
|
|
630
|
+
groups={workGroups}
|
|
631
|
+
completed={!isActive}
|
|
632
|
+
open={expandedWorkEntries.has(entry.key)}
|
|
633
|
+
onToggle={() => toggleWorkEntry(entry.key)}
|
|
634
|
+
>
|
|
635
|
+
{workBlocks.map((block) => {
|
|
636
|
+
switch (block.kind) {
|
|
637
|
+
case "reasoning":
|
|
638
|
+
case "text":
|
|
639
|
+
return (
|
|
640
|
+
<WorkDescriptionRow
|
|
641
|
+
key={block.key}
|
|
642
|
+
text={block.text}
|
|
643
|
+
resolveUrl={resolveUrl}
|
|
644
|
+
/>
|
|
645
|
+
);
|
|
646
|
+
case "toolGroup":
|
|
647
|
+
return (
|
|
648
|
+
<ToolGroupRow
|
|
649
|
+
key={block.key}
|
|
650
|
+
group={block.group}
|
|
651
|
+
open={expandedToolCalls.has(block.group.key)}
|
|
652
|
+
expandedKeys={expandedToolCalls}
|
|
653
|
+
onToggle={toggleToolCall}
|
|
654
|
+
/>
|
|
655
|
+
);
|
|
656
|
+
case "plan":
|
|
657
|
+
return (
|
|
658
|
+
<PlanBlock
|
|
659
|
+
key={block.key}
|
|
660
|
+
steps={block.steps}
|
|
661
|
+
running={block.running}
|
|
662
|
+
defaultOpen={false}
|
|
663
|
+
showCollapsedSummary={false}
|
|
664
|
+
/>
|
|
665
|
+
);
|
|
666
|
+
case "parallel":
|
|
667
|
+
return (
|
|
668
|
+
<ParallelBlock
|
|
669
|
+
key={block.key}
|
|
670
|
+
label={block.label}
|
|
671
|
+
tools={block.tools}
|
|
672
|
+
/>
|
|
673
|
+
);
|
|
674
|
+
case "subagents":
|
|
675
|
+
return (
|
|
676
|
+
<SubAgentsBlock
|
|
677
|
+
key={block.key}
|
|
678
|
+
agents={block.agents}
|
|
679
|
+
/>
|
|
680
|
+
);
|
|
681
|
+
default:
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
})}
|
|
685
|
+
</WorkTraceDisclosure>
|
|
686
|
+
)}
|
|
687
|
+
|
|
688
|
+
{contentBlocks.map((block) => {
|
|
412
689
|
switch (block.kind) {
|
|
413
690
|
case "reasoning":
|
|
414
691
|
return (
|
|
415
|
-
<
|
|
692
|
+
<WorkDescriptionRow
|
|
693
|
+
key={block.key}
|
|
694
|
+
text={block.text}
|
|
695
|
+
resolveUrl={resolveUrl}
|
|
696
|
+
/>
|
|
416
697
|
);
|
|
417
|
-
case "text":
|
|
698
|
+
case "text": {
|
|
699
|
+
const streaming =
|
|
700
|
+
isActive &&
|
|
701
|
+
entryIndex === entries.length - 1 &&
|
|
702
|
+
block === entry.blocks.at(-1);
|
|
418
703
|
return (
|
|
419
704
|
<div
|
|
420
705
|
key={block.key}
|
|
@@ -423,9 +708,13 @@ export function CloudOsChatMessages({
|
|
|
423
708
|
<MarkdownMessage
|
|
424
709
|
message={block.text}
|
|
425
710
|
resolveUrl={resolveUrl}
|
|
711
|
+
resolveArtifactId={resolveArtifactId}
|
|
712
|
+
onOpenArtifact={onOpenArtifact}
|
|
713
|
+
streaming={streaming}
|
|
426
714
|
/>
|
|
427
715
|
</div>
|
|
428
716
|
);
|
|
717
|
+
}
|
|
429
718
|
case "toolGroup":
|
|
430
719
|
return (
|
|
431
720
|
<ToolGroupRow
|
|
@@ -503,14 +792,54 @@ export function CloudOsChatMessages({
|
|
|
503
792
|
<SubAgentsBlock key={block.key} agents={block.agents} />
|
|
504
793
|
);
|
|
505
794
|
case "image":
|
|
795
|
+
{
|
|
796
|
+
const url = resolveUrl?.(block.src) ?? block.src;
|
|
797
|
+
const artifactId = resolveArtifactId?.(url);
|
|
798
|
+
return (
|
|
799
|
+
<Fragment key={block.key}>
|
|
800
|
+
{renderFileView(renderFile, {
|
|
801
|
+
file: {
|
|
802
|
+
url,
|
|
803
|
+
filename: block.alt,
|
|
804
|
+
mediaType: block.mediaType ?? "image/*",
|
|
805
|
+
},
|
|
806
|
+
placement: "assistant-message",
|
|
807
|
+
onOpen: artifactId && onOpenArtifact
|
|
808
|
+
? () => onOpenArtifact(artifactId)
|
|
809
|
+
: undefined,
|
|
810
|
+
})}
|
|
811
|
+
</Fragment>
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
case "file":
|
|
506
815
|
return (
|
|
507
|
-
<
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
816
|
+
<Fragment key={block.key}>
|
|
817
|
+
{renderFileView(renderFile, {
|
|
818
|
+
file: {
|
|
819
|
+
...block.file,
|
|
820
|
+
url:
|
|
821
|
+
resolveUrl?.(block.file.url) ?? block.file.url,
|
|
822
|
+
},
|
|
823
|
+
placement: "assistant-message",
|
|
824
|
+
})}
|
|
825
|
+
</Fragment>
|
|
826
|
+
);
|
|
827
|
+
case "actionPresentation":
|
|
828
|
+
if (
|
|
829
|
+
block.presentation.failureCode === "insufficient_credits" &&
|
|
830
|
+
block !== firstInsufficientFailure
|
|
831
|
+
) return null;
|
|
832
|
+
return (
|
|
833
|
+
<ActionPresentationBlock
|
|
834
|
+
key={block.key}
|
|
835
|
+
presentation={block.presentation}
|
|
836
|
+
failureCount={insufficientFailures.length}
|
|
837
|
+
partial={hasCompletedAction}
|
|
838
|
+
renderFile={renderFile}
|
|
839
|
+
resolveUrl={resolveUrl}
|
|
840
|
+
resolveArtifactId={resolveArtifactId}
|
|
841
|
+
onOpenArtifact={onOpenArtifact}
|
|
842
|
+
/>
|
|
514
843
|
);
|
|
515
844
|
case "error":
|
|
516
845
|
return (
|
|
@@ -571,14 +900,6 @@ export function CloudOsChatMessages({
|
|
|
571
900
|
);
|
|
572
901
|
})}
|
|
573
902
|
|
|
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
903
|
{activity && (
|
|
583
904
|
<div className="mt-5 inline-flex px-1.5 py-1 text-[14px] leading-5 tracking-[-0.25px]">
|
|
584
905
|
<CloudOsActivityIndicator {...activity} />
|
|
@@ -617,16 +938,6 @@ export function CloudOsChatMessages({
|
|
|
617
938
|
<ArrowDown size={15} />
|
|
618
939
|
</WorkshopIconButton>
|
|
619
940
|
|
|
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
941
|
</div>
|
|
631
942
|
);
|
|
632
943
|
}
|