@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.
Files changed (44) hide show
  1. package/dist/data/createInProcessClient.d.ts +1 -3
  2. package/dist/data/createInProcessClient.d.ts.map +1 -1
  3. package/dist/data/createInProcessClient.js +1 -3
  4. package/dist/data/createInProcessClient.js.map +1 -1
  5. package/dist/styles/panel.generated.d.ts +1 -1
  6. package/dist/styles/panel.generated.js +1 -1
  7. package/dist/styles/panel.generated.js.map +1 -1
  8. package/dist/utils/serialization.d.ts.map +1 -1
  9. package/dist/utils/serialization.js +13 -4
  10. package/dist/utils/serialization.js.map +1 -1
  11. package/dist/views/message/MessageMetrics.d.ts.map +1 -1
  12. package/dist/views/message/MessageMetrics.js +1 -1
  13. package/dist/views/message/MessageMetrics.js.map +1 -1
  14. package/dist/views/message/TtftBar.d.ts +1 -1
  15. package/dist/views/message/TtftBar.js +2 -2
  16. package/dist/views/message/TtftBar.js.map +1 -1
  17. package/dist/views/thread/Transcript.d.ts.map +1 -1
  18. package/dist/views/thread/Transcript.js +1 -1
  19. package/dist/views/thread/Transcript.js.map +1 -1
  20. package/dist/views/ui/CopyButton.d.ts +1 -1
  21. package/dist/views/ui/CopyButton.js +3 -3
  22. package/dist/views/ui/CopyButton.js.map +1 -1
  23. package/dist/views/ui/JSONTree.d.ts.map +1 -1
  24. package/dist/views/ui/JSONTree.js +51 -72
  25. package/dist/views/ui/JSONTree.js.map +1 -1
  26. package/dist/views/ui/getInlineJson.d.ts +5 -0
  27. package/dist/views/ui/getInlineJson.d.ts.map +1 -0
  28. package/dist/views/ui/getInlineJson.js +22 -0
  29. package/dist/views/ui/getInlineJson.js.map +1 -0
  30. package/package.json +6 -4
  31. package/src/data/createInProcessClient.ts +1 -3
  32. package/src/styles/panel.generated.ts +1 -1
  33. package/src/utils/serialization.test.ts +19 -0
  34. package/src/utils/serialization.ts +32 -7
  35. package/src/views/message/MessageMetrics.tsx +2 -3
  36. package/src/views/message/TtftBar.test.tsx +20 -0
  37. package/src/views/message/TtftBar.tsx +2 -2
  38. package/src/views/thread/Transcript.tsx +1 -6
  39. package/src/views/ui/CopyButton.tsx +3 -3
  40. package/src/views/ui/JSONTree.interaction.test.tsx +118 -0
  41. package/src/views/ui/JSONTree.tsx +29 -28
  42. package/src/views/ui/JSONTree.work.test.tsx +60 -0
  43. package/src/views/ui/getInlineJson.test.ts +83 -0
  44. 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: unknown): Entry[] => {
14
+ const entriesOf = (value: object, keys: string[], limit: number): Entry[] => {
14
15
  if (Array.isArray(value)) {
15
- return value.map((item, index) => ({ key: String(index), value: item }));
16
+ return value
17
+ .slice(0, limit)
18
+ .map((item, index) => ({ key: String(index), value: item }));
16
19
  }
17
- return Object.entries(value as Record<string, unknown>).map(
18
- ([key, value]) => ({ key, value }),
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 entries = entriesOf(value);
82
- const shown = showAll ? entries : entries.slice(0, ENTRY_LIMIT);
83
- const hidden = entries.length - shown.length;
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">{summaryOf(value)}</span>
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
- () => (isLeaf ? serialize(value, compact) : ""),
218
- [value, compact, isLeaf],
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 = useMemo(() => JSON.stringify(value, null, 2), [value]);
221
+ const copyText = useCallback(() => JSON.stringify(value, null, 2), [value]);
221
222
 
222
- if (isLeaf) {
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
+ }