| ... | @@ -5,12 +5,16 @@ import type { | ... | @@ -5,12 +5,16 @@ import type { |
| 5 | ThreadId, | 5 | ThreadId, |
| 6 | } from "@t3tools/contracts"; | 6 | } from "@t3tools/contracts"; |
| 7 | import { ExternalLinkIcon, GitBranchIcon, RotateCcwIcon } from "lucide-react"; | 7 | import { ExternalLinkIcon, GitBranchIcon, RotateCcwIcon } from "lucide-react"; |
| 8 | import { memo } from "react"; | 8 | import { memo, Suspense, use, useMemo } from "react"; |
| 9 | | 9 | |
| | 10 | import { useTheme } from "../../hooks/useTheme"; |
| | 11 | import { resolveDiffThemeName } from "../../lib/diffRendering"; |
| | 12 | import { getSyntaxHighlighterPromise } from "../../lib/syntaxHighlighting"; |
| 10 | import { useV2ItemSupport } from "../../state/v2ItemSupport"; | 13 | import { useV2ItemSupport } from "../../state/v2ItemSupport"; |
| 11 | import { formatWorkspaceRelativePath } from "../../filePathDisplay"; | 14 | import { formatWorkspaceRelativePath } from "../../filePathDisplay"; |
| 12 | import { Button } from "../ui/button"; | 15 | import { Button } from "../ui/button"; |
| 13 | import ChatMarkdown from "../ChatMarkdown"; | 16 | import ChatMarkdown from "../ChatMarkdown"; |
| | 17 | import { RenderErrorBoundary } from "../RenderErrorBoundary"; |
| 14 | import { resolveExternalWebLinkHref } from "./externalLinkContextMenu"; | 18 | import { resolveExternalWebLinkHref } from "./externalLinkContextMenu"; |
| 15 | | 19 | |
| 16 | interface V2ItemInspectorProps { | 20 | interface V2ItemInspectorProps { |
| ... | @@ -26,12 +30,53 @@ interface V2ItemInspectorProps { | ... | @@ -26,12 +30,53 @@ interface V2ItemInspectorProps { |
| 26 | }) => void; | 30 | }) => void; |
| 27 | } | 31 | } |
| 28 | | 32 | |
| 29 | function StructuredValue({ value }: { readonly value: unknown }) { | 33 | function JsonTokens({ text }: { readonly text: string }) { |
| | 34 | const { resolvedTheme } = useTheme(); |
| | 35 | const highlighter = use(getSyntaxHighlighterPromise("json")); |
| | 36 | const { tokens } = useMemo( |
| | 37 | () => |
| | 38 | highlighter.codeToTokens(text, { lang: "json", theme: resolveDiffThemeName(resolvedTheme) }), |
| | 39 | [highlighter, text, resolvedTheme], |
| | 40 | ); |
| | 41 | return tokens.flatMap((line, lineIndex) => [ |
| | 42 | lineIndex > 0 ? "\n" : "", |
| | 43 | ...line.map((token) => ( |
| | 44 | <span key={token.offset} style={{ color: token.color }}> |
| | 45 | {token.content} |
| | 46 | </span> |
| | 47 | )), |
| | 48 | ]); |
| | 49 | } |
| | 50 | |
| | 51 | function StructuredValue({ |
| | 52 | value, |
| | 53 | highlightJson = false, |
| | 54 | }: { |
| | 55 | readonly value: unknown; |
| | 56 | readonly highlightJson?: boolean; |
| | 57 | }) { |
| 30 | const text = typeof value === "string" ? value : JSON.stringify(value, null, 2); | 58 | const text = typeof value === "string" ? value : JSON.stringify(value, null, 2); |
| | 59 | const isJson = useMemo(() => { |
| | 60 | if (!highlightJson || !text) return false; |
| | 61 | try { |
| | 62 | JSON.parse(text); |
| | 63 | return true; |
| | 64 | } catch { |
| | 65 | return false; |
| | 66 | } |
| | 67 | }, [highlightJson, text]); |
| 31 | if (!text) return null; | 68 | if (!text) return null; |
| 32 | return ( | 69 | return ( |
| 33 | <pre className="max-h-80 overflow-auto whitespace-pre-wrap break-words rounded-md border border-border/50 bg-background/60 p-2 font-mono text-2xs leading-relaxed text-muted-foreground select-text"> | 70 | <pre className="max-h-80 overflow-auto whitespace-pre-wrap break-words rounded-md border border-border/50 bg-background/60 p-2 font-mono text-2xs leading-relaxed text-muted-foreground select-text"> |
| 34 | {text} | 71 | {isJson ? ( |
| | 72 | <RenderErrorBoundary fallback={text}> |
| | 73 | <Suspense fallback={text}> |
| | 74 | <JsonTokens text={text} /> |
| | 75 | </Suspense> |
| | 76 | </RenderErrorBoundary> |
| | 77 | ) : ( |
| | 78 | text |
| | 79 | )} |
| 35 | </pre> | 80 | </pre> |
| 36 | ); | 81 | ); |
| 37 | } | 82 | } |
| ... | @@ -61,7 +106,7 @@ export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspec | ... | @@ -61,7 +106,7 @@ export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspec |
| 61 | | 106 | |
| 62 | {item.type === "command_execution" ? ( | 107 | {item.type === "command_execution" ? ( |
| 63 | <div className="space-y-2"> | 108 | <div className="space-y-2"> |
| 64 | <StructuredValue value={item.input} /> | 109 | <StructuredValue value={item.input} highlightJson /> |
| 65 | {item.exitCode !== undefined ? ( | 110 | {item.exitCode !== undefined ? ( |
| 66 | <p className={item.exitCode === 0 ? "text-success" : "text-destructive"}> | 111 | <p className={item.exitCode === 0 ? "text-success" : "text-destructive"}> |
| 67 | Process exited with code {item.exitCode} | 112 | Process exited with code {item.exitCode} |
| ... | @@ -158,7 +203,7 @@ export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspec | ... | @@ -158,7 +203,7 @@ export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspec |
| 158 | <p className="mb-1 text-3xs font-medium tracking-wide uppercase text-muted-foreground"> | 203 | <p className="mb-1 text-3xs font-medium tracking-wide uppercase text-muted-foreground"> |
| 159 | Input | 204 | Input |
| 160 | </p> | 205 | </p> |
| 161 | <StructuredValue value={item.input} /> | 206 | <StructuredValue value={item.input} highlightJson /> |
| 162 | </div> | 207 | </div> |
| 163 | ) : null} | 208 | ) : null} |
| 164 | | 209 | |