cc-viewer 1.9.14 → 1.9.16
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/cli.js +1 -0
- package/concepts/ar/UltraPlan.md +9 -9
- package/concepts/da/UltraPlan.md +9 -9
- package/concepts/de/UltraPlan.md +9 -9
- package/concepts/en/UltraPlan.md +9 -9
- package/concepts/es/UltraPlan.md +9 -9
- package/concepts/fr/UltraPlan.md +9 -9
- package/concepts/it/UltraPlan.md +9 -9
- package/concepts/ja/UltraPlan.md +9 -9
- package/concepts/ko/UltraPlan.md +9 -9
- package/concepts/no/UltraPlan.md +9 -9
- package/concepts/pl/UltraPlan.md +9 -9
- package/concepts/pt-BR/UltraPlan.md +9 -9
- package/concepts/ru/UltraPlan.md +9 -9
- package/concepts/th/UltraPlan.md +9 -9
- package/concepts/tr/UltraPlan.md +9 -9
- package/concepts/uk/UltraPlan.md +9 -9
- package/concepts/zh/UltraPlan.md +9 -9
- package/concepts/zh-TW/UltraPlan.md +9 -9
- package/dist/assets/App-BvVdj099.css +1 -0
- package/dist/assets/App-CYdOioAz.js +2 -0
- package/dist/assets/{MdxEditorPanel-DI3B4gsM.js → MdxEditorPanel-CuCAe1OG.js} +1 -1
- package/dist/assets/Mobile-CfNYOgRa.js +1 -0
- package/dist/assets/{ProxyStatsModal-B8K0AtAq.js → ProxyStatsModal-Dhj4EVnh.js} +1 -1
- package/dist/assets/index-sgW8MlD3.js +2 -0
- package/dist/assets/seqResourceLoaders-B-5xGs9q.css +41 -0
- package/dist/assets/seqResourceLoaders-Cj0pdz-l.js +2 -0
- package/dist/index.html +1 -1
- package/node_modules/@ccv/core/src/contentFilter.js +3 -0
- package/node_modules/@ccv/core/src/queued-prompt.js +66 -0
- package/node_modules/@ccv/core/src/v2-transcript-normalizer.js +78 -14
- package/package.json +1 -1
- package/server/lib/ask/perm-bridge.js +4 -1
- package/server/lib/auto-approve.js +160 -0
- package/server/lib/chat-queue.js +350 -120
- package/server/lib/file-access-policy.js +24 -2
- package/server/lib/scheduled-task-executor.js +3 -1
- package/server/lib/sdk-manager.js +377 -76
- package/server/lib/v2/conversation-store.js +15 -11
- package/server/lib/v2/v2-writer.js +1 -1
- package/server/lib/view-root.js +4 -4
- package/server/pty-manager.js +8 -8
- package/server/routes/ask-perm.js +54 -3
- package/server/routes/files-content.js +352 -16
- package/server/routes/files-fs.js +122 -36
- package/server/routes/resume.js +1 -1
- package/server/routes/sdk-context.js +13 -2
- package/server/routes/skills.js +19 -8
- package/server/server.js +205 -34
- package/server/system-prompt-templates/presets/kimi-k3.md +1 -0
- package/ultraAgents/code-expert.json +1 -1
- package/dist/assets/App-BzV1ieDH.js +0 -2
- package/dist/assets/App-DU5jmGrx.css +0 -1
- package/dist/assets/Mobile-qBkAnXy3.js +0 -1
- package/dist/assets/index-CU0gt6m1.js +0 -2
- package/dist/assets/seqResourceLoaders-CxUFE36z.css +0 -41
- 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-
|
|
24
|
+
<script type="module" crossorigin src="./assets/index-sgW8MlD3.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
|
-
*
|
|
39
|
-
*
|
|
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 !
|
|
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 (
|
|
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 (
|
|
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 (!
|
|
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
|
-
|
|
264
|
-
|
|
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 (!
|
|
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
|
|
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.
|
|
3
|
+
"version": "1.9.16",
|
|
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",
|
|
@@ -164,7 +164,10 @@ const TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
|
164
164
|
|
|
165
165
|
function postToViewer() {
|
|
166
166
|
return new Promise((resolve, reject) => {
|
|
167
|
-
|
|
167
|
+
// Forward the session cwd so the server can resolve the project-forked
|
|
168
|
+
// autoApproveSeconds for the right workspace (falls back to the server's
|
|
169
|
+
// current project key when absent — backward compatible).
|
|
170
|
+
const body = JSON.stringify({ toolName, input: toolInput, cwd: typeof payload?.cwd === 'string' ? payload.cwd : undefined });
|
|
168
171
|
const req = httpClient.request({
|
|
169
172
|
hostname: '127.0.0.1',
|
|
170
173
|
port: Number(port),
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Server-side auto-approve countdown for plan/permission approvals.
|
|
2
|
+
//
|
|
3
|
+
// The auto-approve countdown used to live entirely in the web client: when the
|
|
4
|
+
// browser tab was closed, backgrounded (timer throttling), or disconnected, no
|
|
5
|
+
// approval ever fired and the pending request degraded to the 5-minute deny
|
|
6
|
+
// timeout. This module moves the deadline to the server: the countdown is armed
|
|
7
|
+
// from the moment the approval request arrives (wall-clock), and the server
|
|
8
|
+
// resolves the pending approval itself at deadline + AUTO_APPROVE_BUFFER_MS.
|
|
9
|
+
// The buffer gives a connected client whose own countdown fires at the exact
|
|
10
|
+
// deadline a head start, so a watched approval is still answered by the client
|
|
11
|
+
// and the server timer is only the fallback.
|
|
12
|
+
//
|
|
13
|
+
// Cancellation is durable while the approval is pending: a client cancel marks
|
|
14
|
+
// the entry (autoCancelled) and clears the server timer, the approval stays
|
|
15
|
+
// pending for a manual decision, and the cancelled state is replayed to newly
|
|
16
|
+
// connected clients so they never re-arm a countdown.
|
|
17
|
+
//
|
|
18
|
+
// Semantics of the settings (shared with the client):
|
|
19
|
+
// plan: prefs.approvalModal.planAutoApproveSeconds (default 0 = off)
|
|
20
|
+
// perm: prefs.autoApproveSeconds (default 3 when unset)
|
|
21
|
+
// 0/off, N>0 = N seconds, -1 = instant (resolve immediately, no buffer).
|
|
22
|
+
// AskUserQuestion approvals are never auto-approved.
|
|
23
|
+
|
|
24
|
+
import { readPrefsRaw } from './prefs-store.js';
|
|
25
|
+
import { getCurrentProjectKey, hasFork, resolveScoped } from './project-prefs.js';
|
|
26
|
+
import { reportSwallowed } from './error-report.js';
|
|
27
|
+
|
|
28
|
+
export const AUTO_APPROVE_BUFFER_MS = 1000;
|
|
29
|
+
export const DEFAULT_PERM_AUTO_APPROVE_SECONDS = 3;
|
|
30
|
+
export const AUTO_APPROVE_INSTANT = -1;
|
|
31
|
+
// A hand-edited preferences.json can hold an absurdly large value. Besides being
|
|
32
|
+
// pointless, a value above the 5-minute hard approval timeout would advertise an
|
|
33
|
+
// autoApproveAt the deny timeout always beats (a lying countdown); and beyond
|
|
34
|
+
// 2^31-1 ms Node's setTimeout clamps the delay to 1ms (instant approval). Cap the
|
|
35
|
+
// countdown at the approval timeout so the advertised deadline is always reachable.
|
|
36
|
+
export const MAX_AUTO_APPROVE_SECONDS = 300;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extract the configured seconds for a kind ('plan' | 'perm') from an effective
|
|
40
|
+
* prefs object. Returns null when disabled (0 / invalid / negative other than -1 /
|
|
41
|
+
* absurdly large).
|
|
42
|
+
*/
|
|
43
|
+
export function effectiveAutoApproveSeconds(kind, prefs) {
|
|
44
|
+
const raw = kind === 'plan'
|
|
45
|
+
? prefs?.approvalModal?.planAutoApproveSeconds
|
|
46
|
+
: (prefs?.autoApproveSeconds ?? DEFAULT_PERM_AUTO_APPROVE_SECONDS);
|
|
47
|
+
const seconds = Number(raw);
|
|
48
|
+
if (!Number.isFinite(seconds)) return null;
|
|
49
|
+
if (seconds === AUTO_APPROVE_INSTANT) return AUTO_APPROVE_INSTANT;
|
|
50
|
+
if (seconds <= 0) return null;
|
|
51
|
+
if (seconds > MAX_AUTO_APPROVE_SECONDS) return null;
|
|
52
|
+
return seconds;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Read prefs (project-fork aware) and compute the auto-approve deadline for a
|
|
57
|
+
* pending approval. `projectKey` is the session cwd (SDK) or hook-reported cwd
|
|
58
|
+
* (PTY hook); falls back to the server's current project key.
|
|
59
|
+
*
|
|
60
|
+
* Returns:
|
|
61
|
+
* { enabled:false } — no auto-approve
|
|
62
|
+
* { enabled:true, instant, seconds, autoApproveAt } — absolute epoch ms deadline
|
|
63
|
+
*/
|
|
64
|
+
export function computeAutoApprove(kind, projectKey, { now = Date.now(), readPrefs = readPrefsRaw, file } = {}) {
|
|
65
|
+
let prefs = {};
|
|
66
|
+
try {
|
|
67
|
+
prefs = readPrefs(file) || {};
|
|
68
|
+
} catch (err) {
|
|
69
|
+
reportSwallowed('auto-approve.read-prefs', err);
|
|
70
|
+
return { enabled: false };
|
|
71
|
+
}
|
|
72
|
+
const key = typeof projectKey === 'string' && projectKey ? projectKey : getCurrentProjectKey();
|
|
73
|
+
const effective = hasFork(prefs, key) ? resolveScoped(prefs, prefs.prefsByProject[key]) : prefs;
|
|
74
|
+
const seconds = effectiveAutoApproveSeconds(kind, effective);
|
|
75
|
+
if (seconds === null) return { enabled: false };
|
|
76
|
+
const instant = seconds === AUTO_APPROVE_INSTANT;
|
|
77
|
+
return {
|
|
78
|
+
enabled: true,
|
|
79
|
+
instant,
|
|
80
|
+
seconds,
|
|
81
|
+
autoApproveAt: instant ? now : now + seconds * 1000,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Delay for the server-side fallback timer: the user-visible deadline plus the
|
|
87
|
+
* request buffer, never negative. Instant fires on the same tick (0).
|
|
88
|
+
*/
|
|
89
|
+
export function autoApproveFireDelayMs(computed, now = Date.now()) {
|
|
90
|
+
if (!computed?.enabled) return null;
|
|
91
|
+
if (computed.instant) return 0;
|
|
92
|
+
return Math.max(0, computed.autoApproveAt - now) + AUTO_APPROVE_BUFFER_MS;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Arm the server-side fallback timer. Returns the auto state stored on the
|
|
97
|
+
* pending entry: { autoApproveAt, seconds, cancelled, timer }.
|
|
98
|
+
* `onFire` is invoked once when the deadline (+buffer) passes; callers must
|
|
99
|
+
* re-check the entry is still pending and not cancelled inside onFire.
|
|
100
|
+
* Instant (-1) fires SYNCHRONOUSLY — the caller MUST register the pending entry
|
|
101
|
+
* (and emit the pending frame) before calling this.
|
|
102
|
+
*/
|
|
103
|
+
export function createAutoApproveState(computed, onFire) {
|
|
104
|
+
const delay = autoApproveFireDelayMs(computed);
|
|
105
|
+
if (delay === null) return null; // disabled — never arm
|
|
106
|
+
const state = {
|
|
107
|
+
autoApproveAt: computed.autoApproveAt,
|
|
108
|
+
seconds: computed.seconds,
|
|
109
|
+
cancelled: false,
|
|
110
|
+
timer: null,
|
|
111
|
+
};
|
|
112
|
+
if (delay === 0) {
|
|
113
|
+
onFire();
|
|
114
|
+
return state;
|
|
115
|
+
}
|
|
116
|
+
state.timer = setTimeout(onFire, delay);
|
|
117
|
+
return state;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Cancel an armed auto-approve state: clears the timer and marks it cancelled.
|
|
122
|
+
* Accepts either the auto state itself or the whole pending entry ({ auto,
|
|
123
|
+
* autoCancelled }) — the entry form also stamps the durable entry-level flag so
|
|
124
|
+
* replays never re-arm. Returns true only on the first cancellation (so callers
|
|
125
|
+
* broadcast once).
|
|
126
|
+
*/
|
|
127
|
+
export function cancelAutoApproveState(autoOrEntry) {
|
|
128
|
+
if (!autoOrEntry) return false;
|
|
129
|
+
// Entry form: { auto, autoCancelled }
|
|
130
|
+
if (Object.prototype.hasOwnProperty.call(autoOrEntry, 'autoCancelled')) {
|
|
131
|
+
const entry = autoOrEntry;
|
|
132
|
+
if (entry.autoCancelled) return false;
|
|
133
|
+
entry.autoCancelled = true;
|
|
134
|
+
if (entry.auto) {
|
|
135
|
+
entry.auto.cancelled = true;
|
|
136
|
+
if (entry.auto.timer) {
|
|
137
|
+
clearTimeout(entry.auto.timer);
|
|
138
|
+
entry.auto.timer = null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
const auto = autoOrEntry;
|
|
144
|
+
if (auto.cancelled) return false;
|
|
145
|
+
auto.cancelled = true;
|
|
146
|
+
if (auto.timer) {
|
|
147
|
+
clearTimeout(auto.timer);
|
|
148
|
+
auto.timer = null;
|
|
149
|
+
}
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Fields merged into pending/replay frames for the client countdown UI. */
|
|
154
|
+
export function replayAutoApproveFields(auto, autoCancelled) {
|
|
155
|
+
return {
|
|
156
|
+
autoApproveAt: autoCancelled ? null : (auto?.autoApproveAt ?? null),
|
|
157
|
+
autoApproveSeconds: auto?.seconds ?? null,
|
|
158
|
+
autoCancelled: autoCancelled === true,
|
|
159
|
+
};
|
|
160
|
+
}
|