@candor-ui/react 2.1.0 → 2.3.0

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.
Files changed (57) hide show
  1. package/dist/adapters/use-ag-ui.d.ts +1 -1
  2. package/dist/adapters/use-candor-chat.d.ts +1 -1
  3. package/dist/adapters/use-claude-agent.d.ts +1 -1
  4. package/dist/blocks/copy-field.js +4 -1
  5. package/dist/blocks/degraded-panel.js +3 -1
  6. package/dist/blocks/locked.js +5 -1
  7. package/dist/blocks/status-dot.d.ts +14 -3
  8. package/dist/blocks/status-dot.js +17 -22
  9. package/dist/blocks/step-list.d.ts +2 -1
  10. package/dist/blocks/step-list.js +19 -27
  11. package/dist/blocks/streaming-text.js +3 -1
  12. package/dist/blocks/surface.d.ts +1 -1
  13. package/dist/chat/attachment-preview.js +5 -2
  14. package/dist/chat/card-registry.js +3 -1
  15. package/dist/chat/citation.js +2 -2
  16. package/dist/chat/composer.js +19 -53
  17. package/dist/chat/context-meter.js +4 -1
  18. package/dist/chat/entity-chip.js +1 -1
  19. package/dist/chat/folded-turns.js +3 -1
  20. package/dist/chat/message-actions.js +5 -2
  21. package/dist/chat/message.js +9 -5
  22. package/dist/chat/payload.js +1 -1
  23. package/dist/chat/permission-prompt.js +6 -1
  24. package/dist/chat/prompt-suggestions.js +4 -1
  25. package/dist/chat/reasoning.js +6 -1
  26. package/dist/chat/thread.js +19 -8
  27. package/dist/chat/tool-call.js +6 -2
  28. package/dist/index.d.ts +8 -0
  29. package/dist/index.js +8 -0
  30. package/dist/lib/motion.d.ts +451 -0
  31. package/dist/lib/motion.js +350 -0
  32. package/dist/styles.css +543 -44
  33. package/dist/ui/accordion.d.ts +23 -0
  34. package/dist/ui/accordion.js +45 -0
  35. package/dist/ui/alert.d.ts +20 -0
  36. package/dist/ui/alert.js +51 -0
  37. package/dist/ui/button.d.ts +1 -1
  38. package/dist/ui/button.js +5 -3
  39. package/dist/ui/checkbox.d.ts +14 -0
  40. package/dist/ui/checkbox.js +32 -0
  41. package/dist/ui/collapsible.d.ts +3 -2
  42. package/dist/ui/collapsible.js +6 -3
  43. package/dist/ui/label.d.ts +10 -0
  44. package/dist/ui/label.js +16 -0
  45. package/dist/ui/popover.d.ts +1 -1
  46. package/dist/ui/popover.js +4 -2
  47. package/dist/ui/progress.d.ts +29 -0
  48. package/dist/ui/progress.js +27 -0
  49. package/dist/ui/skeleton.d.ts +1 -1
  50. package/dist/ui/skeleton.js +5 -2
  51. package/dist/ui/slider.d.ts +28 -0
  52. package/dist/ui/slider.js +41 -0
  53. package/dist/ui/switch.d.ts +14 -0
  54. package/dist/ui/switch.js +23 -0
  55. package/dist/ui/tooltip.d.ts +1 -1
  56. package/dist/ui/tooltip.js +4 -2
  57. package/package.json +1 -1
@@ -5,6 +5,8 @@ import { ChevronRight } from "lucide-react";
5
5
  import { cn } from "../lib/utils.js";
6
6
  import { useStrings } from "../blocks/locale.js";
7
7
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
8
+ import { motion } from "../lib/motion.js";
9
+ const turnMotion = motion("folded-turns", "open", "reveal");
8
10
  /** FoldedTurns' fixed words; override them under `foldedTurns` in LocaleProvider's `strings`. */
