@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
@@ -1,5 +1,46 @@
1
- import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
2
- import type { AgentIdentity, EntityRef, Message } from "../lib/chat.js";
1
+ import type { PermissionResult, PermissionUpdate, SDKMessage } from "@anthropic-ai/claude-agent-sdk";
2
+ import type { AgentIdentity, EntityRef, Message, ToolSource } from "../lib/chat.js";
3
+ /**
4
+ * How the agent's tool calls are approved, as the SDK names it: "default" (ask for anything not allowed already),
5
+ * "acceptEdits", "plan", "auto" (a classifier decides), "dontAsk" (deny what isn't allowed already) and
6
+ * "bypassPermissions". Kept open: a newer SDK's mode passes through as its name.
7
+ */
8
+ export type ClaudePermissionMode = "default" | "acceptEdits" | "plan" | "auto" | "dontAsk" | "bypassPermissions" | (string & {});
9
+ /** One of the agent's questions (the SDK's AskUserQuestion), shaped for ClarifyingQuestions. Options' ids are their labels. */
10
+ export type ClaudeQuestion = {
11
+ /** The question's text, which is also how its answer is keyed back to the SDK. */
12
+ id: string;
13
+ kind: "choice";
14
+ prompt: string;
15
+ /** The SDK's short header ("Library", "Approach"). */
16
+ tag?: string | undefined;
17
+ options: {
18
+ id: string;
19
+ label: string;
20
+ detail?: string | undefined;
21
+ }[];
22
+ multiple: boolean;
23
+ /** The SDK always lets the person answer in their own words. */
24
+ other: true;
25
+ };
26
+ /** A person's answer to one ClaudeQuestion: the chosen labels, their own words, or both. Same shape as ClarifyingAnswer. */
27
+ export type ClaudeQuestionAnswer = {
28
+ questionId: string;
29
+ choice?: string[] | undefined;
30
+ text?: string | undefined;
31
+ };
32
+ /** How a person answered a permission request. */
33
+ export type ClaudeApprovalAnswer = {
34
+ approved: boolean;
35
+ /** Why it was denied, sent to the agent. */
36
+ reason?: string | undefined;
37
+ /** The tool's input as the person edited it before allowing it. The tool runs with exactly this. */
38
+ updatedInput?: Record<string, unknown> | undefined;
39
+ /** Answers to the agent's questions, when the request is one (`questions`). */
40
+ answers?: ClaudeQuestionAnswer[] | undefined;
41
+ /** Which of the request's `suggestions` the person chose to keep ("always allow"), by index. None unless they choose. */
42
+ alwaysAllow?: number[] | undefined;
43
+ };
3
44
  /** Events of Candor's own, mixed into the SDK's stream. */
4
45
  export type CandorAgentEvent =
5
46
  /** What the user sent. Added in the browser; the SDK doesn't echo a plain prompt back. */
@@ -7,7 +48,7 @@ export type CandorAgentEvent =
7
48
  type: "candor_user";
8
49
  id: string;
9
50
  text: string;
10
- references?: EntityRef[];
51
+ references?: EntityRef[] | undefined;
11
52
  }
12
53
  /** A tool call is waiting for a person to allow or deny it. */
