@candor-ui/react 1.7.0 → 1.9.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/claude-agent-sdk.d.ts +17 -0
- package/dist/adapters/claude-agent-sdk.js +46 -2
- package/dist/blocks/copy-field.d.ts +2 -1
- package/dist/blocks/copy-field.js +3 -2
- package/dist/blocks/locked.d.ts +4 -1
- package/dist/blocks/locked.js +24 -2
- package/dist/blocks/status-dot.d.ts +5 -2
- package/dist/blocks/status-dot.js +23 -2
- package/dist/blocks/step-list.d.ts +6 -1
- package/dist/blocks/step-list.js +45 -11
- package/dist/chat/composer.d.ts +9 -3
- package/dist/chat/composer.js +55 -3
- package/dist/chat/context-meter.d.ts +5 -2
- package/dist/chat/context-meter.js +8 -2
- package/dist/chat/message-actions.d.ts +4 -2
- package/dist/chat/message-actions.js +5 -3
- package/dist/chat/permission-prompt.d.ts +6 -1
- package/dist/chat/permission-prompt.js +31 -1
- package/dist/chat/thread.d.ts +4 -0
- package/dist/chat/thread.js +32 -7
- package/dist/chat/tool-call.d.ts +2 -1
- package/dist/chat/tool-call.js +3 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/mcp-input.d.ts +316 -0
- package/dist/lib/mcp-input.js +369 -0
- package/dist/styles.css +94 -0
- package/dist/ui/collapsible.d.ts +5 -1
- package/dist/ui/collapsible.js +7 -2
- package/package.json +1 -1
|
@@ -144,6 +144,21 @@ export type FromClaudeAgentOptions = {
|
|
|
144
144
|
* Claude's own citations and web search results are read without it.
|
|
145
145
|
*/
|
|
146
146
|
sourcesFrom?: ((tool: string, input: unknown, output: string) => ToolSource[] | undefined) | undefined;
|
|
147
|
+
/**
|
|
148
|
+
* The words for lines the adapter adds to the thread itself, as system messages: when the SDK summarised earlier
|
|
149
|
+
* messages to make room (`compacted`, given how it was started), and when the conversation was cleared (`cleared`).
|
|
150
|
+
* Defaults are in English; pass your own for other languages.
|
|
151
|
+
*/
|
|
152
|
+
eventText?: {
|
|
153
|
+
compacted?: ((trigger: "manual" | "auto") => string) | undefined;
|
|
154
|
+
cleared?: string | undefined;
|
|
155
|
+
} | undefined;
|
|
156
|
+
};
|
|
157
|
+
/** A memory the SDK recalled for a reply: the file (or URL) it came from, and whose memory it is. */
|
|
158
|
+
export type ClaudeRecalledMemory = {
|
|
159
|
+
path: string;
|
|
160
|
+
scope: "personal" | "team" | "organization";
|
|
161
|
+
content?: string | undefined;
|
|
147
162
|
};
|
|
148
163
|
export type ClaudeAgentState = {
|
|
149
164
|
messages: Message[];
|
|
@@ -156,6 +171,8 @@ export type ClaudeAgentState = {
|
|
|
156
171
|
notice?: string | undefined;
|
|
157
172
|
/** The permission mode last reported, by the SDK or the app. Absent until one is. */
|
|
158
173
|
permissionMode?: ClaudePermissionMode | undefined;
|
|
174
|
+
/** Memories the SDK recalled for each reply, by the reply's message id, for a `MemoryUsed` line under it. Only replies that recalled any. */
|
|
175
|
+
recalled?: Record<string, ClaudeRecalledMemory[]> | undefined;
|
|
159
176
|
};
|
|
160
177
|
/**
|
|
161
178
|
* Turns Claude Agent SDK events into Candor messages, waiting approvals and usage.
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { markdownBlocks, plainBlocks } from "../lib/safe-markdown.js";
|
|
2
|
+
const compactedText = (trigger) => trigger === "auto"
|
|
3
|
+
? "Earlier messages were summarised to make room. The agent works from that summary now; the messages above are unchanged."
|
|
4
|
+
: "Earlier messages were summarised, as asked. The agent works from that summary now; the messages above are unchanged.";
|
|
5
|
+
const clearedText = "The conversation was cleared. The agent doesn’t see the messages above.";
|
|
2
6
|
/**
|
|
3
7
|
* Turns Claude Agent SDK events into Candor messages, waiting approvals and usage.
|
|
4
8
|
* Pure, so it can run on every render; useClaudeAgent calls it for you.
|
|
@@ -14,11 +18,35 @@ export function fromClaudeAgentEvents(events, options) {
|
|
|
14
18
|
let lastCost = 0;
|
|
15
19
|
let notice;
|
|
16
20
|
let permissionMode;
|
|
17
|
-
const
|
|
21
|
+
const recalled = {};
|
|
22
|
+
// Memories recalled before the reply they're for has started.
|
|
23
|
+
let recalling = [];
|
|
24
|
+
const current = (id) => {
|
|
25
|
+
if (!reply) {
|
|
26
|
+
reply = { id, parts: [], reasoning: [], tools: new Map(), live: new Map(), sources: new Map(), inputs: new Map(), memories: recalling };
|
|
27
|
+
recalling = [];
|
|
28
|
+
}
|
|
29
|
+
return reply;
|
|
30
|
+
};
|
|
31
|
+
// Lines that arrived while a reply was still going: shown after it, so one turn stays one reply.
|
|
32
|
+
let afterReply = [];
|
|
18
33
|
const close = (last) => {
|
|
19
|
-
if (reply)
|
|
34
|
+
if (reply) {
|
|
20
35
|
messages.push(render(reply, { ...options, model, last }));
|
|
36
|
+
if (reply.memories.length)
|
|
37
|
+
recalled[reply.id] = reply.memories;
|
|
38
|
+
}
|
|
21
39
|
reply = null;
|
|
40
|
+
messages.push(...afterReply);
|
|
41
|
+
afterReply = [];
|
|
42
|
+
};
|
|
43
|
+
// A line the adapter adds between replies: the app's own words, drawn like any system message.
|
|
44
|
+
const systemLine = (id, text) => {
|
|
45
|
+
const line = { id, author: { role: "system" }, status: "complete", blocks: plainBlocks(text) };
|
|
46
|
+
if (reply)
|
|
47
|
+
afterReply.push(line);
|
|
48
|
+
else
|
|
49
|
+
messages.push(line);
|
|
22
50
|
};
|
|
23
51
|
for (const e of events) {
|
|
24
52
|
// Subagents report through their parent's tool call; the main thread is what the person reads.
|
|
@@ -90,6 +118,21 @@ export function fromClaudeAgentEvents(events, options) {
|
|
|
90
118
|
else if (e.subtype === "api_retry") {
|
|
91
119
|
notice = `Claude’s API didn’t answer${e.error_status ? ` (${e.error_status})` : ""}; trying again, attempt ${e.attempt} of ${e.max_retries}.`;
|
|
92
120
|
}
|
|
121
|
+
else if (e.subtype === "compact_boundary") {
|
|
122
|
+
// Without this line the person can't tell the agent no longer has the earlier messages word for word.
|
|
123
|
+
const trigger = e.compact_metadata.trigger;
|
|
124
|
+
systemLine(`compact-${e.uuid}`, options.eventText?.compacted?.(trigger) ?? compactedText(trigger));
|
|
125
|
+
}
|
|
126
|
+
else if (e.subtype === "memory_recall") {
|
|
127
|
+
const found = e.memories.map((m) => ({ path: m.path, scope: m.scope, ...(m.content !== undefined && { content: m.content }) }));
|
|
128
|
+
if (reply)
|
|
129
|
+
reply.memories.push(...found);
|
|
130
|
+
else
|
|
131
|
+
recalling.push(...found);
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
case "conversation_reset":
|
|
135
|
+
systemLine(`reset-${e.uuid}`, options.eventText?.cleared ?? clearedText);
|
|
93
136
|
break;
|
|
94
137
|
case "stream_event": {
|
|
95
138
|
const r = current(e.uuid);
|
|
@@ -170,6 +213,7 @@ export function fromClaudeAgentEvents(events, options) {
|
|
|
170
213
|
usage,
|
|
171
214
|
notice: options.running ? notice : undefined,
|
|
172
215
|
permissionMode,
|
|
216
|
+
recalled,
|
|
173
217
|
};
|
|
174
218
|
}
|
|
175
219
|
/** Adds a source once (by key) and returns its id. Markers count up in the order sources first appear. */
|
|
@@ -7,7 +7,8 @@ export declare const copyFieldStrings: {
|
|
|
7
7
|
/**
|
|
8
8
|
* A value a person copies somewhere else (a callback URL, a scope list). The
|
|
9
9
|
* whole field is the copy target; it says "Copied" only once the clipboard
|
|
10
|
-
* took the value
|
|
10
|
+
* took the value, and its tick (or the cross of a refused write) appears then,
|
|
11
|
+
* never while the write is still pending.
|
|
11
12
|
*/
|
|
12
13
|
declare function CopyField({ label, value, className, }: {
|
|
13
14
|
/** What the value is, shown above it. */
|
|
@@ -13,11 +13,12 @@ export const copyFieldStrings = {
|
|
|
13
13
|
/**
|
|
14
14
|
* A value a person copies somewhere else (a callback URL, a scope list). The
|
|
15
15
|
* whole field is the copy target; it says "Copied" only once the clipboard
|
|
16
|
-
* took the value
|
|
16
|
+
* took the value, and its tick (or the cross of a refused write) appears then,
|
|
17
|
+
* never while the write is still pending.
|
|
17
18
|
*/
|
|
18
19
|
function CopyField({ label, value, className, }) {
|
|
19
20
|
const { state, copy } = useCopy();
|
|
20
21
|
const t = useStrings("copyField", copyFieldStrings);
|
|
21
|
-
return (_jsxs("div", { "data-slot": "copy-field", className: cn("flex flex-col gap-1", className), children: [_jsx("span", { className: "text-xs font-medium text-muted-foreground", children: label }), _jsxs("button", { type: "button", onClick: () => copy(value), className: "group/copy flex w-full items-center gap-2 rounded-lg border bg-sunken px-3 py-2 text-start outline-none transition-colors duration-150 hover:border-border-strong focus-visible:ring-[3px] focus-visible:ring-ring/80", children: [_jsx("code", { className: "min-w-0 flex-1 truncate font-mono text-xs", children: value }), _jsx("span", { "aria-live": "polite", className: "inline-flex shrink-0 items-center gap-1 text-xs text-muted-foreground group-hover/copy:text-foreground", children: state === "copied" ? (_jsxs(_Fragment, { children: [_jsx(Check, { className: "size-3 text-success-text" }), " ", t("copied")] })) : state === "failed" ? (_jsxs(_Fragment, { children: [_jsx(X, { className: "size-3 text-destructive-text" }), " ", t("failed")] })) : (_jsxs(_Fragment, { children: [_jsx(Copy, { className: "size-3" }), " ", t("copy")] })) })] })] }));
|
|
22
|
+
return (_jsxs("div", { "data-slot": "copy-field", className: cn("flex flex-col gap-1", className), children: [_jsx("span", { className: "text-xs font-medium text-muted-foreground", children: label }), _jsxs("button", { type: "button", onClick: () => copy(value), className: "group/copy flex w-full items-center gap-2 rounded-lg border bg-sunken px-3 py-2 text-start outline-none transition-colors duration-150 hover:border-border-strong focus-visible:ring-[3px] focus-visible:ring-ring/80", children: [_jsx("code", { className: "min-w-0 flex-1 truncate font-mono text-xs", children: value }), _jsx("span", { "aria-live": "polite", "data-copy": state, className: "inline-flex shrink-0 items-center gap-1 text-xs text-muted-foreground group-hover/copy:text-foreground", children: state === "copied" ? (_jsxs(_Fragment, { children: [_jsx(Check, { className: "size-3 text-success-text animate-pop-in" }), " ", t("copied")] })) : state === "failed" ? (_jsxs(_Fragment, { children: [_jsx(X, { className: "size-3 text-destructive-text animate-pop-in" }), " ", t("failed")] })) : (_jsxs(_Fragment, { children: [_jsx(Copy, { className: "size-3" }), " ", t("copy")] })) })] })] }));
|
|
22
23
|
}
|
|
23
24
|
export { CopyField };
|
package/dist/blocks/locked.d.ts
CHANGED
|
@@ -13,7 +13,10 @@ type Lockable = React.ReactElement<{
|
|
|
13
13
|
* marked `aria-disabled`, its activation is blocked (click, Enter, Space), and
|
|
14
14
|
* it is described by the reason — so keyboard and screen-reader users reach
|
|
15
15
|
* the explanation on the control itself. The reason shows as a tooltip, or
|
|
16
|
-
* printed under the control with `inline`.
|
|
16
|
+
* printed under the control with `inline`. Pressing the control reveals the
|
|
17
|
+
* reason (the tooltip is held open, the printed reason is emphasised) and,
|
|
18
|
+
* under data-motion="expressive" only, shakes it briefly; with reduced motion
|
|
19
|
+
* or the default subtle preset, only the reason is shown.
|
|
17
20
|
*/
|
|
18
21
|
declare function Locked({ reason, inline, children, className, }: {
|
|
19
22
|
/** Why the control can't be used. Without one, the control renders as normal. */
|
package/dist/blocks/locked.js
CHANGED
|
@@ -11,15 +11,37 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.js";
|
|
|
11
11
|
* marked `aria-disabled`, its activation is blocked (click, Enter, Space), and
|
|
12
12
|
* it is described by the reason — so keyboard and screen-reader users reach
|
|
13
13
|
* the explanation on the control itself. The reason shows as a tooltip, or
|
|
14
|
-
* printed under the control with `inline`.
|
|
14
|
+
* printed under the control with `inline`. Pressing the control reveals the
|
|
15
|
+
* reason (the tooltip is held open, the printed reason is emphasised) and,
|
|
16
|
+
* under data-motion="expressive" only, shakes it briefly; with reduced motion
|
|
17
|
+
* or the default subtle preset, only the reason is shown.
|
|
15
18
|
*/
|
|
16
19
|
function Locked({ reason, inline = false, children, className, }) {
|
|
17
20
|
const id = React.useId();
|
|
21
|
+
// The tooltip as hover and focus drive it, and a press, which holds the reason open and shakes (when expressive).
|
|
22
|
+
// `press` counts presses so a second one restarts both timers.
|
|
23
|
+
const [shown, setShown] = React.useState(false);
|
|
24
|
+
const [press, setPress] = React.useState(0);
|
|
25
|
+
const [shaking, setShaking] = React.useState(false);
|
|
26
|
+
const [held, setHeld] = React.useState(false);
|
|
27
|
+
React.useEffect(() => {
|
|
28
|
+
if (press === 0)
|
|
29
|
+
return;
|
|
30
|
+
const shake = window.setTimeout(() => setShaking(false), 400);
|
|
31
|
+
const hold = window.setTimeout(() => setHeld(false), 1800);
|
|
32
|
+
return () => {
|
|
33
|
+
window.clearTimeout(shake);
|
|
34
|
+
window.clearTimeout(hold);
|
|
35
|
+
};
|
|
36
|
+
}, [press]);
|
|
18
37
|
if (!reason)
|
|
19
38
|
return children;
|
|
20
39
|
const block = (e) => {
|
|
21
40
|
e.preventDefault();
|
|
22
41
|
e.stopPropagation();
|
|
42
|
+
setPress((n) => n + 1);
|
|
43
|
+
setShaking(true);
|
|
44
|
+
setHeld(true);
|
|
23
45
|
};
|
|
24
46
|
const control = React.cloneElement(children, {
|
|
25
47
|
"aria-disabled": true,
|
|
@@ -31,6 +53,6 @@ function Locked({ reason, inline = false, children, className, }) {
|
|
|
31
53
|
block(e);
|
|
32
54
|
},
|
|
33
55
|
});
|
|
34
|
-
return (_jsxs("span", { "data-slot": "locked", className: cn("relative inline-flex flex-col items-start gap-1.5", className), children: [inline ? (control) : (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: control }), _jsx(TooltipContent, { children: reason })] })), _jsxs("span", { id: id, className: cn("items-center gap-1 text-xs text-muted-foreground", inline ? "inline-flex" : "sr-only"), children: [_jsx(LockKeyhole, { "aria-hidden": true, className: "size-3" }), reason] })] }));
|
|
56
|
+
return (_jsxs("span", { "data-slot": "locked", "data-pressed": held ? "" : undefined, className: cn("relative inline-flex flex-col items-start gap-1.5", shaking && "animate-shake", className), children: [inline ? (control) : (_jsxs(Tooltip, { open: shown || held, onOpenChange: setShown, children: [_jsx(TooltipTrigger, { asChild: true, children: control }), _jsx(TooltipContent, { children: reason })] })), _jsxs("span", { id: id, "data-slot": "locked-reason", className: cn("items-center gap-1 text-xs text-muted-foreground transition-colors duration-(--cd-dur-base)", inline ? "inline-flex" : "sr-only", held && "text-foreground"), children: [_jsx(LockKeyhole, { "aria-hidden": true, className: "size-3" }), reason] })] }));
|
|
35
57
|
}
|
|
36
58
|
export { Locked };
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import * as React from "react";
|
|
1
2
|
import { type RunStatus } from "../lib/status.js";
|
|
2
3
|
/**
|
|
3
4
|
* A small status mark. Running and waiting pulse, since they are the two
|
|
4
|
-
* states a person may need to watch or act on.
|
|
5
|
+
* states a person may need to watch or act on. When one of them reaches done
|
|
6
|
+
* or failed while on screen, the new dot pops into place (too small for a drawn
|
|
7
|
+
* mark); a dot that mounts finished is still. The label is always available
|
|
5
8
|
* to assistive tech; pass `showLabel` to print it too.
|
|
6
9
|
*/
|
|
7
10
|
declare function StatusDot({ status, showLabel, label, className, }: {
|
|
@@ -12,5 +15,5 @@ declare function StatusDot({ status, showLabel, label, className, }: {
|
|
|
12
15
|
/** Words to use instead of the status name. */
|
|
13
16
|
label?: string | undefined;
|
|
14
17
|
className?: string | undefined;
|
|
15
|
-
}):
|
|
18
|
+
}): React.JSX.Element;
|
|
16
19
|
export { StatusDot };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
3
4
|
import { cn } from "../lib/utils.js";
|
|
4
5
|
import { useStatusLabel } from "../lib/status.js";
|
|
5
6
|
const dotColor = {
|
|
@@ -13,15 +14,35 @@ const dotColor = {
|
|
|
13
14
|
cancelled: "bg-faint-foreground/60",
|
|
14
15
|
skipped: "bg-faint-foreground/40",
|
|
15
16
|
};
|
|
17
|
+
const finished = (status) => status === "succeeded" || status === "failed";
|
|
18
|
+
const over = (status) => finished(status) || status === "cancelled" || status === "skipped";
|
|
19
|
+
/**
|
|
20
|
+
* How many times the status reached done or failed from an unfinished one while the dot was on screen; zero when it
|
|
21
|
+
* mounted finished, so nothing moves for a change the reader never saw.
|
|
22
|
+
*/
|
|
23
|
+
function useSettled(status) {
|
|
24
|
+
const [settled, setSettled] = React.useState(0);
|
|
25
|
+
const previous = React.useRef(status);
|
|
26
|
+
React.useLayoutEffect(() => {
|
|
27
|
+
const was = previous.current;
|
|
28
|
+
previous.current = status;
|
|
29
|
+
if (was !== status && !over(was) && finished(status))
|
|
30
|
+
setSettled((n) => n + 1);
|
|
31
|
+
}, [status]);
|
|
32
|
+
return settled;
|
|
33
|
+
}
|
|
16
34
|
/**
|
|
17
35
|
* A small status mark. Running and waiting pulse, since they are the two
|
|
18
|
-
* states a person may need to watch or act on.
|
|
36
|
+
* states a person may need to watch or act on. When one of them reaches done
|
|
37
|
+
* or failed while on screen, the new dot pops into place (too small for a drawn
|
|
38
|
+
* mark); a dot that mounts finished is still. The label is always available
|
|
19
39
|
* to assistive tech; pass `showLabel` to print it too.
|
|
20
40
|
*/
|
|
21
41
|
function StatusDot({ status, showLabel = false, label, className, }) {
|
|
22
42
|
const statusLabel = useStatusLabel();
|
|
23
43
|
const text = label ?? statusLabel(status);
|
|
24
44
|
const live = status === "running" || status === "waiting";
|
|
25
|
-
|
|
45
|
+
const settled = useSettled(status);
|
|
46
|
+
return (_jsxs("span", { "data-slot": "status-dot", "data-status": status, "data-settled": settled > 0 ? "" : undefined, className: cn("inline-flex items-center gap-1.5 text-xs text-muted-foreground", className), children: [_jsx("span", { "aria-hidden": true, className: cn("size-2 shrink-0 rounded-full", dotColor[status], live && "animate-pulse-dot", settled > 0 && "animate-pop-in") }, settled), showLabel ? _jsx("span", { children: text }) : _jsx("span", { className: "sr-only", children: text })] }));
|
|
26
47
|
}
|
|
27
48
|
export { StatusDot };
|
|
@@ -7,13 +7,18 @@ export type Step = {
|
|
|
7
7
|
detail?: React.ReactNode | undefined;
|
|
8
8
|
meta?: React.ReactNode | undefined;
|
|
9
9
|
};
|
|
10
|
+
/**
|
|
11
|
+
* A step's status as a mark. When the status reaches done or failed while the mark is on screen, the spinner
|
|
12
|
+
* becomes a tick or cross drawn along its path; a mark that mounts already finished is drawn still.
|
|
13
|
+
*/
|
|
10
14
|
declare function StepMark({ status, }: {
|
|
11
15
|
/** The step's status, which picks its mark. */
|
|
12
16
|
status: RunStatus;
|
|
13
17
|
}): React.JSX.Element;
|
|
14
18
|
/**
|
|
15
19
|
* An ordered list of steps joined by a rail: an agent's plan, its reasoning,
|
|
16
|
-
* a run's stages. The rail fills as
|
|
20
|
+
* a run's stages. The rail fills to each step as it finishes, never ahead of
|
|
21
|
+
* it, and a step that finishes while on screen draws its mark.
|
|
17
22
|
*/
|
|
18
23
|
declare function StepList({ steps, className, }: {
|
|
19
24
|
/** The steps in order, each with a status, and optional detail and meta. */
|
package/dist/blocks/step-list.js
CHANGED
|
@@ -1,16 +1,48 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { CircleSlash, Hand, LoaderCircle } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/utils.js";
|
|
6
6
|
import { useStatusLabel } from "../lib/status.js";
|
|
7
|
+
const finished = (status) => status === "succeeded" || status === "failed";
|
|
8
|
+
const over = (status) => finished(status) || status === "cancelled" || status === "skipped";
|
|
9
|
+
/**
|
|
10
|
+
* How many times `status` has reached succeeded or failed from an unfinished status while this component was on
|
|
11
|
+
* screen. Zero for something that mounted already finished, so its mark is drawn still: motion reports a change
|
|
12
|
+
* that was seen, never one that happened before the reader arrived.
|
|
13
|
+
*/
|
|
14
|
+
function useSettled(status) {
|
|
15
|
+
const [settled, setSettled] = React.useState(0);
|
|
16
|
+
const previous = React.useRef(status);
|
|
17
|
+
React.useLayoutEffect(() => {
|
|
18
|
+
const was = previous.current;
|
|
19
|
+
previous.current = status;
|
|
20
|
+
if (was !== status && !over(was) && finished(status))
|
|
21
|
+
setSettled((n) => n + 1);
|
|
22
|
+
}, [status]);
|
|
23
|
+
return settled;
|
|
24
|
+
}
|
|
25
|
+
/** A tick, drawn along its path when `draw` is above zero (and again each time it changes). */
|
|
26
|
+
function Tick({ draw }) {
|
|
27
|
+
return (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 3, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, className: "size-3", children: _jsx("path", { d: "M20 6 9 17l-5-5", pathLength: 1, className: cn(draw > 0 && "[stroke-dasharray:1] animate-tick-draw") }) }, draw));
|
|
28
|
+
}
|
|
29
|
+
/** A cross, its two strokes drawn one after the other when `draw` is above zero. */
|
|
30
|
+
function Cross({ draw }) {
|
|
31
|
+
const drawn = draw > 0 && "[stroke-dasharray:1] animate-tick-draw";
|
|
32
|
+
return (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 3, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, className: "size-3", children: [_jsx("path", { d: "M18 6 6 18", pathLength: 1, className: cn(drawn) }), _jsx("path", { d: "m6 6 12 12", pathLength: 1, className: cn(drawn, drawn && "[animation-delay:var(--cd-dur-quick)]") })] }, draw));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A step's status as a mark. When the status reaches done or failed while the mark is on screen, the spinner
|
|
36
|
+
* becomes a tick or cross drawn along its path; a mark that mounts already finished is drawn still.
|
|
37
|
+
*/
|
|
7
38
|
function StepMark({ status, }) {
|
|
8
|
-
const
|
|
39
|
+
const settled = useSettled(status);
|
|
40
|
+
const base = "relative z-10 flex size-5 shrink-0 items-center justify-center rounded-full transition-colors duration-(--cd-dur-base) ease-standard";
|
|
9
41
|
switch (status) {
|
|
10
42
|
case "succeeded":
|
|
11
|
-
return (_jsx("span", { className: cn(base, "bg-success text-on-success"), children: _jsx(
|
|
43
|
+
return (_jsx("span", { className: cn(base, "bg-success text-on-success"), "data-settled": settled > 0 ? "" : undefined, children: _jsx(Tick, { draw: settled }) }));
|
|
12
44
|
case "failed":
|
|
13
|
-
return (_jsx("span", { className: cn(base, "bg-destructive text-on-destructive"), children: _jsx(
|
|
45
|
+
return (_jsx("span", { className: cn(base, "bg-destructive text-on-destructive"), "data-settled": settled > 0 ? "" : undefined, children: _jsx(Cross, { draw: settled }) }));
|
|
14
46
|
case "running":
|
|
15
47
|
return (_jsx("span", { className: cn(base, "bg-accent-weak text-accent-text"), children: _jsx(LoaderCircle, { className: "size-3.5 animate-spin", strokeWidth: 2.5 }) }));
|
|
16
48
|
case "waiting":
|
|
@@ -24,16 +56,18 @@ function StepMark({ status, }) {
|
|
|
24
56
|
return (_jsx("span", { className: cn(base, "bg-background"), children: _jsx("span", { className: "size-2 rounded-full border-[1.5px] border-faint-foreground/70" }) }));
|
|
25
57
|
}
|
|
26
58
|
}
|
|
59
|
+
function StepRow({ step, index, last, }) {
|
|
60
|
+
const statusLabel = useStatusLabel();
|
|
61
|
+
const done = step.status === "succeeded";
|
|
62
|
+
const settled = useSettled(step.status);
|
|
63
|
+
return (_jsxs("li", { className: "relative flex gap-3 pb-4 last:pb-0 animate-rise-in", style: { animationDelay: `${index * 40}ms` }, children: [!last && (_jsx("span", { "aria-hidden": true, className: "absolute top-5 bottom-0 start-[9.5px] w-px bg-input", children: done && (_jsx("span", { "data-slot": "step-rail-fill", className: cn("absolute inset-0 origin-top bg-success/50", settled > 0 && "animate-fill [--cd-fill-from:1_0]") }, settled)) })), _jsx(StepMark, { status: step.status }), _jsxs("div", { className: "min-w-0 flex-1 pt-px", children: [_jsxs("div", { className: "flex items-baseline justify-between gap-3", children: [_jsxs("p", { className: cn("text-sm font-medium", step.status === "idle" && "text-muted-foreground", (step.status === "skipped" || step.status === "cancelled") && "text-faint-foreground line-through"), children: [step.title, _jsxs("span", { className: "sr-only", children: [" \u2014 ", statusLabel(step.status)] })] }), step.meta && _jsx("span", { className: "shrink-0 text-xs tabular-nums text-faint-foreground", children: step.meta })] }), step.detail && _jsx("div", { className: "mt-1 text-[0.8125rem] text-muted-foreground", children: step.detail })] })] }));
|
|
64
|
+
}
|
|
27
65
|
/**
|
|
28
66
|
* An ordered list of steps joined by a rail: an agent's plan, its reasoning,
|
|
29
|
-
* a run's stages. The rail fills as
|
|
67
|
+
* a run's stages. The rail fills to each step as it finishes, never ahead of
|
|
68
|
+
* it, and a step that finishes while on screen draws its mark.
|
|
30
69
|
*/
|
|
31
70
|
function StepList({ steps, className, }) {
|
|
32
|
-
|
|
33
|
-
return (_jsx("ol", { "data-slot": "step-list", className: cn("flex flex-col", className), children: steps.map((step, i) => {
|
|
34
|
-
const last = i === steps.length - 1;
|
|
35
|
-
const done = step.status === "succeeded";
|
|
36
|
-
return (_jsxs("li", { className: "relative flex gap-3 pb-4 last:pb-0 animate-rise-in", style: { animationDelay: `${i * 40}ms` }, children: [!last && (_jsx("span", { "aria-hidden": true, className: cn("absolute top-5 bottom-0 start-[9.5px] w-px transition-colors duration-500", done ? "bg-success/50" : "bg-input") })), _jsx(StepMark, { status: step.status }), _jsxs("div", { className: "min-w-0 flex-1 pt-px", children: [_jsxs("div", { className: "flex items-baseline justify-between gap-3", children: [_jsxs("p", { className: cn("text-sm font-medium", step.status === "idle" && "text-muted-foreground", (step.status === "skipped" || step.status === "cancelled") && "text-faint-foreground line-through"), children: [step.title, _jsxs("span", { className: "sr-only", children: [" \u2014 ", statusLabel(step.status)] })] }), step.meta && _jsx("span", { className: "shrink-0 text-xs tabular-nums text-faint-foreground", children: step.meta })] }), step.detail && _jsx("div", { className: "mt-1 text-[0.8125rem] text-muted-foreground", children: step.detail })] })] }, step.id));
|
|
37
|
-
}) }));
|
|
71
|
+
return (_jsx("ol", { "data-slot": "step-list", className: cn("flex flex-col", className), children: steps.map((step, i) => (_jsx(StepRow, { step: step, index: i, last: i === steps.length - 1 }, step.id))) }));
|
|
38
72
|
}
|
|
39
73
|
export { StepList, StepMark };
|
package/dist/chat/composer.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export declare const composerStrings: {
|
|
|
13
13
|
hintSend: string;
|
|
14
14
|
hintNewLine: string;
|
|
15
15
|
hintMention: string;
|
|
16
|
+
hintCommand: string;
|
|
16
17
|
stop: string;
|
|
17
18
|
send: string;
|
|
18
19
|
dropFiles: string;
|
|
@@ -42,7 +43,7 @@ export type ComposerState = "ready" | "sending" | "streaming" | {
|
|
|
42
43
|
* (`{ kind: "held", reason }`) keeps typing open and says why sending waits.
|
|
43
44
|
* `textareaRef` focuses the box from outside, e.g. after a new project starts.
|
|
44
45
|
*/
|
|
45
|
-
declare function Composer({ value, onChange, onSubmit, onStop, state, scope, onScopeClick, mentions, commands, references, onReferencesChange, attachments, onAttach, accept, onRemoveAttachment, placeholder: placeholderProp, label: labelProp, tools, textareaRef, sendShape, className, }: {
|
|
46
|
+
declare function Composer({ value, onChange, onSubmit, onStop, state, scope, onScopeClick, mentions, commands, references, onReferencesChange, attachments, onAttach, accept, onRemoveAttachment, placeholder: placeholderProp, label: labelProp, tools, textareaRef, sendShape, hints, className, }: {
|
|
46
47
|
/** The text in the box. */
|
|
47
48
|
value: string;
|
|
48
49
|
/** Called with the new text as the person types. */
|
|
@@ -61,11 +62,11 @@ declare function Composer({ value, onChange, onSubmit, onStop, state, scope, onS
|
|
|
61
62
|
mentions?: MentionOption[] | undefined;
|
|
62
63
|
/** What / can run, only at the start of a message. */
|
|
63
64
|
commands?: SlashCommand[] | undefined;
|
|
64
|
-
/** Entities already attached, shown as chips. */
|
|
65
|
+
/** Entities already attached, shown as chips. A chip added after the composer drew pops in; a removed one pops out. */
|
|
65
66
|
references?: EntityRef[] | undefined;
|
|
66
67
|
/** Called when a chip is added or removed. */
|
|
67
68
|
onReferencesChange?: ((references: EntityRef[]) => void) | undefined;
|
|
68
|
-
/** Files attached, with upload progress or an error. */
|
|
69
|
+
/** Files attached, with upload progress or an error, as chips that pop in when added and out when removed. */
|
|
69
70
|
attachments?: Attachment[] | undefined;
|
|
70
71
|
/** Shows an attach button; called with the chosen files. */
|
|
71
72
|
onAttach?: ((files: File[]) => void) | undefined;
|
|
@@ -82,6 +83,11 @@ declare function Composer({ value, onChange, onSubmit, onStop, state, scope, onS
|
|
|
82
83
|
label?: string | undefined;
|
|
83
84
|
/** Your own controls in the toolbar, after the attach button: a dictation button, a model picker. */
|
|
84
85
|
tools?: React.ReactNode | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* The keyboard hints shown at wide widths ("↵ send · ⇧↵ new line"), naming @ and / only when `mentions` or
|
|
88
|
+
* `commands` are given. Defaults to true; false hides them.
|
|
89
|
+
*/
|
|
90
|
+
hints?: boolean | undefined;
|
|
85
91
|
/** The text box, to focus it or read its selection from outside. */
|
|
86
92
|
textareaRef?: React.Ref<HTMLTextAreaElement> | undefined;
|
|
87
93
|
/** The send button's shape: "square" (the default, rounded corners) or "round". */
|
package/dist/chat/composer.js
CHANGED
|
@@ -20,6 +20,7 @@ export const composerStrings = {
|
|
|
20
20
|
hintSend: "send",
|
|
21
21
|
hintNewLine: "new line",
|
|
22
22
|
hintMention: "mention",
|
|
23
|
+
hintCommand: "command",
|
|
23
24
|
stop: "Stop",
|
|
24
25
|
send: "Send",
|
|
25
26
|
dropFiles: "Drop files to attach",
|
|
@@ -39,6 +40,53 @@ function findTrigger(value, caret) {
|
|
|
39
40
|
const groupOf = (item, commandsLabel = "Commands") => (item.type === "command" ? commandsLabel : item.option.group);
|
|
40
41
|
/** Whether a drag carries files (not text or a link being dragged around the page). */
|
|
41
42
|
const hasFiles = (e) => Array.from(e.dataTransfer?.types ?? []).includes("Files");
|
|
43
|
+
const referenceKey = (r) => `${r.kind}:${r.name}`;
|
|
44
|
+
const attachmentKey = (a) => a.id;
|
|
45
|
+
/**
|
|
46
|
+
* Which chips were added after the composer first drew (they pop in) and which were just removed (they pop out as
|
|
47
|
+
* inert ghosts, then go). Chips present at first render are drawn still: nothing moves for a state the reader
|
|
48
|
+
* already had. The ghosts are cleared by their animation's end, or after 400ms when nothing animates.
|
|
49
|
+
*/
|
|
50
|
+
function useChipMotion(items, keyOf) {
|
|
51
|
+
const [entered, setEntered] = React.useState(() => new Set());
|
|
52
|
+
const [leaving, setLeaving] = React.useState([]);
|
|
53
|
+
const previous = React.useRef(null);
|
|
54
|
+
React.useLayoutEffect(() => {
|
|
55
|
+
const before = previous.current;
|
|
56
|
+
previous.current = items;
|
|
57
|
+
if (!before)
|
|
58
|
+
return;
|
|
59
|
+
const now = new Set(items.map(keyOf));
|
|
60
|
+
const was = new Set(before.map(keyOf));
|
|
61
|
+
const added = items.filter((item) => !was.has(keyOf(item))).map(keyOf);
|
|
62
|
+
const gone = before.filter((item) => !now.has(keyOf(item)));
|
|
63
|
+
if (added.length || gone.length) {
|
|
64
|
+
setEntered((set) => {
|
|
65
|
+
const next = new Set(set);
|
|
66
|
+
for (const item of gone)
|
|
67
|
+
next.delete(keyOf(item));
|
|
68
|
+
for (const key of added)
|
|
69
|
+
next.add(key);
|
|
70
|
+
return next;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (gone.length)
|
|
74
|
+
setLeaving((list) => [...list.filter((item) => !now.has(keyOf(item))), ...gone]);
|
|
75
|
+
}, [items, keyOf]);
|
|
76
|
+
React.useEffect(() => {
|
|
77
|
+
if (leaving.length === 0)
|
|
78
|
+
return;
|
|
79
|
+
const timer = window.setTimeout(() => setLeaving([]), 400);
|
|
80
|
+
return () => window.clearTimeout(timer);
|
|
81
|
+
}, [leaving]);
|
|
82
|
+
const entering = (item) => entered.has(keyOf(item));
|
|
83
|
+
const left = (item) => setLeaving((list) => list.filter((x) => x !== item));
|
|
84
|
+
return { entering, leaving, left };
|
|
85
|
+
}
|
|
86
|
+
/** A removed chip, kept for the length of its pop-out and hidden from assistive tech. */
|
|
87
|
+
function LeavingChip({ onDone, children }) {
|
|
88
|
+
return (_jsx("span", { "aria-hidden": true, "data-leaving": true, className: "pointer-events-none inline-flex max-w-full animate-pop-out", onAnimationEnd: onDone, children: children }));
|
|
89
|
+
}
|
|
42
90
|
/**
|
|
43
91
|
* Where a person asks. Mentions (@) attach things in the user's system as chips the agent
|
|
44
92
|
* receives as references, not as text it has to parse; slash commands (/) start
|
|
@@ -52,7 +100,7 @@ const hasFiles = (e) => Array.from(e.dataTransfer?.types ?? []).includes("Files"
|
|
|
52
100
|
* (`{ kind: "held", reason }`) keeps typing open and says why sending waits.
|
|
53
101
|
* `textareaRef` focuses the box from outside, e.g. after a new project starts.
|
|
54
102
|
*/
|
|
55
|
-
function Composer({ value, onChange, onSubmit, onStop, state = "ready", scope, onScopeClick, mentions = [], commands = [], references = [], onReferencesChange, attachments = [], onAttach, accept, onRemoveAttachment, placeholder: placeholderProp, label: labelProp, tools, textareaRef, sendShape = "square", className, }) {
|
|
103
|
+
function Composer({ value, onChange, onSubmit, onStop, state = "ready", scope, onScopeClick, mentions = [], commands = [], references = [], onReferencesChange, attachments = [], onAttach, accept, onRemoveAttachment, placeholder: placeholderProp, label: labelProp, tools, textareaRef, sendShape = "square", hints = true, className, }) {
|
|
56
104
|
const kindOf = useEntityKinds();
|
|
57
105
|
const t = useStrings("composer", composerStrings);
|
|
58
106
|
const placeholder = placeholderProp ?? t("placeholder");
|
|
@@ -65,6 +113,8 @@ function Composer({ value, onChange, onSubmit, onStop, state = "ready", scope, o
|
|
|
65
113
|
const fileInput = React.useRef(null);
|
|
66
114
|
const [trigger, setTrigger] = React.useState(null);
|
|
67
115
|
const [active, setActive] = React.useState(0);
|
|
116
|
+
const referenceMotion = useChipMotion(references, referenceKey);
|
|
117
|
+
const attachmentMotion = useChipMotion(attachments, attachmentKey);
|
|
68
118
|
const held = typeof state === "object" && state.kind === "held" ? state : null;
|
|
69
119
|
const locked = typeof state === "object" && state.kind !== "held" ? state : null;
|
|
70
120
|
const canAttach = Boolean(onAttach) && !locked;
|
|
@@ -197,7 +247,7 @@ function Composer({ value, onChange, onSubmit, onStop, state = "ready", scope, o
|
|
|
197
247
|
e.preventDefault();
|
|
198
248
|
choose(item);
|
|
199
249
|
}, onMouseMove: () => setActive(i), className: "flex cursor-pointer items-center gap-2.5 rounded-md px-2 py-1.5 text-sm aria-selected:bg-accent", children: item.type === "command" ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "font-mono text-xs text-accent-text", children: ["/", item.command.name] }), item.command.args && _jsx("span", { className: "font-mono text-xs text-faint-foreground", children: item.command.args }), _jsx("span", { className: "ms-auto truncate text-xs text-muted-foreground", children: item.command.description })] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "flex size-6 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground", children: Icon ? _jsx(Icon, { className: "size-3.5" }) : _jsx("span", { className: "text-xs", children: "@" }) }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate", children: item.option.label }), (item.option.description ?? item.option.entity?.name) !== item.option.label && (item.option.description ?? item.option.entity?.name) && (_jsx("span", { className: "block truncate font-mono text-2xs text-faint-foreground", children: item.option.description ?? item.option.entity?.name }))] })] })) })] }, item.type === "command" ? item.command.id : item.option.id));
|
|
200
|
-
}) })), _jsxs("div", { className: cn("rounded-2xl border bg-card shadow-raised transition-[border-color,box-shadow] duration-150 focus-within:border-border-strong focus-within:shadow-float", locked && "bg-muted shadow-none"), children: [(references.length > 0 || attachments.length > 0) && (_jsxs("div", { className: "flex flex-wrap gap-1.5 px-3 pt-3", children: [references.map((r) => (_jsx(EntityChip, { entity: r, onRemove: (e) => onReferencesChange?.(references.filter((x) => x !== e)) },
|
|
250
|
+
}) })), _jsxs("div", { className: cn("rounded-2xl border bg-card shadow-raised transition-[border-color,box-shadow] duration-150 focus-within:border-border-strong focus-within:shadow-float", locked && "bg-muted shadow-none"), children: [(references.length > 0 || attachments.length > 0 || referenceMotion.leaving.length > 0 || attachmentMotion.leaving.length > 0) && (_jsxs("div", { className: "flex flex-wrap gap-1.5 px-3 pt-3", children: [references.map((r) => (_jsx(EntityChip, { entity: r, onRemove: (e) => onReferencesChange?.(references.filter((x) => x !== e)), className: cn(referenceMotion.entering(r) && "animate-pop-in") }, referenceKey(r)))), referenceMotion.leaving.map((r) => (_jsx(LeavingChip, { onDone: () => referenceMotion.left(r), children: _jsx(EntityChip, { entity: r }) }, `leaving:${referenceKey(r)}`))), attachments.map((a) => (_jsx(AttachmentChip, { attachment: a, onRemove: onRemoveAttachment, className: cn(attachmentMotion.entering(a) && "animate-pop-in") }, a.id))), attachmentMotion.leaving.map((a) => (_jsx(LeavingChip, { onDone: () => attachmentMotion.left(a), children: _jsx(AttachmentChip, { attachment: a }) }, `leaving:${a.id}`)))] })), _jsx("label", { htmlFor: id, className: "sr-only", children: label }), _jsx("textarea", { ref: textarea, id: id, rows: 1, value: value, disabled: Boolean(locked), placeholder: locked ? undefined : placeholder, onChange: (e) => {
|
|
201
251
|
onChange(e.target.value);
|
|
202
252
|
requestAnimationFrame(syncTrigger);
|
|
203
253
|
}, onKeyDown: onKeyDown, onKeyUp: (e) => {
|
|
@@ -216,6 +266,8 @@ function Composer({ value, onChange, onSubmit, onStop, state = "ready", scope, o
|
|
|
216
266
|
if (files.length)
|
|
217
267
|
onAttach(files);
|
|
218
268
|
e.target.value = "";
|
|
219
|
-
} }), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": t("attachFiles"), disabled: Boolean(locked), onClick: () => fileInput.current?.click(), className: "flex size-8 items-center justify-center rounded-lg text-muted-foreground transition-colors duration-150 hover:bg-accent hover:text-foreground disabled:opacity-40", children: _jsx(Paperclip, { className: "size-4" }) }) }), _jsx(TooltipContent, { children: t("attachFiles") })] })] })), tools, scope && (_jsxs("button", { type: "button", onClick: onScopeClick, disabled: !onScopeClick, className: cn("inline-flex min-w-0 items-center gap-1.5 rounded-lg px-2 py-1 text-xs transition-colors duration-150 enabled:hover:bg-accent", production ? "bg-warning-weak text-warning-text enabled:hover:bg-warning-weak" : "text-muted-foreground"), children: [production ? _jsx(TriangleAlert, { "aria-hidden": true, className: "size-3 shrink-0" }) : _jsx("span", { "aria-hidden": true, className: "size-1.5 shrink-0 rounded-full bg-success" }), _jsxs("span", { className: "truncate", children: [scope.environment, " \u00B7 ", production ? t("production") : (scope.detail ?? t("nonProduction"))] })] })),
|
|
269
|
+
} }), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": t("attachFiles"), disabled: Boolean(locked), onClick: () => fileInput.current?.click(), className: "flex size-8 items-center justify-center rounded-lg text-muted-foreground transition-colors duration-150 hover:bg-accent hover:text-foreground disabled:opacity-40", children: _jsx(Paperclip, { className: "size-4" }) }) }), _jsx(TooltipContent, { children: t("attachFiles") })] })] })), tools, scope && (_jsxs("button", { type: "button", onClick: onScopeClick, disabled: !onScopeClick, className: cn("inline-flex min-w-0 items-center gap-1.5 rounded-lg px-2 py-1 text-xs transition-colors duration-150 enabled:hover:bg-accent", production ? "bg-warning-weak text-warning-text enabled:hover:bg-warning-weak" : "text-muted-foreground"), children: [production ? _jsx(TriangleAlert, { "aria-hidden": true, className: "size-3 shrink-0" }) : _jsx("span", { "aria-hidden": true, className: "size-1.5 shrink-0 rounded-full bg-success" }), _jsxs("span", { className: "truncate", children: [scope.environment, " \u00B7 ", production ? t("production") : (scope.detail ?? t("nonProduction"))] })] })), hints && (
|
|
270
|
+
// Only what this composer can do: @ and / are named only when there's something for them to offer.
|
|
271
|
+
_jsxs("span", { className: "ms-auto hidden text-2xs text-faint-foreground @lg:inline", children: [_jsx("kbd", { className: "font-sans", children: "\u21B5" }), " ", t("hintSend"), " \u00B7 ", _jsx("kbd", { className: "font-sans", children: "\u21E7\u21B5" }), " ", t("hintNewLine"), mentions.length > 0 && (_jsxs(_Fragment, { children: [" · ", _jsx("kbd", { className: "font-sans", children: "@" }), " ", t("hintMention")] })), commands.length > 0 && (_jsxs(_Fragment, { children: [" · ", _jsx("kbd", { className: "font-sans", children: "/" }), " ", t("hintCommand")] }))] })), state === "streaming" && onStop ? (_jsx("button", { type: "button", "aria-label": t("stop"), onClick: onStop, className: cn("ms-auto flex size-8 items-center justify-center bg-foreground text-background transition-transform duration-150 active:scale-95", hints && "@lg:ms-1", sendShape === "round" ? "rounded-full" : "rounded-lg"), children: _jsx(Square, { className: "size-3 fill-current" }) })) : (_jsx("button", { type: "button", "aria-label": t("send"), onClick: submit, disabled: !canSend, "aria-busy": state === "sending", className: cn("ms-auto flex size-8 items-center justify-center bg-primary text-primary-foreground transition-[background-color,opacity,transform] duration-150 enabled:hover:bg-primary-hover active:scale-95 disabled:opacity-30", hints && "@lg:ms-1", sendShape === "round" ? "rounded-full" : "rounded-lg"), children: state === "sending" ? _jsx(LoaderCircle, { className: "size-4 animate-spin" }) : _jsx(ArrowUp, { className: "size-4", strokeWidth: 2.5 }) }))] })] })] }));
|
|
220
272
|
}
|
|
221
273
|
export { Composer };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as React from "react";
|
|
1
2
|
import type { ContextSegment } from "../lib/know.js";
|
|
2
3
|
/** ContextMeter's fixed words; override them under `contextMeter` in LocaleProvider's `strings`. */
|
|
3
4
|
export declare const contextMeterStrings: {
|
|
@@ -11,7 +12,9 @@ export declare const contextMeterStrings: {
|
|
|
11
12
|
/**
|
|
12
13
|
* What fills the model's context budget, as a ring in the composer that
|
|
13
14
|
* opens to a breakdown: system, instructions, Know, the thread, tools, files.
|
|
14
|
-
* Counts are the service's own token counts; the ring warns past 80%.
|
|
15
|
+
* Counts are the service's own token counts; the ring warns past 80%. When a
|
|
16
|
+
* new count arrives the ring sweeps to it and changes colour at the warning
|
|
17
|
+
* line; it draws its first value in place, with no sweep from zero.
|
|
15
18
|
*/
|
|
16
19
|
declare function ContextMeter({ segments, limit, className, }: {
|
|
17
20
|
/** What fills the context window, by kind, in tokens. */
|
|
@@ -19,5 +22,5 @@ declare function ContextMeter({ segments, limit, className, }: {
|
|
|
19
22
|
/** The model's context window, in tokens. */
|
|
20
23
|
limit: number;
|
|
21
24
|
className?: string | undefined;
|
|
22
|
-
}):
|
|
25
|
+
}): React.JSX.Element;
|
|
23
26
|
export { ContextMeter };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
3
4
|
import { cn } from "../lib/utils.js";
|
|
4
5
|
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover.js";
|
|
5
6
|
import { useFormat, useStrings } from "../blocks/locale.js";
|
|
@@ -29,7 +30,9 @@ function k(n, format, thousands) {
|
|
|
29
30
|
/**
|
|
30
31
|
* What fills the model's context budget, as a ring in the composer that
|
|
31
32
|
* opens to a breakdown: system, instructions, Know, the thread, tools, files.
|
|
32
|
-
* Counts are the service's own token counts; the ring warns past 80%.
|
|
33
|
+
* Counts are the service's own token counts; the ring warns past 80%. When a
|
|
34
|
+
* new count arrives the ring sweeps to it and changes colour at the warning
|
|
35
|
+
* line; it draws its first value in place, with no sweep from zero.
|
|
33
36
|
*/
|
|
34
37
|
function ContextMeter({ segments, limit, className, }) {
|
|
35
38
|
const t = useStrings("contextMeter", contextMeterStrings);
|
|
@@ -40,7 +43,10 @@ function ContextMeter({ segments, limit, className, }) {
|
|
|
40
43
|
const warn = pct > 0.8;
|
|
41
44
|
const r = 7;
|
|
42
45
|
const c = 2 * Math.PI * r;
|
|
43
|
-
|
|
46
|
+
// The sweep is a transition, so it runs only for a value that changed after the first one was drawn.
|
|
47
|
+
const [drawn, setDrawn] = React.useState(false);
|
|
48
|
+
React.useEffect(() => setDrawn(true), []);
|
|
49
|
+
return (_jsxs(Popover, { children: [_jsxs(PopoverTrigger, { "aria-label": t("label", { used: tokens(used), limit: tokens(limit) }), "data-warn": warn ? "" : undefined, className: cn("inline-flex items-center gap-1.5 rounded-md px-1.5 py-1 text-2xs tabular-nums outline-none hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/80", warn ? "text-warning-text" : "text-muted-foreground", className), children: [_jsxs("svg", { viewBox: "0 0 18 18", className: "size-4 -rotate-90", "aria-hidden": true, children: [_jsx("circle", { cx: "9", cy: "9", r: r, fill: "none", stroke: "var(--muted)", strokeWidth: "2.5" }), _jsx("circle", { "data-slot": "context-meter-ring", cx: "9", cy: "9", r: r, fill: "none", stroke: warn ? "var(--warning)" : "var(--primary)", strokeWidth: "2.5", strokeDasharray: `${pct * c} ${c}`, strokeLinecap: "round", className: cn(drawn && "transition-[stroke-dasharray,stroke] duration-(--cd-dur-slow) ease-standard") })] }), fmt.number(Math.round(pct * 100) / 100, { style: "percent" })] }), _jsxs(PopoverContent, { align: "start", className: "w-72 p-3", children: [_jsxs("p", { className: "flex items-baseline justify-between text-sm font-medium", children: [t("context"), " ", _jsx("span", { className: "text-xs font-normal text-muted-foreground tabular-nums", children: t("usage", { used: tokens(used), limit: tokens(limit) }) })] }), _jsx("div", { "aria-hidden": true, className: "mt-2 flex h-2 overflow-hidden rounded-full bg-muted", children: segments.map((s) => (_jsx("span", { style: { width: `${(s.tokens / limit) * 100}%`, background: kindColor[s.kind] } }, s.label))) }), _jsx("ul", { className: "mt-3 flex flex-col gap-1.5", children: [...segments]
|
|
44
50
|
.sort((a, b) => b.tokens - a.tokens)
|
|
45
51
|
.map((s) => (_jsxs("li", { className: "flex items-center gap-2 text-xs", children: [_jsx("span", { "aria-hidden": true, className: "size-2 rounded-xs", style: { background: kindColor[s.kind] } }), _jsx("span", { className: "flex-1", children: s.label }), _jsx("span", { className: "text-muted-foreground tabular-nums", children: tokens(s.tokens) })] }, s.label))) }), warn && _jsx("p", { className: "mt-3 text-xs text-warning-text", children: t("nearlyFull") })] })] }));
|
|
46
52
|
}
|
|
@@ -11,8 +11,10 @@ export declare const messageActionsStrings: {
|
|
|
11
11
|
export type Rating = "up" | "down" | null;
|
|
12
12
|
/**
|
|
13
13
|
* Copy, retry and rate under an agent's reply. Each action appears only when
|
|
14
|
-
* its handler is given. Copy reports what the clipboard did
|
|
15
|
-
*
|
|
14
|
+
* its handler is given. Copy reports what the clipboard did: the icon stays a
|
|
15
|
+
* copy icon until the write settles, then becomes a tick (or a cross when the
|
|
16
|
+
* clipboard refused). A rating is the caller's state, so it shows only what
|
|
17
|
+
* the service recorded.
|
|
16
18
|
*/
|
|
17
19
|
declare function MessageActions({ text, onRetry, rating, onRate, meta, className, }: {
|
|
18
20
|
/** What Copy copies; without it there's no Copy. */
|