cc-viewer 1.9.14 → 1.9.15

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.
Files changed (30) hide show
  1. package/dist/assets/App-BCDnm2Lh.js +2 -0
  2. package/dist/assets/App-CCnPkf2_.css +1 -0
  3. package/dist/assets/{MdxEditorPanel-DI3B4gsM.js → MdxEditorPanel-DRLFmdU5.js} +1 -1
  4. package/dist/assets/Mobile-C8VJqBwa.js +1 -0
  5. package/dist/assets/{ProxyStatsModal-B8K0AtAq.js → ProxyStatsModal-Ba1E1lu4.js} +1 -1
  6. package/dist/assets/{index-CU0gt6m1.js → index-Sp3JYYKo.js} +2 -2
  7. package/dist/assets/seqResourceLoaders-BUfjTaCV.css +41 -0
  8. package/dist/assets/seqResourceLoaders-CHmtPDFO.js +2 -0
  9. package/dist/index.html +1 -1
  10. package/node_modules/@ccv/core/src/contentFilter.js +3 -0
  11. package/node_modules/@ccv/core/src/queued-prompt.js +66 -0
  12. package/node_modules/@ccv/core/src/v2-transcript-normalizer.js +78 -14
  13. package/package.json +1 -1
  14. package/server/lib/chat-queue.js +350 -120
  15. package/server/lib/scheduled-task-executor.js +3 -1
  16. package/server/lib/sdk-manager.js +282 -68
  17. package/server/lib/v2/conversation-store.js +15 -11
  18. package/server/lib/v2/v2-writer.js +1 -1
  19. package/server/lib/view-root.js +4 -4
  20. package/server/pty-manager.js +8 -8
  21. package/server/routes/files-content.js +108 -14
  22. package/server/routes/files-fs.js +122 -36
  23. package/server/routes/resume.js +1 -1
  24. package/server/routes/sdk-context.js +13 -2
  25. package/server/server.js +128 -28
  26. package/dist/assets/App-BzV1ieDH.js +0 -2
  27. package/dist/assets/App-DU5jmGrx.css +0 -1
  28. package/dist/assets/Mobile-qBkAnXy3.js +0 -1
  29. package/dist/assets/seqResourceLoaders-CxUFE36z.css +0 -41
  30. package/dist/assets/seqResourceLoaders-nGCs11zu.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-CU0gt6m1.js"></script>
24
+ <script type="module" crossorigin src="./assets/index-Sp3JYYKo.js"></script>
25
25
  <link rel="modulepreload" crossorigin href="./assets/vendor-antd-I6I2F7Rv.js">
26
26
  <link rel="modulepreload" crossorigin href="./assets/vendor-codemirror-Zkm6UHQg.js">
27
27
  <link rel="modulepreload" crossorigin href="./assets/vendor-mdxeditor-CC09MDyZ.js">
@@ -312,6 +312,9 @@ function stripSystemTags(text) {
312
312
  // Same kind as the protocol JSON in a <teammate-message> wrapper; after stripping, the
313
313
  // second pass recovers only the user-appended body (empty if none)
314
314
  out = extractProtocolNotifications(out).rest;
315
+ // An interrupt marker may share a text block with the user's next prompt.
316
+ // Strip only its complete leading form; later quoted markers stay verbatim.
317
+ out = out.replace(/^\s*\[Request interrupted[^\]\n]*\]\s*/i, '');
315
318
  return out.trim();
316
319
  }
317
320
 
