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.
- package/README.md +40 -0
- package/package.json +77 -0
- package/src/App.tsx +7 -0
- package/src/components/ProxyViewer.tsx +102 -0
- package/src/components/ProxyViewerContainer.tsx +48 -0
- package/src/components/proxy-viewer/LogEntry.tsx +95 -0
- package/src/components/proxy-viewer/LogEntryHeader.tsx +148 -0
- package/src/components/proxy-viewer/MessageThread.tsx +55 -0
- package/src/components/proxy-viewer/RequestView.tsx +77 -0
- package/src/components/proxy-viewer/ResponseView.tsx +195 -0
- package/src/components/proxy-viewer/SystemPrompt.tsx +65 -0
- package/src/components/proxy-viewer/ToolDefinitions.tsx +82 -0
- package/src/components/proxy-viewer/content-blocks.tsx +288 -0
- package/src/components/ui/badge.tsx +47 -0
- package/src/components/ui/collapsible.tsx +21 -0
- package/src/components/ui/json-viewer.tsx +383 -0
- package/src/components/ui/scroll-area.tsx +54 -0
- package/src/components/ui/select.tsx +178 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/tabs.tsx +88 -0
- package/src/components/ui/tooltip.tsx +51 -0
- package/src/frontend.tsx +26 -0
- package/src/index.css +11 -0
- package/src/index.html +12 -0
- package/src/index.ts +56 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy/handler.ts +146 -0
- package/src/proxy/schemas.ts +241 -0
- package/src/proxy/store.ts +73 -0
- package/styles/globals.css +121 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# cc-inspector
|
|
2
|
+
|
|
3
|
+
A transparent proxy for the Anthropic Claude API that captures every request and response, letting you inspect system prompts, tool definitions, messages, and token usage in a web UI.
|
|
4
|
+
|
|
5
|
+
Built for understanding what Claude Code sends to the API under the hood.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bunx cc-inspector
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then launch Claude Code through the proxy:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
ANTHROPIC_BASE_URL=http://localhost:25947/proxy claude
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Open http://localhost:25947 to see captured requests in real time.
|
|
20
|
+
|
|
21
|
+
## What You Can See
|
|
22
|
+
|
|
23
|
+
- **System prompts** — the full system message Claude Code sends, including CLAUDE.md contents and tool instructions
|
|
24
|
+
- **Tool definitions** — every tool available to the model, with names, descriptions, and input schemas
|
|
25
|
+
- **Messages** — the complete conversation history sent in each request, with renderers for text, thinking, tool_use, and tool_result blocks
|
|
26
|
+
- **Token usage** — input/output token counts per request
|
|
27
|
+
- **Streaming** — captures SSE streaming responses without buffering
|
|
28
|
+
- **Filtering** — filter by session ID or model
|
|
29
|
+
|
|
30
|
+
## How It Works
|
|
31
|
+
|
|
32
|
+
The proxy sits between Claude Code and `api.anthropic.com`. Setting `ANTHROPIC_BASE_URL` tells Claude Code to send API requests to the proxy instead of directly to Anthropic. The proxy forwards everything to the real API and logs both the request and response.
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
Claude Code → cc-inspector (:25947/proxy/*) → api.anthropic.com
|
|
36
|
+
↓
|
|
37
|
+
Web UI (:25947)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Logs are stored in memory only and reset when the server restarts.
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cc-inspector",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Transparent proxy for the Anthropic Claude API that captures and displays requests/responses in a web UI",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/swen128/cc-inspector"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"claude",
|
|
13
|
+
"anthropic",
|
|
14
|
+
"proxy",
|
|
15
|
+
"claude-code",
|
|
16
|
+
"api-inspector",
|
|
17
|
+
"debugging"
|
|
18
|
+
],
|
|
19
|
+
"bin": {
|
|
20
|
+
"cc-inspector": "src/index.ts"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"src",
|
|
24
|
+
"!src/**/*.test.ts",
|
|
25
|
+
"!src/**/*.stories.tsx",
|
|
26
|
+
"!src/**/__fixtures__",
|
|
27
|
+
"styles"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"dev": "bun --hot src/index.ts",
|
|
31
|
+
"start": "NODE_ENV=production bun src/index.ts",
|
|
32
|
+
"build": "bun run build.ts",
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"lint": "eslint .",
|
|
35
|
+
"format": "biome format --write .",
|
|
36
|
+
"format:check": "biome format .",
|
|
37
|
+
"knip": "knip",
|
|
38
|
+
"check": "bun format && bun typecheck && bun lint && bun knip",
|
|
39
|
+
"prepare": "husky",
|
|
40
|
+
"ladle": "ladle serve",
|
|
41
|
+
"ladle:build": "ladle build"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@radix-ui/react-select": "^2.2.6",
|
|
45
|
+
"@tailwindcss/typography": "^0.5.19",
|
|
46
|
+
"bun-plugin-tailwind": "^0.1.2",
|
|
47
|
+
"class-variance-authority": "^0.7.1",
|
|
48
|
+
"clsx": "^2.1.1",
|
|
49
|
+
"lucide-react": "^0.563.0",
|
|
50
|
+
"radix-ui": "^1.4.3",
|
|
51
|
+
"react": "^19",
|
|
52
|
+
"react-dom": "^19",
|
|
53
|
+
"react-markdown": "^10.1.0",
|
|
54
|
+
"tailwind-merge": "^3.4.0",
|
|
55
|
+
"zod": "^4.3.6"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@biomejs/biome": "^2.3.14",
|
|
59
|
+
"@eslint/js": "^10.0.1",
|
|
60
|
+
"@ladle/react": "^5.1.1",
|
|
61
|
+
"@tailwindcss/vite": "^4.1.18",
|
|
62
|
+
"@types/bun": "latest",
|
|
63
|
+
"@types/react": "^19",
|
|
64
|
+
"@types/react-dom": "^19",
|
|
65
|
+
"@typescript-eslint/eslint-plugin": "^8.55.0",
|
|
66
|
+
"@typescript-eslint/parser": "^8.55.0",
|
|
67
|
+
"eslint": "^9.32.0",
|
|
68
|
+
"eslint-plugin-eslint-comments": "^3.2.0",
|
|
69
|
+
"eslint-plugin-functional": "^9.0.2",
|
|
70
|
+
"eslint-plugin-unicorn": "^63.0.0",
|
|
71
|
+
"husky": "^9.1.7",
|
|
72
|
+
"knip": "^5.83.1",
|
|
73
|
+
"tailwindcss": "^4.1.11",
|
|
74
|
+
"tw-animate-css": "^1.4.0",
|
|
75
|
+
"typescript": "^5.9.3"
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/App.tsx
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
import type { CapturedLog } from "../proxy/schemas";
|
|
3
|
+
import { LogEntry } from "./proxy-viewer/LogEntry";
|
|
4
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
|
5
|
+
|
|
6
|
+
function truncateSessionId(id: string): string {
|
|
7
|
+
if (id.length <= 30) return id;
|
|
8
|
+
return id.slice(0, 12) + "…" + id.slice(-12);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function computeTokenSummary(logs: CapturedLog[]): { totalIn: number; totalOut: number } {
|
|
12
|
+
let totalIn = 0;
|
|
13
|
+
let totalOut = 0;
|
|
14
|
+
for (const log of logs) {
|
|
15
|
+
if (log.inputTokens !== null) totalIn += log.inputTokens;
|
|
16
|
+
if (log.outputTokens !== null) totalOut += log.outputTokens;
|
|
17
|
+
}
|
|
18
|
+
return { totalIn, totalOut };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ProxyViewerProps = {
|
|
22
|
+
logs: CapturedLog[];
|
|
23
|
+
sessions: string[];
|
|
24
|
+
models: string[];
|
|
25
|
+
selectedSession: string;
|
|
26
|
+
selectedModel: string;
|
|
27
|
+
onSessionChange: (session: string) => void;
|
|
28
|
+
onModelChange: (model: string) => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function ProxyViewer({
|
|
32
|
+
logs,
|
|
33
|
+
sessions,
|
|
34
|
+
models,
|
|
35
|
+
selectedSession,
|
|
36
|
+
selectedModel,
|
|
37
|
+
onSessionChange,
|
|
38
|
+
onModelChange,
|
|
39
|
+
}: ProxyViewerProps): JSX.Element {
|
|
40
|
+
const { totalIn, totalOut } = computeTokenSummary(logs);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="max-w-[1200px] mx-auto p-6">
|
|
44
|
+
{/* Header */}
|
|
45
|
+
<div className="flex items-center gap-4 mb-6">
|
|
46
|
+
<h1 className="text-lg font-bold flex-1">Claude Code Proxy</h1>
|
|
47
|
+
<span className="text-muted-foreground text-xs font-mono">
|
|
48
|
+
{logs.length} request{logs.length !== 1 ? "s" : ""}
|
|
49
|
+
{totalIn > 0 || totalOut > 0
|
|
50
|
+
? ` · ${totalIn.toLocaleString()} in / ${totalOut.toLocaleString()} out`
|
|
51
|
+
: ""}
|
|
52
|
+
</span>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
{/* Filters */}
|
|
56
|
+
<div className="flex gap-3 mb-6">
|
|
57
|
+
<Select value={selectedSession} onValueChange={onSessionChange}>
|
|
58
|
+
<SelectTrigger className="flex-1 max-w-[400px] text-xs">
|
|
59
|
+
<SelectValue placeholder="All sessions" />
|
|
60
|
+
</SelectTrigger>
|
|
61
|
+
<SelectContent>
|
|
62
|
+
<SelectItem value="__all__">All sessions</SelectItem>
|
|
63
|
+
{sessions.map((s) => (
|
|
64
|
+
<SelectItem key={s} value={s}>
|
|
65
|
+
{truncateSessionId(s)}
|
|
66
|
+
</SelectItem>
|
|
67
|
+
))}
|
|
68
|
+
</SelectContent>
|
|
69
|
+
</Select>
|
|
70
|
+
|
|
71
|
+
<Select value={selectedModel} onValueChange={onModelChange}>
|
|
72
|
+
<SelectTrigger className="text-xs">
|
|
73
|
+
<SelectValue placeholder="All models" />
|
|
74
|
+
</SelectTrigger>
|
|
75
|
+
<SelectContent>
|
|
76
|
+
<SelectItem value="__all__">All models</SelectItem>
|
|
77
|
+
{models.map((m) => (
|
|
78
|
+
<SelectItem key={m} value={m}>
|
|
79
|
+
{m}
|
|
80
|
+
</SelectItem>
|
|
81
|
+
))}
|
|
82
|
+
</SelectContent>
|
|
83
|
+
</Select>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
{/* Log list */}
|
|
87
|
+
<div>
|
|
88
|
+
{logs.length === 0 ? (
|
|
89
|
+
<div className="text-center text-muted-foreground py-16 space-y-4">
|
|
90
|
+
<p className="text-sm">No requests captured yet.</p>
|
|
91
|
+
<p className="text-xs">Configure Claude Code with:</p>
|
|
92
|
+
<pre className="text-blue-500 font-mono text-sm">
|
|
93
|
+
ANTHROPIC_BASE_URL=http://localhost:25947/proxy claude
|
|
94
|
+
</pre>
|
|
95
|
+
</div>
|
|
96
|
+
) : (
|
|
97
|
+
logs.map((log) => <LogEntry key={log.id} log={log} />)
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, type JSX } from "react";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { CapturedLogSchema, type CapturedLog } from "../proxy/schemas";
|
|
4
|
+
import { ProxyViewer } from "./ProxyViewer";
|
|
5
|
+
|
|
6
|
+
export function ProxyViewerContainer(): JSX.Element {
|
|
7
|
+
const [logs, setLogs] = useState<CapturedLog[]>([]);
|
|
8
|
+
const [sessions, setSessions] = useState<string[]>([]);
|
|
9
|
+
const [models, setModels] = useState<string[]>([]);
|
|
10
|
+
const [selectedSession, setSelectedSession] = useState("__all__");
|
|
11
|
+
const [selectedModel, setSelectedModel] = useState("__all__");
|
|
12
|
+
|
|
13
|
+
const fetchData = useCallback(async () => {
|
|
14
|
+
const params = new URLSearchParams();
|
|
15
|
+
if (selectedSession !== "__all__") params.set("sessionId", selectedSession);
|
|
16
|
+
if (selectedModel !== "__all__") params.set("model", selectedModel);
|
|
17
|
+
|
|
18
|
+
const [logsRes, sessionsRes, modelsRes] = await Promise.all([
|
|
19
|
+
fetch(`/api/logs?${params}`),
|
|
20
|
+
fetch("/api/sessions"),
|
|
21
|
+
fetch("/api/models"),
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
setLogs(z.array(CapturedLogSchema).parse(await logsRes.json()));
|
|
25
|
+
setSessions(z.array(z.string()).parse(await sessionsRes.json()));
|
|
26
|
+
setModels(z.array(z.string()).parse(await modelsRes.json()));
|
|
27
|
+
}, [selectedSession, selectedModel]);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
void fetchData();
|
|
31
|
+
const interval = setInterval(() => {
|
|
32
|
+
void fetchData();
|
|
33
|
+
}, 2000);
|
|
34
|
+
return () => clearInterval(interval);
|
|
35
|
+
}, [fetchData]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<ProxyViewer
|
|
39
|
+
logs={logs}
|
|
40
|
+
sessions={sessions}
|
|
41
|
+
models={models}
|
|
42
|
+
selectedSession={selectedSession}
|
|
43
|
+
selectedModel={selectedModel}
|
|
44
|
+
onSessionChange={setSelectedSession}
|
|
45
|
+
onModelChange={setSelectedModel}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import { JsonViewerFromString } from "@/components/ui/json-viewer";
|
|
4
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import { type CapturedLog, parseRequest } from "@/proxy/schemas";
|
|
7
|
+
import { LogEntryHeader } from "./LogEntryHeader";
|
|
8
|
+
import { RequestView } from "./RequestView";
|
|
9
|
+
import { ResponseView } from "./ResponseView";
|
|
10
|
+
|
|
11
|
+
export type LogEntryProps = {
|
|
12
|
+
log: CapturedLog;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function LogEntry({ log }: LogEntryProps): JSX.Element {
|
|
16
|
+
const [expanded, setExpanded] = useState<boolean>(false);
|
|
17
|
+
const parsedRequest = useMemo(() => parseRequest(log.rawRequestBody), [log.rawRequestBody]);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className={cn("border border-border rounded-lg mb-3 overflow-hidden")}>
|
|
21
|
+
<LogEntryHeader
|
|
22
|
+
log={log}
|
|
23
|
+
parsedRequest={parsedRequest}
|
|
24
|
+
expanded={expanded}
|
|
25
|
+
onToggle={() => {
|
|
26
|
+
setExpanded(!expanded);
|
|
27
|
+
}}
|
|
28
|
+
/>
|
|
29
|
+
|
|
30
|
+
{expanded && (
|
|
31
|
+
<div
|
|
32
|
+
onClick={(e) => {
|
|
33
|
+
e.stopPropagation();
|
|
34
|
+
}}
|
|
35
|
+
onKeyDown={(e) => {
|
|
36
|
+
e.stopPropagation();
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
<Tabs defaultValue="request">
|
|
40
|
+
<TabsList className="mx-4 mt-2">
|
|
41
|
+
<TabsTrigger value="request">Request</TabsTrigger>
|
|
42
|
+
<TabsTrigger value="response">Response</TabsTrigger>
|
|
43
|
+
<TabsTrigger value="raw">Raw</TabsTrigger>
|
|
44
|
+
</TabsList>
|
|
45
|
+
|
|
46
|
+
<TabsContent value="request">
|
|
47
|
+
<div className="px-4 py-3">
|
|
48
|
+
{parsedRequest !== null ? (
|
|
49
|
+
<RequestView request={parsedRequest} />
|
|
50
|
+
) : (
|
|
51
|
+
<pre className="font-mono text-xs whitespace-pre-wrap break-words">
|
|
52
|
+
{log.rawRequestBody ?? "No request body captured"}
|
|
53
|
+
</pre>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
</TabsContent>
|
|
57
|
+
|
|
58
|
+
<TabsContent value="response">
|
|
59
|
+
<div className="px-4 py-3">
|
|
60
|
+
<ResponseView
|
|
61
|
+
responseText={log.responseText}
|
|
62
|
+
responseStatus={log.responseStatus}
|
|
63
|
+
streaming={log.streaming}
|
|
64
|
+
inputTokens={log.inputTokens}
|
|
65
|
+
outputTokens={log.outputTokens}
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
</TabsContent>
|
|
69
|
+
|
|
70
|
+
<TabsContent value="raw">
|
|
71
|
+
<div className="px-4 py-3 space-y-4">
|
|
72
|
+
<div>
|
|
73
|
+
<h4 className="text-xs font-medium text-muted-foreground mb-1.5">Request</h4>
|
|
74
|
+
{log.rawRequestBody !== null ? (
|
|
75
|
+
<JsonViewerFromString text={log.rawRequestBody} defaultExpandDepth={1} />
|
|
76
|
+
) : (
|
|
77
|
+
<p className="text-xs text-muted-foreground italic">No request body</p>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
<div>
|
|
81
|
+
<h4 className="text-xs font-medium text-muted-foreground mb-1.5">Response</h4>
|
|
82
|
+
{log.responseText !== null ? (
|
|
83
|
+
<JsonViewerFromString text={log.responseText} defaultExpandDepth={1} />
|
|
84
|
+
) : (
|
|
85
|
+
<p className="text-xs text-muted-foreground italic">No response body</p>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</TabsContent>
|
|
90
|
+
</Tabs>
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { ChevronDown, ChevronRight, Clock, MessageSquare, Radio, Wrench, Zap } from "lucide-react";
|
|
2
|
+
import type { JSX } from "react";
|
|
3
|
+
import { Badge } from "@/components/ui/badge";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
import type { CapturedLog, ClaudeRequest } from "@/proxy/schemas";
|
|
6
|
+
|
|
7
|
+
function formatElapsed(ms: number): string {
|
|
8
|
+
if (ms < 1000) return `${ms}ms`;
|
|
9
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type StatusCategory = "success" | "client_error" | "server_error" | "pending";
|
|
13
|
+
|
|
14
|
+
function getStatusCategory(status: number | null): StatusCategory {
|
|
15
|
+
if (status === null) return "pending";
|
|
16
|
+
if (status >= 200 && status < 300) return "success";
|
|
17
|
+
if (status >= 400 && status < 500) return "client_error";
|
|
18
|
+
return "server_error";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const STATUS_BADGE_CLASSES: Record<StatusCategory, string> = {
|
|
22
|
+
success: "bg-emerald-500/15 text-emerald-400 border-emerald-500/25",
|
|
23
|
+
client_error: "bg-amber-500/15 text-amber-400 border-amber-500/25",
|
|
24
|
+
server_error: "",
|
|
25
|
+
pending: "bg-muted text-muted-foreground border-border",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type LogEntryHeaderProps = {
|
|
29
|
+
log: CapturedLog;
|
|
30
|
+
parsedRequest: ClaudeRequest | null;
|
|
31
|
+
expanded: boolean;
|
|
32
|
+
onToggle: () => void;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function LogEntryHeader({
|
|
36
|
+
log,
|
|
37
|
+
parsedRequest,
|
|
38
|
+
expanded,
|
|
39
|
+
onToggle,
|
|
40
|
+
}: LogEntryHeaderProps): JSX.Element {
|
|
41
|
+
const statusCategory = getStatusCategory(log.responseStatus);
|
|
42
|
+
|
|
43
|
+
const hasTokens = log.inputTokens !== null || log.outputTokens !== null;
|
|
44
|
+
|
|
45
|
+
const messageCount = parsedRequest !== null ? parsedRequest.messages.length : null;
|
|
46
|
+
|
|
47
|
+
const toolCount =
|
|
48
|
+
parsedRequest !== null && parsedRequest.tools !== undefined && parsedRequest.tools.length > 0
|
|
49
|
+
? parsedRequest.tools.length
|
|
50
|
+
: null;
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
role="button"
|
|
55
|
+
tabIndex={0}
|
|
56
|
+
className={cn(
|
|
57
|
+
"flex items-center gap-2.5 px-3 py-2 cursor-pointer transition-colors",
|
|
58
|
+
"hover:bg-muted/50",
|
|
59
|
+
"select-none",
|
|
60
|
+
)}
|
|
61
|
+
onClick={onToggle}
|
|
62
|
+
onKeyDown={(e) => {
|
|
63
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
onToggle();
|
|
66
|
+
}
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
{/* Request ID */}
|
|
70
|
+
<span className="text-blue-400/80 font-mono text-xs font-semibold tabular-nums shrink-0">
|
|
71
|
+
#{log.id}
|
|
72
|
+
</span>
|
|
73
|
+
|
|
74
|
+
{/* Model */}
|
|
75
|
+
{log.model !== null && (
|
|
76
|
+
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 h-5 font-mono">
|
|
77
|
+
{log.model}
|
|
78
|
+
</Badge>
|
|
79
|
+
)}
|
|
80
|
+
|
|
81
|
+
{/* Response Status */}
|
|
82
|
+
{statusCategory === "server_error" ? (
|
|
83
|
+
<Badge variant="destructive" className="text-[10px] px-1.5 py-0 h-5 font-mono tabular-nums">
|
|
84
|
+
{log.responseStatus}
|
|
85
|
+
</Badge>
|
|
86
|
+
) : (
|
|
87
|
+
<Badge
|
|
88
|
+
variant="outline"
|
|
89
|
+
className={cn(
|
|
90
|
+
"text-[10px] px-1.5 py-0 h-5 font-mono tabular-nums",
|
|
91
|
+
STATUS_BADGE_CLASSES[statusCategory],
|
|
92
|
+
)}
|
|
93
|
+
>
|
|
94
|
+
{log.responseStatus !== null ? log.responseStatus : "..."}
|
|
95
|
+
</Badge>
|
|
96
|
+
)}
|
|
97
|
+
|
|
98
|
+
{/* Elapsed time */}
|
|
99
|
+
{log.elapsedMs !== null && (
|
|
100
|
+
<span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
|
|
101
|
+
<Clock className="size-3" />
|
|
102
|
+
<span className="font-mono tabular-nums">{formatElapsed(log.elapsedMs)}</span>
|
|
103
|
+
</span>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
{/* Token counts */}
|
|
107
|
+
{hasTokens && (
|
|
108
|
+
<span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
|
|
109
|
+
<Zap className="size-3" />
|
|
110
|
+
<span className="font-mono tabular-nums">
|
|
111
|
+
{log.inputTokens !== null ? log.inputTokens.toLocaleString() : "—"}
|
|
112
|
+
{" / "}
|
|
113
|
+
{log.outputTokens !== null ? log.outputTokens.toLocaleString() : "—"}
|
|
114
|
+
</span>
|
|
115
|
+
</span>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{/* Message count */}
|
|
119
|
+
{messageCount !== null && (
|
|
120
|
+
<span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
|
|
121
|
+
<MessageSquare className="size-3" />
|
|
122
|
+
<span className="font-mono tabular-nums">{messageCount}</span>
|
|
123
|
+
</span>
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
{/* Tool count */}
|
|
127
|
+
{toolCount !== null && (
|
|
128
|
+
<span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
|
|
129
|
+
<Wrench className="size-3" />
|
|
130
|
+
<span className="font-mono tabular-nums">{toolCount}</span>
|
|
131
|
+
</span>
|
|
132
|
+
)}
|
|
133
|
+
|
|
134
|
+
{/* Streaming indicator */}
|
|
135
|
+
{log.streaming && <Radio className="size-3 text-muted-foreground/60 shrink-0" />}
|
|
136
|
+
|
|
137
|
+
{/* Spacer */}
|
|
138
|
+
<span className="flex-1 min-w-0" />
|
|
139
|
+
|
|
140
|
+
{/* Expand chevron */}
|
|
141
|
+
{expanded ? (
|
|
142
|
+
<ChevronDown className="size-4 text-muted-foreground shrink-0" />
|
|
143
|
+
) : (
|
|
144
|
+
<ChevronRight className="size-4 text-muted-foreground shrink-0" />
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Bot, User } from "lucide-react";
|
|
2
|
+
import type { JSX } from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
import type { MessageType } from "@/proxy/schemas";
|
|
5
|
+
import { ContentBlockRenderer, TextBlock } from "./content-blocks";
|
|
6
|
+
|
|
7
|
+
export type MessageThreadProps = {
|
|
8
|
+
messages: MessageType[];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function MessageThread({ messages }: MessageThreadProps): JSX.Element {
|
|
12
|
+
return (
|
|
13
|
+
<div>
|
|
14
|
+
{messages.map((message, msgIndex) => (
|
|
15
|
+
<div
|
|
16
|
+
key={`msg-${String(msgIndex)}`}
|
|
17
|
+
className={cn(
|
|
18
|
+
"border-b border-border/50 last:border-b-0",
|
|
19
|
+
"border-l-2 pl-3 py-2",
|
|
20
|
+
message.role === "user" ? "border-l-green-500/40" : "border-l-orange-500/40",
|
|
21
|
+
)}
|
|
22
|
+
>
|
|
23
|
+
{/* Role label */}
|
|
24
|
+
<div className="flex items-center gap-1.5 mb-1.5">
|
|
25
|
+
{message.role === "user" ? (
|
|
26
|
+
<>
|
|
27
|
+
<User className="size-3.5 text-green-400 shrink-0" />
|
|
28
|
+
<span className="text-xs font-bold font-mono text-green-400">USER</span>
|
|
29
|
+
</>
|
|
30
|
+
) : (
|
|
31
|
+
<>
|
|
32
|
+
<Bot className="size-3.5 text-orange-400 shrink-0" />
|
|
33
|
+
<span className="text-xs font-bold font-mono text-orange-400">ASSISTANT</span>
|
|
34
|
+
</>
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
{/* Content blocks */}
|
|
39
|
+
<div className="space-y-1">
|
|
40
|
+
{typeof message.content === "string" ? (
|
|
41
|
+
<TextBlock text={message.content} />
|
|
42
|
+
) : (
|
|
43
|
+
message.content.map((block, blockIndex) => (
|
|
44
|
+
<ContentBlockRenderer
|
|
45
|
+
key={`msg-${String(msgIndex)}-block-${String(blockIndex)}`}
|
|
46
|
+
block={block}
|
|
47
|
+
/>
|
|
48
|
+
))
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
))}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Settings } from "lucide-react";
|
|
2
|
+
import type { JSX } from "react";
|
|
3
|
+
import { Badge } from "@/components/ui/badge";
|
|
4
|
+
import { Separator } from "@/components/ui/separator";
|
|
5
|
+
import type { ClaudeRequest } from "../../proxy/schemas";
|
|
6
|
+
import { MessageThread } from "./MessageThread";
|
|
7
|
+
import { SystemPrompt } from "./SystemPrompt";
|
|
8
|
+
import { ToolDefinitions } from "./ToolDefinitions";
|
|
9
|
+
|
|
10
|
+
function formatBudget(budget: number): string {
|
|
11
|
+
if (budget < 1000) return `${budget}`;
|
|
12
|
+
return `${(budget / 1000).toFixed(0)}k`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type RequestViewProps = {
|
|
16
|
+
request: ClaudeRequest;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function RequestView({ request }: RequestViewProps): JSX.Element {
|
|
20
|
+
const hasSystem = request.system !== undefined && request.system.length > 0;
|
|
21
|
+
const hasTools = request.tools !== undefined && request.tools.length > 0;
|
|
22
|
+
|
|
23
|
+
const thinkingEnabled = request.thinking !== undefined && request.thinking.type === "enabled";
|
|
24
|
+
|
|
25
|
+
const hasConfigBadges =
|
|
26
|
+
request.max_tokens !== undefined ||
|
|
27
|
+
request.temperature !== undefined ||
|
|
28
|
+
thinkingEnabled ||
|
|
29
|
+
request.stream === true;
|
|
30
|
+
|
|
31
|
+
const showSeparator = hasConfigBadges || hasSystem || hasTools;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="space-y-3">
|
|
35
|
+
{/* Config bar */}
|
|
36
|
+
{hasConfigBadges && (
|
|
37
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
38
|
+
<Settings className="size-3 text-muted-foreground shrink-0" />
|
|
39
|
+
{request.max_tokens !== undefined && (
|
|
40
|
+
<Badge variant="outline" className="text-xs font-mono px-1.5 py-0 h-5">
|
|
41
|
+
max: {request.max_tokens}
|
|
42
|
+
</Badge>
|
|
43
|
+
)}
|
|
44
|
+
{request.temperature !== undefined && (
|
|
45
|
+
<Badge variant="outline" className="text-xs font-mono px-1.5 py-0 h-5">
|
|
46
|
+
temp: {request.temperature}
|
|
47
|
+
</Badge>
|
|
48
|
+
)}
|
|
49
|
+
{thinkingEnabled &&
|
|
50
|
+
request.thinking !== undefined &&
|
|
51
|
+
request.thinking.type === "enabled" && (
|
|
52
|
+
<Badge variant="outline" className="text-xs font-mono px-1.5 py-0 h-5">
|
|
53
|
+
thinking: {formatBudget(request.thinking.budget_tokens)} budget
|
|
54
|
+
</Badge>
|
|
55
|
+
)}
|
|
56
|
+
{request.stream === true && (
|
|
57
|
+
<Badge variant="outline" className="text-xs font-mono px-1.5 py-0 h-5">
|
|
58
|
+
streaming
|
|
59
|
+
</Badge>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
)}
|
|
63
|
+
|
|
64
|
+
{/* System prompt */}
|
|
65
|
+
{hasSystem && request.system !== undefined && <SystemPrompt blocks={request.system} />}
|
|
66
|
+
|
|
67
|
+
{/* Tool definitions */}
|
|
68
|
+
{hasTools && request.tools !== undefined && <ToolDefinitions tools={request.tools} />}
|
|
69
|
+
|
|
70
|
+
{/* Separator between config/system/tools and messages */}
|
|
71
|
+
{showSeparator && <Separator />}
|
|
72
|
+
|
|
73
|
+
{/* Message thread */}
|
|
74
|
+
<MessageThread messages={request.messages} />
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|