diff --git a/package.json b/package.json index 6e0c1b4..7d8a8ff 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ }, "devDependencies": { "@base-ui/react": "^1.6.0", + "@codemirror/lang-json": "^6.0.2", "@electron-toolkit/eslint-config-prettier": "^3.0.0", "@electron-toolkit/eslint-config-ts": "^3.1.0", "@electron-toolkit/tsconfig": "^2.0.0", @@ -58,6 +59,8 @@ "@types/node": "^22.19.1", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", + "@uiw/codemirror-theme-github": "^4.25.11", + "@uiw/react-codemirror": "^4.25.11", "@vitejs/plugin-react": "^5.1.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c72ea0..4421e82 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,6 +24,9 @@ importers: '@base-ui/react': specifier: ^1.6.0 version: 1.6.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@codemirror/lang-json': + specifier: ^6.0.2 + version: 6.0.2 '@electron-toolkit/eslint-config-prettier': specifier: ^3.0.0 version: 3.0.0(eslint@9.39.4(jiti@2.7.0))(prettier@3.9.5) @@ -48,6 +51,12 @@ importers: '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.17) + '@uiw/codemirror-theme-github': + specifier: ^4.25.11 + version: 4.25.11(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.6) + '@uiw/react-codemirror': + specifier: ^4.25.11 + version: 4.25.11(@babel/runtime@7.29.7)(@codemirror/autocomplete@6.20.3)(@codemirror/language@6.12.4)(@codemirror/lint@6.9.7)(@codemirror/search@6.7.1)(@codemirror/state@6.7.1)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.43.6)(codemirror@6.0.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@vitejs/plugin-react': specifier: ^5.1.1 version: 5.2.0(vite@7.3.6(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)) @@ -295,6 +304,33 @@ packages: '@types/react': optional: true + '@codemirror/autocomplete@6.20.3': + resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==} + + '@codemirror/commands@6.10.4': + resolution: {integrity: sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==} + + '@codemirror/lang-json@6.0.2': + resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} + + '@codemirror/language@6.12.4': + resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==} + + '@codemirror/lint@6.9.7': + resolution: {integrity: sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==} + + '@codemirror/search@6.7.1': + resolution: {integrity: sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==} + + '@codemirror/state@6.7.1': + resolution: {integrity: sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==} + + '@codemirror/theme-one-dark@6.1.3': + resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==} + + '@codemirror/view@6.43.6': + resolution: {integrity: sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA==} + '@dotenvx/dotenvx@1.75.1': resolution: {integrity: sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ==} hasBin: true @@ -786,6 +822,18 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@lezer/common@1.5.2': + resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/json@1.0.3': + resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} + + '@lezer/lr@1.4.10': + resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} + '@malept/cross-spawn-promise@2.0.0': resolution: {integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==} engines: {node: '>= 12.13.0'} @@ -794,6 +842,9 @@ packages: resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} engines: {node: '>= 10.0.0'} + '@marijn/find-cluster-break@1.0.3': + resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==} + '@modelcontextprotocol/sdk@1.29.0': resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} engines: {node: '>=18'} @@ -1210,6 +1261,38 @@ packages: resolution: {integrity: sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@uiw/codemirror-extensions-basic-setup@4.25.11': + resolution: {integrity: sha512-otyFa+n9IOYtEjaKOxPedHkj15fTPUF21wdR9pv0GpZPfuGl27cvmcv6+tognbRu9VvEcsHKE+ESoszeo3KfTw==} + peerDependencies: + '@codemirror/autocomplete': '>=6.0.0' + '@codemirror/commands': '>=6.0.0' + '@codemirror/language': '>=6.0.0' + '@codemirror/lint': '>=6.0.0' + '@codemirror/search': '>=6.0.0' + '@codemirror/state': '>=6.0.0' + '@codemirror/view': '>=6.0.0' + + '@uiw/codemirror-theme-github@4.25.11': + resolution: {integrity: sha512-3s0LK3gX2mvGI996z3G0tEHZqshbeJRly+QMRsKGAI1Tfr1V475bfaW9NvnoDdINuQHt+RB+ri6q57+WWF8d+A==} + + '@uiw/codemirror-themes@4.25.11': + resolution: {integrity: sha512-SBNCOgRsCtewGNocRbmjbCkltGXlFcPJsvhxQ351VynQjnWUiPbUrFcEU/haQ3HanROdAAjWXZJPk5bMBxl2jw==} + peerDependencies: + '@codemirror/language': '>=6.0.0' + '@codemirror/state': '>=6.0.0' + '@codemirror/view': '>=6.0.0' + + '@uiw/react-codemirror@4.25.11': + resolution: {integrity: sha512-DYVFAKLX+F/4JS9N/7xexh+TICrlncwkX9HKKInrP1bwO0tSfc3k0GB6oawTYhelVKh20cX3TuRx+NJSkVXuMw==} + peerDependencies: + '@babel/runtime': '>=7.11.0' + '@codemirror/state': '>=6.0.0' + '@codemirror/theme-one-dark': '>=6.0.0' + '@codemirror/view': '>=6.0.0' + codemirror: '>=6.0.0' + react: '>=17.0.0' + react-dom: '>=17.0.0' + '@vitejs/plugin-react@5.2.0': resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1505,6 +1588,9 @@ packages: code-block-writer@13.0.3: resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + codemirror@6.0.2: + resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1582,6 +1668,9 @@ packages: typescript: optional: true + crelt@1.0.7: + resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==} + cross-dirname@0.1.0: resolution: {integrity: sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==} @@ -3462,6 +3551,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + sumchecker@3.0.1: resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} engines: {node: '>= 8.0'} @@ -3697,6 +3789,9 @@ packages: yaml: optional: true + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + webcrypto-core@1.9.2: resolution: {integrity: sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==} @@ -4037,6 +4132,64 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 + '@codemirror/autocomplete@6.20.3': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 + '@lezer/common': 1.5.2 + + '@codemirror/commands@6.10.4': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 + '@lezer/common': 1.5.2 + + '@codemirror/lang-json@6.0.2': + dependencies: + '@codemirror/language': 6.12.4 + '@lezer/json': 1.0.3 + + '@codemirror/language@6.12.4': + dependencies: + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + style-mod: 4.1.3 + + '@codemirror/lint@6.9.7': + dependencies: + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 + crelt: 1.0.7 + + '@codemirror/search@6.7.1': + dependencies: + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 + crelt: 1.0.7 + + '@codemirror/state@6.7.1': + dependencies: + '@marijn/find-cluster-break': 1.0.3 + + '@codemirror/theme-one-dark@6.1.3': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 + '@lezer/highlight': 1.2.3 + + '@codemirror/view@6.43.6': + dependencies: + '@codemirror/state': 6.7.1 + crelt: 1.0.7 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + '@dotenvx/dotenvx@1.75.1': dependencies: '@dotenvx/primitives': 0.8.0 @@ -4447,6 +4600,22 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@lezer/common@1.5.2': {} + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.2 + + '@lezer/json@1.0.3': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/lr@1.4.10': + dependencies: + '@lezer/common': 1.5.2 + '@malept/cross-spawn-promise@2.0.0': dependencies: cross-spawn: 7.0.6 @@ -4460,6 +4629,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@marijn/find-cluster-break@1.0.3': {} + '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': dependencies: '@hono/node-server': 1.19.14(hono@4.12.29) @@ -4845,6 +5016,47 @@ snapshots: '@typescript-eslint/types': 8.63.0 eslint-visitor-keys: 5.0.1 + '@uiw/codemirror-extensions-basic-setup@4.25.11(@codemirror/autocomplete@6.20.3)(@codemirror/commands@6.10.4)(@codemirror/language@6.12.4)(@codemirror/lint@6.9.7)(@codemirror/search@6.7.1)(@codemirror/state@6.7.1)(@codemirror/view@6.43.6)': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/commands': 6.10.4 + '@codemirror/language': 6.12.4 + '@codemirror/lint': 6.9.7 + '@codemirror/search': 6.7.1 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 + + '@uiw/codemirror-theme-github@4.25.11(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.6)': + dependencies: + '@uiw/codemirror-themes': 4.25.11(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.6) + transitivePeerDependencies: + - '@codemirror/language' + - '@codemirror/state' + - '@codemirror/view' + + '@uiw/codemirror-themes@4.25.11(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.6)': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 + + '@uiw/react-codemirror@4.25.11(@babel/runtime@7.29.7)(@codemirror/autocomplete@6.20.3)(@codemirror/language@6.12.4)(@codemirror/lint@6.9.7)(@codemirror/search@6.7.1)(@codemirror/state@6.7.1)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.43.6)(codemirror@6.0.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@babel/runtime': 7.29.7 + '@codemirror/commands': 6.10.4 + '@codemirror/state': 6.7.1 + '@codemirror/theme-one-dark': 6.1.3 + '@codemirror/view': 6.43.6 + '@uiw/codemirror-extensions-basic-setup': 4.25.11(@codemirror/autocomplete@6.20.3)(@codemirror/commands@6.10.4)(@codemirror/language@6.12.4)(@codemirror/lint@6.9.7)(@codemirror/search@6.7.1)(@codemirror/state@6.7.1)(@codemirror/view@6.43.6) + codemirror: 6.0.2 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + transitivePeerDependencies: + - '@codemirror/autocomplete' + - '@codemirror/language' + - '@codemirror/lint' + - '@codemirror/search' + '@vitejs/plugin-react@5.2.0(vite@7.3.6(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0))': dependencies: '@babel/core': 7.29.7 @@ -5205,6 +5417,16 @@ snapshots: code-block-writer@13.0.3: {} + codemirror@6.0.2: + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/commands': 6.10.4 + '@codemirror/language': 6.12.4 + '@codemirror/lint': 6.9.7 + '@codemirror/search': 6.7.1 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -5269,6 +5491,8 @@ snapshots: optionalDependencies: typescript: 5.9.3 + crelt@1.0.7: {} + cross-dirname@0.1.0: optional: true @@ -7439,6 +7663,8 @@ snapshots: strip-json-comments@3.1.1: {} + style-mod@4.1.3: {} + sumchecker@3.0.1: dependencies: debug: 4.4.3 @@ -7653,6 +7879,8 @@ snapshots: jiti: 2.7.0 lightningcss: 1.32.0 + w3c-keyname@2.2.8: {} + webcrypto-core@1.9.2: dependencies: '@peculiar/asn1-schema': 2.8.0 diff --git a/src/renderer/src/components/json-view.tsx b/src/renderer/src/components/json-view.tsx new file mode 100644 index 0000000..e1fc6cc --- /dev/null +++ b/src/renderer/src/components/json-view.tsx @@ -0,0 +1,53 @@ +import CodeMirror, { EditorView, type BasicSetupOptions } from '@uiw/react-codemirror' +import { json } from '@codemirror/lang-json' +import { githubDarkInit, githubLightInit } from '@uiw/codemirror-theme-github' +import { useTheme } from 'next-themes' + +/** Blend the editor into the surrounding `bg-muted` panel and match the app's `text-xs` code size. */ +const themeSettings = { + background: 'transparent', + gutterBackground: 'transparent', + fontSize: '12px' +} as const + +const lightTheme = githubLightInit({ settings: themeSettings }) +const darkTheme = githubDarkInit({ settings: themeSettings }) + +// Module-level so their identity is stable across renders (a new array would force a reconfigure). +const jsonExtensions = [json(), EditorView.lineWrapping] +const textExtensions = [EditorView.lineWrapping] + +/** A viewer, not an editor: line numbers + folding, no active-line highlight or autocompletion. */ +const readOnlySetup: BasicSetupOptions = { + lineNumbers: true, + foldGutter: true, + highlightActiveLine: false, + highlightActiveLineGutter: false, + autocompletion: false +} + +/** + * Read-only CodeMirror viewer for request/response bodies. `json` mode gives syntax + * highlighting + folding; `text` mode (SSE streams, truncated non-JSON) is plain text but + * keeps search/scroll/wrap. Theme follows the app's light/dark preference. Height is capped + * at 20rem (matching the old `
`), overflowing to an internal scroll.
+ */
+export function JsonView({
+  value,
+  language = 'json'
+}: {
+  value: string
+  language?: 'json' | 'text'
+}): React.JSX.Element {
+  const { resolvedTheme } = useTheme()
+  return (
+    
+  )
+}
diff --git a/src/renderer/src/components/log-trace-dialog.tsx b/src/renderer/src/components/log-trace-dialog.tsx
index a781086..7a16dc9 100644
--- a/src/renderer/src/components/log-trace-dialog.tsx
+++ b/src/renderer/src/components/log-trace-dialog.tsx
@@ -1,5 +1,11 @@
+import { useState } from 'react'
+import { toast } from 'sonner'
+import { CheckIcon, CopyIcon } from 'lucide-react'
 import { Badge } from '@/components/ui/badge'
+import { Button } from '@/components/ui/button'
 import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
+import { JsonView } from '@/components/json-view'
+import { copyText } from '@/lib/clipboard'
 import { cn } from '@/lib/utils'
 import {
   clientLabel,
@@ -24,14 +30,17 @@ interface LogTraceDialogProps {
   onClose: () => void
 }
 
-/** Pretty-print a JSON body; fall back to the raw string (e.g. an SSE stream) when it isn't JSON. */
-function prettyBody(raw: string): string {
+/**
+ * Pretty-print a JSON body; fall back to the raw string (e.g. an SSE stream, or a body
+ * truncated to invalid JSON) when it isn't parseable. `isJson` drives the viewer's language.
+ */
+function formatBody(raw: string): { text: string; isJson: boolean } {
   const trimmed = raw.trim()
-  if (!trimmed) return ''
+  if (!trimmed) return { text: '', isJson: false }
   try {
-    return JSON.stringify(JSON.parse(trimmed), null, 2)
+    return { text: JSON.stringify(JSON.parse(trimmed), null, 2), isJson: true }
   } catch {
-    return raw
+    return { text: raw, isJson: false }
   }
 }
 
@@ -77,15 +86,42 @@ function HeadersTable({
   )
 }
 
+/** A copy-to-clipboard button pinned to the top-right of a body block. */
+function CopyButton({ text, t }: { text: string; t: TFunction }): React.JSX.Element {
+  const [copied, setCopied] = useState(false)
+  const onCopy = async (): Promise => {
+    if (await copyText(text)) {
+      setCopied(true)
+      toast.success(t('toast.bodyCopied'))
+      window.setTimeout(() => setCopied(false), 1500)
+    } else {
+      toast.error(t('toast.copyFailed'))
+    }
+  }
+  return (
+    
+  )
+}
+
 function BodyBlock({ body, t }: { body: string; t: TFunction }): React.JSX.Element {
-  const text = prettyBody(body)
+  const { text, isJson } = formatBody(body)
   if (!text) {
     return 

{t('logs.detail.emptyBody')}

} return ( -
-      {text}
-    
+
+ + +
) } diff --git a/src/renderer/src/i18n/en.ts b/src/renderer/src/i18n/en.ts index d617a59..b336c82 100644 --- a/src/renderer/src/i18n/en.ts +++ b/src/renderer/src/i18n/en.ts @@ -24,6 +24,7 @@ const en = { saveSuccess: 'Configuration saved', baseUrlCopied: 'Base URL copied', copyFailed: 'Copy failed', + bodyCopied: 'Body copied', apiKeyCopied: 'API key copied', apiKeyRegenerated: 'New API key generated', apiKeyRegeneratedReapplied: 'New key generated and re-applied to {count} client(s)', @@ -211,6 +212,7 @@ const en = { url: 'URL', attempt: 'Attempt {n}', emptyBody: '(empty)', + copyBody: 'Copy', truncated: 'Response body truncated to the capture size limit.', headersRedacted: 'Authorization / API-key headers are redacted.', strippedUsage: 'Usage stripped from the client-visible stream.', diff --git a/src/renderer/src/i18n/zh.ts b/src/renderer/src/i18n/zh.ts index a26968c..f8148d6 100644 --- a/src/renderer/src/i18n/zh.ts +++ b/src/renderer/src/i18n/zh.ts @@ -22,6 +22,7 @@ const zh: Dict = { saveSuccess: '配置已保存', baseUrlCopied: '网关地址已复制', copyFailed: '复制失败', + bodyCopied: '已复制内容', apiKeyCopied: 'API Key 已复制', apiKeyRegenerated: '已生成新的 API Key', apiKeyRegeneratedReapplied: '已生成新 Key 并重新接入 {count} 个客户端', @@ -207,6 +208,7 @@ const zh: Dict = { url: 'URL', attempt: '尝试 {n}', emptyBody: '(空)', + copyBody: '复制', truncated: '响应体已按抓取大小上限截断。', headersRedacted: 'Authorization / API-key 等敏感标头已脱敏。', strippedUsage: '已从客户端可见的流中剥离 usage 信息。',