@springbrand/message-panel 0.1.3-alpha.8 → 0.2.0-alpha.37
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 +4 -3
- 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-star.svg +9 -0
- package/cloud-os/capability-chip.tsx +1 -1
- package/cloud-os/chat/cloud-os-chat-messages.tsx +441 -98
- package/cloud-os/chat/code-runner.tsx +106 -0
- 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/range.ts +8 -0
- package/cloud-os/chat/rich-blocks.tsx +285 -227
- package/cloud-os/chat/surfaces.tsx +90 -0
- package/cloud-os/chat/tool-call.tsx +94 -0
- package/cloud-os/chat/tool-presentation.ts +31 -22
- package/cloud-os/chat/tool-rows.tsx +195 -143
- package/cloud-os/chat/tool-timeline.tsx +123 -0
- package/cloud-os/chat/transcript-model.ts +258 -20
- package/cloud-os/chat/web-search.tsx +135 -0
- package/cloud-os/composer/cloud-os-chat-input.tsx +38 -88
- package/cloud-os/composer/cloud-os-model-select.tsx +105 -0
- package/cloud-os/file-view.tsx +195 -12
- package/cloud-os/index.ts +24 -3
- package/cloud-os/layout/cloud-os-workspace-split.tsx +60 -3
- package/cloud-os/primitives/collapsible.tsx +21 -0
- package/cloud-os/primitives/workshop-controls.tsx +2 -2
- package/cloud-os/styles/cloud-os.css +132 -1
- package/demo/camel-chat-showcase.tsx +21 -13
- package/demo/chat-scenarios.ts +214 -40
- package/demo/cloud-os-chat-showcase.tsx +37 -14
- package/demo/fixtures.ts +4 -5
- package/demo/message-panel-gallery.tsx +16 -2
- package/package.json +8 -4
- package/springbrand-pet/LICENSE.bloub +21 -0
- package/springbrand-pet/bot/cycles.test.ts +221 -0
- package/springbrand-pet/bot/cycles.ts +225 -0
- package/springbrand-pet/bot/decor.ts +285 -0
- package/springbrand-pet/bot/engine.test.ts +543 -0
- package/springbrand-pet/bot/engine.ts +558 -0
- package/springbrand-pet/bot/expressions.test.ts +135 -0
- package/springbrand-pet/bot/expressions.ts +192 -0
- package/springbrand-pet/bot/eyefit.ts +455 -0
- package/springbrand-pet/bot/face.test.ts +101 -0
- package/springbrand-pet/bot/face.ts +179 -0
- package/springbrand-pet/bot/math.ts +42 -0
- package/springbrand-pet/bot/profiles.ts +22 -0
- package/springbrand-pet/bot/repere.ts +31 -0
- package/springbrand-pet/bot/shape.test.ts +97 -0
- package/springbrand-pet/bot/shape.ts +310 -0
- package/springbrand-pet/bot/skins.test.ts +325 -0
- package/springbrand-pet/bot/skins.ts +161 -0
- package/springbrand-pet/bot/states.ts +616 -0
- package/springbrand-pet/drag.test.ts +19 -0
- package/springbrand-pet/index.tsx +569 -0
- package/springbrand-pet/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/springbrand-pet/vitest.config.ts +9 -0
- 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.tsx +2 -2
- package/src/contracts.ts +0 -2
- package/src/message-panel.tsx +1 -24
- package/src/parts/index.tsx +103 -64
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { UIMessage } from "ai";
|
|
2
|
+
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
|
2
3
|
import {
|
|
3
4
|
ArrowDown,
|
|
4
5
|
Brain,
|
|
@@ -17,9 +18,15 @@ import {
|
|
|
17
18
|
type ReactNode,
|
|
18
19
|
} from "react";
|
|
19
20
|
import { Tooltip } from "../primitives/tooltip";
|
|
21
|
+
import {
|
|
22
|
+
Collapsible,
|
|
23
|
+
CollapsibleContent,
|
|
24
|
+
CollapsibleTrigger,
|
|
25
|
+
} from "../primitives/collapsible";
|
|
20
26
|
import { WorkshopIconButton } from "../primitives/workshop-controls";
|
|
21
27
|
import {
|
|
22
28
|
MarkdownMessage,
|
|
29
|
+
type CloudOsArtifactResolver,
|
|
23
30
|
type CloudOsUrlResolver,
|
|
24
31
|
} from "./markdown-message";
|
|
25
32
|
import {
|
|
@@ -34,7 +41,10 @@ import {
|
|
|
34
41
|
type ApprovalDecision,
|
|
35
42
|
} from "./rich-blocks";
|
|
36
43
|
import { ThinkingTraceRow, ToolGroupRow } from "./tool-rows";
|
|
44
|
+
import { collapsePanel } from "./surfaces";
|
|
37
45
|
import { CloudOsActivityIndicator } from "./activity-indicator";
|
|
46
|
+
import { ProductGeneration } from "./image-generation";
|
|
47
|
+
import { GenerationLoader } from "./loading-state";
|
|
38
48
|
import { CapabilityChip } from "../capability-chip";
|
|
39
49
|
import {
|
|
40
50
|
renderFileView,
|
|
@@ -46,7 +56,9 @@ import {
|
|
|
46
56
|
formatClockTime,
|
|
47
57
|
formatFullTimestamp,
|
|
48
58
|
rhythmTopClass,
|
|
59
|
+
type AssistantBlock,
|
|
49
60
|
type CloudOsEntry,
|
|
61
|
+
type CloudOsActionPresentation,
|
|
50
62
|
} from "./transcript-model";
|
|
51
63
|
|
|
52
64
|
export type CloudOsChatStatus = "ready" | "submitted" | "streaming" | "error";
|
|
@@ -56,8 +68,9 @@ export interface CloudOsChatMessagesProps {
|
|
|
56
68
|
status: CloudOsChatStatus;
|
|
57
69
|
/** 权威 Turn 生命周期;提供时替代本地 status 推断。 */
|
|
58
70
|
turnActive?: boolean;
|
|
59
|
-
isHydrating?: boolean;
|
|
60
71
|
isRecovering?: boolean;
|
|
72
|
+
recoveryAttempt?: number;
|
|
73
|
+
recoveryMax?: number;
|
|
61
74
|
recoveryStatusLabel?: string;
|
|
62
75
|
awaitingApproval?: boolean;
|
|
63
76
|
/** 有 pending steer 时,活动占位说明「正在等当前步骤收尾」。 */
|
|
@@ -85,6 +98,10 @@ export interface CloudOsChatMessagesProps {
|
|
|
85
98
|
renderFile?: FileRenderer;
|
|
86
99
|
/** 把消息里的 `sandbox:/workspace/...` 之类地址翻译成可访问 URL。 */
|
|
87
100
|
resolveUrl?: CloudOsUrlResolver;
|
|
101
|
+
/** 把图片 URL 关联到宿主的持久产物。 */
|
|
102
|
+
resolveArtifactId?: CloudOsArtifactResolver;
|
|
103
|
+
/** 打开宿主拥有的产物视图。 */
|
|
104
|
+
onOpenArtifact?: (artifactId: string) => void;
|
|
88
105
|
/** 消息区宽度是否收敛到 920px 居中(GadgetEditor 里叫 constrainChatWidth)。 */
|
|
89
106
|
constrainWidth?: boolean;
|
|
90
107
|
emptyTitle?: string;
|
|
@@ -129,12 +146,16 @@ function UserBubble({
|
|
|
129
146
|
copied,
|
|
130
147
|
renderFile,
|
|
131
148
|
resolveUrl,
|
|
149
|
+
resolveArtifactId,
|
|
150
|
+
onOpenArtifact,
|
|
132
151
|
}: {
|
|
133
152
|
entry: Extract<CloudOsEntry, { type: "user" }>;
|
|
134
153
|
onCopy: (text: string) => void;
|
|
135
154
|
copied: boolean;
|
|
136
155
|
renderFile?: FileRenderer;
|
|
137
156
|
resolveUrl?: CloudOsUrlResolver;
|
|
157
|
+
resolveArtifactId?: CloudOsArtifactResolver;
|
|
158
|
+
onOpenArtifact?: (artifactId: string) => void;
|
|
138
159
|
}) {
|
|
139
160
|
return (
|
|
140
161
|
<div className="group/message relative flex flex-col items-end">
|
|
@@ -150,23 +171,39 @@ function UserBubble({
|
|
|
150
171
|
</div>
|
|
151
172
|
)}
|
|
152
173
|
{entry.attachments.length > 0 && (
|
|
153
|
-
<div className="mb-1.5 flex max-w-[min(680px,78%)] flex-wrap justify-end gap-2">
|
|
154
|
-
{entry.attachments.map((attachment, index) =>
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
174
|
+
<div className="mb-1.5 flex max-w-[min(680px,78%)] flex-wrap items-start justify-end gap-2">
|
|
175
|
+
{entry.attachments.map((attachment, index) => {
|
|
176
|
+
const view = renderFileView(renderFile, {
|
|
177
|
+
file: {
|
|
178
|
+
...attachment,
|
|
179
|
+
url: resolveUrl?.(attachment.url) ?? attachment.url,
|
|
180
|
+
},
|
|
181
|
+
placement: "user-message",
|
|
182
|
+
});
|
|
183
|
+
return attachment.mediaType?.startsWith("image/") ? (
|
|
184
|
+
<Fragment key={`${attachment.url}:${index}`}>{view}</Fragment>
|
|
185
|
+
) : (
|
|
186
|
+
<span
|
|
187
|
+
key={`${attachment.url}:${index}`}
|
|
188
|
+
className="flex basis-full justify-end"
|
|
189
|
+
>
|
|
190
|
+
{view}
|
|
191
|
+
</span>
|
|
192
|
+
);
|
|
193
|
+
})}
|
|
162
194
|
</div>
|
|
163
195
|
)}
|
|
164
196
|
{entry.text && (
|
|
165
|
-
<div
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
197
|
+
<div
|
|
198
|
+
data-user-message-bubble=""
|
|
199
|
+
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 animate-in fade-in slide-in-from-bottom-1 duration-300 motion-reduce:animate-none"
|
|
200
|
+
>
|
|
201
|
+
<MarkdownMessage
|
|
202
|
+
message={entry.text}
|
|
203
|
+
resolveUrl={resolveUrl}
|
|
204
|
+
resolveArtifactId={resolveArtifactId}
|
|
205
|
+
onOpenArtifact={onOpenArtifact}
|
|
206
|
+
/>
|
|
170
207
|
</div>
|
|
171
208
|
)}
|
|
172
209
|
<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">
|
|
@@ -193,12 +230,147 @@ function UserBubble({
|
|
|
193
230
|
);
|
|
194
231
|
}
|
|
195
232
|
|
|
233
|
+
function ActionPresentationBlock({
|
|
234
|
+
presentation,
|
|
235
|
+
failureCount = 1,
|
|
236
|
+
partial = false,
|
|
237
|
+
renderFile,
|
|
238
|
+
resolveUrl,
|
|
239
|
+
resolveArtifactId,
|
|
240
|
+
onOpenArtifact,
|
|
241
|
+
}: {
|
|
242
|
+
presentation: CloudOsActionPresentation;
|
|
243
|
+
failureCount?: number;
|
|
244
|
+
partial?: boolean;
|
|
245
|
+
renderFile?: FileRenderer;
|
|
246
|
+
resolveUrl?: CloudOsUrlResolver;
|
|
247
|
+
resolveArtifactId?: CloudOsArtifactResolver;
|
|
248
|
+
onOpenArtifact?: (artifactId: string) => void;
|
|
249
|
+
}) {
|
|
250
|
+
const { state } = presentation;
|
|
251
|
+
if (state === "ready") {
|
|
252
|
+
return (
|
|
253
|
+
<div
|
|
254
|
+
data-action-presentation={state}
|
|
255
|
+
role="status"
|
|
256
|
+
aria-label="Completed"
|
|
257
|
+
className="space-y-2"
|
|
258
|
+
>
|
|
259
|
+
{presentation.content.length === 0
|
|
260
|
+
? <p className="m-0 text-[14px] leading-5 text-kumo-subtle">Completed</p>
|
|
261
|
+
: presentation.content.map((file, index) => {
|
|
262
|
+
const url = resolveUrl?.(file.url) ?? file.url;
|
|
263
|
+
const artifactId = resolveArtifactId?.(url);
|
|
264
|
+
return (
|
|
265
|
+
<Fragment key={`${file.url}:${index}`}>
|
|
266
|
+
{renderFileView(renderFile, {
|
|
267
|
+
file: {
|
|
268
|
+
...file,
|
|
269
|
+
filename: "Generated file",
|
|
270
|
+
url,
|
|
271
|
+
},
|
|
272
|
+
placement: "assistant-message",
|
|
273
|
+
variant: "product",
|
|
274
|
+
onOpen: file.mediaType?.startsWith("image/") && artifactId && onOpenArtifact
|
|
275
|
+
? () => onOpenArtifact(artifactId)
|
|
276
|
+
: undefined,
|
|
277
|
+
})}
|
|
278
|
+
</Fragment>
|
|
279
|
+
);
|
|
280
|
+
})}
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const product = presentation.produces;
|
|
286
|
+
const productName = product?.type ?? "other";
|
|
287
|
+
const isFileProduct = productName !== "data" && productName !== "other";
|
|
288
|
+
const insufficientCredits = state === "failed" &&
|
|
289
|
+
presentation.failureCode === "insufficient_credits";
|
|
290
|
+
const body = state === "running"
|
|
291
|
+
? isFileProduct
|
|
292
|
+
? `Generating ${productName}${product?.mediaType ? ` (${product.mediaType})` : ""}`
|
|
293
|
+
: "Generating"
|
|
294
|
+
: state === "outcome_unknown"
|
|
295
|
+
? "The provider may have completed this action. Check the status before trying again."
|
|
296
|
+
: insufficientCredits
|
|
297
|
+
? `${failureCount} API call${failureCount === 1 ? " was" : "s were"} not started because there weren't enough Credits.`
|
|
298
|
+
: undefined;
|
|
299
|
+
if (state === "running") {
|
|
300
|
+
return (
|
|
301
|
+
<div
|
|
302
|
+
data-action-presentation={state}
|
|
303
|
+
data-product-type={productName}
|
|
304
|
+
role="status"
|
|
305
|
+
aria-label={body}
|
|
306
|
+
>
|
|
307
|
+
{isFileProduct
|
|
308
|
+
? <ProductGeneration type={productName} mediaType={product?.mediaType} />
|
|
309
|
+
: <GenerationLoader />}
|
|
310
|
+
</div>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
return (
|
|
314
|
+
<div
|
|
315
|
+
data-action-presentation={state}
|
|
316
|
+
role={state === "failed" ? "alert" : "status"}
|
|
317
|
+
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"
|
|
318
|
+
>
|
|
319
|
+
<span>
|
|
320
|
+
<strong className="block font-medium text-kumo-default">
|
|
321
|
+
{insufficientCredits
|
|
322
|
+
? partial ? "Partially completed" : "Not completed"
|
|
323
|
+
: state === "failed" ? "Action failed" : "Status unavailable"}
|
|
324
|
+
</strong>
|
|
325
|
+
{body && <span className="block text-kumo-subtle">{body}</span>}
|
|
326
|
+
</span>
|
|
327
|
+
</div>
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function TurnWorkDisclosure({
|
|
332
|
+
running,
|
|
333
|
+
open,
|
|
334
|
+
onOpenChange,
|
|
335
|
+
children,
|
|
336
|
+
}: {
|
|
337
|
+
running: boolean;
|
|
338
|
+
open: boolean;
|
|
339
|
+
onOpenChange: (open: boolean) => void;
|
|
340
|
+
children: ReactNode;
|
|
341
|
+
}) {
|
|
342
|
+
return (
|
|
343
|
+
<Collapsible
|
|
344
|
+
data-slot="turn-work-disclosure"
|
|
345
|
+
open={open}
|
|
346
|
+
onOpenChange={onOpenChange}
|
|
347
|
+
>
|
|
348
|
+
<CollapsibleTrigger
|
|
349
|
+
aria-disabled={running}
|
|
350
|
+
className="group/trigger flex items-center gap-1.5 rounded-md py-1 text-[13.5px] text-kumo-inactive transition-colors outline-none hover:text-kumo-subtle"
|
|
351
|
+
>
|
|
352
|
+
<CaretRight
|
|
353
|
+
size={14}
|
|
354
|
+
className="shrink-0 transition-transform duration-200 group-data-open/trigger:rotate-90 group-data-panel-open/trigger:rotate-90 motion-reduce:transition-none"
|
|
355
|
+
/>
|
|
356
|
+
<span>{running ? "Working…" : "Work"}</span>
|
|
357
|
+
</CollapsibleTrigger>
|
|
358
|
+
<CollapsibleContent className={collapsePanel}>
|
|
359
|
+
<div className="cos-trace-chain relative space-y-4 pt-2">
|
|
360
|
+
{children}
|
|
361
|
+
</div>
|
|
362
|
+
</CollapsibleContent>
|
|
363
|
+
</Collapsible>
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
196
367
|
export function CloudOsChatMessages({
|
|
197
368
|
messages,
|
|
198
369
|
status,
|
|
199
370
|
turnActive,
|
|
200
|
-
isHydrating = false,
|
|
201
371
|
isRecovering = false,
|
|
372
|
+
recoveryAttempt,
|
|
373
|
+
recoveryMax,
|
|
202
374
|
recoveryStatusLabel,
|
|
203
375
|
awaitingApproval = false,
|
|
204
376
|
hasPendingSteer = false,
|
|
@@ -210,6 +382,7 @@ export function CloudOsChatMessages({
|
|
|
210
382
|
renderApproval,
|
|
211
383
|
renderFile,
|
|
212
384
|
resolveUrl,
|
|
385
|
+
resolveArtifactId,
|
|
213
386
|
constrainWidth = true,
|
|
214
387
|
emptyTitle = "What are we working on?",
|
|
215
388
|
onRetry,
|
|
@@ -217,14 +390,19 @@ export function CloudOsChatMessages({
|
|
|
217
390
|
onSuggestion,
|
|
218
391
|
onRespond,
|
|
219
392
|
onOpenSchedule,
|
|
393
|
+
onOpenArtifact,
|
|
220
394
|
onCopy,
|
|
221
395
|
}: CloudOsChatMessagesProps) {
|
|
222
396
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
223
397
|
const endRef = useRef<HTMLDivElement>(null);
|
|
224
398
|
const [showScrollButton, setShowScrollButton] = useState(false);
|
|
399
|
+
const reducedMotion = useReducedMotion();
|
|
225
400
|
const [expandedToolCalls, setExpandedToolCalls] = useState<ReadonlySet<string>>(
|
|
226
401
|
() => new Set(),
|
|
227
402
|
);
|
|
403
|
+
const [expandedTurnWork, setExpandedTurnWork] = useState<ReadonlySet<string>>(
|
|
404
|
+
() => new Set(),
|
|
405
|
+
);
|
|
228
406
|
const [expandedCompactions, setExpandedCompactions] = useState<
|
|
229
407
|
ReadonlySet<string>
|
|
230
408
|
>(() => new Set());
|
|
@@ -242,6 +420,20 @@ export function CloudOsChatMessages({
|
|
|
242
420
|
}),
|
|
243
421
|
[approvalIdOf, isActive, messages, showThinkingTraces],
|
|
244
422
|
);
|
|
423
|
+
const latestUserIndex = entries.findLastIndex((entry) => entry.type === "user");
|
|
424
|
+
const activeTurnEntry = isActive
|
|
425
|
+
? entries.findLast(
|
|
426
|
+
(entry, index): entry is Extract<CloudOsEntry, { type: "assistant" }> =>
|
|
427
|
+
index > latestUserIndex && entry.type === "assistant",
|
|
428
|
+
)
|
|
429
|
+
: undefined;
|
|
430
|
+
const currentPlan = activeTurnEntry?.blocks.findLast(
|
|
431
|
+
(block): block is Extract<AssistantBlock, { kind: "plan" }> =>
|
|
432
|
+
block.kind === "plan",
|
|
433
|
+
);
|
|
434
|
+
const completedPlanSteps = currentPlan?.steps.filter(
|
|
435
|
+
(step) => step.status === "done",
|
|
436
|
+
).length ?? 0;
|
|
245
437
|
|
|
246
438
|
const entryTopClasses = useMemo(
|
|
247
439
|
() =>
|
|
@@ -260,6 +452,15 @@ export function CloudOsChatMessages({
|
|
|
260
452
|
});
|
|
261
453
|
}, []);
|
|
262
454
|
|
|
455
|
+
const setTurnWorkOpen = useCallback((key: string, open: boolean) => {
|
|
456
|
+
setExpandedTurnWork((current) => {
|
|
457
|
+
const next = new Set(current);
|
|
458
|
+
if (open) next.add(key);
|
|
459
|
+
else next.delete(key);
|
|
460
|
+
return next;
|
|
461
|
+
});
|
|
462
|
+
}, []);
|
|
463
|
+
|
|
263
464
|
const toggleCompaction = useCallback((key: string) => {
|
|
264
465
|
setExpandedCompactions((current) => {
|
|
265
466
|
const next = new Set(current);
|
|
@@ -286,6 +487,8 @@ export function CloudOsChatMessages({
|
|
|
286
487
|
const activity = deriveTurnActivity(entries, {
|
|
287
488
|
isActive,
|
|
288
489
|
isRecovering,
|
|
490
|
+
recoveryAttempt,
|
|
491
|
+
recoveryMax,
|
|
289
492
|
recoveryStatusLabel,
|
|
290
493
|
awaitingApproval,
|
|
291
494
|
hasPendingSteer,
|
|
@@ -300,22 +503,23 @@ export function CloudOsChatMessages({
|
|
|
300
503
|
className="relative flex min-h-0 flex-1 flex-col overflow-hidden bg-kumo-base"
|
|
301
504
|
data-cloud-os-transcript=""
|
|
302
505
|
data-status={status}
|
|
303
|
-
aria-busy={isActive ||
|
|
506
|
+
aria-busy={isActive || isRecovering}
|
|
304
507
|
>
|
|
305
|
-
<div
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
508
|
+
<div className="relative flex min-h-0 flex-1">
|
|
509
|
+
<div
|
|
510
|
+
ref={scrollRef}
|
|
511
|
+
onScroll={syncScrollButton}
|
|
512
|
+
tabIndex={0}
|
|
513
|
+
role="region"
|
|
514
|
+
aria-label="Chat messages"
|
|
515
|
+
className="cos-chat-panel flex-1 overflow-y-auto"
|
|
516
|
+
>
|
|
313
517
|
<div
|
|
314
518
|
className={`flex flex-col px-6 pb-8 pt-8 ${
|
|
315
|
-
constrainWidth ? "mx-auto w-full max-w-[
|
|
519
|
+
constrainWidth ? "mx-auto w-full max-w-[var(--cos-conversation-max-width)]" : ""
|
|
316
520
|
}`}
|
|
317
521
|
>
|
|
318
|
-
{entries.length === 0 && !startedWithPending &&
|
|
522
|
+
{entries.length === 0 && !startedWithPending && (
|
|
319
523
|
<div className="flex min-h-56 items-center justify-center text-center text-[24px] leading-8 font-semibold tracking-[-0.4px] text-kumo-default">
|
|
320
524
|
{emptyTitle}
|
|
321
525
|
</div>
|
|
@@ -326,13 +530,19 @@ export function CloudOsChatMessages({
|
|
|
326
530
|
|
|
327
531
|
if (entry.type === "user") {
|
|
328
532
|
return (
|
|
329
|
-
<div
|
|
533
|
+
<div
|
|
534
|
+
key={entry.key}
|
|
535
|
+
data-user-message-entry=""
|
|
536
|
+
className={topClass}
|
|
537
|
+
>
|
|
330
538
|
<UserBubble
|
|
331
539
|
entry={entry}
|
|
332
540
|
onCopy={copyText}
|
|
333
541
|
copied={copied === entry.text}
|
|
334
542
|
renderFile={renderFile}
|
|
335
543
|
resolveUrl={resolveUrl}
|
|
544
|
+
resolveArtifactId={resolveArtifactId}
|
|
545
|
+
onOpenArtifact={onOpenArtifact}
|
|
336
546
|
/>
|
|
337
547
|
</div>
|
|
338
548
|
);
|
|
@@ -341,7 +551,7 @@ export function CloudOsChatMessages({
|
|
|
341
551
|
if (entry.type === "slashCommand") {
|
|
342
552
|
return (
|
|
343
553
|
<div key={entry.key} className={`${topClass} flex flex-col items-end`}>
|
|
344
|
-
<div className="themed-user-bubble-shadow w-fit max-w-[min(680px,78%)] rounded-[
|
|
554
|
+
<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">
|
|
345
555
|
<span className="whitespace-pre-wrap font-mono">{entry.text}</span>
|
|
346
556
|
</div>
|
|
347
557
|
</div>
|
|
@@ -411,6 +621,8 @@ export function CloudOsChatMessages({
|
|
|
411
621
|
<MarkdownMessage
|
|
412
622
|
message={entry.summary}
|
|
413
623
|
resolveUrl={resolveUrl}
|
|
624
|
+
resolveArtifactId={resolveArtifactId}
|
|
625
|
+
onOpenArtifact={onOpenArtifact}
|
|
414
626
|
/>
|
|
415
627
|
</div>
|
|
416
628
|
)}
|
|
@@ -418,34 +630,95 @@ export function CloudOsChatMessages({
|
|
|
418
630
|
);
|
|
419
631
|
}
|
|
420
632
|
|
|
421
|
-
const
|
|
422
|
-
|
|
633
|
+
const entryIsActive =
|
|
634
|
+
isActive && entryIndex === entries.length - 1;
|
|
635
|
+
const finalTextBlocks = entryIsActive
|
|
636
|
+
? []
|
|
637
|
+
: entry.blocks.filter(
|
|
638
|
+
(block): block is Extract<AssistantBlock, { kind: "text" }> =>
|
|
639
|
+
block.kind === "text" && block.final,
|
|
640
|
+
);
|
|
641
|
+
const actionResultBlocks = entry.blocks.filter(
|
|
642
|
+
(block): block is Extract<AssistantBlock, { kind: "actionPresentation" }> =>
|
|
643
|
+
block.kind === "actionPresentation",
|
|
644
|
+
);
|
|
645
|
+
const finalMediaBlocks = entry.blocks.filter(
|
|
646
|
+
(block): block is Extract<AssistantBlock, { kind: "image" | "file" }> =>
|
|
647
|
+
block.kind === "image" || block.kind === "file",
|
|
648
|
+
);
|
|
649
|
+
const workBlocks = entry.blocks.filter(
|
|
650
|
+
(block) =>
|
|
651
|
+
block.kind !== "suggestions" &&
|
|
652
|
+
block.kind !== "image" &&
|
|
653
|
+
block.kind !== "file" &&
|
|
654
|
+
block.kind !== "actionPresentation" &&
|
|
655
|
+
!(block.kind === "text" && block.final && !entryIsActive),
|
|
656
|
+
);
|
|
657
|
+
const hasNarrative = finalTextBlocks.length > 0;
|
|
658
|
+
const insufficientFailures = entry.blocks.filter(
|
|
659
|
+
(block) => block.kind === "actionPresentation" &&
|
|
660
|
+
block.presentation.state === "failed" &&
|
|
661
|
+
block.presentation.failureCode === "insufficient_credits",
|
|
662
|
+
);
|
|
663
|
+
const firstInsufficientFailure = insufficientFailures[0];
|
|
664
|
+
const hasCompletedAction = entry.blocks.some(
|
|
665
|
+
(block) => block.kind === "actionPresentation" &&
|
|
666
|
+
block.presentation.state === "ready",
|
|
667
|
+
);
|
|
668
|
+
const followUpBlocks = entry.blocks.filter(
|
|
669
|
+
(block) => block.kind === "suggestions",
|
|
423
670
|
);
|
|
671
|
+
const footerTimestamp =
|
|
672
|
+
hasNarrative &&
|
|
673
|
+
entry.timestamp !== undefined &&
|
|
674
|
+
!entryIsActive &&
|
|
675
|
+
!entry.blocks.some(
|
|
676
|
+
(block) => block.kind === "reasoning" && block.running,
|
|
677
|
+
)
|
|
678
|
+
? entry.timestamp
|
|
679
|
+
: undefined;
|
|
424
680
|
|
|
425
681
|
return (
|
|
426
682
|
<div
|
|
427
683
|
key={entry.key}
|
|
428
|
-
className={`${topClass} min-w-0 w-full max-w-[860px] space-y-2`}
|
|
684
|
+
className={`${topClass} group/agentMessage min-w-0 w-full max-w-[860px] space-y-2`}
|
|
429
685
|
>
|
|
430
|
-
|
|
431
|
-
|
|
686
|
+
{workBlocks.length > 0 && (
|
|
687
|
+
<TurnWorkDisclosure
|
|
688
|
+
running={entryIsActive}
|
|
689
|
+
open={entryIsActive || expandedTurnWork.has(entry.key)}
|
|
690
|
+
onOpenChange={(open) => {
|
|
691
|
+
if (!entryIsActive) setTurnWorkOpen(entry.key, open);
|
|
692
|
+
}}
|
|
693
|
+
>
|
|
694
|
+
{workBlocks.map((block) => {
|
|
432
695
|
switch (block.kind) {
|
|
433
696
|
case "reasoning":
|
|
434
697
|
return (
|
|
435
|
-
<ThinkingTraceRow
|
|
698
|
+
<ThinkingTraceRow
|
|
699
|
+
key={block.key}
|
|
700
|
+
reasoning={block.text}
|
|
701
|
+
running={block.running}
|
|
702
|
+
/>
|
|
436
703
|
);
|
|
437
|
-
case "text":
|
|
704
|
+
case "text": {
|
|
705
|
+
const streaming =
|
|
706
|
+
entryIsActive && block === entry.blocks.at(-1);
|
|
438
707
|
return (
|
|
439
708
|
<div
|
|
440
709
|
key={block.key}
|
|
441
|
-
className="cos-markdown text-[14px] leading-[22px] tracking-[-0.25px] text-kumo-default"
|
|
710
|
+
className="cos-markdown px-1.5 text-[14px] leading-[22px] tracking-[-0.25px] text-kumo-default"
|
|
442
711
|
>
|
|
443
712
|
<MarkdownMessage
|
|
444
713
|
message={block.text}
|
|
445
714
|
resolveUrl={resolveUrl}
|
|
715
|
+
resolveArtifactId={resolveArtifactId}
|
|
716
|
+
onOpenArtifact={onOpenArtifact}
|
|
717
|
+
streaming={streaming}
|
|
446
718
|
/>
|
|
447
719
|
</div>
|
|
448
720
|
);
|
|
721
|
+
}
|
|
449
722
|
case "toolGroup":
|
|
450
723
|
return (
|
|
451
724
|
<ToolGroupRow
|
|
@@ -457,6 +730,7 @@ export function CloudOsChatMessages({
|
|
|
457
730
|
/>
|
|
458
731
|
);
|
|
459
732
|
case "plan":
|
|
733
|
+
if (currentPlan?.key === block.key) return null;
|
|
460
734
|
return (
|
|
461
735
|
<PlanBlock
|
|
462
736
|
key={block.key}
|
|
@@ -473,13 +747,7 @@ export function CloudOsChatMessages({
|
|
|
473
747
|
/>
|
|
474
748
|
);
|
|
475
749
|
case "suggestions":
|
|
476
|
-
return
|
|
477
|
-
<SuggestionsBlock
|
|
478
|
-
key={block.key}
|
|
479
|
-
items={block.items}
|
|
480
|
-
onSuggestion={(text) => onSuggestion?.(text)}
|
|
481
|
-
/>
|
|
482
|
-
);
|
|
750
|
+
return null;
|
|
483
751
|
case "schedule":
|
|
484
752
|
return (
|
|
485
753
|
<ScheduleBlock
|
|
@@ -522,44 +790,92 @@ export function CloudOsChatMessages({
|
|
|
522
790
|
return (
|
|
523
791
|
<SubAgentsBlock key={block.key} agents={block.agents} />
|
|
524
792
|
);
|
|
525
|
-
case "
|
|
793
|
+
case "actionPresentation":
|
|
794
|
+
return null;
|
|
795
|
+
case "error":
|
|
526
796
|
return (
|
|
527
|
-
<
|
|
528
|
-
{
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
mediaType: block.mediaType ?? "image/*",
|
|
533
|
-
},
|
|
534
|
-
placement: "assistant-message",
|
|
535
|
-
})}
|
|
536
|
-
</Fragment>
|
|
797
|
+
<ErrorBlock
|
|
798
|
+
key={block.key}
|
|
799
|
+
title={block.title}
|
|
800
|
+
body={block.body}
|
|
801
|
+
/>
|
|
537
802
|
);
|
|
538
|
-
|
|
803
|
+
}
|
|
804
|
+
})}
|
|
805
|
+
</TurnWorkDisclosure>
|
|
806
|
+
)}
|
|
807
|
+
|
|
808
|
+
{actionResultBlocks.length > 0 && (
|
|
809
|
+
<div data-action-results="" className="space-y-4">
|
|
810
|
+
{actionResultBlocks.map((block) => {
|
|
811
|
+
if (
|
|
812
|
+
block.presentation.failureCode === "insufficient_credits" &&
|
|
813
|
+
block !== firstInsufficientFailure
|
|
814
|
+
) return null;
|
|
815
|
+
return (
|
|
816
|
+
<ActionPresentationBlock
|
|
817
|
+
key={block.key}
|
|
818
|
+
presentation={block.presentation}
|
|
819
|
+
failureCount={insufficientFailures.length}
|
|
820
|
+
partial={hasCompletedAction}
|
|
821
|
+
renderFile={renderFile}
|
|
822
|
+
resolveUrl={resolveUrl}
|
|
823
|
+
resolveArtifactId={resolveArtifactId}
|
|
824
|
+
onOpenArtifact={onOpenArtifact}
|
|
825
|
+
/>
|
|
826
|
+
);
|
|
827
|
+
})}
|
|
828
|
+
</div>
|
|
829
|
+
)}
|
|
830
|
+
|
|
831
|
+
{(finalMediaBlocks.length > 0 || finalTextBlocks.length > 0) && (
|
|
832
|
+
<div data-final-message="" className="space-y-4">
|
|
833
|
+
{finalMediaBlocks.map((block) => {
|
|
834
|
+
if (block.kind === "image") {
|
|
835
|
+
const url = resolveUrl?.(block.src) ?? block.src;
|
|
836
|
+
const artifactId = resolveArtifactId?.(url);
|
|
539
837
|
return (
|
|
540
838
|
<Fragment key={block.key}>
|
|
541
839
|
{renderFileView(renderFile, {
|
|
542
840
|
file: {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
841
|
+
url,
|
|
842
|
+
filename: block.alt,
|
|
843
|
+
mediaType: block.mediaType ?? "image/*",
|
|
546
844
|
},
|
|
547
845
|
placement: "assistant-message",
|
|
846
|
+
onOpen: artifactId && onOpenArtifact
|
|
847
|
+
? () => onOpenArtifact(artifactId)
|
|
848
|
+
: undefined,
|
|
548
849
|
})}
|
|
549
850
|
</Fragment>
|
|
550
851
|
);
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
852
|
+
}
|
|
853
|
+
return (
|
|
854
|
+
<Fragment key={block.key}>
|
|
855
|
+
{renderFileView(renderFile, {
|
|
856
|
+
file: {
|
|
857
|
+
...block.file,
|
|
858
|
+
url: resolveUrl?.(block.file.url) ?? block.file.url,
|
|
859
|
+
},
|
|
860
|
+
placement: "assistant-message",
|
|
861
|
+
})}
|
|
862
|
+
</Fragment>
|
|
863
|
+
);
|
|
864
|
+
})}
|
|
865
|
+
{finalTextBlocks.map((block) => (
|
|
866
|
+
<div key={block.key} className="cos-markdown px-1.5 text-[14px] leading-[22px] tracking-[-0.25px] text-kumo-default">
|
|
867
|
+
<MarkdownMessage
|
|
868
|
+
message={block.text}
|
|
869
|
+
resolveUrl={resolveUrl}
|
|
870
|
+
resolveArtifactId={resolveArtifactId}
|
|
871
|
+
onOpenArtifact={onOpenArtifact}
|
|
872
|
+
/>
|
|
873
|
+
</div>
|
|
874
|
+
))}
|
|
875
|
+
</div>
|
|
876
|
+
)}
|
|
561
877
|
|
|
562
|
-
|
|
878
|
+
{entry.terminal?.kind === "error" && (
|
|
563
879
|
<ErrorBlock
|
|
564
880
|
title={entry.terminal.title}
|
|
565
881
|
body={entry.terminal.body}
|
|
@@ -567,7 +883,7 @@ export function CloudOsChatMessages({
|
|
|
567
883
|
/>
|
|
568
884
|
)}
|
|
569
885
|
|
|
570
|
-
|
|
886
|
+
{entry.terminal?.kind === "interrupted" && (
|
|
571
887
|
<div
|
|
572
888
|
className="px-1.5 py-1 text-[13px] leading-[19px] text-kumo-inactive"
|
|
573
889
|
role="status"
|
|
@@ -576,8 +892,11 @@ export function CloudOsChatMessages({
|
|
|
576
892
|
</div>
|
|
577
893
|
)}
|
|
578
894
|
|
|
579
|
-
|
|
580
|
-
<div
|
|
895
|
+
{footerTimestamp !== undefined && (
|
|
896
|
+
<div
|
|
897
|
+
data-agent-message-footer=""
|
|
898
|
+
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"
|
|
899
|
+
>
|
|
581
900
|
<Tooltip
|
|
582
901
|
content={copied === entry.copyText ? "Copied!" : "Copy message"}
|
|
583
902
|
asChild
|
|
@@ -595,14 +914,21 @@ export function CloudOsChatMessages({
|
|
|
595
914
|
)}
|
|
596
915
|
</button>
|
|
597
916
|
</Tooltip>
|
|
598
|
-
<Tooltip content={formatFullTimestamp(
|
|
917
|
+
<Tooltip content={formatFullTimestamp(footerTimestamp)} asChild>
|
|
599
918
|
<span className="px-1 font-mono text-[11px] leading-4 text-kumo-inactive">
|
|
600
|
-
{formatClockTime(
|
|
919
|
+
{formatClockTime(footerTimestamp)}
|
|
601
920
|
</span>
|
|
602
921
|
</Tooltip>
|
|
603
922
|
</div>
|
|
604
923
|
)}
|
|
605
|
-
|
|
924
|
+
|
|
925
|
+
{followUpBlocks.map((block) => (
|
|
926
|
+
<SuggestionsBlock
|
|
927
|
+
key={block.key}
|
|
928
|
+
items={block.items}
|
|
929
|
+
onSuggestion={(text) => onSuggestion?.(text)}
|
|
930
|
+
/>
|
|
931
|
+
))}
|
|
606
932
|
</div>
|
|
607
933
|
);
|
|
608
934
|
})}
|
|
@@ -631,30 +957,47 @@ export function CloudOsChatMessages({
|
|
|
631
957
|
|
|
632
958
|
<div ref={endRef} />
|
|
633
959
|
</div>
|
|
960
|
+
</div>
|
|
961
|
+
|
|
962
|
+
<WorkshopIconButton
|
|
963
|
+
aria-label="Scroll to bottom"
|
|
964
|
+
onClick={() => endRef.current?.scrollIntoView({ behavior: "smooth" })}
|
|
965
|
+
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 ${
|
|
966
|
+
showScrollButton
|
|
967
|
+
? "translate-y-0 opacity-100"
|
|
968
|
+
: "pointer-events-none translate-y-2 opacity-0"
|
|
969
|
+
}`}
|
|
970
|
+
>
|
|
971
|
+
<ArrowDown size={15} />
|
|
972
|
+
</WorkshopIconButton>
|
|
634
973
|
</div>
|
|
635
974
|
|
|
636
|
-
<
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
:
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
975
|
+
<AnimatePresence initial={false}>
|
|
976
|
+
{currentPlan && completedPlanSteps < currentPlan.steps.length && (
|
|
977
|
+
<motion.div
|
|
978
|
+
key="task-progress"
|
|
979
|
+
initial={reducedMotion ? { opacity: 1 } : { opacity: 0, y: 8, scale: 0.98 }}
|
|
980
|
+
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
981
|
+
exit={reducedMotion ? { opacity: 0 } : { opacity: 0, y: 8, scale: 0.98 }}
|
|
982
|
+
transition={{ duration: reducedMotion ? 0 : 0.2, ease: [0.32, 0.72, 0, 1] }}
|
|
983
|
+
className="relative z-10 mx-auto w-full max-w-3xl shrink-0 px-4"
|
|
984
|
+
>
|
|
985
|
+
<section
|
|
986
|
+
data-cloud-os-plan-progress=""
|
|
987
|
+
aria-label="Task progress"
|
|
988
|
+
className="mx-auto border border-b-0 border-kumo-line bg-kumo-base px-4 py-2.5"
|
|
989
|
+
style={{ width: "90%", borderRadius: "1rem 1rem 0 0" }}
|
|
990
|
+
>
|
|
991
|
+
<PlanBlock
|
|
992
|
+
steps={currentPlan.steps}
|
|
993
|
+
running={isActive}
|
|
994
|
+
defaultOpen={false}
|
|
995
|
+
/>
|
|
996
|
+
</section>
|
|
997
|
+
</motion.div>
|
|
998
|
+
)}
|
|
999
|
+
</AnimatePresence>
|
|
1000
|
+
|
|
658
1001
|
</div>
|
|
659
1002
|
);
|
|
660
1003
|
}
|