@candor-ui/react 1.3.0 → 1.7.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 (74) hide show
  1. package/dist/adapters/ag-ui.d.ts +81 -16
  2. package/dist/adapters/ag-ui.js +308 -1
  3. package/dist/adapters/ai-sdk.d.ts +11 -6
  4. package/dist/adapters/ai-sdk.js +18 -1
  5. package/dist/adapters/claude-agent-sdk.d.ts +123 -14
  6. package/dist/adapters/claude-agent-sdk.js +163 -5
  7. package/dist/adapters/claude-agent-server.d.ts +199 -14
  8. package/dist/adapters/claude-agent-server.js +448 -49
  9. package/dist/adapters/use-ag-ui.d.ts +12 -6
  10. package/dist/adapters/use-ag-ui.js +23 -5
  11. package/dist/adapters/use-candor-chat.d.ts +1 -1
  12. package/dist/adapters/use-candor-chat.js +3 -3
  13. package/dist/adapters/use-claude-agent.d.ts +38 -9
  14. package/dist/adapters/use-claude-agent.js +96 -20
  15. package/dist/blocks/copy-field.d.ts +1 -1
  16. package/dist/blocks/copy-field.js +1 -1
  17. package/dist/blocks/degraded-panel.d.ts +5 -5
  18. package/dist/blocks/locale.d.ts +6 -6
  19. package/dist/blocks/locked.d.ts +8 -8
  20. package/dist/blocks/relative-time.d.ts +6 -6
  21. package/dist/blocks/status-dot.d.ts +3 -3
  22. package/dist/blocks/step-list.d.ts +3 -3
  23. package/dist/blocks/streaming-text.d.ts +3 -3
  24. package/dist/chat/agent-avatar.d.ts +4 -4
  25. package/dist/chat/ai-disclosure.d.ts +37 -0
  26. package/dist/chat/ai-disclosure.js +48 -0
  27. package/dist/chat/attachment-preview.d.ts +6 -6
  28. package/dist/chat/attachment-preview.js +2 -2
  29. package/dist/chat/card-registry.js +1 -1
  30. package/dist/chat/citation.d.ts +3 -3
  31. package/dist/chat/citation.js +6 -4
  32. package/dist/chat/composer.d.ts +31 -20
  33. package/dist/chat/composer.js +10 -6
  34. package/dist/chat/context-meter.d.ts +1 -1
  35. package/dist/chat/context-meter.js +1 -1
  36. package/dist/chat/entity-chip.d.ts +3 -3
  37. package/dist/chat/entity-chip.js +1 -1
  38. package/dist/chat/folded-turns.d.ts +11 -6
  39. package/dist/chat/folded-turns.js +2 -2
  40. package/dist/chat/message-actions.d.ts +6 -6
  41. package/dist/chat/message-actions.js +1 -1
  42. package/dist/chat/message-content.d.ts +13 -7
  43. package/dist/chat/message-content.js +10 -5
  44. package/dist/chat/message.d.ts +64 -37
  45. package/dist/chat/message.js +17 -11
  46. package/dist/chat/payload.d.ts +4 -4
  47. package/dist/chat/payload.js +5 -2
  48. package/dist/chat/permission-prompt.d.ts +129 -0
  49. package/dist/chat/permission-prompt.js +144 -0
  50. package/dist/chat/prompt-suggestions.d.ts +4 -4
  51. package/dist/chat/prompt-suggestions.js +1 -1
  52. package/dist/chat/reasoning.d.ts +3 -3
  53. package/dist/chat/reasoning.js +1 -1
  54. package/dist/chat/thread-divider.d.ts +3 -3
  55. package/dist/chat/thread-event.d.ts +10 -5
  56. package/dist/chat/thread-event.js +3 -2
  57. package/dist/chat/thread.d.ts +23 -7
  58. package/dist/chat/thread.js +29 -6
  59. package/dist/chat/tool-call.d.ts +7 -2
  60. package/dist/chat/tool-call.js +8 -3
  61. package/dist/index.d.ts +2 -0
  62. package/dist/index.js +2 -0
  63. package/dist/lib/chat.d.ts +53 -38
  64. package/dist/lib/know.d.ts +14 -12
  65. package/dist/lib/render-prop.d.ts +4 -4
  66. package/dist/lib/safe-markdown.d.ts +1 -1
  67. package/dist/lib/status.d.ts +21 -0
  68. package/dist/lib/status.js +17 -0
  69. package/dist/lib/use-shortcut.d.ts +1 -1
  70. package/dist/lib/use-virtual-list.d.ts +6 -6
  71. package/dist/styles.css +4 -1
  72. package/dist/ui/button.d.ts +1 -1
  73. package/dist/ui/button.js +1 -1
  74. package/package.json +1 -1
