cc-viewer 1.7.14 → 1.7.17
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-D11UR-a4.js → App-BzhZff4n.js} +2 -2
- package/dist/assets/{MdxEditorPanel-Dd__S_61.js → MdxEditorPanel-DDD8Of6g.js} +1 -1
- package/dist/assets/{Mobile-DozTru1m.js → Mobile-S2A7u-mh.js} +1 -1
- package/dist/assets/{ProxyStatsModal-BKZHRUOy.js → ProxyStatsModal-C8l4YL-L.js} +1 -1
- package/dist/assets/index-8EGcVOKG.js +2 -0
- package/dist/assets/seqResourceLoaders-C7Vn-EYS.js +2 -0
- package/dist/assets/{seqResourceLoaders-pBUn3c_a.css → seqResourceLoaders-bfmeQ_vN.css} +2 -2
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/server/lib/delta-reconstructor.js +3 -0
- package/server/lib/v2/adapter.js +44 -2
- package/server/lib/v2/agent-id.js +55 -0
- package/server/lib/v2/live-feed.js +6 -5
- package/server/lib/v2/meta-rows.js +5 -3
- package/server/lib/v2/v2-writer.js +6 -0
- package/server/lib/v2-transcript-normalizer.js +519 -0
- package/src/utils/contentFilter.js +17 -3
- package/src/utils/identityHeal.js +9 -0
- package/src/utils/requestType.js +30 -0
- package/src/utils/sessionManager.js +15 -4
- package/src/utils/sessionMerge.js +62 -3
- package/src/utils/toolResultBuilder.js +19 -1
- package/src/utils/toolResultCore.js +27 -0
- package/src/utils/v3Assembler.js +12 -0
- package/dist/assets/index-CPLS-pSI.js +0 -2
- package/dist/assets/seqResourceLoaders-BV8bzDPn.js +0 -2
package/dist/index.html
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// 整体显示大小已弃用 CSS zoom:Electron 改用 webFrame.setZoomFactor(首屏抢占见
|
|
22
22
|
// electron/tab-content-preload.js),纯浏览器交由用户用浏览器自带快捷键缩放,故此处不再设 zoom。
|
|
23
23
|
</script>
|
|
24
|
-
<script type="module" crossorigin src="./assets/index-
|
|
24
|
+
<script type="module" crossorigin src="./assets/index-8EGcVOKG.js"></script>
|
|
25
25
|
<link rel="modulepreload" crossorigin href="./assets/vendor-antd-DADYo_zg.js">
|
|
26
26
|
<link rel="modulepreload" crossorigin href="./assets/vendor-codemirror-tF6HNoR6.js">
|
|
27
27
|
<link rel="modulepreload" crossorigin href="./assets/vendor-mdxeditor-CFAmRN3Y.js">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-viewer",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.17",
|
|
4
4
|
"description": "Claude Code logging, visualization, and management toolkit — launch a web viewer alongside Claude Code with full request/response tracing, proxy, and mobile support",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "server.js",
|
|
@@ -244,6 +244,9 @@ export function reconstructEntries(entries) {
|
|
|
244
244
|
*/
|
|
245
245
|
function _tryRepairFromCandidate(brokenEntry, expectedCount, candidate) {
|
|
246
246
|
if (!candidate.mainAgent || candidate.teammate || !Array.isArray(candidate.body?.messages)) return false;
|
|
247
|
+
// V2 transcript synthetic entries are never a valid repair source for legacy
|
|
248
|
+
// delta rows — their messages are a different conversation's content.
|
|
249
|
+
if (candidate._syntheticV2) return false;
|
|
247
250
|
const candidateMsgs = candidate.body.messages;
|
|
248
251
|
const candidateTotal = candidate._totalMessageCount || candidateMsgs.length;
|
|
249
252
|
const isFullEntry = !candidate._deltaFormat || isCheckpointEntry(candidate);
|
package/server/lib/v2/adapter.js
CHANGED
|
@@ -34,6 +34,7 @@ import { isMainAgentRequest } from '../interceptor-core.js';
|
|
|
34
34
|
import { readSession } from './replay.js';
|
|
35
35
|
import { iterateJsonlLines } from './jsonl-read.js';
|
|
36
36
|
import { isDiscardableSession } from './session-select.js';
|
|
37
|
+
import { LIVE_SESSION_MTIME_MS } from '../log-management.js';
|
|
37
38
|
import { blobPath, isSupportedWireFormat } from './layout.js';
|
|
38
39
|
import { SingleFlight } from './singleflight.js';
|
|
39
40
|
import { listV2Sessions } from './session-list.js';
|
|
@@ -183,6 +184,29 @@ export class SessionSynthesizer {
|
|
|
183
184
|
this._leader = opts.teammateOf || this._meta.leader || null;
|
|
184
185
|
this._loadBlob = makeBlobLoader({ blobsDir: join(sessionDir, 'blobs') }, this.sessionId);
|
|
185
186
|
this.unsupported = false;
|
|
187
|
+
// Liveness gate — KEEP IN SYNC with log-management.js deleteLogFiles
|
|
188
|
+
// (same pid(0)-probe + EPERM + LIVE_SESSION_MTIME_MS mtime triple; the
|
|
189
|
+
// constant is imported here so only the ordering can drift). Cold reads
|
|
190
|
+
// finalize crash orphans as completed deltas ONLY when the session is dead.
|
|
191
|
+
// A live writer may still emit an orphan's done later — its completed twin
|
|
192
|
+
// would then be replayed by the client's seq guard as stale (_staleReorder)
|
|
193
|
+
// and the tail of the chat would vanish (cold-read + live-feed race).
|
|
194
|
+
this._sessionLive = false;
|
|
195
|
+
try {
|
|
196
|
+
const meta = this._meta;
|
|
197
|
+
if (meta && typeof meta.pid === 'number' && meta.pid !== process.pid) {
|
|
198
|
+
try { process.kill(meta.pid, 0); this._sessionLive = true; } catch (err) {
|
|
199
|
+
// EPERM = the pid exists under another user — that IS live.
|
|
200
|
+
if (err && err.code === 'EPERM') this._sessionLive = true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} catch { /* unreadable meta — fall through to the mtime guard */ }
|
|
204
|
+
if (!this._sessionLive) {
|
|
205
|
+
try {
|
|
206
|
+
const jStat = statSync(join(sessionDir, 'journal.jsonl'));
|
|
207
|
+
if (this._now() - jStat.mtimeMs < LIVE_SESSION_MTIME_MS) this._sessionLive = true;
|
|
208
|
+
} catch { /* no journal — nothing fresh to protect */ }
|
|
209
|
+
}
|
|
186
210
|
if (meta && meta.wireFormat != null && !isSupportedWireFormat(meta.wireFormat)) {
|
|
187
211
|
this._markUnsupported(meta.wireFormat);
|
|
188
212
|
}
|
|
@@ -445,7 +469,11 @@ export class SessionSynthesizer {
|
|
|
445
469
|
isHeartbeat: HEARTBEAT_URL_RE.test(req.url || ''),
|
|
446
470
|
isCountTokens: COUNT_TOKENS_URL_RE.test(req.url || ''),
|
|
447
471
|
mainAgent: false, // finalized below once messages are attached
|
|
448
|
-
...(isTeammateEntry && { teammate: leader.agentName || true, ...(leader.teamName && { teamName: leader.teamName }) }),
|
|
472
|
+
...(isTeammateEntry && { teammate: leader.agentName || req.agent?.agentName || true, ...(leader.teamName && { teamName: leader.teamName }) }),
|
|
473
|
+
// Wire agent identity persisted by the writer (see agent-id.js) — the
|
|
474
|
+
// client uses req.agent.agentName as the display name when the heuristic
|
|
475
|
+
// registry has nothing (native teammate, cold load).
|
|
476
|
+
...(req.agent && { agent: req.agent }),
|
|
449
477
|
...(req.proxy && { proxyProfile: req.proxy.profile, ...(req.proxy.url && { proxyUrl: req.proxy.url }) }),
|
|
450
478
|
};
|
|
451
479
|
|
|
@@ -508,9 +536,23 @@ export class SessionSynthesizer {
|
|
|
508
536
|
|
|
509
537
|
if (this._dones.has(seq)) {
|
|
510
538
|
this._tryComplete(seq);
|
|
539
|
+
} else if (this._deferMs === 0 && !this._sessionLive) {
|
|
540
|
+
// Cold read of a DEAD session: the orphan is definitively terminal. Emit
|
|
541
|
+
// it as a completed delta (no inProgress / requestId — the exact shape
|
|
542
|
+
// _complete produces) so the client batch reconstructor accumulates its
|
|
543
|
+
// message slice: inProgress rows are skipped by the reconstructor, which
|
|
544
|
+
// would otherwise drop the slice and fail every later completed entry's
|
|
545
|
+
// _totalMessageCount integrity check — blanking the whole chat.
|
|
546
|
+
entry.response = { body: null }; // mirrors _complete's no-response branch
|
|
547
|
+
// Push the SAME item reference (phase aside): _complete's dedup splice
|
|
548
|
+
// (this._out.indexOf(item)) depends on the queued object identity — a
|
|
549
|
+
// spread copy would dodge it and the twin completed frame would double.
|
|
550
|
+
item.phase = 'completed';
|
|
551
|
+
this._out.push(item);
|
|
511
552
|
} else {
|
|
512
553
|
// req without done — in-flight or the process died (spec §4). This IS the
|
|
513
|
-
// v1 placeholder, so it wears the same flags.
|
|
554
|
+
// v1 placeholder, so it wears the same flags. Live reads keep it so the
|
|
555
|
+
// incremental reconstructor can render the in-flight turn as it streams.
|
|
514
556
|
entry.inProgress = true;
|
|
515
557
|
entry.requestId = req.rid || `${seq}`;
|
|
516
558
|
this._out.push(item);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Agent identity parser — teammate/subagent names from the wire header.
|
|
2
|
+
//
|
|
3
|
+
// Claude Code 2.x sends an `x-claude-code-agent-id` request header on every
|
|
4
|
+
// sub-agent (teammate or subagent) request. Two shapes appear in real data:
|
|
5
|
+
// - named teammate: `frontend-reviewer@session-17e1f37a-...`
|
|
6
|
+
// - anonymous subagent: `a7eea0a140349f80d` (16-hex agent id, no @)
|
|
7
|
+
// The header is absent on main-agent requests, so its presence alone marks a
|
|
8
|
+
// sub-agent stream. We persist the NAME so display no longer depends on the
|
|
9
|
+
// frontend's window-scoped heuristic registry (resolveTeammateNames), which
|
|
10
|
+
// loses names when the lead's Agent tool_use falls outside the current window.
|
|
11
|
+
|
|
12
|
+
const ANON_RE = /^[0-9a-f]{8,64}$/i; // pure hex id, any plausible length
|
|
13
|
+
const MAX_NAME_LEN = 64;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse an `x-claude-code-agent-id` header value.
|
|
17
|
+
*
|
|
18
|
+
* @param {string|null|undefined} header
|
|
19
|
+
* @returns {{agentName: string|null, named: boolean}|null}
|
|
20
|
+
* - named teammate: {agentName, named: true}
|
|
21
|
+
* - anonymous agent: {agentName: null, named: false}
|
|
22
|
+
* - null when the header is missing/empty or its shape is not an agent id
|
|
23
|
+
* (never fabricate a name from an unrecognized value).
|
|
24
|
+
*/
|
|
25
|
+
export function parseAgentId(header) {
|
|
26
|
+
if (typeof header !== 'string') return null;
|
|
27
|
+
const h = header.trim();
|
|
28
|
+
if (h.length === 0) return null;
|
|
29
|
+
const at = h.indexOf('@');
|
|
30
|
+
if (at === -1) {
|
|
31
|
+
// Anonymous: a bare hex agent id. Anything else is not an agent id — do
|
|
32
|
+
// not guess.
|
|
33
|
+
return ANON_RE.test(h) ? { agentName: null, named: false } : null;
|
|
34
|
+
}
|
|
35
|
+
const name = h.slice(0, at).trim();
|
|
36
|
+
// The @-suffix is deliberately NOT validated (real shapes include
|
|
37
|
+
// `session-<uuid>` and possibly plain uuids) — presence of @ + a non-hex
|
|
38
|
+
// prefix is enough to treat it as a named agent. A hex prefix is treated as
|
|
39
|
+
// anonymous (guards against `<uuid>@session-...` being misread as a name).
|
|
40
|
+
if (name.length === 0) return null;
|
|
41
|
+
if (ANON_RE.test(name)) return { agentName: null, named: false };
|
|
42
|
+
return { agentName: name.slice(0, MAX_NAME_LEN), named: true };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Case-insensitive header lookup (undici folds to lowercase; plain objects
|
|
46
|
+
* may keep the original casing). */
|
|
47
|
+
export function findHeader(headers, key) {
|
|
48
|
+
if (!headers || typeof headers !== 'object') return undefined;
|
|
49
|
+
if (Object.prototype.hasOwnProperty.call(headers, key)) return headers[key];
|
|
50
|
+
const lower = key.toLowerCase();
|
|
51
|
+
for (const k of Object.keys(headers)) {
|
|
52
|
+
if (k.toLowerCase() === lower) return headers[k];
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
@@ -32,7 +32,7 @@ import { isDiscardableSession } from './session-select.js';
|
|
|
32
32
|
import { isForeignLiveOwned } from './session-owner.js';
|
|
33
33
|
import { isConvertRunning } from './convert-manager.js';
|
|
34
34
|
import { computeCacheLoss } from './meta-rows.js';
|
|
35
|
-
import { classifyRequest } from '../../../src/utils/requestType.js';
|
|
35
|
+
import { classifyRequest, withAgentNameSubType } from '../../../src/utils/requestType.js';
|
|
36
36
|
|
|
37
37
|
const FSWATCH_DEBOUNCE_MS = 80;
|
|
38
38
|
const SAFETY_POLL_MS = 5000;
|
|
@@ -679,6 +679,9 @@ export class V2LiveFeed {
|
|
|
679
679
|
kind: item.kind || (item.isMain ? 'main' : (parsed.teammate ? 'teammate' : 'sub')),
|
|
680
680
|
mainAgent: item.isMain === true,
|
|
681
681
|
teammate: parsed.teammate || undefined,
|
|
682
|
+
// Wire agent identity (x-claude-code-agent-id) — display name for native
|
|
683
|
+
// teammates (see agent-id.js).
|
|
684
|
+
agent: parsed.agent,
|
|
682
685
|
model: parsed.body?.model,
|
|
683
686
|
proxyUrl: parsed.proxyUrl || undefined,
|
|
684
687
|
status: parsed.response?.status,
|
|
@@ -698,8 +701,7 @@ export class V2LiveFeed {
|
|
|
698
701
|
_emitRow(cur, item, parsed) {
|
|
699
702
|
const row = this._rowFrom(item, parsed);
|
|
700
703
|
try {
|
|
701
|
-
|
|
702
|
-
row.typeTag = tag ? { type: tag.type, subType: tag.subType ?? null } : null;
|
|
704
|
+
row.typeTag = withAgentNameSubType(classifyRequest(parsed, null), parsed);
|
|
703
705
|
} catch (err) { reportSwallowed('v2-live.row-classify', err); }
|
|
704
706
|
if (row.mainAgent && !row.inProgress) {
|
|
705
707
|
const u = parsed.response?.body?.usage;
|
|
@@ -714,8 +716,7 @@ export class V2LiveFeed {
|
|
|
714
716
|
const prev = cur._v3Last;
|
|
715
717
|
if (prev && (prev.row.sessionId !== row.sessionId || prev.row.seq !== row.seq)) {
|
|
716
718
|
try {
|
|
717
|
-
const
|
|
718
|
-
const corrected = tag ? { type: tag.type, subType: tag.subType ?? null } : null;
|
|
719
|
+
const corrected = withAgentNameSubType(classifyRequest(prev.entry, parsed), prev.entry);
|
|
719
720
|
if (JSON.stringify(corrected) !== JSON.stringify(prev.row.typeTag)) {
|
|
720
721
|
prev.row.typeTag = corrected;
|
|
721
722
|
sendEventToClients(this._clients, 'v2_requests_delta', prev.row);
|
|
@@ -22,7 +22,7 @@ import { iterateJsonlLines } from './jsonl-read.js';
|
|
|
22
22
|
import { iterateV2Items, findTeammateSessionDirs } from './adapter.js';
|
|
23
23
|
import { SingleFlight } from './singleflight.js';
|
|
24
24
|
import { reportSwallowed } from '../error-report.js';
|
|
25
|
-
import { classifyRequest } from '../../../src/utils/requestType.js';
|
|
25
|
+
import { classifyRequest, withAgentNameSubType } from '../../../src/utils/requestType.js';
|
|
26
26
|
|
|
27
27
|
// KEEP IN SYNC: server/lib/v2/adapter.js itemKey — (sessionId, seq) identity.
|
|
28
28
|
const itemKey = (sessionId, seq) => `${sessionId}\x00${seq}`;
|
|
@@ -113,6 +113,9 @@ function foldDir(dir) {
|
|
|
113
113
|
mainAgent: r.kind === 'main' && !isTeammateDir,
|
|
114
114
|
// v1 entry contract (adapter.js:427): teammate = agentName string | true
|
|
115
115
|
teammate: isTeammateDir ? ((meta.leader && meta.leader.agentName) || true) : undefined,
|
|
116
|
+
// Wire agent identity (x-claude-code-agent-id) persisted on the journal
|
|
117
|
+
// req line — the display name for native teammates (see agent-id.js).
|
|
118
|
+
agent: r.agent,
|
|
116
119
|
model: r.model,
|
|
117
120
|
proxyUrl: r.proxy && r.proxy.url ? r.proxy.url : undefined,
|
|
118
121
|
status: d && typeof d.http === 'number' ? d.http : undefined,
|
|
@@ -318,8 +321,7 @@ async function attachBodyFields(sessionDir, rows) {
|
|
|
318
321
|
const finish = (slot, nextEntry) => {
|
|
319
322
|
const { row, entry } = slot;
|
|
320
323
|
try {
|
|
321
|
-
|
|
322
|
-
row.typeTag = tag ? { type: tag.type, subType: tag.subType ?? null } : null;
|
|
324
|
+
row.typeTag = withAgentNameSubType(classifyRequest(entry, nextEntry), entry);
|
|
323
325
|
} catch (err) {
|
|
324
326
|
// Diagnostic-worthy (a throw silently mis-tags the row; the live path
|
|
325
327
|
// reports the same failure) — CLAUDE.md swallow rule.
|
|
@@ -27,6 +27,7 @@ import { BlobStore } from './blob-store.js';
|
|
|
27
27
|
import { Journal } from './journal.js';
|
|
28
28
|
import { ConversationStore } from './conversation-store.js';
|
|
29
29
|
import { parseUserId, classifyKind, ConvResolver } from './identity.js';
|
|
30
|
+
import { parseAgentId, findHeader } from './agent-id.js';
|
|
30
31
|
import { extractUserTexts, flattenPromptText, isSuggestionMode, readPromptsHead } from '../user-prompt-extract.js';
|
|
31
32
|
|
|
32
33
|
// Below this many free bytes on the log volume, v2 skips writing and reports
|
|
@@ -505,6 +506,10 @@ export class V2Writer {
|
|
|
505
506
|
}
|
|
506
507
|
|
|
507
508
|
// 3. Journal req line — LAST, so it never references missing content.
|
|
509
|
+
// Persist the wire agent identity (x-claude-code-agent-id) so teammate
|
|
510
|
+
// names survive cold reads — display no longer depends on the frontend's
|
|
511
|
+
// window-scoped name registry (see agent-id.js).
|
|
512
|
+
const agent = parseAgentId(findHeader(entry.headers, 'x-claude-code-agent-id'));
|
|
508
513
|
s.journal.writeReq({
|
|
509
514
|
seq,
|
|
510
515
|
rid,
|
|
@@ -517,6 +522,7 @@ export class V2Writer {
|
|
|
517
522
|
...(entry.body && entry.body.model && { model: entry.body.model }),
|
|
518
523
|
...(entry.isStream && { isStream: true }),
|
|
519
524
|
...(entry.headers && { headers: entry.headers }),
|
|
525
|
+
...(agent && { agent }),
|
|
520
526
|
...(params && { params }),
|
|
521
527
|
...((toolsRef || sysRef) && { blobs: { ...(toolsRef && { tools: toolsRef }), ...(sysRef && { sys: sysRef }) } }),
|
|
522
528
|
...(convResult && { msgFrom: convResult.msgFrom, msgTo: convResult.msgTo }),
|