diff --git a/.changeset/data-inspector-expanded-brackets.md b/.changeset/data-inspector-expanded-brackets.md
new file mode 100644
index 0000000000..98128f03af
--- /dev/null
+++ b/.changeset/data-inspector-expanded-brackets.md
@@ -0,0 +1,5 @@
+---
+'@workflow/web-shared': patch
+---
+
+Rework the data inspector's JSON rendering: bracket notation (`{ … }` / `[ … ]`), colored keys, typed value colors, `▸`/`▾` disclosure icons, trailing commas, and a `...` collapsed indicator. Replaces the `react-inspector` engine with an in-house tree renderer while keeping the workflow-specific value handling (StreamRef/RunRef badges, encrypted markers, decoded byte streams, dates, class instances).
diff --git a/packages/web-shared/package.json b/packages/web-shared/package.json
index 27879eacc8..df7f3bdb59 100644
--- a/packages/web-shared/package.json
+++ b/packages/web-shared/package.json
@@ -59,7 +59,6 @@
"lucide-react": "0.575.0",
"react": "19.1.0",
"react-dom": "19.1.0",
- "react-inspector": "9.0.0",
"react-use-measure": "2.1.1",
"react-virtuoso": "4.18.1",
"shiki": "4.0.0",
diff --git a/packages/web-shared/src/components/ui/data-inspector.styles.ts b/packages/web-shared/src/components/ui/data-inspector.styles.ts
new file mode 100644
index 0000000000..5af1224455
--- /dev/null
+++ b/packages/web-shared/src/components/ui/data-inspector.styles.ts
@@ -0,0 +1,96 @@
+/**
+ * Class names and styles for the data inspector tree.
+ *
+ * Kept out of the component module for readability. The CSS is injected via a
+ * React 19 hoistable `
+
+ >
+ );
if (onStreamClick) {
- wrapped = (
+ content = (
- {wrapped}
+ {content}
);
}
if (onRunClick) {
- wrapped = (
+ content = (
- {wrapped}
+ {content}
);
}
if (onDecrypt) {
- wrapped = (
+ content = (
- {wrapped}
+ {content}
);
}
- return wrapped;
+ return content;
}
+// ---------------------------------------------------------------------------
+// Render stabilization (avoid re-renders when data is deeply equal)
+// ---------------------------------------------------------------------------
+
function useStableInspectorData(next: T): T {
const previousRef = useRef(next);
if (!isDeepEqual(previousRef.current, next)) {
diff --git a/packages/web-shared/src/components/ui/inspector-theme.ts b/packages/web-shared/src/components/ui/inspector-theme.ts
deleted file mode 100644
index 5d04188082..0000000000
--- a/packages/web-shared/src/components/ui/inspector-theme.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-/**
- * Shared theme configuration for react-inspector's ObjectInspector.
- *
- * Colors follow Geist's Shiki JSON palette so the inspector reads the same
- * as highlighted code blocks across the product:
- * - property names / punctuation: --ds-gray-1000 (default foreground)
- * - strings / numbers / booleans: --ds-green-900
- * - null / undefined: --ds-gray-900 (muted)
- * - regexp / function: --ds-purple-900
- * - date: --ds-pink-900
- *
- * Because the `--ds-*` tokens adapt to theme automatically, the light and
- * dark objects are intentionally identical.
- */
-
-// ---------------------------------------------------------------------------
-// Extended color tokens not supported by react-inspector's built-in theme
-// system, applied via our custom nodeRenderer in data-inspector.tsx.
-// ---------------------------------------------------------------------------
-
-export interface InspectorThemeExtended {
- /** Color for Date values (Node: 'magenta') */
- OBJECT_VALUE_DATE_COLOR: string;
-}
-
-export const inspectorThemeExtendedLight: InspectorThemeExtended = {
- OBJECT_VALUE_DATE_COLOR: 'var(--ds-pink-900)',
-};
-
-export const inspectorThemeExtendedDark: InspectorThemeExtended = {
- OBJECT_VALUE_DATE_COLOR: 'var(--ds-pink-900)',
-};
-
-// ---------------------------------------------------------------------------
-// Shared structural values (same in both themes)
-// ---------------------------------------------------------------------------
-
-const shared = {
- BASE_FONT_SIZE: '11px',
- BASE_LINE_HEIGHT: 1.4,
- BASE_BACKGROUND_COLOR: 'transparent',
- OBJECT_PREVIEW_ARRAY_MAX_PROPERTIES: 10,
- OBJECT_PREVIEW_OBJECT_MAX_PROPERTIES: 5,
- HTML_TAGNAME_TEXT_TRANSFORM: 'lowercase' as const,
- ARROW_MARGIN_RIGHT: 3,
- ARROW_FONT_SIZE: 12,
- TREENODE_FONT_FAMILY: 'var(--font-mono)',
- TREENODE_FONT_SIZE: '11px',
- TREENODE_LINE_HEIGHT: 1.4,
- TREENODE_PADDING_LEFT: 12,
- TABLE_DATA_BACKGROUND_IMAGE: 'none',
- TABLE_DATA_BACKGROUND_SIZE: '0',
-};
-
-// ---------------------------------------------------------------------------
-// Light theme
-// ---------------------------------------------------------------------------
-
-const geistTheme = {
- ...shared,
-
- // Base text
- BASE_COLOR: 'var(--ds-gray-1000)',
-
- // Property names — default foreground (matches JSON key color in Geist Shiki)
- OBJECT_NAME_COLOR: 'var(--ds-gray-1000)',
-
- // Strings & symbols — green
- OBJECT_VALUE_STRING_COLOR: 'var(--ds-green-900)',
- OBJECT_VALUE_SYMBOL_COLOR: 'var(--ds-green-900)',
-
- // Numbers & booleans — green (Geist JSON tokens)
- OBJECT_VALUE_NUMBER_COLOR: 'var(--ds-green-900)',
- OBJECT_VALUE_BOOLEAN_COLOR: 'var(--ds-green-900)',
-
- // null — muted foreground
- OBJECT_VALUE_NULL_COLOR: 'var(--ds-gray-900)',
-
- // undefined — muted foreground
- OBJECT_VALUE_UNDEFINED_COLOR: 'var(--ds-gray-900)',
-
- // RegExp — purple
- OBJECT_VALUE_REGEXP_COLOR: 'var(--ds-purple-900)',
-
- // Functions — purple
- OBJECT_VALUE_FUNCTION_PREFIX_COLOR: 'var(--ds-purple-900)',
-
- // HTML (rarely used here, kept consistent with the palette)
- HTML_TAG_COLOR: 'var(--ds-gray-900)',
- HTML_TAGNAME_COLOR: 'var(--ds-blue-900)',
- HTML_ATTRIBUTE_NAME_COLOR: 'var(--ds-amber-900)',
- HTML_ATTRIBUTE_VALUE_COLOR: 'var(--ds-green-900)',
- HTML_COMMENT_COLOR: 'var(--ds-gray-700)',
- HTML_DOCTYPE_COLOR: 'var(--ds-gray-700)',
-
- // Structural
- ARROW_COLOR: 'var(--ds-gray-700)',
- TABLE_BORDER_COLOR: 'var(--ds-gray-300)',
- TABLE_TH_BACKGROUND_COLOR: 'var(--ds-gray-100)',
- TABLE_TH_HOVER_COLOR: 'var(--ds-gray-200)',
- TABLE_SORT_ICON_COLOR: 'var(--ds-gray-700)',
-};
-
-export const inspectorThemeLight = geistTheme;
-
-// ---------------------------------------------------------------------------
-// Dark theme
-// ---------------------------------------------------------------------------
-
-export const inspectorThemeDark = geistTheme;
diff --git a/packages/web-shared/src/lib/hydration.ts b/packages/web-shared/src/lib/hydration.ts
index 936c35c1fb..2caf2cf0c7 100644
--- a/packages/web-shared/src/lib/hydration.ts
+++ b/packages/web-shared/src/lib/hydration.ts
@@ -298,7 +298,7 @@ export function getWebRevivers(): Revivers {
// Web-specific overrides for class instances.
// Create objects with a dynamically-named constructor so that
- // react-inspector shows the class name (it reads constructor.name).
+ // the data inspector shows the class name (it reads constructor.name).
Class: (value) => ``,
Instance: (value) => {
// Run instances are rendered as clickable RunRef badges
@@ -311,7 +311,7 @@ export function getWebRevivers(): Revivers {
const props =
data && typeof data === 'object' ? { ...data } : { value: data };
// Create a constructor with the right name using computed property
- // so react-inspector's `object.constructor.name` shows the class name.
+ // so the data inspector's `object.constructor.name` shows the class name.
// Must use `function` (not arrow) because arrow functions have no .prototype.
// biome-ignore lint/complexity/useArrowFunction: arrow functions have no .prototype
const ctor = { [className]: function () {} }[className]!;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 94ed2b11a2..6bfef0f6c5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1215,9 +1215,6 @@ importers:
react-dom:
specifier: 19.1.0
version: 19.1.0(react@19.1.0)
- react-inspector:
- specifier: 9.0.0
- version: 9.0.0(react@19.1.0)
react-use-measure:
specifier: 2.1.1
version: 2.1.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -15272,11 +15269,6 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19
- react-inspector@9.0.0:
- resolution: {integrity: sha512-w/VJucSeHxlwRa2nfM2k7YhpT1r5EtlDOClSR+L7DyQP91QMdfFEDXDs9bPYN4kzP7umFtom7L0b2GGjph4Kow==}
- peerDependencies:
- react: ^18.0.0 || ^19.0.0
-
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@@ -26050,14 +26042,6 @@ snapshots:
optionalDependencies:
vite: 7.3.2(@types/node@22.19.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.9.0)
- '@vitest/mocker@4.0.18(vite@7.3.2(@types/node@22.19.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.9.0))':
- dependencies:
- '@vitest/spy': 4.0.18
- estree-walker: 3.0.3
- magic-string: 0.30.21
- optionalDependencies:
- vite: 7.3.2(@types/node@22.19.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.9.0)
-
'@vitest/mocker@4.0.18(vite@7.3.2(@types/node@24.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.9.0))':
dependencies:
'@vitest/spy': 4.0.18
@@ -33817,10 +33801,6 @@ snapshots:
dependencies:
react: 19.2.3
- react-inspector@9.0.0(react@19.1.0):
- dependencies:
- react: 19.1.0
-
react-is@16.13.1: {}
react-is@17.0.2: {}
@@ -36417,7 +36397,7 @@ snapshots:
vitest@4.0.18(@opentelemetry/api@1.9.1)(@types/node@22.19.0)(jiti@2.7.0)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.9.0):
dependencies:
'@vitest/expect': 4.0.18
- '@vitest/mocker': 4.0.18(vite@7.3.2(@types/node@22.19.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.9.0))
+ '@vitest/mocker': 4.0.18(vite@7.3.2(@types/node@24.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.9.0))
'@vitest/pretty-format': 4.0.18
'@vitest/runner': 4.0.18
'@vitest/snapshot': 4.0.18