cc-viewer 1.9.4 → 1.9.6
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 +5 -2
- package/dist/assets/App-GhvyKB0E.js +2 -0
- package/dist/assets/{MdxEditorPanel-C8Cij93y.js → MdxEditorPanel-DwwKGPIQ.js} +1 -1
- package/dist/assets/{Mobile-OKCVi8un.js → Mobile-CGiDFGRO.js} +1 -1
- package/dist/assets/{ProxyStatsModal-DGlngtM9.js → ProxyStatsModal-CI5PkF_B.js} +1 -1
- package/dist/assets/{index-Clt4dw8Y.js → index-D0_0_HIv.js} +2 -2
- package/dist/assets/seqResourceLoaders-DHr1E1Ye.js +2 -0
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/server/interceptor.js +165 -14
- package/server/lib/git-diff.js +14 -8
- package/server/lib/scope-router.js +68 -0
- package/server/lib/sdk-manager.js +155 -32
- package/server/lib/sdk-session-index.js +25 -0
- package/server/lib/task-state.js +74 -34
- package/server/lib/turn-end-bridge.js +6 -1
- package/server/lib/v2/live-feed.js +7 -6
- package/server/lib/v2/view-router.js +48 -0
- package/server/lib/view-root.js +30 -10
- package/server/lib/workflow-watcher.js +13 -6
- package/server/lib/ws-mux.js +492 -0
- package/server/pty-manager.js +53 -0
- package/server/routes/ask-perm.js +13 -8
- package/server/routes/events.js +185 -86
- package/server/routes/project-meta.js +11 -4
- package/server/routes/proxy-stats.js +8 -4
- package/server/routes/resume.js +64 -10
- package/server/routes/session-pin.js +12 -2
- package/server/routes/workflow-journal.js +5 -2
- package/server/routes/workspaces.js +7 -1
- package/server/server.js +353 -120
- package/dist/assets/App-DtBVNYE7.js +0 -2
- package/dist/assets/seqResourceLoaders-zlIVjapi.js +0 -2
|
@@ -38,6 +38,54 @@ export function filterClientsByViewProject(clientList, project, boundProject) {
|
|
|
38
38
|
return clientList.filter((c) => c && ((c._ccvViewProject || bound) === project));
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Instance-narrowed variant of filterClientsByViewProject (multi-instance,
|
|
43
|
+
* 2026-10-06 rule lifted from live-feed._clientsForInstance): a frame stamped
|
|
44
|
+
* with an instance is visible to every viewer of the project EXCEPT clients
|
|
45
|
+
* stamped with a DIFFERENT instance. Unset on either side ⇒ visible (a client
|
|
46
|
+
* with no `_ccvViewInstance` stamp sees every instance; a frame with no
|
|
47
|
+
* instance reaches all of the project's viewers).
|
|
48
|
+
*
|
|
49
|
+
* @param {Array} clientList - SSE response objects.
|
|
50
|
+
* @param {string} project - the frame's project ('' ⇒ bound).
|
|
51
|
+
* @param {string|null} instance - the frame's instance, when known.
|
|
52
|
+
* @param {string} boundProject - the server's bound project.
|
|
53
|
+
* @returns {Array} the subset of clients that should receive the frame.
|
|
54
|
+
*/
|
|
55
|
+
export function filterClientsByScope(clientList, project, instance, boundProject) {
|
|
56
|
+
// Composition over duplication: project half, then the shared instance rule —
|
|
57
|
+
// the two can never drift apart.
|
|
58
|
+
return filterClientsByInstance(filterClientsByViewProject(clientList, project, boundProject), instance);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Instance-only narrowing over an ALREADY project-filtered client list — the
|
|
63
|
+
* live-feed composes its own project filter (`opts.clientFilter`, which may be
|
|
64
|
+
* a server-supplied closure rather than the pure predicate) with this rule.
|
|
65
|
+
* Same semantics as the instance half of filterClientsByScope: unset on either
|
|
66
|
+
* side ⇒ visible; nobody instance-stamped ⇒ the input array (same reference).
|
|
67
|
+
*
|
|
68
|
+
* @param {Array} clientList - already project-filtered SSE responses.
|
|
69
|
+
* @param {string|null} instance - the frame's instance, when known.
|
|
70
|
+
* @returns {Array}
|
|
71
|
+
*/
|
|
72
|
+
export function filterClientsByInstance(clientList, instance) {
|
|
73
|
+
if (!instance) return clientList;
|
|
74
|
+
// Exclude ONLY clients explicitly stamped with a DIFFERENT instance. An
|
|
75
|
+
// UNSTAMPED (project-level) viewer of the same project must also receive the
|
|
76
|
+
// frame: if an earlier `active:true` frame reached it while no instance viewer
|
|
77
|
+
// was online, the matching `active:false` must reach it too or its spinner
|
|
78
|
+
// never clears (the clear would be narrowed away the moment a parallel
|
|
79
|
+
// instance chip connects). Keeping active/clear on the SAME recipient set
|
|
80
|
+
// avoids that split delivery.
|
|
81
|
+
// Fast path: nobody is excluded (no differently-stamped client) ⇒ return the
|
|
82
|
+
// SAME array reference (no filter alloc), preserving legacy identity.
|
|
83
|
+
let anyExcluded = false;
|
|
84
|
+
for (const c of clientList) { if (c && c._ccvViewInstance && c._ccvViewInstance !== instance) { anyExcluded = true; break; } }
|
|
85
|
+
if (!anyExcluded) return clientList;
|
|
86
|
+
return clientList.filter((c) => !c || !c._ccvViewInstance || c._ccvViewInstance === instance);
|
|
87
|
+
}
|
|
88
|
+
|
|
41
89
|
/**
|
|
42
90
|
* Extract the owning project dir name from a session dir path of the form
|
|
43
91
|
* `<LOG_DIR>/<projectSan>/sessions/<dirName>`. Returns the on-disk `<projectSan>`
|
package/server/lib/view-root.js
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
import { realpathSync, existsSync, statSync } from 'node:fs';
|
|
28
28
|
import { projectKeyForCwd } from './system-prompt-snapshots.js';
|
|
29
|
+
import { listSdkSessionInstances } from './sdk-session-index.js';
|
|
29
30
|
|
|
30
31
|
// A project name must be sanitizer-stable: projectKeyForCwd maps every legal
|
|
31
32
|
// basename to [a-zA-Z0-9_\-\.] (collapsing only ALL-dot degenerate names to
|
|
@@ -49,11 +50,13 @@ function isValidProjectName(name) {
|
|
|
49
50
|
* @param {string} opts.boundCwd - the bound project root (CCV_PROJECT_DIR || process.cwd()).
|
|
50
51
|
* @param {() => Array<{path:string, projectName:string}>} opts.loadWorkspaces - registry reader.
|
|
51
52
|
* @param {() => Array<{cwd:string, instanceKey?:string}>} opts.listLivePtys - live PTY records.
|
|
52
|
-
* @returns {{ ok:true, root:string, via:'bound'|'registry'|'live'|'live-instance' } | { ok:false, status:number, error:string }}
|
|
53
|
+
* @returns {{ ok:true, root:string, projectName:string, via:'bound'|'registry'|'live'|'live-instance' } | { ok:false, status:number, error:string }}
|
|
54
|
+
* `root` is the realpath'd real dir; `projectName` is the RAW-path store key
|
|
55
|
+
* (basename of the matched candidate BEFORE realpath) — the wire/log-store name.
|
|
56
|
+
* They differ for a leaf-symlinked workspace, and the store is keyed by the raw name.
|
|
53
57
|
*/
|
|
54
|
-
export function resolveViewRoot({ projectParam, instanceParam, boundCwd, loadWorkspaces, listLivePtys } = {}) {
|
|
55
|
-
const bound = boundCwd || process.cwd()
|
|
56
|
-
// Multi-instance (2026-10-06): `instance` is the per-spawn instanceKey, the
|
|
58
|
+
export function resolveViewRoot({ projectParam, instanceParam, boundCwd, loadWorkspaces, listLivePtys, listSdkSessions = listSdkSessionInstances } = {}) {
|
|
59
|
+
const bound = boundCwd || process.cwd();// Multi-instance (2026-10-06): `instance` is the per-spawn instanceKey, the
|
|
57
60
|
// authoritative disambiguator when two same-basename projects run at once —
|
|
58
61
|
// the name path below would 400 on that pair as "ambiguous". Resolve it FIRST
|
|
59
62
|
// (before the empty-name bound short-circuit AND name validation): a valid live
|
|
@@ -67,13 +70,21 @@ export function resolveViewRoot({ projectParam, instanceParam, boundCwd, loadWor
|
|
|
67
70
|
const rec = (listLivePtys() || []).find((p) => p && p.instanceKey === instance);
|
|
68
71
|
if (rec && typeof rec.cwd === 'string' && rec.cwd) {
|
|
69
72
|
if (existsSync(rec.cwd) && statSync(rec.cwd).isDirectory()) {
|
|
70
|
-
return { ok: true, root: realpathSync(rec.cwd), via: 'live-instance' };
|
|
73
|
+
return { ok: true, root: realpathSync(rec.cwd), projectName: projectKeyForCwd(rec.cwd), via: 'live-instance' };
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
} catch { /* pty map unavailable / fs race → fall through to name path */ }
|
|
77
|
+
try {
|
|
78
|
+
const rec = (listSdkSessions() || []).find((s) => s && s.instanceKey === instance);
|
|
79
|
+
if (rec && typeof rec.cwd === 'string' && rec.cwd) {
|
|
80
|
+
if (existsSync(rec.cwd) && statSync(rec.cwd).isDirectory()) {
|
|
81
|
+
return { ok: true, root: realpathSync(rec.cwd), projectName: projectKeyForCwd(rec.cwd), via: 'live-instance' };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch { /* SDK registry unavailable / fs race → fall through to name path */ }
|
|
74
85
|
}
|
|
75
86
|
const name = (typeof projectParam === 'string' && projectParam) || '';
|
|
76
|
-
if (!name) return { ok: true, root: bound, via: 'bound' };
|
|
87
|
+
if (!name) return { ok: true, root: bound, projectName: projectKeyForCwd(bound), via: 'bound' };
|
|
77
88
|
if (!isValidProjectName(name)) {
|
|
78
89
|
return { ok: false, status: 400, error: 'invalid project name' };
|
|
79
90
|
}
|
|
@@ -103,6 +114,14 @@ export function resolveViewRoot({ projectParam, instanceParam, boundCwd, loadWor
|
|
|
103
114
|
}
|
|
104
115
|
}
|
|
105
116
|
} catch { /* pty map unavailable → registry-only */ }
|
|
117
|
+
try {
|
|
118
|
+
for (const s of listSdkSessions() || []) {
|
|
119
|
+
const cwd = s && s.cwd;
|
|
120
|
+
if (typeof cwd === 'string' && cwd && projectKeyForCwd(cwd) === name) {
|
|
121
|
+
candidates.push({ path: cwd, via: 'live' });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch { /* SDK registry unavailable → PTY/registry-only */ }
|
|
106
125
|
|
|
107
126
|
// Dedupe by path string (a project live AND registered under the SAME path
|
|
108
127
|
// is one candidate). Keys are the raw candidate paths — not realpathSync —
|
|
@@ -132,7 +151,7 @@ export function resolveViewRoot({ projectParam, instanceParam, boundCwd, loadWor
|
|
|
132
151
|
if (!existsSync(root) || !statSync(root).isDirectory()) {
|
|
133
152
|
return { ok: false, status: 404, error: 'project dir missing' };
|
|
134
153
|
}
|
|
135
|
-
return { ok: true, root: realpathSync(root), via: 'live' };
|
|
154
|
+
return { ok: true, root: realpathSync(root), projectName: projectKeyForCwd(root), via: 'live' };
|
|
136
155
|
} catch {
|
|
137
156
|
return { ok: false, status: 404, error: 'project dir unreadable' };
|
|
138
157
|
}
|
|
@@ -156,7 +175,7 @@ export function resolveViewRoot({ projectParam, instanceParam, boundCwd, loadWor
|
|
|
156
175
|
}
|
|
157
176
|
const root = alive[0].path;
|
|
158
177
|
try {
|
|
159
|
-
return { ok: true, root: realpathSync(root), via: alive[0].via };
|
|
178
|
+
return { ok: true, root: realpathSync(root), projectName: projectKeyForCwd(root), via: alive[0].via };
|
|
160
179
|
} catch {
|
|
161
180
|
return { ok: false, status: 404, error: 'project dir unreadable' };
|
|
162
181
|
}
|
|
@@ -166,7 +185,7 @@ export function resolveViewRoot({ projectParam, instanceParam, boundCwd, loadWor
|
|
|
166
185
|
if (!existsSync(root) || !statSync(root).isDirectory()) {
|
|
167
186
|
return { ok: false, status: 404, error: 'project dir missing' };
|
|
168
187
|
}
|
|
169
|
-
return { ok: true, root: realpathSync(root), via: uniq[0].via };
|
|
188
|
+
return { ok: true, root: realpathSync(root), projectName: projectKeyForCwd(root), via: uniq[0].via };
|
|
170
189
|
} catch {
|
|
171
190
|
return { ok: false, status: 404, error: 'project dir unreadable' };
|
|
172
191
|
}
|
|
@@ -192,13 +211,14 @@ export function resolveViewRoot({ projectParam, instanceParam, boundCwd, loadWor
|
|
|
192
211
|
* @param {string} [opts.bodyInstance] - POST-body instance override (wins over query).
|
|
193
212
|
* @returns {string|null} the resolved absolute root, or null (already replied).
|
|
194
213
|
*/
|
|
195
|
-
export function viewRootOrReply(req, res, parsedUrl, { boundCwd, loadWorkspaces, listLivePtys, bodyProject, bodyInstance } = {}) {
|
|
214
|
+
export function viewRootOrReply(req, res, parsedUrl, { boundCwd, loadWorkspaces, listLivePtys, listSdkSessions, bodyProject, bodyInstance } = {}) {
|
|
196
215
|
const r = resolveViewRoot({
|
|
197
216
|
projectParam: (typeof bodyProject === 'string' && bodyProject) || parsedUrl?.searchParams?.get('project'),
|
|
198
217
|
instanceParam: (typeof bodyInstance === 'string' && bodyInstance) || parsedUrl?.searchParams?.get('instance'),
|
|
199
218
|
boundCwd,
|
|
200
219
|
loadWorkspaces,
|
|
201
220
|
listLivePtys,
|
|
221
|
+
listSdkSessions,
|
|
202
222
|
});
|
|
203
223
|
if (!r.ok) {
|
|
204
224
|
res.writeHead(r.status, { 'Content-Type': 'application/json' });
|
|
@@ -39,7 +39,7 @@ function _signature(p) {
|
|
|
39
39
|
function _isJournalFile(f) { return f.startsWith('wf_') && f.endsWith('.json'); }
|
|
40
40
|
|
|
41
41
|
function _scan(state) {
|
|
42
|
-
const { workflowsDir, sessionId, project, clients, seen } = state;
|
|
42
|
+
const { workflowsDir, sessionId, project, clients, clientFilter, seen } = state;
|
|
43
43
|
let files;
|
|
44
44
|
try { files = readdirSync(workflowsDir); } catch { return; }
|
|
45
45
|
for (const f of files) {
|
|
@@ -51,7 +51,11 @@ function _scan(state) {
|
|
|
51
51
|
seen.set(f, sig);
|
|
52
52
|
const data = readNormalizedJournal(p);
|
|
53
53
|
if (!data) continue;
|
|
54
|
-
|
|
54
|
+
// Per-workspace: a journal belongs to one project; when a client filter is
|
|
55
|
+
// installed the frame reaches only that project's viewers (legacy default:
|
|
56
|
+
// broadcast to all).
|
|
57
|
+
const target = typeof clientFilter === 'function' ? clientFilter(clients, project) : clients;
|
|
58
|
+
sendEventToClients(target, 'workflow_update', {
|
|
55
59
|
sessionId,
|
|
56
60
|
project: project || null,
|
|
57
61
|
runId: data.runId,
|
|
@@ -99,7 +103,7 @@ function _baseline(state) {
|
|
|
99
103
|
* 武装对某 workflows 目录的监视。同目录重复调用只刷新 clients 引用。
|
|
100
104
|
* @param {{ workflowsDir: string, sessionId?: string, project?: string, clients: Array }} opts
|
|
101
105
|
*/
|
|
102
|
-
export function armWorkflowWatch({ workflowsDir, sessionId, project, clients } = {}) {
|
|
106
|
+
export function armWorkflowWatch({ workflowsDir, sessionId, project, clients, clientFilter } = {}) {
|
|
103
107
|
if (!workflowsDir || !Array.isArray(clients)) return null;
|
|
104
108
|
|
|
105
109
|
const existing = _armed.get(workflowsDir);
|
|
@@ -107,6 +111,7 @@ export function armWorkflowWatch({ workflowsDir, sessionId, project, clients } =
|
|
|
107
111
|
|
|
108
112
|
const state = {
|
|
109
113
|
workflowsDir, sessionId, project, clients,
|
|
114
|
+
clientFilter: typeof clientFilter === 'function' ? clientFilter : null,
|
|
110
115
|
seen: new Map(), watcher: null,
|
|
111
116
|
debounceTimer: null, safetyTimer: null, presenceTimer: null,
|
|
112
117
|
};
|
|
@@ -177,13 +182,14 @@ function _authoritativeJournalExists(runDir, runId) {
|
|
|
177
182
|
}
|
|
178
183
|
|
|
179
184
|
function _scanLive(state) {
|
|
180
|
-
const { runDir, runId, sessionId, project, clients } = state;
|
|
185
|
+
const { runDir, runId, sessionId, project, clients, clientFilter } = state;
|
|
181
186
|
const data = deriveLiveJournal(runDir, runId);
|
|
182
187
|
if (data) {
|
|
183
188
|
const sig = _liveSignature(data);
|
|
184
189
|
if (sig !== state.lastSig) { // 有实质变化才广播
|
|
185
190
|
state.lastSig = sig;
|
|
186
|
-
|
|
191
|
+
const target = typeof clientFilter === 'function' ? clientFilter(clients, project) : clients;
|
|
192
|
+
sendEventToClients(target, 'workflow_update', {
|
|
187
193
|
sessionId,
|
|
188
194
|
project: project || null,
|
|
189
195
|
runId: data.runId,
|
|
@@ -228,7 +234,7 @@ function _startLiveWatch(state) {
|
|
|
228
234
|
* 同 runDir 重复调用只刷新 clients。arm 时立即广播一次当前推导态(闭合 REST/arm 竞态)。
|
|
229
235
|
* @param {{ runDir: string, runId: string, sessionId?: string, project?: string, clients: Array }} opts
|
|
230
236
|
*/
|
|
231
|
-
export function armWorkflowLiveWatch({ runDir, runId, sessionId, project, clients } = {}) {
|
|
237
|
+
export function armWorkflowLiveWatch({ runDir, runId, sessionId, project, clients, clientFilter } = {}) {
|
|
232
238
|
if (!runDir || !runId || !Array.isArray(clients)) return null;
|
|
233
239
|
|
|
234
240
|
const existing = _armedLive.get(runDir);
|
|
@@ -236,6 +242,7 @@ export function armWorkflowLiveWatch({ runDir, runId, sessionId, project, client
|
|
|
236
242
|
|
|
237
243
|
const state = {
|
|
238
244
|
runDir, runId, sessionId, project, clients,
|
|
245
|
+
clientFilter: typeof clientFilter === 'function' ? clientFilter : null,
|
|
239
246
|
lastSig: null, watcher: null,
|
|
240
247
|
debounceTimer: null, safetyTimer: null, presenceTimer: null,
|
|
241
248
|
};
|