@@ -0,0 +1,66 @@
1
+ // CLIENT-SAFE: no node deps. Home: @ccv/core (queued-prompt.js) — consumed by apps/web + packages/app.
2
+ // Display-only recovery of user prompts inserted while Claude Code is working.
3
+ // The original JSONL and model wire roles/content are never changed by these helpers.
4
+
5
+ import { extractDisplayText } from './contentFilter.js';
6
+
7
+ const WIRE_PREFIX = 'The user sent a new message while you were working:\n';
8
+ const WIRE_SUFFIX = '\n\nIMPORTANT: After completing your current task, you MUST address the user\'s message above. Do not ignore it.';
9
+
10
+ /** Extract a prompt from Claude Code's exact mid-conversation-system envelope. */
11
+ export function extractQueuedPromptWireMessage(message) {
12
+ if (!message || message.role !== 'system') return null;
13
+ const textBlocks = typeof message.content === 'string'
14
+ ? [message.content]
15
+ : Array.isArray(message.content)
16
+ ? message.content.filter((block) => block?.type === 'text').map((block) => block.text)
17
+ : [];
18
+ if (textBlocks.length !== 1 || typeof textBlocks[0] !== 'string') return null;
19
+ const text = textBlocks[0];
20
+ if (!text.startsWith(WIRE_PREFIX) || !text.endsWith(WIRE_SUFFIX)) return null;
21
+ const content = text.slice(WIRE_PREFIX.length, -WIRE_SUFFIX.length);
22
+ return content ? { content, source: 'wire' } : null;
23
+ }
24
+
25
+ function hasDisplayText(content) {
26
+ if (typeof content === 'string') return !!extractDisplayText(content);
27
+ if (!Array.isArray(content)) return false;
28
+ return content.some((block) => block?.type === 'text' && !!extractDisplayText(block.text));
29
+ }
30
+
31
+ /** Render a recovered prompt without changing its transcript/model representation. */
32
+ export function queuedPromptDisplayText(content) {
33
+ if (typeof content === 'string') return extractDisplayText(content);
34
+ if (!Array.isArray(content)) return '';
35
+ return content
36
+ .filter((block) => block?.type === 'text')
37
+ .map((block) => extractDisplayText(block.text))
38
+ .filter(Boolean)
39
+ .join('\n\n');
40
+ }
41
+
42
+ /**
43
+ * Convert a confirmed Claude Code transcript queue record to a display input.
44
+ * An enqueue alone is deliberately insufficient: it may still be pending or cancelled.
45
+ */
46
+ export function extractQueuedPromptTranscriptLine(line, position = 0) {
47
+ if (!line || line.isSidechain === true || line.isMeta === true) return null;
48
+ const attachment = line.type === 'attachment' ? line.attachment : null;
49
+ if (attachment) {
50
+ if (attachment.type !== 'queued_command' || attachment.commandMode !== 'prompt') return null;
51
+ const origin = attachment.origin?.kind;
52
+ const sdkBlocks = line.entrypoint === 'sdk-ts' && !origin && Array.isArray(attachment.prompt);
53
+ if (origin !== 'human' && !sdkBlocks) return null;
54
+ const content = attachment.prompt;
55
+ if (!hasDisplayText(content)) return null;
56
+ return {
57
+ content,
58
+ source: 'queued_command',
59
+ sourceId: line.uuid || `queued_command:${position}`,
60
+ parentUuid: line.parentUuid || null,
61
+ sessionId: line.sessionId || null,
62
+ submittedTimestamp: attachment.timestamp || line.timestamp || null,
63
+ };
64
+ }
65
+ return null;
66
+ }
@@ -1,4 +1,5 @@
1
1
  // CLIENT-SAFE: no node deps. Home: @ccv/core (v2-transcript-normalizer.js) — consumed by apps/web + packages/app. Do not add fs/process/node: imports.
2
+ import { extractQueuedPromptTranscriptLine } from './queued-prompt.js';
2
3
  /**
3
4
  * V2 Transcript Normalizer — Claude Code 2.x JSONL → legacy-compatible entry
4
5
  *
@@ -35,8 +36,9 @@
35
36
  * same image) is ignored — message.content already carries the standard
36
37
  * base64 data; honoring both would double memory.
37
38
  * - Metadata rows (mode / ai-title / last-prompt / file-history-snapshot /
38
- * queue-operation) carry no message and are dropped — isRelevantRequest
39
- * would otherwise surface them as garbage request rows.
39
+ * system frames) carry no message and are dropped. Confirmed queued_command
40
+ * attachments are the exception: they become display-only messages, while
41
+ * pending enqueue / dequeue / remove / popAll remain non-renderable.
40
42
  */
