cc-viewer 1.9.0 → 1.9.2

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 (160) hide show
  1. package/README.md +3 -1
  2. package/cli.js +33 -4
  3. package/concepts/ar/UltraPlan.md +1 -1
  4. package/concepts/da/UltraPlan.md +1 -1
  5. package/concepts/de/UltraPlan.md +1 -1
  6. package/concepts/en/UltraPlan.md +1 -1
  7. package/concepts/es/UltraPlan.md +1 -1
  8. package/concepts/fr/UltraPlan.md +1 -1
  9. package/concepts/it/UltraPlan.md +1 -1
  10. package/concepts/ja/UltraPlan.md +1 -1
  11. package/concepts/ko/UltraPlan.md +1 -1
  12. package/concepts/no/UltraPlan.md +1 -1
  13. package/concepts/pl/UltraPlan.md +1 -1
  14. package/concepts/pt-BR/UltraPlan.md +1 -1
  15. package/concepts/ru/UltraPlan.md +1 -1
  16. package/concepts/th/UltraPlan.md +1 -1
  17. package/concepts/tr/UltraPlan.md +1 -1
  18. package/concepts/uk/UltraPlan.md +1 -1
  19. package/concepts/zh/UltraPlan.md +1 -1
  20. package/concepts/zh-TW/UltraPlan.md +1 -1
  21. package/dist/assets/App-DJ4yNvwz.css +1 -0
  22. package/dist/assets/App-DPRn6oGn.js +2 -0
  23. package/dist/assets/{MdxEditorPanel-3j8rS3IT.js → MdxEditorPanel-Ci4ldhd7.js} +1 -1
  24. package/dist/assets/{MdxEditorPanel-Dpn-4ydO.css → MdxEditorPanel-DTfdj6GS.css} +1 -1
  25. package/dist/assets/Mobile-Dq-KcO1s.js +1 -0
  26. package/dist/assets/ProxyStatsModal-CUFpqt1s.js +1 -0
  27. package/dist/assets/{_baseUniq-B3e4JaL0.js → _baseUniq-CwQykUuc.js} +1 -1
  28. package/dist/assets/{arc-DnoPn_qY.js → arc-EdUdBxtG.js} +1 -1
  29. package/dist/assets/{architectureDiagram-Q4EWVU46-DrPKTbxH.js → architectureDiagram-Q4EWVU46-DtBIdElY.js} +1 -1
  30. package/dist/assets/{blockDiagram-DXYQGD6D-CEaO0Vcw.js → blockDiagram-DXYQGD6D-guNZIKkZ.js} +1 -1
  31. package/dist/assets/{c4Diagram-AHTNJAMY-BLaawFeg.js → c4Diagram-AHTNJAMY-xV5vB-Z9.js} +1 -1
  32. package/dist/assets/{channel-DnYR1fF4.js → channel-DYEktNap.js} +1 -1
  33. package/dist/assets/{chunk-4BX2VUAB-Bfd4edKZ.js → chunk-4BX2VUAB-ZTkItpvj.js} +1 -1
  34. package/dist/assets/{chunk-4TB4RGXK-t2nMQmY7.js → chunk-4TB4RGXK-dYIewyyR.js} +1 -1
  35. package/dist/assets/{chunk-55IACEB6-C6tLOjGX.js → chunk-55IACEB6-CSsPNgPX.js} +1 -1
  36. package/dist/assets/{chunk-EDXVE4YY-dWCvxbCb.js → chunk-EDXVE4YY-CnMiuqzQ.js} +1 -1
  37. package/dist/assets/{chunk-FMBD7UC4-BHELZLPK.js → chunk-FMBD7UC4-CVpATPZ3.js} +1 -1
  38. package/dist/assets/{chunk-OYMX7WX6-C8XTkH1S.js → chunk-OYMX7WX6-Dpq4s8dW.js} +1 -1
  39. package/dist/assets/{chunk-QZHKN3VN-CD0j6SAs.js → chunk-QZHKN3VN-Bnojh6X6.js} +1 -1
  40. package/dist/assets/{chunk-YZCP3GAM-D4dKfXJJ.js → chunk-YZCP3GAM-BYqK4Dsa.js} +1 -1
  41. package/dist/assets/classDiagram-6PBFFD2Q-ClGF33bF.js +1 -0
  42. package/dist/assets/classDiagram-v2-HSJHXN6E-ClGF33bF.js +1 -0
  43. package/dist/assets/clone-CFj55h49.js +1 -0
  44. package/dist/assets/{cose-bilkent-S5V4N54A-BF2HsiSD.js → cose-bilkent-S5V4N54A-OJ7mmWyV.js} +1 -1
  45. package/dist/assets/{dagre-KV5264BT-f3h967yx.js → dagre-KV5264BT-B1Kwd_Rk.js} +1 -1
  46. package/dist/assets/{diagram-5BDNPKRD-B9PzHQfh.js → diagram-5BDNPKRD-D8qrw8sp.js} +1 -1
  47. package/dist/assets/{diagram-G4DWMVQ6-kR953Bqm.js → diagram-G4DWMVQ6-ip-N4ho8.js} +1 -1
  48. package/dist/assets/{diagram-MMDJMWI5-DAewa2eg.js → diagram-MMDJMWI5-2PZaskUF.js} +1 -1
  49. package/dist/assets/{diagram-TYMM5635-C8HNTH6I.js → diagram-TYMM5635-CeAOQUZP.js} +1 -1
  50. package/dist/assets/{erDiagram-SMLLAGMA-BQ5P8wWK.js → erDiagram-SMLLAGMA-BCBaEYEb.js} +1 -1
  51. package/dist/assets/{flowDiagram-DWJPFMVM-BIpEkrMR.js → flowDiagram-DWJPFMVM-CsGyIqcX.js} +1 -1
  52. package/dist/assets/{ganttDiagram-T4ZO3ILL-CWqSfB5v.js → ganttDiagram-T4ZO3ILL-COsj3FZ-.js} +1 -1
  53. package/dist/assets/{gitGraphDiagram-UUTBAWPF-Ba-9yEbQ.js → gitGraphDiagram-UUTBAWPF-DvQVQGJG.js} +1 -1
  54. package/dist/assets/{graph-D3zXKQ6P.js → graph-BSB9fDSP.js} +1 -1
  55. package/dist/assets/index-BLzca-ta.css +1 -0
  56. package/dist/assets/{index-G3xd-gLo.js → index-BRw4Iz2O.js} +1 -1
  57. package/dist/assets/{index-DClD_EkN.js → index-Bo-12bug.js} +1 -1
  58. package/dist/assets/{index-DSd5P3WQ.js → index-BuVComcY.js} +1 -1
  59. package/dist/assets/{index-HLrnYsI1.js → index-CNNAYjkF.js} +1 -1
  60. package/dist/assets/index-Ci38xZiX.js +2 -0
  61. package/dist/assets/{index-Bm3A5VLb.js → index-DKufsAzx.js} +1 -1
  62. package/dist/assets/{index-C9N0rZ7Z.js → index-DXBfwPYD.js} +1 -1
  63. package/dist/assets/{index-Q8wp4Nof.js → index-eM2ih5n7.js} +1 -1
  64. package/dist/assets/{infoDiagram-42DDH7IO-Cxm79ZJR.js → infoDiagram-42DDH7IO-eNucS4iW.js} +1 -1
  65. package/dist/assets/{ishikawaDiagram-UXIWVN3A-Cd0vZP1B.js → ishikawaDiagram-UXIWVN3A-Dxxj-gUX.js} +1 -1
  66. package/dist/assets/{journeyDiagram-VCZTEJTY-B2b57HXk.js → journeyDiagram-VCZTEJTY-yJJzVEBb.js} +1 -1
  67. package/dist/assets/{jszip.min-Bj5uiEb2.js → jszip.min-ByI0ds5u.js} +1 -1
  68. package/dist/assets/{kanban-definition-6JOO6SKY-YCERiUCO.js → kanban-definition-6JOO6SKY-2rfTi2zw.js} +1 -1
  69. package/dist/assets/{layout-Do5TlO2U.js → layout-DTmHj0Wm.js} +1 -1
  70. package/dist/assets/{linear-BiJNTDgN.js → linear-BOA3a9FE.js} +1 -1
  71. package/dist/assets/{mermaid.core-Dg0McRZt.js → mermaid.core-5H-NR79H.js} +3 -3
  72. package/dist/assets/{min-T51iefht.js → min-CpjFTEec.js} +1 -1
  73. package/dist/assets/{mindmap-definition-QFDTVHPH-Cu_GJxY3.js → mindmap-definition-QFDTVHPH-B8KCjFgB.js} +1 -1
  74. package/dist/assets/{pieDiagram-DEJITSTG-DldHHZXi.js → pieDiagram-DEJITSTG-BARy96g5.js} +1 -1
  75. package/dist/assets/{quadrantDiagram-34T5L4WZ-BzyTPNAJ.js → quadrantDiagram-34T5L4WZ-DYoTapuI.js} +1 -1
  76. package/dist/assets/{requirementDiagram-MS252O5E-C5xxFkvG.js → requirementDiagram-MS252O5E-EeANLLK2.js} +1 -1
  77. package/dist/assets/{sankeyDiagram-XADWPNL6-OxdjoaE1.js → sankeyDiagram-XADWPNL6-DmQ-YVPj.js} +1 -1
  78. package/dist/assets/seqResourceLoaders-B0p7e_lM.js +2 -0
  79. package/dist/assets/seqResourceLoaders-BsMFNfvR.css +41 -0
  80. package/dist/assets/{sequenceDiagram-FGHM5R23-C7c89uIS.js → sequenceDiagram-FGHM5R23-SZtW0rYC.js} +1 -1
  81. package/dist/assets/{stateDiagram-FHFEXIEX-ssdJnDrO.js → stateDiagram-FHFEXIEX-CEygsIld.js} +1 -1
  82. package/dist/assets/{stateDiagram-v2-QKLJ7IA2-BP-0WClK.js → stateDiagram-v2-QKLJ7IA2-BU1XPhBe.js} +1 -1
  83. package/dist/assets/{timeline-definition-GMOUNBTQ-sWScEpSM.js → timeline-definition-GMOUNBTQ-DhPBaQBt.js} +1 -1
  84. package/dist/assets/{vendor-antd-DKIytXho.js → vendor-antd-CS_S2mBN.js} +1 -1
  85. package/dist/assets/{vendor-codemirror-4qgXq03k.js → vendor-codemirror-B5l-VbBO.js} +1 -1
  86. package/dist/assets/{vendor-mdxeditor-D5Z5D9fq.js → vendor-mdxeditor-Cz09a1_I.js} +2 -2
  87. package/dist/assets/{vendor-qrcode-B7lb5qk1.js → vendor-qrcode-qbbntD6-.js} +1 -1
  88. package/dist/assets/{vendor-virtuoso-BqoXlbhH.js → vendor-virtuoso-Sk8fQ4Hx.js} +1 -1
  89. package/dist/assets/{vennDiagram-DHZGUBPP-DNfXR0ge.js → vennDiagram-DHZGUBPP-NniTHxDw.js} +1 -1
  90. package/dist/assets/{wardley-RL74JXVD-JXd--CRD.js → wardley-RL74JXVD-D9SNOrDw.js} +1 -1
  91. package/dist/assets/{wardleyDiagram-NUSXRM2D-DHmNyVPM.js → wardleyDiagram-NUSXRM2D-DvHXKNza.js} +1 -1
  92. package/dist/assets/{xychartDiagram-5P7HB3ND-CTb0gltZ.js → xychartDiagram-5P7HB3ND-CaiwPTQB.js} +1 -1
  93. package/dist/index.html +5 -5
  94. package/package.json +1 -1
  95. package/server/interceptor.js +192 -24
  96. package/server/lib/ask/ask-store.js +7 -3
  97. package/server/lib/async-file-lock.js +29 -5
  98. package/server/lib/auth.js +71 -54
  99. package/server/lib/chat-queue.js +67 -4
  100. package/server/lib/config-backup.js +8 -5
  101. package/server/lib/credential-access.js +233 -0
  102. package/server/lib/credential-migrate.js +147 -0
  103. package/server/lib/credential-store.js +120 -0
  104. package/server/lib/credential-vault.js +112 -0
  105. package/server/lib/file-access-policy.js +16 -0
  106. package/server/lib/git-diff.js +49 -20
  107. package/server/lib/im/im-config.js +81 -31
  108. package/server/lib/im-deny.js +22 -8
  109. package/server/lib/json-store.js +264 -0
  110. package/server/lib/log-watcher.js +9 -1
  111. package/server/lib/prefs-store.js +8 -33
  112. package/server/lib/same-origin.js +22 -0
  113. package/server/lib/session-pin-store.js +7 -14
  114. package/server/lib/v2/convert.js +1 -1
  115. package/server/lib/v2/layout.js +53 -3
  116. package/server/lib/v2/live-feed.js +41 -4
  117. package/server/lib/v2/resume-list.js +150 -0
  118. package/server/lib/v2/session-select.js +63 -14
  119. package/server/lib/v2/v2-writer.js +172 -34
  120. package/server/lib/v2/view-router.js +83 -0
  121. package/server/lib/view-root.js +209 -0
  122. package/server/pty-manager.js +809 -163
  123. package/server/routes/ask-perm.js +21 -1
  124. package/server/routes/auth.js +21 -1
  125. package/server/routes/events.js +123 -7
  126. package/server/routes/expert.js +46 -14
  127. package/server/routes/files-content.js +72 -7
  128. package/server/routes/files-fs.js +28 -3
  129. package/server/routes/git.js +31 -5
  130. package/server/routes/im.js +15 -2
  131. package/server/routes/preferences.js +59 -46
  132. package/server/routes/project-meta.js +32 -3
  133. package/server/routes/proxy-stats.js +33 -5
  134. package/server/routes/resume.js +457 -0
  135. package/server/routes/search.js +23 -1
  136. package/server/routes/skills.js +18 -2
  137. package/server/routes/v2.js +6 -1
  138. package/server/routes/workspaces.js +18 -5
  139. package/server/server.js +199 -25
  140. package/server/system-prompt-templates/presets/GLM-5.2.md +0 -1
  141. package/server/system-prompt-templates/presets/GLM-5.3.md +0 -1
  142. package/server/system-prompt-templates/presets/Qwen-3.md +0 -1
  143. package/server/system-prompt-templates/presets/deepseek-v4-flash.md +0 -1
  144. package/server/system-prompt-templates/presets/deepseek-v4-pro.md +0 -1
  145. package/server/system-prompt-templates/presets/kimi-k2.7-code.md +0 -1
  146. package/server/system-prompt-templates/presets/kimi-k3.md +1 -1
  147. package/server/system-prompt-templates/systemPromptModel.md +0 -1
  148. package/server/workspace-registry.js +150 -75
  149. package/ultraAgents/code-expert.json +1 -1
  150. package/dist/assets/App-M61pqQEx.css +0 -1
  151. package/dist/assets/App-_HfxMJvv.js +0 -2
  152. package/dist/assets/Mobile-DaRLB7Gh.js +0 -1
  153. package/dist/assets/ProxyStatsModal-dlPIDlx-.js +0 -1
  154. package/dist/assets/classDiagram-6PBFFD2Q-BecPAAuk.js +0 -1
  155. package/dist/assets/classDiagram-v2-HSJHXN6E-BecPAAuk.js +0 -1
  156. package/dist/assets/clone-B0_ghF3F.js +0 -1
  157. package/dist/assets/index-CjbBUDGP.js +0 -2
  158. package/dist/assets/index-Nvk8r3tO.css +0 -1
  159. package/dist/assets/seqResourceLoaders-B_FSQh3U.css +0 -41
  160. package/dist/assets/seqResourceLoaders-hqLhsiGO.js +0 -2
