mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-31 03:22:19 +03:00
feat(dashboard): collapsible JSON tree viewer for request/response payloads (#11703)
Resynced onto the release tip. Two fixes applied during boarding: (1) the branch forked before the recent optionalDependencies placement of @huggingface/transformers and onnxruntime-node — its own diff re-added both into "dependencies" as duplicates alongside the real new dependency (react18-json-view); removed the duplicates, ran npm install to sync the lockfile. (2) config/quality/dependency-allowlist.json referenced the wrong package name (react-json-view-lite, an earlier iteration per the PR body) — the code actually imports react18-json-view; fixed the allowlist entry to match. RequestLoggerDetail.tsx crossed its frozen file-size cap (1018->1111); rebaselined with a note — the PR does split out the new logic (RequestLoggerDetail.sections.tsx, JsonTreeExpandControls.tsx, useTimestampTitles.ts, jsonTreeExpandStore.ts, all well under cap), the growth here is irreducible wiring. typecheck:core, check:dashboard-typecheck, check:file-size, check-deps all green after resync; 8/8 vitest + 11/11 native tests pass. Nice, well-structured 6-commit feature with full i18n and good test coverage. Thanks!
This commit is contained in:
@@ -119,6 +119,7 @@
|
||||
"react",
|
||||
"react-dom",
|
||||
"react-is",
|
||||
"react18-json-view",
|
||||
"react-markdown",
|
||||
"react-reconciler",
|
||||
"recharts",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
525
package-lock.json
generated
525
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"}
|
||||
</span>
|
||||
<span className="text-amber-300 font-mono font-medium">{name}</span>
|
||||
<span className="text-text-muted text-xs font-mono ml-auto">{id.slice(0, 8)}</span>
|
||||
<span className="text-text-muted text-xs font-mono ml-auto" title={id}>
|
||||
{shortCallId(id)}
|
||||
</span>
|
||||
</button>
|
||||
{expanded && (
|
||||
<div className="mt-2 border-t border-amber-500/20 pt-2">
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { JsonViewer } from "../shared/JsonViewer";
|
||||
import { shortCallId } from "@/shared/utils/formatting";
|
||||
|
||||
interface ToolResultBlockProps {
|
||||
toolUseId: string;
|
||||
@@ -22,7 +23,9 @@ export function ToolResultBlock({ toolUseId, content }: ToolResultBlockProps) {
|
||||
{expanded ? "expand_less" : "expand_more"}
|
||||
</span>
|
||||
<span className="text-green-300 font-mono font-medium text-xs">tool_result</span>
|
||||
<span className="text-text-muted text-xs font-mono ml-auto">{toolUseId.slice(0, 8)}</span>
|
||||
<span className="text-text-muted text-xs font-mono ml-auto" title={toolUseId}>
|
||||
{shortCallId(toolUseId)}
|
||||
</span>
|
||||
</button>
|
||||
{expanded && (
|
||||
<div className="mt-2 border-t border-green-500/20 pt-2">
|
||||
|
||||
@@ -378,6 +378,20 @@ button .material-symbols-outlined,
|
||||
"opsz" 24;
|
||||
}
|
||||
|
||||
/* react18-json-view fold control: the SVG chevron itself is ~14px, too small
|
||||
a touch target on mobile. Padding + a matching negative margin grows the
|
||||
hit area without shifting sibling layout -- unlike the row-level icon this
|
||||
replaces (see the reverted react-json-view-lite alignment attempt), this
|
||||
click target is a trailing decoration appended after the object/array's
|
||||
opening brace, not a leading element sibling keys align against, so
|
||||
padding it here cannot reproduce that regression. */
|
||||
.jv-size-chevron {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 10px 4px;
|
||||
margin: -10px -4px;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes spin {
|
||||
from {
|
||||
@@ -506,83 +520,294 @@ select option {
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
RTL (Right-to-Left) Compatibility — fixes #7680
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
html[dir="rtl"] .text-left { text-align: start; }
|
||||
html[dir="rtl"] .text-right { text-align: end; }
|
||||
html[dir="rtl"] .ml-0 { --rtl-ms: 0; }
|
||||
html[dir="rtl"] .ml-0\.5 { --rtl-ms: 0.5; }
|
||||
html[dir="rtl"] .ml-1 { --rtl-ms: 1; }
|
||||
html[dir="rtl"] .ml-1\.5 { --rtl-ms: 1.5; }
|
||||
html[dir="rtl"] .ml-2 { --rtl-ms: 2; }
|
||||
html[dir="rtl"] .ml-3 { --rtl-ms: 3; }
|
||||
html[dir="rtl"] .ml-4 { --rtl-ms: 4; }
|
||||
html[dir="rtl"] .ml-5 { --rtl-ms: 5; }
|
||||
html[dir="rtl"] .ml-6 { --rtl-ms: 6; }
|
||||
html[dir="rtl"] .ml-7 { --rtl-ms: 7; }
|
||||
html[dir="rtl"] :is(.ml-0, .ml-0\.5, .ml-1, .ml-1\.5, .ml-2, .ml-3, .ml-4, .ml-5, .ml-6, .ml-7) { margin-left: 0; margin-inline-start: calc(var(--spacing) * var(--rtl-ms)); }
|
||||
html[dir="rtl"] .ml-auto { margin-left: 0; margin-inline-start: auto; }
|
||||
html[dir="rtl"] .mr-0\.5 { --rtl-me: 0.5; }
|
||||
html[dir="rtl"] .mr-1 { --rtl-me: 1; }
|
||||
html[dir="rtl"] .mr-2 { --rtl-me: 2; }
|
||||
html[dir="rtl"] .mr-3 { --rtl-me: 3; }
|
||||
html[dir="rtl"] :is(.mr-0\.5, .mr-1, .mr-2, .mr-3) { margin-right: 0; margin-inline-end: calc(var(--spacing) * var(--rtl-me)); }
|
||||
html[dir="rtl"] .mr-auto { margin-right: 0; margin-inline-end: auto; }
|
||||
html[dir="rtl"] .pl-1 { --rtl-ps: 1; }
|
||||
html[dir="rtl"] .pl-2 { --rtl-ps: 2; }
|
||||
html[dir="rtl"] .pl-3 { --rtl-ps: 3; }
|
||||
html[dir="rtl"] .pl-4 { --rtl-ps: 4; }
|
||||
html[dir="rtl"] .pl-5 { --rtl-ps: 5; }
|
||||
html[dir="rtl"] .pl-6 { --rtl-ps: 6; }
|
||||
html[dir="rtl"] .pl-7 { --rtl-ps: 7; }
|
||||
html[dir="rtl"] .pl-8 { --rtl-ps: 8; }
|
||||
html[dir="rtl"] .pl-9 { --rtl-ps: 9; }
|
||||
html[dir="rtl"] .pl-10 { --rtl-ps: 10; }
|
||||
html[dir="rtl"] .pl-12 { --rtl-ps: 12; }
|
||||
html[dir="rtl"] :is(.pl-1, .pl-2, .pl-3, .pl-4, .pl-5, .pl-6, .pl-7, .pl-8, .pl-9, .pl-10, .pl-12) { padding-left: 0; padding-inline-start: calc(var(--spacing) * var(--rtl-ps)); }
|
||||
html[dir="rtl"] .pr-1 { --rtl-pe: 1; }
|
||||
html[dir="rtl"] .pr-2 { --rtl-pe: 2; }
|
||||
html[dir="rtl"] .pr-2\.5 { --rtl-pe: 2.5; }
|
||||
html[dir="rtl"] .pr-3 { --rtl-pe: 3; }
|
||||
html[dir="rtl"] .pr-4 { --rtl-pe: 4; }
|
||||
html[dir="rtl"] .pr-9 { --rtl-pe: 9; }
|
||||
html[dir="rtl"] .pr-10 { --rtl-pe: 10; }
|
||||
html[dir="rtl"] :is(.pr-1, .pr-2, .pr-2\.5, .pr-3, .pr-4, .pr-9, .pr-10) { padding-right: 0; padding-inline-end: calc(var(--spacing) * var(--rtl-pe)); }
|
||||
html[dir="rtl"] .left-0 { --rtl-is: 0; }
|
||||
html[dir="rtl"] .left-0\.5 { --rtl-is: calc(var(--spacing) * 0.5); }
|
||||
html[dir="rtl"] .left-1 { --rtl-is: calc(var(--spacing) * 1); }
|
||||
html[dir="rtl"] .left-1\.5 { --rtl-is: calc(var(--spacing) * 1.5); }
|
||||
html[dir="rtl"] .left-2 { --rtl-is: calc(var(--spacing) * 2); }
|
||||
html[dir="rtl"] .left-2\.5 { --rtl-is: calc(var(--spacing) * 2.5); }
|
||||
html[dir="rtl"] .left-3 { --rtl-is: calc(var(--spacing) * 3); }
|
||||
html[dir="rtl"] .left-5 { --rtl-is: calc(var(--spacing) * 5); }
|
||||
html[dir="rtl"] .left-1\/4 { --rtl-is: 25%; }
|
||||
html[dir="rtl"] .left-1\/2 { --rtl-is: 50%; }
|
||||
html[dir="rtl"] .left-\[16\%\] { --rtl-is: 16%; }
|
||||
html[dir="rtl"] :is(.left-0, .left-0\.5, .left-1, .left-1\.5, .left-2, .left-2\.5, .left-3, .left-5, .left-1\/4, .left-1\/2, .left-\[16\%\]) { left: auto; inset-inline-start: var(--rtl-is); }
|
||||
html[dir="rtl"] .right-0 { --rtl-ie: 0; }
|
||||
html[dir="rtl"] .right-1 { --rtl-ie: calc(var(--spacing) * 1); }
|
||||
html[dir="rtl"] .right-2 { --rtl-ie: calc(var(--spacing) * 2); }
|
||||
html[dir="rtl"] .right-3 { --rtl-ie: calc(var(--spacing) * 3); }
|
||||
html[dir="rtl"] .right-4 { --rtl-ie: calc(var(--spacing) * 4); }
|
||||
html[dir="rtl"] .right-1\/4 { --rtl-ie: 25%; }
|
||||
html[dir="rtl"] .right-\[16\%\] { --rtl-ie: 16%; }
|
||||
html[dir="rtl"] .-right-2\.5 { --rtl-ie: calc(var(--spacing) * -2.5); }
|
||||
html[dir="rtl"] .-right-20 { --rtl-ie: calc(var(--spacing) * -20); }
|
||||
html[dir="rtl"] :is(.right-0, .right-1, .right-2, .right-3, .right-4, .right-1\/4, .right-\[16\%\], .-right-2\.5, .-right-20) { right: auto; inset-inline-end: var(--rtl-ie); }
|
||||
html[dir="rtl"] .border-l-2 { --rtl-bw: 2px; }
|
||||
html[dir="rtl"] .border-l-4 { --rtl-bw: 4px; }
|
||||
html[dir="rtl"] .border-l-\[3px\] { --rtl-bw: 3px; }
|
||||
html[dir="rtl"] :is(.border-l-2, .border-l-4, .border-l-\[3px\]) { border-left-width: 0; border-inline-start-width: var(--rtl-bw); }
|
||||
html[dir="rtl"] .border-l-primary { border-left-color: var(--color-border); border-inline-start-color: var(--color-primary); }
|
||||
html[dir="rtl"] .border-r { border-right-width: 0; border-inline-end-width: 1px; }
|
||||
html[dir="rtl"] .last\:border-r-0:last-child { border-left-width: 0; }
|
||||
html[dir="rtl"] .rounded-r-lg { border-top-right-radius: 0; border-bottom-right-radius: 0; border-start-end-radius: var(--radius-lg); border-end-end-radius: var(--radius-lg); }
|
||||
html[dir="rtl"] .translate-x-0\.5 { --rtl-tx: -0.5; }
|
||||
html[dir="rtl"] .translate-x-1 { --rtl-tx: -1; }
|
||||
html[dir="rtl"] .translate-x-4 { --rtl-tx: -4; }
|
||||
html[dir="rtl"] .translate-x-5 { --rtl-tx: -5; }
|
||||
html[dir="rtl"] .translate-x-6 { --rtl-tx: -6; }
|
||||
html[dir="rtl"] :is(.translate-x-0\.5, .translate-x-1, .translate-x-4, .translate-x-5, .translate-x-6) { translate: calc(var(--spacing) * var(--rtl-tx)) var(--tw-translate-y); }
|
||||
html[dir="rtl"] .-translate-x-1\/2 { translate: 50% var(--tw-translate-y); }
|
||||
@media (min-width: 48rem) { html[dir="rtl"] .md\:ml-2 { margin-left: 0; margin-inline-start: calc(var(--spacing) * 2); } html[dir="rtl"] .md\:ml-auto { margin-left: 0; margin-inline-start: auto; } html[dir="rtl"] .md\:pl-6 { padding-left: 0; padding-inline-start: calc(var(--spacing) * 6); } }
|
||||
html[dir="rtl"] :where(code, pre, kbd, samp, input[type="email"], input[type="url"]) { direction: ltr; text-align: left; }
|
||||
html[dir="rtl"] :where(.material-symbols-outlined, .traffic-lights, .react-flow) { direction: ltr; }
|
||||
html[dir="rtl"] .text-left {
|
||||
text-align: start;
|
||||
}
|
||||
html[dir="rtl"] .text-right {
|
||||
text-align: end;
|
||||
}
|
||||
html[dir="rtl"] .ml-0 {
|
||||
--rtl-ms: 0;
|
||||
}
|
||||
html[dir="rtl"] .ml-0\.5 {
|
||||
--rtl-ms: 0.5;
|
||||
}
|
||||
html[dir="rtl"] .ml-1 {
|
||||
--rtl-ms: 1;
|
||||
}
|
||||
html[dir="rtl"] .ml-1\.5 {
|
||||
--rtl-ms: 1.5;
|
||||
}
|
||||
html[dir="rtl"] .ml-2 {
|
||||
--rtl-ms: 2;
|
||||
}
|
||||
html[dir="rtl"] .ml-3 {
|
||||
--rtl-ms: 3;
|
||||
}
|
||||
html[dir="rtl"] .ml-4 {
|
||||
--rtl-ms: 4;
|
||||
}
|
||||
html[dir="rtl"] .ml-5 {
|
||||
--rtl-ms: 5;
|
||||
}
|
||||
html[dir="rtl"] .ml-6 {
|
||||
--rtl-ms: 6;
|
||||
}
|
||||
html[dir="rtl"] .ml-7 {
|
||||
--rtl-ms: 7;
|
||||
}
|
||||
html[dir="rtl"] :is(.ml-0, .ml-0\.5, .ml-1, .ml-1\.5, .ml-2, .ml-3, .ml-4, .ml-5, .ml-6, .ml-7) {
|
||||
margin-left: 0;
|
||||
margin-inline-start: calc(var(--spacing) * var(--rtl-ms));
|
||||
}
|
||||
html[dir="rtl"] .ml-auto {
|
||||
margin-left: 0;
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
html[dir="rtl"] .mr-0\.5 {
|
||||
--rtl-me: 0.5;
|
||||
}
|
||||
html[dir="rtl"] .mr-1 {
|
||||
--rtl-me: 1;
|
||||
}
|
||||
html[dir="rtl"] .mr-2 {
|
||||
--rtl-me: 2;
|
||||
}
|
||||
html[dir="rtl"] .mr-3 {
|
||||
--rtl-me: 3;
|
||||
}
|
||||
html[dir="rtl"] :is(.mr-0\.5, .mr-1, .mr-2, .mr-3) {
|
||||
margin-right: 0;
|
||||
margin-inline-end: calc(var(--spacing) * var(--rtl-me));
|
||||
}
|
||||
html[dir="rtl"] .mr-auto {
|
||||
margin-right: 0;
|
||||
margin-inline-end: auto;
|
||||
}
|
||||
html[dir="rtl"] .pl-1 {
|
||||
--rtl-ps: 1;
|
||||
}
|
||||
html[dir="rtl"] .pl-2 {
|
||||
--rtl-ps: 2;
|
||||
}
|
||||
html[dir="rtl"] .pl-3 {
|
||||
--rtl-ps: 3;
|
||||
}
|
||||
html[dir="rtl"] .pl-4 {
|
||||
--rtl-ps: 4;
|
||||
}
|
||||
html[dir="rtl"] .pl-5 {
|
||||
--rtl-ps: 5;
|
||||
}
|
||||
html[dir="rtl"] .pl-6 {
|
||||
--rtl-ps: 6;
|
||||
}
|
||||
html[dir="rtl"] .pl-7 {
|
||||
--rtl-ps: 7;
|
||||
}
|
||||
html[dir="rtl"] .pl-8 {
|
||||
--rtl-ps: 8;
|
||||
}
|
||||
html[dir="rtl"] .pl-9 {
|
||||
--rtl-ps: 9;
|
||||
}
|
||||
html[dir="rtl"] .pl-10 {
|
||||
--rtl-ps: 10;
|
||||
}
|
||||
html[dir="rtl"] .pl-12 {
|
||||
--rtl-ps: 12;
|
||||
}
|
||||
html[dir="rtl"] :is(.pl-1, .pl-2, .pl-3, .pl-4, .pl-5, .pl-6, .pl-7, .pl-8, .pl-9, .pl-10, .pl-12) {
|
||||
padding-left: 0;
|
||||
padding-inline-start: calc(var(--spacing) * var(--rtl-ps));
|
||||
}
|
||||
html[dir="rtl"] .pr-1 {
|
||||
--rtl-pe: 1;
|
||||
}
|
||||
html[dir="rtl"] .pr-2 {
|
||||
--rtl-pe: 2;
|
||||
}
|
||||
html[dir="rtl"] .pr-2\.5 {
|
||||
--rtl-pe: 2.5;
|
||||
}
|
||||
html[dir="rtl"] .pr-3 {
|
||||
--rtl-pe: 3;
|
||||
}
|
||||
html[dir="rtl"] .pr-4 {
|
||||
--rtl-pe: 4;
|
||||
}
|
||||
html[dir="rtl"] .pr-9 {
|
||||
--rtl-pe: 9;
|
||||
}
|
||||
html[dir="rtl"] .pr-10 {
|
||||
--rtl-pe: 10;
|
||||
}
|
||||
html[dir="rtl"] :is(.pr-1, .pr-2, .pr-2\.5, .pr-3, .pr-4, .pr-9, .pr-10) {
|
||||
padding-right: 0;
|
||||
padding-inline-end: calc(var(--spacing) * var(--rtl-pe));
|
||||
}
|
||||
html[dir="rtl"] .left-0 {
|
||||
--rtl-is: 0;
|
||||
}
|
||||
html[dir="rtl"] .left-0\.5 {
|
||||
--rtl-is: calc(var(--spacing) * 0.5);
|
||||
}
|
||||
html[dir="rtl"] .left-1 {
|
||||
--rtl-is: calc(var(--spacing) * 1);
|
||||
}
|
||||
html[dir="rtl"] .left-1\.5 {
|
||||
--rtl-is: calc(var(--spacing) * 1.5);
|
||||
}
|
||||
html[dir="rtl"] .left-2 {
|
||||
--rtl-is: calc(var(--spacing) * 2);
|
||||
}
|
||||
html[dir="rtl"] .left-2\.5 {
|
||||
--rtl-is: calc(var(--spacing) * 2.5);
|
||||
}
|
||||
html[dir="rtl"] .left-3 {
|
||||
--rtl-is: calc(var(--spacing) * 3);
|
||||
}
|
||||
html[dir="rtl"] .left-5 {
|
||||
--rtl-is: calc(var(--spacing) * 5);
|
||||
}
|
||||
html[dir="rtl"] .left-1\/4 {
|
||||
--rtl-is: 25%;
|
||||
}
|
||||
html[dir="rtl"] .left-1\/2 {
|
||||
--rtl-is: 50%;
|
||||
}
|
||||
html[dir="rtl"] .left-\[16\%\] {
|
||||
--rtl-is: 16%;
|
||||
}
|
||||
html[dir="rtl"]
|
||||
:is(
|
||||
.left-0,
|
||||
.left-0\.5,
|
||||
.left-1,
|
||||
.left-1\.5,
|
||||
.left-2,
|
||||
.left-2\.5,
|
||||
.left-3,
|
||||
.left-5,
|
||||
.left-1\/4,
|
||||
.left-1\/2,
|
||||
.left-\[16\%\]
|
||||
) {
|
||||
left: auto;
|
||||
inset-inline-start: var(--rtl-is);
|
||||
}
|
||||
html[dir="rtl"] .right-0 {
|
||||
--rtl-ie: 0;
|
||||
}
|
||||
html[dir="rtl"] .right-1 {
|
||||
--rtl-ie: calc(var(--spacing) * 1);
|
||||
}
|
||||
html[dir="rtl"] .right-2 {
|
||||
--rtl-ie: calc(var(--spacing) * 2);
|
||||
}
|
||||
html[dir="rtl"] .right-3 {
|
||||
--rtl-ie: calc(var(--spacing) * 3);
|
||||
}
|
||||
html[dir="rtl"] .right-4 {
|
||||
--rtl-ie: calc(var(--spacing) * 4);
|
||||
}
|
||||
html[dir="rtl"] .right-1\/4 {
|
||||
--rtl-ie: 25%;
|
||||
}
|
||||
html[dir="rtl"] .right-\[16\%\] {
|
||||
--rtl-ie: 16%;
|
||||
}
|
||||
html[dir="rtl"] .-right-2\.5 {
|
||||
--rtl-ie: calc(var(--spacing) * -2.5);
|
||||
}
|
||||
html[dir="rtl"] .-right-20 {
|
||||
--rtl-ie: calc(var(--spacing) * -20);
|
||||
}
|
||||
html[dir="rtl"]
|
||||
:is(
|
||||
.right-0,
|
||||
.right-1,
|
||||
.right-2,
|
||||
.right-3,
|
||||
.right-4,
|
||||
.right-1\/4,
|
||||
.right-\[16\%\],
|
||||
.-right-2\.5,
|
||||
.-right-20
|
||||
) {
|
||||
right: auto;
|
||||
inset-inline-end: var(--rtl-ie);
|
||||
}
|
||||
html[dir="rtl"] .border-l-2 {
|
||||
--rtl-bw: 2px;
|
||||
}
|
||||
html[dir="rtl"] .border-l-4 {
|
||||
--rtl-bw: 4px;
|
||||
}
|
||||
html[dir="rtl"] .border-l-\[3px\] {
|
||||
--rtl-bw: 3px;
|
||||
}
|
||||
html[dir="rtl"] :is(.border-l-2, .border-l-4, .border-l-\[3px\]) {
|
||||
border-left-width: 0;
|
||||
border-inline-start-width: var(--rtl-bw);
|
||||
}
|
||||
html[dir="rtl"] .border-l-primary {
|
||||
border-left-color: var(--color-border);
|
||||
border-inline-start-color: var(--color-primary);
|
||||
}
|
||||
html[dir="rtl"] .border-r {
|
||||
border-right-width: 0;
|
||||
border-inline-end-width: 1px;
|
||||
}
|
||||
html[dir="rtl"] .last\:border-r-0:last-child {
|
||||
border-left-width: 0;
|
||||
}
|
||||
html[dir="rtl"] .rounded-r-lg {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-start-end-radius: var(--radius-lg);
|
||||
border-end-end-radius: var(--radius-lg);
|
||||
}
|
||||
html[dir="rtl"] .translate-x-0\.5 {
|
||||
--rtl-tx: -0.5;
|
||||
}
|
||||
html[dir="rtl"] .translate-x-1 {
|
||||
--rtl-tx: -1;
|
||||
}
|
||||
html[dir="rtl"] .translate-x-4 {
|
||||
--rtl-tx: -4;
|
||||
}
|
||||
html[dir="rtl"] .translate-x-5 {
|
||||
--rtl-tx: -5;
|
||||
}
|
||||
html[dir="rtl"] .translate-x-6 {
|
||||
--rtl-tx: -6;
|
||||
}
|
||||
html[dir="rtl"]
|
||||
:is(.translate-x-0\.5, .translate-x-1, .translate-x-4, .translate-x-5, .translate-x-6) {
|
||||
translate: calc(var(--spacing) * var(--rtl-tx)) var(--tw-translate-y);
|
||||
}
|
||||
html[dir="rtl"] .-translate-x-1\/2 {
|
||||
translate: 50% var(--tw-translate-y);
|
||||
}
|
||||
@media (min-width: 48rem) {
|
||||
html[dir="rtl"] .md\:ml-2 {
|
||||
margin-left: 0;
|
||||
margin-inline-start: calc(var(--spacing) * 2);
|
||||
}
|
||||
html[dir="rtl"] .md\:ml-auto {
|
||||
margin-left: 0;
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
html[dir="rtl"] .md\:pl-6 {
|
||||
padding-left: 0;
|
||||
padding-inline-start: calc(var(--spacing) * 6);
|
||||
}
|
||||
}
|
||||
html[dir="rtl"] :where(code, pre, kbd, samp, input[type="email"], input[type="url"]) {
|
||||
direction: ltr;
|
||||
text-align: left;
|
||||
}
|
||||
html[dir="rtl"] :where(.material-symbols-outlined, .traffic-lights, .react-flow) {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
@@ -11258,6 +11258,11 @@
|
||||
"copy": "Copy",
|
||||
"autoscrollOn": "Autoscroll: on",
|
||||
"autoscrollOff": "Autoscroll: off",
|
||||
"collapseAllLevels": "Collapse all",
|
||||
"collapseOneLevel": "Collapse one level",
|
||||
"currentExpandLevel": "Current expand level",
|
||||
"expandOneLevel": "Expand one level",
|
||||
"expandAllLevels": "Expand all",
|
||||
"payload": {
|
||||
"clientRawRequest": "Client Raw Request",
|
||||
"clientRequest": "Client Request",
|
||||
|
||||
@@ -11258,6 +11258,11 @@
|
||||
"copy": "Copiar",
|
||||
"autoscrollOn": "Rolagem Automática: ativada",
|
||||
"autoscrollOff": "Deslocamento Automático: desligado",
|
||||
"collapseAllLevels": "Recolher tudo",
|
||||
"collapseOneLevel": "Recolher um nível",
|
||||
"currentExpandLevel": "Nível de expansão atual",
|
||||
"expandOneLevel": "Expandir um nível",
|
||||
"expandAllLevels": "Expandir tudo",
|
||||
"payload": {
|
||||
"clientRawRequest": "Solicitação Bruta do Cliente",
|
||||
"clientRequest": "Solicitação do Cliente",
|
||||
|
||||
69
src/shared/components/JsonTreeExpandControls.tsx
Normal file
69
src/shared/components/JsonTreeExpandControls.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import useJsonTreeExpandStore, { useJsonTreeExpandLevel } from "@/store/jsonTreeExpandStore";
|
||||
|
||||
interface JsonTreeExpandControlsProps {
|
||||
/** Stable, locale-independent identifier for this box (e.g. "openaiRequest",
|
||||
* "providerEventStream") -- NOT the translated title. Each sectionId tracks
|
||||
* its own expand level, shared across different log entries but independent
|
||||
* from every other section on the page, since different payload/stream
|
||||
* boxes carry "interesting" data at different nesting depths. */
|
||||
sectionId: string;
|
||||
}
|
||||
|
||||
export function JsonTreeExpandControls({ sectionId }: JsonTreeExpandControlsProps) {
|
||||
const t = useTranslations("requestLogger.detail");
|
||||
const level = useJsonTreeExpandLevel(sectionId);
|
||||
const { collapseAll, collapseOneLevel, expandOneLevel, expandAll } = useJsonTreeExpandStore();
|
||||
|
||||
const buttonClass =
|
||||
"p-1 rounded hover:bg-bg-subtle text-text-muted hover:text-text-primary transition-colors";
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => collapseAll(sectionId)}
|
||||
title={t("collapseAllLevels")}
|
||||
aria-label={t("collapseAllLevels")}
|
||||
className={buttonClass}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">collapse_all</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => collapseOneLevel(sectionId)}
|
||||
title={t("collapseOneLevel")}
|
||||
aria-label={t("collapseOneLevel")}
|
||||
className={buttonClass}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">remove</span>
|
||||
</button>
|
||||
<span
|
||||
className="min-w-[1.5em] text-center text-[11px] font-mono text-text-muted tabular-nums"
|
||||
title={t("currentExpandLevel")}
|
||||
>
|
||||
{level}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => expandOneLevel(sectionId)}
|
||||
title={t("expandOneLevel")}
|
||||
aria-label={t("expandOneLevel")}
|
||||
className={buttonClass}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">add</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => expandAll(sectionId)}
|
||||
title={t("expandAllLevels")}
|
||||
aria-label={t("expandAllLevels")}
|
||||
className={buttonClass}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">expand_all</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useState, useEffect, useRef, useMemo } from "react";
|
||||
import { 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 { ChatBubble } from "@/app/(dashboard)/dashboard/tools/traffic-inspector/components/chat/ChatBubble";
|
||||
import { buildRequestTurns, buildResponseTurns } from "@/mitm/inspector/conversationNormalizer";
|
||||
import type { InterceptedRequest, NormalizedTurn } from "@/mitm/inspector/types";
|
||||
import { useTheme } from "@/shared/hooks/useTheme";
|
||||
import {
|
||||
useTimestampTitles,
|
||||
timestampMarkerCustomizeNode,
|
||||
} from "@/shared/hooks/useTimestampTitles";
|
||||
import { JsonTreeExpandControls } from "@/shared/components/JsonTreeExpandControls";
|
||||
import { useJsonTreeExpandLevel } from "@/store/jsonTreeExpandStore";
|
||||
|
||||
// ─── Payload Code Block ─────────────────────────────────────────────────────
|
||||
// Renders parsed payloads as a collapsible JSON tree (react18-json-view) so
|
||||
// deeply nested request/response bodies (tool args, message arrays) can be
|
||||
// collapsed instead of scrolled through as one raw text dump. Falls back to
|
||||
// the plain <pre> 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 (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
@@ -40,18 +79,35 @@ export function PayloadSection({ title, json, onCopy, collapsible = true, defaul
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs text-text-muted hover:text-text-primary transition-colors"
|
||||
aria-label={t("copyTitle", { title })}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">
|
||||
{copied ? "check" : "content_copy"}
|
||||
</span>
|
||||
{copied ? t("copied") : t("copy")}
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs text-text-muted hover:text-text-primary transition-colors"
|
||||
aria-label={t("copyTitle", { title })}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">
|
||||
{copied ? "check" : "content_copy"}
|
||||
</span>
|
||||
{copied ? t("copied") : t("copy")}
|
||||
</button>
|
||||
{parsedJson !== null && <JsonTreeExpandControls sectionId={resolvedSectionId} />}
|
||||
</div>
|
||||
</div>
|
||||
{open && (
|
||||
{open && parsedJson !== null && (
|
||||
<div
|
||||
ref={treeContainerRef}
|
||||
className="rounded-xl bg-black/5 dark:bg-black/30 border border-border max-h-150 overflow-auto p-4 text-xs font-mono"
|
||||
>
|
||||
<JsonView
|
||||
src={parsedJson}
|
||||
dark={isDark}
|
||||
collapsed={expandLevel}
|
||||
customizeNode={timestampMarkerCustomizeNode}
|
||||
displaySize
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{open && parsedJson === null && (
|
||||
<pre className="p-4 rounded-xl bg-black/5 dark:bg-black/30 border border-border overflow-x-auto text-xs font-mono text-text-main max-h-150 overflow-y-auto leading-relaxed whitespace-pre-wrap break-words">
|
||||
{json}
|
||||
</pre>
|
||||
|
||||
@@ -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 (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
@@ -144,14 +197,33 @@ function StreamSection({ title, json, onCopy }) {
|
||||
</span>
|
||||
{copied ? t("copied") : t("copy")}
|
||||
</button>
|
||||
{segments.some((s) => s.type === "json") && (
|
||||
<JsonTreeExpandControls sectionId={resolvedSectionId} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{open && (
|
||||
<div
|
||||
ref={ref}
|
||||
className="p-4 rounded-xl bg-black/5 dark:bg-black/30 border border-border overflow-x-auto text-xs font-mono text-text-main max-h-150 overflow-y-auto leading-relaxed whitespace-pre-wrap break-words"
|
||||
className="p-4 rounded-xl bg-black/5 dark:bg-black/30 border border-border overflow-x-auto text-xs font-mono text-text-main max-h-150 overflow-y-auto leading-relaxed"
|
||||
>
|
||||
{json}
|
||||
{segments.map((segment, i) =>
|
||||
segment.type === "json" ? (
|
||||
<div key={i} className="my-1">
|
||||
<JsonView
|
||||
src={segment.value}
|
||||
dark={isDark}
|
||||
collapsed={expandLevel}
|
||||
customizeNode={timestampMarkerCustomizeNode}
|
||||
displaySize
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<span key={i} className="whitespace-pre-wrap break-words">
|
||||
{segment.value}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -201,6 +273,7 @@ export default function RequestLoggerDetail({
|
||||
}) {
|
||||
const t = useTranslations("requestLogger.detail");
|
||||
const locale = useLocale();
|
||||
const modalScrollRef = useRef(null);
|
||||
// Close on Escape key
|
||||
useEffect(() => {
|
||||
const handler = (e) => {
|
||||
@@ -210,6 +283,21 @@ export default function RequestLoggerDetail({
|
||||
return () => globalThis.removeEventListener("keydown", handler);
|
||||
}, [onClose]);
|
||||
|
||||
// The overlay is full-viewport, but the modal panel itself is centered and
|
||||
// narrower than the viewport (max-w-225), leaving backdrop margin on every
|
||||
// side. A wheel event there has no scrollable target under the cursor, so
|
||||
// scrolling only worked once the pointer happened to be over the panel's
|
||||
// own content. Forward wheel scrolling from anywhere in the overlay to the
|
||||
// panel instead, so the backdrop margin scrolls it too.
|
||||
const handleOverlayWheel = (e) => {
|
||||
const panel = modalScrollRef.current;
|
||||
// Let native scrolling handle wheel events that already land inside the
|
||||
// panel -- only forward the ones from the backdrop margin around it.
|
||||
if (!panel || panel.contains(e.target)) return;
|
||||
panel.scrollTop += e.deltaY;
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const statusStyle = getStatusStyle(log.status);
|
||||
const protocolKey = log.sourceFormat || log.provider;
|
||||
const protocol = getProtocolColor(protocolKey, log.provider);
|
||||
@@ -377,12 +465,14 @@ export default function RequestLoggerDetail({
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-start justify-center px-2 pt-[5vh] sm:px-4"
|
||||
onClick={onClose}
|
||||
onWheel={handleOverlayWheel}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={t("ariaLabel")}
|
||||
>
|
||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
||||
<div
|
||||
ref={modalScrollRef}
|
||||
className="relative w-full max-w-225 max-h-[90vh] overflow-x-hidden overflow-y-auto rounded-xl border border-border bg-bg-primary shadow-2xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
@@ -722,7 +812,7 @@ export default function RequestLoggerDetail({
|
||||
{detail?.comboName || log.comboName}
|
||||
</span>
|
||||
) : (
|
||||
<div className="text-sm text-text-muted">\u2014</div>
|
||||
<div className="text-sm text-text-muted">{"\u2014"}</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
@@ -734,10 +824,11 @@ export default function RequestLoggerDetail({
|
||||
className="text-sm font-mono select-all"
|
||||
title={detail?.sessionTag || log.sessionTag}
|
||||
>
|
||||
{(detail?.sessionTag || log.sessionTag).slice(0, 20)}\u2026
|
||||
{(detail?.sessionTag || log.sessionTag).slice(0, 20)}
|
||||
{"\u2026"}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-text-muted">\u2014</div>
|
||||
<div className="text-sm text-text-muted">{"\u2014"}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -915,6 +1006,7 @@ export default function RequestLoggerDetail({
|
||||
{streamChunks && streamChunks.provider && (
|
||||
<StreamSection
|
||||
title={t("providerEventStream")}
|
||||
sectionId="providerEventStream"
|
||||
json={
|
||||
Array.isArray(streamChunks.provider)
|
||||
? streamChunks.provider.join("")
|
||||
@@ -933,6 +1025,7 @@ export default function RequestLoggerDetail({
|
||||
{streamChunks && streamChunks.client && (
|
||||
<StreamSection
|
||||
title={t("clientEventStream")}
|
||||
sectionId="clientEventStream"
|
||||
json={
|
||||
Array.isArray(streamChunks.client)
|
||||
? streamChunks.client.join("")
|
||||
@@ -954,6 +1047,7 @@ export default function RequestLoggerDetail({
|
||||
!streamChunks.client && (
|
||||
<StreamSection
|
||||
title={t("eventStream")}
|
||||
sectionId="eventStream"
|
||||
json={
|
||||
Array.isArray(streamChunks.openai)
|
||||
? streamChunks.openai.join("")
|
||||
@@ -974,6 +1068,7 @@ export default function RequestLoggerDetail({
|
||||
<PayloadSection
|
||||
key={section.key}
|
||||
title={section.title}
|
||||
sectionId={section.key}
|
||||
json={section.json}
|
||||
onCopy={() => onCopy(section.json)}
|
||||
/>
|
||||
@@ -982,6 +1077,7 @@ export default function RequestLoggerDetail({
|
||||
{payloadSections.length === 0 && responseJson && (
|
||||
<PayloadSection
|
||||
title={t("responsePayloadLegacy")}
|
||||
sectionId="responsePayloadLegacy"
|
||||
json={responseJson}
|
||||
onCopy={() => onCopy(responseJson)}
|
||||
/>
|
||||
@@ -990,6 +1086,7 @@ export default function RequestLoggerDetail({
|
||||
{payloadSections.length === 0 && requestJson && (
|
||||
<PayloadSection
|
||||
title={t("requestPayloadLegacy")}
|
||||
sectionId="requestPayloadLegacy"
|
||||
json={requestJson}
|
||||
onCopy={() => onCopy(requestJson)}
|
||||
/>
|
||||
|
||||
72
src/shared/hooks/useTimestampTitles.ts
Normal file
72
src/shared/hooks/useTimestampTitles.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, type RefObject } from "react";
|
||||
import type { JsonViewProps } from "react18-json-view";
|
||||
|
||||
type CustomizeNode = NonNullable<JsonViewProps["customizeNode"]>;
|
||||
|
||||
// react18-json-view exposes no direct tooltip prop, but customizeNode gives a
|
||||
// per-value className hook without touching what value is actually rendered
|
||||
// -- the tree's byte-accurate rendering is the point, so a marker class + DOM
|
||||
// title-attribute pass is safer here than swapping in a replacement element
|
||||
// with hand-reproduced number formatting.
|
||||
export const TIMESTAMP_VALUE_MARKER_CLASS = "json-tree-number-value";
|
||||
|
||||
const TIMESTAMP_FIELD_NAME = /created|updated|modified|timestamp|(^|_)ts$|_at$/i;
|
||||
// Unix epoch bounds spanning year 2000 to year 2100, checked at both
|
||||
// seconds and millisecond precision (both are common on the wire).
|
||||
const EPOCH_SECONDS_MIN = 946684800;
|
||||
const EPOCH_SECONDS_MAX = 4102444800;
|
||||
const EPOCH_MS_MIN = EPOCH_SECONDS_MIN * 1000;
|
||||
const EPOCH_MS_MAX = EPOCH_SECONDS_MAX * 1000;
|
||||
|
||||
function epochValueToDate(value: number): Date | null {
|
||||
if (!Number.isFinite(value)) return null;
|
||||
if (value >= EPOCH_SECONDS_MIN && value <= EPOCH_SECONDS_MAX) return new Date(value * 1000);
|
||||
if (value >= EPOCH_MS_MIN && value <= EPOCH_MS_MAX) return new Date(value);
|
||||
return null;
|
||||
}
|
||||
|
||||
// react18-json-view's customizeNode receives the key (indexOrName) and value
|
||||
// (node) together, so the timestamp-field-name check can run at the source
|
||||
// instead of a DOM sibling-walk -- it only needs to mark the match, the
|
||||
// tooltip text itself is still applied via useTimestampTitles below.
|
||||
export const timestampMarkerCustomizeNode: CustomizeNode = ({ node, indexOrName }) => {
|
||||
if (typeof indexOrName !== "string" || !TIMESTAMP_FIELD_NAME.test(indexOrName)) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof node !== "number" || !epochValueToDate(node)) {
|
||||
return undefined;
|
||||
}
|
||||
return { className: TIMESTAMP_VALUE_MARKER_CLASS };
|
||||
};
|
||||
|
||||
// Adds a human-readable date/time title (tooltip) to marked number values.
|
||||
// Re-runs on any DOM change inside the container (expanding an individual
|
||||
// node is internal react18-json-view state, not a prop change this
|
||||
// component would otherwise see) and is idempotent, so it's safe to call
|
||||
// repeatedly as the tree gets expanded/collapsed.
|
||||
export function useTimestampTitles(containerRef: RefObject<HTMLElement | null>, active: boolean) {
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!active || !container) return;
|
||||
|
||||
const annotate = () => {
|
||||
const values = container.querySelectorAll<HTMLElement>(`.${TIMESTAMP_VALUE_MARKER_CLASS}`);
|
||||
for (const valueEl of values) {
|
||||
if (valueEl.title) continue;
|
||||
const date = epochValueToDate(Number(valueEl.textContent));
|
||||
if (!date) continue;
|
||||
valueEl.title = date.toLocaleString(undefined, {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "medium",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
annotate();
|
||||
const observer = new MutationObserver(annotate);
|
||||
observer.observe(container, { childList: true, subtree: true });
|
||||
return () => observer.disconnect();
|
||||
}, [containerRef, active]);
|
||||
}
|
||||
@@ -182,6 +182,24 @@ export function formatResetCountdown(resetAt: string | number | null | undefined
|
||||
return `${seconds}s`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a git-log-style short identifier from a tool/call id for compact
|
||||
* display. IDs carry decorative prefixes that vary by provider (call_,
|
||||
* call-, toolu_, or composite forms like call_call-<uuid>_fc_cal_<hex>) and
|
||||
* aren't unique on their own — naively slicing the whole id can leave as
|
||||
* little as 3 significant characters visible. Finds the first UUID-shaped
|
||||
* segment (or long hex run) and takes its leading characters instead, so the
|
||||
* visible part is actually where the entropy is.
|
||||
*/
|
||||
export function shortCallId(id: string | null | undefined, length = 7): string {
|
||||
if (!id) return "";
|
||||
const uuidMatch = id.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i);
|
||||
if (uuidMatch) return uuidMatch[0].slice(0, length);
|
||||
const hexMatch = id.match(/[0-9a-f]{8,}/i);
|
||||
if (hexMatch) return hexMatch[0].slice(0, length);
|
||||
return id.slice(0, length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerces a persisted log `error` field into safe display text. It is
|
||||
* legitimate for this field to be a structured object (e.g. `{ code,
|
||||
|
||||
63
src/store/jsonTreeExpandStore.ts
Normal file
63
src/store/jsonTreeExpandStore.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
// The level a JSON tree node is rendered at (react18-json-view's own
|
||||
// `collapsed` numeric-depth prop) is 0-indexed, so this store's level uses
|
||||
// the same convention: level 0 means nothing is expanded ("collapse all"),
|
||||
// and a node at depth N is expanded when N < level.
|
||||
const MIN_LEVEL = 0;
|
||||
const MAX_LEVEL = 64; // deep enough for any real request/response payload
|
||||
export const DEFAULT_JSON_TREE_EXPAND_LEVEL = 2;
|
||||
|
||||
// Level is tracked per section (keyed by a stable, locale-independent
|
||||
// sectionId -- e.g. "openaiRequest", "providerEventStream" -- not the
|
||||
// translated title), because different payload/stream boxes carry
|
||||
// "interesting" data at different nesting depths. The same sectionId across
|
||||
// different log entries shares one level, so it still carries over between
|
||||
// requests, just scoped per box instead of one setting for the whole page.
|
||||
interface JsonTreeExpandState {
|
||||
levels: Record<string, number>;
|
||||
collapseAll: (sectionId: string) => void;
|
||||
collapseOneLevel: (sectionId: string) => void;
|
||||
expandOneLevel: (sectionId: string) => void;
|
||||
expandAll: (sectionId: string) => void;
|
||||
}
|
||||
|
||||
function clamp(level: number): number {
|
||||
return Math.max(MIN_LEVEL, Math.min(MAX_LEVEL, level));
|
||||
}
|
||||
|
||||
const useJsonTreeExpandStore = create<JsonTreeExpandState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
levels: {},
|
||||
collapseAll: (sectionId) => set((s) => ({ levels: { ...s.levels, [sectionId]: MIN_LEVEL } })),
|
||||
collapseOneLevel: (sectionId) =>
|
||||
set((s) => ({
|
||||
levels: {
|
||||
...s.levels,
|
||||
[sectionId]: clamp((s.levels[sectionId] ?? DEFAULT_JSON_TREE_EXPAND_LEVEL) - 1),
|
||||
},
|
||||
})),
|
||||
expandOneLevel: (sectionId) =>
|
||||
set((s) => ({
|
||||
levels: {
|
||||
...s.levels,
|
||||
[sectionId]: clamp((s.levels[sectionId] ?? DEFAULT_JSON_TREE_EXPAND_LEVEL) + 1),
|
||||
},
|
||||
})),
|
||||
expandAll: (sectionId) => set((s) => ({ levels: { ...s.levels, [sectionId]: MAX_LEVEL } })),
|
||||
}),
|
||||
{
|
||||
name: "omniroute-json-tree-expand-levels",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export function useJsonTreeExpandLevel(sectionId: string): number {
|
||||
return useJsonTreeExpandStore((s) => s.levels[sectionId] ?? DEFAULT_JSON_TREE_EXPAND_LEVEL);
|
||||
}
|
||||
|
||||
export default useJsonTreeExpandStore;
|
||||
@@ -0,0 +1,81 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// Regression guard: PayloadSection rendered every payload (request/response
|
||||
// bodies, pipeline stages) as a flat <pre> 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
|
||||
// <pre> 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(
|
||||
<PayloadSection title="Test Payload" json={json} onCopy={vi.fn().mockResolvedValue(true)} />
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
describe("PayloadSection collapsible JSON rendering", () => {
|
||||
it("renders a valid JSON object payload as a collapsible tree, not a flat <pre> 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 <pre> 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 <pre> dump for a JSON primitive (not an object/array)", () => {
|
||||
renderPayload(JSON.stringify("just a string"));
|
||||
|
||||
const pre = container.querySelector("pre");
|
||||
expect(pre).not.toBeNull();
|
||||
});
|
||||
});
|
||||
94
tests/unit/dashboard/timestamp-titles.test.tsx
Normal file
94
tests/unit/dashboard/timestamp-titles.test.tsx
Normal file
@@ -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(
|
||||
<PayloadSection
|
||||
title="Test Payload"
|
||||
sectionId="test-payload"
|
||||
json={JSON.stringify(payload, null, 2)}
|
||||
onCopy={vi.fn().mockResolvedValue(true)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function findValueSpan(text: string): HTMLElement | undefined {
|
||||
return Array.from(container.querySelectorAll<HTMLElement>("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");
|
||||
});
|
||||
});
|
||||
57
tests/unit/shared/json-tree-expand-store.test.ts
Normal file
57
tests/unit/shared/json-tree-expand-store.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
38
tests/unit/shared/short-call-id.test.ts
Normal file
38
tests/unit/shared/short-call-id.test.ts
Normal file
@@ -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(""), "");
|
||||
});
|
||||
Reference in New Issue
Block a user