@@ -26,7 +26,8 @@ export const messageStrings = {
26
26
  /**
27
27
  * One message in a thread.
28
28
  *
29
- * - A person's message sits right, on the user surface.
29
+ * - A person's message sits right, on the user surface, with their name and
30
+ * time under it (`meta` hides or replaces that line).
30
31
  * - An agent's sits left, under its avatar, name and role, with reasoning above
31
32
  * the answer, sources and actions below. While streaming, a caret follows the
32
33
  * last block; an interrupted reply keeps what arrived and says it stopped.
@@ -35,8 +36,13 @@ export const messageStrings = {
35
36
  * - A system line is centred and quiet.
36
37
  * - A reply a person checked (or edited) before it went out can say so in a
37
38
  * caption under it, so a reader knows a person stood behind it.
39
+ * - Times are relative ("5 minutes ago") unless `timeFormat` asks for an
40
+ * absolute "time", "date" or "datetime" in the locale.
41
+ *
42
+ * Build `blocks` with `plainBlocks(text)` for a person's words and
43
+ * `markdownBlocks(text)` for an agent's, from `safe-markdown`.
38
44
  */
39
- function Message({ message, onOpenEntity, onNavigate, onOpenAttachment, onRetry, onContinue, rating, onRate, reviewedBy, actions, className, }) {
45
+ function Message({ message, onOpenEntity, onNavigate, onOpenAttachment, onRetry, onContinue, rating, onRate, reviewedBy, footer, actions, meta, timeFormat, density = "default", bareCode, className, }) {
40
46
  const { author } = message;
41
47
  const roleOf = useAgentRoles();
42
48
  const t = useStrings("message", messageStrings);
@@ -44,24 +50,24 @@ function Message({ message, onOpenEntity, onNavigate, onOpenAttachment, onRetry,
44
50
  return (_jsx(ThreadEvent, { "data-slot": "message", "data-role": "system", className: className, children: _jsx(MessageContent, { blocks: message.blocks, className: "text-xs leading-normal" }) }));
45
51
  }
46
52
  if (author.role === "user") {
47
- 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, className: "gap-2" }) }), _jsxs("span", { className: "px-1 text-2xs text-faint-foreground", children: [author.name ?? t("you"), message.createdAt && (_jsxs(_Fragment, { children: [" · ", _jsx(RelativeTime, { date: message.createdAt })] }))] })] }) }));
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)] }) }));
48
54
  }
49
55
  if (author.role === "person") {
50
- 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 }), _jsx(MessageContent, { blocks: message.blocks, sources: message.sources, onOpenEntity: onOpenEntity, onNavigate: onNavigate, onOpenAttachment: onOpenAttachment, className: "mt-1" })] })] }));
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" })] })] }));
51
57
  }
52
58
  const { agent } = author;
53
59
  const streaming = message.status === "streaming";
54
- 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 }), _jsxs("div", { className: "mt-1 flex flex-col gap-3", children: [_jsx(MessageBody, { message: message, onOpenEntity: onOpenEntity, onNavigate: onNavigate, onOpenAttachment: onOpenAttachment }), _jsx(MessageFooter, { message: message, onRetry: onRetry, onContinue: onContinue, rating: rating, onRate: onRate, reviewedBy: reviewedBy, actions: actions })] })] })] }));
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 })] })] })] }));
55
61
  }
56
62
  /** The row above a reply: who sent it, a detail (their specialty or team), and when. */
57
- function MessageHeader({ name, detail, createdAt, children, className, }) {
58
- return (_jsxs("header", { "data-slot": "message-header", className: cn("flex h-7 items-center gap-2", className), children: [_jsx("span", { className: "text-sm font-semibold", children: name }), detail && _jsx("span", { className: "truncate text-xs text-muted-foreground", children: detail }), createdAt && (_jsx("span", { className: "text-xs text-faint-foreground", children: _jsx(RelativeTime, { date: createdAt }) })), children] }));
63
+ function MessageHeader({ name, detail, createdAt, timeFormat, density = "default", children, className, }) {
64
+ return (_jsxs("header", { "data-slot": "message-header", "data-density": density, className: cn("flex items-center gap-2", density === "compact" ? "h-6" : "h-7", className), children: [_jsx("span", { className: cn("font-semibold", density === "compact" ? "text-xs" : "text-sm"), children: name }), detail && _jsx("span", { className: cn("truncate text-muted-foreground", density === "compact" ? "text-2xs" : "text-xs"), children: detail }), createdAt && (_jsx("span", { className: cn("text-faint-foreground", density === "compact" ? "text-2xs" : "text-xs"), children: _jsx(RelativeTime, { date: createdAt, format: timeFormat }) })), children] }));
59
65
  }
60
66
  /** A reply's content: its reasoning, then its blocks, with a caret after the last one while it streams. */
61
- function MessageBody({ message, onOpenEntity, onNavigate, onOpenAttachment, className, }) {
67
+ function MessageBody({ message, onOpenEntity, onNavigate, onOpenAttachment, bareCode, className, }) {
62
68
  const streaming = message.status === "streaming";
63
69
  const thinking = streaming && message.blocks.length === 0;
64
- 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 }), 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" })] }))] }));
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" })] }))] }));
65
71
  }
