cc-viewer 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/assets/index-Bf2poirE.js +514 -0
- package/dist/assets/index-C_eOlam6.css +1 -0
- package/dist/index.html +2 -2
- package/interceptor.js +148 -9
- package/locales/ar.json +10 -1
- package/locales/da.json +10 -1
- package/locales/de.json +10 -1
- package/locales/en.json +12 -1
- package/locales/es.json +10 -1
- package/locales/fr.json +10 -1
- package/locales/it.json +10 -1
- package/locales/ja.json +10 -1
- package/locales/ko.json +10 -1
- package/locales/no.json +10 -1
- package/locales/pl.json +10 -1
- package/locales/pt-BR.json +10 -1
- package/locales/ru.json +10 -1
- package/locales/th.json +10 -1
- package/locales/tr.json +10 -1
- package/locales/uk.json +10 -1
- package/locales/zh-TW.json +10 -1
- package/locales/zh.json +12 -1
- package/package.json +1 -1
- package/server.js +154 -6
- package/dist/assets/index-C7-c9XfU.css +0 -1
- package/dist/assets/index-DZBQn9fO.js +0 -555
package/server.js
CHANGED
|
@@ -1,14 +1,44 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
-
import { readFileSync, existsSync, watchFile, unwatchFile, statSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, watchFile, unwatchFile, statSync, readdirSync, renameSync } from 'node:fs';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { dirname, join, extname, basename } from 'node:path';
|
|
5
|
-
import { homedir } from 'node:os';
|
|
6
|
-
import {
|
|
5
|
+
import { homedir, userInfo, platform } from 'node:os';
|
|
6
|
+
import { execSync } from 'node:child_process';
|
|
7
|
+
import { LOG_FILE, _initPromise, _resumeState, resolveResumeChoice } from './interceptor.js';
|
|
7
8
|
import { t } from './i18n.js';
|
|
8
9
|
|
|
9
10
|
const LOG_DIR = join(homedir(), '.claude', 'cc-viewer');
|
|
11
|
+
const PREFS_FILE = join(LOG_DIR, 'preferences.json');
|
|
10
12
|
const SHOW_ALL_FILE = '/tmp/cc-viewer-show-all';
|
|
11
13
|
|
|
14
|
+
// macOS user profile (avatar + display name), cached once
|
|
15
|
+
let _userProfile = null;
|
|
16
|
+
function getUserProfile() {
|
|
17
|
+
if (_userProfile) return _userProfile;
|
|
18
|
+
const info = userInfo();
|
|
19
|
+
const name = info.username || 'User';
|
|
20
|
+
let displayName = name;
|
|
21
|
+
let avatarBase64 = null;
|
|
22
|
+
|
|
23
|
+
if (platform() === 'darwin') {
|
|
24
|
+
try {
|
|
25
|
+
const rn = execSync(`dscl . -read /Users/${name} RealName`, { encoding: 'utf-8', timeout: 3000 });
|
|
26
|
+
const match = rn.match(/RealName:\n?\s*(.+)/);
|
|
27
|
+
if (match && match[1].trim()) displayName = match[1].trim();
|
|
28
|
+
} catch {}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const buf = execSync(`dscl . -read /Users/${name} JPEGPhoto | tail -1 | xxd -r -p`, { timeout: 5000, maxBuffer: 1024 * 1024 });
|
|
32
|
+
if (buf && buf.length > 100) {
|
|
33
|
+
avatarBase64 = `data:image/jpeg;base64,${buf.toString('base64')}`;
|
|
34
|
+
}
|
|
35
|
+
} catch {}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_userProfile = { name: displayName, avatar: avatarBase64 };
|
|
39
|
+
return _userProfile;
|
|
40
|
+
}
|
|
41
|
+
|
|
12
42
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
43
|
const __dirname = dirname(__filename);
|
|
14
44
|
const START_PORT = 7008;
|
|
@@ -85,6 +115,11 @@ function watchLogFile(logFile) {
|
|
|
85
115
|
}
|
|
86
116
|
});
|
|
87
117
|
}
|
|
118
|
+
|
|
119
|
+
// 检测日志文件是否已轮转到新文件
|
|
120
|
+
if (LOG_FILE !== logFile && !watchedFiles.has(LOG_FILE)) {
|
|
121
|
+
watchLogFile(LOG_FILE);
|
|
122
|
+
}
|
|
88
123
|
} catch (err) {
|
|
89
124
|
// File not yet created, will retry on next poll
|
|
90
125
|
}
|
|
@@ -109,6 +144,35 @@ function handleRequest(req, res) {
|
|
|
109
144
|
return;
|
|
110
145
|
}
|
|
111
146
|
|
|
147
|
+
// User preferences API
|
|
148
|
+
if (url === '/api/preferences' && method === 'GET') {
|
|
149
|
+
let prefs = {};
|
|
150
|
+
try { if (existsSync(PREFS_FILE)) prefs = JSON.parse(readFileSync(PREFS_FILE, 'utf-8')); } catch {}
|
|
151
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
152
|
+
res.end(JSON.stringify(prefs));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (url === '/api/preferences' && method === 'POST') {
|
|
157
|
+
let body = '';
|
|
158
|
+
req.on('data', chunk => { body += chunk; });
|
|
159
|
+
req.on('end', () => {
|
|
160
|
+
try {
|
|
161
|
+
const incoming = JSON.parse(body);
|
|
162
|
+
let prefs = {};
|
|
163
|
+
try { if (existsSync(PREFS_FILE)) prefs = JSON.parse(readFileSync(PREFS_FILE, 'utf-8')); } catch {}
|
|
164
|
+
Object.assign(prefs, incoming);
|
|
165
|
+
writeFileSync(PREFS_FILE, JSON.stringify(prefs, null, 2));
|
|
166
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
167
|
+
res.end(JSON.stringify(prefs));
|
|
168
|
+
} catch {
|
|
169
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
170
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
112
176
|
// 注册新的日志文件进行 watch(供新进程复用旧服务时调用)
|
|
113
177
|
if (url === '/api/register-log' && method === 'POST') {
|
|
114
178
|
let body = '';
|
|
@@ -132,6 +196,50 @@ function handleRequest(req, res) {
|
|
|
132
196
|
return;
|
|
133
197
|
}
|
|
134
198
|
|
|
199
|
+
// 用户选择继续/新开日志
|
|
200
|
+
if (url === '/api/resume-choice' && method === 'POST') {
|
|
201
|
+
let body = '';
|
|
202
|
+
req.on('data', chunk => { body += chunk; });
|
|
203
|
+
req.on('end', () => {
|
|
204
|
+
try {
|
|
205
|
+
const { choice } = JSON.parse(body);
|
|
206
|
+
if (choice !== 'continue' && choice !== 'new') {
|
|
207
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
208
|
+
res.end(JSON.stringify({ error: 'Invalid choice' }));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const result = resolveResumeChoice(choice);
|
|
212
|
+
if (!result) {
|
|
213
|
+
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
214
|
+
res.end(JSON.stringify({ error: 'Already resolved' }));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
// 重新 watch 最终的日志文件
|
|
218
|
+
watchLogFile(result.logFile);
|
|
219
|
+
// 广播 resume_resolved + full_reload
|
|
220
|
+
const resolvedData = JSON.stringify({ logFile: result.logFile });
|
|
221
|
+
clients.forEach(client => {
|
|
222
|
+
try {
|
|
223
|
+
client.write(`event: resume_resolved\ndata: ${resolvedData}\n\n`);
|
|
224
|
+
} catch {}
|
|
225
|
+
});
|
|
226
|
+
// 发送 full_reload 让客户端重新加载数据
|
|
227
|
+
const entries = readLogFile();
|
|
228
|
+
clients.forEach(client => {
|
|
229
|
+
try {
|
|
230
|
+
client.write(`event: full_reload\ndata: ${JSON.stringify(entries)}\n\n`);
|
|
231
|
+
} catch {}
|
|
232
|
+
});
|
|
233
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
234
|
+
res.end(JSON.stringify({ ok: true, logFile: result.logFile }));
|
|
235
|
+
} catch {
|
|
236
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
237
|
+
res.end(JSON.stringify({ error: 'Invalid request body' }));
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
135
243
|
// SSE endpoint
|
|
136
244
|
if (url === '/events' && method === 'GET') {
|
|
137
245
|
res.writeHead(200, {
|
|
@@ -142,6 +250,11 @@ function handleRequest(req, res) {
|
|
|
142
250
|
|
|
143
251
|
clients.push(res);
|
|
144
252
|
|
|
253
|
+
// 如果有待决的 resume 选择,发送 resume_prompt 事件
|
|
254
|
+
if (_resumeState) {
|
|
255
|
+
res.write(`event: resume_prompt\ndata: ${JSON.stringify({ recentFileName: _resumeState.recentFileName })}\n\n`);
|
|
256
|
+
}
|
|
257
|
+
|
|
145
258
|
const entries = readLogFile();
|
|
146
259
|
entries.forEach(entry => {
|
|
147
260
|
res.write(`data: ${JSON.stringify(entry)}\n\n`);
|
|
@@ -169,6 +282,14 @@ function handleRequest(req, res) {
|
|
|
169
282
|
return;
|
|
170
283
|
}
|
|
171
284
|
|
|
285
|
+
// macOS 用户头像和显示名
|
|
286
|
+
if (url === '/api/user-profile' && method === 'GET') {
|
|
287
|
+
const profile = getUserProfile();
|
|
288
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
289
|
+
res.end(JSON.stringify(profile));
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
172
293
|
// 列出本地日志文件(按项目分组)
|
|
173
294
|
if (url === '/api/local-logs' && method === 'GET') {
|
|
174
295
|
try {
|
|
@@ -319,6 +440,16 @@ export async function startViewer() {
|
|
|
319
440
|
}
|
|
320
441
|
|
|
321
442
|
export function stopViewer() {
|
|
443
|
+
// 如果用户未做选择,将临时文件转为正式文件
|
|
444
|
+
if (_resumeState && _resumeState.tempFile) {
|
|
445
|
+
try {
|
|
446
|
+
const { tempFile } = _resumeState;
|
|
447
|
+
if (existsSync(tempFile)) {
|
|
448
|
+
const newPath = tempFile.replace('_temp.jsonl', '.jsonl');
|
|
449
|
+
renameSync(tempFile, newPath);
|
|
450
|
+
}
|
|
451
|
+
} catch {}
|
|
452
|
+
}
|
|
322
453
|
for (const logFile of watchedFiles.keys()) {
|
|
323
454
|
unwatchFile(logFile);
|
|
324
455
|
}
|
|
@@ -330,7 +461,24 @@ export function stopViewer() {
|
|
|
330
461
|
}
|
|
331
462
|
}
|
|
332
463
|
|
|
333
|
-
// Auto-start the viewer
|
|
334
|
-
|
|
335
|
-
|
|
464
|
+
// Auto-start the viewer after log file init completes
|
|
465
|
+
_initPromise.then(() => {
|
|
466
|
+
startViewer().catch(err => {
|
|
467
|
+
console.error('Failed to start CC Viewer:', err);
|
|
468
|
+
});
|
|
336
469
|
});
|
|
470
|
+
|
|
471
|
+
// 进程退出时,将未决的临时文件转为正式文件
|
|
472
|
+
function handleExit() {
|
|
473
|
+
if (_resumeState && _resumeState.tempFile) {
|
|
474
|
+
try {
|
|
475
|
+
if (existsSync(_resumeState.tempFile)) {
|
|
476
|
+
const newPath = _resumeState.tempFile.replace('_temp.jsonl', '.jsonl');
|
|
477
|
+
renameSync(_resumeState.tempFile, newPath);
|
|
478
|
+
}
|
|
479
|
+
} catch {}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
process.on('exit', handleExit);
|
|
483
|
+
process.on('SIGINT', () => { handleExit(); process.exit(); });
|
|
484
|
+
process.on('SIGTERM', () => { handleExit(); process.exit(); });
|
|
@@ -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}._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}
|