@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
@@ -19,18 +19,41 @@ export const threadStrings = {
19
19
  * For long histories, `virtualize` renders only the messages in or near view
20
20
  * (each child is one item, keyed by its `key`). The column stays a `log`, the
21
21
  * message holding focus stays rendered, and Jump to latest works the same.
22
+ *
23
+ * It opens at the bottom, or at `anchor` (the `id` of something in it, such as
24
+ * a chapter divider or the card that needs the reader), and then waits for the
25
+ * reader instead of following.
22
26
  */
23
- function Thread({ children, empty, footer, className, contentClassName, virtualize = false, }) {
27
+ function Thread({ children, empty, footer, className, contentClassName, virtualize = false, anchor, scrollerRef, onDrop, fade = true, }) {
24
28
  const t = useStrings("thread", threadStrings);
25
29
  const scroller = React.useRef(null);
26
30
  const content = React.useRef(null);
27
- const pinnedRef = React.useRef(true);
28
- const [pinned, setPinned] = React.useState(true);
31
+ // Opening at an anchor means not following the bottom until the reader gets there.
32
+ const anchoring = Boolean(anchor) && !virtualize;
33
+ const pinnedRef = React.useRef(!anchoring);
34
+ const [pinned, setPinned] = React.useState(!anchoring);
35
+ React.useImperativeHandle(scrollerRef, () => scroller.current, []);
29
36
  const toBottom = React.useCallback((behavior = "auto") => {
30
37
  const el = scroller.current;
31
38
  if (el)
32
39
  el.scrollTo({ top: el.scrollHeight, behavior });
33
40
  }, []);
41
+ // Open at the anchor once, when it first appears.
42
+ const anchored = React.useRef(false);
43
+ React.useLayoutEffect(() => {
44
+ if (!anchoring || anchored.current || !content.current?.childElementCount)
45
+ return;
46
+ anchored.current = true;
47
+ const target = content.current.querySelector(`#${CSS.escape(anchor)}`);
48
+ if (target) {
49
+ target.scrollIntoView?.({ block: "start" });
50
+ return;
51
+ }
52
+ // The anchor isn't in the thread: open at the bottom and follow, as without one.
53
+ pinnedRef.current = true;
54
+ setPinned(true);
55
+ toBottom();
56
+ });
34
57
  React.useEffect(() => {
35
58
  const el = content.current;
36
59
  if (!el)
@@ -75,14 +98,14 @@ function Thread({ children, empty, footer, className, contentClassName, virtuali
75
98
  pin: focused,
76
99
  gap: () => (content.current ? Number.parseFloat(getComputedStyle(content.current).rowGap) || 0 : 0),
77
100
  });
78
- return (_jsxs("div", { "data-slot": "thread", className: cn("flex min-h-0 flex-col", className), children: [_jsxs("div", { className: "relative min-h-0 flex-1", children: [_jsx("div", { ref: scroller, onScroll: onScroll, onFocus: virtualize
101
+ return (_jsxs("div", { "data-slot": "thread", className: cn("flex min-h-0 flex-col", className), children: [_jsxs("div", { className: "relative min-h-0 flex-1", children: [_jsx("div", { ref: scroller, onScroll: onScroll, onDragOver: onDrop ? (e) => e.preventDefault() : undefined, onDrop: onDrop, onFocus: virtualize
79
102
  ? (e) => {
80
103
  setFocused(e.target.closest("[data-virtual-key]")?.getAttribute("data-virtual-key") ?? null);
81
104
  }
82
- : undefined, onBlur: virtualize ? (e) => !e.currentTarget.contains(e.relatedTarget) && setFocused(null) : undefined, className: "h-full 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 && (_jsxs("button", { type: "button", onClick: () => {
105
+ : 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 && (_jsxs("button", { type: "button", onClick: () => {
83
106
  pinnedRef.current = true;
84
107
  setPinned(true);
85
108
  toBottom("smooth");
86
- }, className: "absolute bottom-3 left-1/2 z-10 inline-flex -translate-x-1/2 items-center gap-1.5 rounded-full border bg-popover px-3 py-1.5 text-xs font-medium shadow-float animate-in fade-in-0 slide-in-from-bottom-1", children: [_jsx(ArrowDown, { "aria-hidden": true, className: "size-3" }), " ", t("jumpToLatest")] })), _jsx("div", { "aria-hidden": true, 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 })] }));
109
+ }, className: "absolute bottom-3 left-1/2 z-10 inline-flex -translate-x-1/2 items-center gap-1.5 rounded-full border bg-popover px-3 py-1.5 text-xs font-medium shadow-float animate-in fade-in-0 slide-in-from-bottom-1", 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 })] }));
87
110
  }
88
111
  export { Thread };
@@ -11,6 +11,11 @@ export declare const toolCallStrings: {
11
11
  done: string;
12
12
  failed: string;
13
13
  working: string;
14
+ approvedAutomatically: string;
15
+ approvedByPerson: string;
16
+ editedBeforeRun: string;
17
+ editedNote: string;
18
+ proposed: string;
14
19
  };
15
20
  /**
16
21
  * One tool call as a compact row: what it did in plain words, its status, and
@@ -20,12 +25,12 @@ export declare const toolCallStrings: {
20
25
  declare function ToolCall({ call, className, }: {
21
26
  /** The call: tool name, plain-words title, status, input, output, duration or error. */
22
27
  call: ToolCallData;
23
- className?: string;
28
+ className?: string | undefined;
24
29
  }): React.JSX.Element;
25
30
  /** A run of tool calls, with a count when it's long enough to fold. */
26
31
  declare function ToolCallGroup({ calls, className, }: {
27
32
  /** Several calls, summarised as a group. */
28
33
  calls: ToolCallData[];
29
- className?: string;
34
+ className?: string | undefined;
30
35
  }): React.JSX.Element;
31
36
  export { ToolCall, ToolCallGroup };
@@ -17,6 +17,11 @@ export const toolCallStrings = {
17
17
  done: "{count} done",
18
18
  failed: "{count} failed",
19
19
  working: "working",
20
+ approvedAutomatically: "Approved automatically",
21
+ approvedByPerson: "Approved by a person",
22
+ editedBeforeRun: "Edited before it ran",
23
+ editedNote: "A person changed the input before allowing it. Input is what ran.",
24
+ proposed: "As the agent proposed it",
20
25
  };
21
26
  /**
22
27
  * One tool call as a compact row: what it did in plain words, its status, and
@@ -28,8 +33,8 @@ function ToolCall({ call, className, }) {
28
33
  const statusLabel = useStatusLabel();
29
34
  const failed = call.status === "failed";
30
35
  const [open, setOpen] = React.useState(failed);
31
- const hasDetail = Boolean(call.input || call.output || call.error);
32
- 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/40", 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)] })] }), _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, { className: "data-[state=open]:animate-in data-[state=open]:fade-in-0", 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.input && _jsx(Payload, { language: "json", title: t("input"), text: call.input }), call.output && _jsx(Payload, { language: call.outputLanguage ?? "text", title: t("output"), text: call.output })] }) })] }));
36
+ const hasDetail = Boolean(call.input || call.output || call.error || call.approval?.note || call.approval?.originalInput);
37
+ 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, { className: "data-[state=open]:animate-in data-[state=open]:fade-in-0", 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 })] }) })] }));
33
38
  }
34
39
  /** A run of tool calls, with a count when it's long enough to fold. */
35
40
  function ToolCallGroup({ calls, className, }) {
@@ -41,6 +46,6 @@ function ToolCallGroup({ calls, className, }) {
41
46
  if (calls.length <= 2) {
42
47
  return (_jsx("div", { "data-slot": "tool-call-group", className: cn("flex flex-col", className), children: calls.map((c) => (_jsx(ToolCall, { call: c }, c.id))) }));
43
48
  }
44
- 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/40 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))) }) })] }));
49
+ 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))) }) })] }));
45
50
  }
