diff --git a/config/quality/dependency-allowlist.json b/config/quality/dependency-allowlist.json index 1a6097e13c..5b7d3e2256 100644 --- a/config/quality/dependency-allowlist.json +++ b/config/quality/dependency-allowlist.json @@ -119,6 +119,7 @@ "react", "react-dom", "react-is", + "react18-json-view", "react-markdown", "react-reconciler", "recharts", diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index a602c0a80e..ca2372b50c 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -483,7 +483,8 @@ "_rebaseline_2026_08_28_mergebatch_v3851_qwen_retirement": "/merge-batch 2026-08-28 (v3.8.51): #11713 (Qwen Web retirement) own growth: open-sse/services/autoCombo/virtualFactory.ts 1132->1135 (+3, combining the Designer + Runtime retirement-guard filter into the single runtimeConnections predicate at the existing candidate-pool chokepoint, now excluding Qwen Web alongside Felo Web). Irreducible per-mechanism wiring, additive not a new branch. Covered by tests/unit/virtual-auto-combo.test.ts.", "_rebaseline_2026_08_28_mergebatch_v3851_chatgptweb_retirement": "/merge-batch 2026-08-28 (v3.8.51): #11754 (common ChatGPT Web retirement) own growth: open-sse/services/autoCombo/virtualFactory.ts 1135->1138 (+3, an early `available` connection filter for the retired chatgpt-web/cgpt-web ids applied to both the active and disabled-noauth connection lists, ahead of the existing Designer+Runtime runtimeConnections filter). Irreducible per-mechanism wiring, additive not a new branch. Covered by tests/unit/virtual-auto-combo.test.ts.", "src/lib/cloudflaredTunnel.ts": 1078, - "src/shared/components/RequestLoggerDetail.tsx": 1018, + "src/shared/components/RequestLoggerDetail.tsx": 1111, + "_rebaseline_2026_08_30_11703_json_tree_viewer": "/merge-batch 2026-08-30 (v3.8.51): #11703 (hartmark) own growth: src/shared/components/RequestLoggerDetail.tsx 1018->1111 (+93). The 2026-07-22 annotation on this same file said 'no further growth without split rationale' — this PR does split: the collapsible-JSON-tree rendering logic itself lives in the sibling RequestLoggerDetail.sections.tsx (PayloadSection/StreamSection extraction, +82 lines there) plus two new leaves (JsonTreeExpandControls.tsx, useTimestampTitles.ts) and a new store (jsonTreeExpandStore.ts) — all well under cap. The +93 remaining here is the irreducible call-site wiring: import + mount JsonTreeExpandControls, wire the per-section expand-level state and timestamp-tooltip hook into the existing detail panel layout. Covered by the PR's own tests/unit/dashboard/payload-section-collapsible-json.test.tsx, timestamp-titles.test.tsx, tests/unit/shared/json-tree-expand-store.test.ts, short-call-id.test.ts (43/43 vitest + 11/11 native pass).", "src/app/api/providers/[id]/test/route.ts": 1255, "src/lib/guardrails/videoBridgeRuntime.ts": 1009, "_rebaseline_2026_08_28_mergebatch_v3851_ratchet_bank_reconcile": "/merge-batch 2026-08-28 (v3.8.51): boarding #11702 (fix/verify-ratchet-bank object-note comparator) surfaced a large stale `frozen`/`testFrozen` snapshot on PR #11702's own branch (forked before the 08-11 banking outage — see the object-valued `_rebaseline_2026_08_11_v3850_merge_storm_provider_registry` note above, the exact bug #11702 fixes in the verifier) — its conflicting block duplicated ~85 already-tracked files with sizes smaller than the current release tip, and still listed open-sse/executors/chatgpt-web.ts (deleted by the #11754 retirement). Resolved by re-measuring every file in the union of both sides directly on the boarded tree (split(\"\\n\").length, matching check-file-size.mjs) rather than trusting either stale snapshot; dropped the dead chatgpt-web.ts entry; kept the two genuinely-new entries PR #11702's branch had that this tip did not yet track (src/app/api/providers/[id]/test/route.ts, src/lib/guardrails/videoBridgeRuntime.ts, both re-measured). Same reconciliation applied to the testFrozen block above.", diff --git a/package-lock.json b/package-lock.json index 4214aef2e4..746a2500bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,6 +72,7 @@ "react-is": "^19.2.6", "react-markdown": "^10.1.0", "react-reconciler": "^0.33.0", + "react18-json-view": "0.2.10", "recharts": "^3.8.1", "safe-regex": "^2.1.1", "selfsigned": "^5.5.0", @@ -16994,6 +16995,15 @@ "node": ">=6.6.0" } }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -20992,490 +21002,6 @@ } } }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/aix-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", - "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/android-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", - "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/android-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", - "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/android-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", - "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/darwin-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", - "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/darwin-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", - "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", - "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/freebsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", - "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/linux-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", - "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/linux-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", - "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/linux-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", - "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/linux-loong64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", - "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/linux-mips64el": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", - "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/linux-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", - "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/linux-riscv64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", - "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/linux-s390x": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", - "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/linux-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", - "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", - "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/netbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", - "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", - "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/openbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", - "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", - "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/sunos-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", - "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/win32-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", - "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/win32-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", - "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/@esbuild/win32-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", - "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/fumadocs-mdx/node_modules/esbuild": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", - "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.2", - "@esbuild/android-arm": "0.28.2", - "@esbuild/android-arm64": "0.28.2", - "@esbuild/android-x64": "0.28.2", - "@esbuild/darwin-arm64": "0.28.2", - "@esbuild/darwin-x64": "0.28.2", - "@esbuild/freebsd-arm64": "0.28.2", - "@esbuild/freebsd-x64": "0.28.2", - "@esbuild/linux-arm": "0.28.2", - "@esbuild/linux-arm64": "0.28.2", - "@esbuild/linux-ia32": "0.28.2", - "@esbuild/linux-loong64": "0.28.2", - "@esbuild/linux-mips64el": "0.28.2", - "@esbuild/linux-ppc64": "0.28.2", - "@esbuild/linux-riscv64": "0.28.2", - "@esbuild/linux-s390x": "0.28.2", - "@esbuild/linux-x64": "0.28.2", - "@esbuild/netbsd-arm64": "0.28.2", - "@esbuild/netbsd-x64": "0.28.2", - "@esbuild/openbsd-arm64": "0.28.2", - "@esbuild/openbsd-x64": "0.28.2", - "@esbuild/openharmony-arm64": "0.28.2", - "@esbuild/sunos-x64": "0.28.2", - "@esbuild/win32-arm64": "0.28.2", - "@esbuild/win32-ia32": "0.28.2", - "@esbuild/win32-x64": "0.28.2" - } - }, "node_modules/fumadocs-mdx/node_modules/magic-string": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.2.2.tgz", @@ -26176,17 +25702,6 @@ "node": ">= 14" } }, - "node_modules/libxmljs2/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/libxmljs2/node_modules/cacache": { "version": "19.0.1", "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", @@ -31633,7 +31148,9 @@ "version": "1.61.1", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, "license": "Apache-2.0", + "optional": true, "bin": { "playwright-core": "cli.js" }, @@ -33257,6 +32774,18 @@ } } }, + "node_modules/react18-json-view": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/react18-json-view/-/react18-json-view-0.2.10.tgz", + "integrity": "sha512-rYEbaCG/U4THY1qp1xY14/Kbnp9yY3W6Qm3Rmu+jlCIdxzMS5EcD+wI97kCKRoN3CuJyJU8hqkax5xWfl8A4EA==", + "license": "MIT", + "dependencies": { + "copy-to-clipboard": "^3.3.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/read-cmd-shim": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-6.0.0.tgz", @@ -36292,6 +35821,12 @@ "node": ">=8.0" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", diff --git a/package.json b/package.json index e75bea536d..0f29b3ac4b 100644 --- a/package.json +++ b/package.json @@ -323,6 +323,7 @@ "react-is": "^19.2.6", "react-markdown": "^10.1.0", "react-reconciler": "^0.33.0", + "react18-json-view": "0.2.10", "recharts": "^3.8.1", "safe-regex": "^2.1.1", "selfsigned": "^5.5.0", diff --git a/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/chat/ToolCallBlock.tsx b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/chat/ToolCallBlock.tsx index 6d01759e8f..2a4acc810d 100644 --- a/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/chat/ToolCallBlock.tsx +++ b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/chat/ToolCallBlock.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { JsonViewer } from "../shared/JsonViewer"; +import { shortCallId } from "@/shared/utils/formatting"; interface ToolCallBlockProps { id: string; @@ -23,7 +24,9 @@ export function ToolCallBlock({ id, name, input }: ToolCallBlockProps) { {expanded ? "expand_less" : "expand_more"} {name} - {id.slice(0, 8)} + + {shortCallId(id)} + {expanded && (
dump for anything that isn't valid JSON (e.g. a captured
+// error string), since json is display text sourced from JSON.stringify with
+// a String() fallback on failure -- it is not guaranteed parseable.
-export function PayloadSection({ title, json, onCopy, collapsible = true, defaultOpen = true }) {
+export function PayloadSection({
+ title,
+ sectionId,
+ json,
+ onCopy,
+ collapsible = true,
+ defaultOpen = true,
+}) {
const t = useTranslations("requestLogger.detail");
+ const { isDark } = useTheme();
+ const resolvedSectionId = sectionId || title;
+ const expandLevel = useJsonTreeExpandLevel(resolvedSectionId);
const [copied, setCopied] = useState(false);
const [open, setOpen] = useState(defaultOpen);
+ const treeContainerRef = useRef(null);
+
+ const parsedJson = useMemo(() => {
+ if (typeof json !== "string") return null;
+ try {
+ const value = JSON.parse(json);
+ return value !== null && typeof value === "object" ? value : null;
+ } catch {
+ return null;
+ }
+ }, [json]);
const handleCopy = async () => {
const success = await onCopy();
@@ -21,6 +58,8 @@ export function PayloadSection({ title, json, onCopy, collapsible = true, defaul
}
};
+ useTimestampTitles(treeContainerRef, open && parsedJson !== null);
+
return (
@@ -40,18 +79,35 @@ export function PayloadSection({ title, json, onCopy, collapsible = true, defaul
)}
-
+
+
+ {parsedJson !== null && }
+
- {open && (
+ {open && parsedJson !== null && (
+
+
+
+ )}
+ {open && parsedJson === null && (
{json}
diff --git a/src/shared/components/RequestLoggerDetail.tsx b/src/shared/components/RequestLoggerDetail.tsx
index 2da720a730..45fd9214e6 100644
--- a/src/shared/components/RequestLoggerDetail.tsx
+++ b/src/shared/components/RequestLoggerDetail.tsx
@@ -1,7 +1,10 @@
"use client";
-import { useState, useEffect, useRef } from "react";
+import { useState, useEffect, useMemo, useRef } from "react";
import { useLocale, useTranslations } from "next-intl";
+import JsonView from "react18-json-view";
+import "react18-json-view/src/style.css";
+import "react18-json-view/src/dark.css";
import {
PROVIDER_COLORS,
getHttpStatusStyle as getStatusStyle,
@@ -9,6 +12,13 @@ import {
} from "@/shared/constants/colors";
import { formatDuration, formatApiKeyLabel, maskAccount } from "@/shared/utils/formatting";
import { formatErrorForDisplay } from "@/shared/utils/formatting";
+import { useTheme } from "@/shared/hooks/useTheme";
+import {
+ useTimestampTitles,
+ timestampMarkerCustomizeNode,
+} from "@/shared/hooks/useTimestampTitles";
+import { JsonTreeExpandControls } from "@/shared/components/JsonTreeExpandControls";
+import { useJsonTreeExpandLevel } from "@/store/jsonTreeExpandStore";
import {
PayloadSection,
ConversationContextSection,
@@ -66,7 +76,44 @@ export function buildCopyAllText({
// ─── Stream section + Detail Modal ───────────────────────────────────────────────────────────
-function StreamSection({ title, json, onCopy }) {
+// Raw stream chunks are captured at the network level (see streamChunks
+// capture) with a `[HH:MM:SS.mmm] ` prefix inserted per chunk boundary, which
+// can land mid-token inside an SSE event's JSON payload once chunks are
+// joined. Strip those markers first so a `data:` line isn't interrupted.
+const STREAM_TIMESTAMP_PREFIX = /\[\d{2}:\d{2}:\d{2}\.\d{3}\] /g;
+
+type StreamSegment =
+ { type: "text"; value: string } | { type: "json"; value: unknown; raw: string };
+
+// Splits a raw joined SSE capture into renderable segments: each `data:`
+// line that parses as JSON becomes its own segment (rendered as a
+// collapsible tree), everything else (comments, keep-alives, [DONE],
+// non-JSON payloads) stays as plain text, byte-identical to the raw capture.
+function parseStreamIntoSegments(joined: string): StreamSegment[] {
+ const text = joined.replace(STREAM_TIMESTAMP_PREFIX, "");
+ const events = text.split(/(?<=\n\n)/); // keep event boundaries, preserve exact text
+ const segments: StreamSegment[] = [];
+ for (const event of events) {
+ if (!event) continue;
+ const dataLines = event
+ .split("\n")
+ .filter((line) => line.startsWith("data:"))
+ .map((line) => line.slice(5).trim());
+ const payload = dataLines.join("");
+ if (dataLines.length === 0 || !payload || payload === "[DONE]") {
+ segments.push({ type: "text", value: event });
+ continue;
+ }
+ try {
+ segments.push({ type: "json", value: JSON.parse(payload), raw: event });
+ } catch {
+ segments.push({ type: "text", value: event });
+ }
+ }
+ return segments;
+}
+
+function StreamSection({ title, sectionId, json, onCopy }) {
const t = useTranslations("requestLogger.detail");
const [copied, setCopied] = useState(false);
const [open, setOpen] = useState(true);
@@ -79,6 +126,10 @@ function StreamSection({ title, json, onCopy }) {
}
});
const ref = useRef(null);
+ const { isDark } = useTheme();
+ const resolvedSectionId = sectionId || title;
+ const expandLevel = useJsonTreeExpandLevel(resolvedSectionId);
+ const segments = useMemo(() => parseStreamIntoSegments(json), [json]);
const handleCopy = async () => {
const success = await onCopy();
@@ -108,6 +159,8 @@ function StreamSection({ title, json, onCopy }) {
} catch {}
};
+ useTimestampTitles(ref, open);
+
return (
@@ -144,14 +197,33 @@ function StreamSection({ title, json, onCopy }) {
{copied ? t("copied") : t("copy")}
+ {segments.some((s) => s.type === "json") && (
+
+ )}
{open && (
- {json}
+ {segments.map((segment, i) =>
+ segment.type === "json" ? (
+
+
+
+ ) : (
+
+ {segment.value}
+
+ )
+ )}
)}
text dump, forcing operators to
+// scroll through the entire raw JSON to find anything in a nested request.
+// It now renders parseable JSON payloads as a collapsible tree
+// (react-json-view-lite) instead, while still falling back to the plain
+// dump for anything that isn't valid JSON (e.g. a captured error
+// string), since the `json` prop is display text that isn't guaranteed
+// parseable.
+import React, { act } from "react";
+import { createRoot, type Root } from "react-dom/client";
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+
+vi.mock("next-intl", () => ({
+ useTranslations: () => (key: string) => key,
+}));
+
+vi.mock("@/shared/hooks/useTheme", () => ({
+ useTheme: () => ({ isDark: false }),
+}));
+
+const { PayloadSection } =
+ await import("../../../src/shared/components/RequestLoggerDetail.sections.tsx");
+
+let container: HTMLDivElement;
+let root: Root;
+
+beforeEach(() => {
+ container = document.createElement("div");
+ document.body.appendChild(container);
+ root = createRoot(container);
+});
+
+afterEach(() => {
+ act(() => root.unmount());
+ container.remove();
+ vi.clearAllMocks();
+});
+
+function renderPayload(json: string) {
+ act(() => {
+ root.render(
+
+ );
+ });
+}
+
+describe("PayloadSection collapsible JSON rendering", () => {
+ it("renders a valid JSON object payload as a collapsible tree, not a flat dump", () => {
+ renderPayload(JSON.stringify({ model: "gpt-5.6-luna", input: [{ role: "user" }] }, null, 2));
+
+ expect(container.querySelector("pre")).toBeNull();
+ // react-json-view-lite renders expand/collapse controls for object nodes.
+ expect(container.querySelectorAll("[aria-label]").length).toBeGreaterThan(0);
+ expect(container.textContent).toContain("model");
+ expect(container.textContent).toContain("gpt-5.6-luna");
+ });
+
+ it("renders a valid JSON array payload as a collapsible tree", () => {
+ renderPayload(JSON.stringify([{ type: "output_text", text: "answer" }], null, 2));
+
+ expect(container.querySelector("pre")).toBeNull();
+ expect(container.textContent).toContain("output_text");
+ });
+
+ it("falls back to the plain dump for non-JSON payload text", () => {
+ renderPayload("Upstream request failed: Model is unavailable.");
+
+ const pre = container.querySelector("pre");
+ expect(pre).not.toBeNull();
+ expect(pre?.textContent).toBe("Upstream request failed: Model is unavailable.");
+ });
+
+ it("falls back to the plain dump for a JSON primitive (not an object/array)", () => {
+ renderPayload(JSON.stringify("just a string"));
+
+ const pre = container.querySelector("pre");
+ expect(pre).not.toBeNull();
+ });
+});
diff --git a/tests/unit/dashboard/timestamp-titles.test.tsx b/tests/unit/dashboard/timestamp-titles.test.tsx
new file mode 100644
index 0000000000..15c6c645d2
--- /dev/null
+++ b/tests/unit/dashboard/timestamp-titles.test.tsx
@@ -0,0 +1,94 @@
+// @vitest-environment jsdom
+//
+// Regression guard: JSON tree values that are actually Unix timestamps
+// (e.g. `created: 1787562749` in an event-stream payload) rendered as a
+// bare number with no way to read the actual date/time without doing the
+// epoch math by hand. react-json-view-lite exposes no per-value render
+// hook, so useTimestampTitles walks the rendered tree and adds a
+// human-readable title/tooltip to number values whose field name looks
+// like a timestamp and whose value is a plausible Unix epoch -- without
+// changing the value actually displayed.
+import React, { act } from "react";
+import { createRoot, type Root } from "react-dom/client";
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+
+vi.mock("next-intl", () => ({
+ useTranslations: () => (key: string) => key,
+}));
+
+vi.mock("@/shared/hooks/useTheme", () => ({
+ useTheme: () => ({ isDark: false }),
+}));
+
+const { PayloadSection } =
+ await import("../../../src/shared/components/RequestLoggerDetail.sections.tsx");
+
+let container: HTMLDivElement;
+let root: Root;
+
+beforeEach(() => {
+ container = document.createElement("div");
+ document.body.appendChild(container);
+ root = createRoot(container);
+});
+
+afterEach(() => {
+ act(() => root.unmount());
+ container.remove();
+ vi.clearAllMocks();
+});
+
+function renderPayload(payload: unknown) {
+ act(() => {
+ root.render(
+
+ );
+ });
+}
+
+function findValueSpan(text: string): HTMLElement | undefined {
+ return Array.from(container.querySelectorAll("span")).find(
+ (el) => el.children.length === 0 && el.textContent === text
+ );
+}
+
+describe("timestamp title tooltips in JSON trees", () => {
+ it("adds a human-readable title to a field named created with a plausible epoch-seconds value", () => {
+ renderPayload({ id: "gen-1", created: 1787562749, model: "nemotron-3-ultra-free" });
+
+ const valueSpan = findValueSpan("1787562749");
+ expect(valueSpan).toBeDefined();
+ expect(valueSpan?.title).not.toBe("");
+ // 1787562749 seconds since epoch -> 2026-08-24-ish; just confirm it parsed
+ // as a real, non-garbage date rather than asserting an exact locale string.
+ expect(valueSpan?.title).toMatch(/202\d/);
+ });
+
+ it("does not add a title to a plain number field that only happens to share the epoch-seconds range", () => {
+ renderPayload({ id: "gen-1", max_tokens: 1787562749 });
+
+ const valueSpan = findValueSpan("1787562749");
+ expect(valueSpan).toBeDefined();
+ expect(valueSpan?.title).toBe("");
+ });
+
+ it("does not add a title to a timestamp-named field whose value is not a plausible epoch", () => {
+ renderPayload({ created: 42 });
+
+ const valueSpan = findValueSpan("42");
+ expect(valueSpan).toBeDefined();
+ expect(valueSpan?.title).toBe("");
+ });
+
+ it("leaves the displayed value unchanged -- only the title attribute is added", () => {
+ renderPayload({ created: 1787562749 });
+
+ const valueSpan = findValueSpan("1787562749");
+ expect(valueSpan?.textContent).toBe("1787562749");
+ });
+});
diff --git a/tests/unit/shared/json-tree-expand-store.test.ts b/tests/unit/shared/json-tree-expand-store.test.ts
new file mode 100644
index 0000000000..b202e27d3f
--- /dev/null
+++ b/tests/unit/shared/json-tree-expand-store.test.ts
@@ -0,0 +1,57 @@
+import assert from "node:assert/strict";
+import test, { beforeEach } from "node:test";
+import useJsonTreeExpandStore, {
+ DEFAULT_JSON_TREE_EXPAND_LEVEL,
+} from "../../../src/store/jsonTreeExpandStore.ts";
+
+// Regression guard: collapse/expand-level controls track one level per
+// section (sectionId), not one global level for the whole page -- different
+// payload/stream boxes carry "interesting" data at different nesting depths.
+// Level is 0-indexed to match react-json-view-lite's own
+// shouldExpandNode(level) convention: 0 means nothing is expanded.
+
+function reset() {
+ useJsonTreeExpandStore.setState({ levels: {} });
+}
+
+beforeEach(reset);
+
+test("an unset section defaults to the default expand level", () => {
+ assert.equal(useJsonTreeExpandStore.getState().levels.openaiRequest, undefined);
+});
+
+test("collapseAll sets only the given section's level to 0", () => {
+ useJsonTreeExpandStore.getState().collapseAll("openaiRequest");
+ assert.equal(useJsonTreeExpandStore.getState().levels.openaiRequest, 0);
+ assert.equal(useJsonTreeExpandStore.getState().levels.providerRequest, undefined);
+});
+
+test("expandAll sets only the given section's level to the max depth", () => {
+ useJsonTreeExpandStore.getState().expandAll("openaiRequest");
+ assert.equal(useJsonTreeExpandStore.getState().levels.openaiRequest, 64);
+ assert.equal(useJsonTreeExpandStore.getState().levels.providerRequest, undefined);
+});
+
+test("collapseOneLevel decrements from the default and clamps at 0, independently per section", () => {
+ const { collapseOneLevel } = useJsonTreeExpandStore.getState();
+ collapseOneLevel("openaiRequest");
+ assert.equal(
+ useJsonTreeExpandStore.getState().levels.openaiRequest,
+ DEFAULT_JSON_TREE_EXPAND_LEVEL - 1
+ );
+ assert.equal(useJsonTreeExpandStore.getState().levels.providerRequest, undefined);
+
+ collapseOneLevel("openaiRequest");
+ collapseOneLevel("openaiRequest");
+ collapseOneLevel("openaiRequest");
+ assert.equal(useJsonTreeExpandStore.getState().levels.openaiRequest, 0);
+});
+
+test("expandOneLevel increments and clamps at the max, independently per section", () => {
+ useJsonTreeExpandStore.setState({ levels: { openaiRequest: 63 } });
+ const { expandOneLevel } = useJsonTreeExpandStore.getState();
+ expandOneLevel("openaiRequest");
+ assert.equal(useJsonTreeExpandStore.getState().levels.openaiRequest, 64);
+ expandOneLevel("openaiRequest");
+ assert.equal(useJsonTreeExpandStore.getState().levels.openaiRequest, 64);
+});
diff --git a/tests/unit/shared/short-call-id.test.ts b/tests/unit/shared/short-call-id.test.ts
new file mode 100644
index 0000000000..850f49786e
--- /dev/null
+++ b/tests/unit/shared/short-call-id.test.ts
@@ -0,0 +1,38 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { shortCallId } from "../../../src/shared/utils/formatting.ts";
+
+// Regression guard: naive id.slice(0, 8) truncation showed as little as 3
+// significant characters for ids with decorative prefixes (call-, call_call-,
+// etc.), making tool-call/tool-result ids visually indistinguishable in the
+// conversation context view. shortCallId finds the actual guid/hex entropy
+// and truncates that instead, git-log-short-hash style.
+
+test("extracts a git-log-style short id from a real UUID-shaped call id", () => {
+ assert.equal(shortCallId("call-bf7c895d-ae74-421c-890b-6f47eab77508"), "bf7c895");
+});
+
+test("extracts the first uuid segment from a composite/doubled-prefix id", () => {
+ assert.equal(
+ shortCallId("call_call-c5602375-cb46-40e8-867a-4b6ac50f6d2d_fc_cal_f08ecb5ec5"),
+ "c560237"
+ );
+});
+
+test("falls back to a leading hex run when there is no full uuid", () => {
+ assert.equal(shortCallId("toolu_01a2b3c4d5e6f7089900"), "01a2b3c");
+});
+
+test("falls back to a plain slice when the id has no recognizable hex/uuid segment", () => {
+ assert.equal(shortCallId("call_1"), "call_1");
+});
+
+test("respects a custom length", () => {
+ assert.equal(shortCallId("call-bf7c895d-ae74-421c-890b-6f47eab77508", 4), "bf7c");
+});
+
+test("returns an empty string for missing input", () => {
+ assert.equal(shortCallId(null), "");
+ assert.equal(shortCallId(undefined), "");
+ assert.equal(shortCallId(""), "");
+});