@springbrand/message-panel 0.1.3-alpha.13 → 0.1.3-alpha.14

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.
@@ -0,0 +1,3 @@
1
+ <svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="11.6667" height="8.33333" viewBox="0 0 11.6667 8.33333" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path id="Vector (Stroke)" d="M3.92575 0.159107C4.14614 -0.0530669 4.50354 -0.0530047 4.72403 0.159107C4.94441 0.371284 4.94435 0.715363 4.72403 0.927642L1.92639 3.62317H11.1021C11.4139 3.62317 11.6667 3.8665 11.6667 4.16667C11.6667 4.46683 11.4139 4.71016 11.1021 4.71016H1.92639L4.72403 7.40569C4.94435 7.61797 4.94441 7.96205 4.72403 8.17423C4.50354 8.38634 4.14614 8.3864 3.92575 8.17423L0.165189 4.55093C-0.0550631 4.33872 -0.0550631 3.99461 0.165189 3.7824L3.92575 0.159107Z" fill="black" fill-opacity="0.5"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="21.8891" height="21.9254" viewBox="0 0 21.8891 21.9254" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path id="Vector" d="M11.6017 6.11877H0.752769C0.337056 6.11877 3.87367e-05 5.78171 3.87367e-05 5.36611L0 0.849598C0 0.433872 0.337004 0.0968018 0.75273 0.0968018H11.5558L15.6704 0.00163975C19.0193 -0.0757287 21.7878 2.59451 21.8311 5.94405L21.8891 10.4301V21.1726C21.8891 21.5883 21.552 21.9254 21.1363 21.9254H16.6199C16.2042 21.9254 15.8671 21.5883 15.8671 21.1726V10.3695L15.7581 7.55744C15.7267 6.74739 15.0595 6.10781 14.249 6.11039L11.6017 6.11877Z" fill="#FF9938"/>
3
+ </svg>
@@ -0,0 +1,16 @@
1
+ <svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="37.5518" height="37.4985" viewBox="0 0 37.5518 37.4985" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <g id="Group" filter="url(#filter0_f_0_4)">
3
+ <path id="Vector" d="M31.8173 8.87482C32.1113 9.16882 32.1113 9.64541 31.8173 9.93941L24.16 17.5967H35.009C35.4247 17.5967 35.7616 17.9338 35.7616 18.3494L35.7618 22.8659C35.7618 23.2816 35.4247 23.6185 35.009 23.6185H24.2059L31.8173 31.2299C32.1113 31.5239 32.1113 32.0005 31.8173 32.2945L28.6236 35.488C28.3298 35.782 27.8532 35.782 27.5592 35.488L22.4352 30.3641C22.2939 30.2227 22.2146 30.0309 22.2148 29.831L22.216 28.8867L22.1335 21.8329C22.0894 18.0581 18.9682 15.0475 15.1943 15.1395L8.62867 15.2995L7.68452 15.3007C7.48456 15.3009 7.29271 15.2216 7.15132 15.0802L2.0105 9.93941C1.71653 9.64541 1.71653 9.16882 2.0105 8.87482L5.20408 5.68127C5.49804 5.38731 5.97465 5.38731 6.26861 5.68127L13.8727 13.2853V2.54284C13.8727 2.12712 14.2096 1.79009 14.6254 1.79008L19.1417 1.79002C19.5575 1.79002 19.8945 2.12707 19.8945 2.54279V13.3458L27.5592 5.68127C27.8532 5.38731 28.3298 5.38731 28.6236 5.68127L31.8173 8.87482Z" fill="url(#paint0_linear_0_4)"/>
4
+ </g>
5
+ <defs>
6
+ <filter id="filter0_f_0_4" x="-1.86265e-09" y="0" width="37.5518" height="37.4985" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
7
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
8
+ <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
9
+ <feGaussianBlur stdDeviation="0.895012" result="effect1_foregroundBlur_0_4"/>
10
+ </filter>
11
+ <linearGradient id="paint0_linear_0_4" x1="30.2205" y1="7.27806" x2="22.5471" y2="31.3091" gradientUnits="userSpaceOnUse">
12
+ <stop stop-color="#FFD078"/>
13
+ <stop offset="1" stop-color="#FF9938"/>
14
+ </linearGradient>
15
+ </defs>
16
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="9.81853" height="9.8186" viewBox="0 0 9.81853 9.8186" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path id="Vector" d="M4.67857 0.881903C5.85442 -0.293968 7.76085 -0.293968 8.93672 0.881903C10.1125 2.05777 10.1125 3.96426 8.93672 5.14L4.25811 9.8186L0 5.5605L4.67857 0.881903Z" fill="#FF9938"/>
3
+ </svg>
@@ -56,7 +56,6 @@ export interface CloudOsChatMessagesProps {
56
56
  status: CloudOsChatStatus;
57
57
  /** 权威 Turn 生命周期;提供时替代本地 status 推断。 */
58
58
  turnActive?: boolean;
59
- isHydrating?: boolean;
60
59
  isRecovering?: boolean;
61
60
  recoveryStatusLabel?: string;
62
61
  awaitingApproval?: boolean;
@@ -173,7 +172,7 @@ function UserBubble({
173
172
  </div>
174
173
  )}
175
174
  {entry.text && (
176
- <div className="themed-user-bubble-shadow cos-markdown cos-user-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">
175
+ <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">
177
176
  <MarkdownMessage message={entry.text} resolveUrl={resolveUrl} />
178
177
  </div>
179
178
  )}
@@ -205,7 +204,6 @@ export function CloudOsChatMessages({
205
204
  messages,
206
205
  status,
207
206
  turnActive,
208
- isHydrating = false,
209
207
  isRecovering = false,
210
208
  recoveryStatusLabel,
211
209
  awaitingApproval = false,
@@ -308,7 +306,7 @@ export function CloudOsChatMessages({
308
306
  className="relative flex min-h-0 flex-1 flex-col overflow-hidden bg-kumo-base"
309
307
  data-cloud-os-transcript=""
310
308
  data-status={status}
311
- aria-busy={isActive || isHydrating || isRecovering}
309
+ aria-busy={isActive || isRecovering}
312
310
  >
313
311
  <div
314
312
  ref={scrollRef}
@@ -323,7 +321,7 @@ export function CloudOsChatMessages({
323
321
  constrainWidth ? "mx-auto w-full max-w-[920px]" : ""
324
322
  }`}
325
323
  >
326
- {entries.length === 0 && !startedWithPending && !isHydrating && (
324
+ {entries.length === 0 && !startedWithPending && (
327
325
  <div className="flex min-h-56 items-center justify-center text-center text-[24px] leading-8 font-semibold tracking-[-0.4px] text-kumo-default">
328
326
  {emptyTitle}
329
327
  </div>
@@ -349,7 +347,7 @@ export function CloudOsChatMessages({
349
347
  if (entry.type === "slashCommand") {
350
348
  return (
351
349
  <div key={entry.key} className={`${topClass} flex flex-col items-end`}>
352
- <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">
350
+ <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">
353
351
  <span className="whitespace-pre-wrap font-mono">{entry.text}</span>
354
352
  </div>
355
353
  </div>
@@ -440,7 +438,11 @@ export function CloudOsChatMessages({
440
438
  switch (block.kind) {
441
439
  case "reasoning":
442
440
  return (
443
- <ThinkingTraceRow key={block.key} reasoning={block.text} />
441
+ <ThinkingTraceRow
442
+ key={block.key}
443
+ reasoning={block.text}
444
+ running={block.running}
445
+ />
444
446
  );
445
447
  case "text":
446
448
  return (
@@ -653,16 +655,6 @@ export function CloudOsChatMessages({
653
655
  <ArrowDown size={15} />
654
656
  </WorkshopIconButton>
655
657
 
656
- {isHydrating && (
657
- <div className="absolute inset-0 z-30 grid place-items-center bg-kumo-base/90">
658
- <div className="flex flex-col items-center gap-3">
659
- <div className="h-6 w-6 animate-spin rounded-full border-2 border-kumo-brand border-t-transparent" />
660
- <p className="m-0 text-[13px] leading-[18px] text-kumo-subtle">
661
- Loading conversation…
662
- </p>
663
- </div>
664
- </div>
665
- )}
666
658
  </div>
667
659
  );
668
660
  }
@@ -452,23 +452,26 @@ export function SuggestionsBlock({
452
452
  onSuggestion: (text: string) => void;
453
453
  }) {
454
454
  return (
455
- <div className="max-w-[860px] space-y-2 pt-1">
456
- <div className="flex items-center gap-2 px-1.5 text-[13px] leading-[18px] text-kumo-success">
457
- <Check size={14} weight="bold" />
458
- <span>Task complete</span>
459
- </div>
460
- <div className="flex flex-wrap gap-1.5 px-1.5">
461
- {items.map((item) => (
462
- <button
463
- key={item}
464
- type="button"
465
- onClick={() => onSuggestion(item)}
466
- className="inline-flex h-7 cursor-pointer items-center rounded-full border border-kumo-line bg-kumo-base px-3 text-[13px] leading-none tracking-[-0.25px] text-kumo-subtle transition-colors duration-150 ease-out hover:bg-kumo-tint hover:text-kumo-default focus-visible:outline-none active:scale-[0.98]"
467
- >
468
- {item}
469
- </button>
470
- ))}
471
- </div>
455
+ <div className="flex max-w-[860px] flex-col items-start gap-3">
456
+ {items.map((item) => (
457
+ <button
458
+ key={item}
459
+ type="button"
460
+ onClick={() => onSuggestion(item)}
461
+ className="inline-flex cursor-pointer items-center gap-2 text-[14px] leading-[1.4] text-kumo-inactive transition-colors duration-150 ease-out hover:text-kumo-brand focus-visible:text-kumo-brand focus-visible:outline-none active:scale-[0.98]"
462
+ >
463
+ <span className="grid size-5 flex-shrink-0 place-items-center" aria-hidden="true">
464
+ <span className="rotate-180 -scale-y-100">
465
+ <img
466
+ src={new URL("../assets/followup-arrow.svg", import.meta.url).href}
467
+ alt=""
468
+ className="h-[8.33333px] w-[11.6667px]"
469
+ />
470
+ </span>
471
+ </span>
472
+ <span className="whitespace-nowrap">{item}</span>
473
+ </button>
474
+ ))}
472
475
  </div>
473
476
  );
474
477
  }
@@ -513,7 +516,7 @@ function scheduleCadence(input: Record<string, unknown>): string {
513
516
  return timezone ? `${cadence} · ${timezone}` : cadence;
514
517
  }
515
518
 
516
- /** `schedule` 工具的 details 是 `{ scheduled: true, id }`,id 即任务在 SchedulerAgent 里的主键。 */
519
+ /** `schedule` 工具的 details 是 `{ scheduled: true, id }`,id Inbox Schedule Definition 主键。 */
517
520
  function scheduleIdOf(output: unknown): string {
518
521
  if (typeof output !== "object" || output === null) return "";
519
522
  const id = (output as Record<string, unknown>).id;
@@ -539,7 +542,7 @@ export function ScheduleConfirmation({
539
542
  return (
540
543
  <section
541
544
  data-slot="schedule-card"
542
- aria-label={`确认定时任务:${name}`}
545
+ aria-label={`Confirm scheduled task: ${name}`}
543
546
  className="themed-surface-inset flex w-full max-w-sm flex-col gap-3 rounded-2xl border border-kumo-line/70 bg-kumo-elevated p-4 text-kumo-default shadow-[0_1px_2px_rgba(0,0,0,0.04),0_12px_32px_-16px_rgba(0,0,0,0.12)]"
544
547
  >
545
548
  <div className="flex items-center gap-2.5">
@@ -570,10 +573,10 @@ export function ScheduleConfirmation({
570
573
 
571
574
  <div className="flex items-center justify-end gap-2">
572
575
  <WorkshopButton disabled={disabled} onClick={onCancel}>
573
- 取消
576
+ Cancel
574
577
  </WorkshopButton>
575
578
  <WorkshopButton tone="primary" disabled={disabled} onClick={onConfirm}>
576
- {disabled ? "正在确认…" : "确认安排"}
579
+ {disabled ? "Confirming…" : "Confirm schedule"}
577
580
  </WorkshopButton>
578
581
  </div>
579
582
  </section>
@@ -677,22 +680,22 @@ export function PermissionGrant({
677
680
  "aria-label"?: string;
678
681
  }) {
679
682
  const actionClassName =
680
- "inline-flex h-8 items-center rounded-full px-3 text-[12px] leading-none font-medium tracking-[-0.25px] transition-[background-color,color,opacity,transform] duration-150 ease-out active:scale-[0.96] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-kumo-ring/40 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100";
683
+ "inline-flex h-10 w-[120px] items-center justify-center rounded-lg border border-kumo-line px-4 text-[14px] leading-[1.4] font-medium transition-[background-color,color,opacity,transform] duration-150 ease-out active:scale-[0.96] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-kumo-ring/40 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100";
681
684
 
682
685
  return (
683
686
  <section
684
687
  aria-label={ariaLabel}
685
- className="themed-surface-inset w-full max-w-sm rounded-[20px] border border-kumo-line bg-kumo-elevated p-4 text-kumo-default shadow-sm"
688
+ className="themed-surface-inset w-full rounded-xl border border-kumo-line bg-kumo-elevated p-4 text-kumo-default"
686
689
  >
687
- <div className="flex items-center gap-2.5">
690
+ <div className="flex items-center gap-2">
688
691
  <span
689
- className="flex size-7 shrink-0 items-center justify-center rounded-lg bg-kumo-fill/70 text-kumo-subtle"
692
+ className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-kumo-fill text-kumo-subtle"
690
693
  aria-hidden="true"
691
694
  >
692
695
  <Key size={14} weight="bold" />
693
696
  </span>
694
697
  <div className="flex min-w-0 flex-1 flex-col">
695
- <span className="truncate text-[13.5px] leading-[18px] font-medium">
698
+ <span className="truncate text-[14px] leading-[1.4] font-medium">
696
699
  {capability}
697
700
  </span>
698
701
  <span className="truncate text-[12px] leading-4 text-kumo-inactive">
@@ -701,41 +704,22 @@ export function PermissionGrant({
701
704
  </div>
702
705
  </div>
703
706
 
704
- <div className="mt-3.5 flex max-h-[200px] flex-col gap-1 overflow-y-auto pr-1">
705
- <span className="font-mono text-[10px] leading-4 text-kumo-inactive">
707
+ <div className="mt-4 flex max-h-[200px] flex-col gap-2 overflow-y-auto">
708
+ <span className="text-[12px] leading-4 text-kumo-inactive">
706
709
  this grants
707
710
  </span>
708
711
  {reach.map((item, index) => (
709
712
  <span
710
713
  key={`${index}:${item}`}
711
- className="flex items-baseline gap-2 text-[12px] leading-[17px] text-kumo-subtle"
714
+ className="flex items-baseline gap-2 text-[14px] leading-[1.4] text-kumo-subtle"
712
715
  >
713
- <span
714
- className="size-1 shrink-0 rounded-full bg-kumo-interact"
715
- aria-hidden="true"
716
- />
716
+ <span aria-hidden="true">•</span>
717
717
  <span className="min-w-0 break-words whitespace-pre-wrap">{item}</span>
718
718
  </span>
719
719
  ))}
720
720
  </div>
721
721
 
722
- <div className="mt-3.5 flex min-h-8 flex-wrap items-center justify-end gap-1">
723
- <button
724
- type="button"
725
- disabled={disabled}
726
- onClick={() => onGrant("deny")}
727
- className={`${actionClassName} text-kumo-subtle enabled:hover:bg-kumo-fill enabled:hover:text-kumo-default`}
728
- >
729
- Deny
730
- </button>
731
- <button
732
- type="button"
733
- disabled={disabled}
734
- onClick={() => onGrant("allow_once")}
735
- className={`${actionClassName} text-kumo-subtle enabled:hover:bg-kumo-fill enabled:hover:text-kumo-default`}
736
- >
737
- Allow once
738
- </button>
722
+ <div className="mt-4 flex flex-wrap items-center gap-2">
739
723
  <Tooltip
740
724
  content="Allow every action at this execution level, without future prompts."
741
725
  asChild
@@ -744,11 +728,27 @@ export function PermissionGrant({
744
728
  type="button"
745
729
  disabled={disabled}
746
730
  onClick={() => onGrant("allow_level")}
747
- className={`${actionClassName} bg-kumo-contrast text-kumo-inverse enabled:hover:bg-kumo-strong`}
731
+ className={`${actionClassName} border-0 bg-[linear-gradient(90deg,#ffd077_0%,#ff9938_100%)] text-kumo-inverse enabled:hover:brightness-95`}
748
732
  >
749
733
  Allow level
750
734
  </button>
751
735
  </Tooltip>
736
+ <button
737
+ type="button"
738
+ disabled={disabled}
739
+ onClick={() => onGrant("allow_once")}
740
+ className={`${actionClassName} text-kumo-subtle enabled:hover:bg-kumo-fill enabled:hover:text-kumo-default`}
741
+ >
742
+ Allow once
743
+ </button>
744
+ <button
745
+ type="button"
746
+ disabled={disabled}
747
+ onClick={() => onGrant("deny")}
748
+ className={`${actionClassName} text-kumo-subtle enabled:hover:bg-kumo-fill enabled:hover:text-kumo-default`}
749
+ >
750
+ Deny
751
+ </button>
752
752
  </div>
753
753
  </section>
754
754
  );
@@ -1,11 +1,10 @@
1
- import { CaretRight } from "@phosphor-icons/react";
2
- import { memo } from "react";
1
+ import { Atom, CaretDown, CaretRight, Copy } from "@phosphor-icons/react";
2
+ import { memo, useEffect, useId, useState } from "react";
3
3
  import { MarkdownMessage } from "./markdown-message";
4
4
  import {
5
5
  getToolCallSummary,
6
6
  getToolIcon,
7
7
  normalizeOutputText,
8
- safeStringify,
9
8
  type CloudOsToolCall,
10
9
  type PhosphorIcon,
11
10
  type ToolCallGroup,
@@ -22,19 +21,42 @@ export function WorkIcon({ Icon }: { Icon: PhosphorIcon }) {
22
21
  return <Icon size={15} className="text-kumo-inactive" />;
23
22
  }
24
23
 
25
- function OutputPre({ label, value }: { label?: string; value: string }) {
26
- if (!value) return null;
24
+ function displayOutput(value: unknown, fallback: string): string {
25
+ if (value == null || typeof value !== "object" || Array.isArray(value)) {
26
+ return fallback;
27
+ }
28
+ const record = value as Record<string, unknown>;
29
+ if (typeof record.summary === "string" && record.summary.trim()) {
30
+ return record.summary.trim();
31
+ }
32
+ const displayValue = (item: unknown): string =>
33
+ item != null && typeof item === "object"
34
+ ? Object.entries(item).map(([key, child]) => `${key}: ${displayValue(child)}`).join("\n")
35
+ : String(item ?? "");
36
+ return displayValue(record);
37
+ }
38
+
39
+ function OutputPre({ value }: { value: string }) {
27
40
  return (
28
- <>
29
- {label && (
30
- <span className="font-mono text-[11px] leading-4 text-kumo-inactive uppercase tracking-[0.08em]">
31
- {label}
32
- </span>
33
- )}
34
- <pre className="max-h-56 overflow-auto rounded-xl border border-kumo-line/70 bg-kumo-base p-3 font-mono text-[12px] leading-[18px] text-kumo-subtle whitespace-pre-wrap">
41
+ <div
42
+ className="rounded-xl border border-kumo-line/70 bg-kumo-elevated/45 px-4 py-3 text-kumo-subtle"
43
+ data-output-card=""
44
+ >
45
+ <div className="flex items-center justify-between gap-3 text-[14px] font-medium leading-5">
46
+ <span>Output</span>
47
+ <button
48
+ type="button"
49
+ onClick={() => navigator.clipboard.writeText(value)}
50
+ className="flex h-4 w-4 items-center justify-center text-kumo-subtle transition-colors hover:text-kumo-default focus-visible:outline-none focus-visible:text-kumo-default"
51
+ aria-label="Copy output"
52
+ >
53
+ <Copy size={16} />
54
+ </button>
55
+ </div>
56
+ <pre className="mt-2 max-h-56 overflow-auto border-t border-kumo-line/70 pt-3 font-sans text-[14px] font-normal leading-5 whitespace-pre-wrap">
35
57
  {value}
36
58
  </pre>
37
- </>
59
+ </div>
38
60
  );
39
61
  }
40
62
 
@@ -43,14 +65,7 @@ export const ToolCallDetails = memo(function ToolCallDetails({
43
65
  }: {
44
66
  toolCall: CloudOsToolCall;
45
67
  }) {
46
- const code =
47
- typeof tc.input.code === "string"
48
- ? tc.input.code
49
- : typeof tc.input.command === "string"
50
- ? tc.input.command
51
- : "";
52
- const inputText =
53
- Object.keys(tc.input).length > 0 ? safeStringify(tc.input) : "";
68
+ const output = displayOutput(tc.output, tc.outputText);
54
69
  return (
55
70
  <div className="space-y-2">
56
71
  {tc.errorText && (
@@ -58,18 +73,8 @@ export const ToolCallDetails = memo(function ToolCallDetails({
58
73
  {tc.errorText}
59
74
  </pre>
60
75
  )}
61
- {code ? (
62
- <>
63
- <OutputPre label="Code" value={code} />
64
- <OutputPre label="Output" value={tc.outputText} />
65
- </>
66
- ) : (
67
- <>
68
- <OutputPre value={inputText} />
69
- {tc.outputText && <OutputPre label="Output" value={tc.outputText} />}
70
- </>
71
- )}
72
- {!code && !inputText && !tc.outputText && !tc.errorText && (
76
+ {output && <OutputPre value={output} />}
77
+ {!output && !tc.errorText && (
73
78
  <p className="m-0 text-[12px] leading-4 text-kumo-inactive">
74
79
  No additional data
75
80
  </p>
@@ -118,7 +123,7 @@ export const NestedToolCallRow = memo(function NestedToolCallRow({
118
123
  </span>
119
124
  </button>
120
125
  {open && (
121
- <div className="themed-surface-inset ml-8 mt-1 space-y-3 rounded-2xl border border-kumo-line/70 bg-kumo-elevated/45 p-3">
126
+ <div className="ml-8 mt-1 space-y-3">
122
127
  <ToolCallDetails toolCall={tc} />
123
128
  </div>
124
129
  )}
@@ -128,14 +133,52 @@ export const NestedToolCallRow = memo(function NestedToolCallRow({
128
133
 
129
134
  export const ThinkingTraceRow = memo(function ThinkingTraceRow({
130
135
  reasoning,
136
+ running,
131
137
  }: {
132
138
  reasoning: string;
139
+ running: boolean;
133
140
  }) {
141
+ const [open, setOpen] = useState(running);
142
+ const contentId = useId();
143
+ const summary = reasoning.replace(/\s+/g, " ").trim();
144
+
145
+ useEffect(() => setOpen(running), [running]);
146
+
134
147
  return (
135
- <div className="min-w-0 py-1 text-kumo-subtle">
136
- <div className="cos-markdown min-w-0 text-[13px] leading-[19px]">
137
- <MarkdownMessage message={reasoning} />
138
- </div>
148
+ <div
149
+ className="flex min-w-0 flex-col items-start gap-2"
150
+ data-thinking-trace=""
151
+ data-state={open ? "open" : "closed"}
152
+ >
153
+ <button
154
+ type="button"
155
+ onClick={() => setOpen((current) => !current)}
156
+ className="group -ml-0.5 flex w-full min-w-0 items-center gap-1.5 rounded-xl px-1.5 py-1 text-left text-[14px] leading-5 text-kumo-inactive transition-colors hover:text-kumo-default focus-visible:text-kumo-default focus-visible:outline-none"
157
+ aria-expanded={open}
158
+ aria-controls={contentId}
159
+ >
160
+ <span className="relative grid size-4 flex-shrink-0 place-items-center" aria-hidden="true">
161
+ <Atom
162
+ size={14}
163
+ className="transition-opacity group-hover:opacity-0 group-focus-visible:opacity-0"
164
+ />
165
+ <CaretDown
166
+ size={14}
167
+ className={`absolute opacity-0 transition-[opacity,transform] group-hover:opacity-100 group-focus-visible:opacity-100 ${open ? "" : "-rotate-90"}`}
168
+ />
169
+ </span>
170
+ <span className="flex-shrink-0 text-kumo-subtle">Think</span>
171
+ <span aria-hidden="true">·</span>
172
+ <span className="min-w-0 flex-1 truncate">{summary}</span>
173
+ </button>
174
+ {open && (
175
+ <div
176
+ id={contentId}
177
+ className="cos-markdown w-full rounded-xl border border-kumo-line bg-kumo-elevated/45 px-4 py-3 text-[14px] leading-[1.4] text-kumo-default/70"
178
+ >
179
+ <MarkdownMessage message={reasoning} />
180
+ </div>
181
+ )}
139
182
  </div>
140
183
  );
141
184
  });
@@ -189,7 +232,7 @@ export const ToolGroupRow = memo(function ToolGroupRow({
189
232
  </button>
190
233
  {open &&
191
234
  (group.calls.length === 1 ? (
192
- <div className="themed-surface-inset ml-8 mt-1 space-y-3 rounded-2xl border border-kumo-line/70 bg-kumo-elevated/45 p-3">
235
+ <div className="ml-8 mt-1 space-y-3">
193
236
  <ToolCallDetails toolCall={group.calls[0]} />
194
237
  </div>
195
238
  ) : (
@@ -51,7 +51,12 @@ export interface PlanStep {
51
51
  }
52
52
 
53
53
  export type AssistantBlock =
54
- | { kind: "reasoning"; key: string; text: string }
54
+ | {
55
+ kind: "reasoning";
56
+ key: string;
57
+ text: string;
58
+ running: boolean;
59
+ }
55
60
  | { kind: "text"; key: string; text: string }
56
61
  | { kind: "toolGroup"; key: string; group: ToolCallGroup }
57
62
  | { kind: "plan"; key: string; steps: PlanStep[]; running: boolean }
@@ -326,7 +331,12 @@ function assistantBlocks(
326
331
  const text = String(record.text ?? "").trim();
327
332
  if (!text || !showThinkingTraces) return;
328
333
  flush();
329
- blocks.push({ kind: "reasoning", key, text });
334
+ blocks.push({
335
+ kind: "reasoning",
336
+ key,
337
+ text,
338
+ running: record.state === "streaming",
339
+ });
330
340
  return;
331
341
  }
332
342
 
package/cloud-os/index.ts CHANGED
@@ -21,7 +21,10 @@ export type {
21
21
  FileViewProps,
22
22
  FileViewStatus,
23
23
  } from "./file-view";
24
- export { CloudOsWorkspaceSplit } from "./layout/cloud-os-workspace-split";
24
+ export {
25
+ CloudOsConversationLoading,
26
+ CloudOsWorkspaceSplit,
27
+ } from "./layout/cloud-os-workspace-split";
25
28
  export type { CloudOsWorkspaceSplitProps } from "./layout/cloud-os-workspace-split";
26
29
 
27
30
  export { CloudOsChatMessages } from "./chat/cloud-os-chat-messages";
@@ -6,6 +6,7 @@ import {
6
6
  type PointerEvent as ReactPointerEvent,
7
7
  type ReactNode,
8
8
  } from "react";
9
+ import { motion, useReducedMotion } from "motion/react";
9
10
 
10
11
  /**
11
12
  * 左聊天 / 右工作区的双栏骨架。从 cloudflare-os-main `GadgetEditor.tsx` 的 BODY
@@ -24,6 +25,56 @@ const WORKSPACE_TRANSITION_MS = 200;
24
25
 
25
26
  const isBrowser = typeof window !== "undefined";
26
27
 
28
+ const loadingAssets = {
29
+ spark: new URL("../assets/loading-spark.svg", import.meta.url).href,
30
+ corner: new URL("../assets/loading-corner.svg", import.meta.url).href,
31
+ mark: new URL("../assets/loading-mark.svg", import.meta.url).href,
32
+ };
33
+
34
+ const loadingSpring = (t: number) =>
35
+ 1 - Math.exp(-t * 7.6657) * (Math.cos(t * 6.7605) + 1.1339 * Math.sin(t * 6.7605));
36
+
37
+ export function CloudOsConversationLoading() {
38
+ const reducedMotion = useReducedMotion();
39
+ const repeat = reducedMotion ? 0 : Infinity;
40
+
41
+ return (
42
+ <div className="absolute inset-0 z-30 grid place-items-center bg-kumo-base/90">
43
+ <div role="status" aria-label="Loading conversation..." className="flex flex-col items-center gap-4">
44
+ <div className="relative h-[49px] w-[49px] overflow-hidden" aria-hidden="true">
45
+ <motion.img
46
+ src={loadingAssets.spark}
47
+ alt=""
48
+ className="absolute bottom-[18.5%] right-[61.46%] h-[20.04%] w-[20.04%] will-change-transform"
49
+ initial={reducedMotion ? false : { opacity: 0, rotate: 200, scaleX: 0.5, scaleY: 0.5, x: -140, y: 120 }}
50
+ animate={reducedMotion ? { opacity: 1, rotate: 0, scaleX: 1, scaleY: 1, x: 0, y: 0 } : { opacity: [0, 0, 1, 1, 0, 0], rotate: [200, 200, 10, 0, 0, 200, 200], scaleX: [0.5, 0.5, 1.3, 0.8, 1, 1], scaleY: [0.5, 0.5, 0.75, 1.2, 1, 1], x: [-140, -140, 2, 0, 0, -140, -140], y: [120, 120, -2, 0, 0, 120, 120] }}
51
+ transition={{ opacity: { duration: 1.68, times: [0, 0.2083, 0.25, 0.9998, 0.9999, 1], ease: ["linear", "linear", "linear", "easeIn", "linear"], repeat }, rotate: { duration: 1.68, times: [0, 0.2083, 0.3869, 0.506, 0.9998, 0.9999, 1], ease: ["linear", [0, 0, 0.15, 1], loadingSpring, "linear", "easeIn", "linear"], repeat }, scaleX: { duration: 1.68, times: [0, 0.2083, 0.3274, 0.3869, 0.506, 1], ease: ["linear", [0, 0, 0.15, 1], "easeOut", loadingSpring, "linear"], repeat }, scaleY: { duration: 1.68, times: [0, 0.2083, 0.3274, 0.3869, 0.506, 1], ease: ["linear", [0, 0, 0.15, 1], "easeOut", loadingSpring, "linear"], repeat }, x: { duration: 1.68, times: [0, 0.2083, 0.3869, 0.506, 0.9998, 0.9999, 1], ease: ["linear", [0, 0, 0.15, 1], loadingSpring, "linear", "easeIn", "linear"], repeat }, y: { duration: 1.68, times: [0, 0.2083, 0.3869, 0.506, 0.9998, 0.9999, 1], ease: ["linear", [0, 0, 0.15, 1], loadingSpring, "linear", "easeIn", "linear"], repeat } }}
52
+ />
53
+ <motion.img
54
+ src={loadingAssets.corner}
55
+ alt=""
56
+ className="absolute bottom-[11.65%] right-[43.79%] h-[44.74%] w-[44.67%] will-change-transform"
57
+ initial={reducedMotion ? false : { opacity: 0, scaleX: 1, scaleY: 1, x: 0, y: 0 }}
58
+ animate={reducedMotion ? { opacity: 1, scaleX: 1, scaleY: 1, x: 0, y: 0 } : { opacity: [0, 1, 1, 0, 0], scaleX: [1, 1, 1.06, 1, 1], scaleY: [1, 1, 0.95, 1, 1], x: [0, 0, 4, 0, 0], y: [0, 0, -2, 0, 0] }}
59
+ transition={{ opacity: { duration: 1.68, times: [0, 0.1786, 0.9998, 0.9999, 1], ease: [[0, 0, 0.3, 1], "linear", "easeIn", "linear"], repeat }, scaleX: { duration: 1.68, times: [0, 0.3988, 0.4345, 0.6548, 1], ease: ["linear", [0.2, 0.9, 0.3, 1], loadingSpring, "linear"], repeat }, scaleY: { duration: 1.68, times: [0, 0.3988, 0.4345, 0.6548, 1], ease: ["linear", [0.2, 0.9, 0.3, 1], loadingSpring, "linear"], repeat }, x: { duration: 1.68, times: [0, 0.3988, 0.4345, 0.6845, 1], ease: ["linear", [0.2, 0.9, 0.3, 1], loadingSpring, "linear"], repeat }, y: { duration: 1.68, times: [0, 0.3988, 0.4345, 0.6845, 1], ease: ["linear", [0.2, 0.9, 0.3, 1], loadingSpring, "linear"], repeat } }}
60
+ />
61
+ <motion.img
62
+ src={loadingAssets.mark}
63
+ alt=""
64
+ className="absolute bottom-[19.14%] right-[11.53%] h-[69.22%] w-[69.33%] will-change-transform"
65
+ initial={reducedMotion ? false : { opacity: 0, rotate: 0, scaleX: 0.85, scaleY: 0.85, x: 0, y: 0 }}
66
+ animate={reducedMotion ? { opacity: 1, rotate: 0, scaleX: 1, scaleY: 1, x: 0, y: 0 } : { opacity: [0, 1, 1, 0, 0], rotate: [0, 0, -3, 0, 0], scaleX: [0.85, 1, 1, 1.1, 0.95, 1, 1], scaleY: [0.85, 1, 1, 0.92, 1.05, 1, 1], x: [0, 0, 6, 0, 0], y: [0, 0, -4, 0, 0] }}
67
+ transition={{ opacity: { duration: 1.68, times: [0, 0.2083, 0.9998, 0.9999, 1], ease: [[0, 0, 0.3, 1], "linear", "easeIn", "linear"], repeat }, rotate: { duration: 1.68, times: [0, 0.3869, 0.4286, 0.6548, 1], ease: ["linear", [0.2, 0.9, 0.3, 1], loadingSpring, "linear"], repeat }, scaleX: { duration: 1.68, times: [0, 0.2083, 0.3869, 0.4167, 0.4643, 0.6548, 1], ease: [[0, 0, 0.3, 1], "linear", [0.2, 0.9, 0.3, 1], "easeOut", loadingSpring, "linear"], repeat }, scaleY: { duration: 1.68, times: [0, 0.2083, 0.3869, 0.4167, 0.4643, 0.6548, 1], ease: [[0, 0, 0.3, 1], "linear", [0.2, 0.9, 0.3, 1], "easeOut", loadingSpring, "linear"], repeat }, x: { duration: 1.68, times: [0, 0.3869, 0.4167, 0.6548, 1], ease: ["linear", [0.2, 0.9, 0.3, 1], loadingSpring, "linear"], repeat }, y: { duration: 1.68, times: [0, 0.3869, 0.4167, 0.6548, 1], ease: ["linear", [0.2, 0.9, 0.3, 1], loadingSpring, "linear"], repeat } }}
68
+ />
69
+ </div>
70
+ <p className="m-0 text-center text-[14px] font-normal leading-[1.4] text-kumo-default">
71
+ Loading conversation...
72
+ </p>
73
+ </div>
74
+ </div>
75
+ );
76
+ }
77
+
27
78
  function clampChatWidth(width: number, containerWidth?: number) {
28
79
  if (!isBrowser) {
29
80
  return Math.max(MIN_CHAT_WIDTH, Math.min(DEFAULT_CHAT_WIDTH, width));
@@ -62,6 +113,8 @@ export interface CloudOsWorkspaceSplitProps {
62
113
  /** 右栏收起时依然保留的窄边栏宽度(原文件的 Outputs rail)。 */
63
114
  railWidth?: number;
64
115
  rail?: ReactNode;
116
+ /** 覆盖整个聊天 + 工作区主体;页面级导航应放在分栏外。 */
117
+ isLoading?: boolean;
65
118
  }
66
119
 
67
120
  export function CloudOsWorkspaceSplit({
@@ -70,6 +123,7 @@ export function CloudOsWorkspaceSplit({
70
123
  workspaceOpen,
71
124
  railWidth = 0,
72
125
  rail,
126
+ isLoading = false,
73
127
  }: CloudOsWorkspaceSplitProps) {
74
128
  const containerRef = useRef<HTMLDivElement>(null);
75
129
  const resizeSessionRef = useRef<ResizeSession | null>(null);
@@ -184,6 +238,7 @@ export function CloudOsWorkspaceSplit({
184
238
  <div
185
239
  ref={containerRef}
186
240
  className="relative flex h-full min-h-0 flex-1 overflow-hidden bg-kumo-base"
241
+ aria-busy={isLoading}
187
242
  >
188
243
  {/* ── 左:聊天 ──────────────────────────────────────────────────────── */}
189
244
  <div
@@ -223,6 +278,8 @@ export function CloudOsWorkspaceSplit({
223
278
  </div>
224
279
 
225
280
  {rail}
281
+
282
+ {isLoading && <CloudOsConversationLoading />}
226
283
  </div>
227
284
  );
228
285
  }
@@ -11,7 +11,7 @@ const buttonBaseClassName =
11
11
 
12
12
  const buttonToneClassNames = {
13
13
  primary:
14
- "!h-9 bg-kumo-contrast px-3 text-kumo-inverse enabled:hover:bg-kumo-strong disabled:opacity-50",
14
+ "!h-9 bg-gradient-to-br from-[#FFD077] to-[#FF9938] px-3 text-white enabled:hover:opacity-90 disabled:opacity-50",
15
15
  secondary:
16
16
  "!h-8 border border-kumo-line bg-kumo-base px-3 text-kumo-default enabled:hover:bg-kumo-elevated disabled:opacity-40",
17
17
  danger:
@@ -72,7 +72,8 @@ export function CamelChatShowcase() {
72
72
  : {};
73
73
  return (
74
74
  record.type === "tool-write_file" ||
75
- record.type === "tool-cancel_schedule"
75
+ record.type === "tool-cancel_schedule" ||
76
+ record.type === "tool-schedule"
76
77
  ) &&
77
78
  record.state === "approval-requested"
78
79
  ? { id: "camel-demo-approval", approved: approval }
@@ -256,6 +257,14 @@ export function CamelChatShowcase() {
256
257
  }}
257
258
  onApprove={(_approvalId, decision) => {
258
259
  const approved = decision !== "deny";
260
+ if (scenario === "schedule_create") {
261
+ setApproval(approved);
262
+ selectScenario(approved ? "schedule" : "submitted");
263
+ setLastEvent(approved
264
+ ? "Scheduled task created"
265
+ : "Schedule creation cancelled");
266
+ return;
267
+ }
259
268
  if (scenario === "schedule_cancel") {
260
269
  setApproval(approved);
261
270
  setLastEvent(
@@ -37,6 +37,7 @@ export type DemoScenario =
37
37
  | "temporary_agent"
38
38
  | "agent_tools"
39
39
  | "background_agent"
40
+ | "schedule_create"
40
41
  | "schedule"
41
42
  | "schedule_cancel"
42
43
  | "approval"
@@ -118,9 +119,15 @@ export const scenarios: Record<DemoScenario, DemoScenarioMeta> = {
118
119
  status: "ready",
119
120
  flowStep: 3,
120
121
  },
122
+ schedule_create: {
123
+ label: "Create scheduled task",
124
+ description: "Review the task, cadence, and timezone before creating the schedule.",
125
+ status: "ready",
126
+ flowStep: 2,
127
+ },
121
128
  schedule: {
122
- label: "定时任务已创建",
123
- description: "schedule 执行成功后展示任务名称、触发规则、时区和已安排状态。",
129
+ label: "Scheduled task created",
130
+ description: "A completed schedule shows its task, cadence, timezone, and final status.",
124
131
  status: "ready",
125
132
  flowStep: 3,
126
133
  },
@@ -292,8 +299,26 @@ function askUserRequest(response?: DemoAskUserResponse): UIMessage {
292
299
  const planningWork = {
293
300
  id: "camel-demo-planning-work",
294
301
  role: "assistant",
295
- metadata: { turnStartedAt: demoTurnStartedAt + 3_000 },
302
+ metadata: {
303
+ turnStartedAt: demoTurnStartedAt + 3_000,
304
+ turnDurationMs: 18_000,
305
+ },
296
306
  parts: [
307
+ {
308
+ type: "reasoning",
309
+ text: `先确认受众和交付范围,再验证产品案例。
310
+
311
+ 用户选择的读者是管理层,交付物是报告。因此内容不能只罗列界面趋势,还需要把每项趋势连接到业务价值、落地成本和潜在风险,帮助管理层判断优先级。
312
+
313
+ 接下来会分三步推进:
314
+
315
+ 1. 检索 2025–2026 年主流 AI Agent 产品,优先采集官方发布说明、产品文档和可验证的真实界面。
316
+ 2. 对比任务规划、工具调用、人工确认、后台执行和结果交付等关键交互,区分普遍模式与单个产品的特色设计。
317
+ 3. 把证据整理成可执行建议,并为每条建议标注适用场景、实现复杂度和预期收益。
318
+
319
+ 第一轮检索先覆盖通用 Agent、开发者 Agent 与企业自动化产品。如果不同来源对同一趋势的描述冲突,会保留分歧并继续寻找一手证据,而不是直接下结论。`,
320
+ state: "done",
321
+ },
297
322
  {
298
323
  type: "tool-update_plan",
299
324
  state: "output-available",
@@ -478,6 +503,26 @@ const backgroundResult = {
478
503
  ],
479
504
  } as unknown as UIMessage;
480
505
 
506
+ export const demoScheduleInput = {
507
+ label: "Weekly competitor report",
508
+ prompt: "Generate and send this week's competitor report.",
509
+ trigger: { kind: "cron", cron: "0 9 * * 1" },
510
+ timezone: "Asia/Shanghai",
511
+ };
512
+
513
+ const scheduleRequest = {
514
+ id: "camel-demo-schedule-request",
515
+ role: "assistant",
516
+ parts: [
517
+ {
518
+ type: "tool-schedule",
519
+ toolCallId: "camel-demo-schedule-request-1",
520
+ state: "approval-requested",
521
+ input: demoScheduleInput,
522
+ },
523
+ ],
524
+ } as unknown as UIMessage;
525
+
481
526
  const scheduledTask = {
482
527
  id: "camel-demo-schedule",
483
528
  role: "assistant",
@@ -486,12 +531,7 @@ const scheduledTask = {
486
531
  type: "tool-schedule",
487
532
  toolCallId: "camel-demo-schedule-1",
488
533
  state: "output-available",
489
- input: {
490
- label: "每周发送竞品报告",
491
- prompt: "生成并发送本周竞品报告。",
492
- trigger: { kind: "cron", cron: "0 9 * * 1" },
493
- timezone: "Asia/Shanghai",
494
- },
534
+ input: demoScheduleInput,
495
535
  output: { scheduled: true, id: "schedule-1" },
496
536
  },
497
537
  ],
@@ -877,6 +917,9 @@ export function messagesForScenario(
877
917
  if (scenario === "background_agent") {
878
918
  return [...conversation, backgroundDispatch, backgroundResult];
879
919
  }
920
+ if (scenario === "schedule_create") {
921
+ return [...conversation, scheduleRequest];
922
+ }
880
923
  if (scenario === "schedule") {
881
924
  return [...conversation, scheduledTask];
882
925
  }
@@ -13,6 +13,7 @@ import {
13
13
  CloudOsRoot,
14
14
  CloudOsWorkspacePanel,
15
15
  CloudOsWorkspaceSplit,
16
+ ScheduleConfirmation,
16
17
  WorkshopIconButton,
17
18
  buildCloudOsEntries,
18
19
  type CloudOsAttachmentView,
@@ -32,7 +33,7 @@ import {
32
33
 
33
34
  /**
34
35
  * cloud-os 版快速预览。场景表和 camel-chat-showcase 共用 `chat-scenarios.ts`,
35
- * 所以两边的状态逐条对齐(同样 22 个场景、同样的 7 步流程条、同样的初始 URL 参数)。
36
+ * 所以两边的状态逐条对齐(同样 23 个场景、同样的 7 步流程条、同样的初始 URL 参数)。
36
37
  * 差别只在外壳:这里是 cloudflare-os 的左聊天 / 右工作区双栏。
37
38
  */
38
39
 
@@ -155,7 +156,8 @@ export function CloudOsChatShowcase() {
155
156
  ? (part as { type?: unknown; state?: unknown })
156
157
  : {};
157
158
  return (record.type === "tool-write_file" ||
158
- record.type === "tool-cancel_schedule") &&
159
+ record.type === "tool-cancel_schedule" ||
160
+ record.type === "tool-schedule") &&
159
161
  record.state === "approval-requested"
160
162
  ? "cloud-os-demo-approval"
161
163
  : undefined;
@@ -285,6 +287,7 @@ export function CloudOsChatShowcase() {
285
287
 
286
288
  <CloudOsWorkspaceSplit
287
289
  workspaceOpen={workspaceOpen}
290
+ isLoading={scenario === "hydrating"}
288
291
  chat={
289
292
  <div className="flex h-full min-h-0 flex-col bg-kumo-base">
290
293
  <aside
@@ -353,18 +356,36 @@ export function CloudOsChatShowcase() {
353
356
  status === "submitted" ||
354
357
  status === "streaming" ||
355
358
  scenario === "ask" ||
359
+ scenario === "schedule_create" ||
356
360
  ((scenario === "approval" || scenario === "schedule_cancel") &&
357
361
  approval === undefined)
358
362
  }
359
- isHydrating={scenario === "hydrating"}
360
363
  isRecovering={scenario === "recovering"}
361
- recoveryStatusLabel="模型响应暂时中断,正在重试…"
364
+ recoveryStatusLabel="Model response interrupted. Retrying…"
362
365
  awaitingApproval={
363
- (scenario === "approval" || scenario === "schedule_cancel") &&
366
+ (scenario === "approval" ||
367
+ scenario === "schedule_cancel" ||
368
+ scenario === "schedule_create") &&
364
369
  approval === undefined
365
370
  }
366
371
  showThinkingTraces={showThinkingTraces}
367
372
  approvalIdOf={approvalIdOf}
373
+ renderApproval={({ toolName, input }) =>
374
+ toolName === "schedule"
375
+ ? (
376
+ <ScheduleConfirmation
377
+ input={input}
378
+ onCancel={() => {
379
+ selectScenario("submitted");
380
+ setLastEvent("Schedule creation cancelled");
381
+ }}
382
+ onConfirm={() => {
383
+ selectScenario("schedule");
384
+ setLastEvent("Scheduled task created");
385
+ }}
386
+ />
387
+ )
388
+ : undefined}
368
389
  error={statusError}
369
390
  constrainWidth={!workspaceOpen}
370
391
  onRetry={() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/message-panel",
3
- "version": "0.1.3-alpha.13",
3
+ "version": "0.1.3-alpha.14",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -31,6 +31,7 @@
31
31
  "@streamdown/math": "^1.0.2",
32
32
  "@streamdown/mermaid": "^1.0.2",
33
33
  "lucide-react": "^1.24.0",
34
+ "motion": "^13.1.0",
34
35
  "radix-ui": "^1.6.2",
35
36
  "react-markdown": "^10.1.0",
36
37
  "remark-gfm": "^4.0.1",