46
51
  export { ToolCall, ToolCallGroup };
package/dist/index.d.ts CHANGED
@@ -40,6 +40,8 @@ export * from "./ui/popover.js";
40
40
  export * from "./ui/skeleton.js";
41
41
  export * from "./lib/know.js";
42
42
  export * from "./lib/render-prop.js";
43
+ export * from "./chat/ai-disclosure.js";
44
+ export * from "./chat/permission-prompt.js";
43
45
  export * from "./lib/utils.js";
44
46
  export { ToolCall } from "./chat/tool-call.js";
45
47
  export type { ToolCall as ToolCallData } from "./lib/chat.js";
package/dist/index.js CHANGED
@@ -40,6 +40,8 @@ export * from "./ui/popover.js";
40
40
  export * from "./ui/skeleton.js";
41
41
  export * from "./lib/know.js";
42
42
  export * from "./lib/render-prop.js";
43
+ export * from "./chat/ai-disclosure.js";
44
+ export * from "./chat/permission-prompt.js";
43
45
  export * from "./lib/utils.js";
44
46
  export { ToolCall } from "./chat/tool-call.js";
45
47
  export { Message } from "./chat/message.js";
@@ -17,11 +17,11 @@ export type AgentIdentity = {
17
17
  /** The displayed name, e.g. "Architect agent". Never derived from model output. */
18
18
  name: string;
19
19
  role: AgentRole;
20
- specialty?: string;
20
+ specialty?: string | undefined;
21
21
  };