13
54
  | {
@@ -17,20 +58,55 @@ export type CandorAgentEvent =
17
58
  tool: string;
18
59
  input: unknown;
19
60
  /** The SDK's own sentence for the request, e.g. "Claude wants to edit notes.md". */
20
- title?: string;
21
- description?: string;
61
+ title?: string | undefined;
62
+ description?: string | undefined;
22
63
  /** Why the SDK asked, when it says. */
23
- reason?: string;
24
- blockedPath?: string;
64
+ reason?: string | undefined;
65
+ blockedPath?: string | undefined;
25
66
  /** Open on "Deny" and offer no one-key approve: the request grants more than it looks. */
26
- defaultToNo?: boolean;
67
+ defaultToNo?: boolean | undefined;
68
+ /** A short name for the action: "Edit file". */
69
+ displayName?: string | undefined;
70
+ /**
71
+ * Rules the SDK suggests so this isn't asked again ("always allow"), as the SDK sends them. Offer them as a
72
+ * choice the person makes; never keep one unless they pick it.
73
+ */
74
+ suggestions?: PermissionUpdate[] | undefined;
75
+ /** Offer no "always allow" for this request: its rule would grant more than this one action. */
76
+ noAlwaysAllow?: boolean | undefined;
77
+ /** The permission mode the run was in when it asked. */
78
+ mode?: ClaudePermissionMode | undefined;
79
+ /** The agent's questions, when the tool is AskUserQuestion: answer them instead of allowing a tool. */
80
+ questions?: ClaudeQuestion[] | undefined;
81
+ /** Set when a subagent asked, with its id. */
82
+ agentId?: string | undefined;
83
+ /** For an MCP tool: its server's name and where that server was configured, as the SDK reports them. */
84
+ mcpServer?: {
85
+ name: string;
86
+ source: string;
87
+ } | undefined;
88
+ /** Whether the server lets a person edit this call's input before allowing it. False means no; left out means yes (1.5.0). */
89
+ editable?: boolean | undefined;
27
90
  }
28
91
  /** The request was answered, or the run ended before anyone did. */
29
92
  | {
30
93
  type: "candor_approval_closed";
31
94
  id: string;
32
95
  approved: boolean;
33
- reason?: string;
96
+ reason?: string | undefined;
97
+ /** The person changed the input before allowing it. */
98
+ edited?: boolean | undefined;
99
+ /** The input the tool ran with, when the person edited it. */
100
+ input?: unknown;
101
+ /** Nobody answered in time (the server's `approvalTimeoutMs`). */
102
+ expired?: boolean | undefined;
103
+ /** How many "always allow" rules the person kept. */
104
+ kept?: number | undefined;
105
+ }
106
+ /** The run's permission mode changed, by the app (`setPermissionMode`). The SDK's own mode reports also count. */
107
+ | {
108
+ type: "candor_mode";
109
+ mode: ClaudePermissionMode;
34
110
  }
35
111
  /** A typed card for the app's card registry, sent by your server (a tool's result as a diff, a plan, a table). */
