@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
@@ -15,8 +15,8 @@ import { fromAiSdkMessages } from "./ai-sdk.js";
15
15
  export function useCandorChat(chat, options) {
16
16
  const [value, setValue] = React.useState("");
17
17
  const [references, setReferences] = React.useState([]);
18
- const { agent, createdAt, toolTitle, summarize } = options;
19
- const messages = React.useMemo(() => fromAiSdkMessages(chat.messages, { agent, createdAt, toolTitle, summarize, chatStatus: chat.status }), [chat.messages, chat.status, agent, createdAt, toolTitle, summarize]);
18
+ const { agent, createdAt, toolTitle, summarize, sourcesFrom } = options;
19
+ const messages = React.useMemo(() => fromAiSdkMessages(chat.messages, { agent, createdAt, toolTitle, summarize, sourcesFrom, chatStatus: chat.status }), [chat.messages, chat.status, agent, createdAt, toolTitle, summarize, sourcesFrom]);
20
20
  const state = chat.status === "submitted" ? "sending" : chat.status === "streaming" ? "streaming" : "ready";
21
21
  const approvals = React.useMemo(() => {
22
22
  const last = chat.messages.findLast((m) => m.role === "assistant");
@@ -30,7 +30,7 @@ export function useCandorChat(chat, options) {
30
30
  });
31
31
  }, [chat.messages]);
32
32
  const { sendMessage, stop, addToolApprovalResponse } = chat;