package/server/server.js CHANGED
@@ -40,6 +40,7 @@ import { teamRoutes } from './routes/team.js';
40
40
  import { authRoutes } from './routes/auth.js';
41
41
  import { dingtalkRoutes } from './routes/dingtalk.js';
42
42
  import { imRoutes } from './routes/im.js';
43
+ import { resumeRoutes } from './routes/resume.js';
43
44
  import { proxyStatsRoutes } from './routes/proxy-stats.js';
44
45
  import { setProxyStatsListener } from './lib/proxy/proxy-stats.js';
45
46
  import * as imCore from './lib/im/im-bridge-core.js';
@@ -96,6 +97,7 @@ function execWithStdin(cmd, args, input, options) {
96
97
  }
97
98
  import { _initPromise, _projectName, _logDir, _v2Writer, streamingState, resetStreamingState, PROFILE_PATH, RETRY_CONFIG_PATH, _retryConfigState, setLivePort, getImLiveText, resetImLiveText, markSessionStart } from './interceptor.js';
98
99
  import { V2LiveFeed } from './lib/v2/live-feed.js';
100
+ import { filterClientsByViewProject, projectOfSessionDir, resolveActivityFeedKey } from './lib/v2/view-router.js';
99
101
  import { sanitizePathComponent } from './lib/v2/layout.js';