66
72
  /** Model and cost, on a line of their own: what the actions row shows, for when it's hidden or replaced. */
67
73
  function MessageAttribution({ attribution, className, }) {
@@ -75,11 +81,11 @@ function MessageAttribution({ attribution, className, }) {
75
81
  * and cost. `actions={false}` hides the row and a node replaces it; either way the model and cost stay, on their own
76
82
  * line, because a reader should always see what produced the answer.
77
83
  */
78
- function MessageFooter({ message, onRetry, onContinue, rating, onRate, reviewedBy, actions, className, }) {
84
+ function MessageFooter({ message, onRetry, onContinue, rating, onRate, reviewedBy, footer, actions, timeFormat, className, }) {
79
85
  const t = useStrings("message", messageStrings);
80
86
  const streaming = message.status === "streaming";
81
87
  const settled = !streaming && message.status !== "failed";
82
88
  const meta = [message.attribution?.model, message.attribution?.cost].filter(Boolean).join(" · ");
83
- 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: "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 })] }))] })), 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 })] }));
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 })] }));
84
90
  }
85
91
  export { Message, MessageHeader, MessageBody, MessageFooter, MessageAttribution };
@@ -23,11 +23,11 @@ declare function Payload({ language, text, title, bare, highlight, className, }:
23
23
  /** The code or query, shown as text. */
24
24
  text: string;
25
25
  /** What it is, e.g. "The query I ran". Not shown when `bare`. */
26
- title?: string;
26
+ title?: string | undefined;
27
27
  /** Just the code: no border, language label or copy button, for inside something that has them. */
28
- bare?: boolean;
28
+ bare?: boolean | undefined;
29
29
  /** Colour the code by its syntax. Left out, it follows the nearest `data-syntax` attribute (off by default). */
30
- highlight?: boolean;
31
- className?: string;
30
+ highlight?: boolean | undefined;
31
+ className?: string | undefined;
32
32
  }): React.JSX.Element;
33
33
  export { Payload };
@@ -39,9 +39,12 @@ function Payload({ language, text, title, bare = false, highlight, className, })
39
39
  const syntax = highlight === undefined ? undefined : highlight ? "highlight" : "off";
40
40
  const code = _jsx(Code, { text: text, language: language });
41
41
  if (bare) {
42
- return (_jsx("pre", { "data-slot": "payload", "data-bare": "", "data-language": language, "data-syntax": syntax, className: cn("max-h-80 overflow-auto rounded-md bg-sunken px-3 py-2.5 font-mono text-xs leading-relaxed text-foreground", className), children: code }));
42
+ return (
43
+ // It scrolls on its own, so it takes focus: keyboard users can scroll long output.
44
+ // oxlint-disable-next-line jsx-a11y/no-noninteractive-tabindex
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 }));
43
46
  }
44
- 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: "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/40", 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", { className: "max-h-80 overflow-auto px-3 py-2.5 font-mono text-xs leading-relaxed text-foreground", 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 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 })] }));
45
48
  }
46
49
  /** The code as token spans. Each token is a text child, so nothing in it is parsed as markup. */