36
112
  | {
@@ -59,22 +135,55 @@ export type FromClaudeAgentOptions = {
59
135
  /** Who the agent is. Candor always names the agent. */
60
136
  agent: AgentIdentity;
61
137
  /** A request is open now: the last reply is still streaming and its approvals can be answered. */
62
- running?: boolean;
138
+ running?: boolean | undefined;
63
139
  /** A plain-words title for a tool call: "Read notes.md". */
64
- toolTitle?: (tool: string, input: unknown) => string | undefined;
140
+ toolTitle?: ((tool: string, input: unknown) => string | undefined) | undefined;
141
+ /**
142
+ * The sources a tool's result supports, for the reply's source list: `(tool, input, output) => [{ kind: "document",
143
+ * label: "Pricing policy v3" }]`. Read only from what the tool returned, never from numbers in the agent's text.
144
+ * Claude's own citations and web search results are read without it.
145
+ */
146
+ sourcesFrom?: ((tool: string, input: unknown, output: string) => ToolSource[] | undefined) | undefined;
65
147
  };
66
148
  export type ClaudeAgentState = {
67
149
  messages: Message[];
68
150
  /** Tool calls waiting on a person, oldest first. Empty once the request has ended. */
69
151
  approvals: ClaudeApproval[];
70
152
  /** Send it back with the next message to continue the same session (the SDK's `resume`). */
71
- sessionId?: string;
72
- usage?: ClaudeAgentUsage;
153
+ sessionId?: string | undefined;
154
+ usage?: ClaudeAgentUsage | undefined;
73
155
  /** A passing problem the agent is handling itself, such as a retried API call. */
74
- notice?: string;
156
+ notice?: string | undefined;
157
+ /** The permission mode last reported, by the SDK or the app. Absent until one is. */
158
+ permissionMode?: ClaudePermissionMode | undefined;
75
159
  };
76
160
  /**
77
161
  * Turns Claude Agent SDK events into Candor messages, waiting approvals and usage.
78
162
  * Pure, so it can run on every render; useClaudeAgent calls it for you.
79
163
  */
80
164
  export declare function fromClaudeAgentEvents(events: AgentEvent[], options: FromClaudeAgentOptions): ClaudeAgentState;
165
+ /**
166
+ * The questions in an AskUserQuestion call, ready for ClarifyingQuestions; undefined when the input isn't one.
167
+ * Option ids are their labels, which is what the SDK takes back.
168
+ */
169
+ export declare function claudeQuestions(input: unknown): ClaudeQuestion[] | undefined;
170
+ type Answerable = Pick<ClaudeApproval, "input" | "suggestions" | "noAlwaysAllow" | "questions">;
171
+ /**
172
+ * What to hand back to the SDK for a person's answer: the `PermissionResult` canUseTool resolves with. Pure, so it
173
+ * works wherever the answer lands (your own route, a queue, a workflow signal). Questions become AskUserQuestion's
174
+ * `answers`, keyed by question text, several choices joined with ", " and the person's own words used as they are.
175
+ * Only the suggestions the person picked are kept, and none when the request offers no "always allow".
176
+ */
177
+ export declare function claudePermissionResult(request: Answerable, answer: ClaudeApprovalAnswer): PermissionResult;
178
+ /** A "don't ask again" rule in plain parts, for PermissionPrompt's `suggestions`. */
179
+ export type ClaudeSuggestion = {
180
+ action: "allow" | "deny" | "ask" | "remove" | "mode" | "directories" | "other";
181
+ what: string;
182
+ scope?: string | undefined;
183
+ };
184
+ /**
185
+ * The SDK's suggested permission updates as plain parts PermissionPrompt shows, in the same order, so the indexes a
186
+ * person ticks are the ones `alwaysAllow` sends back.
187
+ */
188
+ export declare function claudeSuggestions(updates: PermissionUpdate[] | undefined): ClaudeSuggestion[];
189
+ export {};
@@ -13,7 +13,8 @@ export function fromClaudeAgentEvents(events, options) {
13
13
  let usage;
14
14
  let lastCost = 0;
15
15
  let notice;
16
- const current = (id) => (reply ??= { id, parts: [], reasoning: [], tools: new Map(), live: new Map() });
16
+ let permissionMode;
17
+ const current = (id) => (reply ??= { id, parts: [], reasoning: [], tools: new Map(), live: new Map(), sources: new Map(), inputs: new Map() });
17
18
  const close = (last) => {
18
19
  if (reply)
19
20
  messages.push(render(reply, { ...options, model, last }));
@@ -43,11 +44,24 @@ export function fromClaudeAgentEvents(events, options) {
43
44
  const call = approval && reply?.tools.get(approval.toolUseId);
44
45
  if (call && call.status === "waiting") {
45
46
  call.status = e.approved ? "running" : "cancelled";
46
- if (!e.approved)
47
+ if (e.expired)
48
+ call.error = `Expired: ${e.reason ?? "nobody answered in time."}`;
49
+ else if (!e.approved)
47
50
  call.error = e.reason ? `Denied: ${e.reason}` : "Denied.";
51
+ // What ran is what the person allowed, so the card shows it, with what the agent proposed kept beside it.
52
+ if (e.approved && e.input !== undefined) {
53
+ call.approval = { by: "person", originalInput: call.input };
54
+ call.input = json(e.input);
55
+ reply.inputs.set(call.id, e.input);
56
+ }
57
+ else if (e.approved)
58
+ call.approval = { by: "person" };
48
59
  }
49
60
  break;
50
61
  }
62
+ case "candor_mode":
63
+ permissionMode = e.mode;
64
+ break;
51
65
  case "candor_card":
52
66
  current(`card-${messages.length}`).parts.push({ kind: "card", card: e.card, props: e.props, summary: e.summary });
53
67
  break;
@@ -59,6 +73,12 @@ export function fromClaudeAgentEvents(events, options) {
59
73
  if (e.subtype === "init") {
60
74
  sessionId = e.session_id;
61
75
  model = e.model;
76
+ if (e.permissionMode)
77
+ permissionMode = e.permissionMode;
78
+ }
79
+ else if (e.subtype === "status") {
80
+ if (e.permissionMode)
81
+ permissionMode = e.permissionMode;
62
82
  }
63
83
  else if (e.subtype === "permission_denied") {
64
84
  const call = reply?.tools.get(e.tool_use_id);
@@ -117,6 +137,9 @@ export function fromClaudeAgentEvents(events, options) {
117
137
  call.status = block.is_error ? "failed" : "succeeded";
118
138
  if (block.is_error)
119
139
  call.error = firstLine(output) ?? "The tool failed and didn’t say why.";
140
+ else if (options.sourcesFrom)
141
+ for (const found of options.sourcesFrom(call.tool, reply.inputs.get(call.id), output) ?? [])
142
+ addSource(reply, found);
120
143
  }
121
144
  break;
122
145
  }
@@ -146,12 +169,40 @@ export function fromClaudeAgentEvents(events, options) {
146
169
  sessionId,
147
170
  usage,
148
171
  notice: options.running ? notice : undefined,
172
+ permissionMode,
149
173
  };
150
174
  }
175
+ /** Adds a source once (by key) and returns its id. Markers count up in the order sources first appear. */
176
+ function addSource(r, source) {
177
+ const { key: own, ...rest } = source;
178
+ const key = own ?? `${rest.kind}:${rest.label}`;
179
+ const known = r.sources.get(key);
180
+ if (known)
181
+ return known.id;
182
+ const id = `${r.id}-s${r.sources.size + 1}`;
183
+ r.sources.set(key, { ...rest, id, marker: r.sources.size + 1 });
184
+ return id;
185
+ }
186
+ /** One of Claude's citations as a source: a web page by its address, a document or search result by its title. */
187
+ function citationSource(c) {
188
+ const snippet = c.cited_text?.trim() ? clip(c.cited_text.trim(), 280) : undefined;
189
+ if (c.url)
190
+ return { key: `web:${c.url}`, kind: "web", label: c.title || c.url, origin: host(c.url), snippet };
191
+ const title = c.document_title ?? c.title ?? c.source;
192
+ if (title)
193
+ return { key: `document:${title}`, kind: "document", label: title, snippet };
194
+ return undefined;
195
+ }
151
196
  function addBlock(r, block, options) {
152
197
  if (block.type === "text") {
153
- if (block.text.trim())
154
- r.parts.push({ kind: "text", text: block.text });
198
+ if (!block.text.trim())
199
+ return;
200
+ // Claude's citations point at what it read; the [n] comes from them, never from numbers in the text.
201
+ const cites = (block.citations ?? []).flatMap((c) => {
202
+ const source = citationSource(c);
203
+ return source ? [addSource(r, source)] : [];
204
+ });
205
+ r.parts.push({ kind: "text", text: block.text, cites: cites.length ? [...new Set(cites)] : undefined });
155
206
  }
156
207
  else if (block.type === "thinking") {
157
208
  // With the SDK's default display, thinking arrives empty; there's nothing to show then.
@@ -161,6 +212,7 @@ function addBlock(r, block, options) {
161
212
  else if (block.type === "tool_use" || block.type === "server_tool_use" || block.type === "mcp_tool_use") {
162
213
  const call = { id: block.id, tool: block.name, title: options.toolTitle?.(block.name, block.input) ?? block.name, status: "running", input: json(block.input) };
163
214
  r.tools.set(block.id, call);
215
+ r.inputs.set(block.id, block.input);
164
216
  r.parts.push({ kind: "tool", id: block.id });
165
217
  }
166
218
  else if ("tool_use_id" in block) {
@@ -174,6 +226,13 @@ function addBlock(r, block, options) {
174
226
  call.outputLanguage = "json";
175
227
  if (failed)
176
228
  call.error = `The tool failed: ${String(content.error_code)}.`;
229
+ // Web search results are sources the reply can stand on, whether or not the text cites them.
230
+ if (!failed && block.type === "web_search_tool_result" && Array.isArray(content)) {
231
+ for (const item of content) {
232
+ if (item.type === "web_search_result" && item.url)
233
+ addSource(r, { key: `web:${item.url}`, kind: "web", label: item.title || item.url, origin: host(item.url) });
234
+ }
235
+ }
177
236
  }
178
237
  }
179
238
  }
@@ -210,7 +269,7 @@ function render(r, o) {
210
269
  if (p.kind === "card")
211
270
  blocks.push({ kind: "card", card: p.card, props: p.props, summary: p.summary });
212
271
  else
213
- blocks.push(...markdownBlocks(p.text));
272
+ blocks.push(...withCitations(markdownBlocks(p.text), p.cites));
214
273
  }
215
274
  flush();
216
275
  const error = r.failure ?? r.result?.error;
@@ -222,6 +281,7 @@ function render(r, o) {
222
281
  status: open ? "streaming" : r.result?.ok ? "complete" : error ? "failed" : "interrupted",
223
282
  blocks,
224
283
  reasoning: reasoning.length ? reasoning : undefined,
284
+ sources: r.sources.size ? [...r.sources.values()] : undefined,
225
285
  attribution: r.result ? { model: o.model, cost: r.result.cost ? `$${r.result.cost.toFixed(r.result.cost < 0.01 ? 4 : 2)} (estimated)` : undefined } : undefined,
226
286
  };
227
287
  }
@@ -251,6 +311,84 @@ function resultText(content) {
251
311
  return content.map((c) => (c.type === "text" ? (c.text ?? "") : c.type ? `[${c.type}]` : "")).join("\n");
252
312
  }
253
313
  const firstLine = (text) => text.trim().split("\n")[0] || undefined;
314
+ const clip = (text, max) => (text.length > max ? `${text.slice(0, max - 1)}…` : text);
315
+ const host = (url) => {
316
+ try {
317
+ return new URL(url).hostname;
318
+ }
319
+ catch {
320
+ return undefined;
321
+ }
322
+ };
323
+ /** Puts a text block's [n] markers at its end: after the last paragraph's, heading's or list item's words. */
324
+ function withCitations(blocks, cites) {
325
+ if (!cites?.length)
326
+ return blocks;
327
+ const marks = cites.map((sourceId) => ({ kind: "citation", sourceId }));
328
+ const out = [...blocks];
329
+ for (let i = out.length - 1; i >= 0; i--) {
330
+ const b = out[i];
331
+ if (b.kind === "paragraph" || b.kind === "heading") {
332
+ out[i] = { ...b, nodes: [...b.nodes, ...marks] };
333
+ return out;
334
+ }
335
+ if (b.kind === "list" && b.items.length) {
336
+ out[i] = { ...b, items: [...b.items.slice(0, -1), [...b.items.at(-1), ...marks]] };
337
+ return out;
338
+ }
339
+ }
340
+ // Only code or plain text: the markers get a line of their own.
341
+ return [...out, { kind: "paragraph", nodes: marks }];
342
+ }
343
+ /**
344
+ * The questions in an AskUserQuestion call, ready for ClarifyingQuestions; undefined when the input isn't one.
345
+ * Option ids are their labels, which is what the SDK takes back.
346
+ */
347
+ export function claudeQuestions(input) {
348
+ const qs = input?.questions;
349
+ if (!Array.isArray(qs) || !qs.length)
350
+ return undefined;
351
+ const out = [];
352
+ for (const q of qs) {
353
+ if (typeof q?.question !== "string" || !Array.isArray(q.options))
354
+ return undefined;
355
+ out.push({
356
+ id: q.question,
357
+ kind: "choice",
358
+ prompt: q.question,
359
+ tag: typeof q.header === "string" && q.header ? q.header : undefined,
360
+ options: q.options.filter((o) => typeof o?.label === "string").map((o) => ({ id: o.label, label: o.label, detail: o.description || undefined })),
361
+ multiple: q.multiSelect === true,
362
+ other: true,
363
+ });
364
+ }
365
+ return out;
366
+ }
367
+ /**
368
+ * What to hand back to the SDK for a person's answer: the `PermissionResult` canUseTool resolves with. Pure, so it
369
+ * works wherever the answer lands (your own route, a queue, a workflow signal). Questions become AskUserQuestion's
370
+ * `answers`, keyed by question text, several choices joined with ", " and the person's own words used as they are.
371
+ * Only the suggestions the person picked are kept, and none when the request offers no "always allow".
372
+ */
373
+ export function claudePermissionResult(request, answer) {
374
+ if (!answer.approved) {
375
+ return { behavior: "deny", message: answer.reason ? `The user declined: ${answer.reason}` : "The user declined this action." };
376
+ }
377
+ const input = (request.input && typeof request.input === "object" ? request.input : {});
378
+ let updatedInput = answer.updatedInput ?? input;
379
+ if (request.questions) {
380
+ const answers = {};
381
+ for (const q of request.questions) {
382
+ const a = answer.answers?.find((x) => x.questionId === q.id);
383
+ const words = [...(a?.choice ?? []).filter((c) => q.options.some((o) => o.id === c)), ...(a?.text?.trim() ? [a.text.trim()] : [])];
384
+ if (words.length)
385
+ answers[q.id] = words.join(", ");
386
+ }
387
+ updatedInput = { ...input, answers };
388
+ }
389
+ const kept = request.noAlwaysAllow ? [] : [...new Set(answer.alwaysAllow ?? [])].flatMap((i) => (request.suggestions?.[i] ? [request.suggestions[i]] : []));
390
+ return { behavior: "allow", updatedInput, ...(kept.length && { updatedPermissions: kept }) };
391
+ }
254
392
  const json = (v) => (typeof v === "string" ? v : JSON.stringify(v, null, 2));
255
393
  function reasoningEntry(id, text, streaming) {
256
394
  const first = text.trim().split(/\n|(?<=[.!?])\s/)[0] ?? "";
@@ -263,3 +401,23 @@ function reasoningEntry(id, text, streaming) {
263
401
  assumption: "The model’s own reasoning; nothing checked it.",
264
402
  };
265
403
  }
404
+ const scopes = { session: "session", localSettings: "local", projectSettings: "project", userSettings: "user", cliArg: "session" };
405
+ /**
406
+ * The SDK's suggested permission updates as plain parts PermissionPrompt shows, in the same order, so the indexes a
407
+ * person ticks are the ones `alwaysAllow` sends back.
408
+ */
409
+ export function claudeSuggestions(updates) {
410
+ return (updates ?? []).map((u) => {
411
+ const scope = scopes[u.destination] ?? u.destination;
412
+ if (u.type === "setMode")
413
+ return { action: "mode", what: u.mode, scope };
414
+ if (u.type === "addDirectories")
415
+ return { action: "directories", what: u.directories.join(", "), scope };
416
+ if (u.type === "removeDirectories")
417
+ return { action: "other", what: u.directories.join(", "), scope };
418
+ const what = u.rules.map((r) => (r.ruleContent ? `${r.toolName}(${r.ruleContent})` : r.toolName)).join(", ");
419
+ if (u.type === "removeRules")
420
+ return { action: "remove", what, scope };
421
+ return { action: u.behavior, what, scope };
422
+ });
423
+ }