@assistant-ui/react-devtools 1.2.18 → 1.2.19
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/data/createInProcessClient.d.ts +1 -3
- package/dist/data/createInProcessClient.d.ts.map +1 -1
- package/dist/data/createInProcessClient.js +1 -3
- package/dist/data/createInProcessClient.js.map +1 -1
- package/dist/styles/panel.generated.d.ts +1 -1
- package/dist/styles/panel.generated.js +1 -1
- package/dist/styles/panel.generated.js.map +1 -1
- package/dist/utils/serialization.d.ts.map +1 -1
- package/dist/utils/serialization.js +13 -4
- package/dist/utils/serialization.js.map +1 -1
- package/dist/views/message/MessageMetrics.d.ts.map +1 -1
- package/dist/views/message/MessageMetrics.js +1 -1
- package/dist/views/message/MessageMetrics.js.map +1 -1
- package/dist/views/message/TtftBar.d.ts +1 -1
- package/dist/views/message/TtftBar.js +2 -2
- package/dist/views/message/TtftBar.js.map +1 -1
- package/dist/views/thread/Transcript.d.ts.map +1 -1
- package/dist/views/thread/Transcript.js +1 -1
- package/dist/views/thread/Transcript.js.map +1 -1
- package/dist/views/ui/CopyButton.d.ts +1 -1
- package/dist/views/ui/CopyButton.js +3 -3
- package/dist/views/ui/CopyButton.js.map +1 -1
- package/dist/views/ui/JSONTree.d.ts.map +1 -1
- package/dist/views/ui/JSONTree.js +51 -72
- package/dist/views/ui/JSONTree.js.map +1 -1
- package/dist/views/ui/getInlineJson.d.ts +5 -0
- package/dist/views/ui/getInlineJson.d.ts.map +1 -0
- package/dist/views/ui/getInlineJson.js +22 -0
- package/dist/views/ui/getInlineJson.js.map +1 -0
- package/package.json +6 -4
- package/src/data/createInProcessClient.ts +1 -3
- package/src/styles/panel.generated.ts +1 -1
- package/src/utils/serialization.test.ts +19 -0
- package/src/utils/serialization.ts +32 -7
- package/src/views/message/MessageMetrics.tsx +2 -3
- package/src/views/message/TtftBar.test.tsx +20 -0
- package/src/views/message/TtftBar.tsx +2 -2
- package/src/views/thread/Transcript.tsx +1 -6
- package/src/views/ui/CopyButton.tsx +3 -3
- package/src/views/ui/JSONTree.interaction.test.tsx +118 -0
- package/src/views/ui/JSONTree.tsx +29 -28
- package/src/views/ui/JSONTree.work.test.tsx +60 -0
- package/src/views/ui/getInlineJson.test.ts +83 -0
- package/src/views/ui/getInlineJson.ts +38 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { useMemo, useState } from "react";
|
|
1
|
+
import { useCallback, useMemo, useState } from "react";
|
|
2
2
|
import clsx from "clsx";
|
|
3
3
|
import { CopyButton } from "./CopyButton";
|
|
4
|
+
import { getInlineJson } from "./getInlineJson";
|
|
4
5
|
|
|
5
6
|
const ENTRY_LIMIT = 100;
|
|
6
7
|
const INLINE_MAX = 88;
|
|
@@ -10,29 +11,20 @@ const isContainer = (value: unknown): value is object =>
|
|
|
10
11
|
|
|
11
12
|
type Entry = { key: string; value: unknown };
|
|
12
13
|
|
|
13
|
-
const entriesOf = (value:
|
|
14
|
+
const entriesOf = (value: object, keys: string[], limit: number): Entry[] => {
|
|
14
15
|
if (Array.isArray(value)) {
|
|
15
|
-
return value
|
|
16
|
+
return value
|
|
17
|
+
.slice(0, limit)
|
|
18
|
+
.map((item, index) => ({ key: String(index), value: item }));
|
|
16
19
|
}
|
|
17
|
-
return
|
|
18
|
-
(
|
|
19
|
-
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const summaryOf = (value: unknown): string => {
|
|
23
|
-
if (Array.isArray(value)) return `[${value.length}]`;
|
|
24
|
-
return `{${Object.keys(value as object).length}}`;
|
|
20
|
+
return keys
|
|
21
|
+
.slice(0, limit)
|
|
22
|
+
.map((key) => ({ key, value: (value as Record<string, unknown>)[key] }));
|
|
25
23
|
};
|
|
26
24
|
|
|
27
25
|
const serialize = (value: unknown, compact: boolean) =>
|
|
28
26
|
JSON.stringify(value, null, compact ? 0 : 2);
|
|
29
27
|
|
|
30
|
-
const isInline = (value: unknown, compact: boolean) => {
|
|
31
|
-
if (!compact) return false;
|
|
32
|
-
if (!isContainer(value)) return true;
|
|
33
|
-
return serialize(value, true).length <= INLINE_MAX;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
28
|
const Leaf = ({ value }: { value: unknown }) => {
|
|
37
29
|
if (typeof value === "string") {
|
|
38
30
|
return (
|
|
@@ -78,9 +70,12 @@ const TreeNode = ({
|
|
|
78
70
|
);
|
|
79
71
|
}
|
|
80
72
|
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
const
|
|
73
|
+
const keys = Array.isArray(value) ? [] : Object.keys(value);
|
|
74
|
+
const count = Array.isArray(value) ? value.length : keys.length;
|
|
75
|
+
const shown = open
|
|
76
|
+
? entriesOf(value, keys, showAll ? count : ENTRY_LIMIT)
|
|
77
|
+
: [];
|
|
78
|
+
const hidden = count - shown.length;
|
|
84
79
|
|
|
85
80
|
return (
|
|
86
81
|
<div className="min-w-0">
|
|
@@ -101,7 +96,9 @@ const TreeNode = ({
|
|
|
101
96
|
›
|
|
102
97
|
</span>
|
|
103
98
|
{name !== undefined ? <span>{name}</span> : null}
|
|
104
|
-
<span className="opacity-60">
|
|
99
|
+
<span className="opacity-60">
|
|
100
|
+
{Array.isArray(value) ? `[${count}]` : `{${count}}`}
|
|
101
|
+
</span>
|
|
105
102
|
</button>
|
|
106
103
|
{open ? (
|
|
107
104
|
<div
|
|
@@ -141,7 +138,7 @@ const InlineJson = ({
|
|
|
141
138
|
compact,
|
|
142
139
|
}: {
|
|
143
140
|
serialized: string;
|
|
144
|
-
copyText: string;
|
|
141
|
+
copyText: () => string;
|
|
145
142
|
compact: boolean;
|
|
146
143
|
}) => (
|
|
147
144
|
<div className="flex min-w-0 items-start gap-0.5">
|
|
@@ -171,7 +168,7 @@ const TreeWithCopy = ({
|
|
|
171
168
|
}: {
|
|
172
169
|
value: unknown;
|
|
173
170
|
openDepth: number;
|
|
174
|
-
copyText: string;
|
|
171
|
+
copyText: () => string;
|
|
175
172
|
compact: boolean;
|
|
176
173
|
}) => (
|
|
177
174
|
<div className="flex min-w-0 items-start gap-0.5">
|
|
@@ -212,14 +209,18 @@ export const JSONTree = ({
|
|
|
212
209
|
openDepth?: number;
|
|
213
210
|
compact?: boolean;
|
|
214
211
|
}) => {
|
|
215
|
-
const isLeaf = isInline(value, compact) || !isContainer(value);
|
|
216
212
|
const serialized = useMemo(
|
|
217
|
-
() =>
|
|
218
|
-
|
|
213
|
+
() =>
|
|
214
|
+
!isContainer(value)
|
|
215
|
+
? serialize(value, compact)
|
|
216
|
+
: compact
|
|
217
|
+
? getInlineJson(value, INLINE_MAX)
|
|
218
|
+
: undefined,
|
|
219
|
+
[value, compact],
|
|
219
220
|
);
|
|
220
|
-
const copyText =
|
|
221
|
+
const copyText = useCallback(() => JSON.stringify(value, null, 2), [value]);
|
|
221
222
|
|
|
222
|
-
if (
|
|
223
|
+
if (serialized !== undefined) {
|
|
223
224
|
return (
|
|
224
225
|
<InlineJson
|
|
225
226
|
serialized={serialized}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
2
|
+
import { createRenderCounter } from "@assistant-ui/x-performance";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { JSONTree } from "./JSONTree";
|
|
5
|
+
|
|
6
|
+
afterEach(() => vi.restoreAllMocks());
|
|
7
|
+
|
|
8
|
+
function trackedValue(size: number, array: boolean) {
|
|
9
|
+
const counter = createRenderCounter();
|
|
10
|
+
const value: Record<string, unknown> | unknown[] = array ? [] : {};
|
|
11
|
+
for (let index = 0; index < size; index++) {
|
|
12
|
+
Object.defineProperty(value, String(index), {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: () => {
|
|
15
|
+
counter.useRender("entry");
|
|
16
|
+
return index;
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
const stringify = JSON.stringify;
|
|
21
|
+
vi.spyOn(JSON, "stringify").mockImplementation((...args) => {
|
|
22
|
+
if (args[0] === value) counter.useRender("serialization");
|
|
23
|
+
return Reflect.apply(stringify, JSON, args);
|
|
24
|
+
});
|
|
25
|
+
return { value, counter };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("JSONTree work bounds", () => {
|
|
29
|
+
it.each([false, true])(
|
|
30
|
+
"does not read a collapsed array in compact=%s",
|
|
31
|
+
(compact) => {
|
|
32
|
+
const { value, counter } = trackedValue(10_000, true);
|
|
33
|
+
const html = renderToStaticMarkup(
|
|
34
|
+
<JSONTree value={value} openDepth={0} compact={compact} />,
|
|
35
|
+
);
|
|
36
|
+
expect(html).toContain("[10000]");
|
|
37
|
+
expect(counter.renders("entry")).toBe(0);
|
|
38
|
+
expect(counter.renders("serialization")).toBe(0);
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
it("does not read a collapsed object's values", () => {
|
|
43
|
+
const { value, counter } = trackedValue(10_000, false);
|
|
44
|
+
const html = renderToStaticMarkup(<JSONTree value={value} openDepth={0} />);
|
|
45
|
+
expect(html).toContain("{10000}");
|
|
46
|
+
expect(counter.renders("entry")).toBe(0);
|
|
47
|
+
expect(counter.renders("serialization")).toBe(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it.each([false, true])(
|
|
51
|
+
"reads only the first 100 entries for array=%s",
|
|
52
|
+
(array) => {
|
|
53
|
+
const { value, counter } = trackedValue(10_000, array);
|
|
54
|
+
const html = renderToStaticMarkup(<JSONTree value={value} />);
|
|
55
|
+
expect(html).toContain("9900");
|
|
56
|
+
expect(counter.renders("entry")).toBe(100);
|
|
57
|
+
expect(counter.renders("serialization")).toBe(0);
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createRenderCounter } from "@assistant-ui/x-performance";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { getInlineJson } from "./getInlineJson";
|
|
4
|
+
|
|
5
|
+
describe("getInlineJson", () => {
|
|
6
|
+
it("preserves JSON output and the exact inline length boundary", () => {
|
|
7
|
+
const values: unknown[] = [
|
|
8
|
+
null,
|
|
9
|
+
true,
|
|
10
|
+
false,
|
|
11
|
+
0,
|
|
12
|
+
NaN,
|
|
13
|
+
Infinity,
|
|
14
|
+
{},
|
|
15
|
+
[],
|
|
16
|
+
undefined,
|
|
17
|
+
];
|
|
18
|
+
for (const size of [0, 1, 20, 42, 43, 44, 85, 86, 87, 88, 89, 100]) {
|
|
19
|
+
for (const char of ["x", "\n", '"', "\\", "😀", "\ud800"]) {
|
|
20
|
+
const text = char.repeat(size);
|
|
21
|
+
values.push(text, { text }, { nested: { text } }, [text]);
|
|
22
|
+
}
|
|
23
|
+
values.push(
|
|
24
|
+
Array(size).fill(0),
|
|
25
|
+
Array(size),
|
|
26
|
+
Object.fromEntries(
|
|
27
|
+
Array.from({ length: size }, (_, i) => [String(i), undefined]),
|
|
28
|
+
),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
values.push({
|
|
32
|
+
omitted: undefined,
|
|
33
|
+
fn: () => {},
|
|
34
|
+
symbol: Symbol(),
|
|
35
|
+
kept: "yes",
|
|
36
|
+
});
|
|
37
|
+
values.push(new Date("2026-01-01T00:00:00.000Z"));
|
|
38
|
+
values.push({ toJSON: () => ({ short: true }) });
|
|
39
|
+
const customArray = Object.assign(Array(100).fill(0), {
|
|
40
|
+
toJSON: () => "short",
|
|
41
|
+
});
|
|
42
|
+
values.push(customArray);
|
|
43
|
+
for (const value of values) {
|
|
44
|
+
const serialized = JSON.stringify(value);
|
|
45
|
+
expect(getInlineJson(value, 88)).toBe(
|
|
46
|
+
serialized !== undefined && serialized.length <= 88
|
|
47
|
+
? serialized
|
|
48
|
+
: undefined,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("stops reading a large object after the preview budget is exhausted", () => {
|
|
54
|
+
const counter = createRenderCounter();
|
|
55
|
+
const value: Record<string, number> = {};
|
|
56
|
+
for (let index = 0; index < 10_000; index++) {
|
|
57
|
+
Object.defineProperty(value, `row${index}`, {
|
|
58
|
+
enumerable: true,
|
|
59
|
+
get: () => {
|
|
60
|
+
counter.useRender("entry");
|
|
61
|
+
return index;
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
expect(getInlineJson(value, 88)).toBeUndefined();
|
|
66
|
+
expect(counter.renders("entry")).toBe(17);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("does not turn unrelated serialization errors into a large preview", () => {
|
|
70
|
+
const failure = new Error("serialization failed");
|
|
71
|
+
expect(() =>
|
|
72
|
+
getInlineJson(
|
|
73
|
+
{
|
|
74
|
+
toJSON: () => {
|
|
75
|
+
throw failure;
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
88,
|
|
79
|
+
),
|
|
80
|
+
).toThrow(failure);
|
|
81
|
+
expect(() => getInlineJson({ value: 1n }, 88)).toThrow(TypeError);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const TOO_LARGE = Symbol();
|
|
2
|
+
|
|
3
|
+
export function getInlineJson(value: unknown, limit: number) {
|
|
4
|
+
if (
|
|
5
|
+
Array.isArray(value) &&
|
|
6
|
+
!("toJSON" in value) &&
|
|
7
|
+
value.length * 2 + 1 > limit
|
|
8
|
+
) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let remaining = limit;
|
|
13
|
+
try {
|
|
14
|
+
const serialized = JSON.stringify(value, function (key, entry: unknown) {
|
|
15
|
+
if (
|
|
16
|
+
!Array.isArray(this) &&
|
|
17
|
+
(entry === undefined ||
|
|
18
|
+
typeof entry === "function" ||
|
|
19
|
+
typeof entry === "symbol")
|
|
20
|
+
) {
|
|
21
|
+
return entry;
|
|
22
|
+
}
|
|
23
|
+
// This lower bound stops large values without replacing JSON escaping or omission rules.
|
|
24
|
+
remaining -=
|
|
25
|
+
1 +
|
|
26
|
+
(Array.isArray(this) ? 0 : key.length) +
|
|
27
|
+
(typeof entry === "string" ? entry.length : 0);
|
|
28
|
+
if (remaining < 0) throw TOO_LARGE;
|
|
29
|
+
return entry;
|
|
30
|
+
});
|
|
31
|
+
return serialized !== undefined && serialized.length <= limit
|
|
32
|
+
? serialized
|
|
33
|
+
: undefined;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error === TOO_LARGE) return undefined;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|