47
50
  function Code({ text, language }) {
@@ -0,0 +1,129 @@
1
+ import * as React from "react";
2
+ /** PermissionPrompt's fixed words; override them under `permissionPrompt` in LocaleProvider's `strings`. */
3
+ export declare const permissionPromptStrings: {
4
+ label: string;
5
+ fallbackTitle: string;
6
+ why: string;
7
+ mode: string;
8
+ modeDefault: string;
9
+ modeAcceptEdits: string;
10
+ modePlan: string;
11
+ modeAuto: string;
12
+ modeDontAsk: string;
13
+ modeBypassPermissions: string;
14
+ blockedPath: string;
15
+ subagent: string;
16
+ server: string;
17
+ input: string;
18
+ edit: string;
19
+ editLabel: string;
20
+ editInvalid: string;
21
+ changed: string;
22
+ unchanged: string;
23
+ cancelEdit: string;
24
+ remember: string;
25
+ rememberHint: string;
26
+ ruleAllow: string;
27
+ ruleDeny: string;
28
+ ruleAsk: string;
29
+ ruleRemove: string;
30
+ setMode: string;
31
+ warnBypassPermissions: string;
32
+ warnAuto: string;
33
+ directories: string;
34
+ other: string;
35
+ scopeSession: string;
36
+ scopeLocal: string;
37
+ scopeProject: string;
38
+ scopeUser: string;
39
+ allow: string;
40
+ allowEdited: string;
41
+ deny: string;
42
+ answer: string;
43
+ otherOption: string;
44
+ otherLabel: string;
45
+ questions: string;
46
+ };
47
+ /** One "don't ask again" choice, in the plain shape PermissionPrompt shows. claudeSuggestions builds them from the SDK's. */
48
+ export type PermissionSuggestion = {
49
+ /** What the rule does. */
50
+ action: "allow" | "deny" | "ask" | "remove" | "mode" | "directories" | "other";
51
+ /** What it covers, as the source writes it: `Bash(npm test:*)`, a mode's name, directories. Shown as code. */
52
+ what: string;
53
+ /** Where it's kept: "session", "local" (this project, this computer), "project" (shared), "user", or the source's own word. */
54
+ scope?: string | undefined;
55
+ };
56
+ /** A question the agent asks through the prompt: pick one or more labels, or answer in its own words. */
57
+ export type PermissionQuestion = {
58
+ id: string;
59
+ prompt: string;
60
+ tag?: string | undefined;
61
+ options: {
62
+ id: string;
63
+ label: string;
64
+ detail?: string | undefined;
65
+ }[];
66
+ multiple: boolean;
67
+ other?: boolean | undefined;
68
+ };
69
+ /** A tool call waiting on a person, in the shape the adapters give (ClaudeApproval fits it). */
70
+ export type PermissionRequest = {
71
+ id: string;
72
+ tool: string;
73
+ input: unknown;
74
+ /** The source's own sentence for the request: "Claude wants to run npm test". */
75
+ title?: string | undefined;
76
+ description?: string | undefined;
77
+ /** Why it reached a person (no rule allowed it, a safety check, a path outside the project). */
78
+ reason?: string | undefined;
79
+ blockedPath?: string | undefined;
80
+ /** Put the focus on Deny: the request grants more than it looks. */
81
+ defaultToNo?: boolean | undefined;
82
+ /** Set when a subagent asked. */
83
+ agentId?: string | undefined;
84
+ mcpServer?: {
85
+ name: string;
86
+ source: string;
87
+ } | undefined;
88
+ /** The questions, when the request is the agent asking rather than a tool to allow. */
89
+ questions?: PermissionQuestion[] | undefined;
90
+ /** False when the server won't accept an edited input for this call: no Edit is offered, whatever `editable` says. */
91
+ editable?: boolean | undefined;
92
+ };
93
+ export type PermissionAnswer = {
94
+ /** The input as edited, when the person changed it. */
95
+ updatedInput?: Record<string, unknown> | undefined;
96
+ /** The suggestions the person ticked, by index. */
97
+ alwaysAllow?: number[] | undefined;
98
+ /** Answers to `questions`. */
99
+ answers?: {
100
+ questionId: string;
101
+ choice?: string[] | undefined;
102
+ text?: string | undefined;
103
+ }[] | undefined;
104
+ };
105
+ /**
106
+ * A tool call waiting for a person's go-ahead: what it will do, why it reached them, and the permission mode the
107
+ * run is in. They can edit the input before allowing it and see exactly what they changed; Allow then sends that
108
+ * input, nothing else. "Don't ask again" rules are offered as boxes nobody has ticked, and are kept only if the
109
+ * person ticks them. When the agent is asking questions instead, it shows them as a short form. Words from the
110
+ * request (its title, reason, input) are shown as text, never markup.
111
+ */
112
+ declare function PermissionPrompt({ request, mode, suggestions, editable, onAllow, onDeny, busy, className, }: {
113
+ /** The waiting request. */
114
+ request: PermissionRequest;
115
+ /** The run's permission mode, as the SDK names it ("default", "acceptEdits", "plan", "auto"…). Left out, it isn't shown. */
116
+ mode?: string | undefined;
117
+ /** "Don't ask again" rules to offer. None is ticked. */
118
+ suggestions?: PermissionSuggestion[] | undefined;
119
+ /** Let the person edit the input as JSON before allowing it. */
120
+ editable?: boolean | undefined;
121
+ /** Called with the edited input, the ticked rules or the answers. */
122
+ onAllow: (answer: PermissionAnswer) => void;
123
+ /** Called when the person denies it (or won't answer the questions). */
124
+ onDeny: () => void;
125
+ /** The answer is being sent: both buttons wait. */
126
+ busy?: boolean | undefined;
127
+ className?: string | undefined;
128
+ }): React.JSX.Element;
129
+ export { PermissionPrompt };
@@ -0,0 +1,144 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { Info, Pencil, ShieldAlert, ShieldQuestion } from "lucide-react";
5
+ import { cn } from "../lib/utils.js";
6
+ import { useStrings } from "../blocks/locale.js";
7
+ import { Payload } from "./payload.js";
8
+ import { Button } from "../ui/button.js";
9
+ /** PermissionPrompt's fixed words; override them under `permissionPrompt` in LocaleProvider's `strings`. */
10
+ export const permissionPromptStrings = {
11
+ label: "Permission request: {title}",
12
+ fallbackTitle: "The agent wants to use {tool}",
13
+ why: "Why you’re asked",
14
+ mode: "Permission mode",
15
+ modeDefault: "Ask before acting",
16
+ modeAcceptEdits: "Accept file edits",
17
+ modePlan: "Plan only",
18
+ modeAuto: "Automatic, by a classifier",
19
+ modeDontAsk: "Don’t ask; deny what isn’t allowed",
20
+ modeBypassPermissions: "Allow everything",
21
+ blockedPath: "Path outside what’s allowed: {path}",
22
+ subagent: "A subagent asked.",
23
+ server: "From the tool server “{name}”, configured in {source}.",
24
+ input: "What it will run with",
25
+ edit: "Edit before allowing",
26
+ editLabel: "Input, as JSON",
27
+ editInvalid: "That isn’t a JSON object, so it can’t be used.",
28
+ changed: "You changed",
29
+ unchanged: "Nothing changed yet.",
30
+ cancelEdit: "Use the original",
31
+ remember: "Don’t ask again for",
32
+ rememberHint: "Optional. Nothing is remembered unless you tick it.",
33
+ ruleAllow: "Allow {what}",
34
+ ruleDeny: "Always deny {what}",
35
+ ruleAsk: "Always ask for {what}",
36
+ ruleRemove: "Stop applying the rule for {what}",
37
+ setMode: "Switch to: {mode}",
38
+ warnBypassPermissions: "After this, nothing is checked: the agent can run any command and change or delete any file it can reach.",
39
+ warnAuto: "After this, a classifier allows or blocks each action instead of you.",
40
+ directories: "Access to {what}",
41
+ other: "A permission change: {what}",
42
+ scopeSession: "for this session",
43
+ scopeLocal: "in this project, on this computer",
44
+ scopeProject: "in this project, for everyone on it",
45
+ scopeUser: "for you, in every project",
46
+ allow: "Allow",
47
+ allowEdited: "Allow as edited",
48
+ deny: "Deny",
49
+ answer: "Send answers",
50
+ otherOption: "Other",
51
+ otherLabel: "Your own answer to: {question}",
52
+ questions: "The agent has questions",
53
+ };
54
+ const modeKeys = {
55
+ default: "modeDefault",
56
+ acceptEdits: "modeAcceptEdits",
57
+ plan: "modePlan",
58
+ auto: "modeAuto",
59
+ dontAsk: "modeDontAsk",
60
+ bypassPermissions: "modeBypassPermissions",
61
+ };
62
+ const json = (v) => (typeof v === "string" ? v : JSON.stringify(v, null, 2));
63
+ /** Parses edited JSON into an object, or null when it isn't one. */
64
+ function parseObject(text) {
65
+ try {
66
+ const v = JSON.parse(text);
67
+ return v && typeof v === "object" && !Array.isArray(v) ? v : null;
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
73
+ /**
74
+ * A tool call waiting for a person's go-ahead: what it will do, why it reached them, and the permission mode the
75
+ * run is in. They can edit the input before allowing it and see exactly what they changed; Allow then sends that
76
+ * input, nothing else. "Don't ask again" rules are offered as boxes nobody has ticked, and are kept only if the
77
+ * person ticks them. When the agent is asking questions instead, it shows them as a short form. Words from the
78
+ * request (its title, reason, input) are shown as text, never markup.
79
+ */
80
+ function PermissionPrompt({ request, mode, suggestions, editable = false, onAllow, onDeny, busy = false, className, }) {
81
+ // Keyed by the request, so an edit, a ticked rule or an answer never carries over to the next request.
82
+ return _jsx(PromptBody, { request, mode, suggestions, editable: editable && request.editable !== false, onAllow, onDeny, busy, className }, request.id);
83
+ }
84
+ function PromptBody({ request, mode, suggestions, editable, onAllow, onDeny, busy, className, }) {
85
+ const t = useStrings("permissionPrompt", permissionPromptStrings);
86
+ const id = React.useId();
87
+ const original = React.useMemo(() => json(request.input ?? {}), [request.input]);
88
+ const [editing, setEditing] = React.useState(false);
89
+ const [draft, setDraft] = React.useState(original);
90
+ const [ticked, setTicked] = React.useState([]);
91
+ const [answers, setAnswers] = React.useState({});
92
+ const edited = editing ? parseObject(draft) : null;
93
+ const proposed = parseObject(original);
94
+ const changedKeys = edited && proposed ? [...new Set([...Object.keys(proposed), ...Object.keys(edited)])].filter((k) => JSON.stringify(proposed[k]) !== JSON.stringify(edited[k])) : [];
95
+ const invalid = editing && !edited;
96
+ const title = request.title ?? t("fallbackTitle", { tool: request.tool });
97
+ const questions = request.questions;
98
+ // A request that grants more than it looks opens on Deny, so a stray Enter can't allow it.
99
+ const denyRef = React.useRef(null);
100
+ React.useEffect(() => {
101
+ if (request.defaultToNo)
102
+ denyRef.current?.focus();
103
+ }, [request.id, request.defaultToNo]);
104
+ const word = (s) => {
105
+ const key = { allow: "ruleAllow", deny: "ruleDeny", ask: "ruleAsk", remove: "ruleRemove", mode: "setMode", directories: "directories", other: "other" }[s.action];
106
+ const scope = s.scope && { session: "scopeSession", local: "scopeLocal", project: "scopeProject", user: "scopeUser" }[s.scope];
107
+ const what = s.action === "mode" && modeKeys[s.what] ? t(modeKeys[s.what]) : s.what;
108
+ return { text: t(key, { what, mode: what }), scope: scope ? t(scope) : s.scope };
109
+ };
110
+ const answered = (q) => {
111
+ const a = answers[q.id];
112
+ return Boolean(a && (a.choice.length || (a.other && a.text.trim())));
113
+ };
114
+ const choose = (q, option) => setAnswers((prev) => {
115
+ const a = prev[q.id] ?? { choice: [], text: "", other: false };
116
+ const choice = q.multiple ? (a.choice.includes(option) ? a.choice.filter((c) => c !== option) : [...a.choice, option]) : [option];
117
+ return { ...prev, [q.id]: { ...a, choice, other: q.multiple ? a.other : false } };
118
+ });
119
+ const toggleOther = (q) => setAnswers((prev) => {
120
+ const a = prev[q.id] ?? { choice: [], text: "", other: false };
121
+ return { ...prev, [q.id]: { ...a, other: !a.other, choice: q.multiple || a.other ? a.choice : [] } };
122
+ });
123
+ const allow = () => {
124
+ if (busy)
125
+ return;
126
+ if (questions) {
127
+ if (!questions.every(answered))
128
+ return;
129
+ onAllow({ answers: questions.map((q) => ({ questionId: q.id, choice: answers[q.id]?.choice, text: answers[q.id]?.other ? answers[q.id]?.text.trim() : undefined })) });
130
+ return;
131
+ }
132
+ if (invalid)
133
+ return;
134
+ onAllow({ ...(edited && changedKeys.length && { updatedInput: edited }), ...(ticked.length && { alwaysAllow: ticked }) });
135
+ };
136
+ return (_jsxs("section", { "data-slot": "permission-prompt", "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", className), children: [_jsxs("header", { className: "flex items-start gap-2.5 bg-warning-weak/50 px-3.5 py-2.5", children: [_jsx(ShieldQuestion, { "aria-hidden": true, className: "mt-0.5 size-4 shrink-0 text-warning-text" }), _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) => {
137
+ const a = answers[q.id];
138
+ 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));
139
+ }) })) : (_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) => {
140
+ const w = word(s);
141
+ return (_jsxs("label", { className: "flex min-h-6 items-start gap-2", children: [_jsx("input", { type: "checkbox", checked: ticked.includes(i), onChange: (e) => setTicked((prev) => (e.target.checked ? [...prev, i] : prev.filter((x) => x !== i))), className: "mt-0.5 size-4 accent-[var(--primary)]" }), _jsxs("span", { children: [_jsx("span", { className: "font-mono text-xs", children: w.text }), w.scope && _jsxs("span", { className: "text-muted-foreground", children: [" \u00B7 ", w.scope] }), s.action === "mode" && (s.what === "bypassPermissions" || s.what === "auto") && (_jsxs("span", { className: "mt-0.5 flex items-start gap-1 text-xs text-warning-text", children: [_jsx(ShieldAlert, { "aria-hidden": true, className: "mt-px size-3.5 shrink-0" }), t(s.what === "auto" ? "warnAuto" : "warnBypassPermissions")] }))] })] }, i));
142
+ })] }))] })), _jsxs("footer", { className: "flex flex-wrap justify-end gap-2 border-t bg-sunken px-3.5 py-2", children: [_jsx(Button, { ref: denyRef, size: "sm", variant: "outline", onClick: onDeny, disabled: busy, children: t("deny") }), _jsx(Button, { size: "sm", onClick: allow, disabled: busy || invalid || (questions ? !questions.every(answered) : false), "aria-busy": busy, children: questions ? t("answer") : edited && changedKeys.length ? t("allowEdited") : t("allow") })] })] }));
143
+ }
144
+ export { PermissionPrompt };
@@ -14,11 +14,11 @@ declare function PromptSuggestions({ suggestions, onChoose, label: labelProp, lo
14
14
  /** Called with the chosen one; put it in the composer to edit rather than sending it. */
15
15
  onChoose: (suggestion: PromptSuggestion) => void;
16
16
  /** The heading above the suggestions; "Try asking" by default. */
17
- label?: string;
17
+ label?: string | undefined;
18
18
  /** Shows placeholders while suggestions load. */
19
- loading?: boolean;
19
+ loading?: boolean | undefined;
20
20
  /** Compact chips, or larger cards for an empty thread. */
21
- variant?: "chips" | "cards";
22
- className?: string;
21
+ variant?: "chips" | "cards" | undefined;
22
+ className?: string | undefined;
23
23
  }): import("react").JSX.Element | null;
