@springbrand/message-panel 0.1.3-alpha.34 → 0.1.3-alpha.36
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 +2 -1
- package/cloud-os/assets/thinking-star.svg +9 -0
- package/cloud-os/chat/cloud-os-chat-messages.tsx +262 -77
- package/cloud-os/chat/code-runner.tsx +106 -0
- package/cloud-os/chat/range.ts +8 -0
- package/cloud-os/chat/rich-blocks.tsx +161 -260
- package/cloud-os/chat/surfaces.tsx +90 -0
- package/cloud-os/chat/tool-call.tsx +94 -0
- package/cloud-os/chat/tool-presentation.ts +3 -11
- package/cloud-os/chat/tool-rows.tsx +145 -141
- package/cloud-os/chat/tool-timeline.tsx +123 -0
- package/cloud-os/chat/transcript-model.ts +92 -15
- package/cloud-os/chat/web-search.tsx +135 -0
- package/cloud-os/composer/cloud-os-chat-input.tsx +1 -1
- package/cloud-os/index.ts +10 -0
- package/cloud-os/primitives/collapsible.tsx +21 -0
- package/cloud-os/styles/cloud-os.css +18 -0
- package/package.json +3 -1
package/cloud-os/README.md
CHANGED
|
@@ -64,7 +64,8 @@ pnpm dev:cloud-os # http://localhost:9992
|
|
|
64
64
|
|
|
65
65
|
## 宿主接线要求
|
|
66
66
|
|
|
67
|
-
1. Tailwind
|
|
67
|
+
1. Tailwind 入口先 `@import "tw-animate-css"`,再 **`@import` 本包的 css,
|
|
68
|
+
且必须和其它 `@import` 挤在最前面**。排到
|
|
68
69
|
`@source` / `@layer` 之后,Tailwind 就不会内联它,`@theme` 块会被整块忽略,
|
|
69
70
|
kumo 的 utility 一个都生成不出来(踩过一次)。
|
|
70
71
|
2. 把本目录加进 `@source`,且必须写显式 glob:`"…/cloud-os/**/*.{ts,tsx}"`。
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="12.2369" height="12.2369" viewBox="0 0 12.2369 12.2369" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path id="Star 14" d="M5.18061 0.652963C5.50276 -0.217655 6.73415 -0.217654 7.05631 0.652964L7.64089 2.23277C8.04603 3.32764 8.90927 4.19089 10.0042 4.59603L11.584 5.18061C12.4546 5.50276 12.4546 6.73415 11.584 7.05631L10.0041 7.64089C8.90927 8.04603 8.04603 8.90927 7.64089 10.0042L7.05631 11.584C6.73415 12.4546 5.50276 12.4546 5.18061 11.584L4.59603 10.0041C4.19089 8.90927 3.32764 8.04603 2.23277 7.64089L0.652963 7.05631C-0.217655 6.73415 -0.217654 5.50276 0.652964 5.18061L2.23277 4.59603C3.32764 4.19089 4.19089 3.32764 4.59603 2.23277L5.18061 0.652963Z" fill="url(#paint0_radial_0_26)"/>
|
|
3
|
+
<defs>
|
|
4
|
+
<radialGradient id="paint0_radial_0_26" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(6.11846 2.11846) rotate(90) scale(12)">
|
|
5
|
+
<stop stop-color="#FF9938"/>
|
|
6
|
+
<stop offset="1" stop-color="#FFE46C"/>
|
|
7
|
+
</radialGradient>
|
|
8
|
+
</defs>
|
|
9
|
+
</svg>
|
|
@@ -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,6 +18,11 @@ 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,
|
|
@@ -35,6 +41,7 @@ import {
|
|
|
35
41
|
type ApprovalDecision,
|
|
36
42
|
} from "./rich-blocks";
|
|
37
43
|
import { ThinkingTraceRow, ToolGroupRow } from "./tool-rows";
|
|
44
|
+
import { collapsePanel } from "./surfaces";
|
|
38
45
|
import { CloudOsActivityIndicator } from "./activity-indicator";
|
|
39
46
|
import { ProductGeneration } from "./image-generation";
|
|
40
47
|
import { GenerationLoader } from "./loading-state";
|
|
@@ -49,6 +56,7 @@ import {
|
|
|
49
56
|
formatClockTime,
|
|
50
57
|
formatFullTimestamp,
|
|
51
58
|
rhythmTopClass,
|
|
59
|
+
type AssistantBlock,
|
|
52
60
|
type CloudOsEntry,
|
|
53
61
|
type CloudOsActionPresentation,
|
|
54
62
|
} from "./transcript-model";
|
|
@@ -61,6 +69,8 @@ export interface CloudOsChatMessagesProps {
|
|
|
61
69
|
/** 权威 Turn 生命周期;提供时替代本地 status 推断。 */
|
|
62
70
|
turnActive?: boolean;
|
|
63
71
|
isRecovering?: boolean;
|
|
72
|
+
recoveryAttempt?: number;
|
|
73
|
+
recoveryMax?: number;
|
|
64
74
|
recoveryStatusLabel?: string;
|
|
65
75
|
awaitingApproval?: boolean;
|
|
66
76
|
/** 有 pending steer 时,活动占位说明「正在等当前步骤收尾」。 */
|
|
@@ -184,7 +194,10 @@ function UserBubble({
|
|
|
184
194
|
</div>
|
|
185
195
|
)}
|
|
186
196
|
{entry.text && (
|
|
187
|
-
<div
|
|
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
|
+
>
|
|
188
201
|
<MarkdownMessage
|
|
189
202
|
message={entry.text}
|
|
190
203
|
resolveUrl={resolveUrl}
|
|
@@ -315,11 +328,49 @@ function ActionPresentationBlock({
|
|
|
315
328
|
);
|
|
316
329
|
}
|
|
317
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
|
+
|
|
318
367
|
export function CloudOsChatMessages({
|
|
319
368
|
messages,
|
|
320
369
|
status,
|
|
321
370
|
turnActive,
|
|
322
371
|
isRecovering = false,
|
|
372
|
+
recoveryAttempt,
|
|
373
|
+
recoveryMax,
|
|
323
374
|
recoveryStatusLabel,
|
|
324
375
|
awaitingApproval = false,
|
|
325
376
|
hasPendingSteer = false,
|
|
@@ -345,9 +396,13 @@ export function CloudOsChatMessages({
|
|
|
345
396
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
346
397
|
const endRef = useRef<HTMLDivElement>(null);
|
|
347
398
|
const [showScrollButton, setShowScrollButton] = useState(false);
|
|
399
|
+
const reducedMotion = useReducedMotion();
|
|
348
400
|
const [expandedToolCalls, setExpandedToolCalls] = useState<ReadonlySet<string>>(
|
|
349
401
|
() => new Set(),
|
|
350
402
|
);
|
|
403
|
+
const [expandedTurnWork, setExpandedTurnWork] = useState<ReadonlySet<string>>(
|
|
404
|
+
() => new Set(),
|
|
405
|
+
);
|
|
351
406
|
const [expandedCompactions, setExpandedCompactions] = useState<
|
|
352
407
|
ReadonlySet<string>
|
|
353
408
|
>(() => new Set());
|
|
@@ -365,6 +420,20 @@ export function CloudOsChatMessages({
|
|
|
365
420
|
}),
|
|
366
421
|
[approvalIdOf, isActive, messages, showThinkingTraces],
|
|
367
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;
|
|
368
437
|
|
|
369
438
|
const entryTopClasses = useMemo(
|
|
370
439
|
() =>
|
|
@@ -383,6 +452,15 @@ export function CloudOsChatMessages({
|
|
|
383
452
|
});
|
|
384
453
|
}, []);
|
|
385
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
|
+
|
|
386
464
|
const toggleCompaction = useCallback((key: string) => {
|
|
387
465
|
setExpandedCompactions((current) => {
|
|
388
466
|
const next = new Set(current);
|
|
@@ -409,6 +487,8 @@ export function CloudOsChatMessages({
|
|
|
409
487
|
const activity = deriveTurnActivity(entries, {
|
|
410
488
|
isActive,
|
|
411
489
|
isRecovering,
|
|
490
|
+
recoveryAttempt,
|
|
491
|
+
recoveryMax,
|
|
412
492
|
recoveryStatusLabel,
|
|
413
493
|
awaitingApproval,
|
|
414
494
|
hasPendingSteer,
|
|
@@ -425,14 +505,15 @@ export function CloudOsChatMessages({
|
|
|
425
505
|
data-status={status}
|
|
426
506
|
aria-busy={isActive || isRecovering}
|
|
427
507
|
>
|
|
428
|
-
<div
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
+
>
|
|
436
517
|
<div
|
|
437
518
|
className={`flex flex-col px-6 pb-8 pt-8 ${
|
|
438
519
|
constrainWidth ? "mx-auto w-full max-w-[920px]" : ""
|
|
@@ -449,7 +530,11 @@ export function CloudOsChatMessages({
|
|
|
449
530
|
|
|
450
531
|
if (entry.type === "user") {
|
|
451
532
|
return (
|
|
452
|
-
<div
|
|
533
|
+
<div
|
|
534
|
+
key={entry.key}
|
|
535
|
+
data-user-message-entry=""
|
|
536
|
+
className={topClass}
|
|
537
|
+
>
|
|
453
538
|
<UserBubble
|
|
454
539
|
entry={entry}
|
|
455
540
|
onCopy={copyText}
|
|
@@ -545,9 +630,34 @@ export function CloudOsChatMessages({
|
|
|
545
630
|
);
|
|
546
631
|
}
|
|
547
632
|
|
|
548
|
-
const
|
|
549
|
-
|
|
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 finalActionBlocks = entryIsActive
|
|
642
|
+
? []
|
|
643
|
+
: entry.blocks.filter(
|
|
644
|
+
(block): block is Extract<AssistantBlock, { kind: "actionPresentation" }> =>
|
|
645
|
+
block.kind === "actionPresentation" &&
|
|
646
|
+
block.presentation.state === "ready",
|
|
647
|
+
);
|
|
648
|
+
const finalMediaBlocks = entry.blocks.filter(
|
|
649
|
+
(block): block is Extract<AssistantBlock, { kind: "image" | "file" }> =>
|
|
650
|
+
block.kind === "image" || block.kind === "file",
|
|
651
|
+
);
|
|
652
|
+
const workBlocks = entry.blocks.filter(
|
|
653
|
+
(block) =>
|
|
654
|
+
block.kind !== "suggestions" &&
|
|
655
|
+
block.kind !== "image" &&
|
|
656
|
+
block.kind !== "file" &&
|
|
657
|
+
!(block.kind === "text" && block.final && !entryIsActive) &&
|
|
658
|
+
!(block.kind === "actionPresentation" && block.presentation.state === "ready"),
|
|
550
659
|
);
|
|
660
|
+
const hasNarrative = finalTextBlocks.length > 0;
|
|
551
661
|
const insufficientFailures = entry.blocks.filter(
|
|
552
662
|
(block) => block.kind === "actionPresentation" &&
|
|
553
663
|
block.presentation.state === "failed" &&
|
|
@@ -558,14 +668,33 @@ export function CloudOsChatMessages({
|
|
|
558
668
|
(block) => block.kind === "actionPresentation" &&
|
|
559
669
|
block.presentation.state === "ready",
|
|
560
670
|
);
|
|
671
|
+
const followUpBlocks = entry.blocks.filter(
|
|
672
|
+
(block) => block.kind === "suggestions",
|
|
673
|
+
);
|
|
674
|
+
const footerTimestamp =
|
|
675
|
+
hasNarrative &&
|
|
676
|
+
entry.timestamp !== undefined &&
|
|
677
|
+
!entryIsActive &&
|
|
678
|
+
!entry.blocks.some(
|
|
679
|
+
(block) => block.kind === "reasoning" && block.running,
|
|
680
|
+
)
|
|
681
|
+
? entry.timestamp
|
|
682
|
+
: undefined;
|
|
561
683
|
|
|
562
684
|
return (
|
|
563
685
|
<div
|
|
564
686
|
key={entry.key}
|
|
565
|
-
className={`${topClass} min-w-0 w-full max-w-[860px] space-y-2`}
|
|
687
|
+
className={`${topClass} group/agentMessage min-w-0 w-full max-w-[860px] space-y-2`}
|
|
566
688
|
>
|
|
567
|
-
|
|
568
|
-
|
|
689
|
+
{workBlocks.length > 0 && (
|
|
690
|
+
<TurnWorkDisclosure
|
|
691
|
+
running={entryIsActive}
|
|
692
|
+
open={entryIsActive || expandedTurnWork.has(entry.key)}
|
|
693
|
+
onOpenChange={(open) => {
|
|
694
|
+
if (!entryIsActive) setTurnWorkOpen(entry.key, open);
|
|
695
|
+
}}
|
|
696
|
+
>
|
|
697
|
+
{workBlocks.map((block) => {
|
|
569
698
|
switch (block.kind) {
|
|
570
699
|
case "reasoning":
|
|
571
700
|
return (
|
|
@@ -577,13 +706,11 @@ export function CloudOsChatMessages({
|
|
|
577
706
|
);
|
|
578
707
|
case "text": {
|
|
579
708
|
const streaming =
|
|
580
|
-
|
|
581
|
-
entryIndex === entries.length - 1 &&
|
|
582
|
-
block === entry.blocks.at(-1);
|
|
709
|
+
entryIsActive && block === entry.blocks.at(-1);
|
|
583
710
|
return (
|
|
584
711
|
<div
|
|
585
712
|
key={block.key}
|
|
586
|
-
className="cos-markdown text-[14px] leading-[22px] tracking-[-0.25px] text-kumo-default"
|
|
713
|
+
className="cos-markdown px-1.5 text-[14px] leading-[22px] tracking-[-0.25px] text-kumo-default"
|
|
587
714
|
>
|
|
588
715
|
<MarkdownMessage
|
|
589
716
|
message={block.text}
|
|
@@ -606,6 +733,7 @@ export function CloudOsChatMessages({
|
|
|
606
733
|
/>
|
|
607
734
|
);
|
|
608
735
|
case "plan":
|
|
736
|
+
if (currentPlan?.key === block.key) return null;
|
|
609
737
|
return (
|
|
610
738
|
<PlanBlock
|
|
611
739
|
key={block.key}
|
|
@@ -622,13 +750,7 @@ export function CloudOsChatMessages({
|
|
|
622
750
|
/>
|
|
623
751
|
);
|
|
624
752
|
case "suggestions":
|
|
625
|
-
return
|
|
626
|
-
<SuggestionsBlock
|
|
627
|
-
key={block.key}
|
|
628
|
-
items={block.items}
|
|
629
|
-
onSuggestion={(text) => onSuggestion?.(text)}
|
|
630
|
-
/>
|
|
631
|
-
);
|
|
753
|
+
return null;
|
|
632
754
|
case "schedule":
|
|
633
755
|
return (
|
|
634
756
|
<ScheduleBlock
|
|
@@ -671,39 +793,6 @@ export function CloudOsChatMessages({
|
|
|
671
793
|
return (
|
|
672
794
|
<SubAgentsBlock key={block.key} agents={block.agents} />
|
|
673
795
|
);
|
|
674
|
-
case "image":
|
|
675
|
-
{
|
|
676
|
-
const url = resolveUrl?.(block.src) ?? block.src;
|
|
677
|
-
const artifactId = resolveArtifactId?.(url);
|
|
678
|
-
return (
|
|
679
|
-
<Fragment key={block.key}>
|
|
680
|
-
{renderFileView(renderFile, {
|
|
681
|
-
file: {
|
|
682
|
-
url,
|
|
683
|
-
filename: block.alt,
|
|
684
|
-
mediaType: block.mediaType ?? "image/*",
|
|
685
|
-
},
|
|
686
|
-
placement: "assistant-message",
|
|
687
|
-
onOpen: artifactId && onOpenArtifact
|
|
688
|
-
? () => onOpenArtifact(artifactId)
|
|
689
|
-
: undefined,
|
|
690
|
-
})}
|
|
691
|
-
</Fragment>
|
|
692
|
-
);
|
|
693
|
-
}
|
|
694
|
-
case "file":
|
|
695
|
-
return (
|
|
696
|
-
<Fragment key={block.key}>
|
|
697
|
-
{renderFileView(renderFile, {
|
|
698
|
-
file: {
|
|
699
|
-
...block.file,
|
|
700
|
-
url:
|
|
701
|
-
resolveUrl?.(block.file.url) ?? block.file.url,
|
|
702
|
-
},
|
|
703
|
-
placement: "assistant-message",
|
|
704
|
-
})}
|
|
705
|
-
</Fragment>
|
|
706
|
-
);
|
|
707
796
|
case "actionPresentation":
|
|
708
797
|
if (
|
|
709
798
|
block.presentation.failureCode === "insufficient_credits" &&
|
|
@@ -731,8 +820,67 @@ export function CloudOsChatMessages({
|
|
|
731
820
|
);
|
|
732
821
|
}
|
|
733
822
|
})}
|
|
823
|
+
</TurnWorkDisclosure>
|
|
824
|
+
)}
|
|
825
|
+
|
|
826
|
+
{(finalMediaBlocks.length > 0 || finalActionBlocks.length > 0 || finalTextBlocks.length > 0) && (
|
|
827
|
+
<div data-final-message="" className="space-y-4">
|
|
828
|
+
{finalMediaBlocks.map((block) => {
|
|
829
|
+
if (block.kind === "image") {
|
|
830
|
+
const url = resolveUrl?.(block.src) ?? block.src;
|
|
831
|
+
const artifactId = resolveArtifactId?.(url);
|
|
832
|
+
return (
|
|
833
|
+
<Fragment key={block.key}>
|
|
834
|
+
{renderFileView(renderFile, {
|
|
835
|
+
file: {
|
|
836
|
+
url,
|
|
837
|
+
filename: block.alt,
|
|
838
|
+
mediaType: block.mediaType ?? "image/*",
|
|
839
|
+
},
|
|
840
|
+
placement: "assistant-message",
|
|
841
|
+
onOpen: artifactId && onOpenArtifact
|
|
842
|
+
? () => onOpenArtifact(artifactId)
|
|
843
|
+
: undefined,
|
|
844
|
+
})}
|
|
845
|
+
</Fragment>
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
return (
|
|
849
|
+
<Fragment key={block.key}>
|
|
850
|
+
{renderFileView(renderFile, {
|
|
851
|
+
file: {
|
|
852
|
+
...block.file,
|
|
853
|
+
url: resolveUrl?.(block.file.url) ?? block.file.url,
|
|
854
|
+
},
|
|
855
|
+
placement: "assistant-message",
|
|
856
|
+
})}
|
|
857
|
+
</Fragment>
|
|
858
|
+
);
|
|
859
|
+
})}
|
|
860
|
+
{finalActionBlocks.map((block) => (
|
|
861
|
+
<ActionPresentationBlock
|
|
862
|
+
key={block.key}
|
|
863
|
+
presentation={block.presentation}
|
|
864
|
+
renderFile={renderFile}
|
|
865
|
+
resolveUrl={resolveUrl}
|
|
866
|
+
resolveArtifactId={resolveArtifactId}
|
|
867
|
+
onOpenArtifact={onOpenArtifact}
|
|
868
|
+
/>
|
|
869
|
+
))}
|
|
870
|
+
{finalTextBlocks.map((block) => (
|
|
871
|
+
<div key={block.key} className="cos-markdown px-1.5 text-[14px] leading-[22px] tracking-[-0.25px] text-kumo-default">
|
|
872
|
+
<MarkdownMessage
|
|
873
|
+
message={block.text}
|
|
874
|
+
resolveUrl={resolveUrl}
|
|
875
|
+
resolveArtifactId={resolveArtifactId}
|
|
876
|
+
onOpenArtifact={onOpenArtifact}
|
|
877
|
+
/>
|
|
878
|
+
</div>
|
|
879
|
+
))}
|
|
880
|
+
</div>
|
|
881
|
+
)}
|
|
734
882
|
|
|
735
|
-
|
|
883
|
+
{entry.terminal?.kind === "error" && (
|
|
736
884
|
<ErrorBlock
|
|
737
885
|
title={entry.terminal.title}
|
|
738
886
|
body={entry.terminal.body}
|
|
@@ -740,7 +888,7 @@ export function CloudOsChatMessages({
|
|
|
740
888
|
/>
|
|
741
889
|
)}
|
|
742
890
|
|
|
743
|
-
|
|
891
|
+
{entry.terminal?.kind === "interrupted" && (
|
|
744
892
|
<div
|
|
745
893
|
className="px-1.5 py-1 text-[13px] leading-[19px] text-kumo-inactive"
|
|
746
894
|
role="status"
|
|
@@ -749,8 +897,11 @@ export function CloudOsChatMessages({
|
|
|
749
897
|
</div>
|
|
750
898
|
)}
|
|
751
899
|
|
|
752
|
-
|
|
753
|
-
<div
|
|
900
|
+
{footerTimestamp !== undefined && (
|
|
901
|
+
<div
|
|
902
|
+
data-agent-message-footer=""
|
|
903
|
+
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"
|
|
904
|
+
>
|
|
754
905
|
<Tooltip
|
|
755
906
|
content={copied === entry.copyText ? "Copied!" : "Copy message"}
|
|
756
907
|
asChild
|
|
@@ -768,14 +919,21 @@ export function CloudOsChatMessages({
|
|
|
768
919
|
)}
|
|
769
920
|
</button>
|
|
770
921
|
</Tooltip>
|
|
771
|
-
<Tooltip content={formatFullTimestamp(
|
|
922
|
+
<Tooltip content={formatFullTimestamp(footerTimestamp)} asChild>
|
|
772
923
|
<span className="px-1 font-mono text-[11px] leading-4 text-kumo-inactive">
|
|
773
|
-
{formatClockTime(
|
|
924
|
+
{formatClockTime(footerTimestamp)}
|
|
774
925
|
</span>
|
|
775
926
|
</Tooltip>
|
|
776
927
|
</div>
|
|
777
928
|
)}
|
|
778
|
-
|
|
929
|
+
|
|
930
|
+
{followUpBlocks.map((block) => (
|
|
931
|
+
<SuggestionsBlock
|
|
932
|
+
key={block.key}
|
|
933
|
+
items={block.items}
|
|
934
|
+
onSuggestion={(text) => onSuggestion?.(text)}
|
|
935
|
+
/>
|
|
936
|
+
))}
|
|
779
937
|
</div>
|
|
780
938
|
);
|
|
781
939
|
})}
|
|
@@ -804,19 +962,46 @@ export function CloudOsChatMessages({
|
|
|
804
962
|
|
|
805
963
|
<div ref={endRef} />
|
|
806
964
|
</div>
|
|
965
|
+
</div>
|
|
966
|
+
|
|
967
|
+
<WorkshopIconButton
|
|
968
|
+
aria-label="Scroll to bottom"
|
|
969
|
+
onClick={() => endRef.current?.scrollIntoView({ behavior: "smooth" })}
|
|
970
|
+
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 ${
|
|
971
|
+
showScrollButton
|
|
972
|
+
? "translate-y-0 opacity-100"
|
|
973
|
+
: "pointer-events-none translate-y-2 opacity-0"
|
|
974
|
+
}`}
|
|
975
|
+
>
|
|
976
|
+
<ArrowDown size={15} />
|
|
977
|
+
</WorkshopIconButton>
|
|
807
978
|
</div>
|
|
808
979
|
|
|
809
|
-
<
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
:
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
980
|
+
<AnimatePresence initial={false}>
|
|
981
|
+
{currentPlan && completedPlanSteps < currentPlan.steps.length && (
|
|
982
|
+
<motion.div
|
|
983
|
+
key="task-progress"
|
|
984
|
+
initial={reducedMotion ? { opacity: 1 } : { opacity: 0, y: 8, scale: 0.98 }}
|
|
985
|
+
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
986
|
+
exit={reducedMotion ? { opacity: 0 } : { opacity: 0, y: 8, scale: 0.98 }}
|
|
987
|
+
transition={{ duration: reducedMotion ? 0 : 0.2, ease: [0.32, 0.72, 0, 1] }}
|
|
988
|
+
className="relative z-10 mx-auto w-full max-w-3xl shrink-0 px-4"
|
|
989
|
+
>
|
|
990
|
+
<section
|
|
991
|
+
data-cloud-os-plan-progress=""
|
|
992
|
+
aria-label="Task progress"
|
|
993
|
+
className="mx-auto border border-b-0 border-kumo-line bg-kumo-base px-4 py-2.5"
|
|
994
|
+
style={{ width: "90%", borderRadius: "1rem 1rem 0 0" }}
|
|
995
|
+
>
|
|
996
|
+
<PlanBlock
|
|
997
|
+
steps={currentPlan.steps}
|
|
998
|
+
running={isActive}
|
|
999
|
+
defaultOpen={false}
|
|
1000
|
+
/>
|
|
1001
|
+
</section>
|
|
1002
|
+
</motion.div>
|
|
1003
|
+
)}
|
|
1004
|
+
</AnimatePresence>
|
|
820
1005
|
|
|
821
1006
|
</div>
|
|
822
1007
|
);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ComponentProps } from "react";
|
|
4
|
+
import { Loader2Icon, PlayIcon } from "lucide-react";
|
|
5
|
+
import { cn } from "../internal/cn";
|
|
6
|
+
import { codeScroll, codeSurface, ghostButton, mono, paper } from "./surfaces";
|
|
7
|
+
|
|
8
|
+
export type RunState = "idle" | "running" | "ok" | "error";
|
|
9
|
+
|
|
10
|
+
export function CodeRunner({
|
|
11
|
+
language,
|
|
12
|
+
code,
|
|
13
|
+
state,
|
|
14
|
+
output,
|
|
15
|
+
durationMs,
|
|
16
|
+
onRun,
|
|
17
|
+
className,
|
|
18
|
+
...props
|
|
19
|
+
}: Omit<
|
|
20
|
+
ComponentProps<"div">,
|
|
21
|
+
"children" | "language" | "code" | "state" | "output" | "durationMs" | "onRun"
|
|
22
|
+
> & {
|
|
23
|
+
language: string;
|
|
24
|
+
code: string;
|
|
25
|
+
state: RunState;
|
|
26
|
+
output: readonly string[];
|
|
27
|
+
durationMs?: number;
|
|
28
|
+
onRun?: () => void;
|
|
29
|
+
}) {
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
data-slot="code-runner"
|
|
33
|
+
className={cn(
|
|
34
|
+
paper,
|
|
35
|
+
"flex w-full max-w-md flex-col overflow-hidden rounded-2xl",
|
|
36
|
+
className,
|
|
37
|
+
)}
|
|
38
|
+
{...props}
|
|
39
|
+
>
|
|
40
|
+
<div className="flex items-center gap-2 px-3.5 py-2">
|
|
41
|
+
<span className={cn(mono, "text-foreground/35 min-w-0 flex-1")}>
|
|
42
|
+
{language}
|
|
43
|
+
</span>
|
|
44
|
+
{durationMs !== undefined && state !== "running" && (
|
|
45
|
+
<span
|
|
46
|
+
className={cn(mono, "text-foreground/30 shrink-0 tabular-nums")}
|
|
47
|
+
>
|
|
48
|
+
{durationMs}ms
|
|
49
|
+
</span>
|
|
50
|
+
)}
|
|
51
|
+
{onRun && (
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
aria-label="Run this snippet"
|
|
55
|
+
onClick={onRun}
|
|
56
|
+
disabled={state === "running"}
|
|
57
|
+
className={cn(
|
|
58
|
+
ghostButton,
|
|
59
|
+
"size-7 shrink-0 disabled:pointer-events-none",
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
62
|
+
{state === "running" ? (
|
|
63
|
+
<Loader2Icon className="size-3.5 animate-spin motion-reduce:animate-none" />
|
|
64
|
+
) : (
|
|
65
|
+
<PlayIcon className="size-3.5 translate-x-px" />
|
|
66
|
+
)}
|
|
67
|
+
</button>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<pre className={cn(codeScroll, "border-foreground/[0.07] border-t px-3.5 py-2.5 font-mono text-xs leading-relaxed")}>
|
|
72
|
+
<code className="text-foreground/75">{code}</code>
|
|
73
|
+
</pre>
|
|
74
|
+
|
|
75
|
+
{state !== "idle" && (
|
|
76
|
+
<div className="border-foreground/[0.07] fade-in animate-in flex flex-col border-t px-3.5 py-2.5 duration-300">
|
|
77
|
+
<span className={cn(mono, "text-foreground/30 pb-1")}>output</span>
|
|
78
|
+
<div className={codeScroll}>
|
|
79
|
+
<div className={cn(codeSurface, "flex flex-col")}>
|
|
80
|
+
{output.map((line, index) => (
|
|
81
|
+
<span
|
|
82
|
+
key={`${index}-${line}`}
|
|
83
|
+
className={cn(
|
|
84
|
+
"fade-in animate-in fill-mode-both font-mono text-xs leading-relaxed whitespace-pre",
|
|
85
|
+
state === "error"
|
|
86
|
+
? "text-red-600 dark:text-red-400"
|
|
87
|
+
: "text-foreground/70",
|
|
88
|
+
)}
|
|
89
|
+
style={{ animationDelay: `${index * 80}ms` }}
|
|
90
|
+
>
|
|
91
|
+
{line}
|
|
92
|
+
</span>
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
{state === "running" && (
|
|
97
|
+
<span
|
|
98
|
+
aria-hidden
|
|
99
|
+
className="bg-foreground/40 h-[1em] w-[2px] animate-pulse rounded-full motion-reduce:animate-none"
|
|
100
|
+
/>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function clamp(value: number, min: number, max: number) {
|
|
2
|
+
if (Number.isNaN(value)) return min;
|
|
3
|
+
return Math.min(max, Math.max(min, value));
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function take<T>(items: readonly T[], count: number) {
|
|
7
|
+
return items.slice(0, Math.floor(clamp(count, 0, items.length)));
|
|
8
|
+
}
|