@tonyclaw/llm-inspector 1.15.1 → 1.16.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/.output/cli.js +1 -0
- package/.output/nitro.json +1 -1
- package/.output/public/assets/index-BmkN9DxE.js +107 -0
- package/.output/public/assets/index-DPe3eOih.css +1 -0
- package/.output/public/assets/{main-C2-qvdhC.js → main-BjnjXVBU.js} +1 -1
- package/.output/server/_ssr/{index-C001qcnM.mjs → index-BIOEVAzU.mjs} +216 -180
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-D7aEu4dR.mjs → router-THS9ptvu.mjs} +392 -170
- package/.output/server/{_tanstack-start-manifest_v-BXMwlSXD.mjs → _tanstack-start-manifest_v-BYhN7q_z.mjs} +1 -1
- package/.output/server/index.mjs +27 -27
- package/package.json +1 -1
- package/src/cli.ts +1 -0
- package/src/components/ProxyViewer.tsx +16 -54
- package/src/components/ProxyViewerContainer.tsx +121 -77
- package/src/components/providers/ImportWizardDialog.tsx +27 -3
- package/src/components/proxy-viewer/ConversationGroup.tsx +3 -3
- package/src/components/proxy-viewer/ConversationHeader.tsx +1 -1
- package/src/components/proxy-viewer/LogEntry.tsx +184 -171
- package/src/components/proxy-viewer/LogEntryHeader.tsx +126 -137
- package/src/components/proxy-viewer/ResponseView.tsx +3 -2
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +3 -3
- package/src/components/proxy-viewer/TurnGroup.tsx +4 -4
- package/src/components/proxy-viewer/diff/DiffView.tsx +5 -3
- package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +13 -9
- package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +3 -3
- package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +7 -3
- package/src/components/ui/json-viewer.tsx +3 -3
- package/src/lib/objectUtils.ts +22 -0
- package/src/proxy/claudeCodeStrip.ts +5 -8
- package/src/proxy/logIndex.ts +58 -43
- package/src/proxy/logger.ts +51 -27
- package/src/proxy/openaiOrphanToolStrip.ts +11 -17
- package/src/proxy/providerImporters.ts +245 -19
- package/src/proxy/providers.ts +20 -7
- package/src/proxy/schemas.ts +5 -9
- package/src/proxy/socketTracker.ts +109 -78
- package/src/proxy/store.ts +52 -83
- package/.output/public/assets/index-BIUbzgJN.css +0 -1
- package/.output/public/assets/index-TwYgzIL4.js +0 -107
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const tsrStartManifest = () => ({ "routes": { "__root__": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/__root.tsx", "children": ["/", "/api/config", "/api/health", "/api/logs", "/api/mcp", "/api/models", "/api/providers", "/api/sessions", "/proxy/$"], "preloads": ["/assets/main-
|
|
1
|
+
const tsrStartManifest = () => ({ "routes": { "__root__": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/__root.tsx", "children": ["/", "/api/config", "/api/health", "/api/logs", "/api/mcp", "/api/models", "/api/providers", "/api/sessions", "/proxy/$"], "preloads": ["/assets/main-BjnjXVBU.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-BmkN9DxE.js"] }, "/api/config": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/config.ts", "children": ["/api/config/paths"] }, "/api/health": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/health.ts" }, "/api/logs": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.ts", "children": ["/api/logs/$id", "/api/logs/stream"] }, "/api/mcp": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/mcp.ts" }, "/api/models": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/models.ts" }, "/api/providers": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.ts", "children": ["/api/providers/$providerId", "/api/providers/export", "/api/providers/import", "/api/providers/scan"] }, "/api/sessions": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/sessions.ts" }, "/proxy/$": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/proxy/$.ts" }, "/api/config/paths": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/config.paths.ts" }, "/api/logs/$id": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.ts", "children": ["/api/logs/$id/chunks", "/api/logs/$id/replay"] }, "/api/logs/stream": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.stream.ts" }, "/api/providers/$providerId": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.ts", "children": ["/api/providers/$providerId/test"] }, "/api/providers/export": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.export.ts" }, "/api/providers/import": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.import.ts" }, "/api/providers/scan": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.scan.ts" }, "/api/logs/$id/chunks": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.chunks.ts" }, "/api/logs/$id/replay": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.replay.ts" }, "/api/providers/$providerId/test": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.test.ts", "children": ["/api/providers/$providerId/test/log"] }, "/api/providers/$providerId/test/log": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.test.log.ts" } }, "clientEntry": "/assets/main-BjnjXVBU.js" });
|
|
2
2
|
export {
|
|
3
3
|
tsrStartManifest
|
|
4
4
|
};
|
package/.output/server/index.mjs
CHANGED
|
@@ -38,51 +38,51 @@ const assets = {
|
|
|
38
38
|
"/assets/alibaba-TTwafVwX.svg": {
|
|
39
39
|
"type": "image/svg+xml",
|
|
40
40
|
"etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
|
|
41
|
-
"mtime": "2026-06-
|
|
41
|
+
"mtime": "2026-06-13T05:45:10.081Z",
|
|
42
42
|
"size": 5915,
|
|
43
43
|
"path": "../public/assets/alibaba-TTwafVwX.svg"
|
|
44
44
|
},
|
|
45
|
-
"/assets/index-
|
|
45
|
+
"/assets/index-DPe3eOih.css": {
|
|
46
46
|
"type": "text/css; charset=utf-8",
|
|
47
|
-
"etag": '"
|
|
48
|
-
"mtime": "2026-06-
|
|
49
|
-
"size":
|
|
50
|
-
"path": "../public/assets/index-
|
|
47
|
+
"etag": '"15a32-YUlUFJPXe5WrOzMkLVz9gOyERE4"',
|
|
48
|
+
"mtime": "2026-06-13T05:45:10.081Z",
|
|
49
|
+
"size": 88626,
|
|
50
|
+
"path": "../public/assets/index-DPe3eOih.css"
|
|
51
|
+
},
|
|
52
|
+
"/assets/qwen-CONDcHqt.png": {
|
|
53
|
+
"type": "image/png",
|
|
54
|
+
"etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
|
|
55
|
+
"mtime": "2026-06-13T05:45:10.081Z",
|
|
56
|
+
"size": 357059,
|
|
57
|
+
"path": "../public/assets/qwen-CONDcHqt.png"
|
|
58
|
+
},
|
|
59
|
+
"/assets/main-BjnjXVBU.js": {
|
|
60
|
+
"type": "text/javascript; charset=utf-8",
|
|
61
|
+
"etag": '"50599-TBFFt8m3KX+84o0h5FGqLcw9zvM"',
|
|
62
|
+
"mtime": "2026-06-13T05:45:10.081Z",
|
|
63
|
+
"size": 329113,
|
|
64
|
+
"path": "../public/assets/main-BjnjXVBU.js"
|
|
51
65
|
},
|
|
52
66
|
"/assets/minimax-BPMzvuL-.jpeg": {
|
|
53
67
|
"type": "image/jpeg",
|
|
54
68
|
"etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
|
|
55
|
-
"mtime": "2026-06-
|
|
69
|
+
"mtime": "2026-06-13T05:45:10.078Z",
|
|
56
70
|
"size": 6918,
|
|
57
71
|
"path": "../public/assets/minimax-BPMzvuL-.jpeg"
|
|
58
72
|
},
|
|
59
73
|
"/assets/zhipuai-BPNAnxo-.svg": {
|
|
60
74
|
"type": "image/svg+xml",
|
|
61
75
|
"etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
|
|
62
|
-
"mtime": "2026-06-
|
|
76
|
+
"mtime": "2026-06-13T05:45:10.081Z",
|
|
63
77
|
"size": 11256,
|
|
64
78
|
"path": "../public/assets/zhipuai-BPNAnxo-.svg"
|
|
65
79
|
},
|
|
66
|
-
"/assets/
|
|
67
|
-
"type": "image/png",
|
|
68
|
-
"etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
|
|
69
|
-
"mtime": "2026-06-13T01:52:42.193Z",
|
|
70
|
-
"size": 357059,
|
|
71
|
-
"path": "../public/assets/qwen-CONDcHqt.png"
|
|
72
|
-
},
|
|
73
|
-
"/assets/main-C2-qvdhC.js": {
|
|
74
|
-
"type": "text/javascript; charset=utf-8",
|
|
75
|
-
"etag": '"50599-m0++N6iqOd8eV+QO9BaomZTQ8VY"',
|
|
76
|
-
"mtime": "2026-06-13T01:52:42.193Z",
|
|
77
|
-
"size": 329113,
|
|
78
|
-
"path": "../public/assets/main-C2-qvdhC.js"
|
|
79
|
-
},
|
|
80
|
-
"/assets/index-TwYgzIL4.js": {
|
|
80
|
+
"/assets/index-BmkN9DxE.js": {
|
|
81
81
|
"type": "text/javascript; charset=utf-8",
|
|
82
|
-
"etag": '"
|
|
83
|
-
"mtime": "2026-06-
|
|
84
|
-
"size":
|
|
85
|
-
"path": "../public/assets/index-
|
|
82
|
+
"etag": '"9ae0c-h7IO5tRUMmZre12bMt7hEiFWLhQ"',
|
|
83
|
+
"mtime": "2026-06-13T05:45:10.081Z",
|
|
84
|
+
"size": 634380,
|
|
85
|
+
"path": "../public/assets/index-BmkN9DxE.js"
|
|
86
86
|
}
|
|
87
87
|
};
|
|
88
88
|
function readAsset(id) {
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -126,6 +126,7 @@ console.log(``);
|
|
|
126
126
|
console.log(`Route AI coding tools through the proxy:`);
|
|
127
127
|
console.log(` Claude Code: ANTHROPIC_BASE_URL=${url}/proxy claude`);
|
|
128
128
|
console.log(` OpenCode: LLM_BASE_URL=${url}/proxy opencode`);
|
|
129
|
+
console.log(` MiMo Code: OPENAI_BASE_URL=${url}/proxy mimo`);
|
|
129
130
|
console.log(` Direct HTTP: curl ${url}/proxy/v1/messages -d '{"model":"...","messages":[...]}'`);
|
|
130
131
|
console.log(``);
|
|
131
132
|
console.log(`Routing environment variables:`);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { type JSX, useCallback, useEffect, useMemo, useState
|
|
2
|
-
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
1
|
+
import { type JSX, useCallback, useEffect, useMemo, useState } from "react";
|
|
3
2
|
import { Download } from "lucide-react";
|
|
4
3
|
|
|
5
4
|
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "./ui/tooltip";
|
|
@@ -115,7 +114,7 @@ export function ProxyViewer({
|
|
|
115
114
|
onViewModeChange,
|
|
116
115
|
strip,
|
|
117
116
|
}: ProxyViewerProps): JSX.Element {
|
|
118
|
-
const { totalIn, totalOut } = computeTokenSummary(logs);
|
|
117
|
+
const { totalIn, totalOut } = useMemo(() => computeTokenSummary(logs), [logs]);
|
|
119
118
|
const [exporting, setExporting] = useState(false);
|
|
120
119
|
const [comparePair, setComparePair] = useState<[CapturedLog, CapturedLog] | null>(null);
|
|
121
120
|
const [crabEntrancePhase, setCrabEntrancePhase] = useState<"hidden" | "playing" | "done">(
|
|
@@ -142,8 +141,6 @@ export function ProxyViewer({
|
|
|
142
141
|
setExporting(false);
|
|
143
142
|
}
|
|
144
143
|
}, [logs]);
|
|
145
|
-
const parentRef = useRef<HTMLDivElement>(null);
|
|
146
|
-
|
|
147
144
|
// Close the compare drawer when the user changes the session or model
|
|
148
145
|
// filter, since the predecessor relationship may no longer be meaningful.
|
|
149
146
|
useEffect(() => {
|
|
@@ -165,17 +162,6 @@ export function ProxyViewer({
|
|
|
165
162
|
[comparisonPredecessors],
|
|
166
163
|
);
|
|
167
164
|
|
|
168
|
-
const rowVirtualizer = useVirtualizer({
|
|
169
|
-
count: groups.length,
|
|
170
|
-
getScrollElement: () => parentRef.current,
|
|
171
|
-
estimateSize: () => 150,
|
|
172
|
-
measureElement:
|
|
173
|
-
typeof window !== "undefined"
|
|
174
|
-
? (element) => element.getBoundingClientRect().height
|
|
175
|
-
: undefined,
|
|
176
|
-
overscan: 5,
|
|
177
|
-
});
|
|
178
|
-
|
|
179
165
|
return (
|
|
180
166
|
<div className="max-w-[1200px] mx-auto flex flex-col h-screen" style={{ maxHeight: "100vh" }}>
|
|
181
167
|
{/* Brand row */}
|
|
@@ -338,44 +324,20 @@ export function ProxyViewer({
|
|
|
338
324
|
<CopyableCommand command="LLM_BASE_URL=http://localhost:25947/proxy <your-tool>" />
|
|
339
325
|
</div>
|
|
340
326
|
) : (
|
|
341
|
-
<div
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
data-index={virtualRow.index}
|
|
356
|
-
ref={rowVirtualizer.measureElement}
|
|
357
|
-
style={{
|
|
358
|
-
position: "absolute",
|
|
359
|
-
top: 0,
|
|
360
|
-
left: 0,
|
|
361
|
-
width: "100%",
|
|
362
|
-
transform: `translateY(${virtualRow.start}px)`,
|
|
363
|
-
}}
|
|
364
|
-
>
|
|
365
|
-
<ConversationGroup
|
|
366
|
-
group={group}
|
|
367
|
-
viewMode={viewMode}
|
|
368
|
-
strip={strip}
|
|
369
|
-
cacheTrends={cacheTrends}
|
|
370
|
-
onCompareWithPrevious={handleCompareWithPrevious}
|
|
371
|
-
comparisonPredecessors={comparisonPredecessors}
|
|
372
|
-
onClearGroup={onClearGroup}
|
|
373
|
-
standalone={groups.length === 1}
|
|
374
|
-
/>
|
|
375
|
-
</div>
|
|
376
|
-
);
|
|
377
|
-
})}
|
|
378
|
-
</div>
|
|
327
|
+
<div className="overflow-y-auto h-full flex flex-col gap-2">
|
|
328
|
+
{groups.map((group) => (
|
|
329
|
+
<ConversationGroup
|
|
330
|
+
key={group.id}
|
|
331
|
+
group={group}
|
|
332
|
+
viewMode={viewMode}
|
|
333
|
+
strip={strip}
|
|
334
|
+
cacheTrends={cacheTrends}
|
|
335
|
+
onCompareWithPrevious={handleCompareWithPrevious}
|
|
336
|
+
comparisonPredecessors={comparisonPredecessors}
|
|
337
|
+
onClearGroup={onClearGroup}
|
|
338
|
+
standalone={groups.length === 1}
|
|
339
|
+
/>
|
|
340
|
+
))}
|
|
379
341
|
</div>
|
|
380
342
|
)}
|
|
381
343
|
</div>
|
|
@@ -1,23 +1,9 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback, useRef, type JSX } from "react";
|
|
1
|
+
import { useState, useEffect, useCallback, useRef, useMemo, type JSX } from "react";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { CapturedLogSchema, type CapturedLog } from "../proxy/schemas";
|
|
4
4
|
import { useStripConfig } from "../lib/useStripConfig";
|
|
5
5
|
import { ProxyViewer } from "./ProxyViewer";
|
|
6
6
|
|
|
7
|
-
type logsResponse = {
|
|
8
|
-
logs: CapturedLog[];
|
|
9
|
-
total: number;
|
|
10
|
-
offset: number;
|
|
11
|
-
limit: number;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const LogsResponseSchema = z.object({
|
|
15
|
-
logs: z.array(CapturedLogSchema),
|
|
16
|
-
total: z.number(),
|
|
17
|
-
offset: z.number(),
|
|
18
|
-
limit: z.number(),
|
|
19
|
-
});
|
|
20
|
-
|
|
21
7
|
type SSEUpdate =
|
|
22
8
|
| {
|
|
23
9
|
type: "init";
|
|
@@ -55,8 +41,31 @@ function extractModels(logs: CapturedLog[]): string[] {
|
|
|
55
41
|
return [...set];
|
|
56
42
|
}
|
|
57
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Filter logs by selected session/model. Both `__all__` and missing values
|
|
46
|
+
* pass through. Returns the input array reference when no log matches the
|
|
47
|
+
* filter, so referential equality is preserved for empty results.
|
|
48
|
+
*/
|
|
49
|
+
function filterLogs(
|
|
50
|
+
logs: CapturedLog[],
|
|
51
|
+
selectedSession: string,
|
|
52
|
+
selectedModel: string,
|
|
53
|
+
): CapturedLog[] {
|
|
54
|
+
if (selectedSession === "__all__" && selectedModel === "__all__") return logs;
|
|
55
|
+
return logs.filter((l) => {
|
|
56
|
+
if (selectedSession !== "__all__" && l.sessionId !== selectedSession) return false;
|
|
57
|
+
if (selectedModel !== "__all__" && l.model !== selectedModel) return false;
|
|
58
|
+
return true;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const DEBOUNCE_MS = 50;
|
|
63
|
+
|
|
58
64
|
export function ProxyViewerContainer(): JSX.Element {
|
|
59
|
-
|
|
65
|
+
// `allLogs` is the unfiltered set populated by the SSE. The single SSE
|
|
66
|
+
// connection never re-opens on filter change — we always carry the full
|
|
67
|
+
// set and derive the displayed view with `useMemo` below.
|
|
68
|
+
const [allLogs, setAllLogs] = useState<CapturedLog[]>([]);
|
|
60
69
|
const [sessions, setSessions] = useState<string[]>([]);
|
|
61
70
|
const [models, setModels] = useState<string[]>([]);
|
|
62
71
|
const [selectedSession, setSelectedSession] = useState("__all__");
|
|
@@ -66,24 +75,63 @@ export function ProxyViewerContainer(): JSX.Element {
|
|
|
66
75
|
const eventSourceRef = useRef<EventSource | null>(null);
|
|
67
76
|
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
68
77
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
// O(1) log lookup by id
|
|
79
|
+
const logIndexRef = useRef<Map<number, number>>(new Map());
|
|
80
|
+
const logsRef = useRef<CapturedLog[]>([]);
|
|
81
|
+
|
|
82
|
+
// Debounce buffer for SSE updates
|
|
83
|
+
const pendingUpdatesRef = useRef<CapturedLog[]>([]);
|
|
84
|
+
const flushTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
85
|
+
|
|
86
|
+
// Derived view: the logs the user actually sees. Recomputed only when the
|
|
87
|
+
// underlying set or filter changes — never on every SSE message.
|
|
88
|
+
const logs = useMemo(
|
|
89
|
+
() => filterLogs(allLogs, selectedSession, selectedModel),
|
|
90
|
+
[allLogs, selectedSession, selectedModel],
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const flushUpdates = useCallback(() => {
|
|
94
|
+
flushTimerRef.current = null;
|
|
95
|
+
const updates = pendingUpdatesRef.current;
|
|
96
|
+
pendingUpdatesRef.current = [];
|
|
97
|
+
if (updates.length === 0) return;
|
|
98
|
+
|
|
99
|
+
setAllLogs((prev) => {
|
|
100
|
+
let next = prev;
|
|
101
|
+
let sessionsChanged = false;
|
|
102
|
+
let modelsChanged = false;
|
|
103
|
+
for (const log of updates) {
|
|
104
|
+
const idx = logIndexRef.current.get(log.id);
|
|
105
|
+
if (idx !== undefined) {
|
|
106
|
+
// Replace existing entry
|
|
107
|
+
next = [...next.slice(0, idx), log, ...next.slice(idx + 1)];
|
|
108
|
+
} else {
|
|
109
|
+
logIndexRef.current.set(log.id, next.length);
|
|
110
|
+
next = [...next, log];
|
|
111
|
+
}
|
|
112
|
+
if (log.sessionId !== null && log.sessionId !== "" && !sessions.includes(log.sessionId)) {
|
|
113
|
+
sessionsChanged = true;
|
|
114
|
+
}
|
|
115
|
+
if (log.model !== null && log.model !== "" && !models.includes(log.model)) {
|
|
116
|
+
modelsChanged = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
logsRef.current = next;
|
|
120
|
+
if (sessionsChanged) setSessions((s) => extractSessions(next));
|
|
121
|
+
if (modelsChanged) setModels((m) => extractModels(next));
|
|
122
|
+
return next;
|
|
123
|
+
});
|
|
124
|
+
}, [sessions, models]);
|
|
125
|
+
|
|
126
|
+
const scheduleUpdate = useCallback(
|
|
127
|
+
(log: CapturedLog) => {
|
|
128
|
+
pendingUpdatesRef.current.push(log);
|
|
129
|
+
if (flushTimerRef.current === null) {
|
|
130
|
+
flushTimerRef.current = setTimeout(flushUpdates, DEBOUNCE_MS);
|
|
82
131
|
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}, []);
|
|
132
|
+
},
|
|
133
|
+
[flushUpdates],
|
|
134
|
+
);
|
|
87
135
|
|
|
88
136
|
const connectSSE = useCallback(() => {
|
|
89
137
|
// Clean up existing connection
|
|
@@ -91,11 +139,12 @@ export function ProxyViewerContainer(): JSX.Element {
|
|
|
91
139
|
eventSourceRef.current.close();
|
|
92
140
|
}
|
|
93
141
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
142
|
+
// Stable, unfiltered connection. The frontend derives filtered views
|
|
143
|
+
// from the complete set, so the SSE never needs to reopen on filter
|
|
144
|
+
// change. If the in-memory set ever grows past what the client can
|
|
145
|
+
// handle, swap this back to a parameterized URL and reconnect on
|
|
146
|
+
// filter change.
|
|
147
|
+
const es = new EventSource("/api/logs/stream");
|
|
99
148
|
eventSourceRef.current = es;
|
|
100
149
|
|
|
101
150
|
es.onmessage = (event: MessageEvent) => {
|
|
@@ -109,42 +158,27 @@ export function ProxyViewerContainer(): JSX.Element {
|
|
|
109
158
|
}
|
|
110
159
|
const update = updateResult.data;
|
|
111
160
|
if (update.type === "init") {
|
|
112
|
-
|
|
161
|
+
// Flush any pending debounce
|
|
162
|
+
if (flushTimerRef.current !== null) {
|
|
163
|
+
clearTimeout(flushTimerRef.current);
|
|
164
|
+
flushTimerRef.current = null;
|
|
165
|
+
}
|
|
166
|
+
pendingUpdatesRef.current = [];
|
|
167
|
+
|
|
168
|
+
// Build fresh index
|
|
169
|
+
const idx = new Map<number, number>();
|
|
170
|
+
for (let i = 0; i < update.logs.length; i++) {
|
|
171
|
+
const log = update.logs[i];
|
|
172
|
+
if (log !== undefined) idx.set(log.id, i);
|
|
173
|
+
}
|
|
174
|
+
logIndexRef.current = idx;
|
|
175
|
+
logsRef.current = update.logs;
|
|
176
|
+
setAllLogs(update.logs);
|
|
113
177
|
setSessions(extractSessions(update.logs));
|
|
114
178
|
setModels(extractModels(update.logs));
|
|
115
179
|
setError(null);
|
|
116
180
|
} else if (update.type === "update") {
|
|
117
|
-
|
|
118
|
-
// Filter by current selection before adding
|
|
119
|
-
const sessionMatch =
|
|
120
|
-
selectedSession === "__all__" || update.log.sessionId === selectedSession;
|
|
121
|
-
const modelMatch = selectedModel === "__all__" || update.log.model === selectedModel;
|
|
122
|
-
if (!sessionMatch || !modelMatch) return prev;
|
|
123
|
-
|
|
124
|
-
// Update existing log or add as new
|
|
125
|
-
const existsIndex = prev.findIndex((l) => l.id === update.log.id);
|
|
126
|
-
if (existsIndex >= 0) {
|
|
127
|
-
// Replace the existing log with updated data
|
|
128
|
-
return prev.map((l) => (l.id === update.log.id ? update.log : l));
|
|
129
|
-
}
|
|
130
|
-
// Add to end (newest)
|
|
131
|
-
return [...prev, update.log];
|
|
132
|
-
});
|
|
133
|
-
// Update sessions and models
|
|
134
|
-
setSessions((prev) => {
|
|
135
|
-
const sessionId = update.log.sessionId;
|
|
136
|
-
if (sessionId !== null && sessionId !== undefined && sessionId !== "") {
|
|
137
|
-
return prev.includes(sessionId) ? prev : [...prev, sessionId];
|
|
138
|
-
}
|
|
139
|
-
return prev;
|
|
140
|
-
});
|
|
141
|
-
setModels((prev) => {
|
|
142
|
-
const model = update.log.model;
|
|
143
|
-
if (model !== null && model !== undefined && model !== "") {
|
|
144
|
-
return prev.includes(model) ? prev : [...prev, model];
|
|
145
|
-
}
|
|
146
|
-
return prev;
|
|
147
|
-
});
|
|
181
|
+
scheduleUpdate(update.log);
|
|
148
182
|
}
|
|
149
183
|
} catch {
|
|
150
184
|
setError("Failed to parse SSE data");
|
|
@@ -154,16 +188,12 @@ export function ProxyViewerContainer(): JSX.Element {
|
|
|
154
188
|
es.onerror = () => {
|
|
155
189
|
setError("SSE connection lost, reconnecting...");
|
|
156
190
|
es.close();
|
|
157
|
-
// Clear any existing reconnect timeout
|
|
158
191
|
if (reconnectTimeoutRef.current !== null) {
|
|
159
192
|
clearTimeout(reconnectTimeoutRef.current);
|
|
160
193
|
}
|
|
161
|
-
// Reconnect after 3 seconds
|
|
162
194
|
reconnectTimeoutRef.current = setTimeout(connectSSE, 3000);
|
|
163
195
|
};
|
|
164
|
-
|
|
165
|
-
void fetchSessionsAndModels();
|
|
166
|
-
}, [selectedSession, selectedModel, fetchSessionsAndModels]);
|
|
196
|
+
}, [scheduleUpdate]);
|
|
167
197
|
|
|
168
198
|
useEffect(() => {
|
|
169
199
|
connectSSE();
|
|
@@ -176,6 +206,10 @@ export function ProxyViewerContainer(): JSX.Element {
|
|
|
176
206
|
clearTimeout(reconnectTimeoutRef.current);
|
|
177
207
|
reconnectTimeoutRef.current = null;
|
|
178
208
|
}
|
|
209
|
+
if (flushTimerRef.current !== null) {
|
|
210
|
+
clearTimeout(flushTimerRef.current);
|
|
211
|
+
flushTimerRef.current = null;
|
|
212
|
+
}
|
|
179
213
|
};
|
|
180
214
|
}, [connectSSE]);
|
|
181
215
|
|
|
@@ -187,7 +221,9 @@ export function ProxyViewerContainer(): JSX.Element {
|
|
|
187
221
|
setError("Failed to clear logs");
|
|
188
222
|
return;
|
|
189
223
|
}
|
|
190
|
-
|
|
224
|
+
logIndexRef.current.clear();
|
|
225
|
+
logsRef.current = [];
|
|
226
|
+
setAllLogs([]);
|
|
191
227
|
setSessions([]);
|
|
192
228
|
setModels([]);
|
|
193
229
|
setError(null);
|
|
@@ -211,8 +247,16 @@ export function ProxyViewerContainer(): JSX.Element {
|
|
|
211
247
|
return;
|
|
212
248
|
}
|
|
213
249
|
const idSet = new Set(ids);
|
|
214
|
-
|
|
250
|
+
setAllLogs((prev) => {
|
|
215
251
|
const remaining = prev.filter((l) => !idSet.has(l.id));
|
|
252
|
+
// Rebuild index
|
|
253
|
+
const idx = new Map<number, number>();
|
|
254
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
255
|
+
const log = remaining[i];
|
|
256
|
+
if (log !== undefined) idx.set(log.id, i);
|
|
257
|
+
}
|
|
258
|
+
logIndexRef.current = idx;
|
|
259
|
+
logsRef.current = remaining;
|
|
216
260
|
setSessions(extractSessions(remaining));
|
|
217
261
|
setModels(extractModels(remaining));
|
|
218
262
|
return remaining;
|
|
@@ -38,6 +38,29 @@ function OpenCodeIcon(): JSX.Element {
|
|
|
38
38
|
);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function MiMoCodeIcon(): JSX.Element {
|
|
42
|
+
return (
|
|
43
|
+
<svg
|
|
44
|
+
fill="currentColor"
|
|
45
|
+
viewBox="0 0 24 24"
|
|
46
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
47
|
+
className="size-6 shrink-0"
|
|
48
|
+
>
|
|
49
|
+
<title>MiMo Code</title>
|
|
50
|
+
<text
|
|
51
|
+
x="12"
|
|
52
|
+
y="18"
|
|
53
|
+
textAnchor="middle"
|
|
54
|
+
fontSize="16"
|
|
55
|
+
fontWeight="800"
|
|
56
|
+
fontFamily="system-ui, sans-serif"
|
|
57
|
+
>
|
|
58
|
+
M
|
|
59
|
+
</text>
|
|
60
|
+
</svg>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
41
64
|
const ExternalProviderSchema = z.object({
|
|
42
65
|
name: z.string(),
|
|
43
66
|
apiKey: z.string(),
|
|
@@ -45,7 +68,7 @@ const ExternalProviderSchema = z.object({
|
|
|
45
68
|
anthropicBaseUrl: z.string(),
|
|
46
69
|
openaiBaseUrl: z.string(),
|
|
47
70
|
models: z.array(z.string()),
|
|
48
|
-
sourceTool: z.enum(["claude-code", "opencode"]),
|
|
71
|
+
sourceTool: z.enum(["claude-code", "opencode", "mimo-code"]),
|
|
49
72
|
alreadyExists: z.boolean(),
|
|
50
73
|
});
|
|
51
74
|
|
|
@@ -64,6 +87,7 @@ const ImportResponseSchema = z.object({
|
|
|
64
87
|
const sourceLogoMap: Record<string, () => JSX.Element> = {
|
|
65
88
|
"claude-code": ClaudeCodeIcon,
|
|
66
89
|
opencode: OpenCodeIcon,
|
|
90
|
+
"mimo-code": MiMoCodeIcon,
|
|
67
91
|
};
|
|
68
92
|
|
|
69
93
|
type ImportWizardDialogProps = {
|
|
@@ -213,7 +237,7 @@ export function ImportWizardDialog({
|
|
|
213
237
|
<DialogHeader>
|
|
214
238
|
<DialogTitle>Import from External Tools</DialogTitle>
|
|
215
239
|
<p className="text-xs text-muted-foreground">
|
|
216
|
-
Detect provider configurations from Claude Code and
|
|
240
|
+
Detect provider configurations from Claude Code, OpenCode, and MiMo Code.
|
|
217
241
|
</p>
|
|
218
242
|
</DialogHeader>
|
|
219
243
|
|
|
@@ -238,7 +262,7 @@ export function ImportWizardDialog({
|
|
|
238
262
|
<br />
|
|
239
263
|
<span className="text-xs">
|
|
240
264
|
Supported tools: Claude Code (~/.claude/settings.json), OpenCode
|
|
241
|
-
(~/.config/opencode/opencode.json)
|
|
265
|
+
(~/.config/opencode/opencode.json), MiMo Code (~/.config/mimocode/mimocode.jsonc)
|
|
242
266
|
</span>
|
|
243
267
|
</p>
|
|
244
268
|
)}
|
|
@@ -54,7 +54,7 @@ export const ConversationGroup = memo(function ({
|
|
|
54
54
|
standalone = false,
|
|
55
55
|
}: ConversationGroupProps): JSX.Element {
|
|
56
56
|
const [expanded, setExpanded] = useState(false);
|
|
57
|
-
const stats = computeStats(group.logs);
|
|
57
|
+
const stats = useMemo(() => computeStats(group.logs), [group.logs]);
|
|
58
58
|
const startTime = group.logs[0]?.timestamp ?? new Date().toISOString();
|
|
59
59
|
const endTime = group.logs[group.logs.length - 1]?.timestamp ?? new Date().toISOString();
|
|
60
60
|
const mixed = hasMixedApiFormat(group.logs);
|
|
@@ -70,7 +70,7 @@ export const ConversationGroup = memo(function ({
|
|
|
70
70
|
: group.conversationId;
|
|
71
71
|
|
|
72
72
|
return (
|
|
73
|
-
<div className="mb-
|
|
73
|
+
<div className="mb-2">
|
|
74
74
|
{!standalone && (
|
|
75
75
|
<ConversationHeader
|
|
76
76
|
conversationId={displayId}
|
|
@@ -90,7 +90,7 @@ export const ConversationGroup = memo(function ({
|
|
|
90
90
|
)}
|
|
91
91
|
|
|
92
92
|
{shouldRenderConversationContent(standalone, expanded) && (
|
|
93
|
-
<div>
|
|
93
|
+
<div className="max-h-[70vh] overflow-y-auto">
|
|
94
94
|
{turnGroups.map((tg) => (
|
|
95
95
|
<TurnGroup
|
|
96
96
|
key={tg.turnIndex}
|
|
@@ -82,7 +82,7 @@ export function ConversationHeader({
|
|
|
82
82
|
"flex items-center gap-3 px-3 py-2 cursor-pointer transition-colors",
|
|
83
83
|
"hover:bg-muted/50",
|
|
84
84
|
"select-none",
|
|
85
|
-
"border border-border rounded-lg mb-2 bg-
|
|
85
|
+
"border border-border rounded-lg mb-2 bg-background sticky top-0 z-10",
|
|
86
86
|
)}
|
|
87
87
|
onClick={onToggle}
|
|
88
88
|
onKeyDown={(e) => {
|