24
24
  export { PromptSuggestions };
@@ -20,6 +20,6 @@ function PromptSuggestions({ suggestions, onChoose, label: labelProp, loading =
20
20
  return null;
21
21
  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
22
  ? 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/40", 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/40", children: s.text }, s.id))) })] }));
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))) })] }));
24
24
  }
25
25
  export { PromptSuggestions };
@@ -25,9 +25,9 @@ declare function Reasoning({ entries, thinking, thoughtFor, className, }: {
25
25
  /** The steps, each verified (with evidence), believed or assumed. */
26
26
  entries: ReasoningEntry[];
27
27
  /** The agent is still thinking; opens the panel and shows progress. */
28
- thinking?: boolean;
28
+ thinking?: boolean | undefined;
29
29
  /** How long it thought, once known, e.g. "6s". */
30
- thoughtFor?: string;
31
- className?: string;
30
+ thoughtFor?: string | undefined;
31
+ className?: string | undefined;
32
32
  }): React.JSX.Element;
33
33
  export { Reasoning };
@@ -35,6 +35,6 @@ function Reasoning({ entries, thinking = false, thoughtFor, className, }) {
35
35
  React.useEffect(() => setOpen(thinking), [thinking]);
36
36
  const assumed = entries.filter((e) => e.basis === "assumed").length;
37
37
  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: "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/40", 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))) }) })] }));
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))) }) })] }));
39
39
  }
