@candor-ui/react 1.0.0 → 1.0.1
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.js +3 -3
- package/dist/adapters/ai-sdk.d.ts +2 -2
- package/dist/adapters/ai-sdk.js +7 -6
- package/dist/adapters/claude-agent-sdk.js +2 -2
- package/dist/chat/message-content.js +13 -0
- package/dist/lib/chat.d.ts +20 -0
- package/dist/lib/safe-markdown.d.ts +7 -2
- package/dist/lib/safe-markdown.js +13 -4
- package/package.json +2 -3
package/dist/adapters/ag-ui.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventType } from "@ag-ui/core";
|
|
2
|
-
import { markdownBlocks } from "../lib/safe-markdown.js";
|
|
2
|
+
import { markdownBlocks, plainBlocks } from "../lib/safe-markdown.js";
|
|
3
3
|
/**
|
|
4
4
|
* Turns AG-UI events into Candor messages, waiting approvals and usage.
|
|
5
5
|
* Pure (given `now`), so it can run on every render; useAgUi calls it for you.
|
|
@@ -58,7 +58,7 @@ export function fromAgUiEvents(events, options) {
|
|
|
58
58
|
continue;
|
|
59
59
|
switch (e.type) {
|
|
60
60
|
case "candor_user":
|
|
61
|
-
turns.push({ user: { id: e.id, author: { role: "user" }, status: "complete", blocks:
|
|
61
|
+
turns.push({ user: { id: e.id, author: { role: "user" }, status: "complete", blocks: plainBlocks(e.text) } });
|
|
62
62
|
break;
|
|
63
63
|
case "candor_answer": {
|
|
64
64
|
answers.set(e.interruptId, { approved: e.approved, reason: e.reason });
|
|
@@ -261,7 +261,7 @@ function applySnapshot(previous, snapshot) {
|
|
|
261
261
|
if (m.subagentRunId)
|
|
262
262
|
continue;
|
|
263
263
|
if (m.role === "user") {
|
|
264
|
-
turns.push({ user: { id: m.id, author: { role: "user" }, status: "complete", blocks:
|
|
264
|
+
turns.push({ user: { id: m.id, author: { role: "user" }, status: "complete", blocks: plainBlocks(resultText(m.content)) }, reply: byUser.get(m.id) });
|
|
265
265
|
continue;
|
|
266
266
|
}
|
|
267
267
|
if (m.role !== "assistant" && m.role !== "tool" && m.role !== "reasoning")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { UIMessage } from "ai";
|
|
2
2
|
import type { AgentIdentity, Message } from "../lib/chat.js";
|
|
3
|
-
import { inline, markdownBlocks } from "../lib/safe-markdown.js";
|
|
4
|
-
export { inline, markdownBlocks };
|
|
3
|
+
import { inline, markdownBlocks, plainBlocks } from "../lib/safe-markdown.js";
|
|
4
|
+
export { inline, markdownBlocks, plainBlocks };
|
|
5
5
|
export type FromAiSdkOptions = {
|
|
6
6
|
/** Who the assistant is. Candor always names the agent. */
|
|
7
7
|
agent: AgentIdentity;
|
package/dist/adapters/ai-sdk.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { inline, markdownBlocks } from "../lib/safe-markdown.js";
|
|
2
|
-
// Re-exported for apps that
|
|
3
|
-
export { inline, markdownBlocks };
|
|
1
|
+
import { inline, markdownBlocks, plainBlocks } from "../lib/safe-markdown.js";
|
|
2
|
+
// Re-exported for apps that use them from here. (Imported first: shadcn rewrites import paths, not export-from paths.)
|
|
3
|
+
export { inline, markdownBlocks, plainBlocks };
|
|
4
4
|
/** Convert a whole useChat message list. */
|
|
5
5
|
export function fromAiSdkMessages(messages, options) {
|
|
6
6
|
const lastAssistant = messages.findLastIndex((m) => m.role === "assistant");
|
|
@@ -26,17 +26,18 @@ export function fromAiSdkMessage(message, options) {
|
|
|
26
26
|
flushTools();
|
|
27
27
|
switch (part.type) {
|
|
28
28
|
case "text":
|
|
29
|
-
|
|
29
|
+
// A person's own words are kept as typed; only the assistant's text is read as markdown.
|
|
30
|
+
blocks.push(...(message.role === "user" ? plainBlocks(part.text) : markdownBlocks(part.text)));
|
|
30
31
|
break;
|
|
31
32
|
case "reasoning":
|
|
32
33
|
if (part.text.trim())
|
|
33
34
|
reasoning.push(reasoningEntry(part.id ?? `r${reasoning.length}`, part.text, part.state === "streaming"));
|
|
34
35
|
break;
|
|
35
36
|
case "source-url":
|
|
36
|
-
sources.push({ id: part.sourceId, marker: sources.length + 1, kind: "
|
|
37
|
+
sources.push({ id: part.sourceId, marker: sources.length + 1, kind: "web", label: part.title ?? part.url, origin: host(part.url) });
|
|
37
38
|
break;
|
|
38
39
|
case "source-document":
|
|
39
|
-
sources.push({ id: part.sourceId, marker: sources.length + 1, kind: "
|
|
40
|
+
sources.push({ id: part.sourceId, marker: sources.length + 1, kind: "document", label: part.title, origin: part.filename });
|
|
40
41
|
break;
|
|
41
42
|
case "file":
|
|
42
43
|
blocks.push({ kind: "paragraph", nodes: [{ kind: "text", text: "Attached " }, { kind: "code", text: part.filename ?? part.mediaType }] });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { markdownBlocks } from "../lib/safe-markdown.js";
|
|
1
|
+
import { markdownBlocks, plainBlocks } from "../lib/safe-markdown.js";
|
|
2
2
|
/**
|
|
3
3
|
* Turns Claude Agent SDK events into Candor messages, waiting approvals and usage.
|
|
4
4
|
* Pure, so it can run on every render; useClaudeAgent calls it for you.
|
|
@@ -26,7 +26,7 @@ export function fromClaudeAgentEvents(events, options) {
|
|
|
26
26
|
switch (e.type) {
|
|
27
27
|
case "candor_user":
|
|
28
28
|
close(false);
|
|
29
|
-
messages.push({ id: e.id, author: { role: "user" }, status: "complete", blocks:
|
|
29
|
+
messages.push({ id: e.id, author: { role: "user" }, status: "complete", blocks: plainBlocks(e.text) });
|
|
30
30
|
notice = undefined;
|
|
31
31
|
break;
|
|
32
32
|
case "candor_approval": {
|
|
@@ -28,6 +28,7 @@ function Inline({ nodes, }) {
|
|
|
28
28
|
return _jsx("em", { children: n.text }, i);
|
|
29
29
|
case "code":
|
|
30
30
|
return (_jsx("code", { className: "rounded-xs bg-muted px-1 py-px font-mono text-[0.88em]", children: n.text }, i));
|
|
31
|
+
case "measured":
|
|
31
32
|
case "orgData":
|
|
32
33
|
return (_jsx("span", { title: n.source ? `From the ${terms.environment}: ${n.source}` : `From the ${terms.environment}`, className: "font-medium tabular-nums text-foreground", children: n.text }, i));
|
|
33
34
|
case "provisional":
|
|
@@ -50,6 +51,15 @@ function Block({ block, }) {
|
|
|
50
51
|
switch (block.kind) {
|
|
51
52
|
case "paragraph":
|
|
52
53
|
return (_jsx("p", { children: _jsx(Inline, { nodes: block.nodes }) }));
|
|
54
|
+
case "plain":
|
|
55
|
+
return _jsx("p", { className: "whitespace-pre-wrap break-words", children: block.text });
|
|
56
|
+
case "heading": {
|
|
57
|
+
// Levels are relative to the message: the thread page owns h1 and h2.
|
|
58
|
+
// Blocks can arrive as JSON, so an out-of-range level is clamped rather than trusted.
|
|
59
|
+
const level = Math.min(Math.max(Math.round(block.level) || 1, 1), 3);
|
|
60
|
+
const H = ["h3", "h4", "h5"][level - 1];
|
|
61
|
+
return (_jsx(H, { className: cn("font-semibold tracking-tight text-foreground", level === 1 ? "mt-1 text-lg" : level === 2 ? "mt-1 text-base" : "text-sm"), children: _jsx(Inline, { nodes: block.nodes }) }));
|
|
62
|
+
}
|
|
53
63
|
case "list": {
|
|
54
64
|
const List = block.ordered ? "ol" : "ul";
|
|
55
65
|
return (_jsx(List, { className: cn("flex flex-col gap-1 pl-5", block.ordered ? "list-decimal" : "list-disc marker:text-faint-foreground"), children: block.items.map((item, i) => (_jsx("li", { className: "pl-0.5", children: _jsx(Inline, { nodes: item }) }, i))) }));
|
|
@@ -90,7 +100,10 @@ export function blocksToText(blocks, sources = []) {
|
|
|
90
100
|
.map((b) => {
|
|
91
101
|
switch (b.kind) {
|
|
92
102
|
case "paragraph":
|
|
103
|
+
case "heading":
|
|
93
104
|
return inline(b.nodes);
|
|
105
|
+
case "plain":
|
|
106
|
+
return b.text;
|
|
94
107
|
case "list":
|
|
95
108
|
return b.items.map((it, i) => `${b.ordered ? `${i + 1}.` : "-"} ${inline(it)}`).join("\n");
|
|
96
109
|
case "payload":
|
package/dist/lib/chat.d.ts
CHANGED
|
@@ -65,6 +65,12 @@ export type InlineNode = {
|
|
|
65
65
|
text: string;
|
|
66
66
|
}
|
|
67
67
|
/** A value read from the environment (a count, a setting). Shown with its source on hover. */
|
|
68
|
+
| {
|
|
69
|
+
kind: "measured";
|
|
70
|
+
text: string;
|
|
71
|
+
source?: string;
|
|
72
|
+
}
|
|
73
|
+
/** @deprecated Renamed `measured`; still rendered the same way until 2.0. */
|
|
68
74
|
| {
|
|
69
75
|
kind: "orgData";
|
|
70
76
|
text: string;
|
|
@@ -136,6 +142,20 @@ export type PayloadLanguage = string;
|
|
|
136
142
|
export type MessageBlock = {
|
|
137
143
|
kind: "paragraph";
|
|
138
144
|
nodes: InlineNode[];
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Text exactly as someone typed it: line breaks and spacing kept, nothing parsed. For a person's own words, so
|
|
148
|
+
* `*this*` stays `*this*` and a pasted list keeps its lines.
|
|
149
|
+
*/
|
|
150
|
+
| {
|
|
151
|
+
kind: "plain";
|
|
152
|
+
text: string;
|
|
153
|
+
}
|
|
154
|
+
/** A heading inside a reply. Levels are relative to the message, 1 the largest; the page's own headings sit above. */
|
|
155
|
+
| {
|
|
156
|
+
kind: "heading";
|
|
157
|
+
level: 1 | 2 | 3;
|
|
158
|
+
nodes: InlineNode[];
|
|
139
159
|
} | {
|
|
140
160
|
kind: "list";
|
|
141
161
|
ordered?: boolean;
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { InlineNode, MessageBlock } from "./chat.js";
|
|
2
2
|
/**
|
|
3
|
-
* Reads model text as safe markdown: paragraphs, lists and fenced code. Never HTML.
|
|
3
|
+
* Reads model text as safe markdown: paragraphs, lists and fenced code. Never HTML. `#` lines are bold paragraphs
|
|
4
|
+
* unless `headings` is set, when `#`, `##` and `###` (and deeper, as level 3) become heading blocks.
|
|
4
5
|
*/
|
|
5
|
-
export declare function markdownBlocks(text: string
|
|
6
|
+
export declare function markdownBlocks(text: string, { headings }?: {
|
|
7
|
+
headings?: boolean;
|
|
8
|
+
}): MessageBlock[];
|
|
6
9
|
/** `code`, **strong**, *em* and links (as text). Everything else stays literal. */
|
|
7
10
|
export declare function inline(text: string): InlineNode[];
|
|
11
|
+
/** A person's own words as one block, exactly as typed. Adapters use this for user messages: what someone typed is never read as markdown. */
|
|
12
|
+
export declare function plainBlocks(text: string): MessageBlock[];
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
*/
|
|
8
8
|
const fenceLanguage = (lang) => (lang === "json" ? "json" : lang === "xml" || lang === "html" ? "xml" : "text");
|
|
9
9
|
/**
|
|
10
|
-
* Reads model text as safe markdown: paragraphs, lists and fenced code. Never HTML.
|
|
10
|
+
* Reads model text as safe markdown: paragraphs, lists and fenced code. Never HTML. `#` lines are bold paragraphs
|
|
11
|
+
* unless `headings` is set, when `#`, `##` and `###` (and deeper, as level 3) become heading blocks.
|
|
11
12
|
*/
|
|
12
|
-
export function markdownBlocks(text) {
|
|
13
|
+
export function markdownBlocks(text, { headings = false } = {}) {
|
|
13
14
|
const blocks = [];
|
|
14
15
|
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
15
16
|
let para = [];
|
|
@@ -53,10 +54,12 @@ export function markdownBlocks(text) {
|
|
|
53
54
|
continue;
|
|
54
55
|
}
|
|
55
56
|
flushList();
|
|
56
|
-
const heading = /^\s
|
|
57
|
+
const heading = /^\s*(#{1,6})\s+(.*)$/.exec(line);
|
|
57
58
|
if (heading) {
|
|
58
59
|
flushPara();
|
|
59
|
-
blocks.push(
|
|
60
|
+
blocks.push(headings
|
|
61
|
+
? { kind: "heading", level: Math.min(heading[1].length, 3), nodes: inline(heading[2]) }
|
|
62
|
+
: { kind: "paragraph", nodes: [{ kind: "strong", text: heading[2] }] });
|
|
60
63
|
continue;
|
|
61
64
|
}
|
|
62
65
|
para.push(line.replace(/^\s*>\s?/, "").trim());
|
|
@@ -87,3 +90,9 @@ export function inline(text) {
|
|
|
87
90
|
nodes.push({ kind: "text", text: text.slice(at) });
|
|
88
91
|
return nodes;
|
|
89
92
|
}
|
|
93
|
+
/** A person's own words as one block, exactly as typed. Adapters use this for user messages: what someone typed is never read as markdown. */
|
|
94
|
+
export function plainBlocks(text) {
|
|
95
|
+
// Blank lines at the ends would show as empty space in the bubble; inside, they're the person's.
|
|
96
|
+
const t = text.replace(/\r\n/g, "\n").replace(/^\n+|\n+$/g, "");
|
|
97
|
+
return t.trim() ? [{ kind: "plain", text: t }] : [];
|
|
98
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@candor-ui/react",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Candor UI's free tier as an npm package: React components for agents that do real work, on shadcn/ui and Tailwind 4.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Methodic LLC",
|
|
7
7
|
"homepage": "https://candor.themethodic.com",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/MethodicDev/candor
|
|
11
|
-
"directory": "packages/react"
|
|
10
|
+
"url": "git+https://github.com/MethodicDev/candor.git"
|
|
12
11
|
},
|
|
13
12
|
"keywords": ["react", "agent", "chat", "ai", "shadcn", "tailwindcss", "claude", "ai-sdk", "ag-ui"],
|
|
14
13
|
"type": "module",
|