100
102
  import { maybeResumeConvert } from './lib/v2/convert-manager.js';
101
103
  import { LOG_DIR, setLogDir, getClaudeConfigDir, isBrowserOpenSuppressed } from '../findcc.js';
@@ -118,6 +120,7 @@ import { applyTaskEvent, resetTasks, getTaskSnapshot, shouldResetTasks } from '.
118
120
  import { createImLogWatcher } from './lib/im/im-log-watcher.js';
119
121
  import { unwatchAllWorkflows } from './lib/workflow-watcher.js';
120
122
  import { backupConfigs } from './lib/config-backup.js';
123
+ import { migrateCredentialsToVault } from './lib/credential-migrate.js';
121
124
  import { normalizeBasePath, validateBasePath, stripBasePath } from './lib/base-path.js';
122
125
  import { createHardenedCleanup } from './lib/term-signals.js';
123
126
  import { createBackpressureGate } from './lib/ws-backpressure.js';
@@ -172,9 +175,12 @@ const IGNORED_PATTERNS = new Set([
172
175
  '.idea', '.vscode'
173
176
  ]);
174
177
 
175
- // 多 git 仓库支持:解析 repo 参数为安全的 cwd 路径
176
- function resolveRepoCwd(repoParam) {
177
- const projectDir = process.env.CCV_PROJECT_DIR || process.cwd();
178
+ // 多 git 仓库支持:解析 repo 参数为安全的 cwd 路径。
179
+ // Multi-project (2026-10): `baseDir` lets a viewed parallel project override
180
+ // the bound root (git READ routes pass resolveViewRoot's result); omitting it
181
+ // keeps the bound behavior byte-identical.
182
+ function resolveRepoCwd(repoParam, baseDir) {
183
+ const projectDir = baseDir || process.env.CCV_PROJECT_DIR || process.cwd();
178
184
  if (!repoParam || repoParam === '.') return projectDir;
179
185
  if (repoParam.includes('/') || repoParam.includes('..') || repoParam.includes('\\')) return null;
180
186
  const candidate = join(projectDir, repoParam);
@@ -490,34 +496,108 @@ const MIME_TYPES = {
490
496
  '.woff2': 'font/woff2',
491
497
  };
492
498
 
493
- // S6b: v2 live feed — the live channel when the v2 writer is active. Lazy
494
- // singleton so worker/test paths that never watch anything pay nothing.
495
- let _v2LiveFeed = null;
496
- function _ensureV2LiveFeed() {
497
- if (!_v2LiveFeed) {
498
- _v2LiveFeed = new V2LiveFeed({
499
+ // S6b: v2 live feed — the live channel when the v2 writer is active. ONE feed
500
+ // per project (the bound project plus any project a client is currently
501
+ // VIEWING via /events?project=). Feeds are lazy so worker/test paths that never
502
+ // watch anything pay nothing. Keyed by the absolute project dir.
503
+ const _v2LiveFeeds = new Map(); // projectDir → V2LiveFeed
504
+
505
+ // Per-project client filter handed to every feed: a feed for project P must
506
+ // broadcast only to SSE clients whose VIEWED project is P. Pure predicate lives
507
+ // in lib/v2/view-router.js (unit-tested); this closure just binds the live
508
+ // bound-project name.
509
+ function _filterClientsByViewProject(clientList, project) {
510
+ return filterClientsByViewProject(clientList, project, _projectName || '');
511
+ }
512
+
513
+ function _ensureProjectFeed(projectName) {
514
+ if (!projectName) return null;
515
+ const projectDir = join(LOG_DIR, sanitizePathComponent(projectName));
516
+ let feed = _v2LiveFeeds.get(projectDir);
517
+ if (!feed) {
518
+ feed = new V2LiveFeed({
499
519
  clients,
500
520
  getClaudePid,
501
521
  runParallelHook,
502
522
  // The stats worker's v2 units are session dirs (stats-worker.js v9).
503
523
  notifyStatsWorker,
504
524
  wireV3: WIRE_V3,
525
+ project: projectName,
526
+ clientFilter: _filterClientsByViewProject,
505
527
  });
528
+ feed.start(projectDir);
529
+ _v2LiveFeeds.set(projectDir, feed);
530
+ } else if (!feed._active) {
531
+ feed.start(projectDir);
506
532
  }
507
- return _v2LiveFeed;
533
+ return feed;
508
534
  }
509
535
 
510
- /** Start the live channel for the current project (v2 live feed). No-op until
511
- * a project is bound (workspace mode defers to the launch route). */
536
+ /** Start the live channel for the bound project (v2 live feed) and route the
537
+ * writer's per-session activity to the right project's feed. No-op until a
538
+ * project is bound (workspace mode defers to the launch route). */
512
539
  function startLogWatch() {
513
540
  if (!_projectName) return;
514
- const feed = _ensureV2LiveFeed();
515
- feed.start(join(LOG_DIR, sanitizePathComponent(_projectName)));
516
- _v2Writer.setOnActivity((dir) => feed.tick(dir));
541
+ _ensureProjectFeed(_projectName);
542
+ // The writer holds sessions for EVERY live project (per-project PTYs write
543
+ // through it). Route each activity dir to its owning project's feed by the
544
+ // <LOG_DIR>/<projectSan>/sessions/ prefix, so a viewed background project's
545
+ // live entries reach ITS feed (and its viewing clients) too.
546
+ _v2Writer.setOnActivity((dir) => {
547
+ const feed = _feedForSessionDir(dir);
548
+ if (feed) feed.tick(dir);
549
+ });
550
+ // Feed each resolved (sid, project) into the PTY sid index so chat sends can
551
+ // be routed to the PTY owning that conversation (anchor routing, 2026-10-05).
552
+ // Lazy import keeps pty-manager out of the early module graph (and off the
553
+ // boundary allowlist); a failure must never break logging.
554
+ // Multi-instance (2026-10-06): prefer the instanceKey-exact route when the claude
555
+ // self-reported one (`x-ccv-instance`) — it pins the sid to THIS process, which basename
556
+ // routing cannot do for two same-cwd instances. Fall back to the basename route for
557
+ // legacy / external producers that send no instance header.
558
+ _v2Writer.setOnSessionResolved((sid, project, instanceKey) => {
559
+ import('./pty-manager.js')
560
+ .then((pm) => {
561
+ try {
562
+ const pinned = instanceKey && typeof pm.setPtySessionIdForInstance === 'function'
563
+ ? pm.setPtySessionIdForInstance(instanceKey, sid)
564
+ : false;
565
+ if (!pinned) pm.setPtySessionId(project, sid);
566
+ } catch (err) { reportSwallowed('server.sid-pin', err); }
567
+ })
568
+ .catch((err) => { reportSwallowed('server.sid-pin-import', err); });
569
+ });
570
+ }
571
+
572
+ // The bound project's feed (may not exist yet). Single key builder, shared by
573
+ // both _feedForSessionDir fallbacks so the ensure/fallback keys can't drift.
574
+ function _boundFeed() {
575
+ return _v2LiveFeeds.get(join(LOG_DIR, sanitizePathComponent(_projectName || ''))) || null;
576
+ }
577
+
578
+ // Resolve the live feed that owns a session dir, from its
579
+ // <LOG_DIR>/<projectSan>/sessions/<dir> path. When the prefix isolates a
580
+ // known project but that project has NO live feed (no client is viewing it —
581
+ // feeds are lazy), return null rather than falling back to the bound feed:
582
+ // the activity belongs to a DIFFERENT project, and routing it to the bound
583
+ // project's viewers is exactly the cross-project bleed this layer exists to
584
+ // prevent (2026-10-05). Only when the prefix does not resolve a project at all
585
+ // (a dir outside <LOG_DIR>/<projectSan>/sessions/) do we fall back to the
586
+ // bound feed defensively.
587
+ function _feedForSessionDir(dir) {
588
+ const { feedKey, allowBoundFallback } = resolveActivityFeedKey(dir, LOG_DIR, sep);
589
+ // A resolved project owns this activity: route to ITS feed, or drop when it
590
+ // has none (lazy feeds — no viewer, no broadcast). Never re-route a foreign
591
+ // project's activity to the bound feed (the cross-project bleed, 2026-10-05).
592
+ if (feedKey) return _v2LiveFeeds.get(join(LOG_DIR, feedKey)) || null;
593
+ // No owning project (dir outside every <LOG_DIR>/<projectSan>/sessions/
594
+ // root): bound feed, defensively — the legacy single-project behavior.
595
+ return allowBoundFallback ? _boundFeed() : null;
517
596
  }
518
597
 
519
598
  function stopLogWatch() {
520
- if (_v2LiveFeed) _v2LiveFeed.stop();
599
+ for (const feed of _v2LiveFeeds.values()) feed.stop();
600
+ _v2LiveFeeds.clear();
521
601
  _v2Writer.setOnActivity(null);
522
602
  }
523
603
 
@@ -610,6 +690,7 @@ const deps = {
610
690
  notifyParentPending: _notifyParentPending,
611
691
  startLogWatch,
612
692
  stopLogWatch,
693
+ ensureProjectFeed: _ensureProjectFeed,
613
694
  scheduleTurnEndBroadcast: _scheduleTurnEndBroadcast,
614
695
  // SessionStart hook notify → conversation-switch signal (in-terminal
615
696
  // /resume). The source gate + V2Writer re-bind live in the interceptor.
@@ -732,6 +813,7 @@ const _routes = [
732
813
  ...dingtalkRoutes,
733
814
  ...imRoutes,
734
815
  ...proxyStatsRoutes,
816
+ ...resumeRoutes,
735
817
  ];
736
818
  const dispatch = createDispatcher(_routes);
737
819
 
@@ -785,6 +867,7 @@ async function handleRequest(req, res) {
785
867
  accessToken: ACCESS_TOKEN,
786
868
  enabled: authConfig.enabled,
787
869
  password: authConfig.password,
870
+ passwordUnreadable: authConfig.passwordUnreadable,
788
871
  wantsHtml,
789
872
  });
790
873
  // Remote-admin elevation (container/cloud deploy): an authenticated remote caller is
@@ -990,6 +1073,11 @@ export async function startViewer() {
990
1073
  // (滚动留 10 份)。2026-06-06 事故:配置随 LOG_DIR 整树丢失后无处可恢复。fire-and-forget。
991
1074
  setImmediate(() => { try { backupConfigs(); } catch { /* ignore */ } });
992
1075
 
1076
+ // 启动期一次性凭证迁移:preferences/profile 里的明文/base64 密钥 → credentials.json(AES-256-GCM)。
1077
+ // 必须在 backupConfigs 之后挂(先留一份迁移前备份),幂等(逐字段写 vault→读回比对→剔旧字段),
1078
+ // 失败字段保留原值不丢,下次启动重试。fire-and-forget,不阻塞启动。
1079
+ setImmediate(() => { try { migrateCredentialsToVault(); } catch { /* ignore */ } });
1080
+
993
1081
  // 启动时清理磁盘上 ASK_HOOK_TIMEOUT_MS 之前的 ask 条目(兜底防泄漏)。
994
1082
  // 内存 Map 不 hydrate:旧 res 已死、新 ask-bridge 重连同 toolUseId 会自动复用槽位
995
1083
  // (server.js 已有"旧 res 已断 → 复用"分支),无需在这里主动重建内存态。
@@ -1292,7 +1380,7 @@ export async function startViewer() {
1292
1380
  async function setupTerminalWebSocket(httpServer) {
1293
1381
  try {
1294
1382
  const { WebSocketServer } = await import('ws');
1295
- const { writeToPty, writeToPtySequential, resizePty, onPtyData, onPtyExit, getPtyState, getPtyKind, getOutputBuffer, getCurrentWorkspace, spawnShell, findSafeSliceStart } = await import('./pty-manager.js');
1383
+ const { writeToPty, writeToPtySequential, writeToPtyFor, writeToPtySequentialFor, resizePty, onPtyData, onPtyExit, getPtyState, getPtyKind, getOutputBuffer, getCurrentWorkspace, spawnShell, findSafeSliceStart, attachPtyFor } = await import('./pty-manager.js');
1296
1384
  const {
1297
1385
  spawnScratch,
1298
1386
  writeScratch,
@@ -1317,6 +1405,7 @@ async function setupTerminalWebSocket(httpServer) {
1317
1405
  chatQueue.initChatQueue({
1318
1406
  writeToPty,
1319
1407
  writeToPtySequential,
1408
+ writeToPtySequentialFor,
1320
1409
  getPtyKind,
1321
1410
  isStreaming: () => streamingState.active,
1322
1411
  hasPendingApproval: () => pendingAskHooks.size > 0 || pendingPermHooks.size > 0,
@@ -1364,6 +1453,7 @@ async function setupTerminalWebSocket(httpServer) {
1364
1453
  accessToken: ACCESS_TOKEN,
1365
1454
  enabled: authConfig.enabled,
1366
1455
  password: authConfig.password,
1456
+ passwordUnreadable: authConfig.passwordUnreadable,
1367
1457
  wantsHtml: false,
1368
1458
  });
1369
1459
  if (wsAuth.action !== 'allow') {
@@ -1371,6 +1461,18 @@ async function setupTerminalWebSocket(httpServer) {
1371
1461
  return;
1372
1462
  }
1373
1463
  if (pathname === '/ws/terminal') {
1464
+ // Multi-PTY view attach (2026-10): `?project=<name>` pins the freshly
1465
+ // connected client to the project it is VIEWING, so the initial
1466
+ // state/buffer frames below come from that project's record — not from
1467
+ // whatever project happened to spawn last. Unknown/absent project is a
1468
+ // silent no-op (single-project behavior unchanged).
1469
+ const viewProject = wsUrl.searchParams.get('project');
1470
+ // Multi-instance: `&instance=<key>` pins the attach to THAT exact process when a
1471
+ // same-cwd project runs two concurrent instances (basename alone is ambiguous).
1472
+ const viewInstance = wsUrl.searchParams.get('instance');
1473
+ if (viewProject || viewInstance) {
1474
+ try { attachPtyFor({ project: viewProject || undefined, instanceKey: viewInstance || undefined }); } catch { }
1475
+ }
1374
1476
  wss.handleUpgrade(req, socket, head, (ws) => {
1375
1477
  wss.emit('connection', ws, req);
1376
1478
  });
@@ -1775,11 +1877,11 @@ async function setupTerminalWebSocket(httpServer) {
1775
1877
  ws.on('message', async (raw) => {
1776
1878
  try {
1777
1879
  const msg = JSON.parse(raw.toString());
1778
- // SDK 模式没有 PTY:PTY 专属消息(input/sequential input/resync/resize)一律 no-op,
1880
+ // SDK 模式没有 PTY:PTY 专属消息(input/sequential input/resync/resize/attach)一律 no-op,
1779
1881
  // 否则 'input' 会在无会话时触发 spawnShell() 拉出一个用户看不见的裸 shell。
1780
1882
  // sdk-*/ask-*/perm-*/image-* 等控制消息不受影响。scratch WS 是独立 handler,不受此闸门影响。
1781
1883
  if (isSdkMode && (msg.type === 'input' || msg.type === 'input-sequential'
1782
- || msg.type === 'resync-request' || msg.type === 'resize')) {
1884
+ || msg.type === 'resync-request' || msg.type === 'resize' || msg.type === 'attach')) {
1783
1885
  return;
1784
1886
  }
1785
1887
  if (msg.type === 'input') {
@@ -1806,6 +1908,12 @@ async function setupTerminalWebSocket(httpServer) {
1806
1908
  }
1807
1909
  }
1808
1910
  // 拦截连续 Ctrl+C:2秒内连按2次则阻止并提醒,避免误退出 CLI
1911
+ // Anchor routing (2026-10-05): a send carries the viewed project's
1912
+ // {project, sessionId} so the input lands on that conversation's PTY,
1913
+ // not whichever record the global activePtyKey last pointed to. Falls
1914
+ // back to the legacy active-targeted write when no anchor is present.
1915
+ const _anchor = { project: typeof msg.project === 'string' ? msg.project : undefined, sessionId: typeof msg.sessionId === 'string' ? msg.sessionId : undefined, instanceKey: typeof msg.instanceKey === 'string' ? msg.instanceKey : undefined };
1916
+ const _write = (data) => (_anchor.project || _anchor.sessionId || _anchor.instanceKey) ? writeToPtyFor(data, _anchor) : writeToPty(data);
1809
1917
  if (msg.data === '\x03') {
1810
1918
  const now = Date.now();
1811
1919
  if (!ws._ctrlCLastTime) ws._ctrlCLastTime = 0;
@@ -1815,10 +1923,10 @@ async function setupTerminalWebSocket(httpServer) {
1815
1923
  // 不发送第二次 Ctrl+C 到 PTY
1816
1924
  } else {
1817
1925
  ws._ctrlCLastTime = now;
1818
- writeToPty(msg.data);
1926
+ _write(msg.data);
1819
1927
  }
1820
1928
  } else {
1821
- writeToPty(msg.data);
1929
+ _write(msg.data);
1822
1930
  }
1823
1931
  } else if (msg.type === 'input-sequential') {
1824
1932
  // Programmatic sequential input: send chunks one by one, waiting for PTY ACK
@@ -1844,7 +1952,12 @@ async function setupTerminalWebSocket(httpServer) {
1844
1952
  }
1845
1953
  };
1846
1954
  if (Array.isArray(chunks) && chunks.length > 0 && chunks.every(c => typeof c === 'string')) {
1847
- writeToPtySequential(chunks, replyDone, { settleMs: msg.settleMs || 150 });
1955
+ const _seqAnchor = { project: typeof msg.project === 'string' ? msg.project : undefined, sessionId: typeof msg.sessionId === 'string' ? msg.sessionId : undefined, instanceKey: typeof msg.instanceKey === 'string' ? msg.instanceKey : undefined };
1956
+ if (_seqAnchor.project || _seqAnchor.sessionId || _seqAnchor.instanceKey) {
1957
+ writeToPtySequentialFor(chunks, replyDone, { settleMs: msg.settleMs || 150 }, _seqAnchor);
1958
+ } else {
1959
+ writeToPtySequential(chunks, replyDone, { settleMs: msg.settleMs || 150 });
1960
+ }
1848
1961
  } else {
1849
1962
  replyDone(false);
1850
1963
  }
@@ -1854,6 +1967,39 @@ async function setupTerminalWebSocket(httpServer) {
1854
1967
  floodGate.reset();
1855
1968
  sendResync();
1856
1969
  }
1970
+ } else if (msg.type === 'attach') {
1971
+ // Multi-PTY view attach (2026-10): the client switched the main view to
1972
+ // another parallel project (chip click / launch / cold-load landing) —
1973
+ // re-anchor the shared PTY stream to THAT project's record, then replay
1974
+ // its outputBuffer so the terminal paints the right screen immediately.
1975
+ // attachPtyFor is idempotent and never spawns/kills; an unknown project
1976
+ // degrades to attach-failed with the current view left untouched.
1977
+ //
1978
+ // KNOWN LIMITATION (documented, pre-existing): the attachment is a single
1979
+ // server-wide pointer, not per-connection. Two tabs viewing DIFFERENT
1980
+ // projects share it — the last attach wins and the other tab's terminal
1981
+ // follows the new project too (its chat view is unaffected: SSE per-project
1982
+ // filtering is per-connection). Same-project tabs attach identically →
1983
+ // no-op. A per-connection PTY mux is out of scope for this fix.
1984
+ const target = typeof msg.project === 'string' ? msg.project : '';
1985
+ // Multi-instance: an explicit instanceKey pins the attach to THAT exact process
1986
+ // (a same-cwd project's basename alone is ambiguous across two live instances).
1987
+ const targetInstance = typeof msg.instanceKey === 'string' ? msg.instanceKey : '';
1988
+ let res = null;
1989
+ try { res = (target || targetInstance) ? attachPtyFor({ project: target || undefined, instanceKey: targetInstance || undefined }) : null; } catch { res = null; }
1990
+ if (res && res.ok) {
1991
+ try { ws.send(JSON.stringify({ type: 'attached', project: target, running: res.running, ptyKind: res.ptyKind, exitCode: res.exitCode })); } catch { }
1992
+ const snap = getOutputBuffer();
1993
+ if (snap) {
1994
+ try { ws.send(JSON.stringify({ type: 'data', data: snap })); } catch { }
1995
+ }
1996
+ } else {
1997
+ // Multi-instance: surface an ambiguous project-name attach (with the live
1998
+ // candidates) so the client re-issues with an instanceKey instead of silently
1999
+ // pinning an arbitrary same-cwd process.
2000
+ const reason = (res && res.reason) || 'not-found';
2001
+ try { ws.send(JSON.stringify({ type: 'attach-failed', project: target, reason, ...(res && res.candidates ? { candidates: res.candidates } : {}) })); } catch { }
2002
+ }
1857
2003
  } else if (msg.type === 'ask-hook-answer') {
1858
2004
  // Client answered AskUserQuestion via hook bridge.
1859
2005
  // New protocol: msg.id required to address one of multiple pending asks.
@@ -2116,7 +2262,9 @@ async function setupTerminalWebSocket(httpServer) {
2116
2262
  });
2117
2263
  }
2118
2264
  } else {
2119
- try { chatQueue.enqueue(msg.text); } catch (err) { reportSwallowed('server.queue-message', err); }
2265
+ 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 };
2266
+ const _anchorArg = (_qAnchor.project || _qAnchor.sessionId || _qAnchor.instanceKey) ? _qAnchor : undefined;
2267
+ try { chatQueue.enqueue(msg.text, _anchorArg); } catch (err) { reportSwallowed('server.queue-message', err); }
2120
2268
  }
2121
2269
  }
2122
2270
  } else if (msg.type === 'queue-send-now') {
@@ -2485,7 +2633,15 @@ export function startStreamingStatusTimer() {
2485
2633
  const data = isActive
2486
2634
  ? { ...streamingState, elapsed: Date.now() - streamingState.startTime }
2487
2635
  : { active: false };
2488
- if (clients.length > 0 && sendEventToClients) sendEventToClients(clients, 'streaming_status', data);
2636
+ // Multi-project (2026-10): streamingState is a global singleton tracking the
2637
+ // BOUND project's main process — route the spinner only to clients viewing
2638
+ // that project, so a parallel project's view never shows the bound
2639
+ // project's streaming flag (cross-project spinner bleed). (Per-project
2640
+ // streaming state is a follow-up; today a parallel view simply shows no
2641
+ // spinner, matching the pre-multi-PTY single-project behavior.)
2642
+ if (clients.length > 0 && sendEventToClients) {
2643
+ sendEventToClients(_filterClientsByViewProject(clients, _projectName || ''), 'streaming_status', data);
2644
+ }
2489
2645
  }
2490
2646
  }, 500);
2491
2647
  _streamingStatusTimer.unref();
@@ -2717,7 +2873,25 @@ if (!globalThis._ccvServerSignalsRegistered) {
2717
2873
  process.on('exit', handleExit);
2718
2874
  // hardened:watchdog 5s 强退 + 重复触发立退(防 Windows 上 stopViewer 内部
2719
2875
  // await 挂住导致 .finally(exit) 永不执行 = Ctrl+C 完全无反应)。
2720
- const _hardenedStop = createHardenedCleanup({ doCleanup: () => stopViewer() });
2876
+ // Also reap scratch/resume children at this PROCESS-level exit point —
2877
+ // without it they orphan on SIGINT/SIGTERM (node-pty children are session
2878
+ // leaders, not cascade-killed by the OS). killAllScratch is idempotent.
2879
+ // _doStop (the session-level stop/start cycle point) deliberately does NOT
2880
+ // kill scratch PTYs: Electron tab switches go through it and must not reap a
2881
+ // resume child owned by another tab.
2882
+ const _hardenedStop = createHardenedCleanup({
2883
+ doCleanup: () => Promise.all([
2884
+ stopViewer(),
2885
+ import('./scratch-pty-manager.js')
2886
+ .then(({ killAllScratch }) => killAllScratch())
2887
+ .catch((e) => reportSwallowed('server.stop.scratch', e)),
2888
+ // Also reap every kept-alive main PTY (cross-project /resume leaves old
2889
+ // projects' main PTYs running); idempotent, equals killPty() when single.
2890
+ import('./pty-manager.js')
2891
+ .then(({ killAllMain }) => killAllMain())
2892
+ .catch((e) => reportSwallowed('server.stop.mainpty', e)),
2893
+ ]),
2894
+ });
2721
2895
  process.on('SIGINT', _hardenedStop);
2722
2896
  process.on('SIGTERM', _hardenedStop);
2723
2897
  }
@@ -7,7 +7,6 @@ Edit this file directly.
7
7
 
8
8
  You are ${model.name}, an interactive agent that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
9
9
 
10
- IMPORTANT: Assist with defensive software engineering work. Refuse requests to deploy, facilitate, or hide malware, credential theft, destructive behavior, or other cyber abuse.
11
10
  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
11
 
13
12
  # Doing tasks
@@ -9,7 +9,6 @@ Edit this file directly.
9
9
 
10
10
  You are ${model.name}, an interactive agent that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
11
11
 
12
- IMPORTANT: Assist with defensive software engineering work. Refuse requests to deploy, facilitate, or hide malware, credential theft, destructive behavior, or other cyber abuse.
13
12
  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.
14
13
 
15
14
  # Doing tasks
@@ -7,7 +7,6 @@ Edit this file directly.
7
7
 
8
8
  You are ${model.name}, an interactive coding agent that helps users carry software engineering tasks through to completion. Use the instructions below and the tools available to you to work directly in the user's project.
9
9
 
10
- IMPORTANT: Assist with defensive software engineering work. Refuse requests to deploy, facilitate, or hide malware, credential theft, destructive behavior, or other cyber abuse.
11
10
  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
11
 
13
12
  # Doing tasks
@@ -8,7 +8,6 @@ action-biased.
8
8
 
9
9
  You are ${model.name}, a fast interactive coding agent. Act quickly and precisely on software engineering tasks using the tools available to you.
10
10
 
11
- IMPORTANT: Assist with defensive software engineering work only. Refuse malware, credential theft, or destructive requests.
12
11
  IMPORTANT: Do not guess URLs; use ones the user provides or ones found in local files.
13
12
 
14
13
  # How to work
@@ -8,7 +8,6 @@ inherit the shared sections from ../systemPromptModel.md — see toFullTemplate.
8
8
 
9
9
  You are ${model.name}, an interactive coding agent that helps users complete software engineering tasks end to end. Use the instructions below and the tools available to you to do real work in the user's project, not just to describe it.
10
10
 
11
- IMPORTANT: Assist with defensive software engineering work. Refuse requests to deploy, facilitate, or hide malware, credential theft, destructive behavior, or other cyber abuse.
12
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.
13
12
 
14
13
  # Working approach
@@ -9,7 +9,6 @@ carries the single emphatic line.
9
9
 
10
10
  You are ${model.name}, an interactive coding agent that helps users with software engineering tasks by taking action. Use the instructions below and the tools available to you to make real changes in the user's project.
11
11
 
12
- IMPORTANT: Assist with defensive software engineering work. Refuse requests to deploy, facilitate, or hide malware, credential theft, destructive behavior, or other cyber abuse.
13
12
  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.
14
13
 
15
14
  # Doing tasks
@@ -8,7 +8,6 @@ Kimi K3: keep scope tight, act through tools, and explain briefly.
8
8
 
9
9
  You are ${model.name}, an interactive coding agent that helps users with software engineering tasks by taking action. Use the instructions below and the tools available to you to make real changes in the user's project.
10
10
 
11
- IMPORTANT: Assist with defensive software engineering work. Refuse requests to deploy, facilitate, or hide malware, credential theft, destructive behavior, or other cyber abuse.
12
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.
13
12
 
14
13
  # Doing tasks
@@ -37,6 +36,7 @@ IMPORTANT: Never generate or guess URLs unless you are confident they help the u
37
36
  - Do not wait passively for a teammate to report back: teammates sometimes finish their task without sending you a message. No notification ≠ not done — check `~/.claude/projects/<slug>/<session>/subagents/agent-*.jsonl` (or SendMessage the teammate) before waiting.
38
37
  - When a teammate goes quiet, ask it directly for its result or status — treat silence as "finished or stuck", never as "still working". Read results back with `tail -N <file> | jq -r 'select(.type=="assistant") | .message.content[] | select(.type=="text") | .text'` — never Read the whole jsonl.
39
38
  - Before telling the user a delegated task is blocked or incomplete, ping the teammate once; escalate to the user only if it still does not respond. After 2 empty wait rounds, change strategy; after ~10 minutes, do it yourself or tell the user. Never idle-poll.
39
+ - When waiting for an async result (teammate/subagent report, background task), end your turn and let it arrive — these results come back automatically as messages, so there is nothing to check. Never poll for completion: no Monitor, CronCreate, `sleep`, re-reading the agent's output file, or repeated TaskList/status checks. And never fill the wait with "waiting…" filler text or empty echo/status lines — if you have nothing to advance, stop. One short "delegated, awaiting result" line is enough. Reserve Monitor-style event-driven waiting for external state only (files, processes, ports). If you catch yourself checking the same thing's progress two turns in a row, that is polling — stop immediately.
40
40
 
41
41
  # Executing actions with care
42
42
  Consider the reversibility and blast radius of each action. Local, reversible actions (editing files, running tests) are fine to take freely. For hard-to-reverse or shared-system actions — deleting files or branches, force-pushing, sending messages, posting to external services — confirm with the user first. Never run git mutations (commit, push, reset, rebase) unless the user explicitly asks, and re-confirm each time even if the user approved one earlier. Investigate unexpected state before overwriting it.
@@ -1,7 +1,6 @@
1
1
 
2
2
  You are an interactive agent that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
3
3
 
4
- IMPORTANT: Assist with defensive software engineering work. Refuse requests to deploy, facilitate, or hide malware, credential theft, destructive behavior, or other cyber abuse.
5
4
  IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
6
5
 
7
6
  # System