@candor-ui/react 1.1.0 → 1.5.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 +66 -1
- package/dist/adapters/ag-ui.js +308 -1
- package/dist/adapters/ai-sdk.d.ts +6 -1
- package/dist/adapters/ai-sdk.js +18 -1
- package/dist/adapters/claude-agent-sdk.d.ts +111 -2
- package/dist/adapters/claude-agent-sdk.js +163 -5
- package/dist/adapters/claude-agent-server.d.ts +195 -10
- package/dist/adapters/claude-agent-server.js +448 -49
- package/dist/adapters/use-ag-ui.d.ts +6 -0
- package/dist/adapters/use-ag-ui.js +10 -4
- package/dist/adapters/use-candor-chat.js +2 -2
- package/dist/adapters/use-claude-agent.d.ts +35 -6
- package/dist/adapters/use-claude-agent.js +96 -20
- package/dist/chat/ai-disclosure.d.ts +37 -0
- package/dist/chat/ai-disclosure.js +48 -0
- package/dist/chat/attachment-preview.js +1 -1
- package/dist/chat/card-registry.js +1 -1
- package/dist/chat/citation.js +5 -3
- package/dist/chat/composer.d.ts +5 -2
- package/dist/chat/composer.js +4 -3
- package/dist/chat/entity-chip.js +1 -1
- package/dist/chat/folded-turns.js +1 -1
- package/dist/chat/message.js +1 -1
- 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/reasoning.js +1 -1
- package/dist/chat/thread.d.ts +7 -1
- package/dist/chat/thread.js +24 -3
- package/dist/chat/tool-call.d.ts +5 -0
- package/dist/chat/tool-call.js +7 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/lib/chat.d.ts +15 -0
- package/dist/lib/use-virtual-list.d.ts +39 -0
- package/dist/lib/use-virtual-list.js +124 -0
- package/package.json +1 -1
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/** PermissionPrompt's fixed words; override them under `permissionPrompt` in LocaleProvider's `strings`. */
|
|
3
|
+
export declare const permissionPromptStrings: {
|
|
4
|
+
label: string;
|
|
5
|
+
fallbackTitle: string;
|
|
6
|
+
why: string;
|
|
7
|
+
mode: string;
|
|
8
|
+
modeDefault: string;
|
|
9
|
+
modeAcceptEdits: string;
|
|
10
|
+
modePlan: string;
|
|
11
|
+
modeAuto: string;
|
|
12
|
+
modeDontAsk: string;
|
|
13
|
+
modeBypassPermissions: string;
|
|
14
|
+
blockedPath: string;
|
|
15
|
+
subagent: string;
|
|
16
|
+
server: string;
|
|
17
|
+
input: string;
|
|
18
|
+
edit: string;
|
|
19
|
+
editLabel: string;
|
|
20
|
+
editInvalid: string;
|
|
21
|
+
changed: string;
|
|
22
|
+
unchanged: string;
|
|
23
|
+
cancelEdit: string;
|
|
24
|
+
remember: string;
|
|
25
|
+
rememberHint: string;
|
|
26
|
+
ruleAllow: string;
|
|
27
|
+
ruleDeny: string;
|
|
28
|
+
ruleAsk: string;
|
|
29
|
+
ruleRemove: string;
|
|
30
|
+
setMode: string;
|
|
31
|
+
warnBypassPermissions: string;
|
|
32
|
+
warnAuto: string;
|
|
33
|
+
directories: string;
|
|
34
|
+
other: string;
|
|
35
|
+
scopeSession: string;
|
|
36
|
+
scopeLocal: string;
|
|
37
|
+
scopeProject: string;
|
|
38
|
+
scopeUser: string;
|
|
39
|
+
allow: string;
|
|
40
|
+
allowEdited: string;
|
|
41
|
+
deny: string;
|
|
42
|
+
answer: string;
|
|
43
|
+
otherOption: string;
|
|
44
|
+
otherLabel: string;
|
|
45
|
+
questions: string;
|
|
46
|
+
};
|
|
47
|
+
/** One "don't ask again" choice, in the plain shape PermissionPrompt shows. claudeSuggestions builds them from the SDK's. */
|
|
48
|
+
export type PermissionSuggestion = {
|
|
49
|
+
/** What the rule does. */
|
|
50
|
+
action: "allow" | "deny" | "ask" | "remove" | "mode" | "directories" | "other";
|
|
51
|
+
/** What it covers, as the source writes it: `Bash(npm test:*)`, a mode's name, directories. Shown as code. */
|
|
52
|
+
what: string;
|
|
53
|
+
/** Where it's kept: "session", "local" (this project, this computer), "project" (shared), "user", or the source's own word. */
|
|
54
|
+
scope?: string;
|
|
55
|
+
};
|
|
56
|
+
/** A question the agent asks through the prompt: pick one or more labels, or answer in its own words. */
|
|
57
|
+
export type PermissionQuestion = {
|
|
58
|
+
id: string;
|
|
59
|
+
prompt: string;
|
|
60
|
+
tag?: string;
|
|
61
|
+
options: {
|
|
62
|
+
id: string;
|
|
63
|
+
label: string;
|
|
64
|
+
detail?: string;
|
|
65
|
+
}[];
|
|
66
|
+
multiple: boolean;
|
|
67
|
+
other?: boolean;
|
|
68
|
+
};
|
|
69
|
+
/** A tool call waiting on a person, in the shape the adapters give (ClaudeApproval fits it). */
|
|
70
|
+
export type PermissionRequest = {
|
|
71
|
+
id: string;
|
|
72
|
+
tool: string;
|
|
73
|
+
input: unknown;
|
|
74
|
+
/** The source's own sentence for the request: "Claude wants to run npm test". */
|
|
75
|
+
title?: string;
|
|
76
|
+
description?: string;
|
|
77
|
+
/** Why it reached a person (no rule allowed it, a safety check, a path outside the project). */
|
|
78
|
+
reason?: string;
|
|
79
|
+
blockedPath?: string;
|
|
80
|
+
/** Put the focus on Deny: the request grants more than it looks. */
|
|
81
|
+
defaultToNo?: boolean;
|
|
82
|
+
/** Set when a subagent asked. */
|
|
83
|
+
agentId?: string;
|
|
84
|
+
mcpServer?: {
|
|
85
|
+
name: string;
|
|
86
|
+
source: string;
|
|
87
|
+
};
|
|
88
|
+
/** The questions, when the request is the agent asking rather than a tool to allow. */
|
|
89
|
+
questions?: PermissionQuestion[];
|
|
90
|
+
/** False when the server won't accept an edited input for this call: no Edit is offered, whatever `editable` says. */
|
|
91
|
+
editable?: boolean;
|
|
92
|
+
};
|
|
93
|
+
export type PermissionAnswer = {
|
|
94
|
+
/** The input as edited, when the person changed it. */
|
|
95
|
+
updatedInput?: Record<string, unknown>;
|
|
96
|
+
/** The suggestions the person ticked, by index. */
|
|
97
|
+
alwaysAllow?: number[];
|
|
98
|
+
/** Answers to `questions`. */
|
|
99
|
+
answers?: {
|
|
100
|
+
questionId: string;
|
|
101
|
+
choice?: string[];
|
|
102
|
+
text?: string;
|
|
103
|
+
}[];
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* A tool call waiting for a person's go-ahead: what it will do, why it reached them, and the permission mode the
|
|
107
|
+
* run is in. They can edit the input before allowing it and see exactly what they changed; Allow then sends that
|
|
108
|
+
* input, nothing else. "Don't ask again" rules are offered as boxes nobody has ticked, and are kept only if the
|
|
109
|
+
* person ticks them. When the agent is asking questions instead, it shows them as a short form. Words from the
|
|
110
|
+
* request (its title, reason, input) are shown as text, never markup.
|
|
111
|
+
*/
|
|
112
|
+
declare function PermissionPrompt({ request, mode, suggestions, editable, onAllow, onDeny, busy, className, }: {
|
|
113
|
+
/** The waiting request. */
|
|
114
|
+
request: PermissionRequest;
|
|
115
|
+
/** The run's permission mode, as the SDK names it ("default", "acceptEdits", "plan", "auto"…). Left out, it isn't shown. */
|
|
116
|
+
mode?: string;
|
|
117
|
+
/** "Don't ask again" rules to offer. None is ticked. */
|
|
118
|
+
suggestions?: PermissionSuggestion[];
|
|
119
|
+
/** Let the person edit the input as JSON before allowing it. */
|
|
120
|
+
editable?: boolean;
|
|
121
|
+
/** Called with the edited input, the ticked rules or the answers. */
|
|
122
|
+
onAllow: (answer: PermissionAnswer) => void;
|
|
123
|
+
/** Called when the person denies it (or won't answer the questions). */
|
|
124
|
+
onDeny: () => void;
|
|
125
|
+
/** The answer is being sent: both buttons wait. */
|
|
126
|
+
busy?: boolean;
|
|
127
|
+
className?: string;
|
|
128
|
+
}): React.JSX.Element;
|
|
129
|
+
export { PermissionPrompt };
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Info, Pencil, ShieldAlert, ShieldQuestion } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/utils.js";
|
|
6
|
+
import { useStrings } from "../blocks/locale.js";
|
|
7
|
+
import { Payload } from "./payload.js";
|
|
8
|
+
import { Button } from "../ui/button.js";
|
|
9
|
+
/** PermissionPrompt's fixed words; override them under `permissionPrompt` in LocaleProvider's `strings`. */
|
|
10
|
+
export const permissionPromptStrings = {
|
|
11
|
+
label: "Permission request: {title}",
|
|
12
|
+
fallbackTitle: "The agent wants to use {tool}",
|
|
13
|
+
why: "Why you’re asked",
|
|
14
|
+
mode: "Permission mode",
|
|
15
|
+
modeDefault: "Ask before acting",
|
|
16
|
+
modeAcceptEdits: "Accept file edits",
|
|
17
|
+
modePlan: "Plan only",
|
|
18
|
+
modeAuto: "Automatic, by a classifier",
|
|
19
|
+
modeDontAsk: "Don’t ask; deny what isn’t allowed",
|
|
20
|
+
modeBypassPermissions: "Allow everything",
|
|
21
|
+
blockedPath: "Path outside what’s allowed: {path}",
|
|
22
|
+
subagent: "A subagent asked.",
|
|
23
|
+
server: "From the tool server “{name}”, configured in {source}.",
|
|
24
|
+
input: "What it will run with",
|
|
25
|
+
edit: "Edit before allowing",
|
|
26
|
+
editLabel: "Input, as JSON",
|
|
27
|
+
editInvalid: "That isn’t a JSON object, so it can’t be used.",
|
|
28
|
+
changed: "You changed",
|
|
29
|
+
unchanged: "Nothing changed yet.",
|
|
30
|
+
cancelEdit: "Use the original",
|
|
31
|
+
remember: "Don’t ask again for",
|
|
32
|
+
rememberHint: "Optional. Nothing is remembered unless you tick it.",
|
|
33
|
+
ruleAllow: "Allow {what}",
|
|
34
|
+
ruleDeny: "Always deny {what}",
|
|
35
|
+
ruleAsk: "Always ask for {what}",
|
|
36
|
+
ruleRemove: "Stop applying the rule for {what}",
|
|
37
|
+
setMode: "Switch to: {mode}",
|
|
38
|
+
warnBypassPermissions: "After this, nothing is checked: the agent can run any command and change or delete any file it can reach.",
|
|
39
|
+
warnAuto: "After this, a classifier allows or blocks each action instead of you.",
|
|
40
|
+
directories: "Access to {what}",
|
|
41
|
+
other: "A permission change: {what}",
|
|
42
|
+
scopeSession: "for this session",
|
|
43
|
+
scopeLocal: "in this project, on this computer",
|
|
44
|
+
scopeProject: "in this project, for everyone on it",
|
|
45
|
+
scopeUser: "for you, in every project",
|
|
46
|
+
allow: "Allow",
|
|
47
|
+
allowEdited: "Allow as edited",
|
|
48
|
+
deny: "Deny",
|
|
49
|
+
answer: "Send answers",
|
|
50
|
+
otherOption: "Other",
|
|
51
|
+
otherLabel: "Your own answer to: {question}",
|
|
52
|
+
questions: "The agent has questions",
|
|
53
|
+
};
|
|
54
|
+
const modeKeys = {
|
|
55
|
+
default: "modeDefault",
|
|
56
|
+
acceptEdits: "modeAcceptEdits",
|
|
57
|
+
plan: "modePlan",
|
|
58
|
+
auto: "modeAuto",
|
|
59
|
+
dontAsk: "modeDontAsk",
|
|
60
|
+
bypassPermissions: "modeBypassPermissions",
|
|
61
|
+
};
|
|
62
|
+
const json = (v) => (typeof v === "string" ? v : JSON.stringify(v, null, 2));
|
|
63
|
+
/** Parses edited JSON into an object, or null when it isn't one. */
|
|
64
|
+
function parseObject(text) {
|
|
65
|
+
try {
|
|
66
|
+
const v = JSON.parse(text);
|
|
67
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A tool call waiting for a person's go-ahead: what it will do, why it reached them, and the permission mode the
|
|
75
|
+
* run is in. They can edit the input before allowing it and see exactly what they changed; Allow then sends that
|
|
76
|
+
* input, nothing else. "Don't ask again" rules are offered as boxes nobody has ticked, and are kept only if the
|
|
77
|
+
* person ticks them. When the agent is asking questions instead, it shows them as a short form. Words from the
|
|
78
|
+
* request (its title, reason, input) are shown as text, never markup.
|
|
79
|
+
*/
|
|
80
|
+
function PermissionPrompt({ request, mode, suggestions, editable = false, onAllow, onDeny, busy = false, className, }) {
|
|
81
|
+
// Keyed by the request, so an edit, a ticked rule or an answer never carries over to the next request.
|
|
82
|
+
return _jsx(PromptBody, { request, mode, suggestions, editable: editable && request.editable !== false, onAllow, onDeny, busy, className }, request.id);
|
|
83
|
+
}
|
|
84
|
+
function PromptBody({ request, mode, suggestions, editable, onAllow, onDeny, busy, className, }) {
|
|
85
|
+
const t = useStrings("permissionPrompt", permissionPromptStrings);
|
|
86
|
+
const id = React.useId();
|
|
87
|
+
const original = React.useMemo(() => json(request.input ?? {}), [request.input]);
|
|
88
|
+
const [editing, setEditing] = React.useState(false);
|
|
89
|
+
const [draft, setDraft] = React.useState(original);
|
|
90
|
+
const [ticked, setTicked] = React.useState([]);
|
|
91
|
+
const [answers, setAnswers] = React.useState({});
|
|
92
|
+
const edited = editing ? parseObject(draft) : null;
|
|
93
|
+
const proposed = parseObject(original);
|
|
94
|
+
const changedKeys = edited && proposed ? [...new Set([...Object.keys(proposed), ...Object.keys(edited)])].filter((k) => JSON.stringify(proposed[k]) !== JSON.stringify(edited[k])) : [];
|
|
95
|
+
const invalid = editing && !edited;
|
|
96
|
+
const title = request.title ?? t("fallbackTitle", { tool: request.tool });
|
|
97
|
+
const questions = request.questions;
|
|
98
|
+
// A request that grants more than it looks opens on Deny, so a stray Enter can't allow it.
|
|
99
|
+
const denyRef = React.useRef(null);
|
|
100
|
+
React.useEffect(() => {
|
|
101
|
+
if (request.defaultToNo)
|
|
102
|
+
denyRef.current?.focus();
|
|
103
|
+
}, [request.id, request.defaultToNo]);
|
|
104
|
+
const word = (s) => {
|
|
105
|
+
const key = { allow: "ruleAllow", deny: "ruleDeny", ask: "ruleAsk", remove: "ruleRemove", mode: "setMode", directories: "directories", other: "other" }[s.action];
|
|
106
|
+
const scope = s.scope && { session: "scopeSession", local: "scopeLocal", project: "scopeProject", user: "scopeUser" }[s.scope];
|
|
107
|
+
const what = s.action === "mode" && modeKeys[s.what] ? t(modeKeys[s.what]) : s.what;
|
|
108
|
+
return { text: t(key, { what, mode: what }), scope: scope ? t(scope) : s.scope };
|
|
109
|
+
};
|
|
110
|
+
const answered = (q) => {
|
|
111
|
+
const a = answers[q.id];
|
|
112
|
+
return Boolean(a && (a.choice.length || (a.other && a.text.trim())));
|
|
113
|
+
};
|
|
114
|
+
const choose = (q, option) => setAnswers((prev) => {
|
|
115
|
+
const a = prev[q.id] ?? { choice: [], text: "", other: false };
|
|
116
|
+
const choice = q.multiple ? (a.choice.includes(option) ? a.choice.filter((c) => c !== option) : [...a.choice, option]) : [option];
|
|
117
|
+
return { ...prev, [q.id]: { ...a, choice, other: q.multiple ? a.other : false } };
|
|
118
|
+
});
|
|
119
|
+
const toggleOther = (q) => setAnswers((prev) => {
|
|
120
|
+
const a = prev[q.id] ?? { choice: [], text: "", other: false };
|
|
121
|
+
return { ...prev, [q.id]: { ...a, other: !a.other, choice: q.multiple || a.other ? a.choice : [] } };
|
|
122
|
+
});
|
|
123
|
+
const allow = () => {
|
|
124
|
+
if (busy)
|
|
125
|
+
return;
|
|
126
|
+
if (questions) {
|
|
127
|
+
if (!questions.every(answered))
|
|
128
|
+
return;
|
|
129
|
+
onAllow({ answers: questions.map((q) => ({ questionId: q.id, choice: answers[q.id]?.choice, text: answers[q.id]?.other ? answers[q.id]?.text.trim() : undefined })) });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (invalid)
|
|
133
|
+
return;
|
|
134
|
+
onAllow({ ...(edited && changedKeys.length && { updatedInput: edited }), ...(ticked.length && { alwaysAllow: ticked }) });
|
|
135
|
+
};
|
|
136
|
+
return (_jsxs("section", { "data-slot": "permission-prompt", "aria-label": t("label", { title: questions ? t("questions") : title }), className: cn("overflow-hidden rounded-xl border border-warning/50 bg-card text-sm shadow-xs", className), children: [_jsxs("header", { className: "flex items-start gap-2.5 bg-warning-weak/50 px-3.5 py-2.5", children: [_jsx(ShieldQuestion, { "aria-hidden": true, className: "mt-0.5 size-4 shrink-0 text-warning-text" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h3", { className: "font-semibold leading-snug", children: questions ? t("questions") : title }), request.description && _jsx("p", { className: "mt-0.5 text-muted-foreground", children: request.description })] }), _jsx("code", { className: "shrink-0 font-mono text-2xs text-muted-foreground", children: request.tool })] }), _jsxs("dl", { className: "flex flex-col gap-1.5 px-3.5 pt-2.5 text-sm", children: [request.reason && (_jsxs("div", { className: "flex gap-2", children: [_jsx("dt", { className: "shrink-0 text-muted-foreground", children: t("why") }), _jsx("dd", { className: "min-w-0", children: request.reason })] })), mode && (_jsxs("div", { className: "flex gap-2", children: [_jsx("dt", { className: "shrink-0 text-muted-foreground", children: t("mode") }), _jsx("dd", { "data-slot": "permission-mode", children: modeKeys[mode] ? t(modeKeys[mode]) : _jsx("code", { className: "font-mono text-xs", children: mode }) })] }))] }), (request.blockedPath || request.agentId || request.mcpServer) && (_jsxs("ul", { className: "mt-1.5 flex flex-col gap-0.5 px-3.5 text-xs text-muted-foreground", children: [request.blockedPath && _jsx("li", { children: t("blockedPath", { path: request.blockedPath }) }), request.agentId && _jsx("li", { children: t("subagent") }), request.mcpServer && _jsx("li", { children: t("server", { name: request.mcpServer.name, source: request.mcpServer.source }) })] })), questions ? (_jsx("div", { className: "flex flex-col gap-4 px-3.5 py-3", children: questions.map((q) => {
|
|
137
|
+
const a = answers[q.id];
|
|
138
|
+
return (_jsxs("fieldset", { className: "flex flex-col gap-1.5", children: [_jsxs("legend", { className: "mb-1 font-medium", children: [q.tag && _jsx("span", { className: "me-1.5 rounded-xs bg-muted px-1 text-2xs font-medium text-muted-foreground", children: q.tag }), q.prompt] }), q.options.map((o) => (_jsxs("label", { className: "flex min-h-6 items-start gap-2", children: [_jsx("input", { type: q.multiple ? "checkbox" : "radio", name: `${id}-${q.id}`, checked: a?.choice.includes(o.id) ?? false, onChange: () => choose(q, o.id), className: "mt-0.5 size-4 accent-[var(--primary)]" }), _jsxs("span", { children: [o.label, o.detail && _jsx("span", { className: "block text-xs text-muted-foreground", children: o.detail })] })] }, o.id))), q.other && (_jsxs(_Fragment, { children: [_jsxs("label", { className: "flex min-h-6 items-start gap-2", children: [_jsx("input", { type: q.multiple ? "checkbox" : "radio", name: `${id}-${q.id}`, checked: a?.other ?? false, onChange: () => toggleOther(q), className: "mt-0.5 size-4 accent-[var(--primary)]" }), _jsx("span", { children: t("otherOption") })] }), a?.other && (_jsx("textarea", { "aria-label": t("otherLabel", { question: q.prompt }), rows: 2, value: a.text, onChange: (e) => setAnswers((prev) => ({ ...prev, [q.id]: { ...prev[q.id], text: e.target.value } })), className: "ms-6 block resize-none rounded-md border border-input bg-card px-2.5 py-1.5 text-sm outline-none [field-sizing:content] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/30" }))] }))] }, q.id));
|
|
139
|
+
}) })) : (_jsxs("div", { className: "flex flex-col gap-2 px-3.5 py-3", children: [editing ? (_jsxs(_Fragment, { children: [_jsx("label", { htmlFor: `${id}-edit`, className: "text-xs font-medium text-muted-foreground", children: t("editLabel") }), _jsx("textarea", { id: `${id}-edit`, value: draft, onChange: (e) => setDraft(e.target.value), spellCheck: false, rows: Math.min(12, draft.split("\n").length + 1), "aria-invalid": invalid || undefined, "aria-describedby": `${id}-changes`, className: "block w-full resize-y rounded-md border border-input bg-card px-2.5 py-1.5 font-mono text-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/30 aria-invalid:border-destructive" }), _jsx("div", { id: `${id}-changes`, role: "status", className: "text-xs", children: invalid ? (_jsx("p", { className: "text-destructive-text", children: t("editInvalid") })) : changedKeys.length ? (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("p", { className: "text-muted-foreground", children: t("changed") }), _jsx("ul", { className: "flex flex-col gap-1", children: changedKeys.map((k) => (_jsxs("li", { className: "grid gap-0.5 font-mono", children: [_jsx("span", { className: "text-muted-foreground", children: k }), k in proposed && _jsx("span", { className: "rounded-xs bg-destructive-weak/60 px-1 line-through", children: json(proposed[k]) }), k in edited && _jsx("span", { className: "rounded-xs bg-success-weak/70 px-1", children: json(edited[k]) })] }, k))) })] })) : (_jsx("p", { className: "text-muted-foreground", children: t("unchanged") })) }), _jsx(Button, { size: "sm", variant: "ghost", className: "self-start", onClick: () => (setEditing(false), setDraft(original)), children: t("cancelEdit") })] })) : (_jsxs(_Fragment, { children: [_jsx(Payload, { language: "json", title: t("input"), text: original }), editable && proposed && (_jsxs(Button, { size: "sm", variant: "outline", className: "self-start", onClick: () => setEditing(true), children: [_jsx(Pencil, { "aria-hidden": true }), " ", t("edit")] }))] })), suggestions && suggestions.length > 0 && (_jsxs("fieldset", { className: "mt-1 flex flex-col gap-1 rounded-lg border border-dashed px-3 py-2", children: [_jsx("legend", { className: "px-1 text-xs font-medium", children: t("remember") }), _jsxs("p", { className: "flex items-center gap-1 text-xs text-muted-foreground", children: [_jsx(Info, { "aria-hidden": true, className: "size-3" }), " ", t("rememberHint")] }), suggestions.map((s, i) => {
|
|
140
|
+
const w = word(s);
|
|
141
|
+
return (_jsxs("label", { className: "flex min-h-6 items-start gap-2", children: [_jsx("input", { type: "checkbox", checked: ticked.includes(i), onChange: (e) => setTicked((prev) => (e.target.checked ? [...prev, i] : prev.filter((x) => x !== i))), className: "mt-0.5 size-4 accent-[var(--primary)]" }), _jsxs("span", { children: [_jsx("span", { className: "font-mono text-xs", children: w.text }), w.scope && _jsxs("span", { className: "text-muted-foreground", children: [" \u00B7 ", w.scope] }), s.action === "mode" && (s.what === "bypassPermissions" || s.what === "auto") && (_jsxs("span", { className: "mt-0.5 flex items-start gap-1 text-xs text-warning-text", children: [_jsx(ShieldAlert, { "aria-hidden": true, className: "mt-px size-3.5 shrink-0" }), t(s.what === "auto" ? "warnAuto" : "warnBypassPermissions")] }))] })] }, i));
|
|
142
|
+
})] }))] })), _jsxs("footer", { className: "flex flex-wrap justify-end gap-2 border-t bg-sunken px-3.5 py-2", children: [_jsx(Button, { ref: denyRef, size: "sm", variant: "outline", onClick: onDeny, disabled: busy, children: t("deny") }), _jsx(Button, { size: "sm", onClick: allow, disabled: busy || invalid || (questions ? !questions.every(answered) : false), "aria-busy": busy, children: questions ? t("answer") : edited && changedKeys.length ? t("allowEdited") : t("allow") })] })] }));
|
|
143
|
+
}
|
|
144
|
+
export { PermissionPrompt };
|
package/dist/chat/reasoning.js
CHANGED
|
@@ -35,6 +35,6 @@ function Reasoning({ entries, thinking = false, thoughtFor, className, }) {
|
|
|
35
35
|
React.useEffect(() => setOpen(thinking), [thinking]);
|
|
36
36
|
const assumed = entries.filter((e) => e.basis === "assumed").length;
|
|
37
37
|
const summary = thinking ? null : (_jsxs(_Fragment, { children: [t("reasoning"), thoughtFor && _jsxs(_Fragment, { children: [" \u00B7 ", thoughtFor] }), " \u00B7 ", t("steps", { count: entries.length }), assumed > 0 && _jsxs("span", { className: "text-warning-text", children: [" \u00B7 ", t("assumedCount", { count: assumed })] })] }));
|
|
38
|
-
return (_jsxs(Collapsible, { open: open, onOpenChange: setOpen, "data-slot": "reasoning", className: cn("group/reasoning", className), children: [_jsxs(CollapsibleTrigger, { className: "inline-flex items-center gap-1.5 rounded-md py-0.5 pe-1.5 text-xs text-muted-foreground outline-none transition-colors duration-150 hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/40", children: [_jsx(Brain, { "aria-hidden": true, className: cn("size-3.5", thinking && "text-accent-text") }), thinking ? _jsx(ShimmerLabel, { className: "text-xs", children: t("thinking") }) : summary, _jsx(ChevronRight, { "aria-hidden": true, className: "rtl:-scale-x-100 size-3 transition-transform duration-200 ease-standard group-data-[state=open]/reasoning:[transform:rotate(90deg)]" })] }), _jsx(CollapsibleContent, { children: _jsx("ol", { className: "mt-2 ms-1.5 flex flex-col gap-3 border-s ps-4", children: entries.map((e) => (_jsxs("li", { className: "relative animate-rise-in", children: [_jsx("span", { "aria-hidden": true, className: "absolute top-1.5 -start-[1.2rem] size-1.5 rounded-full bg-border-strong" }), _jsxs("div", { className: "flex flex-wrap items-baseline gap-x-2 gap-y-1", children: [_jsx("p", { className: "text-sm font-medium", children: e.title }), _jsx("span", { className: cn("rounded-xs px-1 text-2xs font-medium", basisTone[e.basis]), children: t(e.basis) })] }), e.body && _jsx("p", { className: "mt-0.5 text-sm text-muted-foreground", children: e.body }), _jsx("p", { className: "mt-1 text-xs text-faint-foreground", children: e.basis === "verified" ? _jsxs(_Fragment, { children: [t("evidence"), " ", e.evidence] }) : _jsxs(_Fragment, { children: [t("assumption"), " ", e.assumption] }) })] }, e.id))) }) })] }));
|
|
38
|
+
return (_jsxs(Collapsible, { open: open, onOpenChange: setOpen, "data-slot": "reasoning", className: cn("group/reasoning", className), children: [_jsxs(CollapsibleTrigger, { className: "relative after:absolute after:inset-x-0 after:-inset-y-0.5 inline-flex items-center gap-1.5 rounded-md py-0.5 pe-1.5 text-xs text-muted-foreground outline-none transition-colors duration-150 hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/40", children: [_jsx(Brain, { "aria-hidden": true, className: cn("size-3.5", thinking && "text-accent-text") }), thinking ? _jsx(ShimmerLabel, { className: "text-xs", children: t("thinking") }) : summary, _jsx(ChevronRight, { "aria-hidden": true, className: "rtl:-scale-x-100 size-3 transition-transform duration-200 ease-standard group-data-[state=open]/reasoning:[transform:rotate(90deg)]" })] }), _jsx(CollapsibleContent, { children: _jsx("ol", { className: "mt-2 ms-1.5 flex flex-col gap-3 border-s ps-4", children: entries.map((e) => (_jsxs("li", { className: "relative animate-rise-in", children: [_jsx("span", { "aria-hidden": true, className: "absolute top-1.5 -start-[1.2rem] size-1.5 rounded-full bg-border-strong" }), _jsxs("div", { className: "flex flex-wrap items-baseline gap-x-2 gap-y-1", children: [_jsx("p", { className: "text-sm font-medium", children: e.title }), _jsx("span", { className: cn("rounded-xs px-1 text-2xs font-medium", basisTone[e.basis]), children: t(e.basis) })] }), e.body && _jsx("p", { className: "mt-0.5 text-sm text-muted-foreground", children: e.body }), _jsx("p", { className: "mt-1 text-xs text-faint-foreground", children: e.basis === "verified" ? _jsxs(_Fragment, { children: [t("evidence"), " ", e.evidence] }) : _jsxs(_Fragment, { children: [t("assumption"), " ", e.assumption] }) })] }, e.id))) }) })] }));
|
|
39
39
|
}
|
|
40
40
|
export { Reasoning };
|
package/dist/chat/thread.d.ts
CHANGED
|
@@ -9,8 +9,12 @@ export declare const threadStrings: {
|
|
|
9
9
|
* bottom and stops following the moment you scroll up to read, offering a
|
|
10
10
|
* "Jump to latest" button instead. Pass `empty` for the first-run state and
|
|
11
11
|
* the composer as `footer`.
|
|
12
|
+
*
|
|
13
|
+
* For long histories, `virtualize` renders only the messages in or near view
|
|
14
|
+
* (each child is one item, keyed by its `key`). The column stays a `log`, the
|
|
15
|
+
* message holding focus stays rendered, and Jump to latest works the same.
|
|
12
16
|
*/
|
|
13
|
-
declare function Thread({ children, empty, footer, className, contentClassName, }: {
|
|
17
|
+
declare function Thread({ children, empty, footer, className, contentClassName, virtualize, }: {
|
|
14
18
|
children?: React.ReactNode;
|
|
15
19
|
/** Shown before the first message. */
|
|
16
20
|
empty?: React.ReactNode;
|
|
@@ -19,5 +23,7 @@ declare function Thread({ children, empty, footer, className, contentClassName,
|
|
|
19
23
|
className?: string;
|
|
20
24
|
/** Classes for the message column. */
|
|
21
25
|
contentClassName?: string;
|
|
26
|
+
/** Render only the messages in or near view, for histories of hundreds of messages. Give each child a `key`. Defaults to false. */
|
|
27
|
+
virtualize?: boolean;
|
|
22
28
|
}): React.JSX.Element;
|
|
23
29
|
export { Thread };
|
package/dist/chat/thread.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { ArrowDown } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/utils.js";
|
|
6
6
|
import { useStrings } from "../blocks/locale.js";
|
|
7
|
+
import { useVirtualList } from "../lib/use-virtual-list.js";
|
|
7
8
|
/** Thread's fixed words; override them under `thread` in LocaleProvider's `strings`. */
|
|
8
9
|
export const threadStrings = {
|
|
9
10
|
messages: "Messages",
|
|
@@ -14,8 +15,12 @@ export const threadStrings = {
|
|
|
14
15
|
* bottom and stops following the moment you scroll up to read, offering a
|
|
15
16
|
* "Jump to latest" button instead. Pass `empty` for the first-run state and
|
|
16
17
|
* the composer as `footer`.
|
|
18
|
+
*
|
|
19
|
+
* For long histories, `virtualize` renders only the messages in or near view
|
|
20
|
+
* (each child is one item, keyed by its `key`). The column stays a `log`, the
|
|
21
|
+
* message holding focus stays rendered, and Jump to latest works the same.
|
|
17
22
|
*/
|
|
18
|
-
function Thread({ children, empty, footer, className, contentClassName, }) {
|
|
23
|
+
function Thread({ children, empty, footer, className, contentClassName, virtualize = false, }) {
|
|
19
24
|
const t = useStrings("thread", threadStrings);
|
|
20
25
|
const scroller = React.useRef(null);
|
|
21
26
|
const content = React.useRef(null);
|
|
@@ -58,7 +63,23 @@ function Thread({ children, empty, footer, className, contentClassName, }) {
|
|
|
58
63
|
lastTop.current = top;
|
|
59
64
|
};
|
|
60
65
|
const hasMessages = React.Children.count(children) > 0;
|
|
61
|
-
|
|
66
|
+
const items = virtualize ? React.Children.toArray(children) : [];
|
|
67
|
+
const keyOf = (i) => (React.isValidElement(items[i]) ? (items[i].key ?? i) : i);
|
|
68
|
+
// The message holding focus stays rendered, so focus isn't lost when it scrolls out of view.
|
|
69
|
+
const [focused, setFocused] = React.useState(null);
|
|
70
|
+
const list = useVirtualList({
|
|
71
|
+
enabled: virtualize,
|
|
72
|
+
count: items.length,
|
|
73
|
+
keyOf,
|
|
74
|
+
scroller,
|
|
75
|
+
pin: focused,
|
|
76
|
+
gap: () => (content.current ? Number.parseFloat(getComputedStyle(content.current).rowGap) || 0 : 0),
|
|
77
|
+
});
|
|
78
|
+
return (_jsxs("div", { "data-slot": "thread", className: cn("flex min-h-0 flex-col", className), children: [_jsxs("div", { className: "relative min-h-0 flex-1", children: [_jsx("div", { ref: scroller, onScroll: onScroll, onFocus: virtualize
|
|
79
|
+
? (e) => {
|
|
80
|
+
setFocused(e.target.closest("[data-virtual-key]")?.getAttribute("data-virtual-key") ?? null);
|
|
81
|
+
}
|
|
82
|
+
: undefined, onBlur: virtualize ? (e) => !e.currentTarget.contains(e.relatedTarget) && setFocused(null) : undefined, className: "h-full scroll-pb-14 overflow-y-auto overscroll-contain", children: _jsx("div", { ref: content, role: "log", "aria-label": t("messages"), className: cn("mx-auto flex w-full max-w-(--cd-thread-width) flex-col gap-7 px-4 py-6", contentClassName), children: !hasMessages ? (empty) : virtualize ? (_jsx(_Fragment, { children: list.rows.map((row) => "spacer" in row ? (_jsx("div", { "aria-hidden": true, style: { height: row.spacer } }, row.key)) : (_jsx("div", { ref: list.measure, "data-virtual-key": String(keyOf(row.index)), className: "flex flex-col", children: items[row.index] }, keyOf(row.index)))) })) : (children) }) }), !pinned && hasMessages && (_jsxs("button", { type: "button", onClick: () => {
|
|
62
83
|
pinnedRef.current = true;
|
|
63
84
|
setPinned(true);
|
|
64
85
|
toBottom("smooth");
|
package/dist/chat/tool-call.d.ts
CHANGED
|
@@ -11,6 +11,11 @@ export declare const toolCallStrings: {
|
|
|
11
11
|
done: string;
|
|
12
12
|
failed: string;
|
|
13
13
|
working: string;
|
|
14
|
+
approvedAutomatically: string;
|
|
15
|
+
approvedByPerson: string;
|
|
16
|
+
editedBeforeRun: string;
|
|
17
|
+
editedNote: string;
|
|
18
|
+
proposed: string;
|
|
14
19
|
};
|
|
15
20
|
/**
|
|
16
21
|
* One tool call as a compact row: what it did in plain words, its status, and
|
package/dist/chat/tool-call.js
CHANGED
|
@@ -17,6 +17,11 @@ export const toolCallStrings = {
|
|
|
17
17
|
done: "{count} done",
|
|
18
18
|
failed: "{count} failed",
|
|
19
19
|
working: "working",
|
|
20
|
+
approvedAutomatically: "Approved automatically",
|
|
21
|
+
approvedByPerson: "Approved by a person",
|
|
22
|
+
editedBeforeRun: "Edited before it ran",
|
|
23
|
+
editedNote: "A person changed the input before allowing it. Input is what ran.",
|
|
24
|
+
proposed: "As the agent proposed it",
|
|
20
25
|
};
|
|
21
26
|
/**
|
|
22
27
|
* One tool call as a compact row: what it did in plain words, its status, and
|
|
@@ -28,8 +33,8 @@ function ToolCall({ call, className, }) {
|
|
|
28
33
|
const statusLabel = useStatusLabel();
|
|
29
34
|
const failed = call.status === "failed";
|
|
30
35
|
const [open, setOpen] = React.useState(failed);
|
|
31
|
-
const hasDetail = Boolean(call.input || call.output || call.error);
|
|
32
|
-
return (_jsxs(Collapsible, { open: open, onOpenChange: setOpen, "data-slot": "tool-call", "data-status": call.status, className: cn("group/tool", className), children: [_jsxs(CollapsibleTrigger, { disabled: !hasDetail, className: "flex w-full items-center gap-2.5 rounded-md px-1.5 py-1 text-start outline-none transition-colors duration-150 enabled:hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/40", children: [_jsx(StepMark, { status: call.status }), _jsxs("span", { className: "min-w-0 flex-1 truncate text-sm", children: [call.status === "running" ? _jsx(ShimmerLabel, { className: "font-normal", children: call.title }) : call.title, _jsxs("span", { className: "sr-only", children: [" \u2014 ", statusLabel(call.status)] })] }), _jsx("code", { className: "hidden shrink-0 font-mono text-2xs text-faint-foreground sm:inline", children: call.tool }), call.duration && _jsx("span", { className: "shrink-0 text-xs tabular-nums text-faint-foreground", children: call.duration }), hasDetail && (_jsx(ChevronRight, { "aria-hidden": true, className: "rtl:-scale-x-100 size-3.5 shrink-0 text-faint-foreground transition-transform duration-200 ease-standard group-data-[state=open]/tool:[transform:rotate(90deg)]" }))] }), _jsx(CollapsibleContent, { className: "data-[state=open]:animate-in data-[state=open]:fade-in-0", children: _jsxs("div", { className: "mt-1 mb-2 ms-[1.375rem] flex flex-col gap-2 border-s ps-3.5", children: [call.error && (_jsx("p", { className: "rounded-md bg-destructive-weak px-2.5 py-1.5 text-xs text-destructive-text", children: call.error })), call.input && _jsx(Payload, { language: "json", title: t("input"), text: call.input }), call.output && _jsx(Payload, { language: call.outputLanguage ?? "text", title: t("output"), text: call.output })] }) })] }));
|
|
36
|
+
const hasDetail = Boolean(call.input || call.output || call.error || call.approval?.note || call.approval?.originalInput);
|
|
37
|
+
return (_jsxs(Collapsible, { open: open, onOpenChange: setOpen, "data-slot": "tool-call", "data-status": call.status, className: cn("group/tool", className), children: [_jsxs(CollapsibleTrigger, { disabled: !hasDetail, className: "flex w-full items-center gap-2.5 rounded-md px-1.5 py-1 text-start outline-none transition-colors duration-150 enabled:hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/40", children: [_jsx(StepMark, { status: call.status }), _jsxs("span", { className: "min-w-0 flex-1 truncate text-sm", children: [call.status === "running" ? _jsx(ShimmerLabel, { className: "font-normal", children: call.title }) : call.title, _jsxs("span", { className: "sr-only", children: [" \u2014 ", statusLabel(call.status)] })] }), call.approval?.originalInput !== undefined && (_jsx("span", { "data-slot": "tool-call-edited", className: "shrink-0 rounded-xs bg-warning-weak px-1 text-2xs text-warning-text", children: t("editedBeforeRun") })), call.approval?.by === "automatic" && (_jsx("span", { "data-slot": "tool-call-approval", className: "shrink-0 rounded-xs bg-muted px-1 text-2xs text-muted-foreground", children: t("approvedAutomatically") })), _jsx("code", { className: "hidden shrink-0 font-mono text-2xs text-faint-foreground sm:inline", children: call.tool }), call.duration && _jsx("span", { className: "shrink-0 text-xs tabular-nums text-faint-foreground", children: call.duration }), hasDetail && (_jsx(ChevronRight, { "aria-hidden": true, className: "rtl:-scale-x-100 size-3.5 shrink-0 text-faint-foreground transition-transform duration-200 ease-standard group-data-[state=open]/tool:[transform:rotate(90deg)]" }))] }), _jsx(CollapsibleContent, { className: "data-[state=open]:animate-in data-[state=open]:fade-in-0", children: _jsxs("div", { className: "mt-1 mb-2 ms-[1.375rem] flex flex-col gap-2 border-s ps-3.5", children: [call.error && (_jsx("p", { className: "rounded-md bg-destructive-weak px-2.5 py-1.5 text-xs text-destructive-text", children: call.error })), call.approval?.note && (_jsxs("p", { className: "text-xs text-muted-foreground", children: [call.approval.by === "automatic" ? t("approvedAutomatically") : t("approvedByPerson"), ": ", call.approval.note] })), call.approval?.originalInput !== undefined && _jsx("p", { className: "text-xs text-muted-foreground", children: t("editedNote") }), call.input && _jsx(Payload, { language: "json", title: t("input"), text: call.input }), call.approval?.originalInput !== undefined && _jsx(Payload, { language: "json", title: t("proposed"), text: call.approval.originalInput }), call.output && _jsx(Payload, { language: call.outputLanguage ?? "text", title: t("output"), text: call.output })] }) })] }));
|
|
33
38
|
}
|
|
34
39
|
/** A run of tool calls, with a count when it's long enough to fold. */
|
|
35
40
|
function ToolCallGroup({ calls, className, }) {
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export * from "./lib/chat.js";
|
|
|
2
2
|
export * from "./lib/status.js";
|
|
3
3
|
export * from "./lib/use-copy.js";
|
|
4
4
|
export * from "./lib/use-shortcut.js";
|
|
5
|
+
export * from "./lib/use-virtual-list.js";
|
|
5
6
|
export * from "./blocks/terms.js";
|
|
6
7
|
export * from "./blocks/locale.js";
|
|
7
8
|
export * from "./blocks/locked.js";
|
|
@@ -39,6 +40,8 @@ export * from "./ui/popover.js";
|
|
|
39
40
|
export * from "./ui/skeleton.js";
|
|
40
41
|
export * from "./lib/know.js";
|
|
41
42
|
export * from "./lib/render-prop.js";
|
|
43
|
+
export * from "./chat/ai-disclosure.js";
|
|
44
|
+
export * from "./chat/permission-prompt.js";
|
|
42
45
|
export * from "./lib/utils.js";
|
|
43
46
|
export { ToolCall } from "./chat/tool-call.js";
|
|
44
47
|
export type { ToolCall as ToolCallData } from "./lib/chat.js";
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export * from "./lib/chat.js";
|
|
|
2
2
|
export * from "./lib/status.js";
|
|
3
3
|
export * from "./lib/use-copy.js";
|
|
4
4
|
export * from "./lib/use-shortcut.js";
|
|
5
|
+
export * from "./lib/use-virtual-list.js";
|
|
5
6
|
export * from "./blocks/terms.js";
|
|
6
7
|
export * from "./blocks/locale.js";
|
|
7
8
|
export * from "./blocks/locked.js";
|
|
@@ -39,6 +40,8 @@ export * from "./ui/popover.js";
|
|
|
39
40
|
export * from "./ui/skeleton.js";
|
|
40
41
|
export * from "./lib/know.js";
|
|
41
42
|
export * from "./lib/render-prop.js";
|
|
43
|
+
export * from "./chat/ai-disclosure.js";
|
|
44
|
+
export * from "./chat/permission-prompt.js";
|
|
42
45
|
export * from "./lib/utils.js";
|
|
43
46
|
export { ToolCall } from "./chat/tool-call.js";
|
|
44
47
|
export { Message } from "./chat/message.js";
|
package/dist/lib/chat.d.ts
CHANGED
|
@@ -108,6 +108,11 @@ export type MessageSource = {
|
|
|
108
108
|
origin?: string;
|
|
109
109
|
provisional?: boolean;
|
|
110
110
|
};
|
|
111
|
+
/** A source a tool's result supports, without the id and [n] an adapter assigns. */
|
|
112
|
+
export type ToolSource = Omit<MessageSource, "id" | "marker"> & {
|
|
113
|
+
/** Makes two results naming the same thing one source; defaults to `kind` and `label`. */
|
|
114
|
+
key?: string;
|
|
115
|
+
};
|
|
111
116
|
type ReasoningBase = {
|
|
112
117
|
id: string;
|
|
113
118
|
title: string;
|
|
@@ -136,6 +141,16 @@ export type ToolCall = {
|
|
|
136
141
|
duration?: string;
|
|
137
142
|
/** Required when status is "failed": what went wrong, in a sentence. */
|
|
138
143
|
error?: string;
|
|
144
|
+
/**
|
|
145
|
+
* Who let it run, when it needed a go-ahead: a person, or something automatic (a policy, a classifier, a rule).
|
|
146
|
+
* `note` is why, in the source's words. Left out when it needed none or the source doesn't say.
|
|
147
|
+
*/
|
|
148
|
+
approval?: {
|
|
149
|
+
by: "person" | "automatic";
|
|
150
|
+
note?: string;
|
|
151
|
+
/** The input as the agent proposed it, when a person edited it before it ran; `input` is then what ran. */
|
|
152
|
+
originalInput?: string;
|
|
153
|
+
};
|
|
139
154
|
};
|
|
140
155
|
/** The block's language, e.g. "json", "sql", "python", "text". Shown as a label; never executed or highlighted as HTML. */
|
|
141
156
|
export type PayloadLanguage = string;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/** What to render, in order: an item by its index, or a spacer standing in for the items between. */
|
|
3
|
+
export type VirtualRow = {
|
|
4
|
+
index: number;
|
|
5
|
+
} | {
|
|
6
|
+
spacer: number;
|
|
7
|
+
key: string;
|
|
8
|
+
};
|
|
9
|
+
export type VirtualList = {
|
|
10
|
+
/** The items to render and the spacers between them, in order. */
|
|
11
|
+
rows: VirtualRow[];
|
|
12
|
+
/** The ref for every rendered item, which also carries `data-virtual-key={key}`, so its real height is measured and kept. */
|
|
13
|
+
measure: (el: HTMLElement | null) => void | (() => void);
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Render only the items of a long list that are in or near view, for Thread's and SessionList's `virtualize`.
|
|
17
|
+
* Items keep their measured heights by key, so a list that grows or reorders keeps its place; unmeasured items count
|
|
18
|
+
* as `estimate`. Spacers stand in for the rest. `pin` (an item's key) keeps that item rendered (the one holding
|
|
19
|
+
* focus), on its own if it's far from view, so focus is never lost to an item scrolling away. Until the scroller is
|
|
20
|
+
* measured (on the server, on the first render in the browser, and while it's hidden) the first or last `initial`
|
|
21
|
+
* items render, by `anchor`, so server and browser agree.
|
|
22
|
+
*
|
|
23
|
+
* Give each rendered item `ref={list.measure}` and `data-virtual-key={key}`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function useVirtualList({ enabled, count, keyOf, scroller, estimate, overscan, initial, anchor, gap, pin, }: {
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
count: number;
|
|
28
|
+
keyOf: (index: number) => React.Key;
|
|
29
|
+
scroller: React.RefObject<HTMLElement | null>;
|
|
30
|
+
estimate?: number;
|
|
31
|
+
overscan?: number;
|
|
32
|
+
initial?: number;
|
|
33
|
+
/** Which end to show before the scroller is measured: "end" for a thread (newest last), "start" for a list. Defaults to "end". */
|
|
34
|
+
anchor?: "start" | "end";
|
|
35
|
+
/** The gap between items, in pixels, read once the scroller is measured (e.g. from the list's computed `row-gap`). */
|
|
36
|
+
gap?: () => number;
|
|
37
|
+
/** The key of an item to keep rendered, such as the one holding focus. */
|
|
38
|
+
pin?: string | null;
|
|
39
|
+
}): VirtualList;
|