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/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(homedir(), '.claude', 'cc-viewer', projectName);
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 tempFile = _newLogFile.replace('.jsonl', '_temp.jsonl');
124
- LOG_FILE = tempFile;
125
- _resumeState = {
126
- recentFile: recentLog,
127
- recentFileName: basename(recentLog),
128
- tempFile,
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.5",
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
- // [Crucial Fix]
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
- // [Crucial Fix]
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:`You are a translator. Translate the following text from ${from} to ${targetLang}. Output only the translated text, nothing else.`
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}