cc-inspector 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,195 @@
1
+ import { AlertTriangle, StopCircle, Zap } from "lucide-react";
2
+ import type { JSX } from "react";
3
+ import ReactMarkdown from "react-markdown";
4
+ import { Badge } from "@/components/ui/badge";
5
+ import { Separator } from "@/components/ui/separator";
6
+ import { cn } from "@/lib/utils";
7
+ import { type ClaudeResponse, ClaudeResponseSchema } from "../../proxy/schemas";
8
+ import { ResponseContentBlockRenderer } from "./content-blocks";
9
+
10
+ export type ResponseViewProps = {
11
+ responseText: string | null;
12
+ responseStatus: number | null;
13
+ streaming: boolean;
14
+ inputTokens: number | null;
15
+ outputTokens: number | null;
16
+ };
17
+
18
+ type StatusCategory = "success" | "client_error" | "server_error" | "pending";
19
+
20
+ function getStatusCategory(status: number | null): StatusCategory {
21
+ if (status === null) return "pending";
22
+ if (status >= 200 && status < 300) return "success";
23
+ if (status >= 400 && status < 500) return "client_error";
24
+ return "server_error";
25
+ }
26
+
27
+ function getStatusClasses(category: StatusCategory): string {
28
+ switch (category) {
29
+ case "success":
30
+ return "text-emerald-400";
31
+ case "client_error":
32
+ return "text-amber-400";
33
+ case "server_error":
34
+ return "text-red-400";
35
+ case "pending":
36
+ return "text-muted-foreground";
37
+ }
38
+ }
39
+
40
+ function parseResponse(text: string): ClaudeResponse | null {
41
+ try {
42
+ const json: unknown = JSON.parse(text);
43
+ const result = ClaudeResponseSchema.safeParse(json);
44
+ if (result.success) return result.data;
45
+ return null;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ function formatTokens(count: number): string {
52
+ if (count < 1000) return count.toString();
53
+ return count.toLocaleString();
54
+ }
55
+
56
+ function StatusIndicator({ status }: { status: number | null }): JSX.Element {
57
+ const category = getStatusCategory(status);
58
+ const classes = getStatusClasses(category);
59
+
60
+ if (status === null) {
61
+ return <span className="text-xs text-muted-foreground italic">pending</span>;
62
+ }
63
+
64
+ return (
65
+ <span className={cn("flex items-center gap-1 text-xs font-mono font-semibold", classes)}>
66
+ {category === "server_error" && <AlertTriangle className="size-3" />}
67
+ {status}
68
+ </span>
69
+ );
70
+ }
71
+
72
+ function StructuredResponseView({ response }: { response: ClaudeResponse }): JSX.Element {
73
+ return (
74
+ <div className="space-y-3">
75
+ <div className="flex items-center gap-2 flex-wrap">
76
+ <Badge variant="secondary" className="text-[10px] px-1.5 py-0 h-5 font-mono">
77
+ {response.model}
78
+ </Badge>
79
+
80
+ {response.stop_reason !== null && (
81
+ <Badge
82
+ variant="outline"
83
+ className="text-[10px] px-1.5 py-0 h-5 font-mono flex items-center gap-1"
84
+ >
85
+ <StopCircle className="size-2.5" />
86
+ {response.stop_reason}
87
+ </Badge>
88
+ )}
89
+
90
+ <span className="flex items-center gap-1 text-muted-foreground text-xs">
91
+ <Zap className="size-3" />
92
+ <span className="font-mono tabular-nums">
93
+ {formatTokens(response.usage.input_tokens)} in /{" "}
94
+ {formatTokens(response.usage.output_tokens)} out
95
+ </span>
96
+ </span>
97
+ </div>
98
+
99
+ <Separator className="opacity-50" />
100
+
101
+ <div className="space-y-2">
102
+ {response.content.map((block, i) => (
103
+ <ResponseContentBlockRenderer key={i} block={block} />
104
+ ))}
105
+ {response.content.length === 0 && (
106
+ <p className="text-xs text-muted-foreground italic">Empty response content</p>
107
+ )}
108
+ </div>
109
+ </div>
110
+ );
111
+ }
112
+
113
+ function ErrorResponseView({ text }: { text: string }): JSX.Element {
114
+ return (
115
+ <div className="rounded-md border border-red-500/30 bg-red-500/5 p-3">
116
+ <pre className="text-xs text-red-300 whitespace-pre-wrap font-mono leading-relaxed overflow-auto max-h-[60vh]">
117
+ {text}
118
+ </pre>
119
+ </div>
120
+ );
121
+ }
122
+
123
+ function MarkdownFallbackView({ text }: { text: string }): JSX.Element {
124
+ return (
125
+ <div className="prose prose-sm dark:prose-invert max-w-none [&_pre]:bg-muted [&_pre]:text-foreground [&_code]:text-[0.8em] [&_p]:my-1 [&_ul]:my-1 [&_ol]:my-1">
126
+ <ReactMarkdown>{text}</ReactMarkdown>
127
+ </div>
128
+ );
129
+ }
130
+
131
+ export function ResponseView({
132
+ responseText,
133
+ responseStatus,
134
+ streaming,
135
+ inputTokens,
136
+ outputTokens,
137
+ }: ResponseViewProps): JSX.Element {
138
+ if (responseText === null) {
139
+ return (
140
+ <div className="flex items-center gap-2 py-3">
141
+ <StatusIndicator status={responseStatus} />
142
+ <span className="text-xs text-muted-foreground italic">No response</span>
143
+ </div>
144
+ );
145
+ }
146
+
147
+ const isError = responseStatus !== null && responseStatus >= 400;
148
+
149
+ if (isError) {
150
+ return (
151
+ <div className="space-y-2">
152
+ <StatusIndicator status={responseStatus} />
153
+ <ErrorResponseView text={responseText} />
154
+ </div>
155
+ );
156
+ }
157
+
158
+ if (streaming) {
159
+ return (
160
+ <div className="space-y-2">
161
+ <div className="flex items-center gap-2">
162
+ <StatusIndicator status={responseStatus} />
163
+ {(inputTokens !== null || outputTokens !== null) && (
164
+ <span className="flex items-center gap-1 text-muted-foreground text-xs">
165
+ <Zap className="size-3" />
166
+ <span className="font-mono tabular-nums">
167
+ {inputTokens !== null ? formatTokens(inputTokens) : "—"} in /{" "}
168
+ {outputTokens !== null ? formatTokens(outputTokens) : "—"} out
169
+ </span>
170
+ </span>
171
+ )}
172
+ </div>
173
+ <MarkdownFallbackView text={responseText} />
174
+ </div>
175
+ );
176
+ }
177
+
178
+ const parsed = parseResponse(responseText);
179
+
180
+ if (parsed !== null) {
181
+ return (
182
+ <div className="space-y-2">
183
+ <StatusIndicator status={responseStatus} />
184
+ <StructuredResponseView response={parsed} />
185
+ </div>
186
+ );
187
+ }
188
+
189
+ return (
190
+ <div className="space-y-2">
191
+ <StatusIndicator status={responseStatus} />
192
+ <MarkdownFallbackView text={responseText} />
193
+ </div>
194
+ );
195
+ }
@@ -0,0 +1,65 @@
1
+ import { ChevronDown, ChevronRight, FileText } from "lucide-react";
2
+ import { type JSX, useState } from "react";
3
+ import ReactMarkdown from "react-markdown";
4
+ import { Badge } from "@/components/ui/badge";
5
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
6
+ import { cn } from "@/lib/utils";
7
+ import type { SystemBlockType } from "../../proxy/schemas";
8
+
9
+ function formatCharCount(count: number): string {
10
+ if (count < 1000) return `${count} chars`;
11
+ return `${(count / 1000).toFixed(1)}k chars`;
12
+ }
13
+
14
+ export type SystemPromptProps = {
15
+ blocks: SystemBlockType[];
16
+ };
17
+
18
+ export function SystemPrompt({ blocks }: SystemPromptProps): JSX.Element {
19
+ const [open, setOpen] = useState(false);
20
+
21
+ const totalChars = blocks.reduce((sum, block) => sum + block.text.length, 0);
22
+
23
+ return (
24
+ <Collapsible open={open} onOpenChange={setOpen}>
25
+ <div className="border-l-2 border-yellow-500/40 pl-3">
26
+ <CollapsibleTrigger className="flex items-center gap-1.5 py-1 w-full text-left cursor-pointer hover:bg-yellow-500/5 transition-colors rounded-r-sm group">
27
+ <FileText className="size-3.5 text-yellow-400 shrink-0" />
28
+ <span className="text-xs font-medium text-yellow-400">System</span>
29
+ <Badge
30
+ variant="ghost"
31
+ className="text-[10px] text-muted-foreground px-1.5 py-0 h-4 font-mono"
32
+ >
33
+ {blocks.length} {blocks.length === 1 ? "block" : "blocks"} &middot;{" "}
34
+ {formatCharCount(totalChars)}
35
+ </Badge>
36
+ <span className="flex-1" />
37
+ {open ? (
38
+ <ChevronDown className="size-3 text-muted-foreground" />
39
+ ) : (
40
+ <ChevronRight className="size-3 text-muted-foreground" />
41
+ )}
42
+ </CollapsibleTrigger>
43
+ <CollapsibleContent>
44
+ <div className={cn("max-h-[60vh] overflow-auto", "pb-2 space-y-3")}>
45
+ {blocks.map((block, i) => (
46
+ <div key={i} className="relative">
47
+ {block.cache_control !== undefined && (
48
+ <Badge
49
+ variant="outline"
50
+ className="text-[9px] px-1 py-0 h-3.5 font-mono absolute top-0 right-0"
51
+ >
52
+ cached
53
+ </Badge>
54
+ )}
55
+ <div className="prose prose-sm dark:prose-invert max-w-none [&_pre]:bg-muted [&_pre]:text-foreground [&_code]:text-[0.8em] [&_p]:my-1 [&_ul]:my-1 [&_ol]:my-1">
56
+ <ReactMarkdown>{block.text}</ReactMarkdown>
57
+ </div>
58
+ </div>
59
+ ))}
60
+ </div>
61
+ </CollapsibleContent>
62
+ </div>
63
+ </Collapsible>
64
+ );
65
+ }
@@ -0,0 +1,82 @@
1
+ import { ChevronDown, ChevronRight, Wrench } from "lucide-react";
2
+ import { type JSX, useState } from "react";
3
+ import ReactMarkdown from "react-markdown";
4
+ import { Badge } from "@/components/ui/badge";
5
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
6
+ import { JsonViewer, safeJsonValue } from "@/components/ui/json-viewer";
7
+ import { ScrollArea } from "@/components/ui/scroll-area";
8
+ import type { ToolDefinitionType } from "../../proxy/schemas";
9
+
10
+ function truncateDescription(description: string, max: number): string {
11
+ if (description.length <= max) return description;
12
+ return `${description.slice(0, max)}…`;
13
+ }
14
+
15
+ function ToolItem({ tool }: { tool: ToolDefinitionType }): JSX.Element {
16
+ return (
17
+ <Collapsible>
18
+ <CollapsibleTrigger className="flex items-center gap-2 px-3 py-1 w-full text-left cursor-pointer hover:bg-cyan-500/5 transition-colors rounded-sm group">
19
+ <Badge variant="outline" className="text-[10px] font-mono px-1.5 py-0 h-4 shrink-0">
20
+ {tool.name}
21
+ </Badge>
22
+ {tool.description !== undefined && (
23
+ <span className="text-muted-foreground text-xs truncate">
24
+ {truncateDescription(tool.description, 60)}
25
+ </span>
26
+ )}
27
+ </CollapsibleTrigger>
28
+ <CollapsibleContent>
29
+ <div className="px-3 pb-2 pt-1 space-y-2">
30
+ {tool.description !== undefined && (
31
+ <div className="prose prose-sm dark:prose-invert max-w-none [&_p]:my-1 [&_ul]:my-1 [&_ol]:my-1">
32
+ <ReactMarkdown>{tool.description}</ReactMarkdown>
33
+ </div>
34
+ )}
35
+ {tool.input_schema !== undefined && (
36
+ <ScrollArea className="max-h-[40vh]">
37
+ <JsonViewer data={safeJsonValue(tool.input_schema)} defaultExpandDepth={1} />
38
+ </ScrollArea>
39
+ )}
40
+ </div>
41
+ </CollapsibleContent>
42
+ </Collapsible>
43
+ );
44
+ }
45
+
46
+ export type ToolDefinitionsProps = {
47
+ tools: ToolDefinitionType[];
48
+ };
49
+
50
+ export function ToolDefinitions({ tools }: ToolDefinitionsProps): JSX.Element {
51
+ const [open, setOpen] = useState(false);
52
+
53
+ return (
54
+ <Collapsible open={open} onOpenChange={setOpen}>
55
+ <div className="border-l-2 border-cyan-500/40 pl-3">
56
+ <CollapsibleTrigger className="flex items-center gap-1.5 py-1 w-full text-left cursor-pointer hover:bg-cyan-500/5 transition-colors rounded-r-sm group">
57
+ <Wrench className="size-3.5 text-cyan-400 shrink-0" />
58
+ <span className="text-xs font-medium text-cyan-400">Tools</span>
59
+ <Badge
60
+ variant="ghost"
61
+ className="text-[10px] text-muted-foreground px-1.5 py-0 h-4 font-mono"
62
+ >
63
+ {tools.length}
64
+ </Badge>
65
+ <span className="flex-1" />
66
+ {open ? (
67
+ <ChevronDown className="size-3 text-muted-foreground" />
68
+ ) : (
69
+ <ChevronRight className="size-3 text-muted-foreground" />
70
+ )}
71
+ </CollapsibleTrigger>
72
+ <CollapsibleContent>
73
+ <div className="space-y-1 pt-1">
74
+ {tools.map((tool) => (
75
+ <ToolItem key={tool.name} tool={tool} />
76
+ ))}
77
+ </div>
78
+ </CollapsibleContent>
79
+ </div>
80
+ </Collapsible>
81
+ );
82
+ }
@@ -0,0 +1,288 @@
1
+ import {
2
+ AlertCircle,
3
+ Brain,
4
+ CheckCircle2,
5
+ ChevronDown,
6
+ ChevronRight,
7
+ ImageIcon,
8
+ Terminal,
9
+ } from "lucide-react";
10
+ import { type JSX, useState } from "react";
11
+ import ReactMarkdown from "react-markdown";
12
+ import { Badge } from "@/components/ui/badge";
13
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
14
+ import { JsonViewer, safeJsonValue } from "@/components/ui/json-viewer";
15
+ import { ScrollArea } from "@/components/ui/scroll-area";
16
+ import { cn } from "@/lib/utils";
17
+ import type { ContentBlockType, ResponseContentBlockType } from "../../proxy/schemas";
18
+
19
+ function assertNever(_value: never): JSX.Element {
20
+ return <></>;
21
+ }
22
+
23
+ function SystemReminderBlock({ text }: { text: string }): JSX.Element {
24
+ const [open, setOpen] = useState(false);
25
+
26
+ return (
27
+ <Collapsible open={open} onOpenChange={setOpen}>
28
+ <CollapsibleTrigger className="flex items-center gap-1.5 py-0.5 cursor-pointer hover:opacity-80 transition-opacity group">
29
+ {open ? (
30
+ <ChevronDown className="size-3 text-muted-foreground" />
31
+ ) : (
32
+ <ChevronRight className="size-3 text-muted-foreground" />
33
+ )}
34
+ <span className="text-muted-foreground text-xs italic select-none opacity-60">
35
+ [system-reminder]
36
+ </span>
37
+ </CollapsibleTrigger>
38
+ <CollapsibleContent>
39
+ <div className="pl-4 pt-1">
40
+ <div className="prose prose-sm dark:prose-invert max-w-none [&_pre]:bg-muted [&_pre]:text-foreground [&_code]:text-[0.8em] [&_p]:my-1 [&_ul]:my-1 [&_ol]:my-1">
41
+ <ReactMarkdown>{text}</ReactMarkdown>
42
+ </div>
43
+ </div>
44
+ </CollapsibleContent>
45
+ </Collapsible>
46
+ );
47
+ }
48
+
49
+ export function TextBlock({ text }: { text: string }): JSX.Element {
50
+ if (text.includes("<system-reminder>")) {
51
+ return <SystemReminderBlock text={text} />;
52
+ }
53
+
54
+ return (
55
+ <div className="prose prose-sm dark:prose-invert max-w-none [&_pre]:bg-muted [&_pre]:text-foreground [&_code]:text-[0.8em] [&_p]:my-1 [&_ul]:my-1 [&_ol]:my-1">
56
+ <ReactMarkdown>{text}</ReactMarkdown>
57
+ </div>
58
+ );
59
+ }
60
+
61
+ export function ThinkingBlock({ thinking }: { thinking: string }): JSX.Element {
62
+ const [open, setOpen] = useState(false);
63
+
64
+ return (
65
+ <Collapsible open={open} onOpenChange={setOpen}>
66
+ <div className="border-l-2 border-purple-500/40 my-1">
67
+ <CollapsibleTrigger className="flex items-center gap-1.5 px-3 py-1 w-full text-left cursor-pointer hover:bg-purple-500/5 transition-colors rounded-r-sm group">
68
+ <Brain className="size-3.5 text-purple-400 shrink-0" />
69
+ <span className="text-xs font-medium text-purple-400">Thinking</span>
70
+ <Badge
71
+ variant="ghost"
72
+ className="text-[10px] text-muted-foreground px-1.5 py-0 h-4 font-mono"
73
+ >
74
+ {thinking.length.toLocaleString()} chars
75
+ </Badge>
76
+ <span className="flex-1" />
77
+ {open ? (
78
+ <ChevronDown className="size-3 text-muted-foreground" />
79
+ ) : (
80
+ <ChevronRight className="size-3 text-muted-foreground" />
81
+ )}
82
+ </CollapsibleTrigger>
83
+ <CollapsibleContent>
84
+ <div className="px-3 pb-2">
85
+ <ScrollArea className="max-h-[60vh]">
86
+ <pre className="text-xs text-muted-foreground whitespace-pre-wrap font-mono leading-relaxed">
87
+ {thinking}
88
+ </pre>
89
+ </ScrollArea>
90
+ </div>
91
+ </CollapsibleContent>
92
+ </div>
93
+ </Collapsible>
94
+ );
95
+ }
96
+
97
+ export function ToolUseBlock({
98
+ name,
99
+ input,
100
+ }: {
101
+ name: string;
102
+ input: Record<string, unknown>;
103
+ }): JSX.Element {
104
+ const [open, setOpen] = useState(false);
105
+
106
+ return (
107
+ <Collapsible open={open} onOpenChange={setOpen}>
108
+ <div className="border-l-2 border-blue-500/40 my-1">
109
+ <CollapsibleTrigger className="flex items-center gap-1.5 px-3 py-1 w-full text-left cursor-pointer hover:bg-blue-500/5 transition-colors rounded-r-sm group">
110
+ <Terminal className="size-3.5 text-blue-400 shrink-0" />
111
+ <Badge variant="outline" className="text-[10px] font-mono px-1.5 py-0 h-4">
112
+ {name}
113
+ </Badge>
114
+ <span className="flex-1" />
115
+ {open ? (
116
+ <ChevronDown className="size-3 text-muted-foreground" />
117
+ ) : (
118
+ <ChevronRight className="size-3 text-muted-foreground" />
119
+ )}
120
+ </CollapsibleTrigger>
121
+ <CollapsibleContent>
122
+ <div className="px-3 pb-2">
123
+ <ScrollArea className="max-h-[60vh]">
124
+ <JsonViewer data={safeJsonValue(input)} defaultExpandDepth={2} />
125
+ </ScrollArea>
126
+ </div>
127
+ </CollapsibleContent>
128
+ </div>
129
+ </Collapsible>
130
+ );
131
+ }
132
+
133
+ function ToolResultInlineContent({
134
+ content,
135
+ }: {
136
+ content:
137
+ | string
138
+ | ReadonlyArray<
139
+ | {
140
+ type: "text";
141
+ text: string;
142
+ cache_control?: { type: string; ttl: string; scope?: string };
143
+ }
144
+ | { type: "image"; source: { type: "base64"; media_type: string; data: string } }
145
+ >;
146
+ }): JSX.Element {
147
+ if (typeof content === "string") {
148
+ return (
149
+ <pre className="text-xs text-muted-foreground whitespace-pre-wrap font-mono leading-relaxed">
150
+ {content}
151
+ </pre>
152
+ );
153
+ }
154
+
155
+ return (
156
+ <div className="space-y-1">
157
+ {content.map((item, i) => {
158
+ if (item.type === "text") {
159
+ return (
160
+ <pre
161
+ key={i}
162
+ className="text-xs text-muted-foreground whitespace-pre-wrap font-mono leading-relaxed"
163
+ >
164
+ {item.text}
165
+ </pre>
166
+ );
167
+ }
168
+ return <ImageBlock key={i} source={item.source} />;
169
+ })}
170
+ </div>
171
+ );
172
+ }
173
+
174
+ export function ToolResultBlock({
175
+ content,
176
+ isError,
177
+ }: {
178
+ content:
179
+ | string
180
+ | ReadonlyArray<
181
+ | {
182
+ type: "text";
183
+ text: string;
184
+ cache_control?: { type: string; ttl: string; scope?: string };
185
+ }
186
+ | { type: "image"; source: { type: "base64"; media_type: string; data: string } }
187
+ >;
188
+ isError?: boolean;
189
+ }): JSX.Element {
190
+ const [open, setOpen] = useState(false);
191
+ const hasError = isError === true;
192
+
193
+ return (
194
+ <Collapsible open={open} onOpenChange={setOpen}>
195
+ <div
196
+ className={cn("border-l-2 my-1", hasError ? "border-red-500/40" : "border-green-500/40")}
197
+ >
198
+ <CollapsibleTrigger
199
+ className={cn(
200
+ "flex items-center gap-1.5 px-3 py-1 w-full text-left cursor-pointer transition-colors rounded-r-sm group",
201
+ hasError ? "hover:bg-red-500/5" : "hover:bg-green-500/5",
202
+ )}
203
+ >
204
+ {hasError ? (
205
+ <AlertCircle className="size-3.5 text-red-400 shrink-0" />
206
+ ) : (
207
+ <CheckCircle2 className="size-3.5 text-green-400 shrink-0" />
208
+ )}
209
+ <span className={cn("text-xs font-medium", hasError ? "text-red-400" : "text-green-400")}>
210
+ {hasError ? "Error" : "Result"}
211
+ </span>
212
+ <span className="flex-1" />
213
+ {open ? (
214
+ <ChevronDown className="size-3 text-muted-foreground" />
215
+ ) : (
216
+ <ChevronRight className="size-3 text-muted-foreground" />
217
+ )}
218
+ </CollapsibleTrigger>
219
+ <CollapsibleContent>
220
+ <div className="px-3 pb-2">
221
+ <ScrollArea className="max-h-[60vh]">
222
+ <ToolResultInlineContent content={content} />
223
+ </ScrollArea>
224
+ </div>
225
+ </CollapsibleContent>
226
+ </div>
227
+ </Collapsible>
228
+ );
229
+ }
230
+
231
+ export function ImageBlock({
232
+ source,
233
+ }: {
234
+ source: { type: "base64"; media_type: string; data: string };
235
+ }): JSX.Element {
236
+ return (
237
+ <div className="my-1 inline-flex flex-col gap-1">
238
+ <div className="relative rounded-md border border-border overflow-hidden max-w-[400px]">
239
+ <img
240
+ src={`data:${source.media_type};base64,${source.data}`}
241
+ alt="Content image"
242
+ className="block max-w-full h-auto"
243
+ />
244
+ </div>
245
+ <Badge
246
+ variant="ghost"
247
+ className="text-[10px] text-muted-foreground px-1 py-0 h-4 font-mono w-fit"
248
+ >
249
+ <ImageIcon className="size-2.5" />
250
+ {source.media_type}
251
+ </Badge>
252
+ </div>
253
+ );
254
+ }
255
+
256
+ export function ContentBlockRenderer({ block }: { block: ContentBlockType }): JSX.Element {
257
+ switch (block.type) {
258
+ case "text":
259
+ return <TextBlock text={block.text} />;
260
+ case "thinking":
261
+ return <ThinkingBlock thinking={block.thinking} />;
262
+ case "tool_use":
263
+ return <ToolUseBlock name={block.name} input={block.input} />;
264
+ case "tool_result":
265
+ return <ToolResultBlock content={block.content} isError={block.is_error} />;
266
+ case "image":
267
+ return <ImageBlock source={block.source} />;
268
+ default:
269
+ return assertNever(block);
270
+ }
271
+ }
272
+
273
+ export function ResponseContentBlockRenderer({
274
+ block,
275
+ }: {
276
+ block: ResponseContentBlockType;
277
+ }): JSX.Element {
278
+ switch (block.type) {
279
+ case "text":
280
+ return <TextBlock text={block.text} />;
281
+ case "thinking":
282
+ return <ThinkingBlock thinking={block.thinking} />;
283
+ case "tool_use":
284
+ return <ToolUseBlock name={block.name} input={block.input} />;
285
+ default:
286
+ return assertNever(block);
287
+ }
288
+ }
@@ -0,0 +1,47 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+ import { Slot } from "radix-ui";
3
+ import * as React from "react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const badgeVariants = cva(
8
+ "inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
13
+ secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
14
+ destructive:
15
+ "bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16
+ outline:
17
+ "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
18
+ ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
19
+ link: "text-primary underline-offset-4 [a&]:hover:underline",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ },
25
+ },
26
+ );
27
+
28
+ function Badge({
29
+ className,
30
+ variant = "default",
31
+ asChild = false,
32
+ ...props
33
+ }: React.ComponentProps<"span"> &
34
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }): React.JSX.Element {
35
+ const Comp = asChild ? Slot.Root : "span";
36
+
37
+ return (
38
+ <Comp
39
+ data-slot="badge"
40
+ data-variant={variant}
41
+ className={cn(badgeVariants({ variant }), className)}
42
+ {...props}
43
+ />
44
+ );
45
+ }
46
+
47
+ export { Badge, badgeVariants };