33
- const respond = React.useCallback((approvalId, approved, reason) => addToolApprovalResponse({ id: approvalId, approved, reason }), [addToolApprovalResponse]);
33
+ const respond = React.useCallback((approvalId, approved, reason) => addToolApprovalResponse({ id: approvalId, approved, ...(reason !== undefined && { reason }) }), [addToolApprovalResponse]);
34
34
  return {
35
35
  messages,
36
36
  approvals,
@@ -1,16 +1,37 @@
1
1
  import * as React from "react";
2
2
  import type { EntityRef } from "../lib/chat.js";
3
- import { type FromClaudeAgentOptions } from "./claude-agent-sdk.js";
3
+ import { type AgentEvent, type ClaudeApprovalAnswer, type ClaudePermissionMode, type FromClaudeAgentOptions } from "./claude-agent-sdk.js";
4
4
  export type UseClaudeAgentOptions = Omit<FromClaudeAgentOptions, "running"> & {
5
- /** Your route that runs the agent with streamClaudeAgent. It receives `{ prompt, references, sessionId, ...body }`. */
5
+ /** Your route that runs the agent with streamClaudeAgent. It receives `{ prompt, promptId, references, sessionId, ...body }`. */
6
6
  api: string;
7
- /** Your route that calls answerClaudeAgentApproval. It receives `{ id, approved, reason }`. */
8
- approveApi: string;
7
+ /**
8
+ * Your route that records an answer (answerClaudeApproval). It receives `{ id, approved, reason, updatedInput,
9
+ * answers, alwaysAllow }` and returns `{ ok }`. Not needed with `onRespond`.
10
+ */
11
+ approveApi?: string | undefined;
12
+ /**
13
+ * Answer through your own route instead, e.g. one that signals the workflow running the agent. Resolve true once
14
+ * the answer is recorded, false if it wasn't (the request had ended).
15
+ */
16
+ onRespond?: ((approvalId: string, answer: ClaudeApprovalAnswer) => Promise<boolean>) | undefined;
17
+ /** Events you kept from earlier (onEvent, a run store): the conversation is folded from them, as it was. */
18
+ initialEvents?: AgentEvent[] | undefined;
19
+ /**
20
+ * Where to re-attach to a run after a reload: `(runId, from) => \`/api/agent/${runId}?from=${from}\``, a route that
21
+ * returns attachClaudeAgentRun. With `runId`, the hook re-attaches when it mounts.
22
+ */
23
+ attachUrl?: ((runId: string, from: number) => string) | undefined;
24
+ /** A run to re-attach to when the hook mounts, e.g. the one you saved from `runId` before the reload. */
25
+ runId?: string | undefined;
26
+ /** Your route that calls stopClaudeAgentRun with `{ runId }`. Needed to stop a run that keeps going without the browser (`detach`). */
27
+ stopApi?: string | undefined;
28
+ /** Your route that calls setClaudeAgentPermissionMode with `{ runId, mode }`. Enables `setPermissionMode`. */
29
+ modeApi?: string | undefined;
9
30
  /** Extra fields for every request, e.g. which project the agent works on. */
10
- body?: Record<string, unknown>;
11
- headers?: Record<string, string>;
31
+ body?: Record<string, unknown> | undefined;
32
+ headers?: Record<string, string> | undefined;
12
33
  /** Replaces the global fetch, e.g. to add auth or to play a script in tests and demos. */
13
- fetch?: typeof fetch;
34
+ fetch?: typeof fetch | undefined;
14
35
  };
15
36
  /**
16
37
  * Wires a Claude Agent SDK server route to Candor: messages for <Thread>/<Message>,
@@ -26,7 +47,7 @@ export type UseClaudeAgentOptions = Omit<FromClaudeAgentOptions, "running"> & {
26
47
  export declare function useClaudeAgent(options: UseClaudeAgentOptions): {
27
48
  messages: import("../index.js").MessageData[];
28
49
  approvals: import("./claude-agent-sdk.js").ClaudeApproval[];
29
- respond: (approvalId: string, approved: boolean, reason?: string) => Promise<void>;
50
+ respond: (approvalId: string, approved: boolean, reason?: string, more?: Omit<ClaudeApprovalAnswer, "approved" | "reason">) => Promise<boolean>;
30
51
  /** What the SDK measured for the session: cost (its estimate), tokens and turns. */
31
52
  usage: import("./claude-agent-sdk.js").ClaudeAgentUsage | undefined;
32
53
  /** A passing problem the agent is handling itself, such as a retried API call. */
@@ -34,6 +55,14 @@ export declare function useClaudeAgent(options: UseClaudeAgentOptions): {
34
55
  /** The request failed; say so, and let the person send again. */
35
56
  error: string | undefined;
36
57
  sessionId: string | undefined;
58
+ /** Every event so far, to keep for replay (`initialEvents`). */
59
+ events: AgentEvent[];
60
+ /** The current or last run's id (from `x-candor-run-id`): save it to re-attach after a reload. */
61
+ runId: string | undefined;
62
+ attach: (id: string, from?: number) => Promise<void>;
63
+ /** The permission mode last reported by the SDK or set through `setPermissionMode`. */
64
+ permissionMode: ClaudePermissionMode | undefined;
65
+ setPermissionMode: (mode: ClaudePermissionMode) => Promise<boolean>;
37
66
  /** Forget the conversation and start a new session. */
38
67
  reset: () => void;
39
68
  composer: {
@@ -42,7 +71,7 @@ export declare function useClaudeAgent(options: UseClaudeAgentOptions): {
42
71
  state: "streaming" | "ready" | "sending";
43
72
  references: EntityRef[];
44
73
  onReferencesChange: React.Dispatch<React.SetStateAction<EntityRef[]>>;
45
- onStop: () => void | undefined;
74
+ onStop: () => void;
46
75
  onSubmit: (text: string, refs: EntityRef[]) => void;
47
76
  };
48
77
  };
@@ -12,45 +12,54 @@ import { fromClaudeAgentEvents } from "./claude-agent-sdk.js";
12
12
  * sent as structured `references`, not pasted into the text.
13
13
  */
14
14
  export function useClaudeAgent(options) {
15
- const { api, approveApi, body, headers, agent, toolTitle, fetch: fetcher } = options;
16
- const [events, setEvents] = React.useState([]);
15
+ const { api, approveApi, onRespond, body, headers, agent, toolTitle, sourcesFrom, fetch: fetcher, attachUrl, stopApi, modeApi } = options;
16
+ const [events, setEvents] = React.useState(() => options.initialEvents ?? []);
17
+ const [runId, setRunId] = React.useState(options.runId);
17
18
  const [running, setRunning] = React.useState(false);
18
19
  const [error, setError] = React.useState();
19
20
  const [value, setValue] = React.useState("");
20
21
  const [references, setReferences] = React.useState([]);
21
22
  const run = React.useRef(null);
22
- const state = React.useMemo(() => fromClaudeAgentEvents(events, { agent, toolTitle, running }), [events, agent, toolTitle, running]);
23
+ const state = React.useMemo(() => fromClaudeAgentEvents(events, { agent, toolTitle, sourcesFrom, running }), [events, agent, toolTitle, sourcesFrom, running]);
23
24
  const session = React.useRef(undefined);
24
25
  React.useEffect(() => {
25
26
  session.current = state.sessionId;
26
27
  }, [state.sessionId]);
27
28
  // Stop the agent if the component goes away mid-run.
28
29
  React.useEffect(() => () => run.current?.abort(), []);
29
- const send = React.useCallback(async (text, refs) => {
30
+ /** Reads one response's events into state, a frame at a time. */
31
+ const stream = React.useCallback(async (open, before, replace = false) => {
30
32
  if (run.current)
31
33
  return;
32
34
  const controller = new AbortController();
33
35
  run.current = controller;
34
36
  setError(undefined);
35
37
  setRunning(true);
36
- setEvents((e) => [...e, { type: "candor_user", id: crypto.randomUUID(), text, references: refs.length ? refs : undefined }]);
38
+ if (before)
39
+ setEvents((e) => [...e, before]);
37
40
  // Streamed text arrives a few characters at a time; apply it once per frame.
38
- const pending = { events: [], frame: 0 };
41
+ const pending = { events: [], frame: 0, first: replace };
39
42
  const flush = () => {
40
43
  pending.frame = 0;
41
44
  const batch = pending.events.splice(0);
42
- if (batch.length)
43
- setEvents((e) => [...e, ...batch]);
45
+ if (!batch.length)
46
+ return;
47
+ const first = pending.first ? batch[0] : undefined;
48
+ pending.first = false;
49
+ setEvents((e) => {
50
+ // Re-attaching to a run whose events are already here (from initialEvents): the stored run replaces them,
51
+ // found by the person's message that starts it, rather than being added a second time.
52
+ const at = first?.type === "candor_user" ? e.findIndex((x) => x.type === "candor_user" && x.id === first.id) : -1;
53
+ return [...(at >= 0 ? e.slice(0, at) : e), ...batch];
54
+ });
44
55
  };
45
56
  try {
46
- const res = await (fetcher ?? fetch)(api, {
47
- method: "POST",
48
- headers: { "content-type": "application/json", ...headers },
49
- body: JSON.stringify({ ...body, prompt: text, references: refs, sessionId: session.current }),
50
- signal: controller.signal,
51
- });
57
+ const res = await open(controller.signal);
52
58
  if (!res.ok || !res.body)
53
59
  throw new Error(`The agent request failed (${res.status}).`);
60
+ const id = res.headers.get("x-candor-run-id");
61
+ if (id)
62
+ setRunId(id);
54
63
  for await (const event of readEvents(res.body)) {
55
64
  pending.events.push(event);
56
65
  pending.frame ||= requestAnimationFrame(flush);
@@ -69,19 +78,77 @@ export function useClaudeAgent(options) {
69
78
  run.current = null;
70
79
  setRunning(false);
71
80
  }
72
- }, [api, body, headers, fetcher]);
73
- const respond = React.useCallback(async (approvalId, approved, reason) => {
81
+ }, []);
82
+ const send = React.useCallback((text, refs) => {
83
+ const promptId = crypto.randomUUID();
84
+ return stream((signal) => (fetcher ?? fetch)(api, {
85
+ method: "POST",
86
+ headers: { "content-type": "application/json", ...headers },
87
+ body: JSON.stringify({ ...body, prompt: text, promptId, references: refs, sessionId: session.current }),
88
+ signal,
89
+ }), { type: "candor_user", id: promptId, text, references: refs.length ? refs : undefined });
90
+ }, [api, body, headers, fetcher, stream]);
91
+ /**
92
+ * Re-attach to a run after a reload: its events from `from` on (default 0, the whole run), then live until it
93
+ * ends. Needs `attachUrl`. From 0, when the run store recorded the person's message (the server's `prompt`, with
94
+ * this hook's `promptId`), the run's events replace any copy of them already in `initialEvents`.
95
+ */
96
+ const attach = React.useCallback((id, from = 0) => {
97
+ if (!attachUrl)
98
+ return Promise.resolve();
99
+ setRunId(id);
100
+ return stream((signal) => (fetcher ?? fetch)(attachUrl(id, from), { ...(headers && { headers }), signal }), undefined, from === 0);
101
+ }, [attachUrl, fetcher, headers, stream]);
102
+ // Re-attach to the run named in options once, when the hook mounts.
103
+ const attachOnMount = React.useRef(options.runId);
104
+ React.useEffect(() => {
105
+ const id = attachOnMount.current;
106
+ attachOnMount.current = undefined;
107
+ if (id)
108
+ void attach(id);
109
+ }, [attach]);
110
+ const post = React.useCallback(async (url, payload) => {
111
+ const res = await (fetcher ?? fetch)(url, { method: "POST", headers: { "content-type": "application/json", ...headers }, body: JSON.stringify(payload) });
112
+ return res.ok && !!(await res.json()).ok;
113
+ }, [fetcher, headers]);
114
+ /**
115
+ * Answer a waiting request. `more` carries an edited input (`updatedInput`), answers to the agent's questions
116
+ * (`answers`) and the "always allow" rules the person picked (`alwaysAllow`, indexes into its `suggestions`).
117
+ */
118
+ const respond = React.useCallback(async (approvalId, approved, reason, more) => {
119
+ const answer = { approved, ...(reason !== undefined && { reason }), ...more };
74
120
  try {
75
- const res = await (fetcher ?? fetch)(approveApi, { method: "POST", headers: { "content-type": "application/json", ...headers }, body: JSON.stringify({ id: approvalId, approved, reason }) });
76
- const ok = res.ok && (await res.json()).ok;
121
+ const ok = onRespond ? await onRespond(approvalId, answer) : approveApi ? await post(approveApi, { id: approvalId, ...answer }) : false;
77
122
  if (!ok)
78
123
  setError("Your answer didn’t reach the agent. The request may have ended; try sending the message again.");
124
+ return ok;
79
125
  }
80
126
  catch {
81
127
  setError("Your answer didn’t reach the agent. Check your connection and try again.");
128
+ return false;
129
+ }
130
+ }, [approveApi, onRespond, post]);
131
+ const stop = React.useCallback(() => {
132
+ // A detached run carries on without this browser, so it's stopped on the server too.
133
+ if (stopApi && runId)
134
+ void post(stopApi, { runId }).catch(() => { });
135
+ run.current?.abort();
136
+ }, [stopApi, runId, post]);
137
+ /** Change the run's permission mode. Resolves false when there's no run, no `modeApi`, or the server refused. */
138
+ const setPermissionMode = React.useCallback(async (mode) => {
139
+ if (!modeApi || !runId || !running)
140
+ return false;
141
+ try {
142
+ const ok = await post(modeApi, { runId, mode });
143
+ if (!ok)
144
+ setError("The permission mode didn’t change.");
145
+ return ok;
146
+ }
147
+ catch {
148
+ setError("The permission mode didn’t change. Check your connection and try again.");
149
+ return false;
82
150
  }
83
- }, [approveApi, headers, fetcher]);
84
- const stop = React.useCallback(() => run.current?.abort(), []);
151
+ }, [modeApi, runId, running, post]);
85
152
  const composerState = running ? (state.messages.at(-1)?.author.role === "agent" ? "streaming" : "sending") : "ready";
86
153
  return {
87
154
  messages: state.messages,
@@ -94,10 +161,19 @@ export function useClaudeAgent(options) {
94
161
  /** The request failed; say so, and let the person send again. */
95
162
  error,
96
163
  sessionId: state.sessionId,
164
+ /** Every event so far, to keep for replay (`initialEvents`). */
165
+ events,
166
+ /** The current or last run's id (from `x-candor-run-id`): save it to re-attach after a reload. */
167
+ runId,
168
+ attach,
169
+ /** The permission mode last reported by the SDK or set through `setPermissionMode`. */
170
+ permissionMode: state.permissionMode,
171
+ setPermissionMode,
97
172
  /** Forget the conversation and start a new session. */
98
173
  reset: () => {
99
174
  run.current?.abort();
100
175
  setEvents([]);
176
+ setRunId(undefined);
101
177
  setError(undefined);
102
178
  },
103
179
  composer: {
@@ -14,6 +14,6 @@ declare function CopyField({ label, value, className, }: {
14
14
  label: string;
15
15
  /** The text that is shown and copied. */
16
16
  value: string;
17
- className?: string;
17
+ className?: string | undefined;
18
18
  }): import("react").JSX.Element;
19
19
  export { CopyField };
@@ -18,6 +18,6 @@ export const copyFieldStrings = {
18
18
  function CopyField({ label, value, className, }) {
19
19
  const { state, copy } = useCopy();
20
20
  const t = useStrings("copyField", copyFieldStrings);
21
- return (_jsxs("div", { "data-slot": "copy-field", className: cn("flex flex-col gap-1", className), children: [_jsx("span", { className: "text-xs font-medium text-muted-foreground", children: label }), _jsxs("button", { type: "button", onClick: () => copy(value), className: "group/copy flex w-full items-center gap-2 rounded-lg border bg-sunken px-3 py-2 text-start outline-none transition-colors duration-150 hover:border-border-strong focus-visible:ring-[3px] focus-visible:ring-ring/40", children: [_jsx("code", { className: "min-w-0 flex-1 truncate font-mono text-xs", children: value }), _jsx("span", { "aria-live": "polite", className: "inline-flex shrink-0 items-center gap-1 text-xs text-muted-foreground group-hover/copy:text-foreground", 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("failed")] })) : (_jsxs(_Fragment, { children: [_jsx(Copy, { className: "size-3" }), " ", t("copy")] })) })] })] }));
21
+ return (_jsxs("div", { "data-slot": "copy-field", className: cn("flex flex-col gap-1", className), children: [_jsx("span", { className: "text-xs font-medium text-muted-foreground", children: label }), _jsxs("button", { type: "button", onClick: () => copy(value), className: "group/copy flex w-full items-center gap-2 rounded-lg border bg-sunken px-3 py-2 text-start outline-none transition-colors duration-150 hover:border-border-strong focus-visible:ring-[3px] focus-visible:ring-ring/80", children: [_jsx("code", { className: "min-w-0 flex-1 truncate font-mono text-xs", children: value }), _jsx("span", { "aria-live": "polite", className: "inline-flex shrink-0 items-center gap-1 text-xs text-muted-foreground group-hover/copy:text-foreground", 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("failed")] })) : (_jsxs(_Fragment, { children: [_jsx(Copy, { className: "size-3" }), " ", t("copy")] })) })] })] }));
22
22
  }
23
23
  export { CopyField };
@@ -13,13 +13,13 @@ declare function DegradedPanel({ reason, missing, onRetry, retrying, variant, cl
13
13
  /** What went wrong, in a sentence. */
14
14
  reason: React.ReactNode;
15
15
  /** What the person won't see because of it. */
16
- missing?: React.ReactNode;
16
+ missing?: React.ReactNode | undefined;
17
17
  /** Shows a Try again button that calls this. */
18
- onRetry?: () => void;
18
+ onRetry?: (() => void) | undefined;
19
19
  /** Shows the retry as in progress. */
20
- retrying?: boolean;
20
+ retrying?: boolean | undefined;
21
21
  /** A boxed panel, or one line inside other content. */
22
- variant?: "block" | "inline";
23
- className?: string;
22
+ variant?: "block" | "inline" | undefined;
23
+ className?: string | undefined;
24
24
  }): import("react").JSX.Element;
25
25
  export { DegradedPanel };
@@ -27,19 +27,19 @@ export type TextDirection = "ltr" | "rtl";
27
27
  export declare function directionOf(locale: string | undefined): TextDirection;
28
28
  declare function LocaleProvider({ locale, dir, timeZone, strings, children, }: {
29
29
  /** A BCP 47 tag such as "en-GB", "fr" or "ar-EG", for dates, numbers and plural forms. Left out, the browser's own is used once the page has hydrated. */
30
- locale?: string;
30
+ locale?: string | undefined;
31
31
  /** Text direction. Left out, it follows the locale: "rtl" for Arabic, Hebrew, Persian, Urdu and the like. Set `dir` on `<html>` too, so the page's own layout flips with Candor's. */
32
- dir?: TextDirection;
32
+ dir?: TextDirection | undefined;
33
33
  /** An IANA time zone such as "Europe/Paris". With `locale`, server and browser format dates the same way, so absolute times render on the server too. */
34
- timeZone?: string;
34
+ timeZone?: string | undefined;
35
35
  /** Overrides by component name, e.g. `{ thread: { jumpToLatest: "…" } }`; any left out stay English. Nested providers merge. */
36
- strings?: StringOverrides;
36
+ strings?: StringOverrides | undefined;
37
37
  children: React.ReactNode;
38
38
  }): React.JSX.Element;
39
39
  /** The locale, time zone and text direction in effect. */
40
40
  declare function useLocale(): {
41
- locale?: string;
42
- timeZone?: string;
41
+ locale?: string | undefined;
42
+ timeZone?: string | undefined;
43
43
  dir: TextDirection;
44
44
  };
45
45
  /**
@@ -1,10 +1,10 @@
1
1
  import * as React from "react";
2
2
  type Lockable = React.ReactElement<{
3
- "aria-disabled"?: boolean | "true" | "false";
4
- "aria-describedby"?: string;
5
- onClick?: React.MouseEventHandler;
6
- onKeyDown?: React.KeyboardEventHandler;
7
- "data-locked"?: string;
3
+ "aria-disabled"?: boolean | "true" | "false" | undefined;
4
+ "aria-describedby"?: string | undefined;
5
+ onClick?: React.MouseEventHandler | undefined;
6
+ onKeyDown?: React.KeyboardEventHandler | undefined;
7
+ "data-locked"?: string | undefined;
8
8
  }>;
9
9
  /**
10
10
  * The honest-state rule: a control that cannot be used says why.
@@ -17,10 +17,10 @@ type Lockable = React.ReactElement<{
17
17
  */
18
18
  declare function Locked({ reason, inline, children, className, }: {
19
19
  /** Why the control can't be used. Without one, the control renders as normal. */
20
- reason?: string | null;
20
+ reason?: string | null | undefined;
21
21
  /** Print the reason under the control instead of in a tooltip. */
22
- inline?: boolean;
22
+ inline?: boolean | undefined;
23
23
  children: Lockable;
24
- className?: string;
24
+ className?: string | undefined;
25
25
  }): React.JSX.Element;
26
26
  export { Locked };
@@ -9,9 +9,9 @@ export declare const relativeTimeStrings: {
9
9
  * spaces: "5 minutes".
10
10
  */
11
11
  export declare function formatRelative(date: Date, now?: Date, { locale, justNow, bare }?: {
12
- locale?: string;
13
- justNow?: string;
14
- bare?: boolean;
12
+ locale?: string | undefined;
13
+ justNow?: string | undefined;
14
+ bare?: boolean | undefined;
15
15
  }): string;
16
16
  /** The current time in milliseconds, updated every 30 seconds; null while rendering on the server. */
17
17
  export declare function useNow(): number | null;
@@ -23,9 +23,9 @@ declare function RelativeTime({ date, bare, format, className, }: {
23
23
  /** When it happened. Shown as "5 minutes ago", with the exact time on hover. */
24
24
  date: Date | string;
25
25
  /** Drop "ago" and "in", for tight spaces: "5 minutes". */
26
- bare?: boolean;
26
+ bare?: boolean | undefined;
27
27
  /** "relative" (the default), or an absolute "date", "datetime" or "time" in the locale. */
28
- format?: "relative" | DateStyle;
29
- className?: string;
28
+ format?: "relative" | DateStyle | undefined;
29
+ className?: string | undefined;
30
30
  }): React.JSX.Element;
31
31
  export { RelativeTime };
@@ -8,9 +8,9 @@ declare function StatusDot({ status, showLabel, label, className, }: {
8
8
  /** Which status to show; running and waiting pulse. */
9
9
  status: RunStatus;
10
10
  /** Print the status name next to the dot. */
11
- showLabel?: boolean;
11
+ showLabel?: boolean | undefined;
12
12
  /** Words to use instead of the status name. */
13
- label?: string;
14
- className?: string;
13
+ label?: string | undefined;
14
+ className?: string | undefined;
15
15
  }): import("react").JSX.Element;
16
16
  export { StatusDot };
@@ -4,8 +4,8 @@ export type Step = {
4
4
  id: string;
5
5
  title: React.ReactNode;
6
6
  status: RunStatus;
7
- detail?: React.ReactNode;
8
- meta?: React.ReactNode;
7
+ detail?: React.ReactNode | undefined;
8
+ meta?: React.ReactNode | undefined;
9
9
  };
10
10
  declare function StepMark({ status, }: {
11
11
  /** The step's status, which picks its mark. */
@@ -18,6 +18,6 @@ declare function StepMark({ status, }: {
18
18
  declare function StepList({ steps, className, }: {
19
19
  /** The steps in order, each with a status, and optional detail and meta. */
20
20
  steps: Step[];
21
- className?: string;
21
+ className?: string | undefined;
22
22
  }): React.JSX.Element;
23
23
  export { StepList, StepMark };
@@ -6,12 +6,12 @@ declare function StreamingText({ text, streaming, className, }: {
6
6
  /** The text so far. */
7
7
  text: string;
8
8
  /** Show a caret while more is arriving; announced once when it stops. */
9
- streaming?: boolean;
10
- className?: string;
9
+ streaming?: boolean | undefined;
10
+ className?: string | undefined;
11
11
  }): import("react").JSX.Element;
12
12
  /** A label that shimmers while an agent is working ("Thinking", "Reading the docs"). */
13
13
  declare function ShimmerLabel({ children, className, }: {
14
14
  children: React.ReactNode;
15
- className?: string;
15
+ className?: string | undefined;
16
16
  }): import("react").JSX.Element;
17
17
  export { StreamingText, ShimmerLabel };
@@ -34,15 +34,15 @@ declare function AgentAvatar({ agent, size, className, }: {
34
34
  /** Who the agent is: id, display name and role. */
35
35
  agent: AgentIdentity;
36
36
  /** md for message headers, sm for lists and chips. */
37
- size?: "sm" | "md";
38
- className?: string;
37
+ size?: "sm" | "md" | undefined;
38
+ className?: string | undefined;
39
39
  }): React.JSX.Element;
40
40
  /** Avatar plus name, with the role spelled out for screen readers. */
41
41
  declare function AgentBadge({ agent, size, className, }: {
42
42
  /** Who the agent is: id, display name and role. */
43
43
  agent: AgentIdentity;
44
44
  /** md for message headers, sm for lists and chips. */
45
- size?: "sm" | "md";
46
- className?: string;
45
+ size?: "sm" | "md" | undefined;
46
+ className?: string | undefined;
47
47
  }): React.JSX.Element;
48
48
  export { AgentAvatar, AgentBadge, AgentRolesProvider, useAgentRoles };
@@ -0,0 +1,37 @@
1
+ import type { AgentIdentity } from "../lib/chat.js";
2
+ /** AiDisclosure's fixed words; override them under `aiDisclosure` in LocaleProvider's `strings`. */
3
+ export declare const aiDisclosureStrings: {
4
+ /** The line in the thread when the app names the agent. */
5
+ line: string;
6
+ /** The line in the thread when no name is given. */
7
+ lineUnnamed: string;
8
+ /** The badge's visible text. */
9
+ badge: string;
10
+ /** The badge's accessible name when the app names the agent. */
11
+ badgeLabel: string;
12
+ /** The badge's accessible name when no name is given. */
13
+ badgeLabelUnnamed: string;
14
+ /** The heading of the badge's explanation. */
15
+ about: string;
16
+ aboutUnnamed: string;
17
+ learnMore: string;
18
+ };
19
+ /**
20
+ * A plain notice that the person is interacting with an AI system: a first line in the thread (`variant="line"`) or
21
+ * a compact "AI" badge for a thread header (`variant="badge"`) that opens to the app's own explanation. It helps an
22
+ * app tell people they're talking to an AI, as transparency rules such as Article 50(1) of the EU AI Act ask; whether
23
+ * a given placement meets those rules, and what else applies, stays the deployer's call. The agent's name and any
24
+ * details come from the app, never from model text. It's static: read in place, never announced as a live update.
25
+ */
26
+ declare function AiDisclosure({ agent, variant, details, onLearnMore, className, }: {
27
+ /** The agent, named by the app: an AgentIdentity or a plain name. Left out, the notice says "an AI system". Never model text. */
28
+ agent?: AgentIdentity | string | undefined;
29
+ /** "line" (the default) for a first line in the thread; "badge" for a compact mark in a header, e.g. ThreadHeader's. Place each where it fits. */
30
+ variant?: "line" | "badge" | undefined;
31
+ /** The app's own plain text about the system: who operates it, what data it uses, how to reach a person. Shown under the line, or in the badge's explanation. */
32
+ details?: string | undefined;
33
+ /** Opens the app's own page or dialog about its AI use. A callback, never a URL from the model. Shows a "Learn more" button. */
34
+ onLearnMore?: (() => void) | undefined;
35
+ className?: string | undefined;
36
+ }): import("react").JSX.Element;
37
+ export { AiDisclosure };
@@ -0,0 +1,48 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Bot } from "lucide-react";
4
+ import { cn } from "../lib/utils.js";
5
+ import { useStrings } from "../blocks/locale.js";
6
+ import { Button } from "../ui/button.js";
7
+ import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover.js";
8
+ /** AiDisclosure's fixed words; override them under `aiDisclosure` in LocaleProvider's `strings`. */
9
+ export const aiDisclosureStrings = {
10
+ /** The line in the thread when the app names the agent. */
11
+ line: "{name} is an AI system, not a person.",
12
+ /** The line in the thread when no name is given. */
13
+ lineUnnamed: "You're interacting with an AI system, not a person.",
14
+ /** The badge's visible text. */
15
+ badge: "AI",
16
+ /** The badge's accessible name when the app names the agent. */
17
+ badgeLabel: "{name} is an AI system",
18
+ /** The badge's accessible name when no name is given. */
19
+ badgeLabelUnnamed: "AI system",
20
+ /** The heading of the badge's explanation. */
21
+ about: "About {name}",
22
+ aboutUnnamed: "About this AI system",
23
+ learnMore: "Learn more",
24
+ };
25
+ /**
26
+ * A plain notice that the person is interacting with an AI system: a first line in the thread (`variant="line"`) or
27
+ * a compact "AI" badge for a thread header (`variant="badge"`) that opens to the app's own explanation. It helps an
28
+ * app tell people they're talking to an AI, as transparency rules such as Article 50(1) of the EU AI Act ask; whether
29
+ * a given placement meets those rules, and what else applies, stays the deployer's call. The agent's name and any
30
+ * details come from the app, never from model text. It's static: read in place, never announced as a live update.
31
+ */
32
+ function AiDisclosure({ agent, variant = "line", details, onLearnMore, className, }) {
33
+ const t = useStrings("aiDisclosure", aiDisclosureStrings);
34
+ const name = typeof agent === "string" ? agent : agent?.name;
35
+ const sentence = name ? t("line", { name }) : t("lineUnnamed");
36
+ const learnMore = onLearnMore && (_jsx(Button, { type: "button", variant: "link", size: "xs", className: "h-6 px-0 has-[>svg]:px-0", onClick: onLearnMore, children: t("learnMore") }));
37
+ if (variant === "badge") {
38
+ const label = name ? t("badgeLabel", { name }) : t("badgeLabelUnnamed");
39
+ const mark = (_jsxs(_Fragment, { children: [_jsx(Bot, { "aria-hidden": true, className: "size-3 shrink-0" }), _jsx("span", { "aria-hidden": true, children: t("badge") })] }));
40
+ const badgeClass = "inline-flex min-h-6 min-w-6 items-center justify-center gap-1 rounded-full border px-2 text-2xs font-medium text-muted-foreground";
41
+ // Nothing more to say: a static mark with a name, not a button that opens nothing.
42
+ if (!details && !onLearnMore)
43
+ return (_jsx("span", { "data-slot": "ai-disclosure", "data-variant": "badge", role: "img", "aria-label": label, className: cn(badgeClass, className), children: mark }));
44
+ return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { "data-slot": "ai-disclosure", "data-variant": "badge", "aria-label": label, className: cn(badgeClass, "outline-none hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/80", className), children: mark }), _jsxs(PopoverContent, { align: "start", className: "flex w-72 flex-col gap-1.5 p-3 text-xs", children: [_jsx("p", { className: "text-sm font-medium", children: name ? t("about", { name }) : t("aboutUnnamed") }), _jsx("p", { children: sentence }), details && _jsx("p", { className: "text-muted-foreground", children: details }), learnMore && _jsx("div", { children: learnMore })] })] }));
45
+ }
46
+ return (_jsxs("div", { "data-slot": "ai-disclosure", "data-variant": "line", role: "note", className: cn("flex items-start justify-center gap-1.5 py-1 text-center text-xs text-muted-foreground", className), children: [_jsx(Bot, { "aria-hidden": true, className: "mt-0.5 size-3 shrink-0" }), _jsxs("p", { className: "flex flex-wrap items-center justify-center gap-x-1.5", children: [_jsx("span", { children: sentence }), details && _jsx("span", { children: details }), learnMore] })] }));
47
+ }
48
+ export { AiDisclosure };
@@ -27,8 +27,8 @@ declare function AttachmentChip({ attachment: a, onRemove, className, }: {
27
27
  /** The file, with progress (0–1) while uploading or an error if it failed. */
28
28
  attachment: Attachment;
29
29
  /** Shows a remove button. */
30
- onRemove?: (id: string) => void;
31
- className?: string;
30
+ onRemove?: ((id: string) => void) | undefined;
31
+ className?: string | undefined;
32
32
  }): React.JSX.Element;
