cc-viewer 1.9.2 → 1.9.3
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/README.md +0 -4
- package/dist/assets/{App-DPRn6oGn.js → App-BzXO6ATr.js} +2 -2
- package/dist/assets/{MdxEditorPanel-Ci4ldhd7.js → MdxEditorPanel-Ck8B5a5v.js} +1 -1
- package/dist/assets/{Mobile-Dq-KcO1s.js → Mobile-B8AkKb0z.js} +1 -1
- package/dist/assets/{ProxyStatsModal-CUFpqt1s.js → ProxyStatsModal-DsDVMf-c.js} +1 -1
- package/dist/assets/{index-Ci38xZiX.js → index-Dq5UPr6v.js} +2 -2
- package/dist/assets/seqResourceLoaders-BsQ6KXpV.js +2 -0
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/server/lib/sdk-manager.js +519 -70
- package/server/routes/events.js +47 -6
- package/server/routes/resume.js +12 -0
- package/dist/assets/seqResourceLoaders-B0p7e_lM.js +0 -2
package/server/routes/events.js
CHANGED
|
@@ -318,13 +318,42 @@ async function events(req, res, parsedUrl, isLocal, deps) {
|
|
|
318
318
|
// filter excludes legacy/other-instance dirs, so an empty result would otherwise show the
|
|
319
319
|
// WRONG project's/instance's conversation). Tell the client explicitly (same posture as
|
|
320
320
|
// sid-not-found) so it can surface an empty state instead of cross-project bleed.
|
|
321
|
-
|
|
322
|
-
|
|
321
|
+
//
|
|
322
|
+
// Liveness-aware (2026-10-10): a NEWLY created workspace's claude is alive but has not
|
|
323
|
+
// sent its first request yet, so it legitimately has no session dir. Flagging that as an
|
|
324
|
+
// error made the client detach back to the bound project — and the cold load below then
|
|
325
|
+
// streamed the BOUND project's current session (cross-project bleed). A LIVE instance
|
|
326
|
+
// therefore gets an EMPTY view instead: the cold stream is pinned to the empty source
|
|
327
|
+
// (load_start{total:0} → load_end, no chunks), the client stays on this instance, and the
|
|
328
|
+
// per-project live feed streams its first turn in place the moment the PTY writes its
|
|
329
|
+
// first session dir. Only a DEAD instance keeps the explicit sid-not-found posture (the
|
|
330
|
+
// client toasts + auto-detaches to the bound view — unchanged from before this fix).
|
|
331
|
+
const instanceNoSession = !!(instanceParam && !sidParam && !viewColdDir);
|
|
332
|
+
let instanceAlive = false;
|
|
333
|
+
if (instanceNoSession) {
|
|
334
|
+
try {
|
|
335
|
+
instanceAlive = listLivePtys().some((p) => p && p.instanceKey === instanceParam);
|
|
336
|
+
} catch (err) {
|
|
337
|
+
// Lookup failure degrades to the explicit dead-instance posture (never
|
|
338
|
+
// silently blank the client on a lookup error).
|
|
339
|
+
instanceAlive = false;
|
|
340
|
+
reportSwallowed('events.instance-live', err, { instance: instanceParam });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (instanceNoSession && !instanceAlive) {
|
|
323
344
|
try { sseWrite(res, `event: sid-not-found\ndata: ${JSON.stringify({ sid: null, instance: instanceParam, reason: 'instance-no-session', ts: Date.now() })}\n\n`); } catch { }
|
|
324
345
|
}
|
|
325
346
|
// When a view-scoped dir was requested AND resolved, it replaces the live-source
|
|
326
347
|
// cold load; otherwise fall back to the standard single-current-session source.
|
|
327
348
|
const coldSource = viewColdDir || null;
|
|
349
|
+
// An instance with no session dir has NO legitimate cold source — pin EVERY cold
|
|
350
|
+
// read to the literal empty source, whether the instance is alive (empty live view:
|
|
351
|
+
// the per-project feed streams its first turn in place once written) or dead (the
|
|
352
|
+
// client detaches on the sid-not-found above; the cold frames would be dropped by
|
|
353
|
+
// its stale guard anyway). Both fallback sites below (`coldSource || getLiveLogSource()`,
|
|
354
|
+
// on the legacy wire AND the v3 wire) would otherwise resolve the BOUND project's
|
|
355
|
+
// current session — the exact cross-project bleed this guard exists to stop.
|
|
356
|
+
const emptyLiveView = instanceNoSession;
|
|
328
357
|
// A client viewing a non-bound project needs that project's live feed running
|
|
329
358
|
// (its PTY is alive but its feed is lazy). Start it idempotently; the feed's
|
|
330
359
|
// per-project client filter keeps its entries scoped to this project's viewers.
|
|
@@ -352,7 +381,7 @@ async function events(req, res, parsedUrl, isLocal, deps) {
|
|
|
352
381
|
// Wire v3 (V3.S5): flagged + v2 source ⇒ the legacy full-entry cold stream
|
|
353
382
|
// is REPLACED by rows + native lines (the byte win); the client assembler
|
|
354
383
|
// rebuilds entries locally. v1 legacy files keep the entry pipeline.
|
|
355
|
-
const _v3Src = deps.wireV3 ? (coldSource || getLiveLogSource()) : null;
|
|
384
|
+
const _v3Src = (deps.wireV3 && !emptyLiveView) ? (coldSource || getLiveLogSource()) : null;
|
|
356
385
|
const v3Cold = !!(_v3Src && existsSync(join(_v3Src, 'journal.jsonl')));
|
|
357
386
|
|
|
358
387
|
// S6b: the cold-load source is the current v2 session dir when the v2
|
|
@@ -361,7 +390,7 @@ async function events(req, res, parsedUrl, isLocal, deps) {
|
|
|
361
390
|
// in-flight placeholder, which the client batch gate still blocks — the
|
|
362
391
|
// previous conversation is the better cold load there (interceptor.js
|
|
363
392
|
// getLiveLogSource header comment).
|
|
364
|
-
const coldLoadResult = v3Cold ? null : await streamRawEntriesAsync(coldSource || getLiveLogSource({ serveInFlight: !!deps.wireV3 }), async (raw) => {
|
|
393
|
+
const coldLoadResult = v3Cold ? null : await streamRawEntriesAsync(emptyLiveView ? '' : (coldSource || getLiveLogSource({ serveInFlight: !!deps.wireV3 })), async (raw) => {
|
|
365
394
|
// 直接发送原始 JSON 字符串,不做 parse/reconstruct/stringify
|
|
366
395
|
// ExitPlanMode V2 空 input 的条目按需补全 plan / planFilePath,其它原样透传
|
|
367
396
|
if (res.destroyed || !res.writable) return;
|
|
@@ -406,6 +435,12 @@ async function events(req, res, parsedUrl, isLocal, deps) {
|
|
|
406
435
|
// Pass 1 完成、Pass 2 开始前:发送 load_start
|
|
407
436
|
// 增量模式下不显示 loading 遮罩,非增量模式显示进度
|
|
408
437
|
const loadStartData = { total: totalCount, incremental: !!useIncremental };
|
|
438
|
+
// An instance with no session dir (new workspace / pruned while alive):
|
|
439
|
+
// flag the empty view so the client invalidates any view-cache snapshot it
|
|
440
|
+
// restored for this scope — otherwise an incremental `?since&cc=` resume
|
|
441
|
+
// would merge an empty delta onto the CACHED (now-deleted) conversation and
|
|
442
|
+
// keep rendering it. load_end fires right after with zero chunks.
|
|
443
|
+
if (emptyLiveView) loadStartData.empty = true;
|
|
409
444
|
// 分页模式下附加 hasMore/oldestTs(增量模式由客户端从缓存自行判断)
|
|
410
445
|
if (useLimit) {
|
|
411
446
|
loadStartData.hasMore = !!hasMore;
|
|
@@ -438,6 +473,9 @@ async function events(req, res, parsedUrl, isLocal, deps) {
|
|
|
438
473
|
for (const p of native.convPayloads) v3Bytes += p.length;
|
|
439
474
|
for (const p of native.respPayloads) v3Bytes += p.length;
|
|
440
475
|
const loadStartData = { total: meta.totalCount, incremental: !!useIncremental, v3Bytes };
|
|
476
|
+
// Same empty-view flag as the legacy onReady path above (unreachable on the
|
|
477
|
+
// v3 wire today — an empty source is never a v2 dir — but kept symmetric).
|
|
478
|
+
if (emptyLiveView) loadStartData.empty = true;
|
|
441
479
|
if (useLimit) {
|
|
442
480
|
loadStartData.hasMore = !!meta.hasMore;
|
|
443
481
|
loadStartData.oldestTs = meta.oldestTimestamp || '';
|
|
@@ -497,8 +535,11 @@ async function events(req, res, parsedUrl, isLocal, deps) {
|
|
|
497
535
|
sseWrite(res, `event: context_window\ndata: ${JSON.stringify(latestContextWindow)}\n\n`);
|
|
498
536
|
pushedContextWindow = true;
|
|
499
537
|
}
|
|
500
|
-
// Fallback: no MainAgent in log (e.g. fresh session after -c), read context-window.json
|
|
501
|
-
|
|
538
|
+
// Fallback: no MainAgent in log (e.g. fresh session after -c), read context-window.json.
|
|
539
|
+
// Skipped for an empty live view: CONTEXT_WINDOW_FILE is a single GLOBAL file (not
|
|
540
|
+
// project-scoped), so pushing it here would paint another project's usage onto a
|
|
541
|
+
// brand-new workspace. The first real main turn pushes a genuine measurement instead.
|
|
542
|
+
if (!pushedContextWindow && !emptyLiveView) {
|
|
502
543
|
try {
|
|
503
544
|
const cwRaw = readFileSync(CONTEXT_WINDOW_FILE, 'utf-8');
|
|
504
545
|
const cwFile = JSON.parse(cwRaw);
|
package/server/routes/resume.js
CHANGED
|
@@ -410,6 +410,18 @@ function postResumeSessionHandler(req, res, parsedUrl, isLocal, deps) {
|
|
|
410
410
|
return;
|
|
411
411
|
}
|
|
412
412
|
try {
|
|
413
|
+
// SDK mode: there is no claude PTY to inject `/resume` into — the sdk-manager
|
|
414
|
+
// switches the session identity directly (next user message resumes it).
|
|
415
|
+
// Read the env at request time (not module top) so tests can flip it per case.
|
|
416
|
+
if (process.env.CCV_SDK_MODE === '1') {
|
|
417
|
+
const sdk = await import('../lib/sdk-manager.js');
|
|
418
|
+
const r = sdk.switchToSession(sessionUuid);
|
|
419
|
+
if (r.ok) { sendJson(res, 200, { ok: true }); return; }
|
|
420
|
+
if (r.reason === 'busy') { sendJson(res, 409, { ok: false, reason: 'busy' }); return; }
|
|
421
|
+
if (r.reason === 'unavailable') { sendJson(res, 409, { ok: false, reason: 'unavailable' }); return; }
|
|
422
|
+
sendJson(res, 400, { ok: false, reason: r.reason || 'bad-uuid' });
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
413
425
|
const pm = await import('../pty-manager.js');
|
|
414
426
|
const targetProject = project || (_projectName || '');
|
|
415
427
|
// Multi-instance: a project-name-only resume that matches 2+ live same-basename
|