9
11
  export const foldedTurnsStrings = {
10
12
  show: { one: "Show {count} turn", other: "Show {count} turns" },
@@ -24,6 +26,6 @@ function FoldedTurns({ count, summary, defaultOpen = false, open, onOpenChange,
24
26
  setOwn(next);
25
27
  onOpenChange?.(next);
26
28
  };
27
- return (_jsxs(Collapsible, { open: isOpen, onOpenChange: set, "data-slot": "folded-turns", className: cn("group/folded flex flex-col gap-7", className), children: [_jsxs("div", { "data-slot": "folded-turns-line", className: cn("flex flex-wrap items-center gap-x-3 gap-y-1 text-sm", header ? "flex-col items-stretch" : "rounded-lg border border-dashed px-3 py-2"), children: [header ?? (summary && _jsx("div", { className: "min-w-0 flex-1 text-muted-foreground", children: summary })), _jsxs(CollapsibleTrigger, { className: cn("relative after:absolute after:inset-x-0 after:-inset-y-0.5 ms-auto inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-xs font-medium text-accent-text outline-none hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/80", header && "self-end"), children: [_jsx(ChevronRight, { "aria-hidden": true, className: "rtl:-scale-x-100 size-3.5 transition-transform duration-200 group-data-[state=open]/folded:[transform:rotate(90deg)]" }), t(isOpen ? "hide" : "show", { count })] })] }), _jsx(CollapsibleContent, { className: "flex flex-col gap-7", children: children })] }));
29
+ return (_jsxs(Collapsible, { open: isOpen, onOpenChange: set, "data-slot": "folded-turns", className: cn("group/folded flex flex-col gap-7", className), children: [_jsxs("div", { "data-slot": "folded-turns-line", className: cn("flex flex-wrap items-center gap-x-3 gap-y-1 text-sm", header ? "flex-col items-stretch" : "rounded-lg border border-dashed px-3 py-2"), children: [header ?? (summary && _jsx("div", { className: "min-w-0 flex-1 text-muted-foreground", children: summary })), _jsxs(CollapsibleTrigger, { className: cn("relative after:absolute after:inset-x-0 after:-inset-y-0.5 ms-auto inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-xs font-medium text-accent-text outline-none hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/80", header && "self-end"), children: [_jsx(ChevronRight, { "aria-hidden": true, style: turnMotion, className: "rtl:-scale-x-100 size-3.5 cd-reveal-turn group-data-[state=open]/folded:[transform:rotate(90deg)]" }), t(isOpen ? "hide" : "show", { count })] })] }), _jsx(CollapsibleContent, { style: turnMotion, className: "flex flex-col gap-7", children: children })] }));
28
30
  }
29
31
  export { FoldedTurns };
@@ -6,6 +6,9 @@ import { cn } from "../lib/utils.js";
6
6
  import { useCopy } from "../lib/use-copy.js";
7
7
  import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.js";
8
8
  import { useStrings } from "../blocks/locale.js";
9
+ import { motion } from "../lib/motion.js";
10
+ /** The tick or cross after the clipboard write settles. */
11
+ const tickMotion = motion("message-actions", "tick", "pop");
9
12
  /** MessageActions' fixed words; override them under `messageActions` in LocaleProvider's `strings`. */
10
13
  export const messageActionsStrings = {
11
14
  copy: "Copy",
@@ -16,7 +19,7 @@ export const messageActionsStrings = {
16
19
  bad: "Bad answer",
17
20
  };
18
21
  function ActionButton({ label, onClick, pressed, children, }) {
19
- return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": label, "aria-pressed": pressed, onClick: onClick, className: "flex size-7 items-center justify-center rounded-md text-faint-foreground transition-colors duration-150 outline-none hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/80 aria-pressed:text-accent-text [&_svg]:size-3.5", children: children }) }), _jsx(TooltipContent, { children: label })] }));
22
+ return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": label, "aria-pressed": pressed, onClick: onClick, className: "flex size-7 items-center justify-center rounded-md text-faint-foreground cd-tint outline-none hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/80 aria-pressed:text-accent-text [&_svg]:size-3.5", children: children }) }), _jsx(TooltipContent, { children: label })] }));
20
23
  }
21
24
  /**
22
25
  * Copy, retry and rate under an agent's reply. Each action appears only when
@@ -28,6 +31,6 @@ function ActionButton({ label, onClick, pressed, children, }) {
28
31
  function MessageActions({ text, onRetry, rating, onRate, meta, className, }) {
29
32
  const { state, copy } = useCopy();
30
33
  const t = useStrings("messageActions", messageActionsStrings);
31
- return (_jsxs("div", { "data-slot": "message-actions", className: cn("flex items-center gap-0.5", className), children: [text !== undefined && (_jsx(ActionButton, { label: state === "copied" ? t("copied") : state === "failed" ? t("copyFailed") : t("copy"), onClick: () => copy(text), children: state === "copied" ? (_jsx(Check, { "data-copy": "copied", className: "text-success-text animate-pop-in" })) : state === "failed" ? (_jsx(X, { "data-copy": "failed", className: "text-destructive-text animate-pop-in" })) : (_jsx(Copy, { "data-copy": state })) })), onRetry && (_jsx(ActionButton, { label: t("retry"), onClick: onRetry, children: _jsx(RotateCw, {}) })), onRate && (_jsxs(_Fragment, { children: [_jsx(ActionButton, { label: t("good"), pressed: rating === "up", onClick: () => onRate(rating === "up" ? null : "up"), children: _jsx(ThumbsUp, { className: cn(rating === "up" && "fill-current") }) }), _jsx(ActionButton, { label: t("bad"), pressed: rating === "down", onClick: () => onRate(rating === "down" ? null : "down"), children: _jsx(ThumbsDown, { className: cn(rating === "down" && "fill-current") }) })] })), meta && _jsx("span", { className: "ms-1.5 truncate text-xs text-faint-foreground", children: meta })] }));
34
+ return (_jsxs("div", { "data-slot": "message-actions", className: cn("flex items-center gap-0.5", className), children: [text !== undefined && (_jsx(ActionButton, { label: state === "copied" ? t("copied") : state === "failed" ? t("copyFailed") : t("copy"), onClick: () => copy(text), children: state === "copied" ? (_jsx(Check, { "data-copy": "copied", style: tickMotion, className: "text-success-text cd-pop" })) : state === "failed" ? (_jsx(X, { "data-copy": "failed", style: tickMotion, className: "text-destructive-text cd-pop" })) : (_jsx(Copy, { "data-copy": state })) })), onRetry && (_jsx(ActionButton, { label: t("retry"), onClick: onRetry, children: _jsx(RotateCw, {}) })), onRate && (_jsxs(_Fragment, { children: [_jsx(ActionButton, { label: t("good"), pressed: rating === "up", onClick: () => onRate(rating === "up" ? null : "up"), children: _jsx(ThumbsUp, { className: cn(rating === "up" && "fill-current") }) }), _jsx(ActionButton, { label: t("bad"), pressed: rating === "down", onClick: () => onRate(rating === "down" ? null : "down"), children: _jsx(ThumbsDown, { className: cn(rating === "down" && "fill-current") }) })] })), meta && _jsx("span", { className: "ms-1.5 truncate text-xs text-faint-foreground", children: meta })] }));
32
35
  }
33
36
  export { MessageActions };
@@ -11,6 +11,10 @@ import { MessageActions } from "./message-actions.js";
11
11
  import { Reasoning } from "./reasoning.js";
12
12
  import { ThreadEvent } from "./thread-event.js";
13
13
  import { useStrings } from "../blocks/locale.js";
14
+ import { motion } from "../lib/motion.js";
15
+ /** A message that arrived after the thread first drew rises in (Thread marks it `data-arrived`); loaded history is still. */
16
+ const arriveMotion = motion("message", "arrive", "arrive");
17
+ const caretMotion = motion("message", "caret", "live", { dur: 1000 });
14
18
  /** Message's fixed words; override them under `message` in LocaleProvider's `strings`. */
15
19
  export const messageStrings = {
16
20
  you: "You",
@@ -50,14 +54,14 @@ function Message({ message, onOpenEntity, onNavigate, onOpenAttachment, onRetry,
50
54
  return (_jsx(ThreadEvent, { "data-slot": "message", "data-role": "system", className: className, children: _jsx(MessageContent, { blocks: message.blocks, className: "text-xs leading-normal" }) }));
51
55
  }
52
56
  if (author.role === "user") {
53
- return (_jsx("div", { "data-slot": "message", "data-role": "user", className: cn("flex justify-end ps-10 sm:ps-24 animate-rise-in", className), children: _jsxs("div", { className: "flex max-w-full flex-col items-end gap-1", children: [_jsx("div", { className: "rounded-xl rounded-ee-sm bg-surface-user px-3.5 py-2.5", children: _jsx(MessageContent, { blocks: message.blocks, onOpenEntity: onOpenEntity, onNavigate: onNavigate, onOpenAttachment: onOpenAttachment, bareCode: bareCode, className: "gap-2" }) }), meta === undefined ? (_jsxs("span", { "data-slot": "message-meta", className: "px-1 text-2xs text-faint-foreground", children: [author.name ?? t("you"), message.createdAt && (_jsxs(_Fragment, { children: [" · ", _jsx(RelativeTime, { date: message.createdAt, format: timeFormat })] }))] })) : (meta)] }) }));
57
+ return (_jsx("div", { "data-slot": "message", "data-role": "user", style: arriveMotion, className: cn("flex justify-end ps-10 sm:ps-24 data-[arrived]:cd-arrive", className), children: _jsxs("div", { className: "flex max-w-full flex-col items-end gap-1", children: [_jsx("div", { className: "rounded-xl rounded-ee-sm bg-surface-user px-3.5 py-2.5", children: _jsx(MessageContent, { blocks: message.blocks, onOpenEntity: onOpenEntity, onNavigate: onNavigate, onOpenAttachment: onOpenAttachment, bareCode: bareCode, className: "gap-2" }) }), meta === undefined ? (_jsxs("span", { "data-slot": "message-meta", className: "px-1 text-2xs text-faint-foreground", children: [author.name ?? t("you"), message.createdAt && (_jsxs(_Fragment, { children: [" · ", _jsx(RelativeTime, { date: message.createdAt, format: timeFormat })] }))] })) : (meta)] }) }));
54
58
  }
55
59
  if (author.role === "person") {
56
- return (_jsxs("article", { "data-slot": "message", "data-role": "person", "aria-label": author.team ? t("personLabel", { name: author.name, team: author.team }) : author.name, className: cn("flex gap-3 animate-rise-in", className), children: [_jsx("span", { "aria-hidden": true, className: "mt-px flex size-7 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground", children: _jsx(UserRound, { className: "size-4" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx(MessageHeader, { name: author.name, detail: author.team, createdAt: message.createdAt, timeFormat: timeFormat, density: density }), _jsx(MessageContent, { blocks: message.blocks, sources: message.sources, onOpenEntity: onOpenEntity, onNavigate: onNavigate, onOpenAttachment: onOpenAttachment, bareCode: bareCode, className: "mt-1" })] })] }));
60
+ return (_jsxs("article", { "data-slot": "message", "data-role": "person", "aria-label": author.team ? t("personLabel", { name: author.name, team: author.team }) : author.name, style: arriveMotion, className: cn("flex gap-3 data-[arrived]:cd-arrive", className), children: [_jsx("span", { "aria-hidden": true, className: "mt-px flex size-7 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground", children: _jsx(UserRound, { className: "size-4" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx(MessageHeader, { name: author.name, detail: author.team, createdAt: message.createdAt, timeFormat: timeFormat, density: density }), _jsx(MessageContent, { blocks: message.blocks, sources: message.sources, onOpenEntity: onOpenEntity, onNavigate: onNavigate, onOpenAttachment: onOpenAttachment, bareCode: bareCode, className: "mt-1" })] })] }));
57
61
  }
58
62
  const { agent } = author;
59
63
  const streaming = message.status === "streaming";
60
- return (_jsxs("article", { "data-slot": "message", "data-role": "agent", "data-status": message.status, "aria-busy": streaming, "aria-label": t("agentLabel", { name: agent.name, noun: roleOf(agent.role).noun }), className: cn("group/message flex gap-3 animate-rise-in", className), children: [_jsx(AgentAvatar, { agent: agent, className: "mt-px" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx(MessageHeader, { name: agent.name, detail: agent.specialty, createdAt: message.createdAt, timeFormat: timeFormat, density: density }), _jsxs("div", { className: "mt-1 flex flex-col gap-3", children: [_jsx(MessageBody, { message: message, onOpenEntity: onOpenEntity, onNavigate: onNavigate, onOpenAttachment: onOpenAttachment, bareCode: bareCode }), _jsx(MessageFooter, { message: message, onRetry: onRetry, onContinue: onContinue, rating: rating, onRate: onRate, reviewedBy: reviewedBy, footer: footer, actions: actions, timeFormat: timeFormat })] })] })] }));
64
+ return (_jsxs("article", { "data-slot": "message", "data-role": "agent", "data-status": message.status, "aria-busy": streaming, "aria-label": t("agentLabel", { name: agent.name, noun: roleOf(agent.role).noun }), style: arriveMotion, className: cn("group/message flex gap-3 data-[arrived]:cd-arrive", className), children: [_jsx(AgentAvatar, { agent: agent, className: "mt-px" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx(MessageHeader, { name: agent.name, detail: agent.specialty, createdAt: message.createdAt, timeFormat: timeFormat, density: density }), _jsxs("div", { className: "mt-1 flex flex-col gap-3", children: [_jsx(MessageBody, { message: message, onOpenEntity: onOpenEntity, onNavigate: onNavigate, onOpenAttachment: onOpenAttachment, bareCode: bareCode }), _jsx(MessageFooter, { message: message, onRetry: onRetry, onContinue: onContinue, rating: rating, onRate: onRate, reviewedBy: reviewedBy, footer: footer, actions: actions, timeFormat: timeFormat })] })] })] }));
61
65
  }
62
66
  /** The row above a reply: who sent it, a detail (their specialty or team), and when. */
63
67
  function MessageHeader({ name, detail, createdAt, timeFormat, density = "default", children, className, }) {
@@ -67,7 +71,7 @@ function MessageHeader({ name, detail, createdAt, timeFormat, density = "default
67
71
  function MessageBody({ message, onOpenEntity, onNavigate, onOpenAttachment, bareCode, className, }) {
68
72
  const streaming = message.status === "streaming";
69
73
  const thinking = streaming && message.blocks.length === 0;
70
- return (_jsxs("div", { "data-slot": "message-body", className: cn("flex flex-col gap-3", className), children: [message.reasoning && message.reasoning.length > 0 && _jsx(Reasoning, { entries: message.reasoning, thinking: thinking, thoughtFor: message.thoughtFor }), message.blocks.length > 0 && (_jsxs("div", { className: "relative", children: [_jsx(MessageContent, { blocks: message.blocks, sources: message.sources, onOpenEntity: onOpenEntity, onNavigate: onNavigate, onOpenAttachment: onOpenAttachment, bareCode: bareCode }), streaming && _jsx("span", { "aria-hidden": true, className: "ms-0.5 inline-block h-[1.05em] w-[2px] translate-y-[0.18em] rounded-full bg-primary animate-caret" })] }))] }));
74
+ return (_jsxs("div", { "data-slot": "message-body", className: cn("flex flex-col gap-3", className), children: [message.reasoning && message.reasoning.length > 0 && _jsx(Reasoning, { entries: message.reasoning, thinking: thinking, thoughtFor: message.thoughtFor }), message.blocks.length > 0 && (_jsxs("div", { className: "relative", children: [_jsx(MessageContent, { blocks: message.blocks, sources: message.sources, onOpenEntity: onOpenEntity, onNavigate: onNavigate, onOpenAttachment: onOpenAttachment, bareCode: bareCode }), streaming && _jsx("span", { "aria-hidden": true, style: caretMotion, className: "ms-0.5 inline-block h-[1.05em] w-[2px] translate-y-[0.18em] rounded-full bg-primary cd-live-caret" })] }))] }));
71
75
  }
72
76
  /** Model and cost, on a line of their own: what the actions row shows, for when it's hidden or replaced. */
73
77
  function MessageAttribution({ attribution, className, }) {
@@ -86,6 +90,6 @@ function MessageFooter({ message, onRetry, onContinue, rating, onRate, reviewedB
86
90
  const streaming = message.status === "streaming";
87
91
  const settled = !streaming && message.status !== "failed";
88
92
  const meta = [message.attribution?.model, message.attribution?.cost].filter(Boolean).join(" · ");
89
- return (_jsxs("div", { "data-slot": "message-footer", className: cn("flex flex-col gap-3 empty:hidden", className), children: [message.status === "interrupted" && (_jsxs("div", { className: "flex flex-wrap items-center gap-2 text-xs text-muted-foreground", children: [_jsx(CircleStop, { "aria-hidden": true, className: "size-3.5" }), t("interrupted"), onContinue && (_jsx("button", { type: "button", onClick: onContinue, className: "relative after:absolute after:inset-x-0 after:-inset-y-1 font-medium text-accent-text hover:underline", children: t("continue") }))] })), message.status === "failed" && _jsx(DegradedPanel, { reason: t("failed"), missing: t("failedMissing"), onRetry: onRetry }), !streaming && message.sources && message.sources.length > 0 && _jsx(SourceList, { sources: message.sources }), reviewedBy && (_jsxs("p", { "data-slot": "message-reviewed", className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [_jsx(UserCheck, { "aria-hidden": true, className: "size-3.5" }), t(reviewedBy.edited ? "editedBy" : "checkedBy", { name: reviewedBy.name }), reviewedBy.at && (_jsxs(_Fragment, { children: [" · ", _jsx(RelativeTime, { date: reviewedBy.at, format: timeFormat })] }))] })), footer, settled && actions === undefined && (_jsx(MessageActions, { text: blocksToText(message.blocks, message.sources), onRetry: onRetry, rating: rating, onRate: onRate, meta: meta || undefined, className: "-ms-1.5 opacity-100 transition-opacity duration-150 sm:opacity-0 sm:group-hover/message:opacity-100 sm:group-focus-within/message:opacity-100" })), settled && actions !== undefined && actions !== false && actions, settled && actions !== undefined && _jsx(MessageAttribution, { attribution: message.attribution })] }));
93
+ return (_jsxs("div", { "data-slot": "message-footer", className: cn("flex flex-col gap-3 empty:hidden", className), children: [message.status === "interrupted" && (_jsxs("div", { className: "flex flex-wrap items-center gap-2 text-xs text-muted-foreground", children: [_jsx(CircleStop, { "aria-hidden": true, className: "size-3.5" }), t("interrupted"), onContinue && (_jsx("button", { type: "button", onClick: onContinue, className: "relative after:absolute after:inset-x-0 after:-inset-y-1 font-medium text-accent-text hover:underline", children: t("continue") }))] })), message.status === "failed" && _jsx(DegradedPanel, { reason: t("failed"), missing: t("failedMissing"), onRetry: onRetry }), !streaming && message.sources && message.sources.length > 0 && _jsx(SourceList, { sources: message.sources }), reviewedBy && (_jsxs("p", { "data-slot": "message-reviewed", className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [_jsx(UserCheck, { "aria-hidden": true, className: "size-3.5" }), t(reviewedBy.edited ? "editedBy" : "checkedBy", { name: reviewedBy.name }), reviewedBy.at && (_jsxs(_Fragment, { children: [" · ", _jsx(RelativeTime, { date: reviewedBy.at, format: timeFormat })] }))] })), footer, settled && actions === undefined && (_jsx(MessageActions, { text: blocksToText(message.blocks, message.sources), onRetry: onRetry, rating: rating, onRate: onRate, meta: meta || undefined, className: "-ms-1.5 opacity-100 cd-tint sm:opacity-0 sm:group-hover/message:opacity-100 sm:group-focus-within/message:opacity-100" })), settled && actions !== undefined && actions !== false && actions, settled && actions !== undefined && _jsx(MessageAttribution, { attribution: message.attribution })] }));
90
94
  }
91
95
  export { Message, MessageHeader, MessageBody, MessageFooter, MessageAttribution };
@@ -44,7 +44,7 @@ function Payload({ language, text, title, bare = false, highlight, className, })
44
44
  // oxlint-disable-next-line jsx-a11y/no-noninteractive-tabindex
45
45
  _jsx("pre", { "data-slot": "payload", "data-bare": "", "data-language": language, "data-syntax": syntax, tabIndex: 0, className: cn("max-h-80 overflow-auto rounded-md bg-sunken outline-none focus-visible:ring-[3px] focus-visible:ring-ring/80 focus-visible:ring-inset px-3 py-2.5 font-mono text-xs leading-relaxed text-foreground", className), children: code }));
46
46
  }
47
- return (_jsxs("figure", { "data-slot": "payload", "data-language": language, "data-syntax": syntax, className: cn("overflow-hidden rounded-lg border bg-sunken", className), children: [_jsxs("figcaption", { className: "flex items-center justify-between gap-2 border-b px-3 py-1.5", children: [_jsxs("span", { className: "truncate text-xs text-muted-foreground", children: [_jsx("span", { className: "font-medium text-foreground", children: languageLabel(language, t) }), title && _jsxs("span", { children: [" \u00B7 ", title] })] }), _jsx("button", { type: "button", onClick: () => copy(text), "aria-live": "polite", className: "relative after:absolute after:inset-x-0 after:-inset-y-0.5 inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs text-muted-foreground transition-colors duration-150 outline-none hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/80", children: state === "copied" ? (_jsxs(_Fragment, { children: [_jsx(Check, { className: "size-3 text-success-text" }), " ", t("copied")] })) : state === "failed" ? (_jsxs(_Fragment, { children: [_jsx(X, { className: "size-3 text-destructive-text" }), " ", t("copyFailed")] })) : (_jsxs(_Fragment, { children: [_jsx(Copy, { className: "size-3" }), " ", t("copy")] })) })] }), _jsx("pre", { tabIndex: 0, className: "max-h-80 overflow-auto px-3 py-2.5 font-mono text-xs leading-relaxed text-foreground outline-none focus-visible:ring-[3px] focus-visible:ring-ring/80 focus-visible:ring-inset", children: code })] }));
47
+ return (_jsxs("figure", { "data-slot": "payload", "data-language": language, "data-syntax": syntax, className: cn("overflow-hidden rounded-lg border bg-sunken", className), children: [_jsxs("figcaption", { className: "flex items-center justify-between gap-2 border-b px-3 py-1.5", children: [_jsxs("span", { className: "truncate text-xs text-muted-foreground", children: [_jsx("span", { className: "font-medium text-foreground", children: languageLabel(language, t) }), title && _jsxs("span", { children: [" \u00B7 ", title] })] }), _jsx("button", { type: "button", onClick: () => copy(text), "aria-live": "polite", className: "relative after:absolute after:inset-x-0 after:-inset-y-0.5 inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs text-muted-foreground cd-tint outline-none hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/80", children: state === "copied" ? (_jsxs(_Fragment, { children: [_jsx(Check, { className: "size-3 text-success-text" }), " ", t("copied")] })) : state === "failed" ? (_jsxs(_Fragment, { children: [_jsx(X, { className: "size-3 text-destructive-text" }), " ", t("copyFailed")] })) : (_jsxs(_Fragment, { children: [_jsx(Copy, { className: "size-3" }), " ", t("copy")] })) })] }), _jsx("pre", { tabIndex: 0, className: "max-h-80 overflow-auto px-3 py-2.5 font-mono text-xs leading-relaxed text-foreground outline-none focus-visible:ring-[3px] focus-visible:ring-ring/80 focus-visible:ring-inset", children: code })] }));
48
48
  }
49
49
  /** The code as token spans. Each token is a text child, so nothing in it is parsed as markup. */
50
50
  function Code({ text, language }) {
@@ -6,6 +6,11 @@ import { cn } from "../lib/utils.js";
6
6
  import { useStrings } from "../blocks/locale.js";
7
7
  import { Payload } from "./payload.js";
8
8
  import { Button } from "../ui/button.js";
9
+ import { motion } from "../lib/motion.js";
10
+ /** Three pulses of 1.6 s while the decision waits (4.8 s, then still), shortened from 2.2's 7.2 s. */
11
+ const waitingMotion = motion("permission-prompt", "waiting", "attention", { dur: 1600, amp: "3" });
12
+ /** A press around the prompt instead of on it: one ring, and the refuse shake under expressive. */
13
+ const nudgeMotion = { ...motion("permission-prompt", "nudge", "refuse"), ...motion("permission-prompt", "nudge", "attention") };
9
14
  /** PermissionPrompt's fixed words; override them under `permissionPrompt` in LocaleProvider's `strings`. */
10
15
  export const permissionPromptStrings = {
11
16
  label: "Permission request: {title}",
@@ -166,7 +171,7 @@ function PromptBody({ request, mode, suggestions, editable, onAllow, onDeny, bus
166
171
  return;
167
172
  onAllow({ ...(edited && changedKeys.length && { updatedInput: edited }), ...(ticked.length && { alwaysAllow: ticked }) });
168
173
  };
169
- return (_jsxs("section", { ref: section, "data-slot": "permission-prompt", "data-waiting": busy ? undefined : "", "data-nudged": nudged ? "" : undefined, "aria-label": t("label", { title: questions ? t("questions") : title }), className: cn("overflow-hidden rounded-xl border border-warning/50 bg-card text-sm shadow-xs [--cd-attention-color:var(--warning)]", nudged && "animate-nudge", className), children: [_jsxs("header", { className: "flex items-start gap-2.5 bg-warning-weak/50 px-3.5 py-2.5", children: [_jsx("span", { "aria-hidden": true, className: cn("mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full text-warning-text", !busy && "animate-attention-wait"), children: _jsx(ShieldQuestion, { className: "size-4" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h3", { className: "font-semibold leading-snug", children: questions ? t("questions") : title }), request.description && _jsx("p", { className: "mt-0.5 text-muted-foreground", children: request.description })] }), _jsx("code", { className: "shrink-0 font-mono text-2xs text-muted-foreground", children: request.tool })] }), _jsxs("dl", { className: "flex flex-col gap-1.5 px-3.5 pt-2.5 text-sm", children: [request.reason && (_jsxs("div", { className: "flex gap-2", children: [_jsx("dt", { className: "shrink-0 text-muted-foreground", children: t("why") }), _jsx("dd", { className: "min-w-0", children: request.reason })] })), mode && (_jsxs("div", { className: "flex gap-2", children: [_jsx("dt", { className: "shrink-0 text-muted-foreground", children: t("mode") }), _jsx("dd", { "data-slot": "permission-mode", children: modeKeys[mode] ? t(modeKeys[mode]) : _jsx("code", { className: "font-mono text-xs", children: mode }) })] }))] }), (request.blockedPath || request.agentId || request.mcpServer) && (_jsxs("ul", { className: "mt-1.5 flex flex-col gap-0.5 px-3.5 text-xs text-muted-foreground", children: [request.blockedPath && _jsx("li", { children: t("blockedPath", { path: request.blockedPath }) }), request.agentId && _jsx("li", { children: t("subagent") }), request.mcpServer && _jsx("li", { children: t("server", { name: request.mcpServer.name, source: request.mcpServer.source }) })] })), questions ? (_jsx("div", { className: "flex flex-col gap-4 px-3.5 py-3", children: questions.map((q) => {
174
+ return (_jsxs("section", { ref: section, "data-slot": "permission-prompt", "data-waiting": busy ? undefined : "", "data-nudged": nudged ? "" : undefined, "aria-label": t("label", { title: questions ? t("questions") : title }), style: nudgeMotion, className: cn("overflow-hidden rounded-xl border border-warning/50 bg-card text-sm shadow-xs [--cd-attention-color:var(--warning)]", nudged && "cd-nudge", className), children: [_jsxs("header", { className: "flex items-start gap-2.5 bg-warning-weak/50 px-3.5 py-2.5", children: [_jsx("span", { "aria-hidden": true, style: waitingMotion, className: cn("mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full text-warning-text", !busy && "cd-attention"), children: _jsx(ShieldQuestion, { className: "size-4" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h3", { className: "font-semibold leading-snug", children: questions ? t("questions") : title }), request.description && _jsx("p", { className: "mt-0.5 text-muted-foreground", children: request.description })] }), _jsx("code", { className: "shrink-0 font-mono text-2xs text-muted-foreground", children: request.tool })] }), _jsxs("dl", { className: "flex flex-col gap-1.5 px-3.5 pt-2.5 text-sm", children: [request.reason && (_jsxs("div", { className: "flex gap-2", children: [_jsx("dt", { className: "shrink-0 text-muted-foreground", children: t("why") }), _jsx("dd", { className: "min-w-0", children: request.reason })] })), mode && (_jsxs("div", { className: "flex gap-2", children: [_jsx("dt", { className: "shrink-0 text-muted-foreground", children: t("mode") }), _jsx("dd", { "data-slot": "permission-mode", children: modeKeys[mode] ? t(modeKeys[mode]) : _jsx("code", { className: "font-mono text-xs", children: mode }) })] }))] }), (request.blockedPath || request.agentId || request.mcpServer) && (_jsxs("ul", { className: "mt-1.5 flex flex-col gap-0.5 px-3.5 text-xs text-muted-foreground", children: [request.blockedPath && _jsx("li", { children: t("blockedPath", { path: request.blockedPath }) }), request.agentId && _jsx("li", { children: t("subagent") }), request.mcpServer && _jsx("li", { children: t("server", { name: request.mcpServer.name, source: request.mcpServer.source }) })] })), questions ? (_jsx("div", { className: "flex flex-col gap-4 px-3.5 py-3", children: questions.map((q) => {
170
175
  const a = answers[q.id];
171
176
  return (_jsxs("fieldset", { className: "flex flex-col gap-1.5", children: [_jsxs("legend", { className: "mb-1 font-medium", children: [q.tag && _jsx("span", { className: "me-1.5 rounded-xs bg-muted px-1 text-2xs font-medium text-muted-foreground", children: q.tag }), q.prompt] }), q.options.map((o) => (_jsxs("label", { className: "flex min-h-6 items-start gap-2", children: [_jsx("input", { type: q.multiple ? "checkbox" : "radio", name: `${id}-${q.id}`, checked: a?.choice.includes(o.id) ?? false, onChange: () => choose(q, o.id), className: "mt-0.5 size-4 accent-[var(--primary)]" }), _jsxs("span", { children: [o.label, o.detail && _jsx("span", { className: "block text-xs text-muted-foreground", children: o.detail })] })] }, o.id))), q.other && (_jsxs(_Fragment, { children: [_jsxs("label", { className: "flex min-h-6 items-start gap-2", children: [_jsx("input", { type: q.multiple ? "checkbox" : "radio", name: `${id}-${q.id}`, checked: a?.other ?? false, onChange: () => toggleOther(q), className: "mt-0.5 size-4 accent-[var(--primary)]" }), _jsx("span", { children: t("otherOption") })] }), a?.other && (_jsx("textarea", { "aria-label": t("otherLabel", { question: q.prompt }), rows: 2, value: a.text, onChange: (e) => setAnswers((prev) => ({ ...prev, [q.id]: { ...prev[q.id], text: e.target.value } })), className: "ms-6 block resize-none rounded-md border border-input bg-card px-2.5 py-1.5 text-sm outline-none [field-sizing:content] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/30" }))] }))] }, q.id));
172
177
  }) })) : (_jsxs("div", { className: "flex flex-col gap-2 px-3.5 py-3", children: [editing ? (_jsxs(_Fragment, { children: [_jsx("label", { htmlFor: `${id}-edit`, className: "text-xs font-medium text-muted-foreground", children: t("editLabel") }), _jsx("textarea", { id: `${id}-edit`, value: draft, onChange: (e) => setDraft(e.target.value), spellCheck: false, rows: Math.min(12, draft.split("\n").length + 1), "aria-invalid": invalid || undefined, "aria-describedby": `${id}-changes`, className: "block w-full resize-y rounded-md border border-input bg-card px-2.5 py-1.5 font-mono text-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/30 aria-invalid:border-destructive" }), _jsx("div", { id: `${id}-changes`, role: "status", className: "text-xs", children: invalid ? (_jsx("p", { className: "text-destructive-text", children: t("editInvalid") })) : changedKeys.length ? (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("p", { className: "text-muted-foreground", children: t("changed") }), _jsx("ul", { className: "flex flex-col gap-1", children: changedKeys.map((k) => (_jsxs("li", { className: "grid gap-0.5 font-mono", children: [_jsx("span", { className: "text-muted-foreground", children: k }), k in proposed && _jsx("span", { className: "rounded-xs bg-destructive-weak/60 px-1 line-through", children: json(proposed[k]) }), k in edited && _jsx("span", { className: "rounded-xs bg-success-weak/70 px-1", children: json(edited[k]) })] }, k))) })] })) : (_jsx("p", { className: "text-muted-foreground", children: t("unchanged") })) }), _jsx(Button, { size: "sm", variant: "ghost", className: "self-start", onClick: () => (setEditing(false), setDraft(original)), children: t("cancelEdit") })] })) : (_jsxs(_Fragment, { children: [_jsx(Payload, { language: "json", title: t("input"), text: original }), editable && proposed && (_jsxs(Button, { size: "sm", variant: "outline", className: "self-start", onClick: () => setEditing(true), children: [_jsx(Pencil, { "aria-hidden": true }), " ", t("edit")] }))] })), suggestions && suggestions.length > 0 && (_jsxs("fieldset", { className: "mt-1 flex flex-col gap-1 rounded-lg border border-dashed px-3 py-2", children: [_jsx("legend", { className: "px-1 text-xs font-medium", children: t("remember") }), _jsxs("p", { className: "flex items-center gap-1 text-xs text-muted-foreground", children: [_jsx(Info, { "aria-hidden": true, className: "size-3" }), " ", t("rememberHint")] }), suggestions.map((s, i) => {
@@ -4,6 +4,9 @@ import { ArrowUpRight, Sparkles } from "lucide-react";
4
4
  import { cn } from "../lib/utils.js";
5
5
  import { Skeleton } from "../ui/skeleton.js";
6
6
  import { useStrings } from "../blocks/locale.js";
7
+ import { motion } from "../lib/motion.js";
8
+ /** A suggestion pressed: 2.2's slight scale of 0.99. */
9
+ const pressMotion = motion("prompt-suggestions", "press", "press", { amp: "0.98" });
7
10
  /** PromptSuggestions' fixed words; override them under `promptSuggestions` in LocaleProvider's `strings`. */
8
11
  export const promptSuggestionsStrings = {
9
12
  label: "Try asking",
@@ -20,6 +23,6 @@ function PromptSuggestions({ suggestions, onChoose, label: labelProp, loading =
20
23
  return null;
21
24
  return (_jsxs("section", { "data-slot": "prompt-suggestions", "aria-label": label, className: cn("flex flex-col gap-2", className), children: [_jsxs("p", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [_jsx(Sparkles, { "aria-hidden": true, className: "size-3" }), " ", label] }), _jsx("div", { className: cn(variant === "cards" ? "grid gap-2 sm:grid-cols-2" : "flex flex-wrap gap-1.5"), children: loading
22
25
  ? Array.from({ length: variant === "cards" ? 4 : 3 }, (_, i) => (_jsx(Skeleton, { className: variant === "cards" ? "h-16 rounded-xl" : "h-8 w-44 rounded-lg" }, i)))
23
- : suggestions.map((s) => variant === "cards" ? (_jsxs("button", { type: "button", onClick: () => onChoose(s), className: "group/suggestion flex items-start justify-between gap-3 rounded-xl border bg-card p-3 text-start text-sm transition-[border-color,box-shadow,transform] duration-150 ease-standard outline-none hover:border-border-strong hover:shadow-raised active:scale-[0.99] focus-visible:ring-[3px] focus-visible:ring-ring/80", children: [_jsx("span", { children: s.text }), _jsx(ArrowUpRight, { "aria-hidden": true, className: "size-3.5 shrink-0 text-faint-foreground transition-colors group-hover/suggestion:text-accent-text" })] }, s.id)) : (_jsx("button", { type: "button", onClick: () => onChoose(s), className: "rounded-lg border bg-card px-3 py-1.5 text-sm text-muted-foreground transition-colors duration-150 outline-none hover:border-border-strong hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/80", children: s.text }, s.id))) })] }));
26
+ : suggestions.map((s) => variant === "cards" ? (_jsxs("button", { type: "button", onClick: () => onChoose(s), style: pressMotion, className: "group/suggestion flex items-start justify-between gap-3 rounded-xl border bg-card p-3 text-start text-sm cd-press outline-none hover:border-border-strong hover:shadow-raised focus-visible:ring-[3px] focus-visible:ring-ring/80", children: [_jsx("span", { children: s.text }), _jsx(ArrowUpRight, { "aria-hidden": true, className: "size-3.5 shrink-0 text-faint-foreground cd-tint group-hover/suggestion:text-accent-text" })] }, s.id)) : (_jsx("button", { type: "button", onClick: () => onChoose(s), className: "rounded-lg border bg-card px-3 py-1.5 text-sm text-muted-foreground cd-tint outline-none hover:border-border-strong hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/80", children: s.text }, s.id))) })] }));
24
27
  }
25
28
  export { PromptSuggestions };
@@ -6,6 +6,9 @@ import { cn } from "../lib/utils.js";
6
6
  import { ShimmerLabel } from "../blocks/streaming-text.js";
7
7
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
8
8
  import { useStrings } from "../blocks/locale.js";
9
+ import { motion, useArrived } from "../lib/motion.js";
10
+ const arriveMotion = motion("reasoning", "arrive", "arrive");
11
+ const turnMotion = motion("reasoning", "open", "reveal");
9
12
  /** Reasoning's fixed words; override them under `reasoning` in LocaleProvider's `strings`. */
10
13
  export const reasoningStrings = {
11
14
  verified: "Verified",
@@ -31,10 +34,12 @@ const basisTone = {
31
34
  */
32
35
  function Reasoning({ entries, thinking = false, thoughtFor, className, }) {
33
36
  const t = useStrings("reasoning", reasoningStrings);
37
+ // An entry added while the reasoning is on screen rises in; the ones it drew with are still.
38
+ const arrived = useArrived(React.useMemo(() => entries.map((e) => e.id), [entries]));
34
39
  const [open, setOpen] = React.useState(thinking);
35
40
  React.useEffect(() => setOpen(thinking), [thinking]);
36
41
  const assumed = entries.filter((e) => e.basis === "assumed").length;
37
42
  const summary = thinking ? null : (_jsxs(_Fragment, { children: [t("reasoning"), thoughtFor && _jsxs(_Fragment, { children: [" \u00B7 ", thoughtFor] }), " \u00B7 ", t("steps", { count: entries.length }), assumed > 0 && _jsxs("span", { className: "text-warning-text", children: [" \u00B7 ", t("assumedCount", { count: assumed })] })] }));
38
- return (_jsxs(Collapsible, { open: open, onOpenChange: setOpen, "data-slot": "reasoning", className: cn("group/reasoning", className), children: [_jsxs(CollapsibleTrigger, { className: "relative after:absolute after:inset-x-0 after:-inset-y-0.5 inline-flex items-center gap-1.5 rounded-md py-0.5 pe-1.5 text-xs text-muted-foreground outline-none transition-colors duration-150 hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/80", children: [_jsx(Brain, { "aria-hidden": true, className: cn("size-3.5", thinking && "text-accent-text") }), thinking ? _jsx(ShimmerLabel, { className: "text-xs", children: t("thinking") }) : summary, _jsx(ChevronRight, { "aria-hidden": true, className: "rtl:-scale-x-100 size-3 transition-transform duration-200 ease-standard group-data-[state=open]/reasoning:[transform:rotate(90deg)]" })] }), _jsx(CollapsibleContent, { children: _jsx("ol", { className: "mt-2 ms-1.5 flex flex-col gap-3 border-s ps-4", children: entries.map((e) => (_jsxs("li", { className: "relative animate-rise-in", children: [_jsx("span", { "aria-hidden": true, className: "absolute top-1.5 -start-[1.2rem] size-1.5 rounded-full bg-border-strong" }), _jsxs("div", { className: "flex flex-wrap items-baseline gap-x-2 gap-y-1", children: [_jsx("p", { className: "text-sm font-medium", children: e.title }), _jsx("span", { className: cn("rounded-xs px-1 text-2xs font-medium", basisTone[e.basis]), children: t(e.basis) })] }), e.body && _jsx("p", { className: "mt-0.5 text-sm text-muted-foreground", children: e.body }), _jsx("p", { className: "mt-1 text-xs text-faint-foreground", children: e.basis === "verified" ? _jsxs(_Fragment, { children: [t("evidence"), " ", e.evidence] }) : _jsxs(_Fragment, { children: [t("assumption"), " ", e.assumption] }) })] }, e.id))) }) })] }));
43
+ return (_jsxs(Collapsible, { open: open, onOpenChange: setOpen, "data-slot": "reasoning", className: cn("group/reasoning", className), children: [_jsxs(CollapsibleTrigger, { className: "relative after:absolute after:inset-x-0 after:-inset-y-0.5 inline-flex items-center gap-1.5 rounded-md py-0.5 pe-1.5 text-xs text-muted-foreground outline-none cd-tint hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/80", children: [_jsx(Brain, { "aria-hidden": true, className: cn("size-3.5", thinking && "text-accent-text") }), thinking ? _jsx(ShimmerLabel, { className: "text-xs", children: t("thinking") }) : summary, _jsx(ChevronRight, { "aria-hidden": true, style: turnMotion, className: "rtl:-scale-x-100 size-3 cd-reveal-turn group-data-[state=open]/reasoning:[transform:rotate(90deg)]" })] }), _jsx(CollapsibleContent, { style: turnMotion, children: _jsx("ol", { className: "mt-2 ms-1.5 flex flex-col gap-3 border-s ps-4", children: entries.map((e) => (_jsxs("li", { style: arriveMotion, className: cn("relative", arrived(e.id) && "cd-arrive"), children: [_jsx("span", { "aria-hidden": true, className: "absolute top-1.5 -start-[1.2rem] size-1.5 rounded-full bg-border-strong" }), _jsxs("div", { className: "flex flex-wrap items-baseline gap-x-2 gap-y-1", children: [_jsx("p", { className: "text-sm font-medium", children: e.title }), _jsx("span", { className: cn("rounded-xs px-1 text-2xs font-medium", basisTone[e.basis]), children: t(e.basis) })] }), e.body && _jsx("p", { className: "mt-0.5 text-sm text-muted-foreground", children: e.body }), _jsx("p", { className: "mt-1 text-xs text-faint-foreground", children: e.basis === "verified" ? _jsxs(_Fragment, { children: [t("evidence"), " ", e.evidence] }) : _jsxs(_Fragment, { children: [t("assumption"), " ", e.assumption] }) })] }, e.id))) }) })] }));
39
44
  }
40
45
  export { Reasoning };
@@ -5,6 +5,13 @@ import { ArrowDown } from "lucide-react";
5
5
  import { cn } from "../lib/utils.js";
6
6
  import { useStrings } from "../blocks/locale.js";
7
7
  import { useVirtualList } from "../lib/use-virtual-list.js";
8
+ import { motion, useMotionAllowed } from "../lib/motion.js";
9
+ /** The smooth scroll to the latest message. */
10
+ const scrollMotion = motion("thread", "scroll", "move");
11
+ /** Jump to latest appearing while the reader is scrolled up: 2.2's 150 ms and 4 px rise. */
12
+ const jumpMotion = motion("thread", "jump", "arrive", { dur: 150 });
13
+ /** Its ring when a message arrives while the reader is scrolled up. */
14
+ const arrivalMotion = motion("thread", "arrival", "attention");
8
15
  /** Thread's fixed words; override them under `thread` in LocaleProvider's `strings`. */
9
16
  export const threadStrings = {
10
17
  messages: "Messages",
@@ -37,14 +44,15 @@ function Thread({ children, empty, footer, className, contentClassName, virtuali
37
44
  const pinnedRef = React.useRef(!anchoring);
38
45
  const [pinned, setPinned] = React.useState(!anchoring);
39
46
  React.useImperativeHandle(scrollerRef, () => scroller.current, []);
47
+ // A smooth jump only when nothing asks for stillness: the OS setting (the calm level), data-motion="calm" or "none"
48
+ // around the thread, or a brand that turned the scroll off.
49
+ const smooth = useMotionAllowed(scroller, scrollMotion);
40
50
  const toBottom = React.useCallback((behavior = "auto") => {
41
51
  const el = scroller.current;
42
52
  if (!el)
43
53
  return;
44
- // A smooth jump only when nothing asks for stillness: the OS setting, or data-motion="none" around the thread.
45
- const still = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches || el.closest('[data-motion="none"]') !== null;
46
- el.scrollTo({ top: el.scrollHeight, behavior: still ? "auto" : behavior });
47
- }, []);
54
+ el.scrollTo({ top: el.scrollHeight, behavior: smooth() ? behavior : "auto" });
55
+ }, [smooth]);
48
56
  // A new message while the reader is scrolled up: the Jump to latest control pulses once for each arrival, even when
49
57
  // the messages sit inside a wrapper component or a fragment. A second arrival while it still pulses restarts it.
50
58
  const [arrived, setArrived] = React.useState(false);
@@ -98,8 +106,11 @@ function Thread({ children, empty, footer, className, contentClassName, virtuali
98
106
  const had = new Set(before);
99
107
  const survivors = before.filter((node) => node.isConnected);
100
108
  const added = known.filter((node) => !had.has(node));
101
- const atTheEnd = added.some((node) => survivors.every((old) => old.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING));
102
- if (atTheEnd)
109
+ const arrivals = added.filter((node) => survivors.every((old) => old.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING));
110
+ // A message that arrived after the thread first drew rises in (Message reads data-arrived); loaded history is still.
111
+ for (const node of arrivals)
112
+ node.setAttribute("data-arrived", "");
113
+ if (arrivals.length)
103
114
  arrive();
104
115
  });
105
116
  observer.observe(el, { childList: true, subtree: true });
@@ -177,10 +188,10 @@ function Thread({ children, empty, footer, className, contentClassName, virtuali
177
188
  }
178
189
  : undefined, onBlur: virtualize ? (e) => !e.currentTarget.contains(e.relatedTarget) && setFocused(null) : undefined, className: "h-full scroll-pb-14 overflow-y-auto overscroll-contain", children: _jsx("div", { ref: content, role: "log", "aria-label": t("messages"), className: cn("mx-auto flex w-full max-w-(--cd-thread-width) flex-col gap-7 px-4 py-6", contentClassName), children: !hasMessages ? (empty) : virtualize ? (_jsx(_Fragment, { children: list.rows.map((row) => "spacer" in row ? (_jsx("div", { "aria-hidden": true, style: { height: row.spacer } }, row.key)) : (_jsx("div", { ref: list.measure, "data-virtual-key": String(keyOf(row.index)), className: "flex flex-col", children: items[row.index] }, keyOf(row.index)))) })) : (children) }) }), !pinned && hasMessages && (
179
190
  // The entrance is the wrapper's, so the pulse on the button never replays it.
180
- _jsx("span", { className: "absolute bottom-3 left-1/2 z-10 -translate-x-1/2 animate-in fade-in-0 slide-in-from-bottom-1", children: _jsxs("button", { type: "button", "data-slot": "thread-jump", "data-arrived": arrived ? "" : undefined, onClick: () => {
191
+ _jsx("span", { style: jumpMotion, className: "absolute bottom-3 left-1/2 z-10 -translate-x-1/2 cd-arrive", children: _jsxs("button", { type: "button", "data-slot": "thread-jump", "data-arrived": arrived ? "" : undefined, onClick: () => {
181
192
  pinnedRef.current = true;
182
193
  setPinned(true);
183
194
  toBottom("smooth");
184
- }, className: cn("inline-flex items-center gap-1.5 rounded-full border bg-popover px-3 py-1.5 text-xs font-medium shadow-float [--cd-attention-color:var(--primary)]", arrived && "animate-attention"), children: [_jsx(ArrowDown, { "aria-hidden": true, className: "size-3" }), " ", t("jumpToLatest")] }) })), fade && _jsx("div", { "aria-hidden": true, "data-slot": "thread-fade", className: "pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-background to-transparent" })] }), footer && _jsx("div", { className: "mx-auto w-full max-w-(--cd-thread-width) px-4 pb-4", children: footer })] }));
195
+ }, style: arrivalMotion, className: cn("inline-flex items-center gap-1.5 rounded-full border bg-popover px-3 py-1.5 text-xs font-medium shadow-float [--cd-attention-color:var(--primary)]", arrived && "cd-attention"), children: [_jsx(ArrowDown, { "aria-hidden": true, className: "size-3" }), " ", t("jumpToLatest")] }) })), fade && _jsx("div", { "aria-hidden": true, "data-slot": "thread-fade", className: "pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-background to-transparent" })] }), footer && _jsx("div", { className: "mx-auto w-full max-w-(--cd-thread-width) px-4 pb-4", children: footer })] }));
185
196
  }
186
197
  export { Thread };
@@ -9,6 +9,10 @@ import { useStatusLabel } from "../lib/status.js";
9
9
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
10
10
  import { Payload } from "./payload.js";
11
11
  import { useStrings } from "../blocks/locale.js";
12
+ import { motion } from "../lib/motion.js";
13
+ /** A call's detail opening, and a group of calls. */
14
+ const openMotion = motion("tool-call", "open", "reveal");
15
+ const groupMotion = motion("tool-call", "group-open", "reveal");
12
16
  /** ToolCall's fixed words; override them under `toolCall` in LocaleProvider's `strings`. */
13
17
  export const toolCallStrings = {
14
18
  input: "Input",
@@ -35,7 +39,7 @@ function ToolCall({ call, className, }) {
35
39
  const failed = call.status === "failed";
36
40
  const [open, setOpen] = React.useState(failed);
37
41
  const hasDetail = Boolean(call.input || call.output || call.error || call.approval?.note || call.approval?.originalInput);
38
- return (_jsxs(Collapsible, { open: open, onOpenChange: setOpen, "data-slot": "tool-call", "data-status": call.status, className: cn("group/tool", className), children: [_jsxs(CollapsibleTrigger, { disabled: !hasDetail, className: "flex w-full items-center gap-2.5 rounded-md px-1.5 py-1 text-start outline-none transition-colors duration-150 enabled:hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/80", children: [_jsx(StepMark, { status: call.status }), _jsxs("span", { className: "min-w-0 flex-1 truncate text-sm", children: [call.status === "running" ? _jsx(ShimmerLabel, { className: "font-normal", children: call.title }) : call.title, _jsxs("span", { className: "sr-only", children: [" \u2014 ", statusLabel(call.status)] })] }), call.approval?.originalInput !== undefined && (_jsx("span", { "data-slot": "tool-call-edited", className: "shrink-0 rounded-xs bg-warning-weak px-1 text-2xs text-warning-text", children: t("editedBeforeRun") })), call.approval?.by === "automatic" && (_jsx("span", { "data-slot": "tool-call-approval", className: "shrink-0 rounded-xs bg-muted px-1 text-2xs text-muted-foreground", children: t("approvedAutomatically") })), _jsx("code", { className: "hidden shrink-0 font-mono text-2xs text-faint-foreground sm:inline", children: call.tool }), call.duration && _jsx("span", { className: "shrink-0 text-xs tabular-nums text-faint-foreground", children: call.duration }), hasDetail && (_jsx(ChevronRight, { "aria-hidden": true, className: "rtl:-scale-x-100 size-3.5 shrink-0 text-faint-foreground transition-transform duration-200 ease-standard group-data-[state=open]/tool:[transform:rotate(90deg)]" }))] }), _jsx(CollapsibleContent, { children: _jsxs("div", { className: "mt-1 mb-2 ms-[1.375rem] flex flex-col gap-2 border-s ps-3.5", children: [call.error && (_jsx("p", { className: "rounded-md bg-destructive-weak px-2.5 py-1.5 text-xs text-destructive-text", children: call.error })), call.approval?.note && (_jsxs("p", { className: "text-xs text-muted-foreground", children: [call.approval.by === "automatic" ? t("approvedAutomatically") : t("approvedByPerson"), ": ", call.approval.note] })), call.approval?.originalInput !== undefined && _jsx("p", { className: "text-xs text-muted-foreground", children: t("editedNote") }), call.input && _jsx(Payload, { language: "json", title: t("input"), text: call.input }), call.approval?.originalInput !== undefined && _jsx(Payload, { language: "json", title: t("proposed"), text: call.approval.originalInput }), call.output && _jsx(Payload, { language: call.outputLanguage ?? "text", title: t("output"), text: call.output })] }) })] }));
42
+ return (_jsxs(Collapsible, { open: open, onOpenChange: setOpen, "data-slot": "tool-call", "data-status": call.status, className: cn("group/tool", className), children: [_jsxs(CollapsibleTrigger, { disabled: !hasDetail, className: "flex w-full items-center gap-2.5 rounded-md px-1.5 py-1 text-start outline-none cd-tint enabled:hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/80", children: [_jsx(StepMark, { status: call.status }), _jsxs("span", { className: "min-w-0 flex-1 truncate text-sm", children: [call.status === "running" ? _jsx(ShimmerLabel, { className: "font-normal", children: call.title }) : call.title, _jsxs("span", { className: "sr-only", children: [" \u2014 ", statusLabel(call.status)] })] }), call.approval?.originalInput !== undefined && (_jsx("span", { "data-slot": "tool-call-edited", className: "shrink-0 rounded-xs bg-warning-weak px-1 text-2xs text-warning-text", children: t("editedBeforeRun") })), call.approval?.by === "automatic" && (_jsx("span", { "data-slot": "tool-call-approval", className: "shrink-0 rounded-xs bg-muted px-1 text-2xs text-muted-foreground", children: t("approvedAutomatically") })), _jsx("code", { className: "hidden shrink-0 font-mono text-2xs text-faint-foreground sm:inline", children: call.tool }), call.duration && _jsx("span", { className: "shrink-0 text-xs tabular-nums text-faint-foreground", children: call.duration }), hasDetail && (_jsx(ChevronRight, { "aria-hidden": true, style: openMotion, className: "rtl:-scale-x-100 size-3.5 shrink-0 text-faint-foreground cd-reveal-turn group-data-[state=open]/tool:[transform:rotate(90deg)]" }))] }), _jsx(CollapsibleContent, { style: openMotion, children: _jsxs("div", { className: "mt-1 mb-2 ms-[1.375rem] flex flex-col gap-2 border-s ps-3.5", children: [call.error && (_jsx("p", { className: "rounded-md bg-destructive-weak px-2.5 py-1.5 text-xs text-destructive-text", children: call.error })), call.approval?.note && (_jsxs("p", { className: "text-xs text-muted-foreground", children: [call.approval.by === "automatic" ? t("approvedAutomatically") : t("approvedByPerson"), ": ", call.approval.note] })), call.approval?.originalInput !== undefined && _jsx("p", { className: "text-xs text-muted-foreground", children: t("editedNote") }), call.input && _jsx(Payload, { language: "json", title: t("input"), text: call.input }), call.approval?.originalInput !== undefined && _jsx(Payload, { language: "json", title: t("proposed"), text: call.approval.originalInput }), call.output && _jsx(Payload, { language: call.outputLanguage ?? "text", title: t("output"), text: call.output })] }) })] }));
39
43
  }
40
44
  /** A run of tool calls, with a count when it's long enough to fold. */
41
45
  function ToolCallGroup({ calls, className, }) {
@@ -47,6 +51,6 @@ function ToolCallGroup({ calls, className, }) {
47
51
  if (calls.length <= 2) {
48
52
  return (_jsx("div", { "data-slot": "tool-call-group", className: cn("flex flex-col", className), children: calls.map((c) => (_jsx(ToolCall, { call: c }, c.id))) }));
49
53
  }
50
- return (_jsxs(Collapsible, { open: open, onOpenChange: setOpen, "data-slot": "tool-call-group", className: cn("group/tools rounded-lg border bg-card", className), children: [_jsxs(CollapsibleTrigger, { className: "flex w-full items-center gap-2 px-3 py-2 text-start text-xs text-muted-foreground outline-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/80 rounded-lg", children: [_jsx(Wrench, { "aria-hidden": true, className: "size-3.5" }), _jsxs("span", { className: "flex-1", children: [t("calls", { count: calls.length }), done > 0 && ` · ${t("done", { count: done })}`, failed > 0 && _jsxs("span", { className: "text-destructive-text", children: [" \u00B7 ", t("failed", { count: failed })] }), running && ` · ${t("working")}`] }), _jsx(ChevronRight, { "aria-hidden": true, className: "rtl:-scale-x-100 size-3.5 transition-transform duration-200 ease-standard group-data-[state=open]/tools:[transform:rotate(90deg)]" })] }), _jsx(CollapsibleContent, { children: _jsx("div", { className: "flex flex-col border-t px-1.5 py-1.5", children: calls.map((c) => (_jsx(ToolCall, { call: c }, c.id))) }) })] }));
54
+ return (_jsxs(Collapsible, { open: open, onOpenChange: setOpen, "data-slot": "tool-call-group", className: cn("group/tools rounded-lg border bg-card", className), children: [_jsxs(CollapsibleTrigger, { className: "flex w-full items-center gap-2 px-3 py-2 text-start text-xs text-muted-foreground outline-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/80 rounded-lg", children: [_jsx(Wrench, { "aria-hidden": true, className: "size-3.5" }), _jsxs("span", { className: "flex-1", children: [t("calls", { count: calls.length }), done > 0 && ` · ${t("done", { count: done })}`, failed > 0 && _jsxs("span", { className: "text-destructive-text", children: [" \u00B7 ", t("failed", { count: failed })] }), running && ` · ${t("working")}`] }), _jsx(ChevronRight, { "aria-hidden": true, style: groupMotion, className: "rtl:-scale-x-100 size-3.5 cd-reveal-turn group-data-[state=open]/tools:[transform:rotate(90deg)]" })] }), _jsx(CollapsibleContent, { style: groupMotion, children: _jsx("div", { className: "flex flex-col border-t px-1.5 py-1.5", children: calls.map((c) => (_jsx(ToolCall, { call: c }, c.id))) }) })] }));
51
55
  }
52
56
  export { ToolCall, ToolCallGroup };
package/dist/index.d.ts CHANGED
@@ -43,6 +43,14 @@ export * from "./lib/render-prop.js";
43
43
  export * from "./chat/ai-disclosure.js";
44
44
  export * from "./chat/permission-prompt.js";
45
45
  export * from "./lib/mcp-input.js";
46
+ export * from "./lib/motion.js";
47
+ export * from "./ui/label.js";
48
+ export * from "./ui/checkbox.js";
49
+ export * from "./ui/switch.js";
50
+ export * from "./ui/slider.js";
51
+ export * from "./ui/progress.js";
52
+ export * from "./ui/alert.js";
53
+ export * from "./ui/accordion.js";
46
54
  export * from "./lib/utils.js";
47
55
  export { ToolCall } from "./chat/tool-call.js";
48
56
  export type { ToolCall as ToolCallData } from "./lib/chat.js";
package/dist/index.js CHANGED
@@ -43,6 +43,14 @@ export * from "./lib/render-prop.js";
43
43
  export * from "./chat/ai-disclosure.js";
44
44
  export * from "./chat/permission-prompt.js";
45
45
  export * from "./lib/mcp-input.js";
46
+ export * from "./lib/motion.js";
47
+ export * from "./ui/label.js";
48
+ export * from "./ui/checkbox.js";
49
+ export * from "./ui/switch.js";
50
+ export * from "./ui/slider.js";
51
+ export * from "./ui/progress.js";
52
+ export * from "./ui/alert.js";
53
+ export * from "./ui/accordion.js";
46
54
  export * from "./lib/utils.js";
47
55
  export { ToolCall } from "./chat/tool-call.js";
48
56
  export { Message } from "./chat/message.js";