33
33
  /**
34
34
  * A sent file, as a tile in a message: an image preview when the app supplied one, else an icon for its type, with
@@ -39,15 +39,15 @@ declare function AttachmentPreview({ attachment: a, onOpen, className, }: {
39
39
  /** The file. `previewUrl` must be one your app serves (a thumbnail, a blob: URL); it's shown only for images. */
40
40
  attachment: Attachment;
41
41
  /** Makes the tile a button that opens the file. */
42
- onOpen?: (attachment: Attachment) => void;
43
- className?: string;
42
+ onOpen?: ((attachment: Attachment) => void) | undefined;
43
+ className?: string | undefined;
44
44
  }): React.JSX.Element;
45
45
  /** Several sent files, as a wrapping row of tiles, announced as a list. */
46
46
  declare function AttachmentList({ attachments, onOpen, className, }: {
47
47
  /** The files. */
48
48
  attachments: Attachment[];
49
49
  /** Makes each tile a button that opens its file. */
50
- onOpen?: (attachment: Attachment) => void;
51
- className?: string;
50
+ onOpen?: ((attachment: Attachment) => void) | undefined;
51
+ className?: string | undefined;
52
52
  }): React.JSX.Element;
53
53
  export { AttachmentChip, AttachmentPreview, AttachmentList };
