cc-viewer 1.5.44 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/concepts/ar/KVCacheContent.md +42 -0
- package/concepts/da/KVCacheContent.md +42 -0
- package/concepts/de/KVCacheContent.md +42 -0
- package/concepts/en/KVCacheContent.md +42 -0
- package/concepts/es/KVCacheContent.md +42 -0
- package/concepts/fr/KVCacheContent.md +42 -0
- package/concepts/it/KVCacheContent.md +42 -0
- package/concepts/ja/KVCacheContent.md +42 -0
- package/concepts/ko/KVCacheContent.md +42 -0
- package/concepts/no/KVCacheContent.md +42 -0
- package/concepts/pl/KVCacheContent.md +42 -0
- package/concepts/pt-BR/KVCacheContent.md +42 -0
- package/concepts/ru/KVCacheContent.md +42 -0
- package/concepts/th/KVCacheContent.md +42 -0
- package/concepts/tr/KVCacheContent.md +42 -0
- package/concepts/uk/KVCacheContent.md +42 -0
- package/concepts/zh/KVCacheContent.md +42 -0
- package/concepts/zh-TW/KVCacheContent.md +42 -0
- package/dist/assets/{index-CKQx61Y7.css → index-BawBkbaU.css} +1 -1
- package/dist/assets/{index-Dp7cYMoT.js → index-D0IRVteu.js} +226 -226
- package/dist/index.html +2 -2
- package/interceptor.js +40 -12
- package/lib/context-watcher.js +7 -3
- package/lib/kv-cache-analyzer.js +155 -0
- package/lib/log-watcher.js +21 -0
- package/package.json +1 -1
- package/server.js +12 -0
package/dist/index.html
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<title>Claude Code Viewer</title>
|
|
7
7
|
<link rel="icon" href="/favicon.ico?v=1">
|
|
8
8
|
<link rel="shortcut icon" href="/favicon.ico?v=1">
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-D0IRVteu.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BawBkbaU.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
package/interceptor.js
CHANGED
|
@@ -80,6 +80,19 @@ function resolveResumeChoice(choice) {
|
|
|
80
80
|
return result;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// Teammate 子进程检测:teammate 通过 --parent-session-id 启动(提前到日志路径初始化之前)
|
|
84
|
+
const _isTeammate = process.argv.includes('--parent-session-id');
|
|
85
|
+
// 提取 teammate 元数据(--agent-name worker-1 --team-name fix-ts-errors)
|
|
86
|
+
let _teammateName = null;
|
|
87
|
+
let _teamName = null;
|
|
88
|
+
if (_isTeammate) {
|
|
89
|
+
const args = process.argv;
|
|
90
|
+
const nameIdx = args.indexOf('--agent-name');
|
|
91
|
+
if (nameIdx !== -1 && nameIdx + 1 < args.length) _teammateName = args[nameIdx + 1];
|
|
92
|
+
const teamIdx = args.indexOf('--team-name');
|
|
93
|
+
if (teamIdx !== -1 && teamIdx + 1 < args.length) _teamName = args[teamIdx + 1];
|
|
94
|
+
}
|
|
95
|
+
|
|
83
96
|
// 初始化日志文件路径(异步,支持用户交互)
|
|
84
97
|
// 工作区模式下延迟到选择工作区后再初始化
|
|
85
98
|
let _newLogFile, _logDir, _projectName;
|
|
@@ -87,6 +100,14 @@ if (process.env.CCV_WORKSPACE_MODE === '1') {
|
|
|
87
100
|
_newLogFile = '';
|
|
88
101
|
_logDir = '';
|
|
89
102
|
_projectName = '';
|
|
103
|
+
} else if (_isTeammate) {
|
|
104
|
+
// Teammate 子进程:只需 projectName 和 logDir 来查找 leader 日志,不生成新文件路径
|
|
105
|
+
let cwd;
|
|
106
|
+
try { cwd = process.cwd(); } catch { cwd = homedir(); }
|
|
107
|
+
_projectName = basename(cwd).replace(/[^a-zA-Z0-9_\-\.]/g, '_');
|
|
108
|
+
_logDir = join(LOG_DIR, _projectName);
|
|
109
|
+
const _leaderLog = findRecentLog(_logDir, _projectName);
|
|
110
|
+
_newLogFile = _leaderLog || ''; // 没有 leader 日志时不写入
|
|
90
111
|
} else {
|
|
91
112
|
({ filePath: _newLogFile, dir: _logDir, projectName: _projectName } = generateNewLogFilePath());
|
|
92
113
|
// 启动时清理残留临时文件
|
|
@@ -96,10 +117,11 @@ let LOG_FILE = _newLogFile;
|
|
|
96
117
|
|
|
97
118
|
const _initPromise = (async () => {
|
|
98
119
|
if (!_logDir || !_projectName) return; // 工作区模式下跳过
|
|
120
|
+
if (_isTeammate) return; // Teammate 已在上方同步初始化,跳过 async resume 流程
|
|
99
121
|
try {
|
|
100
122
|
const recentLog = findRecentLog(_logDir, _projectName);
|
|
101
123
|
if (recentLog) {
|
|
102
|
-
//
|
|
124
|
+
// Leader / 普通进程:走 resume 交互流程
|
|
103
125
|
const tempFile = _newLogFile.replace('.jsonl', '_temp.jsonl');
|
|
104
126
|
LOG_FILE = tempFile;
|
|
105
127
|
_resumeState = {
|
|
@@ -195,17 +217,20 @@ export function setupInterceptor() {
|
|
|
195
217
|
globalThis._ccViewerInterceptorInstalled = true;
|
|
196
218
|
|
|
197
219
|
// 启动 viewer 服务(优先根目录 server.js,fallback 到 lib/server.js)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
import(libServerPath).then(module => {
|
|
220
|
+
// Teammate 子进程跳过,避免端口冲突(leader 已启动 viewer)
|
|
221
|
+
if (!_isTeammate) {
|
|
222
|
+
const rootServerPath = join(__dirname, 'server.js');
|
|
223
|
+
const libServerPath = join(__dirname, 'lib', 'server.js');
|
|
224
|
+
import(rootServerPath).then(module => {
|
|
204
225
|
viewerModule = module;
|
|
205
226
|
}).catch(() => {
|
|
206
|
-
|
|
227
|
+
import(libServerPath).then(module => {
|
|
228
|
+
viewerModule = module;
|
|
229
|
+
}).catch(() => {
|
|
230
|
+
// Silently fail if viewer service cannot start
|
|
231
|
+
});
|
|
207
232
|
});
|
|
208
|
-
}
|
|
233
|
+
}
|
|
209
234
|
|
|
210
235
|
// 注册退出处理器
|
|
211
236
|
const cleanupViewer = async () => {
|
|
@@ -318,7 +343,8 @@ export function setupInterceptor() {
|
|
|
318
343
|
isStream: body?.stream === true,
|
|
319
344
|
isHeartbeat: /\/api\/eval\/sdk-/.test(urlStr),
|
|
320
345
|
isCountTokens: /\/messages\/count_tokens/.test(urlStr),
|
|
321
|
-
mainAgent: isMainAgentRequest(body)
|
|
346
|
+
mainAgent: isMainAgentRequest(body),
|
|
347
|
+
...(_isTeammate && { teammate: _teammateName, teamName: _teamName })
|
|
322
348
|
};
|
|
323
349
|
}
|
|
324
350
|
} catch { }
|
|
@@ -494,11 +520,13 @@ export function setupInterceptor() {
|
|
|
494
520
|
// 自动执行拦截器设置
|
|
495
521
|
// proxy 模式下(ccv CLI 或 ccv run),外层 proxy.js 已显式调用 setupInterceptor(),
|
|
496
522
|
// 这里跳过自动执行,避免 Claude 进程中重复拦截 fetch
|
|
497
|
-
|
|
523
|
+
// Teammate 子进程即使继承了 CCV_PROXY_MODE 也需要启用拦截(它是独立 claude 进程,不走 proxy)
|
|
524
|
+
if (!_ccvSkip && (!process.env.CCV_PROXY_MODE || _isTeammate)) setupInterceptor();
|
|
498
525
|
|
|
499
526
|
// 等待日志文件初始化完成后启动 Web Viewer 服务
|
|
500
527
|
// 如果是 ccv --c 通过 proxy 模式启动的,外层已有 server,跳过
|
|
501
|
-
|
|
528
|
+
// Teammate 子进程也跳过,避免端口冲突(leader 已启动 viewer)
|
|
529
|
+
if (!_ccvSkip && !process.env.CCV_PROXY_MODE && !_isTeammate) {
|
|
502
530
|
_initPromise.then(() => import('./server.js')).catch((err) => {
|
|
503
531
|
console.error('[CC-Viewer] Failed to start viewer server:', err);
|
|
504
532
|
});
|
package/lib/context-watcher.js
CHANGED
|
@@ -83,13 +83,17 @@ export function updateContextWindowFromResponse(responseBody, requestBody, model
|
|
|
83
83
|
} catch { }
|
|
84
84
|
|
|
85
85
|
// 仅更新 context_window 字段(如果 Claude Code 的 statusLine 也在写,它的数据更准确会覆盖我们的)
|
|
86
|
+
// 保留 Claude Code 写入的 context_window_size(如 1M),仅在不存在时才用推断值
|
|
87
|
+
const existingSize = existing.context_window?.context_window_size;
|
|
88
|
+
const finalSize = (existingSize && existingSize > contextWindowSize) ? existingSize : contextWindowSize;
|
|
89
|
+
const finalPct = Math.round(((inputTokens + outputTokens) / finalSize) * 100);
|
|
86
90
|
existing.context_window = {
|
|
87
91
|
total_input_tokens: inputTokens,
|
|
88
92
|
total_output_tokens: outputTokens,
|
|
89
|
-
context_window_size:
|
|
93
|
+
context_window_size: finalSize,
|
|
90
94
|
current_usage: usage,
|
|
91
|
-
used_percentage:
|
|
92
|
-
remaining_percentage: 100 -
|
|
95
|
+
used_percentage: finalPct,
|
|
96
|
+
remaining_percentage: 100 - finalPct,
|
|
93
97
|
};
|
|
94
98
|
|
|
95
99
|
writeFileSync(CONTEXT_WINDOW_FILE, JSON.stringify(existing) + '\n');
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side KV-Cache content analyzer.
|
|
3
|
+
* Ported from src/utils/helpers.js + src/utils/contentFilter.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const SUBAGENT_SYSTEM_RE = /command execution specialist|file search specialist|planning specialist|general-purpose agent/i;
|
|
7
|
+
const TEAMMATE_SYSTEM_RE = /running as an agent in a team|Agent Teammate Communication/i;
|
|
8
|
+
|
|
9
|
+
function getSystemText(body) {
|
|
10
|
+
const system = body?.system;
|
|
11
|
+
if (typeof system === 'string') return system;
|
|
12
|
+
if (Array.isArray(system)) {
|
|
13
|
+
return system.map(s => (s && s.text) || '').join('');
|
|
14
|
+
}
|
|
15
|
+
return '';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Determine if a log entry is from the MainAgent (not a teammate or subagent).
|
|
20
|
+
*/
|
|
21
|
+
export function isMainAgentEntry(entry) {
|
|
22
|
+
if (!entry) return false;
|
|
23
|
+
|
|
24
|
+
// Teammate subprocess requests are not MainAgent
|
|
25
|
+
if (entry.teammate) return false;
|
|
26
|
+
const sysText = getSystemText(entry.body || {});
|
|
27
|
+
if (TEAMMATE_SYSTEM_RE.test(sysText)) return false;
|
|
28
|
+
|
|
29
|
+
if (entry.mainAgent === true) {
|
|
30
|
+
if (SUBAGENT_SYSTEM_RE.test(sysText)) return false;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Fallback detection for entries without mainAgent flag
|
|
35
|
+
const body = entry.body || {};
|
|
36
|
+
if (!body.system || !Array.isArray(body.tools)) return false;
|
|
37
|
+
|
|
38
|
+
if (!sysText.includes('You are Claude Code')) return false;
|
|
39
|
+
if (SUBAGENT_SYSTEM_RE.test(sysText)) return false;
|
|
40
|
+
|
|
41
|
+
// New architecture (v2.1.69+): deferred tool loading
|
|
42
|
+
const isSystemArray = Array.isArray(body.system);
|
|
43
|
+
const hasToolSearch = body.tools.some(t => t.name === 'ToolSearch');
|
|
44
|
+
if (isSystemArray && hasToolSearch) {
|
|
45
|
+
const messages = body.messages || [];
|
|
46
|
+
const firstMsgContent = messages.length > 0
|
|
47
|
+
? (typeof messages[0].content === 'string' ? messages[0].content
|
|
48
|
+
: Array.isArray(messages[0].content) ? messages[0].content.map(c => c.text || '').join('') : '')
|
|
49
|
+
: '';
|
|
50
|
+
if (firstMsgContent.includes('<available-deferred-tools>')) return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Old architecture: >10 tools with core tool set
|
|
54
|
+
if (body.tools.length > 10) {
|
|
55
|
+
const hasEdit = body.tools.some(t => t.name === 'Edit');
|
|
56
|
+
const hasBash = body.tools.some(t => t.name === 'Bash');
|
|
57
|
+
const hasTaskOrAgent = body.tools.some(t => t.name === 'Task' || t.name === 'Agent');
|
|
58
|
+
if (hasEdit && hasBash && hasTaskOrAgent) return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Extract text from a tool_result content block.
|
|
66
|
+
*/
|
|
67
|
+
export function extractToolResultText(toolResult) {
|
|
68
|
+
if (!toolResult.content) return String(toolResult.content ?? '');
|
|
69
|
+
if (typeof toolResult.content === 'string') return toolResult.content;
|
|
70
|
+
if (Array.isArray(toolResult.content)) {
|
|
71
|
+
return toolResult.content
|
|
72
|
+
.filter(b => b.type === 'text')
|
|
73
|
+
.map(b => b.text)
|
|
74
|
+
.join('\n');
|
|
75
|
+
}
|
|
76
|
+
return JSON.stringify(toolResult.content);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Extract cached content from a single MainAgent log entry.
|
|
81
|
+
* Returns null if the entry is not a MainAgent entry.
|
|
82
|
+
*/
|
|
83
|
+
export function extractCachedContent(entry) {
|
|
84
|
+
if (!isMainAgentEntry(entry)) return null;
|
|
85
|
+
if (!entry.body) return null;
|
|
86
|
+
|
|
87
|
+
const body = entry.body;
|
|
88
|
+
const usage = entry.response?.body?.usage;
|
|
89
|
+
|
|
90
|
+
const result = {
|
|
91
|
+
system: [],
|
|
92
|
+
messages: [],
|
|
93
|
+
tools: [],
|
|
94
|
+
cacheCreateTokens: usage?.cache_creation_input_tokens || 0,
|
|
95
|
+
cacheReadTokens: usage?.cache_read_input_tokens || 0,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// system: find last block with cache_control, collect 0..lastIndex
|
|
99
|
+
if (Array.isArray(body.system)) {
|
|
100
|
+
let lastCacheIndex = -1;
|
|
101
|
+
for (let i = body.system.length - 1; i >= 0; i--) {
|
|
102
|
+
if (body.system[i].cache_control) { lastCacheIndex = i; break; }
|
|
103
|
+
}
|
|
104
|
+
if (lastCacheIndex >= 0) {
|
|
105
|
+
for (let i = 0; i <= lastCacheIndex; i++) {
|
|
106
|
+
const block = body.system[i];
|
|
107
|
+
if (block.type === 'text' && block.text) result.system.push(block.text);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// messages: find last message with cache_control in content, collect 0..lastIndex
|
|
113
|
+
if (Array.isArray(body.messages)) {
|
|
114
|
+
let lastCacheIndex = -1;
|
|
115
|
+
for (let i = body.messages.length - 1; i >= 0; i--) {
|
|
116
|
+
const content = body.messages[i].content;
|
|
117
|
+
if (Array.isArray(content)) {
|
|
118
|
+
for (const block of content) {
|
|
119
|
+
if (block.cache_control) { lastCacheIndex = i; break; }
|
|
120
|
+
}
|
|
121
|
+
if (lastCacheIndex >= 0) break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (lastCacheIndex >= 0) {
|
|
125
|
+
for (let i = 0; i <= lastCacheIndex; i++) {
|
|
126
|
+
const msg = body.messages[i];
|
|
127
|
+
const content = msg.content;
|
|
128
|
+
if (typeof content === 'string') {
|
|
129
|
+
result.messages.push(`[${msg.role}] ${content}`);
|
|
130
|
+
} else if (Array.isArray(content)) {
|
|
131
|
+
for (const block of content) {
|
|
132
|
+
if (block.type === 'text' && block.text) {
|
|
133
|
+
result.messages.push(`[${msg.role}] ${block.text}`);
|
|
134
|
+
} else if (block.type === 'tool_result') {
|
|
135
|
+
const toolText = extractToolResultText(block);
|
|
136
|
+
if (toolText) result.messages.push(`[tool_result: ${block.tool_use_id}] ${toolText}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// tools: if any tool has cache_control, all tools are cached
|
|
145
|
+
if (Array.isArray(body.tools)) {
|
|
146
|
+
const hasCache = body.tools.some(tool => tool.cache_control);
|
|
147
|
+
if (hasCache) {
|
|
148
|
+
for (const tool of body.tools) {
|
|
149
|
+
result.tools.push(`${tool.name}: ${tool.description || ''}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return result;
|
|
155
|
+
}
|
package/lib/log-watcher.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync, existsSync, watchFile } from 'node:fs';
|
|
2
|
+
import { isMainAgentEntry, extractCachedContent } from './kv-cache-analyzer.js';
|
|
2
3
|
|
|
3
4
|
// 跟踪所有被 watch 的日志文件
|
|
4
5
|
const watchedFiles = new Map();
|
|
@@ -51,6 +52,20 @@ export function sendToClients(clients, entry) {
|
|
|
51
52
|
});
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Send a named SSE event to all connected clients.
|
|
57
|
+
* @param {Array} clients - SSE client array
|
|
58
|
+
* @param {string} eventName - SSE event name
|
|
59
|
+
* @param {object} data - event payload
|
|
60
|
+
*/
|
|
61
|
+
export function sendEventToClients(clients, eventName, data) {
|
|
62
|
+
clients.forEach(client => {
|
|
63
|
+
try {
|
|
64
|
+
client.write(`event: ${eventName}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
65
|
+
} catch (err) {}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
54
69
|
/**
|
|
55
70
|
* Watch a log file for changes and broadcast new entries.
|
|
56
71
|
* @param {object} opts
|
|
@@ -88,6 +103,12 @@ export function watchLogFile(opts) {
|
|
|
88
103
|
}
|
|
89
104
|
sendToClients(clients, parsed);
|
|
90
105
|
runParallelHook('onNewEntry', parsed).catch(() => {});
|
|
106
|
+
if (isMainAgentEntry(parsed)) {
|
|
107
|
+
const cached = extractCachedContent(parsed);
|
|
108
|
+
if (cached) {
|
|
109
|
+
sendEventToClients(clients, 'kv_cache_content', cached);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
91
112
|
} catch (err) {
|
|
92
113
|
// Skip invalid entries
|
|
93
114
|
}
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -42,6 +42,7 @@ import { getUserProfile } from './lib/user-profile.js';
|
|
|
42
42
|
import { getGitDiffs } from './lib/git-diff.js';
|
|
43
43
|
import { watchContextWindow, CONTEXT_WINDOW_FILE } from './lib/context-watcher.js';
|
|
44
44
|
import { readLogFile, watchLogFile, startWatching, getWatchedFiles } from './lib/log-watcher.js';
|
|
45
|
+
import { isMainAgentEntry, extractCachedContent } from './lib/kv-cache-analyzer.js';
|
|
45
46
|
|
|
46
47
|
const PREFS_FILE = join(LOG_DIR, 'preferences.json');
|
|
47
48
|
const isCliMode = process.env.CCV_CLI_MODE === '1';
|
|
@@ -684,6 +685,17 @@ async function handleRequest(req, res) {
|
|
|
684
685
|
res.write(`event: full_reload\ndata: ${JSON.stringify(entriesToSend)}\n\n`);
|
|
685
686
|
}
|
|
686
687
|
|
|
688
|
+
// Compute KV-Cache content for latest MainAgent
|
|
689
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
690
|
+
if (isMainAgentEntry(entries[i])) {
|
|
691
|
+
const cached = extractCachedContent(entries[i]);
|
|
692
|
+
if (cached) {
|
|
693
|
+
res.write(`event: kv_cache_content\ndata: ${JSON.stringify(cached)}\n\n`);
|
|
694
|
+
}
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
687
699
|
req.on('close', () => {
|
|
688
700
|
const idx = clients.indexOf(res);
|
|
689
701
|
if (idx !== -1) clients.splice(idx, 1);
|