@tangle-network/ui 11.16.2 → 11.16.4
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/CHANGELOG.md +15 -0
- package/dist/chat/agent-timeline.js +3 -2
- package/dist/chat/chat-message.js +2 -2
- package/dist/chat/thinking-indicator.js +1 -1
- package/dist/chat/user-message.js +2 -2
- package/dist/markdown/code-block.js +9 -1
- package/dist/run/expandable-content.js +2 -2
- package/dist/run/expanded-tool-detail.js +3 -0
- package/dist/tool-previews/command-preview.js +7 -4
- package/package.json +2 -2
- package/src/chat/agent-timeline.test.tsx +0 -14
- package/src/chat/agent-timeline.tsx +18 -2
- package/src/chat/chat-message.tsx +2 -2
- package/src/chat/thinking-indicator.tsx +1 -1
- package/src/chat/user-message.tsx +2 -2
- package/src/markdown/code-block.tsx +14 -2
- package/src/run/expandable-content.tsx +8 -1
- package/src/run/expanded-tool-detail.tsx +6 -2
- package/src/tool-previews/command-preview.tsx +7 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @tangle-network/ui
|
|
2
2
|
|
|
3
|
+
## 11.16.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 11fbba5: Chat text renders at the 15px body size. The user bubble, assistant prose, legacy `ChatMessage` and thinking timer wrote their size tokens as `text-[var(--font-size-*)]`, which Tailwind compiles to a color, so assistant prose fell back to the Markdown default of 14px and the user bubble inherited 16px. They now use `text-[length:var(...)]`, and the dist check fails on the untyped form.
|
|
8
|
+
|
|
9
|
+
The streaming caret follows the last word instead of starting a new line. A code block that scrolls sideways takes keyboard focus as a named region (axe `scrollable-region-focusable`). The Show more toggle under a tool's output or error sits inside the card's inset instead of against its edge.
|
|
10
|
+
|
|
11
|
+
## 11.16.3
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 0132f5c: Terminal blocks wrap a long command, stdout and stderr at spaces first and break inside a token only when the token is longer than the line. `break-all` split `factory-playground` and `git` mid-word on a phone.
|
|
16
|
+
- a95c717: Each scrollable terminal output region carries `role="region"`, so its `aria-label` (stdout, stderr, error) names a landmark screen readers announce. A label on a bare `<pre>` is prohibited ARIA, and axe flagged it on the platform run page.
|
|
17
|
+
|
|
3
18
|
## 11.16.2
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
|
@@ -53,13 +53,14 @@ function spacingBefore(prev, next) {
|
|
|
53
53
|
return "mt-3";
|
|
54
54
|
}
|
|
55
55
|
const ROW_BLEED = "-mx-3";
|
|
56
|
+
const STREAMING_CARET = "[&>:last-child:not(ul,ol)]:after:ml-0.5 [&>:last-child:not(ul,ol)]:after:inline-block [&>:last-child:not(ul,ol)]:after:h-[1em] [&>:last-child:not(ul,ol)]:after:w-0.5 [&>:last-child:not(ul,ol)]:after:animate-pulse [&>:last-child:not(ul,ol)]:after:rounded-full [&>:last-child:not(ul,ol)]:after:bg-muted-foreground [&>:last-child:not(ul,ol)]:after:align-text-bottom [&>:last-child:not(ul,ol)]:after:content-[''] [&>:is(ul,ol):last-child>li:last-child]:after:ml-0.5 [&>:is(ul,ol):last-child>li:last-child]:after:inline-block [&>:is(ul,ol):last-child>li:last-child]:after:h-[1em] [&>:is(ul,ol):last-child>li:last-child]:after:w-0.5 [&>:is(ul,ol):last-child>li:last-child]:after:animate-pulse [&>:is(ul,ol):last-child>li:last-child]:after:rounded-full [&>:is(ul,ol):last-child>li:last-child]:after:bg-muted-foreground [&>:is(ul,ol):last-child>li:last-child]:after:align-text-bottom [&>:is(ul,ol):last-child>li:last-child]:after:content-['']";
|
|
56
57
|
function AssistantMessage({ item }) {
|
|
57
58
|
return /* @__PURE__ */ jsxs("div", { children: [
|
|
58
59
|
item.content && /* @__PURE__ */ jsx(Markdown, {
|
|
59
|
-
className: "tangle-prose text-[var(--font-size-base)] leading-[1.5]",
|
|
60
|
+
className: cn("tangle-prose text-[length:var(--font-size-base)] leading-[1.5]", item.isStreaming && STREAMING_CARET),
|
|
60
61
|
children: item.content
|
|
61
62
|
}),
|
|
62
|
-
item.isStreaming && /* @__PURE__ */ jsx("span", {
|
|
63
|
+
item.isStreaming && !item.content && /* @__PURE__ */ jsx("span", {
|
|
63
64
|
"aria-hidden": true,
|
|
64
65
|
"data-streaming-caret": "",
|
|
65
66
|
className: "ml-0.5 inline-block h-[1em] w-0.5 animate-pulse rounded-full bg-muted-foreground align-text-bottom"
|
|
@@ -19,10 +19,10 @@ function ChatMessage({ role, content, toolCalls, isStreaming, timestamp, classNa
|
|
|
19
19
|
}), /* @__PURE__ */ jsxs("div", {
|
|
20
20
|
className: cn("min-w-0 max-w-[85%] space-y-1", isUser ? "rounded-[var(--radius-lg)] bg-foreground px-[var(--chat-message-px)] py-[var(--chat-message-py)] text-background" : isAssistant ? "text-foreground" : "rounded-[var(--radius-lg)] border border-border bg-card px-[var(--chat-message-px)] py-[var(--chat-message-py)]"),
|
|
21
21
|
children: [isUser ? /* @__PURE__ */ jsx("div", {
|
|
22
|
-
className: "whitespace-pre-wrap text-[var(--font-size-base)] leading-[var(--line-height-base)]",
|
|
22
|
+
className: "whitespace-pre-wrap text-[length:var(--font-size-base)] leading-[var(--line-height-base)]",
|
|
23
23
|
children: content
|
|
24
24
|
}) : /* @__PURE__ */ jsxs(Fragment, { children: [content && /* @__PURE__ */ jsx(Markdown, {
|
|
25
|
-
className: "tangle-prose text-[var(--font-size-base)] leading-[var(--line-height-base)]",
|
|
25
|
+
className: "tangle-prose text-[length:var(--font-size-base)] leading-[var(--line-height-base)]",
|
|
26
26
|
children: content
|
|
27
27
|
}), isStreaming && /* @__PURE__ */ jsx("span", { className: "ml-0.5 inline-block h-4 w-2 animate-pulse rounded-sm bg-[var(--brand-cool)] align-text-bottom" })] }), toolCalls]
|
|
28
28
|
})]
|
|
@@ -29,7 +29,7 @@ function ThinkingIndicator({ className }) {
|
|
|
29
29
|
})
|
|
30
30
|
]
|
|
31
31
|
}), elapsed > 3 && /* @__PURE__ */ jsxs("span", {
|
|
32
|
-
className: "text-[var(--font-size-xs)] tabular-nums text-muted-foreground",
|
|
32
|
+
className: "text-[length:var(--font-size-xs)] tabular-nums text-muted-foreground",
|
|
33
33
|
children: [elapsed, "s"]
|
|
34
34
|
})]
|
|
35
35
|
});
|
|
@@ -23,10 +23,10 @@ function UserBubble({ text, timestamp, actions }) {
|
|
|
23
23
|
className: "relative w-full min-w-0 rounded-2xl bg-foreground px-4 py-3 text-background",
|
|
24
24
|
children: [timestamp ? /* @__PURE__ */ jsx("span", {
|
|
25
25
|
"data-user-message-time": "",
|
|
26
|
-
className: "pointer-events-none absolute right-full top-1/2 mr-3 -translate-y-1/2 whitespace-nowrap text-[var(--font-size-xs)] text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100",
|
|
26
|
+
className: "pointer-events-none absolute right-full top-1/2 mr-3 -translate-y-1/2 whitespace-nowrap text-[length:var(--font-size-xs)] text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100",
|
|
27
27
|
children: formatTime(timestamp)
|
|
28
28
|
}) : null, /* @__PURE__ */ jsx("div", {
|
|
29
|
-
className: "break-words whitespace-pre-wrap text-[var(--font-size-base)] leading-[1.5]",
|
|
29
|
+
className: "break-words whitespace-pre-wrap text-[length:var(--font-size-base)] leading-[1.5]",
|
|
30
30
|
children: text
|
|
31
31
|
})]
|
|
32
32
|
}), actions ? /* @__PURE__ */ jsx(MessageActions, { children: actions }) : null]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { focusRing } from "../lib/focus.js";
|
|
1
|
+
import { focusRing, focusRingInset } from "../lib/focus.js";
|
|
2
2
|
import { cn } from "../lib/utils.js";
|
|
3
3
|
import { memo, useCallback, useEffect, useRef, useState } from "react";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -93,6 +93,12 @@ const CodeBlock = memo(({ code, language, label, showLineNumbers = false, light,
|
|
|
93
93
|
overflowX: "auto"
|
|
94
94
|
};
|
|
95
95
|
const codeTagProps = { style: { fontFamily: "var(--font-mono, 'JetBrains Mono', ui-monospace, monospace)" } };
|
|
96
|
+
const scrollRegion = {
|
|
97
|
+
tabIndex: 0,
|
|
98
|
+
role: "region",
|
|
99
|
+
"aria-label": headerLabel ? `${headerLabel} code` : "Code",
|
|
100
|
+
className: focusRingInset
|
|
101
|
+
};
|
|
96
102
|
return /* @__PURE__ */ jsxs("div", {
|
|
97
103
|
className: cn("group relative overflow-hidden rounded-lg border font-mono", "bg-card border-border", className),
|
|
98
104
|
...props,
|
|
@@ -121,8 +127,10 @@ const CodeBlock = memo(({ code, language, label, showLineNumbers = false, light,
|
|
|
121
127
|
customStyle,
|
|
122
128
|
codeTagProps,
|
|
123
129
|
wrapLines: false,
|
|
130
|
+
...scrollRegion,
|
|
124
131
|
children: code
|
|
125
132
|
}) : /* @__PURE__ */ jsx("pre", {
|
|
133
|
+
...scrollRegion,
|
|
126
134
|
style: {
|
|
127
135
|
...syntaxTheme.hljs,
|
|
128
136
|
...customStyle
|
|
@@ -7,7 +7,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
7
7
|
* "Show more / Show less" toggle. Content shorter than the cap renders
|
|
8
8
|
* as-is with no toggle.
|
|
9
9
|
*/
|
|
10
|
-
function ExpandableContent({ children, collapsedMaxPx = 180, fadeClassName = "from-muted", className }) {
|
|
10
|
+
function ExpandableContent({ children, collapsedMaxPx = 180, fadeClassName = "from-muted", className, toggleClassName }) {
|
|
11
11
|
const [expanded, setExpanded] = useState(false);
|
|
12
12
|
const [overflowing, setOverflowing] = useState(false);
|
|
13
13
|
const contentRef = useRef(null);
|
|
@@ -32,7 +32,7 @@ function ExpandableContent({ children, collapsedMaxPx = 180, fadeClassName = "fr
|
|
|
32
32
|
}), overflowing ? /* @__PURE__ */ jsx("button", {
|
|
33
33
|
type: "button",
|
|
34
34
|
onClick: () => setExpanded((value) => !value),
|
|
35
|
-
className: "mt-1 text-[11px] font-medium text-muted-foreground transition-colors hover:text-foreground",
|
|
35
|
+
className: cn("mt-1 text-[11px] font-medium text-muted-foreground transition-colors hover:text-foreground", toggleClassName),
|
|
36
36
|
children: expanded ? "Show less" : "Show more"
|
|
37
37
|
}) : null]
|
|
38
38
|
});
|
|
@@ -13,6 +13,7 @@ import { memo } from "react";
|
|
|
13
13
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
14
14
|
import { AlertCircle, ArrowLeft, ArrowRight, FileText, Loader2 } from "lucide-react";
|
|
15
15
|
//#region src/run/expanded-tool-detail.tsx
|
|
16
|
+
const FLUSH_TOGGLE = "mx-3 mb-2";
|
|
16
17
|
const EXT_LANG = {
|
|
17
18
|
ts: "typescript",
|
|
18
19
|
tsx: "tsx",
|
|
@@ -131,6 +132,7 @@ const ExpandedToolDetail = memo(({ part }) => {
|
|
|
131
132
|
})]
|
|
132
133
|
}), /* @__PURE__ */ jsx(ExpandableContent, {
|
|
133
134
|
fadeClassName: "from-card",
|
|
135
|
+
toggleClassName: FLUSH_TOGGLE,
|
|
134
136
|
children: /* @__PURE__ */ jsx(CodeBlock, {
|
|
135
137
|
code: outputStr.length > 2e3 ? outputStr.slice(0, 2e3) + "\n...(truncated)" : outputStr,
|
|
136
138
|
language: "json",
|
|
@@ -148,6 +150,7 @@ const ExpandedToolDetail = memo(({ part }) => {
|
|
|
148
150
|
})]
|
|
149
151
|
}), /* @__PURE__ */ jsx(ExpandableContent, {
|
|
150
152
|
fadeClassName: "from-[var(--surface-danger-bg)]",
|
|
153
|
+
toggleClassName: FLUSH_TOGGLE,
|
|
151
154
|
children: /* @__PURE__ */ jsx("pre", {
|
|
152
155
|
className: "bg-[var(--surface-danger-bg)] p-3 text-xs font-mono whitespace-pre-wrap break-all text-[var(--surface-danger-text)]",
|
|
153
156
|
children: error
|
|
@@ -138,7 +138,7 @@ const CommandPreview = memo(({ part, defaultExpanded = false, className }) => {
|
|
|
138
138
|
children: "$"
|
|
139
139
|
}),
|
|
140
140
|
/* @__PURE__ */ jsx("code", {
|
|
141
|
-
className: cn("min-w-0 flex-1 whitespace-pre-wrap
|
|
141
|
+
className: cn("min-w-0 flex-1 whitespace-pre-wrap [overflow-wrap:anywhere]", expanded ? "" : "line-clamp-2"),
|
|
142
142
|
children: command
|
|
143
143
|
}),
|
|
144
144
|
/* @__PURE__ */ jsx(ExitBadge, {
|
|
@@ -172,20 +172,23 @@ const CommandPreview = memo(({ part, defaultExpanded = false, className }) => {
|
|
|
172
172
|
children: [
|
|
173
173
|
output.stdout ? /* @__PURE__ */ jsx("pre", {
|
|
174
174
|
tabIndex: 0,
|
|
175
|
+
role: "region",
|
|
175
176
|
"aria-label": "stdout",
|
|
176
|
-
className: cn("max-h-80 overflow-auto whitespace-pre-wrap
|
|
177
|
+
className: cn("max-h-80 overflow-auto whitespace-pre-wrap [overflow-wrap:anywhere] px-3 py-2 leading-relaxed", focusRing),
|
|
177
178
|
children: output.stdout
|
|
178
179
|
}) : null,
|
|
179
180
|
output.stderr ? /* @__PURE__ */ jsx("pre", {
|
|
180
181
|
tabIndex: 0,
|
|
182
|
+
role: "region",
|
|
181
183
|
"aria-label": "stderr",
|
|
182
|
-
className: cn("max-h-80 overflow-auto whitespace-pre-wrap
|
|
184
|
+
className: cn("max-h-80 overflow-auto whitespace-pre-wrap [overflow-wrap:anywhere] border-t border-white/10 px-3 py-2 leading-relaxed text-[var(--surface-danger-text)]", focusRing),
|
|
183
185
|
children: output.stderr
|
|
184
186
|
}) : null,
|
|
185
187
|
errorText ? /* @__PURE__ */ jsx("pre", {
|
|
186
188
|
tabIndex: 0,
|
|
189
|
+
role: "region",
|
|
187
190
|
"aria-label": "error",
|
|
188
|
-
className: cn("max-h-80 overflow-auto whitespace-pre-wrap
|
|
191
|
+
className: cn("max-h-80 overflow-auto whitespace-pre-wrap [overflow-wrap:anywhere] border-t border-white/10 px-3 py-2 leading-relaxed text-[var(--surface-danger-text)]", focusRing),
|
|
189
192
|
children: errorText
|
|
190
193
|
}) : null
|
|
191
194
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/ui",
|
|
3
|
-
"version": "11.16.
|
|
3
|
+
"version": "11.16.4",
|
|
4
4
|
"description": "Generic React UI components for Tangle products — primitives, chat, run, files, editor, markdown.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
"peerDependencies": {
|
|
143
143
|
"@hocuspocus/provider": ">=2.15.0 <5.0.0",
|
|
144
144
|
"@nanostores/react": ">=0.8.4 <3.0.0",
|
|
145
|
-
"@tangle-network/brand": "^1.10.
|
|
145
|
+
"@tangle-network/brand": "^1.10.1",
|
|
146
146
|
"@tiptap/core": ">=3.20.4 <4.0.0",
|
|
147
147
|
"@tiptap/extension-collaboration": ">=3.20.4 <4.0.0",
|
|
148
148
|
"@tiptap/extension-collaboration-caret": ">=3.7.0 <4.0.0",
|
|
@@ -253,20 +253,6 @@ describe("AgentTimeline tool actions", () => {
|
|
|
253
253
|
expect(classes[4]).toContain("mt-4") // prose → user
|
|
254
254
|
})
|
|
255
255
|
|
|
256
|
-
it("softens the streaming cursor to a muted caret", () => {
|
|
257
|
-
const { container } = render(
|
|
258
|
-
<AgentTimeline
|
|
259
|
-
items={[
|
|
260
|
-
{ id: "a-1", kind: "message", role: "assistant", content: "Typing", isStreaming: true },
|
|
261
|
-
]}
|
|
262
|
-
/>,
|
|
263
|
-
)
|
|
264
|
-
const caret = container.querySelector("[data-streaming-caret]")
|
|
265
|
-
expect(caret).not.toBeNull()
|
|
266
|
-
expect(caret!.className).toContain("bg-muted-foreground")
|
|
267
|
-
expect(caret!.className).not.toContain("bg-primary")
|
|
268
|
-
})
|
|
269
|
-
|
|
270
256
|
it("renders the source part's real input in the expanded detail", async () => {
|
|
271
257
|
const user = userEvent.setup()
|
|
272
258
|
const probePart: ToolPart = {
|
|
@@ -162,13 +162,29 @@ function spacingBefore(prev: StepKind | undefined, next: StepKind): string {
|
|
|
162
162
|
// wider than the bleed, so nothing clips.
|
|
163
163
|
const ROW_BLEED = "-mx-3";
|
|
164
164
|
|
|
165
|
+
// The streaming caret ends the last line of text. It is drawn as the `::after`
|
|
166
|
+
// of the prose's last block (the last item of a trailing list), so it follows
|
|
167
|
+
// the final word instead of starting a line of its own below the paragraph.
|
|
168
|
+
const STREAMING_CARET =
|
|
169
|
+
"[&>:last-child:not(ul,ol)]:after:ml-0.5 [&>:last-child:not(ul,ol)]:after:inline-block [&>:last-child:not(ul,ol)]:after:h-[1em] [&>:last-child:not(ul,ol)]:after:w-0.5 [&>:last-child:not(ul,ol)]:after:animate-pulse [&>:last-child:not(ul,ol)]:after:rounded-full [&>:last-child:not(ul,ol)]:after:bg-muted-foreground [&>:last-child:not(ul,ol)]:after:align-text-bottom [&>:last-child:not(ul,ol)]:after:content-[''] " +
|
|
170
|
+
"[&>:is(ul,ol):last-child>li:last-child]:after:ml-0.5 [&>:is(ul,ol):last-child>li:last-child]:after:inline-block [&>:is(ul,ol):last-child>li:last-child]:after:h-[1em] [&>:is(ul,ol):last-child>li:last-child]:after:w-0.5 [&>:is(ul,ol):last-child>li:last-child]:after:animate-pulse [&>:is(ul,ol):last-child>li:last-child]:after:rounded-full [&>:is(ul,ol):last-child>li:last-child]:after:bg-muted-foreground [&>:is(ul,ol):last-child>li:last-child]:after:align-text-bottom [&>:is(ul,ol):last-child>li:last-child]:after:content-['']";
|
|
171
|
+
|
|
165
172
|
function AssistantMessage({ item }: { item: AgentTimelineMessageItem }) {
|
|
166
173
|
return (
|
|
167
174
|
<div>
|
|
168
175
|
{item.content && (
|
|
169
|
-
<Markdown
|
|
176
|
+
<Markdown
|
|
177
|
+
// `length:` types the token: a bare `text-[var(--x)]` compiles to a
|
|
178
|
+
// color, which left the reading size at the Markdown default.
|
|
179
|
+
className={cn(
|
|
180
|
+
"tangle-prose text-[length:var(--font-size-base)] leading-[1.5]",
|
|
181
|
+
item.isStreaming && STREAMING_CARET,
|
|
182
|
+
)}
|
|
183
|
+
>
|
|
184
|
+
{item.content}
|
|
185
|
+
</Markdown>
|
|
170
186
|
)}
|
|
171
|
-
{item.isStreaming && (
|
|
187
|
+
{item.isStreaming && !item.content && (
|
|
172
188
|
<span
|
|
173
189
|
aria-hidden
|
|
174
190
|
data-streaming-caret=""
|
|
@@ -75,12 +75,12 @@ export function ChatMessage({
|
|
|
75
75
|
)}
|
|
76
76
|
>
|
|
77
77
|
{isUser ? (
|
|
78
|
-
<div className="whitespace-pre-wrap text-[var(--font-size-base)] leading-[var(--line-height-base)]">
|
|
78
|
+
<div className="whitespace-pre-wrap text-[length:var(--font-size-base)] leading-[var(--line-height-base)]">
|
|
79
79
|
{content}
|
|
80
80
|
</div>
|
|
81
81
|
) : (
|
|
82
82
|
<>
|
|
83
|
-
{content && <Markdown className="tangle-prose text-[var(--font-size-base)] leading-[var(--line-height-base)]">{content}</Markdown>}
|
|
83
|
+
{content && <Markdown className="tangle-prose text-[length:var(--font-size-base)] leading-[var(--line-height-base)]">{content}</Markdown>}
|
|
84
84
|
{isStreaming && (
|
|
85
85
|
<span className="ml-0.5 inline-block h-4 w-2 animate-pulse rounded-sm bg-[var(--brand-cool)] align-text-bottom" />
|
|
86
86
|
)}
|
|
@@ -25,7 +25,7 @@ export function ThinkingIndicator({ className }: ThinkingIndicatorProps) {
|
|
|
25
25
|
<span className="h-[var(--indicator-dot-size)] w-[var(--indicator-dot-size)] animate-bounce rounded-full bg-[var(--brand-glow)]" style={{ animationDelay: "300ms" }} />
|
|
26
26
|
</div>
|
|
27
27
|
{elapsed > 3 && (
|
|
28
|
-
<span className="text-[var(--font-size-xs)] tabular-nums text-muted-foreground">
|
|
28
|
+
<span className="text-[length:var(--font-size-xs)] tabular-nums text-muted-foreground">
|
|
29
29
|
{elapsed}s
|
|
30
30
|
</span>
|
|
31
31
|
)}
|
|
@@ -37,12 +37,12 @@ function UserBubble({ text, timestamp, actions }: {
|
|
|
37
37
|
{timestamp ? (
|
|
38
38
|
<span
|
|
39
39
|
data-user-message-time=""
|
|
40
|
-
className="pointer-events-none absolute right-full top-1/2 mr-3 -translate-y-1/2 whitespace-nowrap text-[var(--font-size-xs)] text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100"
|
|
40
|
+
className="pointer-events-none absolute right-full top-1/2 mr-3 -translate-y-1/2 whitespace-nowrap text-[length:var(--font-size-xs)] text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100"
|
|
41
41
|
>
|
|
42
42
|
{formatTime(timestamp)}
|
|
43
43
|
</span>
|
|
44
44
|
) : null}
|
|
45
|
-
<div className="break-words whitespace-pre-wrap text-[var(--font-size-base)] leading-[1.5]">
|
|
45
|
+
<div className="break-words whitespace-pre-wrap text-[length:var(--font-size-base)] leading-[1.5]">
|
|
46
46
|
{text}
|
|
47
47
|
</div>
|
|
48
48
|
</div>
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "react";
|
|
10
10
|
import type SyntaxHighlighter from "react-syntax-highlighter";
|
|
11
11
|
import { Check, Copy } from "lucide-react";
|
|
12
|
-
import { focusRing } from "../lib/focus";
|
|
12
|
+
import { focusRing, focusRingInset } from "../lib/focus";
|
|
13
13
|
import { cn } from "../lib/utils";
|
|
14
14
|
|
|
15
15
|
// Leave variables unresolved: the browser resolves them on each token span,
|
|
@@ -118,6 +118,17 @@ export const CodeBlock = memo(
|
|
|
118
118
|
overflowX: "auto" as const,
|
|
119
119
|
};
|
|
120
120
|
const codeTagProps = { style: { fontFamily: "var(--font-mono, 'JetBrains Mono', ui-monospace, monospace)" } };
|
|
121
|
+
// A long line scrolls the block sideways, which a keyboard reaches only if
|
|
122
|
+
// the block takes focus (axe scrollable-region-focusable). A label on a bare
|
|
123
|
+
// <pre> is prohibited ARIA, so the block is a named region, as the terminal
|
|
124
|
+
// output regions are.
|
|
125
|
+
const scrollRegion = {
|
|
126
|
+
tabIndex: 0,
|
|
127
|
+
role: "region",
|
|
128
|
+
"aria-label": headerLabel ? `${headerLabel} code` : "Code",
|
|
129
|
+
// Inset: the rounded, overflow-clipped card would cut an outer ring.
|
|
130
|
+
className: focusRingInset,
|
|
131
|
+
} as const;
|
|
121
132
|
|
|
122
133
|
return (
|
|
123
134
|
<div
|
|
@@ -151,11 +162,12 @@ export const CodeBlock = memo(
|
|
|
151
162
|
customStyle={customStyle}
|
|
152
163
|
codeTagProps={codeTagProps}
|
|
153
164
|
wrapLines={false}
|
|
165
|
+
{...scrollRegion}
|
|
154
166
|
>
|
|
155
167
|
{code}
|
|
156
168
|
</Highlighter>
|
|
157
169
|
) : (
|
|
158
|
-
<pre style={{ ...syntaxTheme.hljs, ...customStyle }}>
|
|
170
|
+
<pre {...scrollRegion} style={{ ...syntaxTheme.hljs, ...customStyle }}>
|
|
159
171
|
<code style={{ ...codeTagProps.style, whiteSpace: "pre" }}>{code}</code>
|
|
160
172
|
</pre>
|
|
161
173
|
)}
|
|
@@ -11,6 +11,9 @@ export interface ExpandableContentProps {
|
|
|
11
11
|
*/
|
|
12
12
|
fadeClassName?: string;
|
|
13
13
|
className?: string;
|
|
14
|
+
/** Classes for the Show more / Show less toggle, e.g. an inset when the
|
|
15
|
+
* content sits flush against its card's edges. */
|
|
16
|
+
toggleClassName?: string;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
/**
|
|
@@ -23,6 +26,7 @@ export function ExpandableContent({
|
|
|
23
26
|
collapsedMaxPx = 180,
|
|
24
27
|
fadeClassName = "from-muted",
|
|
25
28
|
className,
|
|
29
|
+
toggleClassName,
|
|
26
30
|
}: ExpandableContentProps) {
|
|
27
31
|
const [expanded, setExpanded] = useState(false);
|
|
28
32
|
const [overflowing, setOverflowing] = useState(false);
|
|
@@ -63,7 +67,10 @@ export function ExpandableContent({
|
|
|
63
67
|
<button
|
|
64
68
|
type="button"
|
|
65
69
|
onClick={() => setExpanded((value) => !value)}
|
|
66
|
-
className=
|
|
70
|
+
className={cn(
|
|
71
|
+
"mt-1 text-[11px] font-medium text-muted-foreground transition-colors hover:text-foreground",
|
|
72
|
+
toggleClassName,
|
|
73
|
+
)}
|
|
67
74
|
>
|
|
68
75
|
{expanded ? "Show less" : "Show more"}
|
|
69
76
|
</button>
|
|
@@ -23,6 +23,10 @@ export interface ExpandedToolDetailProps {
|
|
|
23
23
|
part: ToolPart;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// The clamped output sits flush against its card's edges; the toggle under it
|
|
27
|
+
// takes the card's 12px content inset so it does not touch the border.
|
|
28
|
+
const FLUSH_TOGGLE = "mx-3 mb-2";
|
|
29
|
+
|
|
26
30
|
const EXT_LANG: Record<string, string> = {
|
|
27
31
|
ts: "typescript", tsx: "tsx", js: "javascript", jsx: "jsx",
|
|
28
32
|
rs: "rust", py: "python", go: "go", rb: "ruby",
|
|
@@ -147,7 +151,7 @@ export const ExpandedToolDetail = memo(({ part }: ExpandedToolDetailProps) => {
|
|
|
147
151
|
Output
|
|
148
152
|
</span>
|
|
149
153
|
</div>
|
|
150
|
-
<ExpandableContent fadeClassName="from-card">
|
|
154
|
+
<ExpandableContent fadeClassName="from-card" toggleClassName={FLUSH_TOGGLE}>
|
|
151
155
|
<CodeBlock
|
|
152
156
|
code={
|
|
153
157
|
outputStr.length > 2000
|
|
@@ -170,7 +174,7 @@ export const ExpandedToolDetail = memo(({ part }: ExpandedToolDetailProps) => {
|
|
|
170
174
|
Error
|
|
171
175
|
</span>
|
|
172
176
|
</div>
|
|
173
|
-
<ExpandableContent fadeClassName="from-[var(--surface-danger-bg)]">
|
|
177
|
+
<ExpandableContent fadeClassName="from-[var(--surface-danger-bg)]" toggleClassName={FLUSH_TOGGLE}>
|
|
174
178
|
<pre className="bg-[var(--surface-danger-bg)] p-3 text-xs font-mono whitespace-pre-wrap break-all text-[var(--surface-danger-text)]">
|
|
175
179
|
{error}
|
|
176
180
|
</pre>
|
|
@@ -191,7 +191,7 @@ export const CommandPreview = memo(
|
|
|
191
191
|
</span>
|
|
192
192
|
<code
|
|
193
193
|
className={cn(
|
|
194
|
-
"min-w-0 flex-1 whitespace-pre-wrap
|
|
194
|
+
"min-w-0 flex-1 whitespace-pre-wrap [overflow-wrap:anywhere]",
|
|
195
195
|
expanded ? "" : "line-clamp-2",
|
|
196
196
|
)}
|
|
197
197
|
>
|
|
@@ -237,9 +237,10 @@ export const CommandPreview = memo(
|
|
|
237
237
|
{output.stdout ? (
|
|
238
238
|
<pre
|
|
239
239
|
tabIndex={0}
|
|
240
|
+
role="region"
|
|
240
241
|
aria-label="stdout"
|
|
241
242
|
className={cn(
|
|
242
|
-
"max-h-80 overflow-auto whitespace-pre-wrap
|
|
243
|
+
"max-h-80 overflow-auto whitespace-pre-wrap [overflow-wrap:anywhere] px-3 py-2 leading-relaxed",
|
|
243
244
|
focusRing,
|
|
244
245
|
)}
|
|
245
246
|
>
|
|
@@ -249,9 +250,10 @@ export const CommandPreview = memo(
|
|
|
249
250
|
{output.stderr ? (
|
|
250
251
|
<pre
|
|
251
252
|
tabIndex={0}
|
|
253
|
+
role="region"
|
|
252
254
|
aria-label="stderr"
|
|
253
255
|
className={cn(
|
|
254
|
-
"max-h-80 overflow-auto whitespace-pre-wrap
|
|
256
|
+
"max-h-80 overflow-auto whitespace-pre-wrap [overflow-wrap:anywhere] border-t border-white/10 px-3 py-2 leading-relaxed text-[var(--surface-danger-text)]",
|
|
255
257
|
focusRing,
|
|
256
258
|
)}
|
|
257
259
|
>
|
|
@@ -261,9 +263,10 @@ export const CommandPreview = memo(
|
|
|
261
263
|
{errorText ? (
|
|
262
264
|
<pre
|
|
263
265
|
tabIndex={0}
|
|
266
|
+
role="region"
|
|
264
267
|
aria-label="error"
|
|
265
268
|
className={cn(
|
|
266
|
-
"max-h-80 overflow-auto whitespace-pre-wrap
|
|
269
|
+
"max-h-80 overflow-auto whitespace-pre-wrap [overflow-wrap:anywhere] border-t border-white/10 px-3 py-2 leading-relaxed text-[var(--surface-danger-text)]",
|
|
267
270
|
focusRing,
|
|
268
271
|
)}
|
|
269
272
|
>
|