@@ -58,7 +58,7 @@ function AttachmentChip({ attachment: a, onRemove, className, }) {
58
58
  const t = useStrings("attachmentPreview", attachmentPreviewStrings);
59
59
  const fmt = useFormat();
60
60
  const thumb = thumbnail(a);
61
- return (_jsxs("span", { "data-slot": "attachment-chip", className: cn("relative inline-flex max-w-56 items-center gap-1.5 overflow-hidden rounded-md border bg-card py-1 pe-1 ps-2 text-xs", a.error && "border-destructive/50", className), children: [thumb ? _jsx("img", { src: thumb, alt: "", className: "size-4 shrink-0 rounded-xs object-cover" }) : _jsx(Paperclip, { "aria-hidden": true, className: "size-3 shrink-0 text-muted-foreground" }), _jsx("span", { className: "truncate", children: a.name }), _jsx("span", { className: cn("shrink-0 tabular-nums", a.error ? "text-destructive-text" : "text-faint-foreground"), children: a.error ? a.error : a.progress !== undefined ? fmt.number(Math.round(a.progress * 100) / 100, { style: "percent" }) : formatFileSize(a.size, fmt.number, t) }), onRemove && (_jsx("button", { type: "button", "aria-label": t("remove", { name: a.name }), onClick: () => onRemove(a.id), className: "rounded-sm p-0.5 text-faint-foreground hover:bg-accent hover:text-foreground", children: _jsx(X, { className: "size-3" }) })), a.progress !== undefined && !a.error && _jsx("span", { "aria-hidden": true, className: "absolute bottom-0 start-0 h-0.5 bg-primary transition-[width] duration-300", style: { width: `${a.progress * 100}%` } })] }));
61
+ return (_jsxs("span", { "data-slot": "attachment-chip", className: cn("relative inline-flex max-w-56 items-center gap-1.5 overflow-hidden rounded-md border bg-card py-1 pe-1 ps-2 text-xs", a.error && "border-destructive/50", className), children: [thumb ? _jsx("img", { src: thumb, alt: "", className: "size-4 shrink-0 rounded-xs object-cover" }) : _jsx(Paperclip, { "aria-hidden": true, className: "size-3 shrink-0 text-muted-foreground" }), _jsx("span", { className: "truncate", children: a.name }), _jsx("span", { className: cn("shrink-0 tabular-nums", a.error ? "text-destructive-text" : "text-faint-foreground"), children: a.error ? a.error : a.progress !== undefined ? fmt.number(Math.round(a.progress * 100) / 100, { style: "percent" }) : formatFileSize(a.size, fmt.number, t) }), onRemove && (_jsx("button", { type: "button", "aria-label": t("remove", { name: a.name }), onClick: () => onRemove(a.id), className: "relative after:absolute after:-inset-1 rounded-sm p-0.5 text-faint-foreground hover:bg-accent hover:text-foreground", children: _jsx(X, { className: "size-3" }) })), a.progress !== undefined && !a.error && _jsx("span", { "aria-hidden": true, className: "absolute bottom-0 start-0 h-0.5 bg-primary transition-[width] duration-300", style: { width: `${a.progress * 100}%` } })] }));
62
62
  }
