cc-viewer 1.2.5 → 1.2.8
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 +57 -77
- package/cli.js +14 -97
- package/dist/assets/{index-CSuPDSgh.js → index-CN78DISW.js} +121 -85
- package/dist/assets/index-DhZ8St4J.css +1 -0
- package/dist/favicon.ico +0 -0
- package/dist/index.html +4 -3
- package/findcc.js +107 -0
- package/i18n.js +2140 -6
- package/interceptor.js +18 -11
- package/package.json +3 -2
- package/proxy.js +2 -109
- package/server.js +16 -16
- package/dist/assets/index-BXa9W5oy.css +0 -1
- package/locales/i18n.json +0 -2134
package/interceptor.js
CHANGED
|
@@ -4,6 +4,7 @@ import { appendFileSync, mkdirSync, readdirSync, readFileSync, writeFileSync, st
|
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { dirname, join, basename } from 'node:path';
|
|
7
|
+
import { LOG_DIR } from './findcc.js';
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -30,7 +31,7 @@ function generateNewLogFilePath() {
|
|
|
30
31
|
let cwd;
|
|
31
32
|
try { cwd = process.cwd(); } catch { cwd = homedir(); }
|
|
32
33
|
const projectName = basename(cwd).replace(/[^a-zA-Z0-9_\-\.]/g, '_');
|
|
33
|
-
const dir = join(
|
|
34
|
+
const dir = join(LOG_DIR, projectName);
|
|
34
35
|
try { mkdirSync(dir, { recursive: true }); } catch { }
|
|
35
36
|
return { filePath: join(dir, `${projectName}_${ts}.jsonl`), dir, projectName };
|
|
36
37
|
}
|
|
@@ -119,14 +120,22 @@ const _initPromise = (async () => {
|
|
|
119
120
|
try {
|
|
120
121
|
const recentLog = findRecentLog(_logDir, _projectName);
|
|
121
122
|
if (recentLog) {
|
|
122
|
-
//
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
123
|
+
// Check if file is modified within 1 hour
|
|
124
|
+
const stats = statSync(recentLog);
|
|
125
|
+
const now = new Date();
|
|
126
|
+
const diff = now - stats.mtime;
|
|
127
|
+
const oneHour = 60 * 60 * 1000;
|
|
128
|
+
|
|
129
|
+
if (diff < oneHour) {
|
|
130
|
+
// 设置临时文件,不阻塞
|
|
131
|
+
const tempFile = _newLogFile.replace('.jsonl', '_temp.jsonl');
|
|
132
|
+
LOG_FILE = tempFile;
|
|
133
|
+
_resumeState = {
|
|
134
|
+
recentFile: recentLog,
|
|
135
|
+
recentFileName: basename(recentLog),
|
|
136
|
+
tempFile,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
130
139
|
}
|
|
131
140
|
} catch { }
|
|
132
141
|
})();
|
|
@@ -233,8 +242,6 @@ function isAnthropicApiPath(urlStr) {
|
|
|
233
242
|
}
|
|
234
243
|
}
|
|
235
244
|
|
|
236
|
-
// 不再需要折叠函数,保存完整 JSON 供前端渲染
|
|
237
|
-
|
|
238
245
|
// 组装流式消息为完整的 message 对象
|
|
239
246
|
function assembleStreamMessage(events) {
|
|
240
247
|
let message = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-viewer",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.8",
|
|
4
4
|
"description": "Claude Code Logger visualization management tool",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "server.js",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"proxy.js",
|
|
48
48
|
"interceptor.js",
|
|
49
49
|
"i18n.js",
|
|
50
|
+
"findcc.js",
|
|
50
51
|
"locales/",
|
|
51
52
|
"concepts/"
|
|
52
53
|
],
|
|
@@ -59,4 +60,4 @@
|
|
|
59
60
|
"vite": "^6.3.5",
|
|
60
61
|
"@vitejs/plugin-react": "^4.5.2"
|
|
61
62
|
}
|
|
62
|
-
}
|
|
63
|
+
}
|
package/proxy.js
CHANGED
|
@@ -34,11 +34,6 @@ function getOriginalBaseUrl() {
|
|
|
34
34
|
export function startProxy() {
|
|
35
35
|
return new Promise((resolve, reject) => {
|
|
36
36
|
const server = createServer(async (req, res) => {
|
|
37
|
-
// [Debug] Log incoming request
|
|
38
|
-
// console.error(`[CC-Viewer Proxy] Received request: ${req.method} ${req.url}`);
|
|
39
|
-
|
|
40
|
-
// Handle CORS preflight if needed (though claude cli probably doesn't send OPTIONS)
|
|
41
|
-
|
|
42
37
|
const originalBaseUrl = getOriginalBaseUrl();
|
|
43
38
|
const targetUrl = new URL(req.url, originalBaseUrl);
|
|
44
39
|
|
|
@@ -48,25 +43,12 @@ export function startProxy() {
|
|
|
48
43
|
const headers = { ...req.headers };
|
|
49
44
|
delete headers.host; // Let fetch set the host
|
|
50
45
|
|
|
51
|
-
// [Fix] Handle compressed body
|
|
52
|
-
// If content-encoding is set (e.g. gzip), and we read the raw stream into a buffer,
|
|
53
|
-
// we are passing the compressed buffer as body.
|
|
54
|
-
// fetch will automatically add content-length, but might not handle content-encoding correctly if we just pass the buffer?
|
|
55
|
-
// Actually, fetch should handle it fine if we pass the headers.
|
|
56
|
-
|
|
57
|
-
// However, if we are reading the body here to pass it to fetch, we are buffering it.
|
|
58
|
-
// For large uploads this might be bad, but for text prompts it's fine.
|
|
59
|
-
|
|
60
|
-
// We need to read the body if any
|
|
61
46
|
const buffers = [];
|
|
62
47
|
for await (const chunk of req) {
|
|
63
48
|
buffers.push(chunk);
|
|
64
49
|
}
|
|
65
50
|
const body = Buffer.concat(buffers);
|
|
66
51
|
|
|
67
|
-
// [Debug] Log body size
|
|
68
|
-
// console.error(`[CC-Viewer Proxy] Request body size: ${body.length}`);
|
|
69
|
-
|
|
70
52
|
const fetchOptions = {
|
|
71
53
|
method: req.method,
|
|
72
54
|
headers: headers,
|
|
@@ -80,94 +62,14 @@ export function startProxy() {
|
|
|
80
62
|
fetchOptions.body = body;
|
|
81
63
|
}
|
|
82
64
|
|
|
83
|
-
//
|
|
84
|
-
// If originalBaseUrl is also a proxy or special endpoint, make sure we construct the full URL correctly.
|
|
85
|
-
// originalBaseUrl might end with /v1 or not.
|
|
86
|
-
// req.url from proxy server is usually just the path (e.g. /v1/messages) if client is configured with base_url=http://localhost:port
|
|
87
|
-
// So new URL(req.url, originalBaseUrl) should work.
|
|
88
|
-
|
|
89
|
-
// However, if originalBaseUrl already contains a path (e.g. /api/anthropic), and req.url is /v1/messages,
|
|
90
|
-
// new URL(req.url, originalBaseUrl) might treat req.url as absolute path if it starts with /, replacing the path in originalBaseUrl?
|
|
91
|
-
// Let's test: new URL('/v1/messages', 'https://example.com/api').toString() -> 'https://example.com/v1/messages' (path replaced!)
|
|
92
|
-
|
|
93
|
-
// This is why we get 404! The user's base URL is https://antchat.alipay.com/api/anthropic
|
|
94
|
-
// But our proxy constructs: https://antchat.alipay.com/v1/messages
|
|
95
|
-
// It lost the /api/anthropic part!
|
|
96
|
-
|
|
97
|
-
// We need to append req.url to the pathname of originalBaseUrl, carefully avoiding double slashes.
|
|
98
|
-
|
|
99
|
-
const originalUrlObj = new URL(originalBaseUrl);
|
|
100
|
-
// Ensure original pathname doesn't end with slash if we append
|
|
101
|
-
let basePath = originalUrlObj.pathname;
|
|
102
|
-
if (basePath.endsWith('/')) basePath = basePath.slice(0, -1);
|
|
103
|
-
|
|
104
|
-
// req.url starts with / usually
|
|
105
|
-
const reqPath = req.url.startsWith('/') ? req.url : '/' + req.url;
|
|
106
|
-
|
|
107
|
-
// Check if we should join them
|
|
108
|
-
// If req.url is /v1/messages, and base is /api/anthropic, we want /api/anthropic/v1/messages
|
|
109
|
-
originalUrlObj.pathname = basePath + reqPath;
|
|
110
|
-
// Search params are already in req.url? Yes, req.url includes query string in Node http server.
|
|
111
|
-
// Wait, new URL(req.url, base) parses query string correctly.
|
|
112
|
-
// But if we manually concat pathname, we need to handle query string separately.
|
|
113
|
-
|
|
114
|
-
// Let's do it simpler: use string concatenation for the full URL
|
|
115
|
-
// But we need to handle the origin correctly.
|
|
116
|
-
|
|
117
|
-
// Better approach:
|
|
118
|
-
// 1. Remove trailing slash from originalBaseUrl
|
|
65
|
+
// 拼接完整 URL,保留 originalBaseUrl 中的路径前缀
|
|
119
66
|
const cleanBase = originalBaseUrl.endsWith('/') ? originalBaseUrl.slice(0, -1) : originalBaseUrl;
|
|
120
|
-
// 2. Remove leading slash from req.url
|
|
121
67
|
const cleanReq = req.url.startsWith('/') ? req.url.slice(1) : req.url;
|
|
122
|
-
// 3. Join
|
|
123
68
|
const fullUrl = `${cleanBase}/${cleanReq}`;
|
|
124
69
|
|
|
125
|
-
// [Debug] Proxying to
|
|
126
|
-
// console.error(`[CC-Viewer Proxy] Forwarding to: ${fullUrl}`);
|
|
127
|
-
|
|
128
70
|
const response = await fetch(fullUrl, fetchOptions);
|
|
129
71
|
|
|
130
|
-
//
|
|
131
|
-
// Handle decompression manually if needed.
|
|
132
|
-
// Node's fetch automatically decompresses if 'compress: true' is default?
|
|
133
|
-
// Actually, fetch handles gzip/deflate by default.
|
|
134
|
-
// But if we pipe the response body to the client response (res), we need to be careful.
|
|
135
|
-
// The issue is likely that we are trying to read the body as text/json for logging,
|
|
136
|
-
// but it might be compressed or binary.
|
|
137
|
-
|
|
138
|
-
// Let's modify how we handle the response body.
|
|
139
|
-
// We need to:
|
|
140
|
-
// 1. Pipe the response to the client (res) so Claude Code gets the data.
|
|
141
|
-
// 2. Clone the response to read it for logging? fetch response.clone() might not work with streaming body easily.
|
|
142
|
-
|
|
143
|
-
// Better approach: intercept the stream.
|
|
144
|
-
// Or simply: don't log the response body for now to avoid breaking the stream.
|
|
145
|
-
// User just wants to see the request in the viewer.
|
|
146
|
-
// If we want to log response, we need to handle it carefully.
|
|
147
|
-
|
|
148
|
-
// Let's check where ZlibError comes from. It likely comes from `response.text()` or `response.json()`
|
|
149
|
-
// if the content-encoding header is set but fetch didn't decompress it automatically?
|
|
150
|
-
// Or maybe we are double decompressing?
|
|
151
|
-
|
|
152
|
-
// Wait, if we use `response.body.pipe(res)`, that's fine.
|
|
153
|
-
// But do we read the body elsewhere?
|
|
154
|
-
|
|
155
|
-
// Let's look at how we log the response.
|
|
156
|
-
// We are not logging response body in this proxy.js currently.
|
|
157
|
-
// Wait, line 105: `response.body.pipe(res);`
|
|
158
|
-
|
|
159
|
-
// If the error happens, it might be because `fetch` failed to decompress?
|
|
160
|
-
// Or maybe `response.body` is already decompressed stream, but we are piping it to `res` which expects raw?
|
|
161
|
-
// No, `res` (http.ServerResponse) expects raw data.
|
|
162
|
-
|
|
163
|
-
// If `fetch` decompresses automatically, then `response.body` yields decompressed chunks.
|
|
164
|
-
// But `res` writes those chunks to the client.
|
|
165
|
-
// The client (Claude Code) expects compressed data if it sent `Accept-Encoding: gzip`.
|
|
166
|
-
// If we send decompressed data but keep `Content-Encoding: gzip` header, client will try to decompress again -> ZlibError!
|
|
167
|
-
|
|
168
|
-
// FIX: Remove content-encoding header from response headers before piping to client.
|
|
169
|
-
// This tells the client "the data I'm sending you is NOT compressed" (because fetch already decompressed it).
|
|
170
|
-
|
|
72
|
+
// fetch 自动解压,需移除编码相关 header 避免客户端重复解压
|
|
171
73
|
const responseHeaders = {};
|
|
172
74
|
for (const [key, value] of response.headers.entries()) {
|
|
173
75
|
// Skip Content-Encoding and Transfer-Encoding to let Node/Client handle it
|
|
@@ -178,20 +80,11 @@ export function startProxy() {
|
|
|
178
80
|
|
|
179
81
|
res.writeHead(response.status, responseHeaders);
|
|
180
82
|
|
|
181
|
-
// Also log that we are piping
|
|
182
|
-
// console.error(`[CC-Viewer Proxy] Response status: ${response.status}`);
|
|
183
|
-
|
|
184
83
|
if (response.body) {
|
|
185
|
-
// We need to convert Web Stream (response.body) to Node Stream for piping to res
|
|
186
|
-
// Node 18+ fetch returns a Web ReadableStream.
|
|
187
|
-
// We can use Readable.fromWeb(response.body)
|
|
188
84
|
const { Readable } = await import('node:stream');
|
|
189
85
|
// @ts-ignore
|
|
190
86
|
const nodeStream = Readable.fromWeb(response.body);
|
|
191
87
|
nodeStream.pipe(res);
|
|
192
|
-
|
|
193
|
-
// Optional: Log response body for debugging (careful with streams)
|
|
194
|
-
// For now, let's just ensure reliability.
|
|
195
88
|
} else {
|
|
196
89
|
res.end();
|
|
197
90
|
}
|
package/server.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
-
import { readFileSync, writeFileSync, existsSync, watchFile, unwatchFile, statSync, readdirSync, renameSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, watchFile, unwatchFile, statSync, readdirSync, renameSync, unlinkSync, openSync, readSync, closeSync } from 'node:fs';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { dirname, join, extname, basename } from 'node:path';
|
|
5
5
|
import { homedir, userInfo, platform } from 'node:os';
|
|
6
6
|
import { execSync } from 'node:child_process';
|
|
7
7
|
import { LOG_FILE, _initPromise, _resumeState, resolveResumeChoice, _projectName, _cachedApiKey, _cachedAuthHeader, _cachedHaikuModel } from './interceptor.js';
|
|
8
|
+
import { LOG_DIR } from './findcc.js';
|
|
8
9
|
import { t, detectLanguage } from './i18n.js';
|
|
9
10
|
|
|
10
|
-
const LOG_DIR = join(homedir(), '.claude', 'cc-viewer');
|
|
11
11
|
const PREFS_FILE = join(LOG_DIR, 'preferences.json');
|
|
12
12
|
|
|
13
13
|
|
|
@@ -25,14 +25,14 @@ function getUserProfile() {
|
|
|
25
25
|
const rn = execSync(`dscl . -read /Users/${name} RealName`, { encoding: 'utf-8', timeout: 3000 });
|
|
26
26
|
const match = rn.match(/RealName:\n?\s*(.+)/);
|
|
27
27
|
if (match && match[1].trim()) displayName = match[1].trim();
|
|
28
|
-
} catch {}
|
|
28
|
+
} catch { }
|
|
29
29
|
|
|
30
30
|
try {
|
|
31
31
|
const buf = execSync(`dscl . -read /Users/${name} JPEGPhoto | tail -1 | xxd -r -p`, { timeout: 5000, maxBuffer: 1024 * 1024 });
|
|
32
32
|
if (buf && buf.length > 100) {
|
|
33
33
|
avatarBase64 = `data:image/jpeg;base64,${buf.toString('base64')}`;
|
|
34
34
|
}
|
|
35
|
-
} catch {}
|
|
35
|
+
} catch { }
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
_userProfile = { name: displayName, avatar: avatarBase64 };
|
|
@@ -123,7 +123,7 @@ function watchLogFile(logFile) {
|
|
|
123
123
|
clients.forEach(client => {
|
|
124
124
|
try {
|
|
125
125
|
client.write(`event: full_reload\ndata: ${JSON.stringify(newEntries)}\n\n`);
|
|
126
|
-
} catch {}
|
|
126
|
+
} catch { }
|
|
127
127
|
});
|
|
128
128
|
watchLogFile(LOG_FILE);
|
|
129
129
|
}
|
|
@@ -154,7 +154,7 @@ function handleRequest(req, res) {
|
|
|
154
154
|
// User preferences API
|
|
155
155
|
if (url === '/api/preferences' && method === 'GET') {
|
|
156
156
|
let prefs = {};
|
|
157
|
-
try { if (existsSync(PREFS_FILE)) prefs = JSON.parse(readFileSync(PREFS_FILE, 'utf-8')); } catch {}
|
|
157
|
+
try { if (existsSync(PREFS_FILE)) prefs = JSON.parse(readFileSync(PREFS_FILE, 'utf-8')); } catch { }
|
|
158
158
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
159
159
|
res.end(JSON.stringify(prefs));
|
|
160
160
|
return;
|
|
@@ -167,7 +167,7 @@ function handleRequest(req, res) {
|
|
|
167
167
|
try {
|
|
168
168
|
const incoming = JSON.parse(body);
|
|
169
169
|
let prefs = {};
|
|
170
|
-
try { if (existsSync(PREFS_FILE)) prefs = JSON.parse(readFileSync(PREFS_FILE, 'utf-8')); } catch {}
|
|
170
|
+
try { if (existsSync(PREFS_FILE)) prefs = JSON.parse(readFileSync(PREFS_FILE, 'utf-8')); } catch { }
|
|
171
171
|
Object.assign(prefs, incoming);
|
|
172
172
|
writeFileSync(PREFS_FILE, JSON.stringify(prefs, null, 2));
|
|
173
173
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -228,14 +228,14 @@ function handleRequest(req, res) {
|
|
|
228
228
|
clients.forEach(client => {
|
|
229
229
|
try {
|
|
230
230
|
client.write(`event: resume_resolved\ndata: ${resolvedData}\n\n`);
|
|
231
|
-
} catch {}
|
|
231
|
+
} catch { }
|
|
232
232
|
});
|
|
233
233
|
// 发送 full_reload 让客户端重新加载数据
|
|
234
234
|
const entries = readLogFile();
|
|
235
235
|
clients.forEach(client => {
|
|
236
236
|
try {
|
|
237
237
|
client.write(`event: full_reload\ndata: ${JSON.stringify(entries)}\n\n`);
|
|
238
|
-
} catch {}
|
|
238
|
+
} catch { }
|
|
239
239
|
});
|
|
240
240
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
241
241
|
res.end(JSON.stringify({ ok: true, logFile: result.logFile }));
|
|
@@ -268,7 +268,7 @@ function handleRequest(req, res) {
|
|
|
268
268
|
const prefs = JSON.parse(readFileSync(PREFS_FILE, 'utf-8'));
|
|
269
269
|
if (prefs.lang) targetLang = prefs.lang;
|
|
270
270
|
}
|
|
271
|
-
} catch {}
|
|
271
|
+
} catch { }
|
|
272
272
|
if (!targetLang) targetLang = detectLanguage();
|
|
273
273
|
}
|
|
274
274
|
|
|
@@ -309,10 +309,10 @@ function handleRequest(req, res) {
|
|
|
309
309
|
body: JSON.stringify({
|
|
310
310
|
model: _cachedHaikuModel || 'claude-haiku-4-5-20251001',
|
|
311
311
|
max_tokens: 32000,
|
|
312
|
-
tools:[],
|
|
312
|
+
tools: [],
|
|
313
313
|
system: [{
|
|
314
|
-
type:"text",
|
|
315
|
-
text
|
|
314
|
+
type: "text",
|
|
315
|
+
text: `You are a translator. Translate the following text from ${from} to ${targetLang}. Output only the translated text, nothing else.`
|
|
316
316
|
}],
|
|
317
317
|
messages: [{ role: 'user', content: inputText }],
|
|
318
318
|
stream: false,
|
|
@@ -619,7 +619,7 @@ export async function startViewer() {
|
|
|
619
619
|
const cmd = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'start' : 'xdg-open';
|
|
620
620
|
execSync(`${cmd} ${url}`, { stdio: 'ignore', timeout: 5000 });
|
|
621
621
|
}
|
|
622
|
-
} catch {}
|
|
622
|
+
} catch { }
|
|
623
623
|
startWatching();
|
|
624
624
|
resolve(server);
|
|
625
625
|
});
|
|
@@ -646,7 +646,7 @@ export function stopViewer() {
|
|
|
646
646
|
const newPath = tempFile.replace('_temp.jsonl', '.jsonl');
|
|
647
647
|
renameSync(tempFile, newPath);
|
|
648
648
|
}
|
|
649
|
-
} catch {}
|
|
649
|
+
} catch { }
|
|
650
650
|
}
|
|
651
651
|
for (const logFile of watchedFiles.keys()) {
|
|
652
652
|
unwatchFile(logFile);
|
|
@@ -674,7 +674,7 @@ function handleExit() {
|
|
|
674
674
|
const newPath = _resumeState.tempFile.replace('_temp.jsonl', '.jsonl');
|
|
675
675
|
renameSync(_resumeState.tempFile, newPath);
|
|
676
676
|
}
|
|
677
|
-
} catch {}
|
|
677
|
+
} catch { }
|
|
678
678
|
}
|
|
679
679
|
}
|
|
680
680
|
process.on('exit', handleExit);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
body{margin:0;background-color:#0d0d0d}*{scrollbar-width:thin;scrollbar-color:#3a3a3a #0d0d0d}*::-webkit-scrollbar{width:6px;height:6px}*::-webkit-scrollbar-track{background:#0d0d0d}*::-webkit-scrollbar-thumb{background:#3a3a3a;border-radius:3px}*::-webkit-scrollbar-thumb:hover{background:#555}.diff-view{background:#1a1a2e;border:1px solid #2a2a3e;border-radius:8px;padding:8px 12px}.diff-line-del{background:#ef444426;color:#fca5a5;padding:0 4px}.diff-line-add{background:#22c55e26;color:#86efac;padding:0 4px}.code-highlight{color:#e6edf3}.hl-keyword{color:#ff7b72}.hl-string{color:#a5d6ff}.hl-comment{color:#8b949e;font-style:italic}.hl-number{color:#79c0ff}.hl-linenum{color:#484f58;-webkit-user-select:none;user-select:none}.chat-md pre{background:#0d1117;border:1px solid #2a2a2a;border-radius:6px;padding:12px;overflow-x:auto;font-size:13px;line-height:1.5}.chat-md code{background:#1a1a2e;padding:2px 6px;border-radius:4px;font-size:13px;color:#e5e7eb}.chat-md pre code{background:none;padding:0}.chat-md p{margin:6px 0}.chat-md ul,.chat-md ol{padding-left:20px;margin:6px 0}.chat-md li{margin:2px 0}.chat-md h1,.chat-md h2,.chat-md h3{margin:12px 0 6px;color:#fff}.chat-md h1{font-size:1.3em}.chat-md h2{font-size:1.15em}.chat-md h3{font-size:1.05em}.chat-md blockquote{border-left:3px solid #3b82f6;margin:8px 0;padding:4px 12px;color:#9ca3af}.chat-md table{border-collapse:collapse;margin:8px 0;font-size:13px}.chat-md th,.chat-md td{border:1px solid #2a2a2a;padding:6px 10px}.chat-md th{background:#1a1a1a;color:#fff}.chat-md a{color:#60a5fa}.chat-md hr{border:none;border-top:1px solid #2a2a2a;margin:12px 0}.chat-md strong{color:#f1f5f9}.chat-md em{color:#cbd5e1}._helpBtn_1gxlm_1{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:50%;background:#1a1a1a;border:1px solid #444;color:#aaa;font-size:11px;line-height:1;cursor:pointer;margin-left:4px;vertical-align:middle;transition:background .2s;-webkit-user-select:none;user-select:none}._helpBtn_1gxlm_1:hover{background:#333}._modalBody_1gxlm_24{max-height:60vh;overflow-y:auto;line-height:1.7}._modalBody_1gxlm_24 h1,._modalBody_1gxlm_24 h2,._modalBody_1gxlm_24 h3{margin-top:.8em}._modalBody_1gxlm_24 p{margin:.5em 0}._modalBody_1gxlm_24 code{background:#2a2a2a;padding:1px 4px;border-radius:3px;font-size:.9em}._modalBody_1gxlm_24 pre{background:#1a1a1a;padding:12px;border-radius:6px;overflow-x:auto}._spinWrap_1gxlm_54{display:flex;justify-content:center;padding:40px 0}._headerBar_1kqzq_2{display:flex;align-items:center;justify-content:space-between;width:100%;height:100%}._titleText_1kqzq_11{color:#fff;font-size:18px;cursor:pointer}._titleArrow_1kqzq_17{font-size:12px;margin-left:4px}._tokenStatsTag_1kqzq_23{border-radius:12px;cursor:pointer;background:#2a2a2a;border:1px solid #3a3a3a;color:#ccc}._tokenStatsIcon_1kqzq_31{margin-right:4px}._liveTag_1kqzq_36{border-radius:12px}._liveTag_1kqzq_36 .ant-badge-status-processing{animation:_breathe_1kqzq_1 2.5s ease-in-out infinite}@keyframes _breathe_1kqzq_1{0%,to{opacity:.4;transform:scale(.85)}50%{opacity:1;transform:scale(1.15)}}._liveTagHistory_1kqzq_49{background:#2a2a2a;border-color:#424242;color:#d1d5db}._liveTagText_1kqzq_55{margin-left:4px}._countdownStrong_1kqzq_60{font-variant-numeric:tabular-nums}._langSelector_1kqzq_65,._settingsBtn_1kqzq_76{color:#888;font-size:12px;cursor:pointer;-webkit-user-select:none;user-select:none;border:1px solid #555;border-radius:4px;padding:2px 8px}._settingsBtn_1kqzq_76:hover{color:#bbb;border-color:#777}._settingsItem_1kqzq_92{display:flex;justify-content:space-between;align-items:center;padding:12px 0}._settingsLabel_1kqzq_99{font-size:14px}._tokenStatsEmpty_1kqzq_104{padding:8px 4px;color:#999;font-size:13px}._tokenStatsContainer_1kqzq_111{display:flex;gap:12px;align-items:flex-start}._tokenStatsColumn_1kqzq_117{min-width:240px}._toolStatsColumn_1kqzq_121{min-width:180px}._modelCard_1kqzq_126{border:1px solid #333;border-radius:6px;padding:8px 10px;background:#111}._modelCardSpaced_1kqzq_133{margin-bottom:10px}._modelName_1kqzq_139{font-size:13px;font-weight:600;color:#e5e5e5;margin-bottom:8px;padding-bottom:4px;border-bottom:1px solid #333}._statsTable_1kqzq_149{width:100%;border-collapse:collapse}._th_1kqzq_154{padding:2px 12px;font-size:12px;font-family:monospace;white-space:nowrap;color:#888;font-weight:400;text-align:right}._td_1kqzq_164{padding:2px 12px;font-size:12px;font-family:monospace;white-space:nowrap;color:#e5e5e5;text-align:right}._label_1kqzq_173{padding:2px 12px;font-size:12px;font-family:monospace;white-space:nowrap;color:#aaa;font-weight:400;text-align:left}._rowBorder_1kqzq_183{border-bottom:1px solid #2a2a2a}._rebuildCard_1kqzq_187{border:1px solid #333;border-radius:6px;padding:8px 10px;margin-top:10px;background:#111}._rebuildTotalRow_1kqzq_195{border-top:1px solid #444}._rebuildTotalRow_1kqzq_195 td{font-weight:600}._promptExportBar_1kqzq_204{margin-bottom:12px}._promptScrollArea_1kqzq_208{max-height:500px;overflow:auto}._promptEmpty_1kqzq_213{color:#999;padding:12px}._promptTimestamp_1kqzq_219{color:#666;font-size:12px;margin:12px 0 4px;padding-bottom:6px}._textPromptCard_1kqzq_227{margin:4px 0;background:#141414;border-radius:6px;border:1px solid #303030;padding:10px 14px}._preText_1kqzq_236{white-space:pre-wrap;word-break:break-word;font-size:13px;line-height:1.6;color:#d9d9d9;margin:4px 0}._systemCollapse_1kqzq_246{margin:4px 0;background:#1a1a1a;border:1px solid #303030;border-radius:6px}._systemLabel_1kqzq_253{color:#888;font-size:12px}._preSys_1kqzq_258{white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.5;color:#999;margin:0}._promptTextarea_1kqzq_268{background:#000;width:100%;min-height:400px;color:#d9d9d9;font-family:monospace;font-size:13px;line-height:1.6;border:none;resize:vertical;padding:10px 14px;outline:none}._centerEmpty_ckz8l_1{display:flex;align-items:center;justify-content:center;height:100%}._scrollContainer_ckz8l_8{overflow:auto;height:100%}._listItem_ckz8l_13{cursor:pointer;padding:8px 12px;border-left:3px solid transparent;border-bottom:1px solid #1f1f1f;transition:background .15s}._listItem_ckz8l_13:hover{background:#151515}._listItemActive_ckz8l_25{background:#1a2332;border-left-color:#3b82f6}._listItemActive_ckz8l_25:hover{background:#1a2332}._itemContent_ckz8l_34{width:100%;min-width:0}._itemHeader_ckz8l_39{display:flex;align-items:center;gap:6px;margin-bottom:4px;font-size:12px}._tagNoMargin_ckz8l_47{margin:0;font-size:12px}._modelName_ckz8l_52{font-size:12px}._time_ckz8l_56{font-size:12px;color:#6b7280;margin-left:auto}._detailRow_ckz8l_62{display:flex;gap:8px;font-size:12px;align-items:center}._urlText_ckz8l_69{color:#555;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}._duration_ckz8l_78{color:#6b7280;flex-shrink:0}._statusOk_ckz8l_83{color:#52c41a;opacity:.5;flex-shrink:0}._statusErr_ckz8l_89{color:#ef4444;flex-shrink:0}._statusDefault_ckz8l_94{color:#9ca3af;flex-shrink:0}._usageBox_ckz8l_99{background:#111;border-radius:4px;padding:3px 6px;margin-top:4px;font-size:12px;color:#6b7280;line-height:1.6}._cacheDot_ckz8l_109{display:inline-block;width:6px;height:6px;border-radius:50%;margin:0 3px;vertical-align:middle}._cacheDotLoss_ckz8l_118{background-color:#8b1a1a;cursor:help}._cacheDotNormal_ckz8l_123{background-color:#3a3a3a}._GzYRV{line-height:1.2;white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word}._3eOF8{margin-right:5px;font-weight:700}._3eOF8+._3eOF8{margin-left:-5px}._1MFti{cursor:pointer}._f10Tu{font-size:1.2em;margin-right:5px;-webkit-user-select:none;-moz-user-select:none;user-select:none}._1UmXx:after{content:"▸"}._1LId0:after{content:"▾"}._1pNG9{margin-right:5px}._1pNG9:after{content:"...";font-size:.8em}._2IvMF{background:#eee}._2bkNM{margin:0;padding:0 10px}._1BXBN{margin:0;padding:0}._1MGIk{font-weight:600;margin-right:5px;color:#000}._3uHL6{color:#000}._2T6PJ,._1Gho6{color:#df113a}._vGjyY{color:#2a3f3c}._1bQdo{color:#0b75f5}._3zQKs{color:#469038}._1xvuR{color:#43413d}._oLqym,._2AXVT,._2KJWg{color:#000}._11RoI{background:#002b36}._17H2C,._3QHg2,._3fDAz{color:#fdf6e3}._2bSDX{font-weight:bolder;margin-right:5px;color:#fdf6e3}._gsbQL{color:#fdf6e3}._LaAZe,._GTKgm{color:#81b5ac}._Chy1W{color:#cb4b16}._2bveF{color:#d33682}._2vRm-{color:#ae81ff}._1prJR{color:#268bd2}._container_1h2lr_1{background:#0d1117;border-radius:6px;border:1px solid #2a2a2a;padding:12px;font-size:13px;font-family:monospace;overflow:auto}._container_y3z3z_1{height:100%;overflow:auto;padding:0 16px}._emptyState_y3z3z_7{display:flex;align-items:center;justify-content:center;height:100%}._urlSection_y3z3z_14{padding:12px 0;border-bottom:1px solid #1f1f1f;display:flex;align-items:flex-start}._urlLeft_y3z3z_21{flex:1;min-width:0}._tokenStatsBox_y3z3z_26{flex-shrink:0;padding-left:12px;display:flex;align-items:center}._tokenGrid_y3z3z_33{display:flex;border:1px solid #303030;border-radius:6px;overflow:hidden;min-width:360px;font-size:11px;line-height:1.6}._tokenRows_y3z3z_43{flex:1}._tokenRow_y3z3z_43{display:flex}._tokenRowBorder_y3z3z_51{border-top:1px solid #303030}._tokenLabel_y3z3z_55{color:#888;padding:4px 8px;white-space:nowrap;font-weight:600}._tokenTd_y3z3z_62{flex:1;color:#d1d5db;text-align:right;padding:4px 8px;font-family:monospace;white-space:nowrap}._tokenHitRate_y3z3z_71{display:flex;flex-direction:column;align-items:center;justify-content:center;color:#d1d5db;padding:4px 8px;font-family:monospace;white-space:nowrap;border-left:1px solid #303030;min-width:100px}._tokenHitRateLabel_y3z3z_84{color:#888;font-size:10px;font-family:sans-serif}._tokenRowBorder_y3z3z_51 td{border-top:1px solid #303030}._urlText_y3z3z_94{color:#d1d5db;font-size:13px;margin-bottom:8px;word-break:break-all}._metaText_y3z3z_101,._headersContainer_y3z3z_105{font-size:12px}._headerRow_y3z3z_109{display:flex;padding:4px 0;border-bottom:1px solid #1f1f1f}._headerKey_y3z3z_115{min-width:200px;flex-shrink:0}._headerValue_y3z3z_120{word-break:break-all;margin-left:8px}._streamingBox_y3z3z_125{padding:20px;background:#1a1a1a;border-radius:6px;border:1px solid #2a2a2a}._bodyToolbar_y3z3z_132{display:flex;gap:8px;margin-bottom:8px}._rawTextPre_y3z3z_138{background:#0d1117;border:1px solid #2a2a2a;border-radius:6px;padding:12px;font-size:12px;color:#e5e7eb;overflow:auto;max-height:600px;white-space:pre-wrap;word-break:break-all}._tabContent_y3z3z_151{padding:12px 0}._collapseSpacing_y3z3z_155{margin-bottom:16px}._bodyLabel_y3z3z_159{margin:0}._bodyHeader_y3z3z_163{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}._diffSection_y3z3z_170{margin-bottom:16px}._diffToggle_y3z3z_174{display:inline-block;margin-bottom:8px;cursor:pointer}._diffIcon_y3z3z_180{font-size:12px;margin-left:4px}._viewInChatBtn_y3z3z_185{display:inline-flex;align-items:center;height:26px;border-radius:13px;border:1px solid #424242;background:transparent;color:#888;cursor:pointer;font-size:12px;transition:all .2s;padding:0 10px;white-space:nowrap}._viewInChatBtn_y3z3z_185:hover{border-color:#666;color:#d1d5db;background:#ffffff0f}._reminderSelect_y3z3z_206{min-width:140px;font-size:12px}._reminderSelect_y3z3z_206 .ant-select-selector{border-radius:2px!important;border-color:#424242!important;background:transparent!important;min-height:26px!important;height:auto!important;padding:0 8px!important;font-family:monospace}._reminderSelect_y3z3z_206 .ant-select-selection-placeholder{font-size:11px}._wrapper_1o255_1{margin:6px 0}._header_1o255_5{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}._filePath_1o255_12{color:#a78bfa;font-size:12px;font-weight:600}._toggle_1o255_18{color:#6b7280;font-size:11px;cursor:pointer;-webkit-user-select:none;user-select:none}._code_1o255_25{margin:0;font-size:12px;line-height:1.5;overflow:auto}._plainResult_1uh60_1{background:#111;border-radius:6px;font-size:12px}._plainTitle_1uh60_7{font-size:11px}._plainPre_1uh60_11{color:#d1d5db;margin:0;white-space:pre-wrap;word-break:break-all;font-size:12px;padding:8px 12px}._codeResult_1uh60_20{background:#0d1117;border-radius:6px;border:1px solid #1e2a3a;overflow:hidden}._codeHeader_1uh60_27{display:flex;justify-content:space-between;align-items:center;padding:4px 10px;background:#161b22;border-bottom:1px solid #1e2a3a}._codeTitle_1uh60_36{font-size:11px;color:#8b949e}._codeToggle_1uh60_41{font-size:11px;color:#484f58;cursor:pointer;-webkit-user-select:none;user-select:none}._codePre_1uh60_48{margin:0;padding:8px 12px;font-size:12px;line-height:1.5;overflow:auto;max-height:500px}._markdownBody_1uh60_57{padding:8px 12px;font-size:13px;line-height:1.6;overflow:auto;max-height:500px}._tag_17wfp_1{display:inline-block;font-size:10px;color:#6b7280;background:#ffffff0f;border:1px solid rgba(255,255,255,.1);border-radius:3px;padding:0 4px;margin-left:6px;cursor:pointer;line-height:18px;vertical-align:middle;transition:color .2s,border-color .2s;-webkit-user-select:none;user-select:none}._tag_17wfp_1:hover{color:#93c5fd;border-color:#93c5fd4d}._tagActive_17wfp_22{color:#93c5fd;border-color:#93c5fd40}._tagLoading_17wfp_27{cursor:wait;opacity:.7}._spinner_17wfp_32{display:inline-block;width:10px;height:10px;border:1.5px solid rgba(147,197,253,.3);border-top-color:#93c5fd;border-radius:50%;animation:_spin_17wfp_32 .6s linear infinite;margin-right:3px;vertical-align:middle}@keyframes _spin_17wfp_32{to{transform:rotate(360deg)}}._avatar_1od17_3{width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0}._avatarImg_1od17_13{width:32px;height:32px;border-radius:50%;flex-shrink:0;object-fit:cover}._bubble_1od17_21{border-radius:8px;padding:10px 14px;max-width:100%;font-size:14px;line-height:1.6;word-break:break-word}._messageRow_1od17_32{display:flex;gap:10px;padding:8px 0}._messageRowEnd_1od17_38{display:flex;gap:10px;padding:8px 0;justify-content:flex-end}._contentCol_1od17_47{min-width:0;flex:1}._contentColLimited_1od17_52{min-width:0;max-width:80%;width:fit-content}._labelRow_1od17_60{display:flex;justify-content:space-between;align-items:center;margin-bottom:2px}._labelRight_1od17_67{display:flex;align-items:center;gap:4px;flex-shrink:0;margin-left:auto}._labelText_1od17_75{font-size:11px}._timeText_1od17_79,._timeTextNoMargin_1od17_85{font-size:10px;color:#6b7280;flex-shrink:0}._labelTextRight_1od17_91{font-size:11px;margin-left:auto}._bubbleUser_1od17_98{background:#1668dc;color:#e5e7eb}._bubbleAssistant_1od17_104{background:#141414;color:#e5e7eb;transition:box-shadow 0s;position:relative}._bubbleHighlight_1od17_112{box-shadow:0 0 10px #1668dc99}._bubbleHighlightFading_1od17_116{box-shadow:0 0 10px #1668dc00;transition:box-shadow 5s ease-out}._borderSvg_1od17_121{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;pointer-events:none;overflow:visible}._borderSvgFading_1od17_130{opacity:0;transition:opacity 5s ease-out}._borderRect_1od17_135{animation:_dashRotate_1od17_1 4s linear infinite}@keyframes _dashRotate_1od17_1{0%{stroke-dashoffset:0}to{stroke-dashoffset:-100}}._bubblePlan_1od17_144{background:#141428;border:5px solid #1668dc;font-size:12px;color:#e5e7eb}._bubbleSubAgent_1od17_152{background:#1a1a2e;color:#e5e7eb}._bubbleSelection_1od17_158{background:#1a3a1a;color:#e5e7eb}._systemTagLabel_1od17_166{font-size:12px}._skillLabel_1od17_170{font-size:12px;color:#a78bfa}._systemTagPre_1od17_175{font-size:12px;color:#9ca3af;white-space:pre-wrap;word-break:break-all;margin:0}._collapseMargin_1od17_183{margin:4px 0}._collapseNoMargin_1od17_187{margin:0}._thinkingLabel_1od17_193{font-size:12px}._toolBox_1od17_199{background:#1a1a2e;border:1px solid #2a2a3e;border-radius:8px;padding:8px 12px;margin:6px 0;font-size:12px}._toolLabel_1od17_208{color:#a78bfa}._codePre_1od17_214{font-size:12px;margin:4px 0 0;white-space:pre-wrap;word-break:break-all;background:#0d1117;border-radius:4px;padding:6px 8px}._pathTag_1od17_226{color:#7dd3fc;font-size:11px}._descSpan_1od17_233{color:#6b7280;font-weight:400}._secondarySpan_1od17_238{color:#6b7280}._patternSpan_1od17_242{color:#fbbf24}._kvContainer_1od17_248{margin-top:4px;font-size:11px}._kvItem_1od17_253{margin:2px 0}._kvKey_1od17_257{color:#7dd3fc}._kvValue_1od17_261{color:#9ca3af}._toolResult_1od17_267{background:#111827;border:1px solid #1e293b;border-radius:6px;padding:6px 10px;margin:2px 0 6px;font-size:11px}._toolResultLabel_1od17_276{font-size:11px}._compactLabel_1od17_282{font-size:12px;color:#93c5fd}._compactPre_1od17_287{font-size:12px;color:#d1d5db;white-space:pre-wrap;word-break:break-all;margin:0}._viewRequestBtn_1od17_297{font-size:10px;color:#555;cursor:pointer;margin-left:6px;flex-shrink:0}._viewRequestBtn_1od17_297:hover{color:#93c5fd}._optionList_1od17_311{padding-left:8px}._optionDesc_1od17_315{color:#555;margin-left:6px;font-weight:400}._questionText_1od17_323{font-size:13px;color:#ccc;margin-bottom:4px}._questionSpacing_1od17_329{margin-bottom:10px}._option_1od17_311{font-size:12px;padding:1px 0}._centerEmpty_1j3v3_1{display:flex;align-items:center;justify-content:center;height:100%}._container_1j3v3_8{height:100%;overflow:auto;padding:16px 24px;display:flex;flex-direction:column}._sessionDividerText_1j3v3_16{font-size:11px;color:#555}._lastResponseLabel_1j3v3_21{font-size:11px}._resizer_yamj2_1{width:6px;cursor:col-resize;background:#1f1f1f;flex-shrink:0;transition:background .2s}._resizer_yamj2_1:hover{background:#3b82f6}._layout_12l0f_1{height:100vh;overflow:hidden}._header_12l0f_6{background:#111;border-bottom:1px solid #1f1f1f;padding:0 24px;height:60px;line-height:60px}._content_12l0f_14{flex:1;overflow:hidden}._mainContainer_12l0f_19{display:flex;height:100%}._leftPanel_12l0f_24{flex-shrink:0;border-right:1px solid #1f1f1f;display:flex;flex-direction:column;background:#0a0a0a}._leftPanelHeader_12l0f_32{padding:10px 16px;border-bottom:1px solid #1f1f1f;font-size:13px;color:#9ca3af;font-weight:500;display:flex;justify-content:space-between;align-items:center}._leftPanelCount_12l0f_43{font-size:12px;color:#555;font-weight:400}._leftPanelBody_12l0f_49{flex:1;overflow:hidden}._rightPanel_12l0f_54{flex:1;overflow:hidden;background:#0d0d0d}._modalActions_12l0f_60{margin-bottom:12px}._spinCenter_12l0f_64{text-align:center;padding:40px}._emptyCenter_12l0f_69{text-align:center;color:#999;padding:40px}._logCheckbox_12l0f_75{margin-right:8px}._logListItem_12l0f_79{cursor:pointer;padding:8px 12px}._logItemRow_12l0f_84{display:flex;align-items:center;width:100%;justify-content:space-between}._logFileIcon_12l0f_91{margin-right:8px;color:#3b82f6}._folderIcon_12l0f_96{margin-right:8px}._logTag_12l0f_100{margin-left:8px}._loadingOverlay_12l0f_104{position:fixed;top:0;left:0;width:100vw;height:100vh;background:#000000bf;display:flex;align-items:center;justify-content:center;z-index:9999}._loadingText_12l0f_117{color:#fff;font-size:16px;font-weight:700}._footer_12l0f_123{height:18px;background:#000;display:flex;align-items:center;justify-content:flex-end;padding:0 12px;flex-shrink:0}._footerRight_12l0f_133{display:flex;align-items:center;gap:6px;font-size:11px;color:#555}._footerLink_12l0f_141{display:inline-flex;align-items:center;gap:3px;color:#555;text-decoration:none}._footerLink_12l0f_141:hover{color:#888}._footerIcon_12l0f_153{width:12px;height:12px}._footerDivider_12l0f_158{color:#333}._footerText_12l0f_162{color:#555}
|