cc-viewer 1.9.13 → 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 (33) 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-Db1nmcJV.js → MdxEditorPanel-DRLFmdU5.js} +1 -1
  4. package/dist/assets/Mobile-C8VJqBwa.js +1 -0
  5. package/dist/assets/{ProxyStatsModal-Ch1BH0Kp.js → ProxyStatsModal-Ba1E1lu4.js} +1 -1
  6. package/dist/assets/{index-CAiC6cw-.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/context-rules.js +6 -2
  12. package/node_modules/@ccv/core/src/queued-prompt.js +66 -0
  13. package/node_modules/@ccv/core/src/v2-transcript-normalizer.js +78 -14
  14. package/package.json +1 -1
  15. package/server/lib/chat-queue.js +350 -120
  16. package/server/lib/model-system-prompts.js +4 -1
  17. package/server/lib/scheduled-task-executor.js +3 -1
  18. package/server/lib/sdk-manager.js +282 -68
  19. package/server/lib/v2/conversation-store.js +15 -11
  20. package/server/lib/v2/v2-writer.js +1 -1
  21. package/server/lib/view-root.js +4 -4
  22. package/server/pty-manager.js +8 -8
  23. package/server/routes/files-content.js +108 -14
  24. package/server/routes/files-fs.js +122 -36
  25. package/server/routes/resume.js +1 -1
  26. package/server/routes/sdk-context.js +13 -2
  27. package/server/server.js +128 -28
  28. package/server/system-prompt-templates/presets/kimi-k3.md +4 -0
  29. package/dist/assets/App-BzOM1_y-.js +0 -2
  30. package/dist/assets/App-DU5jmGrx.css +0 -1
  31. package/dist/assets/Mobile-rjfABa-U.js +0 -1
  32. package/dist/assets/seqResourceLoaders-CxUFE36z.css +0 -41
  33. package/dist/assets/seqResourceLoaders-Ds8-_QA3.js +0 -2
package/server/server.js CHANGED
@@ -1738,7 +1738,7 @@ function registerUpgradeRoute(httpServer, name, match, handle) {
1738
1738
  async function setupTerminalWebSocket(httpServer) {
1739
1739
  try {
1740
1740
  const { WebSocketServer } = await import('ws');
1741
- const { writeToPty, writeToPtySequential, writeToPtyFor, writeToPtySequentialFor, resizePty, onPtyData, onPtyExit, getPtyState, getPtyKind, getOutputBuffer, getCurrentWorkspace, spawnShell, findSafeSliceStart, attachPtyFor, listLivePtys } = await import('./pty-manager.js');
1741
+ const { writeToPty, writeToPtySequential, writeToPtyFor, writeToPtySequentialFor, resizePty, onPtyData, onPtyExit, getPtyState, getPtyKind, getPtyKindFor, getOutputBuffer, getCurrentWorkspace, spawnShell, findSafeSliceStart, attachPtyFor, listLivePtys } = await import('./pty-manager.js');
1742
1742
  const {
1743
1743
  spawnScratch,
1744
1744
  writeScratch,
@@ -1770,13 +1770,40 @@ async function setupTerminalWebSocket(httpServer) {
1770
1770
  if (!isSdkMode) {
1771
1771
  chatQueue.initChatQueue({
1772
1772
  writeToPty,
1773
+ writeToPtyFor,
1773
1774
  writeToPtySequential,
1774
1775
  writeToPtySequentialFor,
1775
1776
  getPtyKind,
1776
1777
  isStreaming: () => streamingState.active,
1777
1778
  hasPendingApproval: () => pendingAskHooks.size > 0 || pendingPermHooks.size > 0,
1778
1779
  hasExternalInjection: () => imCore.anyActiveInjection(),
1779
- broadcastWs: broadcastWsMessage,
1780
+ hasLivePtyFor: (anchor) => {
1781
+ if (!anchor || typeof anchor !== 'object') return undefined;
1782
+ if (anchor.instanceKey) return getPtyKindFor({ instanceKey: anchor.instanceKey }) === 'claude';
1783
+ if (anchor.project) return listLivePtys().some((pty) => pty.ptyKind === 'claude' && projectKeyForCwd(pty.cwd) === anchor.project);
1784
+ return undefined;
1785
+ },
1786
+ broadcastWs: (msg, item) => {
1787
+ if (msg.type !== 'queue-state') {
1788
+ const scope = item?.anchor || msg.scope;
1789
+ const { scope: _privateScope, ...publicMsg } = msg;
1790
+ broadcastWsMessage(scope ? { ...publicMsg, project: scope.project, instanceKey: scope.instanceKey } : publicMsg);
1791
+ return;
1792
+ }
1793
+ if (!terminalWss) return;
1794
+ terminalWss.clients.forEach((client) => {
1795
+ if (client.readyState !== 1) return;
1796
+ const view = client._ccvViewInstance
1797
+ ? { instanceKey: client._ccvViewInstance, project: client._ccvViewProject || undefined }
1798
+ : { project: client._ccvViewProject || _projectName || '' };
1799
+ const items = chatQueue.getSnapshot(view.project || view.instanceKey ? view : null);
1800
+ const project = view.project || _projectName || undefined;
1801
+ const instanceKey = view.instanceKey;
1802
+ const { scope: _privateScope, ...publicMsg } = msg;
1803
+ const payload = { ...publicMsg, items, ...(project ? { project } : {}), ...(instanceKey ? { instanceKey } : {}) };
1804
+ try { client.send(JSON.stringify(payload)); } catch (err) { reportSwallowed('chat-queue.client-send', err); }
1805
+ });
1806
+ },
1780
1807
  });
1781
1808
  }
1782
1809
  const wss = new WebSocketServer({ noServer: true });
@@ -2143,31 +2170,53 @@ async function setupTerminalWebSocket(httpServer) {
2143
2170
  // workspace's approval/spinner/queue would pop into the foreground. Filter each
2144
2171
  // snapshot by this connection's `_ccvViewProject` (bound view / no ?project= → all).
2145
2172
  const _viewProj = ws._ccvViewProject || null;
2173
+ const replayQueueSnapshot = () => {
2174
+ const anchor = ws._ccvViewInstance
2175
+ ? { instanceKey: ws._ccvViewInstance, project: ws._ccvViewProject || undefined }
2176
+ : (ws._ccvViewProject ? { project: ws._ccvViewProject } : (isSdkMode ? null : { project: _projectName || undefined }));
2177
+ const items = (isSdkMode && _sdkGetQueueSnapshot)
2178
+ ? _sdkGetQueueSnapshot(anchor)
2179
+ : chatQueue.getSnapshot(anchor);
2180
+ if (!Array.isArray(items)) return;
2181
+ const frame = { type: 'queue-state', items };
2182
+ if (ws._ccvViewProject || (!isSdkMode && _projectName)) frame.project = ws._ccvViewProject || _projectName;
2183
+ if (ws._ccvViewInstance) frame.instanceKey = ws._ccvViewInstance;
2184
+ if (_sdkFrameForView(frame)) {
2185
+ try { ws.send(JSON.stringify(frame)); } catch (err) { reportSwallowed('queue.replay-send', err); }
2186
+ }
2187
+ };
2146
2188
  const _sdkFrameForView = (frame) => {
2147
2189
  if (!frame || typeof frame !== 'object') return true; // no cwd → view-agnostic, replay
2148
- if (!_viewProj) return true; // bound view → all workspaces
2190
+ const viewProject = ws._ccvViewProject || null;
2191
+ if (!viewProject) return true; // bound view → all workspaces
2149
2192
  // The view stamp is a SANITIZED project key (projectKeyForCwd), so compare against the
2150
2193
  // frame's cwd sanitized the same way — a raw basename would silently drop legit frames
2151
2194
  // for space/CJK workspace names.
2152
2195
  const fproj = (typeof frame.cwd === 'string' && frame.cwd) ? (projectKeyForCwd(frame.cwd) || null) : null;
2153
- return !fproj || fproj === _viewProj; // no-cwd frame → replay; else must match
2196
+ return !fproj || fproj === viewProject; // no-cwd frame → replay; else must match
2154
2197
  };
2155
-
2156
- // Replay SDK 模式 pendings(sdk-manager canUseTool 的 ask/plan/perm):
2157
- // 与上面 perm-hook 同理,但快照来自 sdk-manager(_pendingApprovals)。
2158
- // ask 类带 startedAt/timeoutMs —— 与 ask-hook replay 同款剩余时间重算。
2159
- if (isSdkMode && _sdkGetPendingApprovals) {
2198
+ const replaySdkPendingApprovals = ({ planOnly = false } = {}) => {
2199
+ if (!_sdkGetPendingApprovals) return;
2160
2200
  for (const p of _sdkGetPendingApprovals() || []) {
2161
2201
  if (!p || !p.replay || !p.id) continue;
2202
+ if (planOnly && p.kind !== 'plan') continue;
2162
2203
  const payload = { ...p.replay };
2163
- if (!_sdkFrameForView(payload)) continue; // 别的工作区的待审批不进本视图
2204
+ if (!_sdkFrameForView(payload)) continue;
2205
+ // The generic replay filter is project-scoped. When an exact instance
2206
+ // is selected, require ownership metadata too instead of guessing from cwd.
2207
+ if (ws._ccvViewInstance && payload.instanceKey !== ws._ccvViewInstance) continue;
2164
2208
  if (p.kind === 'ask') {
2165
- payload.startedAt = now; // 让前端按"还剩 remaining"起算
2166
- payload.timeoutMs = p.remainingMs; // 剩余可用时间
2209
+ payload.startedAt = Date.now();
2210
+ payload.timeoutMs = p.remainingMs;
2167
2211
  }
2168
- try { ws.send(JSON.stringify(payload)); } catch {}
2212
+ try { ws.send(JSON.stringify(payload)); } catch (err) { reportSwallowed('sdk.approval-replay', err); }
2169
2213
  }
2170
- }
2214
+ };
2215
+
2216
+ // Replay SDK 模式 pendings(sdk-manager canUseTool 的 ask/plan/perm):
2217
+ // 与上面 perm-hook 同理,但快照来自 sdk-manager(_pendingApprovals)。
2218
+ // ask 类带 startedAt/timeoutMs —— 与 ask-hook replay 同款剩余时间重算。
2219
+ if (isSdkMode) replaySdkPendingApprovals();
2171
2220
 
2172
2221
  // Replay sdk-init snapshot so a refreshed/reconnected page restores slash-command
2173
2222
  // surface and model info without waiting for the next session.
@@ -2203,10 +2252,13 @@ async function setupTerminalWebSocket(httpServer) {
2203
2252
  }
2204
2253
 
2205
2254
  // Replay the last sdk-context-usage snapshot so a reconnected panel restores the
2206
- // latest numbers (null snapshot → no frame, the panel falls back to its HTTP pull).
2255
+ // latest numbers. Anchor by view instance (exact) else view project — same precedence
2256
+ // as sdk-skills — and reject a missing/foreign instance when the viewer is pinned to
2257
+ // one (context-only filter; other replay frames keep project-level semantics).
2258
+ // Null snapshot → no frame, the panel falls back to its HTTP pull.
2207
2259
  if (isSdkMode && _sdkGetContextUsageSnapshot) {
2208
- const cuSnap = _sdkGetContextUsageSnapshot();
2209
- if (cuSnap && _sdkFrameForView(cuSnap)) {
2260
+ const cuSnap = sdkContextReplayFrame(_sdkGetContextUsageSnapshot, _viewProj, ws._ccvViewInstance);
2261
+ if (cuSnap) {
2210
2262
  try { ws.send(JSON.stringify(cuSnap)); } catch {}
2211
2263
  }
2212
2264
  }
@@ -2222,13 +2274,7 @@ async function setupTerminalWebSocket(httpServer) {
2222
2274
 
2223
2275
  // Replay the chat-composer busy queue so a refreshed/reconnected page restores the
2224
2276
  // floating queued-message bubbles (server is the source of truth in both modes).
2225
- {
2226
- const qItems = (isSdkMode && _sdkGetQueueSnapshot) ? _sdkGetQueueSnapshot() : chatQueue.getSnapshot();
2227
- if (Array.isArray(qItems) && qItems.length) {
2228
- const qFrame = { type: 'queue-state', items: qItems };
2229
- if (_sdkFrameForView(qFrame)) { try { ws.send(JSON.stringify(qFrame)); } catch {} }
2230
- }
2231
- }
2277
+ replayQueueSnapshot();
2232
2278
 
2233
2279
  // 兜底重绘标记:claude TUI 在 alternate-screen 下只在收到 SIGWINCH 时重绘整屏。
2234
2280
  // 若前端首次 resize 与 PTY 当前尺寸恰好相等,pty.resize noop 不发 SIGWINCH → 前端空白。
@@ -2384,6 +2430,11 @@ async function setupTerminalWebSocket(httpServer) {
2384
2430
  }
2385
2431
  }
2386
2432
  }
2433
+ // View changes use attach rather than a new connection: restore that view's
2434
+ // pending approvals too, so a plan cleared on switch-away can be answered
2435
+ // after switching back instead of waiting for the server timeout.
2436
+ replaySdkPendingApprovals({ planOnly: true });
2437
+ replayQueueSnapshot();
2387
2438
  return;
2388
2439
  }
2389
2440
  } else if (isSdkMode && (msg.type === 'input' || msg.type === 'input-sequential'
@@ -2494,12 +2545,20 @@ async function setupTerminalWebSocket(httpServer) {
2494
2545
  let res = null;
2495
2546
  try { res = (target || targetInstance) ? attachPtyFor({ project: target || undefined, instanceKey: targetInstance || undefined }) : null; } catch { res = null; }
2496
2547
  if (res && res.ok) {
2548
+ ws._ccvViewProject = target || null;
2549
+ ws._ccvViewInstance = targetInstance || null;
2550
+ replayQueueSnapshot();
2497
2551
  try { ws.send(JSON.stringify({ type: 'attached', project: target, running: res.running, ptyKind: res.ptyKind, exitCode: res.exitCode })); } catch { }
2498
2552
  const snap = getOutputBuffer();
2499
2553
  if (snap) {
2500
2554
  try { ws.send(JSON.stringify({ type: 'data', data: snap })); } catch { }
2501
2555
  }
2502
2556
  } else {
2557
+ // The queue belongs to the view, not to a live PTY. A history view or
2558
+ // a dead instance still needs its own queue snapshot after re-stamping.
2559
+ ws._ccvViewProject = target || null;
2560
+ ws._ccvViewInstance = targetInstance || null;
2561
+ replayQueueSnapshot();
2503
2562
  // Multi-instance: surface an ambiguous project-name attach (with the live
2504
2563
  // candidates) so the client re-issues with an instanceKey instead of silently
2505
2564
  // pinning an arbitrary same-cwd process.
@@ -2722,6 +2781,14 @@ async function setupTerminalWebSocket(httpServer) {
2722
2781
  try { ws.send(cmsg); } catch {}
2723
2782
  }
2724
2783
  }
2784
+ } else if (msg.type === 'sdk-plan-auto-cancel') {
2785
+ // Cancellation stops the automatic timers on other viewers too. It is
2786
+ // not an approval/denial: the plan remains pending for manual review.
2787
+ if (_sdkResolveApproval && msg.id) broadcastWsMessage({
2788
+ type: 'sdk-plan-auto-cancel', id: msg.id,
2789
+ cwd: msg.cwd || null, project: msg.project || null,
2790
+ instanceKey: msg.instanceKey || null,
2791
+ });
2725
2792
  } else if (msg.type === 'sdk-plan-answer') {
2726
2793
  // Plan approval in SDK mode
2727
2794
  if (_sdkResolveApproval) {
@@ -2772,7 +2839,7 @@ async function setupTerminalWebSocket(httpServer) {
2772
2839
  });
2773
2840
  }
2774
2841
  } else {
2775
- const _qAnchor = { project: typeof msg.project === 'string' ? msg.project : undefined, sessionId: typeof msg.sessionId === 'string' ? msg.sessionId : undefined, instanceKey: typeof msg.instanceKey === 'string' ? msg.instanceKey : undefined };
2842
+ const _qAnchor = { project: (typeof msg.project === 'string' && msg.project) || _projectName || undefined, sessionId: typeof msg.sessionId === 'string' ? msg.sessionId : undefined, instanceKey: typeof msg.instanceKey === 'string' ? msg.instanceKey : undefined };
2776
2843
  const _anchorArg = (_qAnchor.project || _qAnchor.sessionId || _qAnchor.instanceKey) ? _qAnchor : undefined;
2777
2844
  try { chatQueue.enqueue(msg.text, _anchorArg); } catch (err) { reportSwallowed('server.queue-message', err); }
2778
2845
  }
@@ -2790,7 +2857,8 @@ async function setupTerminalWebSocket(httpServer) {
2790
2857
  catch (err) { reportSwallowed('server.queue-send-now', err); }
2791
2858
  }
2792
2859
  } else {
2793
- try { chatQueue.sendNow(qid); } catch (err) { reportSwallowed('server.queue-send-now', err); }
2860
+ const qAnchor = { project: typeof msg.project === 'string' ? msg.project : undefined, instanceKey: typeof msg.instanceKey === 'string' ? msg.instanceKey : undefined };
2861
+ try { chatQueue.sendNow(qid, qAnchor); } catch (err) { reportSwallowed('server.queue-send-now', err); }
2794
2862
  }
2795
2863
  }
2796
2864
  } else if (msg.type === 'queue-remove') {
@@ -2799,7 +2867,8 @@ async function setupTerminalWebSocket(httpServer) {
2799
2867
  if (isSdkMode) {
2800
2868
  if (_sdkRemoveQueued) { const qAnchor = ((typeof msg.project === 'string' && msg.project) || (typeof msg.instanceKey === 'string' && msg.instanceKey)) ? { project: msg.project, instanceKey: typeof msg.instanceKey === 'string' ? msg.instanceKey : undefined } : undefined; try { _sdkRemoveQueued(qid, qAnchor); } catch (err) { reportSwallowed('server.queue-remove', err); } }
2801
2869
  } else {
2802
- try { chatQueue.remove(qid); } catch (err) { reportSwallowed('server.queue-remove', err); }
2870
+ const qAnchor = { project: typeof msg.project === 'string' ? msg.project : undefined, instanceKey: typeof msg.instanceKey === 'string' ? msg.instanceKey : undefined };
2871
+ try { chatQueue.remove(qid, qAnchor); } catch (err) { reportSwallowed('server.queue-remove', err); }
2803
2872
  }
2804
2873
  }
2805
2874
  } else if (msg.type === 'queue-clear') {
@@ -2808,7 +2877,8 @@ async function setupTerminalWebSocket(httpServer) {
2808
2877
  if (isSdkMode) {
2809
2878
  if (_sdkClearQueued) { const qAnchor = ((typeof msg.project === 'string' && msg.project) || (typeof msg.instanceKey === 'string' && msg.instanceKey)) ? { project: msg.project, instanceKey: typeof msg.instanceKey === 'string' ? msg.instanceKey : undefined } : undefined; try { _sdkClearQueued(qAnchor); } catch (err) { reportSwallowed('server.queue-clear', err); } }
2810
2879
  } else {
2811
- try { chatQueue.clear(); } catch (err) { reportSwallowed('server.queue-clear', err); }
2880
+ const qAnchor = { project: typeof msg.project === 'string' ? msg.project : undefined, sessionId: typeof msg.sessionId === 'string' ? msg.sessionId : undefined, instanceKey: typeof msg.instanceKey === 'string' ? msg.instanceKey : undefined };
2881
+ try { chatQueue.clear(qAnchor.project || qAnchor.sessionId || qAnchor.instanceKey ? qAnchor : undefined); } catch (err) { reportSwallowed('server.queue-clear', err); }
2812
2882
  }
2813
2883
  } else if (msg.type === 'queue-suppress') {
2814
2884
  // PTY Stop pressed: park the queue — the coming turn-end must not auto-drain it.
@@ -3595,6 +3665,36 @@ export function broadcastWsMessage(msg) { if (terminalWss) {
3595
3665
  }
3596
3666
  }
3597
3667
 
3668
+ /**
3669
+ * Context-usage reconnect replay. CONTEXT-ONLY: unlike the generic `_sdkFrameForView`
3670
+ * (project-scoped, shared by init/session-state/queue/MCP/approval replay), a viewer
3671
+ * pinned to an exact instance also requires the replayed frame to carry THAT instanceKey
3672
+ * — otherwise a same-basename twin's snapshot could repaint the wrong panel. Anchor
3673
+ * precedence matches sdk-skills: exact viewed instance, else viewed project, else no
3674
+ * anchor (active session). Unknown/ambiguous anchors resolve to a null snapshot → no
3675
+ * frame; never fall back to the active session. The project check reuses
3676
+ * projectKeyForCwd so sanitized names (spaces/CJK) still match.
3677
+ */
3678
+ export function sdkContextReplayFrame(getSnapshot, viewProject, viewInstance) {
3679
+ if (typeof getSnapshot !== 'function') return null;
3680
+ const anchor = viewInstance
3681
+ ? { instanceKey: viewInstance }
3682
+ : (viewProject ? { project: viewProject } : null);
3683
+ const frame = anchor ? getSnapshot(anchor) : getSnapshot();
3684
+ if (!frame || typeof frame !== 'object') return null;
3685
+ if (viewInstance) {
3686
+ const frameInstance = (typeof frame.instanceKey === 'string' && frame.instanceKey) ? frame.instanceKey : null;
3687
+ if (frameInstance !== viewInstance) return null; // missing or foreign instance
3688
+ }
3689
+ if (viewProject) {
3690
+ const frameProject = (typeof frame.cwd === 'string' && frame.cwd)
3691
+ ? (projectKeyForCwd(frame.cwd) || null)
3692
+ : null;
3693
+ if (frameProject && frameProject !== viewProject) return null;
3694
+ }
3695
+ return frame;
3696
+ }
3697
+
3598
3698
  /**
3599
3699
  * Close open approval modals on every client after a server-side drain of canUseTool
3600
3700
  * approvals (sdk-interrupt Stop, or queue-send-now interrupting a turn mid-approval).
@@ -10,6 +10,10 @@ You are ${model.name}, an interactive coding agent that helps users with softwar
10
10
 
11
11
  IMPORTANT: Never generate or guess URLs unless you are confident they help the user with programming. Prefer URLs the user provides or ones found in local files.
12
12
 
13
+ # Choosing skills
14
+ - At the skill-selection entry point, skip `claude-api` and the skill whose display title is "Building LLM-Powered Applications with Claude": they are the same Anthropic-only API skill, so treat the identifier and title as aliases. Do not invoke it or read its documentation unless the task explicitly requires Claude/Anthropic SDK or API integration. This applies even when Claude Code automatically recommends or triggers it.
15
+ - Working on model-name recognition, model logos, aliases, or context-window classification in an existing project is local application code, NOT LLM API integration. Do not load `claude-api` for these tasks merely because the code mentions models, Claude, or model IDs. Locate the local matcher and its tests first; for a genuinely unrelated skill, do not load it just to determine that it is unrelated.
16
+
13
17
  # Doing tasks
14
18
  - Read the relevant files before acting or answering; ground every claim and change in code you have actually looked at.
15
19
  - Before your first tool call, output a one-sentence action plan; then act.