41
43
 
42
44
  const V2_LINE_TYPES = new Set(['user', 'assistant']);
@@ -59,6 +61,7 @@ const DEDUP_MAX = 1024;
59
61
  */
60
62
  export function isV2TranscriptLine(entry) {
61
63
  if (!entry || typeof entry !== 'object') return false;
64
+ if (entry.type === 'attachment' || entry.type === 'queue-operation') return false;
62
65
  if (!V2_LINE_TYPES.has(entry.type)) return false;
63
66
  if (entry.isSidechain === true) return false;
64
67
  const msg = entry.message;
@@ -66,6 +69,11 @@ export function isV2TranscriptLine(entry) {
66
69
  return V2_LINE_ROLES.has(msg.role);
67
70
  }
68
71
 
72
+ /** True for a confirmed queue input, not an enqueue that may never be consumed. */
73
+ export function isV2TranscriptDisplayLine(entry) {
74
+ return isV2TranscriptLine(entry) || !!extractQueuedPromptTranscriptLine(entry);
75
+ }
76
+
69
77
  /**
70
78
  * True for a legacy (or already-normalized) renderable request row.
71
79
  * Anything that is neither a v2 line nor a legacy request is a metadata row
@@ -86,7 +94,17 @@ function isLegacyRequestLine(entry) {
86
94
  */
87
95
  export function isMetadataRow(entry) {
88
96
  if (!entry || typeof entry !== 'object') return false;
89
- return !isV2TranscriptLine(entry) && !isLegacyRequestLine(entry);
97
+ return !isV2TranscriptDisplayLine(entry) && !isLegacyRequestLine(entry);
98
+ }
99
+
100
+ function queuedPrompt(line, position = 0) {
101
+ const prompt = extractQueuedPromptTranscriptLine(line, position);
102
+ if (!prompt) return null;
103
+ return {
104
+ content: prompt.content,
105
+ _timestamp: prompt.submittedTimestamp,
106
+ _ccvSourceId: prompt.sourceId,
107
+ };
90
108
  }
91
109
 
92
110
  function parseTs(ts) {
@@ -116,8 +134,15 @@ function sortLines(lines) {
116
134
  */
117
135
  function buildSegmentMessages(lines) {
118
136
  const messages = [];
137
+ const queuedPrompts = [];
119
138
  const assistantById = new Map(); // message.id → message object
120
- for (const line of lines) {
139
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
140
+ const line = lines[lineIndex];
141
+ const queued = queuedPrompt(line, lineIndex);
142
+ if (queued) {
143
+ queuedPrompts.push({ ...queued, afterIndex: messages.length - 1 });
144
+ continue;
145
+ }
121
146
  const msg = line.message;
122
147
  if (msg.role === 'user') {
123
148
  messages.push({
@@ -157,7 +182,7 @@ function buildSegmentMessages(lines) {
157
182
  messages.push(m);
158
183
  if (mid) assistantById.set(mid, m);
159
184
  }
160
- return messages;
185
+ return { messages, queuedPrompts };
161
186
  }
162
187
 
163
188
  /**
@@ -167,9 +192,9 @@ function buildSegmentMessages(lines) {
167
192
  * messages[0]._timestamp invariant.
168
193
  */
169
194
  export function buildSyntheticEntry(lines, sessionId, segIdx) {
170
- const messages = buildSegmentMessages(lines);
171
- if (messages.length === 0) return null;
172
- const entryTs = messages[0]._timestamp;
195
+ const { messages, queuedPrompts } = buildSegmentMessages(lines);
196
+ if (messages.length === 0 && queuedPrompts.length === 0) return null;
197
+ const entryTs = messages.length > 0 ? messages[0]._timestamp : queuedPrompts[0]._timestamp;
173
198
  for (const m of messages) m._entryTs = entryTs;
174
199
 
175
200
  // Last assistant row carries model/usage — expose it as a synthetic
@@ -191,6 +216,7 @@ export function buildSyntheticEntry(lines, sessionId, segIdx) {
191
216
  sessionId,
192
217
  _seqEpoch: `v2:${sessionId}:${segIdx}`,
193
218
  _syntheticV2: true,
219
+ _ccvQueuedPrompts: queuedPrompts,
194
220
  _messageCount: messages.length,
195
221
  };
196
222
  if (lastModel || lastUsage) {
@@ -240,7 +266,7 @@ export function normalizeV2Entries(rawEntries) {
240
266
  let v2Count = 0;
241
267
  const kept = [];
242
268
  for (const e of rawEntries) {
243
- if (isV2TranscriptLine(e)) {
269
+ if (isV2TranscriptDisplayLine(e)) {
244
270
  v2Count++;
245
271
  } else if (isLegacyRequestLine(e)) {
246
272
  kept.push(e);
@@ -251,7 +277,7 @@ export function normalizeV2Entries(rawEntries) {
251
277
 
252
278
  const bySession = new Map(); // sessionId → v2 lines
253
279
  for (const e of rawEntries) {
254
- if (!isV2TranscriptLine(e)) continue;
280
+ if (!isV2TranscriptDisplayLine(e)) continue;
255
281
  const sid = e.sessionId || 'unknown';
256
282
  let arr = bySession.get(sid);
257
283
  if (!arr) bySession.set(sid, (arr = []));
@@ -260,8 +286,14 @@ export function normalizeV2Entries(rawEntries) {
260
286
 
261
287
  const synthetics = [];
262
288
  for (const [sid, lines] of bySession) {
263
- const sorted = sortLines(lines);
264
- const segments = splitSegments(sorted);
289
+ // Queue attachments carry their ENQUEUE timestamp, not the later position at
290
+ // which Claude consumed them. Keep the whole file sequence when such a row
291
+ // exists so its tool/assistant neighbours stay faithful to the transcript;
292
+ // sessions without queue inputs retain historical timestamp repair.
293
+ const ordered = lines.some((line) => extractQueuedPromptTranscriptLine(line))
294
+ ? lines
295
+ : sortLines(lines);
296
+ const segments = splitSegments(ordered);
265
297
  for (let i = 0; i < segments.length; i++) {
266
298
  const entry = buildSyntheticEntry(segments[i], sid, i);
267
299
  if (entry) synthetics.push(entry);
@@ -291,6 +323,7 @@ export function createV2IncrementalReconstructor() {
291
323
  // primed cold entry's segment epoch, or the merge
292
324
  // boundary check would split the session)
293
325
  accumulated: null, // array of messages (cold snapshot or live-built)
326
+ queuedPrompts: [],
294
327
  shared: false, // accumulated entries came from prime (copy-on-write)
295
328
  assistantById: new Map(),
296
329
  // tool_use blocks merged into an already-scanned message by a later split
@@ -300,6 +333,8 @@ export function createV2IncrementalReconstructor() {
300
333
  mergedToolUses: [],
301
334
  seenUuids: new Set(),
302
335
  uuidOrder: [],
336
+ lastQueuedLine: null,
337
+ lastQueuedPosition: -1,
303
338
  lastAssistant: null,
304
339
  };
305
340
 
@@ -380,6 +415,7 @@ export function createV2IncrementalReconstructor() {
380
415
  sessionId: state.sessionId,
381
416
  _seqEpoch: epoch,
382
417
  _syntheticV2: true,
418
+ _ccvQueuedPrompts: state.queuedPrompts.map((prompt) => ({ ...prompt })),
383
419
  _messageCount: messages.length,
384
420
  };
385
421
  if (state.lastAssistant) {
@@ -419,9 +455,12 @@ export function createV2IncrementalReconstructor() {
419
455
  ...m,
420
456
  content: Array.isArray(m.content) ? [...m.content] : m.content,
421
457
  }));
458
+ state.queuedPrompts = (syntheticEntry._ccvQueuedPrompts || []).map((prompt) => ({ ...prompt }));
459
+ for (const prompt of state.queuedPrompts) if (prompt._ccvSourceId) state.seenUuids.add(prompt._ccvSourceId);
422
460
  state.shared = true;
423
461
  state.assistantById.clear();
424
462
  for (const m of state.accumulated) {
463
+ if (m._ccvSourceId) state.seenUuids.add(m._ccvSourceId);
425
464
  if (m.role === 'assistant') {
426
465
  if (m._mid) state.assistantById.set(m._mid, m);
427
466
  state.lastAssistant = m;
@@ -436,7 +475,7 @@ export function createV2IncrementalReconstructor() {
436
475
  * isV2TranscriptLine first).
437
476
  */
438
477
  reconstruct(line) {
439
- if (!isV2TranscriptLine(line)) return null;
478
+ if (!isV2TranscriptDisplayLine(line)) return null;
440
479
  // Session switch (workspace switch / another project's file on the same
441
480
  // SSE): the old session is already rendered — drop its baseline and
442
481
  // start a fresh one so rows never cross sessions.
@@ -445,12 +484,23 @@ export function createV2IncrementalReconstructor() {
445
484
  state.entryTs = line.timestamp;
446
485
  state.epoch = null;
447
486
  state.accumulated = [];
487
+ state.queuedPrompts = [];
448
488
  state.shared = false;
449
489
  state.assistantById.clear();
450
490
  state.lastAssistant = null;
451
491
  state.mergedToolUses = [];
452
492
  }
453
- const uuid = line.uuid;
493
+ const position = !line.uuid && line.type === 'attachment' && state.lastQueuedLine === line
494
+ ? state.lastQueuedPosition : state.queuedPrompts.length;
495
+ const queued = queuedPrompt(line, position);
496
+ if (queued && !line.uuid) {
497
+ state.lastQueuedLine = line;
498
+ state.lastQueuedPosition = position;
499
+ } else {
500
+ state.lastQueuedLine = null;
501
+ state.lastQueuedPosition = -1;
502
+ }
503
+ const uuid = queued?._ccvSourceId || line.uuid;
454
504
  if (uuid && state.seenUuids.has(uuid)) return null;
455
505
  if (uuid) {
456
506
  state.seenUuids.add(uuid);
@@ -465,9 +515,21 @@ export function createV2IncrementalReconstructor() {
465
515
  state.entryTs = line.timestamp;
466
516
  state.epoch = null;
467
517
  state.accumulated = [];
518
+ state.queuedPrompts = [];
468
519
  state.shared = false;
469
520
  }
470
521
  const msg = line.message;
522
+ if (queued) {
523
+ if (state.accumulated === null) {
524
+ state.sessionId = line.sessionId || 'unknown';
525
+ state.entryTs = queued._timestamp;
526
+ state.accumulated = [];
527
+ state.queuedPrompts = [];
528
+ state.shared = false;
529
+ }
530
+ state.queuedPrompts.push({ ...queued, afterIndex: state.accumulated.length - 1 });
531
+ return snapshotEntry();
532
+ }
471
533
  if (msg.role === 'user' && isClearRow(line)) {
472
534
  // Live /clear: start a new segment (new epoch) so the batch and live
473
535
  // paths split sessions identically. The command row itself is dropped.
@@ -478,6 +540,7 @@ export function createV2IncrementalReconstructor() {
478
540
  state.epoch = `v2:${state.sessionId}:${(state.epoch?.split(':').pop() ?? 0) + 1}`;
479
541
  state.entryTs = null; // first message of the new segment owns the ts
480
542
  state.accumulated = [];
543
+ state.queuedPrompts = [];
481
544
  state.shared = false;
482
545
  state.assistantById.clear();
483
546
  state.lastAssistant = null;
@@ -509,6 +572,7 @@ export function createV2IncrementalReconstructor() {
509
572
  state.entryTs = null;
510
573
  state.epoch = null;
511
574
  state.accumulated = null;
575
+ state.queuedPrompts = [];
512
576
  state.shared = false;
513
577
  state.assistantById.clear();
514
578
  state.seenUuids.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-viewer",
3
- "version": "1.9.14",
3
+ "version": "1.9.15",
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",