40
40
  export { Reasoning };
@@ -10,9 +10,9 @@ export declare const threadDividerStrings: {
10
10
  */
11
11
  declare function ThreadDivider({ date, label, className, }: {
12
12
  /** A day to mark, shown as Today, Yesterday or the date in the locale. With neither this nor `label`, a plain rule. */
13
- date?: Date | string;
13
+ date?: Date | string | undefined;
14
14
  /** A chapter or any other label; wins over `date`. */
15
- label?: string;
16
- className?: string;
15
+ label?: string | undefined;
16
+ className?: string | undefined;
17
17
  }): import("react").JSX.Element;
18
18
  export { ThreadDivider };
@@ -1,19 +1,24 @@
1
1
  import * as React from "react";
2
2
  import { type LucideIcon } from "lucide-react";
3
+ import type { DateStyle } from "../blocks/locale.js";
3
4
  /**
4
5
  * A quiet, centred line in a thread for something that happened rather than something said: "Sam joined", "Switched
5
6
  * to the staging environment", "Run stopped by a guardrail". System messages are drawn with it too, so app events and
6
7
  * system lines look the same. Text here is the app's own, never model output.
7
8
  */
8
- declare function ThreadEvent({ icon: Icon, tone, at, children, className, ...props }: {
9
+ declare function ThreadEvent({ icon: Icon, tone, at, timeFormat, align, children, className, ...props }: {
9
10
  /** The glyph before the text; `null` for none. Default: an info mark. */
10
- icon?: LucideIcon | null;
11
+ icon?: LucideIcon | null | undefined;
11
12
  /** "quiet" (the default) or "attention", for an event a reader shouldn't miss. */
12
- tone?: "quiet" | "attention";
13
+ tone?: "quiet" | "attention" | undefined;
13
14
  /** When it happened, shown after the text as a relative time. */
14
- at?: string;
15
+ at?: string | undefined;
16
+ /** "relative" (the default) or an absolute "time", "date" or "datetime" in the locale. */
17
+ timeFormat?: "relative" | DateStyle | undefined;
18
+ /** "center" (the default), or "start": a left-aligned line (right-aligned in RTL) with the time at its end. */
19
+ align?: "center" | "start" | undefined;
15
20
  /** What happened, in a short line. */
16
21
  children: React.ReactNode;
17
- className?: string;
22
+ className?: string | undefined;
18
23
  } & Omit<React.ComponentProps<"div">, "children">): React.JSX.Element;
19
24
  export { ThreadEvent };
@@ -9,7 +9,8 @@ import { RelativeTime } from "../blocks/relative-time.js";
9
9
  * to the staging environment", "Run stopped by a guardrail". System messages are drawn with it too, so app events and
10
10
  * system lines look the same. Text here is the app's own, never model output.
11
11
  */
12
- function ThreadEvent({ icon: Icon = Info, tone = "quiet", at, children, className, ...props }) {
13
- return (_jsxs("div", { "data-slot": "thread-event", "data-tone": tone, className: cn("flex items-center justify-center gap-1.5 py-1 text-center text-xs", tone === "attention" ? "text-attention-text" : "text-faint-foreground", className), ...props, children: [Icon && _jsx(Icon, { "aria-hidden": true, className: "size-3 shrink-0" }), children, at && (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx(RelativeTime, { date: at })] }))] }));
12
+ function ThreadEvent({ icon: Icon = Info, tone = "quiet", at, timeFormat, align = "center", children, className, ...props }) {
13
+ return (_jsxs("div", { "data-slot": "thread-event", "data-tone": tone, "data-align": align, className: cn("flex items-center gap-1.5 py-1 text-xs", align === "start" ? "justify-start text-start" : "justify-center text-center", tone === "attention" ? "text-attention-text" : "text-faint-foreground", className), ...props, children: [Icon && _jsx(Icon, { "aria-hidden": true, className: "size-3 shrink-0" }), children, at &&
14
+ (align === "start" ? (_jsx(RelativeTime, { date: at, format: timeFormat, className: "ms-auto shrink-0 tabular-nums" })) : (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx(RelativeTime, { date: at, format: timeFormat })] })))] }));
14
15
  }
