@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.
- package/dist/adapters/ag-ui.d.ts +81 -16
- package/dist/adapters/ag-ui.js +308 -1
- package/dist/adapters/ai-sdk.d.ts +11 -6
- package/dist/adapters/ai-sdk.js +18 -1
- package/dist/adapters/claude-agent-sdk.d.ts +123 -14
- package/dist/adapters/claude-agent-sdk.js +163 -5
- package/dist/adapters/claude-agent-server.d.ts +199 -14
- package/dist/adapters/claude-agent-server.js +448 -49
- package/dist/adapters/use-ag-ui.d.ts +12 -6
- package/dist/adapters/use-ag-ui.js +23 -5
- package/dist/adapters/use-candor-chat.d.ts +1 -1
- package/dist/adapters/use-candor-chat.js +3 -3
- package/dist/adapters/use-claude-agent.d.ts +38 -9
- package/dist/adapters/use-claude-agent.js +96 -20
- package/dist/blocks/copy-field.d.ts +1 -1
- package/dist/blocks/copy-field.js +1 -1
- package/dist/blocks/degraded-panel.d.ts +5 -5
- package/dist/blocks/locale.d.ts +6 -6
- package/dist/blocks/locked.d.ts +8 -8
- package/dist/blocks/relative-time.d.ts +6 -6
- package/dist/blocks/status-dot.d.ts +3 -3
- package/dist/blocks/step-list.d.ts +3 -3
- package/dist/blocks/streaming-text.d.ts +3 -3
- package/dist/chat/agent-avatar.d.ts +4 -4
- package/dist/chat/ai-disclosure.d.ts +37 -0
- package/dist/chat/ai-disclosure.js +48 -0
- package/dist/chat/attachment-preview.d.ts +6 -6
- package/dist/chat/attachment-preview.js +2 -2
- package/dist/chat/card-registry.js +1 -1
- package/dist/chat/citation.d.ts +3 -3
- package/dist/chat/citation.js +6 -4
- package/dist/chat/composer.d.ts +31 -20
- package/dist/chat/composer.js +10 -6
- package/dist/chat/context-meter.d.ts +1 -1
- package/dist/chat/context-meter.js +1 -1
- package/dist/chat/entity-chip.d.ts +3 -3
- package/dist/chat/entity-chip.js +1 -1
- package/dist/chat/folded-turns.d.ts +11 -6
- package/dist/chat/folded-turns.js +2 -2
- package/dist/chat/message-actions.d.ts +6 -6
- package/dist/chat/message-actions.js +1 -1
- package/dist/chat/message-content.d.ts +13 -7
- package/dist/chat/message-content.js +10 -5
- package/dist/chat/message.d.ts +64 -37
- package/dist/chat/message.js +17 -11
- package/dist/chat/payload.d.ts +4 -4
- package/dist/chat/payload.js +5 -2
- package/dist/chat/permission-prompt.d.ts +129 -0
- package/dist/chat/permission-prompt.js +144 -0
- package/dist/chat/prompt-suggestions.d.ts +4 -4
- package/dist/chat/prompt-suggestions.js +1 -1
- package/dist/chat/reasoning.d.ts +3 -3
- package/dist/chat/reasoning.js +1 -1
- package/dist/chat/thread-divider.d.ts +3 -3
- package/dist/chat/thread-event.d.ts +10 -5
- package/dist/chat/thread-event.js +3 -2
- package/dist/chat/thread.d.ts +23 -7
- package/dist/chat/thread.js +29 -6
- package/dist/chat/tool-call.d.ts +7 -2
- package/dist/chat/tool-call.js +8 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/lib/chat.d.ts +53 -38
- package/dist/lib/know.d.ts +14 -12
- package/dist/lib/render-prop.d.ts +4 -4
- package/dist/lib/safe-markdown.d.ts +1 -1
- package/dist/lib/status.d.ts +21 -0
- package/dist/lib/status.js +17 -0
- package/dist/lib/use-shortcut.d.ts +1 -1
- package/dist/lib/use-virtual-list.d.ts +6 -6
- package/dist/styles.css +4 -1
- package/dist/ui/button.d.ts +1 -1
- package/dist/ui/button.js +1 -1
- 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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
+
}
|