cc-viewer 1.6.334 → 1.6.335
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/dist/assets/App-CEJXKHHN.css +1 -0
- package/dist/assets/App-CuBCEhei.js +2 -0
- package/dist/assets/{MdxEditorPanel-B6n0VHxw.js → MdxEditorPanel-BrnxiPH_.js} +1 -1
- package/dist/assets/{MdxEditorPanel-1vSx6ymU.css → MdxEditorPanel-Dpn-4ydO.css} +1 -1
- package/dist/assets/Mobile-B5V_-6Hc.css +1 -0
- package/dist/assets/{Mobile-DaFxkSMs.js → Mobile-RbK1_88q.js} +1 -1
- package/dist/assets/{index-DrsJoCe8.js → index-Dy14VaBK.js} +2 -2
- package/dist/assets/index-f4nPU7KM.css +1 -0
- package/dist/assets/seqResourceLoaders-BDbYWuli.js +2 -0
- package/dist/assets/seqResourceLoaders-DG5M9M0e.css +41 -0
- package/dist/index.html +4 -3
- package/package.json +1 -1
- package/server/lib/ask-bridge.js +45 -0
- package/server/lib/ask-constants.js +8 -0
- package/server/lib/ask-reaper.js +164 -0
- package/server/lib/auth.js +2 -1
- package/server/routes/ask-perm.js +84 -1
- package/server/server.js +71 -2
- package/dist/assets/App-CM76jJTq.css +0 -1
- package/dist/assets/App-DKxE-x5Q.js +0 -2
- package/dist/assets/Mobile-DolwHzoQ.css +0 -1
- package/dist/assets/index-Cy7Xfu81.css +0 -1
- package/dist/assets/seqResourceLoaders-CnYQz2LG.js +0 -2
- package/dist/assets/seqResourceLoaders-P_ytfji6.css +0 -41
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Waiter-liveness reaper for short-poll AskUserQuestion entries.
|
|
2
|
+
//
|
|
3
|
+
// Problem: when the user declines an AskUserQuestion at the Claude Code CLI, the
|
|
4
|
+
// ask-bridge hook subprocess is SIGTERMed. Its short-poll GET loop simply stops;
|
|
5
|
+
// the GET close handler in ask-perm.js only unregisters the poll listener (a socket
|
|
6
|
+
// close is indistinguishable from the normal ~25s re-poll cycle), so nothing ever
|
|
7
|
+
// resolves the entry. It then stays `pending` in memory (24h timer) and on disk,
|
|
8
|
+
// and the WS reconnect replay keeps popping a dead "Question" modal.
|
|
9
|
+
//
|
|
10
|
+
// Fix: track when the waiter last proved it was alive (POST create / GET poll /
|
|
11
|
+
// hanging listener / open POST socket) and cancel entries whose waiter has gone
|
|
12
|
+
// silent for ASK_WAITER_LIVENESS_MS. This is NOT an ask timeout — the
|
|
13
|
+
// "GUI effectively no-timeout" contract holds; a connected bridge keeps its ask
|
|
14
|
+
// alive indefinitely. A falsely-reaped bridge degrades gracefully: its next GET
|
|
15
|
+
// hits consumeIfFinal and receives a definitive `{cancelled}` (never a 404 loop).
|
|
16
|
+
//
|
|
17
|
+
// Cross-instance safety: the ask-store file is SHARED by every cc-viewer instance
|
|
18
|
+
// (global ~/.claude/cc-viewer/). reapDeadAskWaiters therefore touches ONLY entries
|
|
19
|
+
// this instance owns in its pendingAskHooks Map (memory = ownership). Disk-only
|
|
20
|
+
// orphans are handled by sweepOrphanedDiskAsks, a one-shot boot sweep that runs
|
|
21
|
+
// only when no other cc-viewer instance is listening on the CCV port range.
|
|
22
|
+
import { ASK_WAITER_LIVENESS_MS, ASK_WAITER_REAP_INTERVAL_MS } from './ask-constants.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Broadcast a cancel to browsers + short-poll listeners + parent process.
|
|
26
|
+
* Shared by the periodic reaper and the boot sweep.
|
|
27
|
+
*/
|
|
28
|
+
async function cancelAndBroadcast(deps, id, reason) {
|
|
29
|
+
const wrote = await deps.markCancelled(id, reason);
|
|
30
|
+
if (!wrote) return false; // an answer/cancel landed first — never clobber, never broadcast
|
|
31
|
+
try { deps.notifyShortPollCancel?.(id, reason); } catch {}
|
|
32
|
+
try { deps.broadcastCancelled?.(id, reason); } catch {}
|
|
33
|
+
try { deps.notifyParentPending?.({ type: 'ask-hook-cancelled', id }); } catch {}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Periodic sweep over MEMORY-OWNED short-poll entries whose waiter went silent.
|
|
39
|
+
*
|
|
40
|
+
* deps: {
|
|
41
|
+
* pendingAskHooks: Map<id, {questions,res,timer,createdAt,shortPoll}>,
|
|
42
|
+
* shortPollListeners: Map<id, Set<{finished}>>,
|
|
43
|
+
* askWaiterLastPoll: Map<id, ms>, // written by POST create + GET poll
|
|
44
|
+
* markCancelled(id, reason) -> Promise<boolean>,
|
|
45
|
+
* loadAskStore() -> {id: entry},
|
|
46
|
+
* notifyShortPollCancel?(id, reason), broadcastCancelled?(id, reason),
|
|
47
|
+
* notifyParentPending?(msg),
|
|
48
|
+
* livenessMs?, reapIntervalMs?,
|
|
49
|
+
* }
|
|
50
|
+
* state: caller-held mutable { lastSweepAt } so the wake guard survives across calls.
|
|
51
|
+
*/
|
|
52
|
+
export async function reapDeadAskWaiters(deps, state, { now = Date.now() } = {}) {
|
|
53
|
+
const livenessMs = deps.livenessMs ?? ASK_WAITER_LIVENESS_MS;
|
|
54
|
+
const intervalMs = deps.reapIntervalMs ?? ASK_WAITER_REAP_INTERVAL_MS;
|
|
55
|
+
const lastPoll = deps.askWaiterLastPoll;
|
|
56
|
+
|
|
57
|
+
const candidates = [];
|
|
58
|
+
for (const [id, entry] of deps.pendingAskHooks) {
|
|
59
|
+
if (entry && entry.shortPoll === true) candidates.push([id, entry]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Wake guard: after a laptop sleep / long event-loop stall the (same-machine)
|
|
63
|
+
// bridge slept too and hasn't had a chance to re-poll. Re-baseline instead of
|
|
64
|
+
// reaping on the first sweep after resume.
|
|
65
|
+
const prevSweepAt = state.lastSweepAt ?? now;
|
|
66
|
+
state.lastSweepAt = now;
|
|
67
|
+
if (now - prevSweepAt > 3 * intervalMs) {
|
|
68
|
+
for (const [id] of candidates) lastPoll.set(id, now);
|
|
69
|
+
return { reaped: [], rebaselined: true };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const reaped = [];
|
|
73
|
+
for (const [id, entry] of candidates) {
|
|
74
|
+
// Open POST socket = waiter provably alive (covers the plugin onAskRequest
|
|
75
|
+
// window, where the short-poll ack has not been written yet and no GETs flow).
|
|
76
|
+
if (entry.res && !entry.res.writableEnded && !entry.res.destroyed) {
|
|
77
|
+
lastPoll.set(id, now);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
// A hanging unfinished GET listener = waiter connected right now
|
|
81
|
+
// (covers legitimate wait=60000 pollers beyond the liveness window).
|
|
82
|
+
const listeners = deps.shortPollListeners.get(id);
|
|
83
|
+
let hasLive = false;
|
|
84
|
+
if (listeners) {
|
|
85
|
+
for (const l of listeners) { if (l && !l.finished) { hasLive = true; break; } }
|
|
86
|
+
}
|
|
87
|
+
if (hasLive) {
|
|
88
|
+
lastPoll.set(id, now);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const last = lastPoll.get(id) ?? entry.createdAt ?? now;
|
|
92
|
+
if (now - last < livenessMs) continue;
|
|
93
|
+
|
|
94
|
+
// Waiter is gone. Clean memory unconditionally; write/broadcast the cancel
|
|
95
|
+
// only when the shared disk row still exists as ours to resolve (an absent
|
|
96
|
+
// row means another instance's answer consumed it — do not fabricate a
|
|
97
|
+
// cancelled placeholder or grey out a real answer).
|
|
98
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
99
|
+
deps.pendingAskHooks.delete(id);
|
|
100
|
+
lastPoll.delete(id);
|
|
101
|
+
const diskRow = deps.loadAskStore()[id];
|
|
102
|
+
if (diskRow) {
|
|
103
|
+
await cancelAndBroadcast(deps, id, 'hook waiter lost');
|
|
104
|
+
}
|
|
105
|
+
reaped.push(id);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// GC liveness records for ids no longer pending in this instance.
|
|
109
|
+
for (const id of lastPoll.keys()) {
|
|
110
|
+
if (!deps.pendingAskHooks.has(id)) lastPoll.delete(id);
|
|
111
|
+
}
|
|
112
|
+
return { reaped, rebaselined: false };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Parse `lsof -Fp` output into pids, excluding our own. Strict `^p\d+$` lines only
|
|
117
|
+
* (mirrors isAnyCcvBusy in updater.js).
|
|
118
|
+
*/
|
|
119
|
+
function otherCcvPids(lsofOut, ownPid) {
|
|
120
|
+
return String(lsofOut).replace(/\r/g, '').split('\n')
|
|
121
|
+
.filter((l) => /^p\d+$/.test(l))
|
|
122
|
+
.map((l) => Number(l.slice(1)))
|
|
123
|
+
.filter((n) => Number.isFinite(n) && n > 0 && n !== ownPid);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* One-shot boot sweep: cancel disk-only pending entries orphaned by a previous
|
|
128
|
+
* server process. Runs ONLY when this is provably the sole cc-viewer instance
|
|
129
|
+
* (lsof scan of the CCV port range); on any doubt (another pid listening, lsof
|
|
130
|
+
* missing/failing, win32) it skips entirely — another instance may own those
|
|
131
|
+
* entries, and its own reaper / the fallback UI handles them.
|
|
132
|
+
*
|
|
133
|
+
* deps: reapDeadAskWaiters deps plus {
|
|
134
|
+
* bootTime: ms, ownPid: number, portRange: [start, end],
|
|
135
|
+
* lsofImpl?(cmd) -> string, // injectable for tests; default caller-provided
|
|
136
|
+
* platform?: string, // default process.platform
|
|
137
|
+
* }
|
|
138
|
+
*/
|
|
139
|
+
export async function sweepOrphanedDiskAsks(deps, { now = Date.now() } = {}) {
|
|
140
|
+
const plat = deps.platform ?? process.platform;
|
|
141
|
+
if (plat === 'win32' || typeof deps.lsofImpl !== 'function') return { swept: [], skipped: true };
|
|
142
|
+
const [start, end] = deps.portRange;
|
|
143
|
+
let out;
|
|
144
|
+
try {
|
|
145
|
+
// lsofImpl may be sync or async (production uses async exec to keep the
|
|
146
|
+
// event loop free); await handles both.
|
|
147
|
+
out = await deps.lsofImpl(`lsof -iTCP:${start}-${end} -sTCP:LISTEN -P -n -Fp`);
|
|
148
|
+
} catch {
|
|
149
|
+
return { swept: [], skipped: true }; // conservative: cannot prove we are alone
|
|
150
|
+
}
|
|
151
|
+
if (otherCcvPids(out, deps.ownPid).length > 0) return { swept: [], skipped: true };
|
|
152
|
+
|
|
153
|
+
const swept = [];
|
|
154
|
+
const all = deps.loadAskStore();
|
|
155
|
+
for (const e of Object.values(all)) {
|
|
156
|
+
if (!e || e.status !== 'pending') continue;
|
|
157
|
+
if (!Array.isArray(e.questions) || e.questions.length === 0) continue;
|
|
158
|
+
if (typeof e.createdAt === 'number' && e.createdAt >= deps.bootTime) continue; // ours / fresh
|
|
159
|
+
if (deps.pendingAskHooks.has(e.id)) continue; // owned by this instance
|
|
160
|
+
const wrote = await cancelAndBroadcast(deps, e.id, 'orphaned pending ask (owner instance gone)');
|
|
161
|
+
if (wrote) swept.push(e.id);
|
|
162
|
+
}
|
|
163
|
+
return { swept, skipped: false };
|
|
164
|
+
}
|
package/server/lib/auth.js
CHANGED
|
@@ -287,7 +287,8 @@ export function renderLoginPage({ lang = 'en', error = false } = {}) {
|
|
|
287
287
|
body {
|
|
288
288
|
margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center;
|
|
289
289
|
background: #1a1a1a; color: #e8e8e8;
|
|
290
|
-
|
|
290
|
+
/* Standalone document (no SPA CSS bundle): keep in sync with --font-ui in src/global.css */
|
|
291
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
291
292
|
}
|
|
292
293
|
.card {
|
|
293
294
|
width: 100%; max-width: 340px; padding: 32px 28px; margin: 16px;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Ask / permission hook bridge routes (moved verbatim from server.js handleRequest).
|
|
2
|
-
import { loadAskStore, consumeIfFinal as askStoreConsumeIfFinal } from '../lib/ask-store.js';
|
|
2
|
+
import { loadAskStore, consumeIfFinal as askStoreConsumeIfFinal, markCancelled as askStoreMarkCancelled } from '../lib/ask-store.js';
|
|
3
3
|
import { runWaterfallHook, runParallelHook } from '../lib/plugin-loader.js';
|
|
4
4
|
import { sendEventToClients } from '../lib/log-watcher.js';
|
|
5
5
|
|
|
@@ -128,6 +128,9 @@ function askHook(req, res, parsedUrl, isLocal, deps) {
|
|
|
128
128
|
const _placeholderEntry = { questions, res, timer: null, createdAt: Date.now(), shortPoll: shortPollMode };
|
|
129
129
|
deps.pendingAskHooks.set(id, _placeholderEntry);
|
|
130
130
|
deps.persistAskEntry(id, _placeholderEntry);
|
|
131
|
+
// Seed waiter liveness for the reaper (ask-reaper.js). Optional chaining:
|
|
132
|
+
// older deps stubs and third-party callers may not provide the map.
|
|
133
|
+
if (shortPollMode) deps.askWaiterLastPoll?.set(id, Date.now());
|
|
131
134
|
|
|
132
135
|
// res.on('close') 提前注册:handler 用 entry.timer 守卫(占位期 timer:null → 仅 delete Map)
|
|
133
136
|
// Phase 3: short-poll 模式下 res 主动 end,close 不视为 client cancel;entry 由 24h timer 兜底清理。
|
|
@@ -237,6 +240,10 @@ async function askHookResult(req, res, parsedUrl, isLocal, deps) {
|
|
|
237
240
|
}
|
|
238
241
|
const qs = new URLSearchParams(m[2] || '');
|
|
239
242
|
const wait = Math.max(1000, Math.min(60000, parseInt(qs.get('wait') || '30000', 10)));
|
|
243
|
+
// Refresh waiter liveness on every poll arrival (see ask-reaper.js). Unconditional:
|
|
244
|
+
// even a poll that ends in 404/final proves the waiter process is alive; the reaper
|
|
245
|
+
// GCs records for ids that are no longer pending.
|
|
246
|
+
deps.askWaiterLastPoll?.set(id, Date.now());
|
|
240
247
|
|
|
241
248
|
// 1) disk 命中 answered/cancelled:浏览器答得早过 GET 到达 → 立即返并消费(一次性)
|
|
242
249
|
// 用 consumeIfFinal 单次 withLock 内判 status 决定是否 delete —— 旧设计的
|
|
@@ -289,6 +296,81 @@ async function askHookResult(req, res, parsedUrl, isLocal, deps) {
|
|
|
289
296
|
}
|
|
290
297
|
}
|
|
291
298
|
|
|
299
|
+
// POST /api/ask-hook/:id/cancel — best-effort cancel from a dying hook process
|
|
300
|
+
// (ask-bridge SIGTERM/SIGINT handler) or any client that wants to durably resolve
|
|
301
|
+
// a pending ask. Mirrors the WS ask-cancel handler in server.js for the hook path.
|
|
302
|
+
// Auth: standard request prelude (loopback-exempt), same as the sibling ask-hook routes.
|
|
303
|
+
function askHookCancel(req, res, parsedUrl, isLocal, deps) {
|
|
304
|
+
const m = parsedUrl.pathname.match(/^\/api\/ask-hook\/([^/?]+)\/cancel$/);
|
|
305
|
+
const id = m ? decodeURIComponent(m[1]) : '';
|
|
306
|
+
if (!id || id.length > 256 || !/^[a-zA-Z0-9_-]+$/.test(id)) {
|
|
307
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
308
|
+
res.end(JSON.stringify({ error: 'invalid id' }));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
let body = '';
|
|
312
|
+
let bodyTooLarge = false;
|
|
313
|
+
req.on('data', (chunk) => {
|
|
314
|
+
if (bodyTooLarge) return;
|
|
315
|
+
body += chunk;
|
|
316
|
+
if (body.length > 4096) {
|
|
317
|
+
// Respond here, not in 'end': req.destroy() never emits 'end' in real Node,
|
|
318
|
+
// so a deferred 413 would be dead code (mirrors permHook's in-data rejection).
|
|
319
|
+
bodyTooLarge = true;
|
|
320
|
+
try { if (!res.headersSent) { res.writeHead(413, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Request body too large' })); } } catch {}
|
|
321
|
+
req.destroy();
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
req.on('end', async () => {
|
|
325
|
+
if (bodyTooLarge) return;
|
|
326
|
+
let reason = 'hook process exited';
|
|
327
|
+
try {
|
|
328
|
+
const parsed = body ? JSON.parse(body) : {};
|
|
329
|
+
if (typeof parsed.reason === 'string' && parsed.reason) reason = parsed.reason.slice(0, 500);
|
|
330
|
+
} catch {}
|
|
331
|
+
// Respond fast: the caller may be a process that is about to die.
|
|
332
|
+
try { if (!res.headersSent) { res.writeHead(204); res.end(); } } catch {}
|
|
333
|
+
try {
|
|
334
|
+
let handled = false;
|
|
335
|
+
const entry = deps.pendingAskHooks.get(id);
|
|
336
|
+
if (entry) {
|
|
337
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
338
|
+
deps.pendingAskHooks.delete(id);
|
|
339
|
+
deps.askWaiterLastPoll?.delete(id);
|
|
340
|
+
if (entry.shortPoll) {
|
|
341
|
+
const wrote = await askStoreMarkCancelled(id, reason);
|
|
342
|
+
if (wrote) deps.notifyShortPollCancel?.(id, reason);
|
|
343
|
+
handled = wrote;
|
|
344
|
+
} else {
|
|
345
|
+
// Long-poll: end the hanging hook response so the bridge unblocks.
|
|
346
|
+
try {
|
|
347
|
+
if (entry.res && !entry.res.headersSent) {
|
|
348
|
+
entry.res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
349
|
+
entry.res.end(JSON.stringify({ cancelled: true, reason }));
|
|
350
|
+
}
|
|
351
|
+
} catch {}
|
|
352
|
+
deps.persistAskDelete(id);
|
|
353
|
+
handled = true;
|
|
354
|
+
}
|
|
355
|
+
} else {
|
|
356
|
+
// Disk-only (e.g. entry orphaned by a previous server process). Wake any
|
|
357
|
+
// hanging poll listeners too — parity with the WS ask-cancel handler.
|
|
358
|
+
handled = await askStoreMarkCancelled(id, reason);
|
|
359
|
+
if (handled) deps.notifyShortPollCancel?.(id, reason);
|
|
360
|
+
}
|
|
361
|
+
if (handled) {
|
|
362
|
+
if (deps.terminalWss) {
|
|
363
|
+
const cmsg = JSON.stringify({ type: 'ask-hook-cancelled', id, reason });
|
|
364
|
+
deps.terminalWss.clients.forEach((c) => {
|
|
365
|
+
if (c.readyState === 1) try { c.send(cmsg); } catch {}
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
deps.notifyParentPending({ type: 'ask-hook-cancelled', id });
|
|
369
|
+
}
|
|
370
|
+
} catch {}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
292
374
|
// Permission hook bridge: receive tool permission request from perm-bridge.js, long-poll for user decision
|
|
293
375
|
function permHook(req, res, parsedUrl, isLocal, deps) {
|
|
294
376
|
let body = '';
|
|
@@ -447,6 +529,7 @@ export const askPermRoutes = [
|
|
|
447
529
|
{ method: 'GET', match: 'exact', path: '/api/pending-asks', handler: pendingAsks },
|
|
448
530
|
{ method: 'POST', match: 'exact', path: '/api/ask-hook', handler: askHook },
|
|
449
531
|
{ predicate: (url, method) => url.startsWith('/api/ask-hook/') && url.includes('/result') && method === 'GET', handler: askHookResult },
|
|
532
|
+
{ predicate: (url, method) => url.startsWith('/api/ask-hook/') && url.includes('/cancel') && method === 'POST', handler: askHookCancel },
|
|
450
533
|
{ method: 'POST', match: 'exact', path: '/api/perm-hook', handler: permHook },
|
|
451
534
|
{ method: 'POST', match: 'exact', path: '/api/stream-chunk', handler: streamChunk },
|
|
452
535
|
];
|
package/server/server.js
CHANGED
|
@@ -9,8 +9,9 @@ import { execFile, exec, spawn } from 'node:child_process';
|
|
|
9
9
|
import { promisify } from 'node:util';
|
|
10
10
|
import { Worker } from 'node:worker_threads';
|
|
11
11
|
import { isPathContained } from './lib/file-api.js';
|
|
12
|
-
import { setEntry as askStoreSetEntry, deleteEntry as askStoreDeleteEntry, pruneStale as askStorePruneStale, markAnswered as askStoreMarkAnswered, markCancelled as askStoreMarkCancelled } from './lib/ask-store.js';
|
|
13
|
-
import { ASK_TIMEOUT_MS } from './lib/ask-constants.js';
|
|
12
|
+
import { setEntry as askStoreSetEntry, deleteEntry as askStoreDeleteEntry, pruneStale as askStorePruneStale, markAnswered as askStoreMarkAnswered, markCancelled as askStoreMarkCancelled, loadAskStore as askStoreLoad } from './lib/ask-store.js';
|
|
13
|
+
import { ASK_TIMEOUT_MS, ASK_WAITER_REAP_INTERVAL_MS, ASK_WAITER_LIVENESS_MS } from './lib/ask-constants.js';
|
|
14
|
+
import { reapDeadAskWaiters, sweepOrphanedDiskAsks } from './lib/ask-reaper.js';
|
|
14
15
|
import { sdkApprovalCloseType } from './lib/sdk-adapter.js';
|
|
15
16
|
import { DIST_DIR, NODE_MODULES } from './_paths.js';
|
|
16
17
|
import { createDispatcher } from './routes/_dispatch.js';
|
|
@@ -195,6 +196,11 @@ function _persistAskDelete(id) {
|
|
|
195
196
|
// until either an answer/cancel arrives or wait ms elapses (then 204).
|
|
196
197
|
const shortPollListeners = new Map(); // id -> Set<{ res, tid, finished }>
|
|
197
198
|
|
|
199
|
+
// Waiter-liveness tracking for short-poll asks (see server/lib/ask-reaper.js).
|
|
200
|
+
// Memory-only BY DESIGN: persisting it would bump the ask-store SCHEMA_VERSION;
|
|
201
|
+
// the reaper's boot-time orphan sweep covers restarts instead.
|
|
202
|
+
const askWaiterLastPoll = new Map(); // id -> ms of last POST create / GET result poll
|
|
203
|
+
|
|
198
204
|
function _notifyShortPollAnswer(id, answers) {
|
|
199
205
|
const set = shortPollListeners.get(id);
|
|
200
206
|
if (!set) return;
|
|
@@ -493,6 +499,8 @@ const deps = {
|
|
|
493
499
|
pendingAskHooks,
|
|
494
500
|
pendingPermHooks,
|
|
495
501
|
shortPollListeners,
|
|
502
|
+
askWaiterLastPoll,
|
|
503
|
+
notifyShortPollCancel: _notifyShortPollCancel,
|
|
496
504
|
editorSessions,
|
|
497
505
|
gitRestoreLocks,
|
|
498
506
|
liveStreamLastSeq: _liveStreamLastSeq,
|
|
@@ -864,6 +872,56 @@ export async function startViewer() {
|
|
|
864
872
|
}, 60 * 60 * 1000);
|
|
865
873
|
_pruneAskStoreInterval.unref();
|
|
866
874
|
|
|
875
|
+
// Waiter-liveness reaper: resolves short-poll asks whose hook process (ask-bridge)
|
|
876
|
+
// died without notifying us (e.g. AskUserQuestion declined at the CLI → SIGTERM).
|
|
877
|
+
// Keys on waiter liveness, never on wall-clock ask age — the "GUI effectively
|
|
878
|
+
// no-timeout" contract is untouched. See server/lib/ask-reaper.js.
|
|
879
|
+
const _reaperDeps = {
|
|
880
|
+
pendingAskHooks,
|
|
881
|
+
shortPollListeners,
|
|
882
|
+
askWaiterLastPoll,
|
|
883
|
+
markCancelled: askStoreMarkCancelled,
|
|
884
|
+
loadAskStore: askStoreLoad,
|
|
885
|
+
notifyShortPollCancel: _notifyShortPollCancel,
|
|
886
|
+
// terminalWss is assigned later in startServer — resolve at call time via closure.
|
|
887
|
+
broadcastCancelled: (id, reason) => {
|
|
888
|
+
if (!terminalWss) return;
|
|
889
|
+
const cmsg = JSON.stringify({ type: 'ask-hook-cancelled', id, reason });
|
|
890
|
+
terminalWss.clients.forEach((c) => {
|
|
891
|
+
if (c.readyState === 1) try { c.send(cmsg); } catch {}
|
|
892
|
+
});
|
|
893
|
+
},
|
|
894
|
+
notifyParentPending: _notifyParentPending,
|
|
895
|
+
};
|
|
896
|
+
const _reaperState = { lastSweepAt: Date.now() };
|
|
897
|
+
_askReaperTimer = setInterval(() => {
|
|
898
|
+
reapDeadAskWaiters(_reaperDeps, _reaperState).catch(() => {});
|
|
899
|
+
}, ASK_WAITER_REAP_INTERVAL_MS);
|
|
900
|
+
_askReaperTimer.unref();
|
|
901
|
+
// One-shot boot sweep for disk-only orphans left by a previous server process.
|
|
902
|
+
// Delayed one liveness window so a bridge that survived our restart can re-poll
|
|
903
|
+
// and prove ownership first; skipped when another cc-viewer instance is running.
|
|
904
|
+
// Also skipped entirely under custom CCV_START_PORT/CCV_MAX_PORT: the lsof scan
|
|
905
|
+
// covers only OUR range, so an instance on a different custom range would be
|
|
906
|
+
// invisible and its live asks could be falsely swept. The memory-owned reaper
|
|
907
|
+
// and the ApprovalModal fallback UI still cover orphans in that setup.
|
|
908
|
+
const _customPortRange = !!(process.env.CCV_START_PORT || process.env.CCV_MAX_PORT);
|
|
909
|
+
const _bootTime = Date.now();
|
|
910
|
+
if (!_customPortRange) {
|
|
911
|
+
_askOrphanSweepTimer = setTimeout(() => {
|
|
912
|
+
sweepOrphanedDiskAsks({
|
|
913
|
+
..._reaperDeps,
|
|
914
|
+
bootTime: _bootTime,
|
|
915
|
+
ownPid: process.pid,
|
|
916
|
+
portRange: [START_PORT, MAX_PORT],
|
|
917
|
+
// Async on purpose: a wedged lsof would otherwise block the event loop
|
|
918
|
+
// (and every in-flight request) for up to the full timeout.
|
|
919
|
+
lsofImpl: async (cmd) => (await execAsync(cmd, { timeout: 2000, encoding: 'utf-8' })).stdout,
|
|
920
|
+
}).catch(() => {});
|
|
921
|
+
}, ASK_WAITER_LIVENESS_MS);
|
|
922
|
+
_askOrphanSweepTimer.unref();
|
|
923
|
+
}
|
|
924
|
+
|
|
867
925
|
// 通过插件 hook 获取 HTTPS 证书选项
|
|
868
926
|
let httpsOptions = null;
|
|
869
927
|
try {
|
|
@@ -2109,6 +2167,9 @@ export function broadcastTurnEnd(sessionId = null, ts = Date.now()) {
|
|
|
2109
2167
|
// 流式状态 SSE 推送定时器:检测 streamingState 变化并广播给所有客户端。
|
|
2110
2168
|
// rising-edge → turn_end cancel(丢弃 pending,不 flush)由 _observeStreamingTick 统一处理。
|
|
2111
2169
|
let _streamingStatusTimer = null;
|
|
2170
|
+
// Waiter-liveness reaper timers (armed in startServer, cleared in the stop path).
|
|
2171
|
+
let _askReaperTimer = null;
|
|
2172
|
+
let _askOrphanSweepTimer = null;
|
|
2112
2173
|
// 启动后 30s 的更新检查 timer 句柄。必须可清理:
|
|
2113
2174
|
// - .unref() 防止它把事件循环 keep-alive 30s(测试进程靠 --test-force-exit 兜底是时序侥幸);
|
|
2114
2175
|
// - _doStop 里 clearTimeout 防止 stop/start 循环(Electron tab / 测试)泄漏多个 pending 检查。
|
|
@@ -2200,6 +2261,14 @@ async function _doStop() {
|
|
|
2200
2261
|
clearInterval(_streamingStatusTimer);
|
|
2201
2262
|
_streamingStatusTimer = null;
|
|
2202
2263
|
}
|
|
2264
|
+
if (_askReaperTimer) {
|
|
2265
|
+
clearInterval(_askReaperTimer);
|
|
2266
|
+
_askReaperTimer = null;
|
|
2267
|
+
}
|
|
2268
|
+
if (_askOrphanSweepTimer) {
|
|
2269
|
+
clearTimeout(_askOrphanSweepTimer);
|
|
2270
|
+
_askOrphanSweepTimer = null;
|
|
2271
|
+
}
|
|
2203
2272
|
if (_updateCheckTimer) {
|
|
2204
2273
|
clearTimeout(_updateCheckTimer);
|
|
2205
2274
|
_updateCheckTimer = null;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
._liveTag_8g4c3_3{position:relative;display:inline-flex;align-items:center;justify-content:flex-start;border-radius:999px;border:1px solid;border-color:var(--ctx-color);color:var(--ctx-color);padding:0 10px;height:100%;font-size:12px;line-height:1;overflow:hidden;transition:border-color .3s,color .3s;white-space:nowrap;background:var(--bg-base-pure)}._liveTagFill_8g4c3_23{position:absolute;left:0;top:0;bottom:0;width:var(--ctx-percent, 0);background-color:var(--ctx-color);background-image:repeating-linear-gradient(135deg,rgba(255,255,255,.25) 0,rgba(255,255,255,.25) 2px,transparent 2px,transparent 7px);opacity:.35;transition:width .5s ease,background-color .3s;pointer-events:none}._liveTagContent_8g4c3_44{position:relative;z-index:1;display:inline-flex;align-items:center}._liveTagHistory_8g4c3_52{background:var(--bg-surface);border-color:var(--border-light);color:var(--text-primary)}._liveTagText_8g4c3_59{margin-left:4px;font-variant-numeric:tabular-nums}._cachePopoverPlaceholder_8g4c3_65{min-width:300px}._modeRow_8rc0c_1{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px}._modeLeft_8rc0c_9{display:flex;align-items:center;gap:10px}._modeRight_8rc0c_15{display:flex;align-items:center;gap:8px}._overrideWarn_8rc0c_22{font-size:12px;color:var(--color-warning, #d4810a)}._previewLabel_8rc0c_27{font-size:13px;color:var(--text-secondary, #888)}._previewBox_8rc0c_33{border:1px solid var(--border-secondary, #d9d9d9);border-radius:8px;padding:8px 14px;min-height:320px;max-height:60vh;overflow:auto}._previewEmpty_8rc0c_42{font-size:13px;color:var(--text-secondary, #888)}._hint_8rc0c_47{margin-top:10px;font-size:12px;color:var(--text-secondary, #888);line-height:1.6}._dirLine_8rc0c_54{word-break:break-all}._warn_8rc0c_58{margin-top:10px;font-size:12px;color:var(--color-warning, #d4810a)}._tabRow_8rc0c_66{display:flex;align-items:flex-end;gap:6px;margin-bottom:10px;border-bottom:1px solid var(--border-secondary, #d9d9d9);flex-wrap:nowrap;overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}._tabRow_8rc0c_66::-webkit-scrollbar{display:none}._tabBtn_8rc0c_82{display:inline-flex;align-items:center;gap:5px;padding:6px 10px;font-size:12px;line-height:1.6;border:1px solid var(--border-color, #d9d9d9);border-radius:14px;background:var(--bg-main, var(--bg-container));color:var(--text-secondary);cursor:pointer;transition:all .2s;-webkit-user-select:none;user-select:none;white-space:nowrap;flex-shrink:0}._tabBtn_8rc0c_82:hover{border-color:var(--color-primary);color:var(--color-primary)}._tabBtn_8rc0c_82:focus-visible{outline:2px solid var(--color-primary);outline-offset:1px}._tabBtn_8rc0c_82._tabActive_8rc0c_110{background:var(--bg-container);color:var(--color-primary);border-color:var(--border-primary);border-bottom-color:var(--bg-container);border-radius:10px 10px 0 0;font-weight:500;margin-bottom:-1px;position:relative;z-index:1}._tabWrap_8rc0c_122{position:relative;display:inline-flex;align-items:center;flex-shrink:0}._tabTitle_8rc0c_129{max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}._tabDelete_8rc0c_137{position:absolute;top:-2px;inset-inline-end:-4px;width:14px;height:14px;border-radius:50%;background:var(--bg-elevated);border:1px solid var(--border-primary);color:var(--text-secondary);font-size:10px;line-height:1;display:flex;align-items:center;justify-content:center;cursor:pointer;opacity:0;transition:opacity .15s;z-index:2}._tabWrap_8rc0c_122:hover ._tabDelete_8rc0c_137{opacity:1}._tabDelete_8rc0c_137:hover{color:var(--color-error, #d4380d);border-color:var(--color-error, #d4380d)}@media(max-width:768px){._tabDelete_8rc0c_137{opacity:1}}._addBtn_8rc0c_169{display:inline-flex;align-items:center;align-self:center;gap:4px;padding:3px 10px;font-size:12px;line-height:1.6;margin-left:2px;border:none;border-radius:14px;background:transparent;color:var(--text-muted);cursor:pointer;transition:background .15s,color .15s;-webkit-user-select:none;user-select:none;white-space:nowrap;flex-shrink:0}._addBtn_8rc0c_169:hover{color:var(--color-primary);background:var(--bg-surface)}._addBtn_8rc0c_169:disabled{cursor:not-allowed;opacity:.5}[data-theme=light] ._addBtn_8rc0c_169{color:var(--text-secondary)}._scopeBadge_8rc0c_201{font-size:10px;line-height:1.4;padding:0 4px;border-radius:6px;background:var(--bg-surface);color:var(--text-muted)}._dirtyDot_8rc0c_211{width:6px;height:6px;border-radius:50%;background:var(--color-primary);flex-shrink:0}._helpBtn_8rc0c_220{align-self:center;margin-left:auto;width:16px;height:16px;border-radius:50%;border:1px solid var(--border-primary);background:var(--bg-elevated);color:var(--text-secondary);font-size:11px;line-height:1;display:inline-flex;align-items:center;justify-content:center;cursor:help;flex-shrink:0;-webkit-user-select:none;user-select:none}._helpBtn_8rc0c_220:hover{color:var(--color-primary);border-color:var(--color-primary)}._addPopover_8rc0c_244{display:flex;flex-direction:column;gap:8px;width:240px}._addError_8rc0c_250{font-size:12px;color:var(--color-error, #d4380d)}._editButton_31hdb_10{display:inline-flex;align-items:center;justify-content:center;vertical-align:middle;background:transparent;border:none;padding:0 2px;margin-left:4px;cursor:pointer;font-size:inherit;line-height:1;color:var(--text-secondary, #888);opacity:0;border-radius:3px;transition:opacity .12s ease,color .12s ease,background-color .12s ease}._editButton_31hdb_10 .anticon{display:inline-flex;align-items:center;line-height:1;position:relative;top:-1px}._editButton_31hdb_10:hover,._editButton_31hdb_10:focus-visible{opacity:1;color:var(--text-primary, #333);background-color:var(--bg-hover, rgba(0, 0, 0, .04));outline:none}._editButton_31hdb_10:focus-visible{box-shadow:0 0 0 2px var(--focus-ring, rgba(64, 158, 255, .4))}._footer_31hdb_55{display:flex;justify-content:space-between;align-items:center;gap:8px}._footerLeft_31hdb_61,._footerRight_31hdb_65{display:flex;gap:8px}._projectNameRow_31hdb_70{display:flex;align-items:center;gap:8px;margin-bottom:12px;padding:6px 10px;background:var(--bg-secondary, rgba(0, 0, 0, .03));border-radius:4px;font-size:12px}._projectNameLabel_31hdb_80{color:var(--text-secondary, #888)}._projectNameValue_31hdb_83{color:var(--text-primary, #333);font-family:var(--font-mono, ui-monospace, SFMono-Regular, monospace);word-break:break-all}._panel_1nxl3_1{display:flex;flex-direction:column;gap:14px}._required_1nxl3_7{margin-left:2px;color:var(--color-error-light, #ff7b7b)}._optional_1nxl3_12{margin-left:6px;font-size:12px;font-weight:400;color:var(--text-secondary)}._row_1nxl3_19{display:flex;align-items:center;justify-content:space-between;gap:12px}._label_1nxl3_26{font-size:14px;font-weight:500}._control_1nxl3_31{display:inline-flex;align-items:center;gap:10px}._field_1nxl3_37{display:flex;flex-direction:column;gap:6px}._fieldLabel_1nxl3_43{font-size:13px;color:var(--text-secondary)}._help_1nxl3_48{font-size:12px;color:var(--text-secondary);line-height:1.4}._warn_1nxl3_54{font-size:12px;line-height:1.5;color:var(--color-error-light, #ff7b7b)}._hint_1nxl3_60{font-size:12px;line-height:1.5;color:var(--text-secondary)}._helpIcon_1nxl3_67{margin-left:6px;color:var(--text-tertiary, #999);cursor:help;font-size:13px}._detailsToggle_1nxl3_74{display:inline-flex;align-items:center;gap:5px;align-self:flex-start;padding:0;border:none;background:none;cursor:pointer;font-size:12px;color:var(--text-secondary)}._detailsToggle_1nxl3_74:hover{color:var(--text-primary, var(--text-secondary))}._details_1nxl3_74{display:flex;flex-direction:column;gap:10px}._actions_1nxl3_98{display:flex;justify-content:flex-end;gap:10px;margin-top:16px;padding-top:16px;border-top:1px solid var(--border-secondary)}._testBtn_1nxl3_108{margin-right:auto}._tabRow_1q32a_4{display:flex;align-items:flex-end;gap:6px;padding:0 8px;flex-wrap:nowrap;overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}._tabRow_1q32a_4::-webkit-scrollbar{display:none}._tabBtn_1q32a_24{display:inline-flex;align-items:center;gap:7px;padding:8px 16px;font-size:14px;line-height:1.5;border:1px solid transparent;border-radius:10px;background:transparent;color:var(--text-secondary);cursor:pointer;transition:color .18s,border-color .18s,background .18s;white-space:nowrap;flex-shrink:0;-webkit-user-select:none;user-select:none}._tabBtn_1q32a_24:hover{color:var(--color-primary)}._tabBtn_1q32a_24._tabBtnActive_1q32a_52,._tabBtn_1q32a_24._tabBtnActive_1q32a_52:hover{background:var(--bg-container);color:var(--color-primary);border-color:var(--border-primary);border-bottom:2px solid var(--bg-container);border-radius:10px 10px 0 0;font-weight:500;margin-bottom:-1px;position:relative;z-index:2}._toolBody_1q32a_8{border:1px solid var(--border-primary);border-top:none;border-radius:8px;background:var(--bg-container);padding:18px 20px;min-width:0}[data-theme=light] ._toolBody_1q32a_8{box-shadow:0 3px 8px #00000014}[data-theme=light] ._tabBtnActive_1q32a_52{box-shadow:0 -3px 8px #0000000f}[data-theme=dark] ._toolBody_1q32a_8 .ant-input,[data-theme=dark] ._toolBody_1q32a_8 .ant-input-affix-wrapper,[data-theme=dark] ._toolBody_1q32a_8 .ant-select-selector{background-color:var(--bg-elevated)}._scrollBody_riojj_1{height:100%;box-sizing:border-box;overflow-y:auto;overflow-x:hidden;scrollbar-gutter:stable;padding:4px 2px;background:var(--bg-container)}._headerBar_riojj_15{display:flex;align-items:center;gap:8px}._refreshBtn_riojj_21{flex:0 0 auto}._statusTag_riojj_27{display:inline-flex;align-items:center;margin-left:auto}._statusTag_riojj_27 .ant-tag{margin-inline-end:0}._center_riojj_36{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:40px 0;color:var(--text-secondary)}._hint_riojj_46{color:var(--text-secondary);font-size:13px}._chip_bvaji_4{position:relative;line-height:0;cursor:pointer;-webkit-user-select:none;user-select:none;transition:opacity .15s}._chip_bvaji_4:hover{opacity:.75}._logo_bvaji_16{display:block;transition:color .15s}._connecting_bvaji_22{opacity:.5}._dotError_bvaji_27{position:absolute;right:-2px;top:-2px;width:6px;height:6px;border-radius:50%;background:var(--color-error, #ff4d4f);pointer-events:none}._modelCard_8xfmi_3{border:1px solid var(--border-secondary);border-radius:6px;padding:8px 10px;background:var(--bg-container)}._modelName_8xfmi_9{font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:8px;padding-bottom:4px;border-bottom:1px solid var(--border-secondary)}._statsTable_8xfmi_17{width:100%;border-collapse:collapse}._th_8xfmi_21{padding:2px 12px;font-size:12px;font-family:monospace;white-space:nowrap;color:var(--text-tertiary);font-weight:400;text-align:right}._td_8xfmi_30{padding:2px 12px;font-size:12px;font-family:monospace;white-space:nowrap;color:var(--text-primary);text-align:right}._label_8xfmi_38{padding:2px 12px;font-size:12px;font-family:monospace;white-space:nowrap;color:var(--text-light);font-weight:400;text-align:left}._rowBorder_8xfmi_47{border-bottom:1px solid var(--border-primary)}._rebuildTotalRow_8xfmi_50{border-top:1px solid var(--border-light)}._rebuildTotalRow_8xfmi_50 td{font-weight:600}._cachePopoverEmpty_8xfmi_56{padding:8px 4px;color:var(--text-tertiary);font-size:13px}._toolChipGrid_8xfmi_61{display:flex;flex-wrap:wrap;gap:4px;padding:2px 0 6px 2px}._cacheToolChip_8xfmi_67{font-size:11px;padding:0 6px;border-radius:3px;background:var(--bg-surface);color:var(--text-secondary);line-height:18px;max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border:1px solid var(--border-primary);cursor:help}._titleIcon_8xfmi_81{margin-right:8px}._detailMarkdownCard_8xfmi_86{border:1px solid var(--border-secondary);border-radius:6px;padding:8px 10px;background:var(--bg-container)}._memoryMarkdown_8xfmi_93{font-size:12.5px;line-height:1.55;color:var(--text-primary);word-break:break-word}._memoryMarkdown_8xfmi_93 p{margin:0 0 6px}._memoryMarkdown_8xfmi_93 ul,._memoryMarkdown_8xfmi_93 ol{margin:4px 0 6px;padding-left:20px}._memoryMarkdown_8xfmi_93 li{margin:2px 0}._memoryMarkdown_8xfmi_93 h1,._memoryMarkdown_8xfmi_93 h2,._memoryMarkdown_8xfmi_93 h3,._memoryMarkdown_8xfmi_93 h4{font-size:13px;font-weight:600;margin:8px 0 4px;color:var(--text-primary)}._memoryMarkdown_8xfmi_93 h1{font-size:14px}._memoryMarkdown_8xfmi_93 a{color:var(--primary-color, #1677ff);text-decoration:none;cursor:pointer}._memoryMarkdown_8xfmi_93 a:hover{text-decoration:underline}._memoryMarkdown_8xfmi_93 code{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12px;padding:1px 4px;border-radius:3px;background:var(--bg-surface);color:var(--text-primary)}._memoryMarkdown_8xfmi_93 pre{margin:6px 0;padding:8px 10px;border-radius:4px;background:var(--bg-surface);overflow-x:auto}._memoryMarkdown_8xfmi_93 pre code{padding:0;background:transparent;font-size:12px}._memoryMarkdown_8xfmi_93 blockquote{margin:4px 0;padding:2px 8px;border-left:3px solid var(--border-hover);color:var(--text-secondary)}._memoryMarkdown_8xfmi_93 hr{margin:8px 0;border:none;border-top:1px solid var(--border-secondary)}._quickMenuGroup_8xfmi_164{position:relative}._quickMenuRow_8xfmi_168{display:flex;align-items:center;gap:6px;width:100%;background:none;border:none;color:var(--text-primary);font-size:13px;padding:6px 12px;text-align:left;cursor:pointer;border-radius:4px;white-space:nowrap}._quickMenuGroup_8xfmi_164:hover ._quickMenuRow_8xfmi_168,._quickMenuGroupOpen_8xfmi_185 ._quickMenuRow_8xfmi_168{background:var(--border-secondary)}._quickMenuRowIcon_8xfmi_189{display:flex;align-items:center;color:var(--text-tertiary)}._quickMenuRowIcon_8xfmi_189 svg{width:14px;height:14px}._quickMenuLabel_8xfmi_200{flex:1}._quickMenuValue_8xfmi_205{color:var(--text-tertiary);font-size:12px}._quickMenuCaret_8xfmi_210{color:var(--text-tertiary);font-size:10px}._quickMenuSubWrap_8xfmi_218{display:none;position:absolute;left:100%;bottom:-5px;padding-left:6px;z-index:1}._quickMenuGroupOpen_8xfmi_185 ._quickMenuSubWrap_8xfmi_218{display:block}._quickMenuSub_8xfmi_218{display:flex;flex-direction:column;background:var(--bg-elevated);border:1px solid var(--border-hover);border-radius:8px;padding:4px;min-width:76px;max-height:60vh;overflow-y:auto;box-shadow:0 6px 16px #0000002e}._quickMenuOption_8xfmi_248{background:none;border:none;color:var(--text-primary);font-size:13px;padding:5px 12px;text-align:left;cursor:pointer;border-radius:4px;white-space:nowrap}._quickMenuOption_8xfmi_248:hover{background:var(--border-secondary)}._quickMenuOption_8xfmi_248._quickMenuOptionActive_8xfmi_264{color:var(--color-primary);font-weight:500}._quickMenuSubTipBox_8xfmi_270{padding:8px 12px;width:300px;white-space:normal}._headerBar_uzc7r_2{display:flex;align-items:center;justify-content:space-between;width:100%;height:100%}._logoWrap_uzc7r_10{display:inline-flex;align-items:center;position:relative;margin-top:14px}._logoWrapActive_uzc7r_17:after{content:"";position:absolute;top:-10px;bottom:-10px;left:0;right:-200px}._logoImage_uzc7r_26{height:24px;width:24px;border-radius:3px;vertical-align:middle;opacity:.75;transition:opacity .2s;cursor:pointer}._logoImageActive_uzc7r_36{opacity:1}._compactBtn_uzc7r_43{font-size:12px;height:30px;display:inline-flex;align-items:center;justify-content:center}._compactBtnNoBorder_uzc7r_52{width:30px;height:30px;min-width:30px;padding:0;border:none;font-size:18px;line-height:1;display:inline-flex;align-items:center;justify-content:center}._compactBtnNoBorder_uzc7r_52 .anticon{display:inline-flex;align-items:center;justify-content:center;line-height:0}._headerProjectName_uzc7r_79{font-size:12px;color:inherit;white-space:nowrap}._headerProjectName_uzc7r_79:hover [data-alias-edit-trigger],._headerProjectName_uzc7r_79:focus-within [data-alias-edit-trigger]{opacity:.55}._countdownStrong_uzc7r_96{font-variant-numeric:tabular-nums}._qrcodePopover_uzc7r_101{display:flex;flex-direction:column;align-items:center;padding:8px}._qrcodeSection_uzc7r_108{display:flex;flex-direction:column;align-items:center;padding:16px;margin-bottom:12px;border:1px solid var(--border-secondary);border-radius:8px;background:var(--bg-container)}._qrcodeTitle_uzc7r_119{font-size:14px;font-weight:600;color:var(--text-primary);margin-bottom:12px}._qrcodeUrlInput_uzc7r_126{margin-top:12px;font-size:12px;font-family:Menlo,Monaco,monospace}._qrcodeUrlCopy_uzc7r_132{cursor:pointer;color:var(--text-tertiary);transition:color .2s}._qrcodeUrlCopy_uzc7r_132:hover{color:var(--color-primary-light)}._authSection_uzc7r_143{display:flex;flex-direction:column;align-items:stretch;box-sizing:border-box;width:100%;margin-top:12px;padding:12px;border:1px solid var(--border-secondary);border-radius:8px;background:var(--bg-container);gap:8px}._authHeaderRow_uzc7r_157{display:flex;align-items:center;justify-content:space-between}._authTitle_uzc7r_163{font-size:13px;font-weight:600;color:var(--text-primary)}._authPasswordLabel_uzc7r_169{font-size:12px;color:var(--text-secondary)}._authPasswordInput_uzc7r_174{font-size:12px;font-family:Menlo,Monaco,monospace}._authSaveBtn_uzc7r_179{align-self:flex-end}._authEmptyWarn_uzc7r_183{font-size:12px;color:var(--color-error-light);line-height:1.4}._settingsGroupBox_uzc7r_191{border:1px solid var(--border-secondary);border-radius:8px;background:var(--bg-container);padding:4px 16px;margin-bottom:12px}._settingsGroupTitle_uzc7r_199{font-size:14px;font-weight:600;color:var(--text-primary);padding:12px 0 4px;border-bottom:1px solid var(--border-secondary)}._settingsItem_uzc7r_208{display:flex;justify-content:space-between;align-items:center;padding:12px 0}._settingsLabel_uzc7r_215{font-size:14px}._settingsHelpIcon_uzc7r_220{font-size:16px;color:var(--text-disabled);cursor:help;margin-left:4px}._settingsDivider_uzc7r_227{border-top:1px solid var(--border-primary);margin:12px 0}._logDirInput_uzc7r_232{margin-top:8px;background:var(--bg-base-alt);border-color:var(--border-light);color:var(--text-primary);font-family:monospace;font-size:13px}._tokenStatsEmpty_uzc7r_242{padding:8px 4px;color:var(--text-tertiary);font-size:13px}._tokenStatsContainer_uzc7r_249{display:flex;gap:12px;align-items:flex-start}._tokenStatsColumn_uzc7r_255{min-width:240px}._toolStatsColumn_uzc7r_259{min-width:180px}._modelCardSpaced_uzc7r_265{margin-bottom:10px}._rebuildCard_uzc7r_274{border:1px solid var(--border-secondary);border-radius:6px;padding:8px 10px;margin-top:10px;background:var(--bg-container)}._promptExportBar_uzc7r_283{margin-bottom:12px}._promptScrollArea_uzc7r_287{max-height:500px;overflow:auto}._promptEmpty_uzc7r_292{color:var(--text-tertiary);padding:12px}._promptTimestamp_uzc7r_298{color:var(--text-muted);font-size:12px;margin:12px 0 4px;padding-bottom:6px}._textPromptCard_uzc7r_306{margin:4px 0;background:var(--bg-container);border-radius:6px;border:1px solid var(--border-secondary);padding:10px 14px}._preText_uzc7r_315{white-space:pre-wrap;word-break:break-word;font-size:13px;line-height:1.6;color:var(--text-primary);margin:4px 0}._systemCollapse_uzc7r_325{margin:4px 0;background:var(--bg-elevated);border:1px solid var(--border-secondary);border-radius:6px}._systemLabel_uzc7r_332{color:var(--text-tertiary);font-size:12px}._preSys_uzc7r_337{white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.5;color:var(--text-tertiary);margin:0}._promptTextarea_uzc7r_347{box-sizing:border-box;background:var(--bg-base-pure);width:100%;min-height:400px;color:var(--text-primary);font-family:monospace;font-size:13px;line-height:1.6;border:none;resize:vertical;padding:10px 14px;outline:none}._projectStatsCenter_uzc7r_363{display:flex;justify-content:center;padding:40px 0}._projectStatsEmpty_uzc7r_369{color:var(--text-tertiary);padding:40px 0;text-align:center;font-size:13px}._projectStatsContent_uzc7r_376{display:flex;flex-direction:column;gap:16px}._projectStatsUpdated_uzc7r_382{color:var(--text-muted);font-size:12px;text-align:right}._projectStatsSummary_uzc7r_388{display:grid;grid-template-columns:1fr 1fr;gap:10px}._projectStatCard_uzc7r_394{background:var(--bg-container);border:1px solid var(--border-secondary);border-radius:8px;padding:14px 12px;text-align:center}._projectStatValue_uzc7r_402{font-size:22px;font-weight:700;color:var(--text-primary);font-family:monospace;font-variant-numeric:tabular-nums}._projectStatLabel_uzc7r_410{font-size:12px;color:var(--text-tertiary);margin-top:4px}._projectStatsSection_uzc7r_416{display:flex;flex-direction:column;gap:10px}._projectStatsSectionTitle_uzc7r_422{font-size:14px;font-weight:600;color:var(--text-secondary);padding-bottom:4px;border-bottom:1px solid var(--border-secondary)}._projectStatsModelCard_uzc7r_430{background:var(--bg-container);border:1px solid var(--border-secondary);border-radius:6px;padding:10px 12px}._projectStatsModelHeader_uzc7r_437{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;padding-bottom:4px;border-bottom:1px solid var(--border-secondary)}._projectStatsModelName_uzc7r_446{font-size:13px;font-weight:600;color:var(--text-primary)}._projectStatsModelCount_uzc7r_452{font-size:12px;color:var(--text-tertiary);font-family:monospace}._cacheCopyBtn_uzc7r_460{font-size:14px;color:var(--text-tertiary);cursor:pointer;transition:color .2s;margin-left:8px}._cacheCopyBtn_uzc7r_460:hover{color:var(--text-primary)}._cacheTokenInfo_uzc7r_472{display:flex;align-items:center;font-size:12px;font-family:monospace;color:var(--text-light);margin-bottom:8px}._cacheCodeBlock_uzc7r_485{white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.5;color:var(--text-secondary);background:var(--bg-container);border:1px solid var(--border-primary);border-radius:4px;padding:8px;margin:4px 0;font-family:Menlo,Monaco,monospace}._cacheCodeBlockSystem_uzc7r_499{white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.5;color:var(--text-secondary);background:var(--bg-code-system);border:1px solid var(--border-code-system);border-radius:4px;padding:8px;margin:4px 0;font-family:Menlo,Monaco,monospace}._cacheNavBtn_uzc7r_513{margin-left:auto;font-size:11px;color:var(--color-primary);cursor:pointer;border:1px solid var(--color-primary);border-radius:3px;padding:1px 6px;white-space:nowrap}._cacheNavBtn_uzc7r_513:hover{background:var(--color-primary-bg-light)}._cacheNavList_uzc7r_528{width:600px;max-height:300px;overflow-y:auto}._cacheNavItem_uzc7r_534{padding:4px 8px;font-size:12px;color:var(--text-secondary);cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;border-radius:3px}._cacheNavItem_uzc7r_534:hover{background:var(--color-primary-bg-lighter);color:var(--text-white)}._cacheBlockHighlight_uzc7r_550{box-shadow:0 0 10px var(--color-primary-shadow);transition:box-shadow .2s ease-in}._cacheBlockHighlightFading_uzc7r_555{box-shadow:0 0 10px transparent;transition:box-shadow 3s ease-out}._cacheBorderSvg_uzc7r_560{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;pointer-events:none;overflow:visible}._cacheBorderSvgFading_uzc7r_569{opacity:0;transition:opacity 3s ease-out}._cacheBorderRect_uzc7r_574{animation:_cacheDashRotate_uzc7r_1 4s linear infinite}@keyframes _cacheDashRotate_uzc7r_1{0%{stroke-dashoffset:0}to{stroke-dashoffset:-100}}._thLeft_uzc7r_584{text-align:left}._cacheWriteToken_uzc7r_599{color:var(--color-code-orange)}._cacheReadToken_uzc7r_603{color:var(--color-success)}._cacheCtxPercent_uzc7r_607{color:var(--text-tertiary);margin-left:6px}._qrcodeIcon_uzc7r_627{width:30px;height:30px;padding:6px;box-sizing:border-box;color:var(--text-secondary);cursor:pointer;border-radius:6px;transition:color .15s ease,background-color .15s ease;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle;background:none;border:none}._qrcodeIcon_uzc7r_627:hover{color:var(--text-primary);background:var(--bg-hover, rgba(0, 0, 0, .04))}._qrcodeIcon_uzc7r_627:focus-visible{outline:2px solid var(--primary-color, #1677ff);outline-offset:1px}._approvalBell_uzc7r_653{position:relative;width:30px;height:30px;padding:6px;box-sizing:border-box;color:var(--color-warning, #faad14);cursor:pointer;border-radius:6px;transition:color .15s ease,background-color .15s ease;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle;background:none;border:none}._approvalBell_uzc7r_653:hover{color:var(--text-primary);background:var(--bg-hover, rgba(0, 0, 0, .04))}._approvalBell_uzc7r_653:focus-visible{outline:2px solid var(--primary-color, #1677ff);outline-offset:1px}._approvalBellBadge_uzc7r_678{position:absolute;top:0;right:0;min-width:14px;height:14px;padding:0 3px;box-sizing:border-box;background:var(--color-error, #ff4d4f);color:#fff;font-size:9px;line-height:14px;border-radius:7px;text-align:center;font-weight:600}._proxySwapIcon_uzc7r_695{margin-right:4px;font-size:11px}._proxyProfileTag_uzc7r_701{border-radius:12px;background:var(--border-secondary);border:1px solid var(--border-light);color:var(--text-tertiary);font-size:12px;cursor:pointer;transition:color .2s,border-color .2s}._proxyProfileTag_uzc7r_701:hover{color:var(--text-secondary);border-color:var(--text-disabled)}._pinnedShortcut_uzc7r_717{height:24px;margin-top:11px;display:inline-flex;align-items:center;justify-content:center;font-size:16px;line-height:1;color:var(--text-tertiary);opacity:.75;cursor:pointer;transition:opacity .2s,color .2s}._pinnedShortcut_uzc7r_717:hover{opacity:1;color:var(--text-secondary)}._pinnedShortcut_uzc7r_717 .anticon{display:inline-flex;align-items:center;justify-content:center;line-height:0}._themeToggle_uzc7r_746{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle;width:30px;height:30px;padding:0;border:none;border-radius:6px;background:transparent;color:var(--text-secondary);cursor:pointer;transition:background-color .2s ease,color .2s ease;flex-shrink:0;outline:none}._themeToggle_uzc7r_746:hover{background:var(--color-bg-hover, rgba(127, 127, 127, .12));color:var(--text-primary)}._themeToggle_uzc7r_746:focus-visible{box-shadow:0 0 0 2px var(--primary-color, #1677ff)}._themeToggleIcon_uzc7r_772{display:block}._headerRightRow_uzc7r_777 .ant-space-item{display:inline-flex;align-items:center}._headerCountdownTag_uzc7r_783{height:30px;margin:0;padding:0 10px;display:inline-flex;align-items:center;background:var(--bg-surface);border:1px solid var(--border-hover);border-radius:6px;line-height:1}._centerEmpty_1ivpu_1{display:flex;align-items:center;justify-content:center;height:100%}._scrollContainer_1ivpu_8{overflow:auto;height:100%;-webkit-overflow-scrolling:touch;will-change:scroll-position}._listItem_1ivpu_15{cursor:pointer;padding:8px 12px;border-left:6px solid transparent;border-right:1px solid var(--border-primary);border-top:1px solid transparent;border-bottom:1px solid var(--border-primary);transition:background .15s}._listItem_1ivpu_15:hover{border-left-color:var(--border-hover)}._listItemActive_1ivpu_29{background:var(--color-primary-bg-faint);border-left-color:var(--color-primary-light);border-right:1px solid var(--color-primary-light);border-top:1px solid var(--color-primary-light);border-bottom:1px solid var(--color-primary-light)}._listItemActive_1ivpu_29,._listItemActive_1ivpu_29:hover{background:var(--color-primary-bg-faint);border-left-color:var(--color-primary-light);border-right-color:var(--color-primary-light);border-top-color:var(--color-primary-light);border-bottom-color:var(--color-primary-light)}._itemContent_1ivpu_46{width:100%;min-width:0}._itemHeader_1ivpu_51{display:flex;align-items:center;gap:6px;margin-bottom:4px;font-size:12px}._tagNoMargin_1ivpu_59{margin:0;font-size:12px}._modelName_1ivpu_64{font-size:12px;color:var(--text-tertiary)}._modelNameMain_1ivpu_69{color:var(--color-code-orange)}._time_1ivpu_73{font-size:12px;color:var(--text-gray);margin-left:auto}._detailRow_1ivpu_79{display:flex;gap:8px;font-size:12px;align-items:center}._urlText_1ivpu_86{color:var(--text-disabled);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}._duration_1ivpu_95{color:var(--text-gray);flex-shrink:0}._statusOk_1ivpu_100{color:var(--color-success);opacity:.5;flex-shrink:0}._statusErr_1ivpu_106{color:var(--color-error);flex-shrink:0}._statusDefault_1ivpu_111{color:var(--text-tertiary);flex-shrink:0}._usageBox_1ivpu_116{background:var(--bg-container);border-radius:4px;padding:3px 6px;margin-top:4px;font-size:12px;color:var(--text-gray);line-height:1.6}._cacheDot_1ivpu_126{display:inline-block;width:6px;height:6px;border-radius:50%;margin:0 3px;vertical-align:middle}._cacheDotLoss_1ivpu_135{background-color:var(--color-red-dark-bg);cursor:help}._cacheDotNormal_1ivpu_140{background-color:var(--border-hover)}._cacheDotTools_1ivpu_145{background-color:var(--color-code-purple);box-shadow:0 0 0 1.5px var(--color-purple-border)}._tagMainAgent_1ivpu_150{color:var(--color-code-orange);border-color:var(--color-code-orange-border);background:var(--color-code-orange-bg)}._tagPlan_1ivpu_156{color:var(--color-error-muted);border-color:var(--color-error-muted);background-color:var(--bg-base-pure)}._tagMuted_1ivpu_162{color:var(--text-muted);border-color:var(--border-light);background-color:var(--bg-base-pure)}._tooltipPreLine_1ivpu_168{white-space:pre-line}._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_qeuid_1{background:var(--bg-container);border-radius:6px;border:1px solid var(--border-primary);padding:12px;font-size:13px;font-family:monospace;overflow:auto}._root_16dib_1{display:flex;height:100%;min-height:0;gap:0}._sidebar_16dib_9{width:220px;flex-shrink:0;border-right:1px solid var(--border-primary);overflow-y:auto;padding:4px 0;-webkit-overflow-scrolling:touch}._section_16dib_18{-webkit-user-select:none;user-select:none}._sectionHeader_16dib_22{display:flex;align-items:center;gap:6px;width:100%;padding:6px 10px;cursor:pointer;color:var(--text-primary);font-size:12px;font-weight:600;transition:background .15s;background:none;border:0;text-align:left;font:inherit}._sectionHeader_16dib_22:hover{background:var(--overlay-light-faint)}._sectionHeader_16dib_22:focus-visible{outline:1px solid var(--color-primary-outline);outline-offset:-1px}._arrow_16dib_48{font-size:10px;color:var(--text-tertiary);flex-shrink:0}._sectionTitle_16dib_54{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}._sectionCount_16dib_62{font-size:11px;color:var(--text-muted);background:var(--bg-elevated);border-radius:10px;padding:0 6px;line-height:18px}._sectionBody_16dib_71{padding:2px 0}._historyToggle_16dib_76{display:flex;align-items:center;gap:6px;width:100%;padding:4px 10px 4px 14px;cursor:pointer;color:var(--text-muted);font-size:11px;transition:color .15s,background .15s;background:none;border:0;text-align:left;font:inherit}._historyToggle_16dib_76:hover{color:var(--text-tertiary);background:var(--overlay-light-faint)}._historyToggle_16dib_76:focus-visible{outline:1px solid var(--color-primary-outline);outline-offset:-1px}._historyToggleLabel_16dib_102{flex:1}._item_16dib_107{display:flex;align-items:center;justify-content:space-between;width:100%;padding:4px 9px 4px 23px;font-size:12px;color:var(--text-tertiary);cursor:pointer;transition:background .15s,color .15s;background:none;border:1px solid transparent;border-radius:4px;box-sizing:border-box;text-align:left;font:inherit}._item_16dib_107:hover{background:var(--overlay-light-faint);color:var(--text-primary)}._item_16dib_107:focus-visible{outline:1px solid var(--color-primary-outline);outline-offset:-1px}._itemActive_16dib_135,._itemActive_16dib_135:hover{background:var(--color-primary-bg-faint);color:var(--color-primary);border-color:var(--color-primary-light)}._itemContent_16dib_142{flex:1;min-width:0;overflow:hidden}._itemLabel_16dib_148{font-family:monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block}._itemSublabel_16dib_156{font-size:10px;color:var(--text-disabled);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:1px}._itemTime_16dib_165{font-size:9px;color:var(--text-disabled);flex-shrink:0;margin-left:4px;font-family:monospace}._contentWrap_16dib_176{flex:1;min-width:0;display:flex;flex-direction:column}._content_16dib_174{flex:1;min-width:0;min-height:0;overflow:auto;padding:12px 16px;-webkit-overflow-scrolling:touch}._contentEmpty_16dib_192{height:100%;display:flex;align-items:center;justify-content:center}._contentInner_16dib_199{padding-bottom:20px}._emptyWrap_16dib_203{display:flex;align-items:center;justify-content:center;height:200px}._roleHeader_16dib_211{display:flex;align-items:center;gap:8px;margin-bottom:8px;flex-wrap:nowrap}._roleBadge_16dib_219{font-size:10px;font-weight:600;letter-spacing:.04em;padding:2px 7px;border-radius:4px;flex-shrink:0}._role_user_16dib_228{background:var(--color-primary-bg-lighter);color:var(--color-primary-lighter);border:1px solid var(--color-primary-bg-medium)}._role_assistant_16dib_234{background:var(--color-purple-bg);color:var(--color-code-purple);border:1px solid var(--color-purple-border)}._roleLabel_16dib_240{font-size:11px;color:var(--text-muted);flex:1;min-width:0}._contentTime_16dib_247{margin-left:auto;font-size:10px;color:var(--text-disabled);font-family:monospace;flex-shrink:0}._turnDivider_16dib_255{border:none;border-top:1px solid var(--border-primary);margin:14px 0}._textBlock_16dib_262{margin-bottom:8px;border:1px solid var(--border-primary);border-radius:6px;overflow:hidden}._textBlockBar_16dib_269{display:flex;align-items:center;gap:6px;padding:4px 10px;background:var(--bg-container);border-bottom:1px solid var(--border-primary)}._textBlockBody_16dib_278{padding:10px 12px;font-size:13px;line-height:1.7;color:var(--text-primary);word-break:break-word}._textBlockCompact_16dib_286{position:relative;padding:8px 10px;font-size:12px;color:var(--text-light)}._textBlockCompactFloat_16dib_293{float:right;margin-left:6px;margin-bottom:2px}._thinkingBlock_16dib_300{margin-bottom:8px;border:1px solid var(--color-thinking-border);border-radius:6px;overflow:hidden;background:var(--color-thinking-bg)}._thinkingHeader_16dib_308{display:flex;align-items:center;gap:6px;padding:5px 10px;cursor:pointer;font-size:12px;color:var(--text-tertiary);transition:background .15s}._thinkingHeader_16dib_308:hover{background:var(--overlay-light-faint)}._thinkingPreview_16dib_323{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-muted);font-size:11px}._thinkingBody_16dib_333{padding:8px 12px;border-top:1px solid var(--color-thinking-border)}._toolBlock_16dib_339{margin-bottom:8px;border:1px solid var(--border-primary);border-radius:6px;overflow:hidden}._toolBlockResult_16dib_346{border-color:var(--color-green-border)}._toolBlockError_16dib_350{border-color:var(--color-red-dark-border)}._toolBlockHeader_16dib_354{display:flex;align-items:center;gap:8px;padding:5px 10px;background:var(--bg-container);border-bottom:1px solid var(--border-primary);font-size:12px;flex-wrap:wrap}._toolBlockBody_16dib_365{padding:8px 10px;font-size:12px}._toolName_16dib_370{color:var(--text-primary);font-weight:500;font-family:monospace}._toolId_16dib_376{color:var(--text-disabled);font-size:10px;font-family:monospace;margin-left:auto}._errorLabel_16dib_383{font-size:10px;color:var(--color-error-light);background:var(--color-error-bg-light);border:1px solid var(--color-error-border);border-radius:3px;padding:1px 5px}._blockTag_16dib_393{font-size:9px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;padding:1px 5px;border-radius:3px;background:var(--bg-elevated);color:var(--text-muted);border:1px solid var(--border-primary);flex-shrink:0}._blockTagText_16dib_406{background:var(--color-primary-bg-faint);color:var(--color-primary-lighter);border-color:var(--color-primary-bg-lighter)}._blockTagThinking_16dib_412{background:var(--color-warning-bg-faint);color:var(--color-warning);border-color:var(--color-warning-border-light)}._blockTagResult_16dib_418{background:var(--color-green-dark-bg);color:var(--color-success);border-color:var(--color-green-dark-border)}._blockTagError_16dib_424{background:var(--color-error-bg-faint);color:var(--color-error-light);border-color:var(--color-error-border-light)}._jsonBlock_16dib_431{margin-bottom:8px;border:1px solid var(--border-primary);border-radius:6px;overflow:hidden}._jsonBlockLabel_16dib_438{font-size:10px;color:var(--text-muted);padding:3px 10px;background:var(--bg-container);border-bottom:1px solid var(--border-primary);font-family:monospace}._blockSeparator_16dib_448{border:none;border-top:1px solid var(--border-primary);margin:16px 0}._markdownBody_16dib_455{font-size:13px;line-height:1.7;color:var(--text-primary);word-break:break-word}._contentToolbar_16dib_463{display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:8px 16px 0;flex-shrink:0}._contentToolbarLabel_16dib_472{font-size:11px;color:var(--text-muted)}._rawPre_16dib_478{background:var(--bg-code-dark);border:1px solid var(--border-primary);border-radius:6px;padding:12px;font-size:12px;color:var(--text-primary);white-space:pre-wrap;word-break:break-all;margin:0}._toolAdded_16dib_491{color:var(--color-success)}._toolRemoved_16dib_495{color:var(--color-error);text-decoration:line-through}._toolDiffTag_16dib_500{margin-left:6px;font-size:9px;font-family:sans-serif;text-decoration:none;opacity:.85}._toolDiffSummary_16dib_508{margin-left:6px;font-size:11px;font-family:monospace;display:inline-flex;gap:4px;flex-shrink:0}._toolDiffSummaryAdd_16dib_517{color:var(--color-success)}._toolDiffSummaryRemove_16dib_521{color:var(--color-error)}._container_rg6mx_1{height:100%;overflow:hidden;padding:0 16px;display:flex;flex-direction:column;background:var(--bg-base)}._emptyState_rg6mx_10{display:flex;align-items:center;justify-content:center;height:100%}._urlSection_rg6mx_17{padding:12px 0;border-bottom:1px solid var(--border-primary);display:flex;align-items:flex-start;flex-shrink:0}._urlLeft_rg6mx_25{flex:1;min-width:0}._tokenStatsBox_rg6mx_30{flex-shrink:0;padding-left:12px;display:flex;align-items:center}._tokenGrid_rg6mx_37{display:flex;border:1px solid var(--border-secondary);border-radius:6px;overflow:hidden;min-width:360px;font-size:11px;line-height:1.6}._tokenRows_rg6mx_47{flex:1}._tokenRow_rg6mx_47{display:flex}._tokenRowBorder_rg6mx_55{border-top:1px solid var(--border-secondary)}._tokenLabel_rg6mx_59{color:var(--text-tertiary);padding:4px 8px;white-space:nowrap;font-weight:600}._tokenTd_rg6mx_66{flex:1;color:var(--text-primary);text-align:right;padding:4px 8px;font-family:monospace;white-space:nowrap}._tokenHitRate_rg6mx_75{display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--text-primary);padding:4px 8px;font-family:monospace;white-space:nowrap;border-left:1px solid var(--border-secondary);min-width:100px}._tokenHitRateLabel_rg6mx_88{color:var(--text-tertiary);font-size:10px;font-family:sans-serif}._tokenRowBorder_rg6mx_55 td{border-top:1px solid var(--border-secondary)}._urlText_rg6mx_98{color:var(--text-primary);font-size:13px;margin-bottom:8px;word-break:break-all}._metaText_rg6mx_105,._headersContainer_rg6mx_109{font-size:12px}._headerRow_rg6mx_113{display:flex;padding:4px 0;border-bottom:1px solid var(--border-primary)}._headerKey_rg6mx_119{min-width:200px;flex-shrink:0}._headerValue_rg6mx_124{word-break:break-all;margin-left:8px}._streamingBox_rg6mx_129{padding:20px;background:var(--bg-elevated);border-radius:6px;border:1px solid var(--border-primary)}._bodyToolbar_rg6mx_136{display:flex;gap:8px;margin-bottom:8px}._rawTextPre_rg6mx_142{background:var(--bg-code-dark);border:1px solid var(--border-primary);border-radius:6px;padding:12px;font-size:12px;color:var(--text-primary);overflow:auto;max-height:600px;white-space:pre-wrap;word-break:break-all}._tabContent_rg6mx_155{padding:16px 0 0;height:100%;overflow-y:auto;-webkit-overflow-scrolling:touch}._collapseSpacing_rg6mx_163{margin-bottom:16px}._bodyLabel_rg6mx_167{margin:0}._bodyHeader_rg6mx_171{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}._diffSection_rg6mx_178{margin-bottom:16px}._diffToggle_rg6mx_182{display:inline-block;margin-bottom:8px;cursor:pointer}._diffIcon_rg6mx_188{font-size:12px;margin-left:4px}._viewInChatBtn_rg6mx_193{display:inline-flex;align-items:center;height:26px;border-radius:13px;border:1px solid var(--border-light);background:#0000;color:var(--text-tertiary);cursor:pointer;font-size:12px;transition:all .2s;padding:0 10px;white-space:nowrap}._viewInChatBtn_rg6mx_193:hover{border-color:var(--text-muted);color:var(--text-primary);background:var(--overlay-light-faint)}._reminderSelect_rg6mx_214{min-width:140px;font-size:12px}._reminderSelect_rg6mx_214 .ant-select-selector.ant-select-selector{border-radius:2px;border-color:var(--border-light);background:#0000;min-height:26px;height:auto;padding:0 8px;font-family:monospace}._reminderSelect_rg6mx_214 .ant-select-selection-placeholder{font-size:11px}._diffHeaderRow_rg6mx_233{display:flex;align-items:center;gap:8px}._diffSpaceRight_rg6mx_239{margin-left:auto}._reminderFilterWrapper_rg6mx_243{display:inline-flex;align-items:center;gap:4px}._reminderLabel_rg6mx_249{color:var(--text-tertiary);font-size:12px;font-family:monospace}._cacheTabContent_rg6mx_255{padding-top:0;overflow:hidden}._userPromptList_rg6mx_260{width:600px;max-height:300px;overflow-y:auto}._userPromptItem_rg6mx_266{padding:4px 8px;font-size:12px;color:var(--text-secondary);cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;border-radius:3px}._userPromptNavBtn_rg6mx_277{margin-left:auto;font-size:11px;color:var(--color-primary);cursor:pointer;border:1px solid var(--color-primary);border-radius:3px;padding:1px 6px;white-space:nowrap}._cacheContent_rg6mx_288{padding:8px 0;height:100%;display:flex;flex-direction:column}._cacheTokenBar_rg6mx_295{display:flex;align-items:center;font-size:12px;font-family:monospace;color:var(--text-light);margin-bottom:12px;flex-shrink:0}._cacheTokenWrite_rg6mx_305{color:var(--color-code-orange)}._cacheTokenRead_rg6mx_309{color:var(--color-success)}._cacheCopyIcon_rg6mx_313{margin-left:8px;cursor:pointer;color:var(--text-tertiary);transition:color .2s}._cacheScrollArea_rg6mx_320{flex:1;overflow-y:auto;min-height:0}._cacheSectionBlock_rg6mx_326{margin-bottom:12px}._cacheSectionHeader_rg6mx_330{font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:6px;cursor:pointer;-webkit-user-select:none;user-select:none;display:flex;align-items:center;gap:4px}._cacheCollapseArrow_rg6mx_342{display:inline-block;transition:transform .2s;font-size:10px}._cachePre_rg6mx_348{white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.5;color:var(--text-secondary);background:var(--bg-container);border:1px solid var(--border-primary);border-radius:4px;padding:8px;margin:4px 0;font-family:Menlo,Monaco,monospace}._cacheHighlightSvg_rg6mx_362{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;pointer-events:none;overflow:visible}._cachePreSystem_rg6mx_371{white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.5;color:var(--text-secondary);background:var(--bg-code-system);border:1px solid var(--border-code-system);border-radius:4px;padding:8px;margin:4px 0;font-family:Menlo,Monaco,monospace}._container_rg6mx_1 .ant-tabs>.ant-tabs-nav{margin-bottom:0}._container_rg6mx_1 .ant-tabs{flex:1;min-height:0;display:flex;flex-direction:column}._container_rg6mx_1 .ant-tabs>.ant-tabs-content-holder{flex:1;min-height:0}._container_rg6mx_1 .ant-tabs-content,._container_rg6mx_1 .ant-tabs-tabpane-active{height:100%}._resizer_pzn4b_1{width:6px;cursor:col-resize;background:var(--bg-elevated);flex-shrink:0;transition:background .2s}._resizer_pzn4b_1:hover{background:var(--color-primary-light)}._flag_1q4ri_3{display:inline-flex;align-items:center;justify-content:center;font-size:13px;line-height:1;cursor:help;-webkit-user-select:none;user-select:none;height:14px;background:none;border:none;padding:0;color:inherit;font-family:inherit}._flag_1q4ri_3:focus-visible{outline:2px solid var(--primary-color, #1677ff);outline-offset:2px;border-radius:2px}._popover_1q4ri_27{color:var(--text-secondary);font-size:13px;line-height:22px}._meta_1q4ri_33{color:var(--text-tertiary);font-size:12px}._usagePill_18dz6_4{position:relative;display:inline-flex;align-items:center;justify-content:flex-start;border-radius:999px;border:1px solid;border-color:var(--text-disabled);color:var(--text-disabled);padding:0 7px;height:15px;font-size:11px;line-height:1;overflow:hidden;white-space:nowrap;cursor:default;background:var(--bg-base-pure)}._usageFill_18dz6_12{position:absolute;left:0;top:0;bottom:0;width:var(--usage-percent, 0);background-color:#999;opacity:.2;transition:width .5s ease;pointer-events:none}._usageContent_18dz6_38{position:relative;z-index:1;display:inline-flex;align-items:center}._usageText_18dz6_45{font-variant-numeric:tabular-nums}._muted_18dz6_50{border-color:var(--border-light);color:var(--text-disabled);background:var(--bg-surface)}._pop_18dz6_57{min-width:220px;font-size:12px;color:var(--text-primary)}._popTitle_18dz6_63{font-weight:600;margin-bottom:6px}._popTable_18dz6_69{border-collapse:collapse}._popTable_18dz6_69 td{padding-top:3px;padding-bottom:3px;vertical-align:middle}._tdName_18dz6_74{color:var(--text-secondary);padding-right:5px;white-space:nowrap}._tdBar_18dz6_74{padding-right:5px;white-space:nowrap}._tdReset_18dz6_92{color:var(--text-secondary);font-variant-numeric:tabular-nums;white-space:nowrap}._bar_18dz6_101{position:relative;display:inline-flex;align-items:center;justify-content:center;width:100px;height:14px;border-radius:999px;border:1px solid var(--text-disabled);overflow:hidden;background:var(--bg-base-pure);vertical-align:middle}._barFill_18dz6_115{position:absolute;left:0;top:0;bottom:0;background-color:var(--text-secondary);opacity:.15;pointer-events:none}._barText_18dz6_125{position:relative;z-index:1;font-size:11px;line-height:1;font-variant-numeric:tabular-nums;color:var(--text-primary)}
|