cc-viewer 1.6.253 → 1.6.255

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/index.html CHANGED
@@ -19,7 +19,7 @@
19
19
  if (pick) document.documentElement.setAttribute('data-theme', pick);
20
20
  } catch {}
21
21
  </script>
22
- <script type="module" crossorigin src="/assets/index-eTBzi5lA.js"></script>
22
+ <script type="module" crossorigin src="/assets/index-mBNBJDft.js"></script>
23
23
  <link rel="modulepreload" crossorigin href="/assets/vendor-antd-BeN8xqGk.js">
24
24
  <link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-2nbmPewy.js">
25
25
  <link rel="modulepreload" crossorigin href="/assets/vendor-mdxeditor-C7DYEBoH.js">
@@ -0,0 +1,102 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { basename, join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { getClaudeConfigDir } from '../findcc.js';
5
+
6
+ export const KEEP_CLAUDE_NO_FLICKER_ENV = 'CCV_KEEP_CLAUDE_CODE_NO_FLICKER';
7
+
8
+ function shouldKeepClaudeNoFlicker(env = {}, sourceEnv = process.env) {
9
+ return env[KEEP_CLAUDE_NO_FLICKER_ENV] === '1' || sourceEnv?.[KEEP_CLAUDE_NO_FLICKER_ENV] === '1';
10
+ }
11
+
12
+ export function stripClaudeNoFlickerUnlessOptedIn(env, sourceEnv = process.env) {
13
+ if (!shouldKeepClaudeNoFlicker(env, sourceEnv)) {
14
+ delete env.CLAUDE_CODE_NO_FLICKER;
15
+ }
16
+ return env;
17
+ }
18
+
19
+ function ensureWrapperFile(dir, fileName, content) {
20
+ mkdirSync(dir, { recursive: true });
21
+ const file = join(dir, fileName);
22
+ writeFileSync(file, content, { mode: 0o600 });
23
+ return file;
24
+ }
25
+
26
+ function defaultRcDir() {
27
+ return join(getClaudeConfigDir(), 'cc-viewer', 'shell-rc');
28
+ }
29
+
30
+ /**
31
+ * Prepare an interactive shell for cc-viewer's embedded terminals.
32
+ *
33
+ * Deleting CLAUDE_CODE_NO_FLICKER from the spawned env is enough for direct
34
+ * Claude spawns, but not for scratch/fallback shells: users often export the
35
+ * variable from ~/.zshrc or ~/.bashrc, so the shell would recreate it before
36
+ * they type `claude`. For those shells we install a tiny rc wrapper that loads
37
+ * the user's normal rc file and then unsets the variable again.
38
+ */
39
+ export function prepareEmbeddedShellSpawn(shell, env, options = {}) {
40
+ const sourceEnv = options.sourceEnv || process.env;
41
+ const keep = shouldKeepClaudeNoFlicker(env, sourceEnv);
42
+ stripClaudeNoFlickerUnlessOptedIn(env, sourceEnv);
43
+ if (keep) return { command: shell, args: [], env };
44
+
45
+ const shellBase = basename(shell);
46
+ const homeDir = options.homeDir || homedir();
47
+ const rcDir = options.rcDir || defaultRcDir();
48
+
49
+ if (shellBase === 'zsh') {
50
+ const zshEnvWrapper = [
51
+ '# Generated by cc-viewer. Do not edit.',
52
+ '__ccv_wrapper_zdotdir="${ZDOTDIR:-$HOME}"',
53
+ '__ccv_original_zdotdir="${CCV_ORIGINAL_ZDOTDIR:-$HOME}"',
54
+ 'if [[ "$__ccv_original_zdotdir" != "$__ccv_wrapper_zdotdir" && -r "$__ccv_original_zdotdir/.zshenv" ]]; then',
55
+ ' source "$__ccv_original_zdotdir/.zshenv"',
56
+ 'fi',
57
+ 'export CCV_EFFECTIVE_ZDOTDIR="${ZDOTDIR:-$__ccv_original_zdotdir}"',
58
+ 'export ZDOTDIR="$__ccv_wrapper_zdotdir"',
59
+ 'unset __ccv_original_zdotdir __ccv_wrapper_zdotdir',
60
+ '',
61
+ ].join('\n');
62
+ const zshRcWrapper = [
63
+ '# Generated by cc-viewer. Do not edit.',
64
+ '__ccv_wrapper_zdotdir="$ZDOTDIR"',
65
+ '__ccv_original_zdotdir="${CCV_EFFECTIVE_ZDOTDIR:-${CCV_ORIGINAL_ZDOTDIR:-$HOME}}"',
66
+ 'if [[ "$__ccv_original_zdotdir" != "$__ccv_wrapper_zdotdir" && -r "$__ccv_original_zdotdir/.zshrc" ]]; then',
67
+ ' ZDOTDIR="$__ccv_original_zdotdir"',
68
+ ' source "$__ccv_original_zdotdir/.zshrc"',
69
+ ' export ZDOTDIR="$__ccv_wrapper_zdotdir"',
70
+ 'fi',
71
+ 'unset CLAUDE_CODE_NO_FLICKER',
72
+ 'unset CCV_EFFECTIVE_ZDOTDIR',
73
+ 'unset __ccv_original_zdotdir __ccv_wrapper_zdotdir',
74
+ '',
75
+ ].join('\n');
76
+ ensureWrapperFile(rcDir, '.zshenv', zshEnvWrapper);
77
+ ensureWrapperFile(rcDir, '.zshrc', zshRcWrapper);
78
+ env.CCV_ORIGINAL_ZDOTDIR = env.ZDOTDIR || homeDir;
79
+ env.ZDOTDIR = rcDir;
80
+ return { command: shell, args: [], env };
81
+ }
82
+
83
+ if (shellBase === 'bash') {
84
+ const wrapper = [
85
+ '# Generated by cc-viewer. Do not edit.',
86
+ 'if [ -r "${CCV_ORIGINAL_BASHRC:-$HOME/.bashrc}" ]; then',
87
+ ' . "${CCV_ORIGINAL_BASHRC:-$HOME/.bashrc}"',
88
+ 'fi',
89
+ 'unset CLAUDE_CODE_NO_FLICKER',
90
+ '',
91
+ ].join('\n');
92
+ const rcFile = ensureWrapperFile(rcDir, 'bashrc', wrapper);
93
+ env.CCV_ORIGINAL_BASHRC = env.CCV_ORIGINAL_BASHRC || join(homeDir, '.bashrc');
94
+ return { command: shell, args: ['--rcfile', rcFile, '-i'], env };
95
+ }
96
+
97
+ if (shellBase === 'fish') {
98
+ return { command: shell, args: ['--init-command', 'set -e CLAUDE_CODE_NO_FLICKER'], env };
99
+ }
100
+
101
+ return { command: shell, args: [], env };
102
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-viewer",
3
- "version": "1.6.253",
3
+ "version": "1.6.255",
4
4
  "description": "Claude Code Logger visualization management tool",
5
5
  "license": "MIT",
6
6
  "main": "server.js",
@@ -20,7 +20,8 @@
20
20
  "build:sourcemap": "CCV_SOURCEMAP=1 node build.js",
21
21
  "start": "node server.js",
22
22
  "test": "CCV_LOG_DIR=tmp node --test",
23
- "test:coverage": "CCV_LOG_DIR=tmp node --test --experimental-test-coverage --test-coverage-include='lib/*.js' --test-coverage-include='*.js'",
23
+ "test:coverage": "CCV_LOG_DIR=tmp node --test --experimental-test-coverage --test-coverage-include='lib/*.js' --test-coverage-include='src/utils/*.js' --test-coverage-include='*.js'",
24
+ "test:coverage:html": "CCV_LOG_DIR=tmp c8 --reporter=text-summary --reporter=html node --test",
24
25
  "prepublishOnly": "npm run build",
25
26
  "electron:dev": "electron electron/main.js",
26
27
  "electron:build": "npm run build && electron-builder",
@@ -88,6 +89,7 @@
88
89
  "@xterm/addon-webgl": "^0.19.0",
89
90
  "@xterm/xterm": "^6.0.0",
90
91
  "antd": "^5.29.2",
92
+ "c8": "^11.0.0",
91
93
  "diff": "^8.0.3",
92
94
  "electron": "^35.1.2",
93
95
  "electron-builder": "^26.0.12",
@@ -113,5 +115,26 @@
113
115
  },
114
116
  "optionalDependencies": {
115
117
  "@anthropic-ai/claude-agent-sdk": "^0.2.91"
118
+ },
119
+ "c8": {
120
+ "include": [
121
+ "lib/**/*.js",
122
+ "src/utils/**/*.js",
123
+ "*.js"
124
+ ],
125
+ "exclude": [
126
+ "test/**",
127
+ "dist/**",
128
+ "node_modules/**",
129
+ "build.js",
130
+ "vite.config.js",
131
+ "electron/**",
132
+ "scripts/**",
133
+ "coverage/**"
134
+ ],
135
+ "all": true,
136
+ "report-dir": "coverage",
137
+ "skip-full": false,
138
+ "check-coverage": false
116
139
  }
117
140
  }
