@tangle-network/ui 1.0.1 → 4.1.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 +61 -0
- package/dist/chat.js +8 -7
- package/dist/{chunk-TMFOPHHN.js → chunk-52Y3FMFI.js} +2 -2
- package/dist/{chunk-7UO2ZMRQ.js → chunk-5VPTNXX7.js} +2 -2
- package/dist/{chunk-XIHMJ7ZQ.js → chunk-AAUNOHVL.js} +5 -30
- package/dist/{chunk-YJ2G3XO5.js → chunk-CMX2I43A.js} +1 -1
- package/dist/{chunk-2VH6PUXD.js → chunk-DGW77LD7.js} +1 -1
- package/dist/{chunk-CD53GZOM.js → chunk-FJBTCTZM.js} +1 -1
- package/dist/{chunk-YNN4O57I.js → chunk-JBPWIYTQ.js} +4 -4
- package/dist/{chunk-2NFQRQOD.js → chunk-KT5RNO7N.js} +4 -4
- package/dist/{chunk-HJKCSXCH.js → chunk-LELGOLFV.js} +44 -78
- package/dist/{chunk-EEE55AVS.js → chunk-SZ44QDA6.js} +1 -1
- package/dist/{chunk-66BNMOVT.js → chunk-WUQDUBJG.js} +5 -4
- package/dist/chunk-ZRVH3WCA.js +107 -0
- package/dist/{code-block-DjXf8eOG.d.ts → code-block-0kSpWMnf.d.ts} +7 -1
- package/dist/{document-editor-pane-A5LT5H4N.js → document-editor-pane-WCTA3ZOE.js} +3 -3
- package/dist/editor.js +3 -4
- package/dist/files.d.ts +39 -2
- package/dist/files.js +16 -4
- package/dist/hooks.js +5 -5
- package/dist/index.d.ts +46 -7
- package/dist/index.js +120 -37
- package/dist/markdown.d.ts +1 -1
- package/dist/markdown.js +2 -2
- package/dist/nav.d.ts +25 -0
- package/dist/nav.js +16 -0
- package/dist/openui.js +3 -3
- package/dist/primitives.d.ts +1 -1
- package/dist/primitives.js +1 -1
- package/dist/run.js +8 -7
- package/dist/sdk-hooks.js +5 -5
- package/dist/tool-previews.js +3 -2
- package/package.json +13 -3
- package/src/files/file-artifact-pane.tsx +3 -3
- package/src/files/file-format.test.ts +176 -0
- package/src/files/file-format.ts +167 -0
- package/src/files/file-preview.stories.tsx +87 -0
- package/src/files/file-preview.test.tsx +52 -0
- package/src/files/file-preview.tsx +48 -94
- package/src/files/index.ts +8 -0
- package/src/index.ts +1 -2
- package/src/markdown/code-block.test.tsx +62 -0
- package/src/markdown/code-block.tsx +11 -4
- package/src/nav/index.tsx +34 -0
- package/src/redaction/index.ts +7 -0
- package/src/redaction/redacted-document.tsx +150 -0
- package/src/tool-previews/write-file-preview.tsx +2 -30
- package/dist/chunk-Q7EIIWTC.js +0 -0
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Renders any file type beautifully:
|
|
5
5
|
* - PDF: embedded viewer
|
|
6
6
|
* - CSV/XLSX: tabular preview
|
|
7
|
-
* - Code (py/json/yaml/ts/js): line-numbered viewer
|
|
7
|
+
* - Code (py/json/yaml/ts/js): syntax-highlighted, line-numbered viewer
|
|
8
8
|
* - Markdown: rendered prose
|
|
9
9
|
* - Images: inline display
|
|
10
10
|
* - Text: monospace preview
|
|
@@ -17,6 +17,14 @@ import {
|
|
|
17
17
|
} from "lucide-react";
|
|
18
18
|
import { cn } from "../lib/utils";
|
|
19
19
|
import { Markdown } from "../markdown/markdown";
|
|
20
|
+
import { CodeBlock, CopyButton } from "../markdown/code-block";
|
|
21
|
+
import {
|
|
22
|
+
detectFileFormat,
|
|
23
|
+
fileExtension,
|
|
24
|
+
getCodeLanguage,
|
|
25
|
+
getFormatLabel,
|
|
26
|
+
type FileFormat,
|
|
27
|
+
} from "./file-format";
|
|
20
28
|
|
|
21
29
|
export interface FilePreviewProps {
|
|
22
30
|
filename: string;
|
|
@@ -29,87 +37,33 @@ export interface FilePreviewProps {
|
|
|
29
37
|
className?: string;
|
|
30
38
|
}
|
|
31
39
|
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return "unknown";
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function getPreviewLabel(previewType: string) {
|
|
51
|
-
switch (previewType) {
|
|
52
|
-
case "pdf":
|
|
53
|
-
return "PDF";
|
|
54
|
-
case "image":
|
|
55
|
-
return "Image";
|
|
56
|
-
case "csv":
|
|
57
|
-
return "CSV";
|
|
58
|
-
case "spreadsheet":
|
|
59
|
-
return "Spreadsheet";
|
|
60
|
-
case "code":
|
|
61
|
-
return "Code";
|
|
62
|
-
case "json":
|
|
63
|
-
return "JSON";
|
|
64
|
-
case "yaml":
|
|
65
|
-
return "YAML";
|
|
66
|
-
case "markdown":
|
|
67
|
-
return "Markdown";
|
|
68
|
-
case "text":
|
|
69
|
-
return "Text";
|
|
70
|
-
default:
|
|
71
|
-
return "File";
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function CodePreview({ content, filename }: { content: string; filename: string }) {
|
|
76
|
-
const lines = content.split("\n");
|
|
77
|
-
const language = filename.split(".").pop()?.toUpperCase() || "TXT";
|
|
40
|
+
function CodePreview({
|
|
41
|
+
content,
|
|
42
|
+
filename,
|
|
43
|
+
format,
|
|
44
|
+
}: {
|
|
45
|
+
content: string;
|
|
46
|
+
filename: string;
|
|
47
|
+
format: FileFormat;
|
|
48
|
+
}) {
|
|
49
|
+
const lineCount = content.split("\n").length;
|
|
50
|
+
const language = getCodeLanguage(filename, format);
|
|
51
|
+
// Prefer the extension; for an extensionless file (e.g. one detected from its
|
|
52
|
+
// MIME type) fall back to the highlight language so the label stays meaningful.
|
|
53
|
+
const labelToken = fileExtension(filename) || language || "txt";
|
|
78
54
|
|
|
55
|
+
// Same theme-aware highlighter the chat markdown renderer uses, so code looks
|
|
56
|
+
// identical in an artifact pane and inline in a message.
|
|
79
57
|
return (
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
</div>
|
|
90
|
-
<div className="inline-flex items-center gap-2 rounded-[var(--radius-full)] border border-border bg-card px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-muted-foreground">
|
|
91
|
-
<span>{language}</span>
|
|
92
|
-
<span className="h-1 w-1 rounded-full bg-[var(--border-hover)]" />
|
|
93
|
-
<span>{lines.length} lines</span>
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
<div className="overflow-auto max-h-[70vh]">
|
|
97
|
-
<table className="w-full">
|
|
98
|
-
<tbody>
|
|
99
|
-
{lines.map((line, i) => (
|
|
100
|
-
<tr key={i} className="hover:bg-accent">
|
|
101
|
-
<td className="text-right pr-4 pl-4 py-0 select-none text-muted-foreground text-xs font-mono w-10 align-top leading-[1.55]">
|
|
102
|
-
{i + 1}
|
|
103
|
-
</td>
|
|
104
|
-
<td className="pr-4 py-0 font-mono text-[13px] text-foreground leading-[1.55] whitespace-pre">
|
|
105
|
-
{line || " "}
|
|
106
|
-
</td>
|
|
107
|
-
</tr>
|
|
108
|
-
))}
|
|
109
|
-
</tbody>
|
|
110
|
-
</table>
|
|
111
|
-
</div>
|
|
112
|
-
</div>
|
|
58
|
+
<CodeBlock
|
|
59
|
+
code={content}
|
|
60
|
+
language={language}
|
|
61
|
+
label={`${labelToken} · ${lineCount} lines`}
|
|
62
|
+
showLineNumbers
|
|
63
|
+
className="max-h-[70vh] overflow-auto"
|
|
64
|
+
>
|
|
65
|
+
<CopyButton text={content} />
|
|
66
|
+
</CodeBlock>
|
|
113
67
|
);
|
|
114
68
|
}
|
|
115
69
|
|
|
@@ -270,13 +224,13 @@ export function FilePreview({
|
|
|
270
224
|
hideHeader = false,
|
|
271
225
|
className,
|
|
272
226
|
}: FilePreviewProps) {
|
|
273
|
-
const
|
|
274
|
-
const previewLabel =
|
|
227
|
+
const format: FileFormat = detectFileFormat(filename, mimeType);
|
|
228
|
+
const previewLabel = getFormatLabel(format);
|
|
275
229
|
const hasRenderableSource =
|
|
276
230
|
Boolean(content) ||
|
|
277
231
|
Boolean(blobUrl) ||
|
|
278
|
-
|
|
279
|
-
|
|
232
|
+
format === "unknown" ||
|
|
233
|
+
format === "spreadsheet";
|
|
280
234
|
|
|
281
235
|
return (
|
|
282
236
|
<div className={cn("flex flex-col h-full", className)}>
|
|
@@ -312,23 +266,23 @@ export function FilePreview({
|
|
|
312
266
|
)}
|
|
313
267
|
|
|
314
268
|
<div className="flex-1 overflow-auto p-3">
|
|
315
|
-
{
|
|
316
|
-
{
|
|
317
|
-
{
|
|
318
|
-
{(
|
|
319
|
-
<CodePreview content={content} filename={filename} />
|
|
269
|
+
{format === "pdf" && blobUrl && <PdfPreview blobUrl={blobUrl} filename={filename} />}
|
|
270
|
+
{format === "image" && blobUrl && <ImagePreview src={blobUrl} filename={filename} />}
|
|
271
|
+
{format === "csv" && typeof content === "string" && <CsvPreview content={content} />}
|
|
272
|
+
{(format === "code" || format === "json" || format === "yaml") && typeof content === "string" && (
|
|
273
|
+
<CodePreview content={content} filename={filename} format={format} />
|
|
320
274
|
)}
|
|
321
|
-
{
|
|
322
|
-
{
|
|
323
|
-
{
|
|
275
|
+
{format === "text" && typeof content === "string" && <TextPreview content={content} />}
|
|
276
|
+
{format === "markdown" && typeof content === "string" && <MarkdownPreview content={content} />}
|
|
277
|
+
{format === "spreadsheet" && (
|
|
324
278
|
<UnsupportedPreview
|
|
325
279
|
filename={filename}
|
|
326
280
|
title="Spreadsheet preview is not available in this surface"
|
|
327
281
|
description="Download the workbook or convert it to CSV when you need an inline preview."
|
|
328
282
|
/>
|
|
329
283
|
)}
|
|
330
|
-
{
|
|
331
|
-
{
|
|
284
|
+
{format === "unknown" && typeof content !== "string" && <EmptyPreview filename={filename} />}
|
|
285
|
+
{format === "unknown" && typeof content === "string" && <TextPreview content={content} />}
|
|
332
286
|
{!hasRenderableSource && typeof content !== "string" && (
|
|
333
287
|
<UnsupportedPreview
|
|
334
288
|
filename={filename}
|
package/src/files/index.ts
CHANGED
|
@@ -15,3 +15,11 @@ export {
|
|
|
15
15
|
export { FilePreview, type FilePreviewProps } from "./file-preview";
|
|
16
16
|
export { FileTabs, type FileTabsProps, type FileTabData } from "./file-tabs";
|
|
17
17
|
export { FileArtifactPane, type FileArtifactPaneProps } from "./file-artifact-pane";
|
|
18
|
+
export {
|
|
19
|
+
detectFileFormat,
|
|
20
|
+
fileExtension,
|
|
21
|
+
getCodeLanguage,
|
|
22
|
+
getFormatLabel,
|
|
23
|
+
getSyntaxLanguage,
|
|
24
|
+
type FileFormat,
|
|
25
|
+
} from "./file-format";
|
package/src/index.ts
CHANGED
|
@@ -3,8 +3,6 @@ export * from "./chat";
|
|
|
3
3
|
export * from "./run";
|
|
4
4
|
export * from "./openui";
|
|
5
5
|
export * from "./files";
|
|
6
|
-
export { type ConnectionState } from "./editor";
|
|
7
|
-
export * from "./editor";
|
|
8
6
|
export * from "./markdown";
|
|
9
7
|
export * from "./auth";
|
|
10
8
|
export * from "./hooks";
|
|
@@ -12,3 +10,4 @@ export * from "./stores";
|
|
|
12
10
|
export * from "./types";
|
|
13
11
|
export * from "./utils";
|
|
14
12
|
export * from "./tool-previews";
|
|
13
|
+
export * from "./redaction";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import { CodeBlock, CopyButton } from "./code-block";
|
|
4
|
+
|
|
5
|
+
describe("CodeBlock", () => {
|
|
6
|
+
it("renders the code through the syntax highlighter", () => {
|
|
7
|
+
const { container } = render(<CodeBlock code={"const x = 1;"} language="typescript" />);
|
|
8
|
+
expect(container.querySelector("code")).not.toBeNull();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("uses `label` as the header text, overriding `language`", () => {
|
|
12
|
+
const { getByText, queryByText } = render(
|
|
13
|
+
<CodeBlock code={"x"} language="typescript" label="config.ts" />,
|
|
14
|
+
);
|
|
15
|
+
expect(getByText("config.ts")).toBeInTheDocument();
|
|
16
|
+
expect(queryByText("typescript")).toBeNull();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("falls back to `language` for the header when no `label` is given", () => {
|
|
20
|
+
const { getByText } = render(<CodeBlock code={"x"} language="python" />);
|
|
21
|
+
expect(getByText("python")).toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("renders no header when neither label nor language is set", () => {
|
|
25
|
+
const { container } = render(<CodeBlock code={"x"} />);
|
|
26
|
+
// The header is the only element carrying a bottom border.
|
|
27
|
+
expect(container.querySelector(".border-b")).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("treats an explicit empty label as 'no header', even with a language", () => {
|
|
31
|
+
const { container, queryByText } = render(
|
|
32
|
+
<CodeBlock code={"x"} language="python" label="" />,
|
|
33
|
+
);
|
|
34
|
+
expect(container.querySelector(".border-b")).toBeNull();
|
|
35
|
+
expect(queryByText("python")).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("places children in the header when a header is shown", () => {
|
|
39
|
+
const { container } = render(
|
|
40
|
+
<CodeBlock code={"x"} label="run.ts">
|
|
41
|
+
<CopyButton text="x" />
|
|
42
|
+
</CodeBlock>,
|
|
43
|
+
);
|
|
44
|
+
const copyButton = container.querySelector('button[title="Copy to clipboard"]');
|
|
45
|
+
expect(copyButton).not.toBeNull();
|
|
46
|
+
// Inside the bordered header, not the absolute hover overlay.
|
|
47
|
+
expect(copyButton?.closest(".border-b")).not.toBeNull();
|
|
48
|
+
expect(copyButton?.closest(".absolute")).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("moves children to a hover overlay when no header is shown", () => {
|
|
52
|
+
const { container } = render(
|
|
53
|
+
<CodeBlock code={"x"}>
|
|
54
|
+
<CopyButton text="x" />
|
|
55
|
+
</CodeBlock>,
|
|
56
|
+
);
|
|
57
|
+
const copyButton = container.querySelector('button[title="Copy to clipboard"]');
|
|
58
|
+
expect(copyButton).not.toBeNull();
|
|
59
|
+
expect(copyButton?.closest(".absolute")).not.toBeNull();
|
|
60
|
+
expect(copyButton?.closest(".border-b")).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -74,6 +74,12 @@ function getSyntaxTheme(): { [key: string]: React.CSSProperties } {
|
|
|
74
74
|
export interface CodeBlockProps extends HTMLAttributes<HTMLDivElement> {
|
|
75
75
|
code: string;
|
|
76
76
|
language?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Header text. Defaults to `language`. Set this when the display name differs
|
|
79
|
+
* from the highlight.js language id — e.g. a file extension ("BASHRC") whose
|
|
80
|
+
* content highlights as a known language, or none.
|
|
81
|
+
*/
|
|
82
|
+
label?: string;
|
|
77
83
|
showLineNumbers?: boolean;
|
|
78
84
|
/** Force light theme; defaults to dark */
|
|
79
85
|
light?: boolean;
|
|
@@ -107,28 +113,29 @@ function useIsLightTheme(): boolean {
|
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
export const CodeBlock = memo(
|
|
110
|
-
({ code, language, showLineNumbers = false, light: lightProp, className, children, ...props }: CodeBlockProps) => {
|
|
116
|
+
({ code, language, label, showLineNumbers = false, light: lightProp, className, children, ...props }: CodeBlockProps) => {
|
|
111
117
|
const isLight = useIsLightTheme();
|
|
112
118
|
const light = lightProp ?? isLight;
|
|
113
119
|
const theme = getSyntaxTheme();
|
|
114
120
|
const bg = "bg-card border-border";
|
|
115
121
|
const headerBg = light ? "bg-muted/50 border-border" : "bg-background border-border";
|
|
116
122
|
const langColor = "text-muted-foreground";
|
|
123
|
+
const headerLabel = label ?? language;
|
|
117
124
|
|
|
118
125
|
return (
|
|
119
126
|
<div
|
|
120
127
|
className={cn("group relative overflow-hidden rounded-lg border font-mono", bg, className)}
|
|
121
128
|
{...props}
|
|
122
129
|
>
|
|
123
|
-
{
|
|
130
|
+
{headerLabel && (
|
|
124
131
|
<div className={cn("flex items-center justify-between border-b px-3 py-1", headerBg)}>
|
|
125
132
|
<span className={cn("text-[calc(var(--font-size-xs)-1px)] font-mono font-medium uppercase tracking-widest", langColor)}>
|
|
126
|
-
{
|
|
133
|
+
{headerLabel}
|
|
127
134
|
</span>
|
|
128
135
|
{children}
|
|
129
136
|
</div>
|
|
130
137
|
)}
|
|
131
|
-
{!
|
|
138
|
+
{!headerLabel && children && (
|
|
132
139
|
<div className="absolute right-2 top-2 z-10 flex items-center gap-2 opacity-0 transition-opacity group-hover:opacity-100">
|
|
133
140
|
{children}
|
|
134
141
|
</div>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Link as RRLink,
|
|
3
|
+
NavLink as RRNavLink,
|
|
4
|
+
type LinkProps,
|
|
5
|
+
type NavLinkProps,
|
|
6
|
+
} from "react-router";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Navigation primitives that make agent-app products feel snappy by default.
|
|
10
|
+
*
|
|
11
|
+
* The dominant source of the "1–2s when I click through pages" latency in the
|
|
12
|
+
* fleet is NOT slow queries (route loaders' D1 indexes already cover their
|
|
13
|
+
* filters) — it is that every click is a COLD loader round-trip the user waits
|
|
14
|
+
* on, because bare `<Link>`s do no prefetching. React Router can fire the
|
|
15
|
+
* target route's loader on hover/focus (`prefetch="intent"`), overlapping the
|
|
16
|
+
* round-trip with the user's mouse travel so the transition feels instant.
|
|
17
|
+
*
|
|
18
|
+
* These wrappers default `prefetch="intent"` so a product gets that behaviour
|
|
19
|
+
* by importing the shared `<Link>` instead of remembering the flag on every
|
|
20
|
+
* nav element. The default is overridable — a caller that passes `prefetch`
|
|
21
|
+
* wins (the spread is applied after the default).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** `react-router` `<Link>` with `prefetch="intent"` on by default. */
|
|
25
|
+
export function Link({ prefetch = "intent", ...props }: LinkProps) {
|
|
26
|
+
return <RRLink prefetch={prefetch} {...props} />;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** `react-router` `<NavLink>` with `prefetch="intent"` on by default. */
|
|
30
|
+
export function NavLink({ prefetch = "intent", ...props }: NavLinkProps) {
|
|
31
|
+
return <RRNavLink prefetch={prefetch} {...props} />;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type { LinkProps, NavLinkProps } from "react-router";
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { useCallback, useState, type ReactNode } from "react";
|
|
2
|
+
import { Eye, EyeOff, Loader2, ShieldAlert } from "lucide-react";
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Viewer for a server-produced redacted document. Renders text inline and each
|
|
7
|
+
* redacted span as a masked chip; clicking a chip asks the server to reveal that
|
|
8
|
+
* one span. The original plaintext is NEVER in the document the client holds —
|
|
9
|
+
* the chip carries only an id + kind; `onReveal` round-trips to the server, where
|
|
10
|
+
* `@tangle-network/agent-app/redact`'s `revealSpan` runs the authorization check
|
|
11
|
+
* and writes the audit trail. So authz + audit are server-truth; this is display.
|
|
12
|
+
*
|
|
13
|
+
* Structural types (no `@tangle-network/agent-app` dependency) — the viewer needs
|
|
14
|
+
* only `{ id, kind }` per span; the cipher stays server-side.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export type RedactedDocSegment =
|
|
18
|
+
| { type: "text"; text: string }
|
|
19
|
+
| { type: "redacted"; id: string; kind: string };
|
|
20
|
+
|
|
21
|
+
export interface RedactedDocumentData {
|
|
22
|
+
segments: RedactedDocSegment[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RevealResult {
|
|
26
|
+
ok: boolean;
|
|
27
|
+
value?: string;
|
|
28
|
+
/** e.g. `forbidden` | `not_found` when `ok` is false. */
|
|
29
|
+
reason?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RedactedDocumentProps {
|
|
33
|
+
document: RedactedDocumentData;
|
|
34
|
+
/** Reveal one span by id. Wire to a server route that calls agent-app's
|
|
35
|
+
* `revealSpan` (authz + audit happen there). Resolves with the original. */
|
|
36
|
+
onReveal: (spanId: string) => Promise<RevealResult>;
|
|
37
|
+
/** Display label for a redaction kind (default: the kind, upper-cased). */
|
|
38
|
+
labelForKind?: (kind: string) => string;
|
|
39
|
+
className?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type ChipState =
|
|
43
|
+
| { status: "masked" }
|
|
44
|
+
| { status: "loading" }
|
|
45
|
+
| { status: "revealed"; value: string }
|
|
46
|
+
| { status: "denied"; reason?: string };
|
|
47
|
+
|
|
48
|
+
const defaultLabel = (kind: string) => kind.replace(/[-_]/g, " ").toUpperCase();
|
|
49
|
+
|
|
50
|
+
function RedactedChip({
|
|
51
|
+
kind,
|
|
52
|
+
label,
|
|
53
|
+
onReveal,
|
|
54
|
+
}: {
|
|
55
|
+
kind: string;
|
|
56
|
+
label: string;
|
|
57
|
+
onReveal: () => Promise<RevealResult>;
|
|
58
|
+
}) {
|
|
59
|
+
const [state, setState] = useState<ChipState>({ status: "masked" });
|
|
60
|
+
|
|
61
|
+
const reveal = useCallback(async () => {
|
|
62
|
+
setState({ status: "loading" });
|
|
63
|
+
try {
|
|
64
|
+
const r = await onReveal();
|
|
65
|
+
setState(
|
|
66
|
+
r.ok && r.value !== undefined
|
|
67
|
+
? { status: "revealed", value: r.value }
|
|
68
|
+
: { status: "denied", reason: r.reason },
|
|
69
|
+
);
|
|
70
|
+
} catch {
|
|
71
|
+
setState({ status: "denied", reason: "error" });
|
|
72
|
+
}
|
|
73
|
+
}, [onReveal]);
|
|
74
|
+
|
|
75
|
+
if (state.status === "revealed") {
|
|
76
|
+
return (
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
onClick={() => setState({ status: "masked" })}
|
|
80
|
+
title="Revealed — click to hide"
|
|
81
|
+
aria-label={`${label}: revealed, click to hide`}
|
|
82
|
+
className={cn(
|
|
83
|
+
"inline-flex items-center gap-1 rounded-[var(--radius-sm)] px-1 font-medium",
|
|
84
|
+
"bg-[color-mix(in_oklch,var(--color-warning,orange)_18%,transparent)] text-foreground ring-1 ring-warning/40",
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
{state.value}
|
|
88
|
+
<EyeOff className="size-3 opacity-60" />
|
|
89
|
+
</button>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (state.status === "denied") {
|
|
94
|
+
return (
|
|
95
|
+
<span
|
|
96
|
+
title={`Restricted${state.reason ? ` (${state.reason})` : ""}`}
|
|
97
|
+
aria-label={`${label}: restricted`}
|
|
98
|
+
className="inline-flex items-center gap-1 rounded-[var(--radius-sm)] bg-muted px-1 text-muted-foreground"
|
|
99
|
+
>
|
|
100
|
+
<ShieldAlert className="size-3" /> {label}
|
|
101
|
+
</span>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
disabled={state.status === "loading"}
|
|
109
|
+
onClick={reveal}
|
|
110
|
+
title={`${label} — click to reveal`}
|
|
111
|
+
aria-label={`${label} redacted, click to reveal`}
|
|
112
|
+
className={cn(
|
|
113
|
+
"inline-flex items-center gap-1 rounded-[var(--radius-sm)] px-1 font-medium tracking-wide",
|
|
114
|
+
"bg-muted text-muted-foreground hover:bg-muted/80 hover:text-foreground transition-colors",
|
|
115
|
+
"cursor-pointer select-none",
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
118
|
+
{state.status === "loading" ? (
|
|
119
|
+
<Loader2 className="size-3 animate-spin" />
|
|
120
|
+
) : (
|
|
121
|
+
<Eye className="size-3 opacity-60" />
|
|
122
|
+
)}
|
|
123
|
+
<span aria-hidden>{"███"}</span> {label}
|
|
124
|
+
</button>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function RedactedDocument({
|
|
129
|
+
document,
|
|
130
|
+
onReveal,
|
|
131
|
+
labelForKind = defaultLabel,
|
|
132
|
+
className,
|
|
133
|
+
}: RedactedDocumentProps): ReactNode {
|
|
134
|
+
return (
|
|
135
|
+
<div className={cn("whitespace-pre-wrap break-words leading-relaxed", className)}>
|
|
136
|
+
{document.segments.map((seg, i) =>
|
|
137
|
+
seg.type === "text" ? (
|
|
138
|
+
<span key={i}>{seg.text}</span>
|
|
139
|
+
) : (
|
|
140
|
+
<RedactedChip
|
|
141
|
+
key={seg.id}
|
|
142
|
+
kind={seg.kind}
|
|
143
|
+
label={labelForKind(seg.kind)}
|
|
144
|
+
onReveal={() => onReveal(seg.id)}
|
|
145
|
+
/>
|
|
146
|
+
),
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { memo } from "react";
|
|
2
2
|
import { FileEdit } from "lucide-react";
|
|
3
3
|
import type { ToolPart } from "../types/parts";
|
|
4
|
+
import { getSyntaxLanguage } from "../files/file-format";
|
|
4
5
|
import { CodeBlock, CopyButton } from "../markdown/code-block";
|
|
5
6
|
import { PreviewCard, PreviewError, PreviewLoading } from "./preview-primitives";
|
|
6
7
|
|
|
@@ -18,35 +19,6 @@ function extractWriteContent(
|
|
|
18
19
|
return { path, content };
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
function getLanguageFromPath(path: string): string | undefined {
|
|
22
|
-
const ext = path.split(".").pop()?.toLowerCase();
|
|
23
|
-
const map: Record<string, string> = {
|
|
24
|
-
ts: "typescript",
|
|
25
|
-
tsx: "tsx",
|
|
26
|
-
js: "javascript",
|
|
27
|
-
jsx: "jsx",
|
|
28
|
-
rs: "rust",
|
|
29
|
-
py: "python",
|
|
30
|
-
go: "go",
|
|
31
|
-
rb: "ruby",
|
|
32
|
-
json: "json",
|
|
33
|
-
yaml: "yaml",
|
|
34
|
-
yml: "yaml",
|
|
35
|
-
toml: "toml",
|
|
36
|
-
md: "markdown",
|
|
37
|
-
css: "css",
|
|
38
|
-
scss: "scss",
|
|
39
|
-
html: "html",
|
|
40
|
-
sh: "bash",
|
|
41
|
-
bash: "bash",
|
|
42
|
-
zsh: "bash",
|
|
43
|
-
sql: "sql",
|
|
44
|
-
sol: "solidity",
|
|
45
|
-
proto: "protobuf",
|
|
46
|
-
};
|
|
47
|
-
return ext ? map[ext] : undefined;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
22
|
/**
|
|
51
23
|
* Preview for file write/create operations.
|
|
52
24
|
* Shows file path, line count, and the written content.
|
|
@@ -56,7 +28,7 @@ export const WriteFilePreview = memo(({ part }: WriteFilePreviewProps) => {
|
|
|
56
28
|
if (!write) return null;
|
|
57
29
|
|
|
58
30
|
const lineCount = write.content.split("\n").length;
|
|
59
|
-
const language =
|
|
31
|
+
const language = getSyntaxLanguage(write.path);
|
|
60
32
|
|
|
61
33
|
return (
|
|
62
34
|
<PreviewCard
|
package/dist/chunk-Q7EIIWTC.js
DELETED
|
File without changes
|