15
16
  export { ThreadEvent };
@@ -13,17 +13,33 @@ export declare const threadStrings: {
13
13
  * For long histories, `virtualize` renders only the messages in or near view
14
14
  * (each child is one item, keyed by its `key`). The column stays a `log`, the
15
15
  * message holding focus stays rendered, and Jump to latest works the same.
16
+ *
17
+ * It opens at the bottom, or at `anchor` (the `id` of something in it, such as
18
+ * a chapter divider or the card that needs the reader), and then waits for the
19
+ * reader instead of following.
16
20
  */
17
- declare function Thread({ children, empty, footer, className, contentClassName, virtualize, }: {
18
- children?: React.ReactNode;
21
+ declare function Thread({ children, empty, footer, className, contentClassName, virtualize, anchor, scrollerRef, onDrop, fade, }: {
22
+ children?: React.ReactNode | undefined;
19
23
  /** Shown before the first message. */
20
- empty?: React.ReactNode;
24
+ empty?: React.ReactNode | undefined;
21
25
  /** Below the messages, usually the composer. */
22
- footer?: React.ReactNode;
23
- className?: string;
26
+ footer?: React.ReactNode | undefined;
27
+ className?: string | undefined;
24
28
  /** Classes for the message column. */
25
- contentClassName?: string;
29
+ contentClassName?: string | undefined;
26
30
  /** Render only the messages in or near view, for histories of hundreds of messages. Give each child a `key`. Defaults to false. */
27
- virtualize?: boolean;
31
+ virtualize?: boolean | undefined;
32
+ /**
33
+ * The `id` of an element in the thread to open at, instead of the bottom: a chapter divider, or the card that needs
34
+ * the reader. Read once, when the messages first appear; if it isn't among them, the thread opens at the bottom.
35
+ * Ignored with `virtualize`, where it may not be rendered yet.
36
+ */
37
+ anchor?: string | undefined;
38
+ /** The scrolling element, for your own scroll handling. */
39
+ scrollerRef?: React.Ref<HTMLDivElement> | undefined;
40
+ /** Files or other data dropped on the thread. Dragging over it is allowed only when this is given. */
41
+ onDrop?: ((event: React.DragEvent<HTMLDivElement>) => void) | undefined;
42
+ /** The soft fade at the bottom of the column. Defaults to true. */
43
+ fade?: boolean | undefined;
28
44
  }): React.JSX.Element;
29
45
  export { Thread };