package/pty-manager.js CHANGED
@@ -3,6 +3,7 @@ import { fileURLToPath } from 'node:url';
3
3
  import { join, dirname } from 'node:path';
4
4
  import { chmodSync, statSync } from 'node:fs';
5
5
  import { platform, arch } from 'node:os';
6
+ import { prepareEmbeddedShellSpawn, stripClaudeNoFlickerUnlessOptedIn } from './lib/terminal-env.js';
6
7
 
7
8
  const __filename = fileURLToPath(import.meta.url);
8
9
  const __dirname = dirname(__filename);
@@ -147,6 +148,9 @@ export async function spawnClaude(proxyPort, cwd, extraArgs = [], claudePath = n
147
148
  env.CCV_LOG_DIR = LOG_DIR; // 让 fork 出的 Claude Code 进程找到同一份 profile.json 等资源
148
149
  // 剥离 cc-viewer 的内部短路开关,避免泄漏给 claude 子进程
149
150
  delete env.CCV_SKIP_THINKING_DISPLAY;
151
+ // Claude Code NO_FLICKER 会让嵌入式 xterm 走 alt-screen 并丢失 scrollback。
152
+ // cc-viewer 默认剥离继承值;确实需要时可显式设 CCV_KEEP_CLAUDE_CODE_NO_FLICKER=1。
153
+ stripClaudeNoFlickerUnlessOptedIn(env);
150
154
 
151
155
  // Resolve real Node.js path (Electron's process.execPath is the Electron binary)
152
156
  let nodePath = process.execPath;
@@ -173,8 +177,6 @@ export async function spawnClaude(proxyPort, cwd, extraArgs = [], claudePath = n
173
177
  // 禁用 Claude Code CLI 的鼠标事件捕获,保住 xterm 面板原生文本选中(复制粘贴)。
174
178
  // 不设时 Claude 会启 SGR mouse tracking (DECSET ?1000/1006),抢走 xterm 的鼠标事件。
175
179
  // ??= 尊重用户显式 export(比如调试时想看 mouse event)。
176
- // 注意:NO_FLICKER 此处**故意**不注入——它会强制 alt-screen 销毁 xterm scrollback;
177
- // 需要闪烁优化的用户自行 `export CLAUDE_CODE_NO_FLICKER=1`。
178
180
  env.CLAUDE_CODE_DISABLE_MOUSE ??= '1';
179
181
 
180
182
  // 通过 --settings 注入 ANTHROPIC_BASE_URL,确保覆盖 settings.json 中的配置。
@@ -353,15 +355,16 @@ export async function spawnShell() {
353
355
  delete shellEnv.CCVIEWER_PORT;
354
356
  delete shellEnv.CCV_EDITOR_PORT;
355
357
  delete shellEnv.CCVIEWER_PROTOCOL;
356
- // 交互 shell 里手动敲 claude 时也禁鼠标,理由同 spawnClaude;NO_FLICKER 仍不注入
358
+ // 交互 shell 里手动敲 claude 时也禁鼠标,理由同 spawnClaude
357
359
  shellEnv.CLAUDE_CODE_DISABLE_MOUSE ??= '1';
360
+ const shellSpawn = prepareEmbeddedShellSpawn(shell, shellEnv);
358
361
 
359
- ptyProcess = pty.spawn(shell, [], {
362
+ ptyProcess = pty.spawn(shellSpawn.command, shellSpawn.args, {
360
363
  name: 'xterm-256color',
361
364
  cols: lastPtyCols,
362
365
  rows: lastPtyRows,
363
366
  cwd,
364
- env: shellEnv,
367
+ env: shellSpawn.env,
365
368
  });
366
369
 
367
370
  ptyProcess.onData((data) => {
@@ -2,6 +2,7 @@ import { fileURLToPath } from 'node:url';
2
2
  import { dirname, join, basename } from 'node:path';
3
3
  import { chmodSync, statSync } from 'node:fs';
4
4
  import { platform, arch, homedir } from 'node:os';
5
+ import { prepareEmbeddedShellSpawn } from './lib/terminal-env.js';
5
6
 
6
7
  const __filename = fileURLToPath(import.meta.url);
7
8
  const __dirname = dirname(__filename);
@@ -134,16 +135,17 @@ export async function spawnScratch(id) {
134
135
  }
135
136
  delete env.ANTHROPIC_BASE_URL;
136
137
  env.CLAUDE_CODE_DISABLE_MOUSE ??= '1';
138
+ const shellSpawn = prepareEmbeddedShellSpawn(shell, env);
137
139
 
138
140
  s.lastExitCode = null;
139
141
  s.outputBuffer = '';
140
142
 
141
- s.ptyProcess = pty.spawn(shell, [], {
143
+ s.ptyProcess = pty.spawn(shellSpawn.command, shellSpawn.args, {
142
144
  name: 'xterm-256color',
143
145
  cols: s.lastCols,
144
146
  rows: s.lastRows,
145
147
  cwd: STARTUP_CWD,
146
- env,
148
+ env: shellSpawn.env,
147
149
  });
148
150
 
149
151
  s.ptyProcess.onData((data) => {
package/server.js CHANGED
@@ -59,6 +59,8 @@ function getPrefsFile() { return join(LOG_DIR, 'preferences.json'); }
59
59
 
60
60
  // 启动时一次性读取 ~/.claude/settings.json(不 watch)
61
61
  let claudeSettings = {};
62
+ // SSR theme 注入自检状态:模板缺 data-theme 时仅首次 warn(避免高 QPS 刷屏)
63
+ let _ssrThemeAttrWarned = false;
62
64
  try {
63
65
  const settingsPath = join(getClaudeConfigDir(), 'settings.json');
64
66
  if (existsSync(settingsPath)) {
@@ -2382,6 +2384,10 @@ async function handleRequest(req, res) {
2382
2384
  return;
2383
2385
  }
2384
2386
  } else {
2387
+ // Fallback id:当 hook caller 没传 toolUseId(如老 Claude Code PreToolUse hook
2388
+ // payload 不含 tool_use_id),生成 ask_${ts}_${rnd} 占位。这个 id 与 jsonl 里
2389
+ // tool_use.id(toolu_xxx)不同名,前端 portal 决策必须按 ask_* 前缀通配命中。
2390
+ // 协议锚点:src/utils/askPortalMatcher.js — 改此处前缀格式必须同步改 matcher。
2385
2391
  do { id = `ask_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; } while (pendingAskHooks.has(id));
2386
2392
  }
2387
2393
 
@@ -3618,6 +3624,13 @@ async function handleRequest(req, res) {
3618
3624
  // 等 React 拿到 prefs 才切回 dark,肉眼可见一次"白闪"。
3619
3625
  // 这里把当前 prefs 里的 themeColor 直接写进 HTML,inline boot script 仍负责
3620
3626
  // 处理 URL ?theme= 优先级与 localStorage 缓存。
3627
+ //
3628
+ // 主题来源优先级(首屏 → React 接管后):
3629
+ // 1. URL ?theme= (inline boot script 读取,最高优先)
3630
+ // 2. localStorage ccv_themeColor (inline boot script 读取,跨刷新缓存)
3631
+ // 3. preferences.json 的 themeColor (此处 SSR 注入到 <html data-theme="...">,老用户兜底)
3632
+ // 4. dist/index.html 模板里的硬编码 default ("light")
3633
+ // React 接管后 AppBase._applyTheme() 会基于 1/2/3 重新统一 state + DOM + localStorage 三向同步。
3621
3634
  const serveIndexHtml = () => {
3622
3635
  try {
3623
3636
  const indexPath = join(__dirname, 'dist', 'index.html');
@@ -3629,6 +3642,12 @@ async function handleRequest(req, res) {
3629
3642
  if (prefs.themeColor === 'dark' || prefs.themeColor === 'light') themeColor = prefs.themeColor;
3630
3643
  }
3631
3644
  } catch { /* 读 prefs 失败就走默认 light */ }
3645
+ // 自检:模板里没有 <html ... data-theme="..."> 时 replace 静默 no-op,SSR 优化失效但不报错。
3646
+ // 仅首次 warn 避免高 QPS 刷屏(_ssrThemeAttrWarned 单进程一次性)。
3647
+ if (!_ssrThemeAttrWarned && !/<html[^>]*data-theme="[^"]*"/.test(html)) {
3648
+ _ssrThemeAttrWarned = true;
3649
+ console.warn('[serveIndexHtml] dist/index.html 没有 <html data-theme="..."> 属性,SSR theme 注入将不生效。检查 index.html 模板。');
3650
+ }
3632
3651
  html = html.replace(/<html([^>]*?)data-theme="[^"]*"/, `<html$1data-theme="${themeColor}"`);
3633
3652
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache' });
3634
3653
  res.end(html);