@tangle-network/ui 11.33.0 → 11.34.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/CHANGELOG.md +13 -0
- package/dist/editor/document-editor-pane.d.ts +7 -1
- package/dist/editor/document-editor-pane.js +27 -12
- package/dist/editor/markdown-document-editor.js +30 -12
- package/dist/editor/source-markdown-editor.js +256 -0
- package/dist/editor/source-markdown.js +495 -0
- package/dist/files/file-artifact-pane.d.ts +2 -0
- package/dist/files/file-artifact-pane.js +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/markdown/markdown-table.js +1 -1
- package/dist/run/inline-thinking-item.d.ts +7 -1
- package/dist/run/inline-thinking-item.js +6 -5
- package/dist/utils/format.d.ts +6 -0
- package/dist/utils/format.js +9 -1
- package/dist/utils.d.ts +2 -2
- package/dist/utils.js +2 -2
- package/package.json +2 -2
- package/src/editor/document-editor-pane.tsx +54 -16
- package/src/editor/markdown-document-editor.tsx +33 -18
- package/src/editor/source-markdown-editor.test.tsx +156 -0
- package/src/editor/source-markdown-editor.ts +296 -0
- package/src/editor/source-markdown.fixtures.ts +90 -0
- package/src/editor/source-markdown.test.ts +235 -0
- package/src/editor/source-markdown.ts +627 -0
- package/src/files/file-artifact-pane.test.tsx +34 -1
- package/src/files/file-artifact-pane.tsx +3 -0
- package/src/markdown/markdown-table.test.tsx +9 -0
- package/src/markdown/markdown-table.tsx +5 -1
- package/src/run/inline-thinking-item.test.tsx +29 -0
- package/src/run/inline-thinking-item.tsx +11 -4
- package/src/utils/format.test.ts +16 -1
- package/src/utils/format.ts +17 -0
- package/src/utils/index.ts +1 -1
|
@@ -45,6 +45,15 @@ describe("Markdown tables", () => {
|
|
|
45
45
|
expect(document.body.innerHTML).not.toContain("text-xs");
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
it("contains its screen-reader status line, so a table deep in a scrolling panel cannot stretch the page", () => {
|
|
49
|
+
render(<Markdown>{TABLE}</Markdown>);
|
|
50
|
+
const status = screen.getByRole("status");
|
|
51
|
+
expect(status.className).toContain("sr-only");
|
|
52
|
+
// The status is absolutely positioned; the card it sits in must be its containing block.
|
|
53
|
+
const card = status.closest(".not-prose") as HTMLElement;
|
|
54
|
+
expect(card.className.split(" ")).toEqual(expect.arrayContaining(["relative", "overflow-hidden"]));
|
|
55
|
+
});
|
|
56
|
+
|
|
48
57
|
it("copies the table as CSV with quoting and formula-safe cells", async () => {
|
|
49
58
|
const writeText = vi.fn().mockResolvedValue(undefined);
|
|
50
59
|
Object.defineProperty(navigator, "clipboard", { value: { writeText }, configurable: true });
|
|
@@ -138,7 +138,11 @@ export function MarkdownTable({ node, className, children, ...props }: Component
|
|
|
138
138
|
return { csv: tableCsv([...header, ...body]), kinds: columnKinds(body), rowCount: body.length };
|
|
139
139
|
}, [node]);
|
|
140
140
|
return (
|
|
141
|
-
|
|
141
|
+
// `relative` makes the card the containing block of the copy control's
|
|
142
|
+
// screen-reader status line. Without it that absolutely positioned line
|
|
143
|
+
// resolves against the nearest positioned ancestor, often the page, and a
|
|
144
|
+
// table far down a scrolling panel stretches the page past the viewport.
|
|
145
|
+
<div className="not-prose relative my-4 overflow-hidden rounded-lg border border-border bg-card">
|
|
142
146
|
<div className="flex items-center justify-between gap-2 border-b border-border px-3 py-1">
|
|
143
147
|
<span className="text-sm text-muted-foreground">{rowCount === 1 ? "1 row" : `${rowCount} rows`}</span>
|
|
144
148
|
<CopyCsvButton csv={csv} />
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import type { ReasoningPart } from "../types/parts";
|
|
4
|
+
import { InlineThinkingItem } from "./inline-thinking-item";
|
|
5
|
+
|
|
6
|
+
const TEXT = "**Ensuring workspace configuration** I need to be cautious because the saved file is not verifiable.";
|
|
7
|
+
|
|
8
|
+
function part(active: boolean): ReasoningPart {
|
|
9
|
+
return { type: "reasoning", text: TEXT, time: active ? { start: Date.now() } : { start: 0, end: 4_000 } };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("InlineThinkingItem", () => {
|
|
13
|
+
it("previews the reasoning as plain text, never raw markdown marks", () => {
|
|
14
|
+
const { container } = render(<InlineThinkingItem part={part(false)} />);
|
|
15
|
+
expect(container.textContent).toContain("Ensuring workspace configuration I need to be cautious");
|
|
16
|
+
expect(container.textContent).not.toContain("**");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("opens while the model thinks, by default", () => {
|
|
20
|
+
render(<InlineThinkingItem part={part(true)} />);
|
|
21
|
+
expect(screen.getByRole("button", { expanded: true })).toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("stays closed while the model thinks when the product keeps deliberation out of view", () => {
|
|
25
|
+
render(<InlineThinkingItem part={part(true)} openWhileActive={false} />);
|
|
26
|
+
expect(screen.getByRole("button", { expanded: false })).toBeInTheDocument();
|
|
27
|
+
expect(screen.getByText("Thinking…")).toBeInTheDocument();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { memo, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { Brain } from "lucide-react";
|
|
3
|
-
import { truncateText } from "../utils/format";
|
|
3
|
+
import { plainTextFromMarkdown, truncateText } from "../utils/format";
|
|
4
4
|
import type { ReasoningPart } from "../types/parts";
|
|
5
5
|
import { Markdown } from "../markdown/markdown";
|
|
6
6
|
import { TextShimmer } from "../primitives/text-shimmer";
|
|
@@ -11,6 +11,12 @@ export interface InlineThinkingItemProps {
|
|
|
11
11
|
part: ReasoningPart;
|
|
12
12
|
defaultOpen?: boolean;
|
|
13
13
|
autoCollapse?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Open the row while the model is still thinking. A product that keeps
|
|
16
|
+
* deliberation out of the reader's way passes false: the row stays as
|
|
17
|
+
* `defaultOpen` left it, with its one-line preview, until the reader opens it.
|
|
18
|
+
*/
|
|
19
|
+
openWhileActive?: boolean;
|
|
14
20
|
className?: string;
|
|
15
21
|
contentClassName?: string;
|
|
16
22
|
}
|
|
@@ -20,6 +26,7 @@ export const InlineThinkingItem = memo(
|
|
|
20
26
|
part,
|
|
21
27
|
defaultOpen = false,
|
|
22
28
|
autoCollapse = true,
|
|
29
|
+
openWhileActive = true,
|
|
23
30
|
className,
|
|
24
31
|
contentClassName,
|
|
25
32
|
}: InlineThinkingItemProps) => {
|
|
@@ -31,12 +38,12 @@ export const InlineThinkingItem = memo(
|
|
|
31
38
|
const durationMs =
|
|
32
39
|
startTime != null && endTime != null ? endTime - startTime : undefined;
|
|
33
40
|
const isActive = startTime != null && endTime == null;
|
|
34
|
-
const preview = part.text ? truncateText(part.text, 120) : undefined;
|
|
41
|
+
const preview = part.text ? truncateText(plainTextFromMarkdown(part.text), 120) : undefined;
|
|
35
42
|
|
|
36
43
|
useEffect(() => {
|
|
37
44
|
if (isActive) {
|
|
38
45
|
autoCollapsedRef.current = false;
|
|
39
|
-
setOpen(true);
|
|
46
|
+
if (openWhileActive) setOpen(true);
|
|
40
47
|
return;
|
|
41
48
|
}
|
|
42
49
|
|
|
@@ -48,7 +55,7 @@ export const InlineThinkingItem = memo(
|
|
|
48
55
|
|
|
49
56
|
return () => window.clearTimeout(timer);
|
|
50
57
|
}
|
|
51
|
-
}, [autoCollapse, durationMs, isActive]);
|
|
58
|
+
}, [autoCollapse, durationMs, isActive, openWhileActive]);
|
|
52
59
|
|
|
53
60
|
return (
|
|
54
61
|
<RunRowShell
|
package/src/utils/format.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { formatBytes, formatUptime } from "./format";
|
|
2
|
+
import { formatBytes, formatUptime, plainTextFromMarkdown } from "./format";
|
|
3
3
|
|
|
4
4
|
describe("formatUptime", () => {
|
|
5
5
|
it("renders seconds for short durations", () => {
|
|
@@ -41,3 +41,18 @@ describe("formatBytes", () => {
|
|
|
41
41
|
expect(formatBytes(-1)).toBe("—");
|
|
42
42
|
});
|
|
43
43
|
});
|
|
44
|
+
|
|
45
|
+
describe("plainTextFromMarkdown", () => {
|
|
46
|
+
it("drops emphasis, headings, list markers, code marks and link syntax", () => {
|
|
47
|
+
expect(plainTextFromMarkdown("**Ensuring workspace configuration** I need to check")).toBe("Ensuring workspace configuration I need to check");
|
|
48
|
+
expect(plainTextFromMarkdown("## Plan\n- item one\n- *soft* point")).toBe("Plan\nitem one\nsoft point");
|
|
49
|
+
expect(plainTextFromMarkdown("see [docs](https://example.com) and _this_")).toBe("see docs and this");
|
|
50
|
+
expect(plainTextFromMarkdown("**bold** and __also__ ~~gone~~")).toBe("bold and also gone");
|
|
51
|
+
expect(plainTextFromMarkdown("1. First\n2) Second")).toBe("First\nSecond");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("keeps characters that are not markdown marks", () => {
|
|
55
|
+
expect(plainTextFromMarkdown("read `src/a_b.ts` and snake_case_name")).toBe("read src/a_b.ts and snake_case_name");
|
|
56
|
+
expect(plainTextFromMarkdown("2 * 3 * 4 = 24")).toBe("2 * 3 * 4 = 24");
|
|
57
|
+
});
|
|
58
|
+
});
|
package/src/utils/format.ts
CHANGED
|
@@ -8,6 +8,23 @@ export function formatDuration(ms: number): string {
|
|
|
8
8
|
return remaining > 0 ? `${minutes}m ${remaining}s` : `${minutes}m`;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Markdown read as plain text, for a one-line preview: code fences, emphasis,
|
|
13
|
+
* headings, list and quote markers, and link syntax are removed, so a preview
|
|
14
|
+
* never shows `**` or `#`. Underscores inside words (`snake_case`) are kept.
|
|
15
|
+
*/
|
|
16
|
+
export function plainTextFromMarkdown(text: string): string {
|
|
17
|
+
return text
|
|
18
|
+
.replace(/```[^\n]*\n?([\s\S]*?)```/g, "$1")
|
|
19
|
+
.replace(/`([^`]*)`/g, "$1")
|
|
20
|
+
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1")
|
|
21
|
+
.replace(/^[ \t]{0,3}(?:#{1,6}[ \t]+|>[ \t]?|[-*+][ \t]+|\d+[.)][ \t]+)/gm, "")
|
|
22
|
+
.replace(/(\*\*|__)(?=\S)([\s\S]*?\S)\1/g, "$2")
|
|
23
|
+
.replace(/(^|[^\w*])\*(?=\S)([^*\n]*?\S)\*(?![\w*])/g, "$1$2")
|
|
24
|
+
.replace(/(^|[^\w])_(?=\S)([^_\n]*?\S)_(?!\w)/g, "$1$2")
|
|
25
|
+
.replace(/~~(?=\S)([\s\S]*?\S)~~/g, "$1");
|
|
26
|
+
}
|
|
27
|
+
|
|
11
28
|
/** Truncate text to `max` characters, appending "..." if truncated. */
|
|
12
29
|
export function truncateText(text: string, max: number): string {
|
|
13
30
|
const cleaned = text.replace(/\s+/g, " ").trim();
|
package/src/utils/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { copyText } from './copy-text';
|
|
2
|
-
export { formatBytes, formatDuration, formatUptime, truncateText } from './format';
|
|
2
|
+
export { formatBytes, formatDuration, formatUptime, plainTextFromMarkdown, truncateText } from './format';
|
|
3
3
|
export { timeAgo } from './time-ago';
|
|
4
4
|
export {
|
|
5
5
|
getToolCategory,
|