22
22
  export type MessageAuthor = {
23
23
  role: "user";
24
- name?: string;
24
+ name?: string | undefined;
25
25
  } | {
26
26
  role: "agent";
27
27
  agent: AgentIdentity;
@@ -30,7 +30,7 @@ export type MessageAuthor = {
30
30
  | {
31
31
  role: "person";
32
32
  name: string;
33
- team?: string;
33
+ team?: string | undefined;
34
34
  }
35
35
  /** System lines are the app's own strings, never model text. */
36
36
  | {
@@ -47,9 +47,9 @@ export type EntityRef = {
47
47
  /** The identifier, e.g. "orders.status" or "billing.invoice.due_date". */
48
48
  name: string;
49
49
  /** A friendlier label when there is one, e.g. "Region". */
50
- label?: string;
50
+ label?: string | undefined;
51
51
  /** Where it lives, when more than one place is in scope, e.g. "Shop staging". */
52
- location?: string;
52
+ location?: string | undefined;
53
53
  };
54
54
  export type InlineNode = {
55
55
  kind: "text";
@@ -68,19 +68,19 @@ export type InlineNode = {
68
68
  | {
69
69
  kind: "measured";
70
70
  text: string;
71
- source?: string;
71
+ source?: string | undefined;
72
72
  }
73
73
  /** @deprecated Renamed `measured`; still rendered the same way until 2.0. */
74
74
  | {
75
75
  kind: "orgData";
76
76
  text: string;
77
- source?: string;
77
+ source?: string | undefined;
78
78
  }
79
79
  /** The agent asserts this but has not verified it. */
80
80
  | {
81
81
  kind: "provisional";
82
82
  nodes: InlineNode[];
83
- reason?: string;
83
+ reason?: string | undefined;
84
84
  } | {
85
85
  kind: "citation";
86
86
  sourceId: string;
@@ -103,16 +103,21 @@ export type MessageSource = {
103
103
  kind: SourceKind;
104
104
  label: string;
105
105
  /** An inert excerpt shown in the popover. Text only. */
106
- snippet?: string;
106
+ snippet?: string | undefined;
107
107
  /** Where it came from in a human sense: "Docs · Sales process v3", "Query on orders". */
108
- origin?: string;
109
- provisional?: boolean;
108
+ origin?: string | undefined;
109
+ provisional?: boolean | undefined;
110
+ };
111
+ /** A source a tool's result supports, without the id and [n] an adapter assigns. */
112
+ export type ToolSource = Omit<MessageSource, "id" | "marker"> & {
113
+ /** Makes two results naming the same thing one source; defaults to `kind` and `label`. */
114
+ key?: string | undefined;
110
115
  };
111
116
  type ReasoningBase = {
112
117
  id: string;
113
118
  title: string;
114
- body?: string;
115
- streaming?: boolean;
119
+ body?: string | undefined;
120
+ streaming?: boolean | undefined;
116
121
  };
117
122
  export type ReasoningEntry = (ReasoningBase & {
118
123
  basis: "verified";
@@ -129,13 +134,23 @@ export type ToolCall = {
129
134
  title: string;
130
135
  status: RunStatus;
131
136
  /** Shown when expanded. Text only; long values scroll. */
132
- input?: string;
133
- output?: string;
134
- outputLanguage?: PayloadLanguage;
137
+ input?: string | undefined;
138
+ output?: string | undefined;
139
+ outputLanguage?: PayloadLanguage | undefined;
135
140
  /** e.g. "1.2s". Absent while running; never estimated. */
136
- duration?: string;
141
+ duration?: string | undefined;
137
142
  /** Required when status is "failed": what went wrong, in a sentence. */
138
- error?: string;
143
+ error?: string | undefined;
144
+ /**
145
+ * Who let it run, when it needed a go-ahead: a person, or something automatic (a policy, a classifier, a rule).
146
+ * `note` is why, in the source's words. Left out when it needed none or the source doesn't say.
147
+ */
148
+ approval?: {
149
+ by: "person" | "automatic";
150
+ note?: string | undefined;
151
+ /** The input as the agent proposed it, when a person edited it before it ran; `input` is then what ran. */
152
+ originalInput?: string | undefined;
153
+ } | undefined;
139
154
  };
140
155
  /** The block's language, e.g. "json", "sql", "python", "text". Shown as a label; never executed or highlighted as HTML. */
141
156
  export type PayloadLanguage = string;
@@ -158,13 +173,13 @@ export type MessageBlock = {
158
173
  nodes: InlineNode[];
159
174
  } | {
160
175
  kind: "list";
161
- ordered?: boolean;
176
+ ordered?: boolean | undefined;
162
177
  items: InlineNode[][];
163
178
  } | {
164
179
  kind: "payload";
165
180
  language: PayloadLanguage;
166
181
  text: string;
167
- title?: string;
182
+ title?: string | undefined;
168
183
  } | {
169
184
  kind: "tools";
170
185
  calls: ToolCall[];
@@ -173,7 +188,7 @@ export type MessageBlock = {
173
188
  | {
174
189
  kind: "degraded";
175
190
  reason: string;
176
- missing?: string;
191
+ missing?: string | undefined;
177
192
  }
178
193
  /**
179
194
  * A typed card (a result table, a plan, an approval) rendered by the app's
@@ -196,53 +211,53 @@ export type Message = {
196
211
  id: string;
197
212
  author: MessageAuthor;
198
213
  /** When it was sent. Absent when the source doesn't say; then no time is shown. */
199
- createdAt?: string;
214
+ createdAt?: string | undefined;
200
215
  status: MessageStatus;
201
216
  blocks: MessageBlock[];
202
- reasoning?: ReasoningEntry[];
217
+ reasoning?: ReasoningEntry[] | undefined;
203
218
  /** How long the agent spent before answering, e.g. "6s". Only once known. */
204
- thoughtFor?: string;
205
- sources?: MessageSource[];
219
+ thoughtFor?: string | undefined;
220
+ sources?: MessageSource[] | undefined;
206
221
  /** Shown under an agent's reply: model, cost. Only values the service measured. */
207
222
  attribution?: {
208
- model?: string;
209
- cost?: string;
210
- };
223
+ model?: string | undefined;
224
+ cost?: string | undefined;
225
+ } | undefined;
211
226
  };
212
227
  /** Where the next message's work lands. */
213
228
  export type ComposerScope = {
214
- account?: string;
229
+ account?: string | undefined;
215
230
  environment: string;
216
- detail?: string;
231
+ detail?: string | undefined;
217
232
  production: boolean;
218
233
  };
219
234
  export type MentionOption = {
220
235
  id: string;
221
236
  /** The heading it sits under in the menu, e.g. "Files", "People". Groups appear in the order first used. */
222
237
  group: string;
223
- entity?: EntityRef;
238
+ entity?: EntityRef | undefined;
224
239
  label: string;
225
- description?: string;
240
+ description?: string | undefined;
226
241
  };
227
242
  export type SlashCommand = {
228
243
  id: string;
229
244
  name: string;
230
245
  description: string;
231
246
  /** e.g. "<object>" — shown after the name. */
232
- args?: string;
247
+ args?: string | undefined;
233
248
  };
234
249
  export type Attachment = {
235
250
  id: string;
236
251
  name: string;
237
252
  /** In bytes. */
238
- size?: number;
253
+ size?: number | undefined;
239
254
  /** e.g. "image/png", "application/pdf": picks the icon, and allows an image preview. */
240
- mediaType?: string;
255
+ mediaType?: string | undefined;
241
256
  /** A thumbnail your app serves (or a blob: URL), shown for images. Never a URL from model output. */
242
- previewUrl?: string;
257
+ previewUrl?: string | undefined;
243
258
  /** 0–1 while uploading; absent when done. */
244
- progress?: number;
245
- error?: string;
259
+ progress?: number | undefined;
260
+ error?: string | undefined;
246
261
  };
247
262
  export type PromptSuggestion = {
248
263
  id: string;
@@ -14,17 +14,17 @@ export type KnowSource = {
14
14
  door: KnowDoor;
15
15
  label: string;
16
16
  /** Who added it, for files, links and answers. */
17
- addedBy?: string;
18
- addedAt?: string;
17
+ addedBy?: string | undefined;
18
+ addedAt?: string | undefined;
19
19
  /** When it was last read; null if never. */
20
20
  readAt: string | null;
21
21
  /** How often it should be re-read, in hours, when it has a schedule. */
22
- everyHours?: number;
22
+ everyHours?: number | undefined;
23
23
  status: RunStatus;
24
24
  /** "214 objects", "38 pages" — only when counted. */
25
- extent?: string;
25
+ extent?: string | undefined;
26
26
  /** Required when status is "failed". */
27
- error?: string;
27
+ error?: string | undefined;
28
28
  };
29
29
  export type Confidence = "high" | "medium" | "low" | "unknown";
30
30
  export type RaciRole = "R" | "A" | "C" | "I";
@@ -42,7 +42,7 @@ export type RetrievedPassage = {
42
42
  /** Whether the answer actually cites it. */
43
43
  cited: boolean;
44
44
  /** Why it ranked where it did, when the retriever says. */
45
- why?: string;
45
+ why?: string | undefined;
46
46
  readAt: string | null;
47
47
  };
48
48
  /** A sentence in an answer and the passages it rests on. */
@@ -57,7 +57,7 @@ export type ModelOption = {
57
57
  vendor: ModelVendor;
58
58
  name: string;
59
59
  /** One line on what it's good for. */
60
- note?: string;
60
+ note?: string | undefined;
61
61
  /** Relative cost, 1 (cheapest) to 4. */
62
62
  cost: 1 | 2 | 3 | 4;
63
63
  /** The vendor's zero-retention contract has been attested by an operator. */
@@ -70,7 +70,7 @@ export type ModelSettingsValue = {
70
70
  /** Most tool-calling steps per run before it stops and asks. */
71
71
  stepLimit: number;
72
72
  /** Used when the primary is unavailable; must also be attested. */
73
- fallback?: string;
73
+ fallback?: string | undefined;
74
74
  };
75
75
  export type ToolPermission = "always" | "ask" | "never";
76
76
  export type ToolSetting = {
@@ -81,7 +81,7 @@ export type ToolSetting = {
81
81
  effect: "read" | "write" | "deploy" | "outside";
82
82
  permission: ToolPermission;
83
83
  /** Permissions this tool can't take, with why. */
84
- locked?: Partial<Record<ToolPermission, string>>;
84
+ locked?: Partial<Record<ToolPermission, string | undefined>> | undefined;
85
85
  };
86
86
  export type AgentConfig = {
87
87
  agent: AgentIdentity;
@@ -93,7 +93,7 @@ export type AgentConfig = {
93
93
  ok: number;
94
94
  failed: number;
95
95
  }[];
96
- lastFailure?: string;
96
+ lastFailure?: string | undefined;
97
97
  };
98
98
  export type Memory = {
99
99
  id: string;
@@ -106,15 +106,17 @@ export type Memory = {
106
106
  };
107
107
  createdAt: string;
108
108
  scope: "account" | "environment";
109
- environmentId?: string;
109
+ environmentId?: string | undefined;
110
110
  };
111
+ /** Whether agents use memory: on, paused (kept and not used for now), or off. */
112
+ export type MemoryModeValue = "on" | "paused" | "off";
111
113
  export type Version = {
112
114
  id: string;
113
115
  label: string;
114
116
  state: "live" | "draft" | "past";
115
117
  at: string;
116
118
  by: string;
117
- note?: string;
119
+ note?: string | undefined;
118
120
  };
119
121
  export type ContextSegment = {
120
122
  label: string;
@@ -6,11 +6,11 @@ import * as React from "react";
6
6
  */
7
7
  export type RenderProp = {
8
8
  /** Base UI's way to render as another element; the same as `asChild` with that element as the child. */
9
- render?: React.ReactElement;
9
+ render?: React.ReactElement | undefined;
10
10
  /** Base UI's flag for a button rendered as another element; shadcn adds it with `render`. Radix doesn't need it. */
11
- nativeButton?: boolean;
11
+ nativeButton?: boolean | undefined;
12
12
  };
13
13
  export declare function withRender<P extends {
14
- asChild?: boolean;
15
- children?: React.ReactNode;
14
+ asChild?: boolean | undefined;
15
+ children?: React.ReactNode | undefined;
16
16
  }>({ render, nativeButton: _nativeButton, ...props }: P & RenderProp): P;
@@ -4,7 +4,7 @@ import type { InlineNode, MessageBlock } from "./chat.js";
4
4
  * unless `headings` is set, when `#`, `##` and `###` (and deeper, as level 3) become heading blocks.
5
5
  */
6
6
  export declare function markdownBlocks(text: string, { headings }?: {
7
- headings?: boolean;
7
+ headings?: boolean | undefined;
8
8
  }): MessageBlock[];
9
9
  /** `code`, **strong**, *em* and links (as text). Everything else stays literal. */
10
10
  export declare function inline(text: string): InlineNode[];
@@ -34,3 +34,24 @@ export declare const statusTone: {
34
34
  readonly skipped: "outline";
35
35
  };
36
36
  export declare function isTerminal(status: RunStatus): status is "succeeded" | "failed" | "cancelled" | "skipped";
37
+ /** The machine or worker a run is on, when the app can't reach it. */
38
+ export type HostOffline = {
39
+ /** The host's name as the app knows it: "Build runner 3", "Priya’s laptop". Left out, it's "The host". */
40
+ host?: string | undefined;
41
+ /** When the app lost contact. */
42
+ since?: string | undefined;
43
+ };
44
+ /**
45
+ * Whether a run can be called alive. "live": heard from recently, or the app doesn't track it. "stale": running, but
46
+ * nothing heard for longer than `staleAfter`, so it may have stopped. "offline": its host can't be reached.
47
+ * "unchecked": there's no clock yet (rendering on the server), so freshness isn't claimed either way. "settled": it
48
+ * has finished, so silence is expected. Only a running run goes stale: a queued or waiting one is quiet by design.
49
+ */
50
+ export type RunLiveness = "live" | "stale" | "offline" | "unchecked" | "settled";
51
+ /** Two minutes: the default silence after which a running run is no longer called running. */
52
+ export declare const STALE_AFTER = 120000;
53
+ export declare function runLiveness(run: {
54
+ status: RunStatus;
55
+ lastHeardAt?: Date | string | undefined;
56
+ hostOffline?: HostOffline | undefined;
57
+ }, now: number | null, staleAfter?: number): RunLiveness;
@@ -33,3 +33,20 @@ export const statusTone = {
33
33
  export function isTerminal(status) {
34
34
  return status === "succeeded" || status === "failed" || status === "cancelled" || status === "skipped";
35
35
  }
36
+ /** Two minutes: the default silence after which a running run is no longer called running. */
37
+ export const STALE_AFTER = 120_000;
38
+ export function runLiveness(run, now, staleAfter = STALE_AFTER) {
39
+ if (isTerminal(run.status))
40
+ return "settled";
41
+ if (run.hostOffline)
42
+ return "offline";
43
+ if (run.status !== "running" || run.lastHeardAt === undefined)
44
+ return "live";
45
+ const heard = new Date(run.lastHeardAt).getTime();
46
+ // An unreadable time ("", "10:42") is no evidence the run is alive.
47
+ if (Number.isNaN(heard))
48
+ return "stale";
49
+ if (now === null)
50
+ return "unchecked";
51
+ return now - heard > staleAfter ? "stale" : "live";
52
+ }
@@ -11,5 +11,5 @@ export declare function comboKeys(combo: string): string[];
11
11
  * the person is typing in a field, so "?" still types a question mark.
12
12
  */
13
13
  export declare function useShortcut(combo: string, handler: (e: KeyboardEvent) => void, { enabled }?: {
14
- enabled?: boolean;
14
+ enabled?: boolean | undefined;
15
15
  }): void;
@@ -27,13 +27,13 @@ export declare function useVirtualList({ enabled, count, keyOf, scroller, estima
27
27
  count: number;
28
28
  keyOf: (index: number) => React.Key;
29
29
  scroller: React.RefObject<HTMLElement | null>;
30
- estimate?: number;
31
- overscan?: number;
32
- initial?: number;
30
+ estimate?: number | undefined;
31
+ overscan?: number | undefined;
32
+ initial?: number | undefined;
33
33
  /** Which end to show before the scroller is measured: "end" for a thread (newest last), "start" for a list. Defaults to "end". */
34
- anchor?: "start" | "end";
34
+ anchor?: "start" | "end" | undefined;
35
35
  /** The gap between items, in pixels, read once the scroller is measured (e.g. from the list's computed `row-gap`). */
36
- gap?: () => number;
36
+ gap?: (() => number) | undefined;
37
37
  /** The key of an item to keep rendered, such as the one holding focus. */
38
- pin?: string | null;
38
+ pin?: string | null | undefined;
39
39
  }): VirtualList;
package/dist/styles.css CHANGED
@@ -238,6 +238,9 @@
238
238
  * (--primary, --background…), everything else by its --cd-* name. A brand theme
239
239
  * is just such a file.
240
240
  *
241
+ * Font: Inter when the font-inter item is installed (styles/candor-font-inter.css),
242
+ * else the system's sans-serif. Point --cd-font at your own font to use it instead.
243
+ *
241
244
  * Density: data-density="compact" on <html> (or any container) tightens type for
242
245
  * dense apps: 14px conversation text and 13px body and label text, with a narrower
243
246
  * thread and sidebar.
@@ -476,7 +479,7 @@
476
479
 
477
480
  @layer base {
478
481
  * {
479
- @apply border-border outline-ring/50;
482
+ @apply border-border outline-ring/80;
480
483
  }
481
484
  body {
482
485
  @apply bg-background text-foreground font-sans text-sm;
@@ -7,6 +7,6 @@ declare const buttonVariants: (props?: ({
7
7
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
8
8
  declare function Button(allProps: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
9
9
  /** Render the child element (a link, say) with the button's styles instead of a <button>. */
10
- asChild?: boolean;
10
+ asChild?: boolean | undefined;
11
11
  } & RenderProp): React.JSX.Element;
12
12
  export { Button, buttonVariants };
package/dist/ui/button.js CHANGED
@@ -7,7 +7,7 @@ import { withRender } from "../lib/render-prop.js";
7
7
  import { cn } from "../lib/utils.js";
8
8
  // The accent fills the one primary act, the radius is the theme's
9
9
  // --cd-radius-button, and buttons press down slightly on :active.
10
- const buttonVariants = cva("inline-flex shrink-0 items-center justify-center gap-2 rounded-button text-sm font-medium whitespace-nowrap outline-none select-none transition-[background-color,color,border-color,box-shadow,transform,opacity] duration-150 ease-standard active:scale-[0.98] focus-visible:ring-[3px] focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-45 data-[locked]:cursor-not-allowed data-[locked]:opacity-45 data-[locked]:active:scale-100 aria-invalid:ring-destructive/25 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", {
10
+ const buttonVariants = cva("inline-flex shrink-0 items-center justify-center gap-2 rounded-button text-sm font-medium whitespace-nowrap outline-none select-none transition-[background-color,color,border-color,box-shadow,transform,opacity] duration-150 ease-standard active:scale-[0.98] focus-visible:ring-[3px] focus-visible:ring-ring/80 disabled:pointer-events-none disabled:opacity-45 data-[locked]:cursor-not-allowed data-[locked]:opacity-45 data-[locked]:active:scale-100 aria-invalid:ring-destructive/25 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", {
11
11
  variants: {
12
12
  variant: {
13
13
  default: "bg-primary text-primary-foreground hover:bg-primary-hover",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@candor-ui/react",
3
- "version": "1.3.0",
3
+ "version": "1.7.0",
4
4
  "description": "Candor UI's free tier as an npm package: React components for agents that do real work, on shadcn/ui and Tailwind 4.",
5
5
  "license": "MIT",
6
6
  "author": "Methodic LLC",