@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.
@@ -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: markdownBlocks(e.text) } });
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: markdownBlocks(resultText(m.content)) }, reply: byUser.get(m.id) });
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;
@@ -1,6 +1,6 @@
1
- import { inline, markdownBlocks } from "../lib/safe-markdown.js";
2
- // Re-exported for apps that used them from here. (Imported first: shadcn rewrites import paths, not export-from paths.)
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
- blocks.push(...markdownBlocks(part.text));
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: "doc", label: part.title ?? part.url, origin: host(part.url) });
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: "doc", label: part.title, origin: part.filename });
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: markdownBlocks(e.text) });
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":
@@ -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): MessageBlock[];
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*#{1,6}\s+(.*)$/.exec(line);
57
+ const heading = /^\s*(#{1,6})\s+(.*)$/.exec(line);
57
58
  if (heading) {
58
59
  flushPara();
59
- blocks.push({ kind: "paragraph", nodes: [{ kind: "strong", text: heading[1] }] });
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.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-ui.git",
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",