63
63
  /**
64
64
  * A sent file, as a tile in a message: an image preview when the app supplied one, else an icon for its type, with
@@ -72,7 +72,7 @@ function AttachmentPreview({ attachment: a, onOpen, className, }) {
72
72
  const size = formatFileSize(a.size, fmt.number, t);
73
73
  const body = (_jsxs(_Fragment, { children: [thumb ? (_jsx("img", { src: thumb, alt: "", className: "h-24 w-full rounded-md bg-muted object-cover" })) : (_jsx("span", { "aria-hidden": true, className: "flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground", children: _jsx(FileGlyph, { attachment: a }) })), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate text-sm", children: a.name }), (size || a.error) && _jsx("span", { className: cn("block text-xs tabular-nums", a.error ? "text-destructive-text" : "text-faint-foreground"), children: a.error ?? size })] })] }));
74
74
  const frame = cn("flex w-56 max-w-full gap-2.5 rounded-lg border bg-card p-2 text-start", thumb ? "flex-col" : "items-center", className);
75
- return onOpen ? (_jsx("button", { type: "button", "data-slot": "attachment-preview", "aria-label": t("open", { name: a.name }), onClick: () => onOpen(a), className: cn(frame, "outline-none transition-colors duration-150 hover:border-border-strong focus-visible:ring-[3px] focus-visible:ring-ring/40"), children: body })) : (_jsx("div", { "data-slot": "attachment-preview", className: frame, children: body }));
75
+ return onOpen ? (_jsx("button", { type: "button", "data-slot": "attachment-preview", "aria-label": t("open", { name: a.name }), onClick: () => onOpen(a), className: cn(frame, "outline-none transition-colors duration-150 hover:border-border-strong focus-visible:ring-[3px] focus-visible:ring-ring/80"), children: body })) : (_jsx("div", { "data-slot": "attachment-preview", className: frame, children: body }));
76
76
  }
77
77
  /** Several sent files, as a wrapping row of tiles, announced as a list. */
