cc-viewer 1.6.1 → 1.6.2

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
@@ -6,7 +6,7 @@
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-LsDPtdaY.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-CzJcswn8.js"></script>
10
10
  <link rel="stylesheet" crossorigin href="/assets/index-7ty6PCA6.css">
11
11
  </head>
12
12
  <body>
@@ -1,4 +1,4 @@
1
- import { readFileSync, existsSync, watchFile } from 'node:fs';
1
+ import { readFileSync, existsSync, watchFile, openSync, readSync, closeSync, statSync } from 'node:fs';
2
2
  import { isMainAgentEntry, extractCachedContent } from './kv-cache-analyzer.js';
3
3
  import { buildContextWindowEvent, getContextSizeForModel } from './context-watcher.js';
4
4
 
@@ -80,25 +80,64 @@ export function sendEventToClients(clients, eventName, data) {
80
80
  export function watchLogFile(opts) {
81
81
  const { logFile, clients, getClaudePid, runParallelHook, notifyStatsWorker, getLogFile } = opts;
82
82
  if (watchedFiles.has(logFile)) return;
83
- let lastSize = 0;
83
+
84
+ // Track byte offset instead of string length — avoids full-file re-read on every poll
85
+ let lastByteOffset = 0;
86
+ let pendingTail = ''; // incomplete entry carried across polls
84
87
  try {
85
88
  if (existsSync(logFile)) {
86
- lastSize = readFileSync(logFile, 'utf-8').length;
89
+ lastByteOffset = statSync(logFile).size;
87
90
  }
88
91
  } catch {}
92
+
89
93
  watchedFiles.set(logFile, true);
90
94
  watchFile(logFile, { interval: 500 }, () => {
91
95
  try {
92
- const content = readFileSync(logFile, 'utf-8');
93
- const newContent = content.slice(lastSize);
94
- lastSize = content.length;
96
+ const currentSize = statSync(logFile).size;
97
+
98
+ // File truncated (rotation or clear) — reset offset
99
+ if (currentSize < lastByteOffset) {
100
+ lastByteOffset = 0;
101
+ pendingTail = '';
102
+ }
103
+
104
+ if (currentSize <= lastByteOffset) return;
105
+
106
+ // Read only the new bytes
107
+ const bytesToRead = currentSize - lastByteOffset;
108
+ const buf = Buffer.alloc(bytesToRead);
109
+ const fd = openSync(logFile, 'r');
110
+ try {
111
+ readSync(fd, buf, 0, bytesToRead, lastByteOffset);
112
+ } finally {
113
+ closeSync(fd);
114
+ }
115
+ lastByteOffset = currentSize;
116
+
117
+ const raw = pendingTail + buf.toString('utf-8');
118
+ const parts = raw.split('\n---\n');
119
+
120
+ // Last part may be incomplete — keep it for next poll
121
+ pendingTail = parts.pop() || '';
122
+
123
+ // If there's only the tail and no complete entries, check if tail is a complete entry
124
+ // (happens when the file ends without a trailing \n---\n)
125
+ if (parts.length === 0 && pendingTail.trim()) {
126
+ try {
127
+ JSON.parse(pendingTail);
128
+ // Valid JSON — treat as complete entry
129
+ parts.push(pendingTail);
130
+ pendingTail = '';
131
+ } catch {
132
+ // Incomplete — keep in pendingTail for next poll
133
+ }
134
+ }
95
135
 
96
- if (newContent.trim()) {
97
- const entries = newContent.split('\n---\n').filter(line => line.trim());
98
- entries.forEach(entry => {
136
+ const validParts = parts.filter(p => p.trim());
137
+ if (validParts.length > 0) {
138
+ validParts.forEach(entry => {
99
139
  try {
100
140
  const parsed = JSON.parse(entry);
101
- // 注入 Claude 进程 PID:CLI 模式从 PTY 获取,非 CLI 模式使用当前进程 PID
102
141
  if (!parsed.pid) {
103
142
  parsed.pid = getClaudePid();
104
143
  }
@@ -109,7 +148,6 @@ export function watchLogFile(opts) {
109
148
  if (cached) {
110
149
  sendEventToClients(clients, 'kv_cache_content', cached);
111
150
  }
112
- // Push context_window from usage data
113
151
  const usage = parsed.response?.body?.usage;
114
152
  if (usage) {
115
153
  const contextSize = getContextSizeForModel(parsed.body?.model);
@@ -123,14 +161,12 @@ export function watchLogFile(opts) {
123
161
  // Skip invalid entries
124
162
  }
125
163
  });
126
- // 通知 stats worker 更新统计
127
164
  notifyStatsWorker(logFile);
128
165
  }
129
166
 
130
167
  // 检测日志文件是否已轮转到新文件
131
168
  const currentLogFile = getLogFile();
132
169
  if (currentLogFile !== logFile && !watchedFiles.has(currentLogFile)) {
133
- // 轮转发生,发送 full_reload 让客户端重新加载新文件
134
170
  const newEntries = readLogFile(currentLogFile);
135
171
  clients.forEach(client => {
136
172
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-viewer",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
4
4
  "description": "Claude Code Logger visualization management tool",
5
5
  "license": "MIT",
6
6
  "main": "server.js",