78
78
  function AttachmentList({ attachments, onOpen, className, }) {
@@ -32,7 +32,7 @@ class CardBoundary extends React.Component {
32
32
  }
33
33
  function Summary({ text, }) {
34
34
  const t = useStrings("cardRegistry", cardRegistryStrings);
35
- return (_jsxs(Collapsible, { className: "group/summary", children: [_jsxs(CollapsibleTrigger, { className: "inline-flex items-center gap-1 text-xs text-muted-foreground outline-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/40 rounded-sm", children: [_jsx(ChevronRight, { "aria-hidden": true, className: "rtl:-scale-x-100 size-3 transition-transform group-data-[state=open]/summary:[transform:rotate(90deg)]" }), " ", t("showAsText")] }), _jsx(CollapsibleContent, { children: _jsx("p", { className: "mt-1.5 rounded-md bg-sunken px-3 py-2 text-sm whitespace-pre-wrap", children: text }) })] }));
35
+ return (_jsxs(Collapsible, { className: "group/summary", children: [_jsxs(CollapsibleTrigger, { className: "relative after:absolute after:inset-x-0 after:-inset-y-1 inline-flex items-center gap-1 text-xs text-muted-foreground outline-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/80 rounded-sm", children: [_jsx(ChevronRight, { "aria-hidden": true, className: "rtl:-scale-x-100 size-3 transition-transform group-data-[state=open]/summary:[transform:rotate(90deg)]" }), " ", t("showAsText")] }), _jsx(CollapsibleContent, { children: _jsx("p", { className: "mt-1.5 rounded-md bg-sunken px-3 py-2 text-sm whitespace-pre-wrap", children: text }) })] }));
36
36
  }
37
37
  /**
38
38
  * Renders one card from the registry. Three honest fallbacks, each with the