cc-viewer 1.6.261 → 1.6.263

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 (83) hide show
  1. package/README.md +80 -81
  2. package/cli.js +1 -0
  3. package/dist/assets/{App-Co-5BI9q.js → App-CX6bF6ke.js} +1 -1
  4. package/dist/assets/{MdxEditorPanel-DFNyQW6b.js → MdxEditorPanel--reKHew0.js} +1 -1
  5. package/dist/assets/{Mobile-DwYIskc6.js → Mobile-YwIGAQWc.js} +1 -1
  6. package/dist/assets/{_baseUniq-DGKwdzTD.js → _baseUniq-DiLy7vi3.js} +1 -1
  7. package/dist/assets/{arc-nTLBi2ib.js → arc-CAB2oIHx.js} +1 -1
  8. package/dist/assets/{architectureDiagram-Q4EWVU46-pWWvthzV.js → architectureDiagram-Q4EWVU46-Cijl_JpW.js} +1 -1
  9. package/dist/assets/{blockDiagram-DXYQGD6D-CpU6v2gv.js → blockDiagram-DXYQGD6D-Bk4yWCPQ.js} +1 -1
  10. package/dist/assets/{c4Diagram-AHTNJAMY-zdTsx1jJ.js → c4Diagram-AHTNJAMY-Vz4JKuzi.js} +1 -1
  11. package/dist/assets/{channel-Cq1UANGu.js → channel-BnYKz_zI.js} +1 -1
  12. package/dist/assets/{chunk-4BX2VUAB-D6w9m5kB.js → chunk-4BX2VUAB-DM3ZjqKX.js} +1 -1
  13. package/dist/assets/{chunk-4TB4RGXK-DjZZc11c.js → chunk-4TB4RGXK-BTiJOoNa.js} +1 -1
  14. package/dist/assets/{chunk-55IACEB6-Cw2U09Tu.js → chunk-55IACEB6-B4fMQcTE.js} +1 -1
  15. package/dist/assets/{chunk-EDXVE4YY-Cv-G70H7.js → chunk-EDXVE4YY-B_WylnyS.js} +1 -1
  16. package/dist/assets/{chunk-FMBD7UC4-Dykxux6b.js → chunk-FMBD7UC4-Cx2lqZi9.js} +1 -1
  17. package/dist/assets/{chunk-OYMX7WX6-Cn9oAaEC.js → chunk-OYMX7WX6-CPZm7o6V.js} +1 -1
  18. package/dist/assets/{chunk-QZHKN3VN-CA1IccbG.js → chunk-QZHKN3VN-DuYVzv7E.js} +1 -1
  19. package/dist/assets/{chunk-YZCP3GAM-CbReBNg5.js → chunk-YZCP3GAM-Bk3OysLK.js} +1 -1
  20. package/dist/assets/classDiagram-6PBFFD2Q-CeAAXpgl.js +1 -0
  21. package/dist/assets/classDiagram-v2-HSJHXN6E-CeAAXpgl.js +1 -0
  22. package/dist/assets/clone-BcGHaFBY.js +1 -0
  23. package/dist/assets/{cose-bilkent-S5V4N54A-TwxpCIwM.js → cose-bilkent-S5V4N54A-CQuaqKHt.js} +1 -1
  24. package/dist/assets/{dagre-KV5264BT-CxuHKd9a.js → dagre-KV5264BT-BFdoRcuo.js} +1 -1
  25. package/dist/assets/{diagram-5BDNPKRD-BzR9Peib.js → diagram-5BDNPKRD-ByFdSFIu.js} +1 -1
  26. package/dist/assets/{diagram-G4DWMVQ6-PWFXrQqF.js → diagram-G4DWMVQ6-C1TcKWp0.js} +1 -1
  27. package/dist/assets/{diagram-MMDJMWI5-CqnrwtNB.js → diagram-MMDJMWI5-B5N1Sn5F.js} +1 -1
  28. package/dist/assets/{diagram-TYMM5635-Do2MhBCf.js → diagram-TYMM5635-B-payI0e.js} +1 -1
  29. package/dist/assets/{erDiagram-SMLLAGMA-CklZI6kO.js → erDiagram-SMLLAGMA-zziefklH.js} +1 -1
  30. package/dist/assets/{flowDiagram-DWJPFMVM-C_SKJN0d.js → flowDiagram-DWJPFMVM-BOSomu1b.js} +1 -1
  31. package/dist/assets/{ganttDiagram-T4ZO3ILL-ChhhqFiR.js → ganttDiagram-T4ZO3ILL-DILUsv0T.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-UUTBAWPF-HarEJVSI.js → gitGraphDiagram-UUTBAWPF-BKp2DE69.js} +1 -1
  33. package/dist/assets/{graph-BTcI3kpi.js → graph-NObGxitU.js} +1 -1
  34. package/dist/assets/{index-B0NtFDg2.js → index-0aPBVZuP.js} +1 -1
  35. package/dist/assets/{index-B7-pqPyp.js → index-BI-0Lyyt.js} +1 -1
  36. package/dist/assets/{index-CQ579eP2.js → index-BbXZgnby.js} +1 -1
  37. package/dist/assets/{index-BG6Hzhck.js → index-Bq9Sic2n.js} +1 -1
  38. package/dist/assets/{index-NNx0667m.js → index-C88BDuL0.js} +1 -1
  39. package/dist/assets/index-DHUf_c1w.js +2 -0
  40. package/dist/assets/{index-CF0YP7s8.js → index-PsZiLKrC.js} +1 -1
  41. package/dist/assets/{index-Bzp41aRh.js → index-VqFARC4A.js} +1 -1
  42. package/dist/assets/{index-Dzkxj8m_.css → index-_4BCXKKF.css} +1 -1
  43. package/dist/assets/{infoDiagram-42DDH7IO-DOcCtSEj.js → infoDiagram-42DDH7IO-D409o-BL.js} +1 -1
  44. package/dist/assets/{ishikawaDiagram-UXIWVN3A-Crb_pmG4.js → ishikawaDiagram-UXIWVN3A-CMVPOGr3.js} +1 -1
  45. package/dist/assets/{journeyDiagram-VCZTEJTY-BUv2Exj_.js → journeyDiagram-VCZTEJTY-Bl_5WlaZ.js} +1 -1
  46. package/dist/assets/{jszip.min-BuZCDHia.js → jszip.min-C2654z9i.js} +1 -1
  47. package/dist/assets/{kanban-definition-6JOO6SKY-DwSHz9oV.js → kanban-definition-6JOO6SKY-BfCyUP29.js} +1 -1
  48. package/dist/assets/{layout-Dp3N3WRT.js → layout-CgAMa0xE.js} +1 -1
  49. package/dist/assets/{linear-JU5ltHCp.js → linear-J1N1npGr.js} +1 -1
  50. package/dist/assets/{mermaid.core-f1mZHh7u.js → mermaid.core-YnqOkuoS.js} +2 -2
  51. package/dist/assets/{min-CDzial6q.js → min-CowkZam8.js} +1 -1
  52. package/dist/assets/{mindmap-definition-QFDTVHPH-DnhPHplB.js → mindmap-definition-QFDTVHPH-D7yMfot2.js} +1 -1
  53. package/dist/assets/{pieDiagram-DEJITSTG-dMhPj5wG.js → pieDiagram-DEJITSTG-DKUHBCwB.js} +1 -1
  54. package/dist/assets/{quadrantDiagram-34T5L4WZ-GCi1_zHo.js → quadrantDiagram-34T5L4WZ-DhRqBNfT.js} +1 -1
  55. package/dist/assets/{requirementDiagram-MS252O5E-WeErs64x.js → requirementDiagram-MS252O5E-DVE3wKT7.js} +1 -1
  56. package/dist/assets/{sankeyDiagram-XADWPNL6-BKpP6-8Q.js → sankeyDiagram-XADWPNL6-Rn_9b5V_.js} +1 -1
  57. package/dist/assets/seqResourceLoaders-B9D4RGth.js +2 -0
  58. package/dist/assets/{seqResourceLoaders-B8CyM6Ul.css → seqResourceLoaders-DZvMjXCl.css} +2 -2
  59. package/dist/assets/{sequenceDiagram-FGHM5R23-CAC5BKbx.js → sequenceDiagram-FGHM5R23-CemLRaXC.js} +1 -1
  60. package/dist/assets/{stateDiagram-FHFEXIEX-Cxrg604Z.js → stateDiagram-FHFEXIEX-DNT1gAty.js} +1 -1
  61. package/dist/assets/{stateDiagram-v2-QKLJ7IA2-DaUpf340.js → stateDiagram-v2-QKLJ7IA2-DPlMhu-M.js} +1 -1
  62. package/dist/assets/{timeline-definition-GMOUNBTQ-DbvXGDTU.js → timeline-definition-GMOUNBTQ-B-F8vgZN.js} +1 -1
  63. package/dist/assets/{vendor-antd-jGWxgt_-.js → vendor-antd-Dq3DHFa-.js} +1 -1
  64. package/dist/assets/{vendor-codemirror-9gysGjWj.js → vendor-codemirror-DjMkT0sn.js} +1 -1
  65. package/dist/assets/{vendor-mdxeditor-BFiCaYAL.js → vendor-mdxeditor-CrZ9SWce.js} +2 -2
  66. package/dist/assets/{vendor-qrcode-DjZ-VpGR.js → vendor-qrcode-C_77dtHg.js} +1 -1
  67. package/dist/assets/{vendor-virtuoso-DB1J9xKG.js → vendor-virtuoso-aMZPf2fi.js} +1 -1
  68. package/dist/assets/{vennDiagram-DHZGUBPP-BoIw-nWl.js → vennDiagram-DHZGUBPP-K67JHnnN.js} +1 -1
  69. package/dist/assets/{wardley-RL74JXVD-4PjirsHP.js → wardley-RL74JXVD-CCis01LD.js} +1 -1
  70. package/dist/assets/{wardleyDiagram-NUSXRM2D-C8RzJmfU.js → wardleyDiagram-NUSXRM2D-BjqRIOpN.js} +1 -1
  71. package/dist/assets/{xychartDiagram-5P7HB3ND-Ct6fDXwq.js → xychartDiagram-5P7HB3ND-CaXETYTI.js} +1 -1
  72. package/dist/index.html +5 -5
  73. package/lib/ask-bridge.js +19 -1
  74. package/lib/sdk-manager.js +48 -5
  75. package/package.json +1 -1
  76. package/pty-manager.js +1 -1
  77. package/scratch-pty-manager.js +2 -2
  78. package/server.js +110 -11
  79. package/dist/assets/classDiagram-6PBFFD2Q-PgwmelQQ.js +0 -1
  80. package/dist/assets/classDiagram-v2-HSJHXN6E-PgwmelQQ.js +0 -1
  81. package/dist/assets/clone-Dnu1HdEX.js +0 -1
  82. package/dist/assets/index-C6MrFmhs.js +0 -2
  83. package/dist/assets/seqResourceLoaders-80G7f5Kr.js +0 -2
@@ -359,10 +359,16 @@ async function _handleCanUseTool(toolName, input, options) {
359
359
  if (_broadcastWs) {
360
360
  _broadcastWs({ type: 'sdk-plan-pending', id, input });
361
361
  }
362
- const result = await _waitForApproval(id, 5 * 60 * 1000);
362
+ const result = await _waitForApproval(id, 5 * 60 * 1000, 'plan');
363
363
  if (result === null) {
364
364
  return { behavior: 'deny', message: 'Timeout waiting for plan approval' };
365
365
  }
366
+ // cancel sentinel guard:cancelApproval 共用 _pendingApprovals Map,
367
+ // 若 ask-cancel 撞到 plan id(kind tag 已防住,但保留 sentinel guard 作为防御性兜底),
368
+ // 不能 fall through 到 allow。
369
+ if (result && typeof result === 'object' && result.__cancelled__ === true) {
370
+ return { behavior: 'deny', message: result.reason || 'User aborted' };
371
+ }
366
372
  if (typeof result === 'object' && result.approve === false) {
367
373
  return { behavior: 'deny', message: result.feedback || 'User rejected the plan' };
368
374
  }
@@ -378,13 +384,23 @@ async function _handleCanUseTool(toolName, input, options) {
378
384
  }
379
385
  } catch {}
380
386
  }
387
+ const askTimeoutMs = 60 * 60 * 1000; // 60min — 等价 terminal "无超时",与 server.js HOOK_TIMEOUT 同源
388
+ const askStartedAt = Date.now();
381
389
  if (_broadcastWs) {
382
- _broadcastWs({ type: 'sdk-ask-pending', id, questions: input.questions });
390
+ _broadcastWs({ type: 'sdk-ask-pending', id, questions: input.questions, startedAt: askStartedAt, timeoutMs: askTimeoutMs });
383
391
  }
384
- const answers = await _waitForApproval(id, 5 * 60 * 1000);
392
+ const answers = await _waitForApproval(id, askTimeoutMs, 'ask');
385
393
  if (answers === null) {
386
394
  return { behavior: 'deny', message: 'Timeout waiting for user answer' };
387
395
  }
396
+ // cancel sentinel:cancelApproval 经 _waitForApproval 注入的 { __cancelled__: true, reason }。
397
+ // 等价 terminal Claude Code 的 onAbort 路径 — SDK 包会把这个 deny 配成 tool_result.is_error=true
398
+ // 后注入 transcript,下一轮请求 transcript 闭合,会话不卡死。
399
+ // 加 [cc-viewer:cancel] 前缀作为协议级 sentinel — toolResultBuilder.js 用前缀匹配区分
400
+ // cancelled vs rejected。
401
+ if (answers && typeof answers === 'object' && answers.__cancelled__ === true) {
402
+ return { behavior: 'deny', message: '[cc-viewer:cancel] ' + (answers.reason || 'User aborted') };
403
+ }
388
404
  return { behavior: 'allow', updatedInput: { questions: input.questions, answers } };
389
405
  }
390
406
 
@@ -416,10 +432,14 @@ async function _handleCanUseTool(toolName, input, options) {
416
432
  _broadcastWs({ type: 'perm-hook-pending', id, toolName, input });
417
433
  }
418
434
 
419
- const result = await _waitForApproval(id, 5 * 60 * 1000);
435
+ const result = await _waitForApproval(id, 5 * 60 * 1000, 'perm');
420
436
  if (result === null) {
421
437
  return { behavior: 'deny', message: 'Timeout waiting for user approval' };
422
438
  }
439
+ // cancel sentinel guard:同 plan 分支防 cancelApproval 撞 perm id 误 allow
440
+ if (result && typeof result === 'object' && result.__cancelled__ === true) {
441
+ return { behavior: 'deny', message: result.reason || 'User aborted' };
442
+ }
423
443
  const decision = typeof result === 'object' ? result.decision : result;
424
444
  const allowSession = typeof result === 'object' && result.allowSession;
425
445
  if (decision === 'deny') {
@@ -432,13 +452,14 @@ async function _handleCanUseTool(toolName, input, options) {
432
452
  return response;
433
453
  }
434
454
 
435
- function _waitForApproval(id, timeoutMs) {
455
+ function _waitForApproval(id, timeoutMs, kind) {
436
456
  return new Promise((resolve) => {
437
457
  const timer = setTimeout(() => {
438
458
  _pendingApprovals.delete(id);
439
459
  resolve(null);
440
460
  }, timeoutMs);
441
461
  _pendingApprovals.set(id, {
462
+ kind, // 'ask' | 'plan' | 'perm' — 让 cancelApproval 区分类型,避免 ask-cancel 撞 plan/perm id
442
463
  resolve: (value) => {
443
464
  clearTimeout(timer);
444
465
  _pendingApprovals.delete(id);
@@ -461,6 +482,28 @@ export function resolveApproval(id, value) {
461
482
  return false;
462
483
  }
463
484
 
485
+ /**
486
+ * Cancel a pending canUseTool approval — used by ask-cancel WS handler
487
+ * (user clicked Cancel button or typed-interrupt in input bar).
488
+ *
489
+ * 不等价 resolveApproval(id, null):null 在 _waitForApproval 已被 timeout 占用语义。
490
+ * 这里 resolve 一个 { __cancelled__: true, reason } sentinel,让 canUseTool 走 deny 分支
491
+ * 而非 allow(详见 _handleCanUseTool AskUserQuestion 块)。
492
+ *
493
+ * kind 校验:ask-cancel 协议只对 ask 类型生效。撞到 plan / perm id 时返 false 不处理 —
494
+ * 避免取消 ask 的信号被误用成"用户拒绝 plan",让模型上下文写错原因。
495
+ *
496
+ * 与 resolveApproval 共享同一 _pendingApprovals Map + 同一 first-wins atomic guard
497
+ * (pending.resolve clearTimeout + delete),cancel 与 answer 并发时后到的会 no-op。
498
+ */
499
+ export function cancelApproval(id, reason) {
500
+ const pending = _pendingApprovals.get(id);
501
+ if (!pending) return false;
502
+ if (pending.kind && pending.kind !== 'ask') return false;
503
+ pending.resolve({ __cancelled__: true, reason: typeof reason === 'string' ? reason : 'User aborted' });
504
+ return true;
505
+ }
506
+
464
507
  /**
465
508
  * Stop the active SDK session.
466
509
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-viewer",
3
- "version": "1.6.261",
3
+ "version": "1.6.263",
4
4
  "description": "Claude Code Logger visualization management tool",
5
5
  "license": "MIT",
6
6
  "main": "server.js",
package/pty-manager.js CHANGED
@@ -343,7 +343,7 @@ export async function spawnShell() {
343
343
 
344
344
  fixSpawnHelperPermissions();
345
345
 
346
- const shell = process.env.SHELL || '/bin/sh';
346
+ const shell = process.env.SHELL || (process.platform === 'win32' ? (process.env.ComSpec || 'cmd.exe') : '/bin/sh');
347
347
 
348
348
  lastExitCode = null;
349
349
  currentWorkspacePath = cwd;
@@ -124,7 +124,7 @@ export async function spawnScratch(id) {
124
124
  const pty = await getPty();
125
125
  fixSpawnHelperPermissions();
126
126
 
127
- const shell = process.env.SHELL || '/bin/sh';
127
+ const shell = process.env.SHELL || (process.platform === 'win32' ? (process.env.ComSpec || 'cmd.exe') : '/bin/sh');
128
128
  const env = { ...process.env };
129
129
  // 前缀扫描剥离 cc-viewer 主进程的全部协调变量,scratch shell 只继承 cwd + 通用 shell env。
130
130
  // cli.js 会在父进程 set 一批 CCV_*(CCV_CLI_MODE / CCV_PROJECT_DIR / CCV_PROXY_PORT / CCV_SDK_MODE /
@@ -278,5 +278,5 @@ export function hasScratchPty(id) {
278
278
 
279
279
  // 当前 spawn 用的 shell basename(zsh / bash / fish 等),供前端渲染 tab 标签
280
280
  export function getScratchShellBasename() {
281
- return basename(process.env.SHELL || '/bin/sh') || 'shell';
281
+ return basename(process.env.SHELL || (process.platform === 'win32' ? (process.env.ComSpec || 'cmd.exe') : '/bin/sh')) || 'shell';
282
282
  }
package/server.js CHANGED
@@ -158,6 +158,8 @@ function _notifyParentPending(msg) {
158
158
  case 'sdk-ask-timeout':
159
159
  case 'ask-hook-resolved':
160
160
  case 'sdk-ask-resolved':
161
+ case 'ask-hook-cancelled':
162
+ // 注:ask-cancel handler 统一发 ask-hook-cancelled(不论 SDK / Hook 路径)。
161
163
  event = { type: 'pending-remove', kind: 'ask', id: msg.id != null ? String(msg.id) : '__ask__' };
162
164
  break;
163
165
  default:
@@ -2420,7 +2422,8 @@ async function handleRequest(req, res) {
2420
2422
  }
2421
2423
  }
2422
2424
 
2423
- const HOOK_TIMEOUT = 5 * 60 * 1000; // 5 minutes
2425
+ const HOOK_TIMEOUT = 60 * 60 * 1000; // 60 minutes — 等价 terminal Claude Code 的"无超时"体验
2426
+ // (terminal interactiveHandler 本身无 timeout,hook 子进程层 10min;这里 60min 远超人类响应时间)
2424
2427
  // toolUseId 路由策略:
2425
2428
  // - char whitelist + ≤256 长度 防恶意 1MB key 撑大 Map
2426
2429
  // - 已存在同 id 但旧 res 已断(writableEnded/destroyed)→ 复用槽位(ask-bridge 重试场景)
@@ -2505,11 +2508,12 @@ async function handleRequest(req, res) {
2505
2508
  }
2506
2509
  }, HOOK_TIMEOUT);
2507
2510
 
2508
- pendingAskHooks.set(id, { questions, res, timer, createdAt: Date.now() });
2511
+ const askStartedAt = Date.now();
2512
+ pendingAskHooks.set(id, { questions, res, timer, createdAt: askStartedAt });
2509
2513
 
2510
- // Broadcast to all terminal WS clients
2514
+ // Broadcast to all terminal WS clients — 附 startedAt + timeoutMs 让前端渲染倒计时
2511
2515
  if (terminalWss) {
2512
- const pmsg = JSON.stringify({ type: 'ask-hook-pending', id, questions });
2516
+ const pmsg = JSON.stringify({ type: 'ask-hook-pending', id, questions, startedAt: askStartedAt, timeoutMs: HOOK_TIMEOUT });
2513
2517
  terminalWss.clients.forEach((client) => {
2514
2518
  if (client.readyState === 1) {
2515
2519
  try { client.send(pmsg); } catch {}
@@ -4103,6 +4107,27 @@ async function setupTerminalWebSocket(httpServer) {
4103
4107
  ws.send(JSON.stringify({ type: 'data', data: buffer }));
4104
4108
  }
4105
4109
 
4110
+ // Replay pending ask-hook 请求:浏览器关 tab 再开(或 ws 重连)时,
4111
+ // 让新 ws 立即收到当前 server-side 仍 long-poll 的 ask 列表 + startedAt + 剩余 timeoutMs,
4112
+ // 否则前端 askMetaMap 空 → 倒计时不渲染 + lastPendingAskId 派生错。
4113
+ // ASK_HOOK_TIMEOUT 在闭包外(line 2425 const HOOK_TIMEOUT),不直接可见 — 用 60min 字面量同源。
4114
+ const REPLAY_HOOK_TIMEOUT = 60 * 60 * 1000;
4115
+ const now = Date.now();
4116
+ for (const [id, entry] of pendingAskHooks) {
4117
+ const elapsed = now - (entry.createdAt || now);
4118
+ const remaining = Math.max(0, REPLAY_HOOK_TIMEOUT - elapsed);
4119
+ if (remaining <= 0) continue;
4120
+ try {
4121
+ ws.send(JSON.stringify({
4122
+ type: 'ask-hook-pending',
4123
+ id,
4124
+ questions: entry.questions,
4125
+ startedAt: now, // 让前端按"还剩 remaining"起算(不是原 createdAt)
4126
+ timeoutMs: remaining, // 剩余可用时间
4127
+ }));
4128
+ } catch {}
4129
+ }
4130
+
4106
4131
  // 兜底重绘标记:claude TUI 在 alternate-screen 下只在收到 SIGWINCH 时重绘整屏。
4107
4132
  // 若前端首次 resize 与 PTY 当前尺寸恰好相等,pty.resize noop 不发 SIGWINCH → 前端空白。
4108
4133
  // 该 ws 收到第一条 resize 时(见 ws.on('message')),抖动 (rows+1) → (rows) 触发 SIGWINCH。
@@ -4189,18 +4214,15 @@ async function setupTerminalWebSocket(httpServer) {
4189
4214
  } else if (msg.type === 'ask-hook-answer') {
4190
4215
  // Client answered AskUserQuestion via hook bridge.
4191
4216
  // New protocol: msg.id required to address one of multiple pending asks.
4192
- // Legacy fallback: no id → resolve the oldest pending ask (preserves pre-Map client behavior).
4217
+ // 老协议 fallback(取最老)已废弃 pending 时会"答错对象"造成串答;
4218
+ // 缺 id 直接 WARN 并丢弃,让前端在 console 里看到为什么答案没生效。
4193
4219
  let askAnswered = false;
4194
4220
  let askId = msg.id;
4195
4221
  let askEntry = null;
4196
4222
  if (askId) {
4197
4223
  askEntry = pendingAskHooks.get(askId);
4198
4224
  } else {
4199
- const firstId = pendingAskHooks.keys().next().value;
4200
- if (firstId) {
4201
- askId = firstId;
4202
- askEntry = pendingAskHooks.get(firstId);
4203
- }
4225
+ console.warn('[server] ask-hook-answer missing id — legacy fallback removed to prevent cross-question mis-routing; ignoring');
4204
4226
  }
4205
4227
  if (askEntry) {
4206
4228
  const { res: hookRes, timer } = askEntry;
@@ -4222,6 +4244,21 @@ async function setupTerminalWebSocket(httpServer) {
4222
4244
  });
4223
4245
  }
4224
4246
  if (askAnswered) _notifyParentPending({ type: 'ask-hook-resolved', id: askId });
4247
+ // entry 不在(LRU evicted / 已答 / 跨 client race / 60min 超时)— 给发起方 ack
4248
+ // ask-hook-cancelled 让前端关 modal + _pendingFlushQueue 兜底处理(如有 user
4249
+ // message 等 ack 待 flush)。行为对齐 ask-cancel handler handled=false 分支语义。
4250
+ // 不广播给其他 client(与 ack-cancel handled=false 一致),防误覆盖真实 answer。
4251
+ if (!askAnswered && askId) {
4252
+ try {
4253
+ if (ws && ws.readyState === 1) {
4254
+ ws.send(JSON.stringify({
4255
+ type: 'ask-hook-cancelled',
4256
+ id: askId,
4257
+ reason: 'Ask entry no longer exists (timeout / evicted / already resolved)',
4258
+ }));
4259
+ }
4260
+ } catch {}
4261
+ }
4225
4262
  } else if (msg.type === 'perm-hook-answer') {
4226
4263
  // Permission approval — SDK mode (canUseTool) or PTY mode (hook bridge)
4227
4264
  let permAnswered = false;
@@ -4261,6 +4298,61 @@ async function setupTerminalWebSocket(httpServer) {
4261
4298
  });
4262
4299
  }
4263
4300
  if (msg.id) _notifyParentPending({ type: 'sdk-ask-resolved', id: msg.id });
4301
+ } else if (msg.type === 'ask-cancel') {
4302
+ // 用户主动取消 AskUserQuestion(或 ChatInputBar 提交新 prompt 时打断 pending ask)。
4303
+ // 双模式分流:先查 SDK _pendingApprovals → 再查 Hook pendingAskHooks → 都没有也广播 ack
4304
+ // (LRU evicted / plugin-already-resolved / WS 重发等场景兜底,让所有 client modal 同步关掉)。
4305
+ // cancelId/Reason 校验:与 toolUseId 同套白名单(≤256 字符 + [a-zA-Z0-9_-])+ reason ≤500
4306
+ // 防恶意/buggy client 塞超长 key 撑大 _pendingApprovals 或塞 1MB reason 打爆 broadcast。
4307
+ const rawId = msg.id != null ? String(msg.id) : null;
4308
+ const cancelId = rawId && rawId.length > 0 && rawId.length <= 256 && /^[a-zA-Z0-9_-]+$/.test(rawId) ? rawId : null;
4309
+ if (rawId && !cancelId) {
4310
+ console.warn('[server] ask-cancel rejected: invalid id format');
4311
+ return;
4312
+ }
4313
+ const cancelReason = (typeof msg.reason === 'string' ? msg.reason : 'User aborted').slice(0, 500);
4314
+ let handled = false;
4315
+ // SDK 路径:调 cancelApproval 让 _waitForApproval resolve cancel sentinel
4316
+ // (sdk-manager.js canUseTool 检测 sentinel 后返回 { behavior: 'deny', message: cancelReason }
4317
+ // → SDK 包内置 ensureToolResultPairing 兜住 transcript)
4318
+ if (cancelId && _sdkCancelApproval) {
4319
+ try { handled = _sdkCancelApproval(cancelId, cancelReason) === true; } catch {}
4320
+ }
4321
+ // Hook 路径:给对应 res 回 200 + { cancelled: true, reason }
4322
+ // ask-bridge.js 检测 cancelled 字段后输出 { hookSpecificOutput: { permissionDecision: 'deny', ... } }
4323
+ if (!handled && cancelId) {
4324
+ const askEntry = pendingAskHooks.get(cancelId);
4325
+ if (askEntry) {
4326
+ const { res: hookRes, timer } = askEntry;
4327
+ if (timer) clearTimeout(timer);
4328
+ pendingAskHooks.delete(cancelId);
4329
+ handled = true;
4330
+ try {
4331
+ if (hookRes && !hookRes.headersSent) {
4332
+ hookRes.writeHead(200, { 'Content-Type': 'application/json' });
4333
+ hookRes.end(JSON.stringify({ cancelled: true, reason: cancelReason }));
4334
+ }
4335
+ } catch {}
4336
+ }
4337
+ }
4338
+ // ack 广播分两档:
4339
+ // - handled=true(真的取消了 SDK 或 Hook entry)→ 广播给所有 client + 通知 parent
4340
+ // - handled=false(LRU evicted / plugin 已答 / 重发等)→ 只 ack 给发起方,不广播
4341
+ // 原因:那些场景下其他 client 看到的是 answered 而非 cancelled,广播会让前端
4342
+ // localAskAnswers 误覆盖真实 answer 涂成灰态。发起方自身已经乐观写过,此时
4343
+ // server 的 ack 实际只起"sync 错过的 ack"作用。
4344
+ if (cancelId) {
4345
+ const cmsg = JSON.stringify({ type: 'ask-hook-cancelled', id: cancelId, reason: cancelReason });
4346
+ if (handled && terminalWss) {
4347
+ terminalWss.clients.forEach((c) => {
4348
+ if (c.readyState === 1) try { c.send(cmsg); } catch {}
4349
+ });
4350
+ _notifyParentPending({ type: 'ask-hook-cancelled', id: cancelId });
4351
+ } else if (!handled && ws && ws.readyState === 1) {
4352
+ // 仅 ack 给发起方,让其前端 _waitingCancelAck flush user message
4353
+ try { ws.send(cmsg); } catch {}
4354
+ }
4355
+ }
4264
4356
  } else if (msg.type === 'sdk-plan-answer') {
4265
4357
  // Plan approval in SDK mode
4266
4358
  if (_sdkResolveApproval) {
@@ -4465,7 +4557,8 @@ export function broadcastWsMessage(msg) {
4465
4557
  // 显式调用 _notifyParentPending 的分支(ask-hook-resolved 等)走 ws.send 不进这里,无重复触发。
4466
4558
  if (msg && typeof msg === 'object' && typeof msg.type === 'string'
4467
4559
  && (msg.type === 'sdk-ask-pending' || msg.type === 'sdk-ask-resolved' || msg.type === 'sdk-ask-timeout'
4468
- || msg.type === 'ask-hook-pending' || msg.type === 'ask-hook-resolved' || msg.type === 'ask-hook-timeout')) {
4560
+ || msg.type === 'ask-hook-pending' || msg.type === 'ask-hook-resolved' || msg.type === 'ask-hook-timeout'
4561
+ || msg.type === 'ask-hook-cancelled')) {
4469
4562
  _notifyParentPending(msg);
4470
4563
  }
4471
4564
  }
@@ -4474,6 +4567,12 @@ export function broadcastWsMessage(msg) {
4474
4567
  let _sdkResolveApproval = null;
4475
4568
  export function setSdkResolveApproval(fn) { _sdkResolveApproval = fn; }
4476
4569
 
4570
+ /** Reference to sdk-manager's cancelApproval (set by cli.js after import). */
4571
+ // 与 _sdkResolveApproval 平行——但语义不同:cancelApproval 让 _waitForApproval resolve
4572
+ // 一个 cancel sentinel,让 canUseTool 走 deny 分支(而非 allow)。
4573
+ let _sdkCancelApproval = null;
4574
+ export function setSdkCancelApproval(fn) { _sdkCancelApproval = fn; }
4575
+
4477
4576
  /** Reference to sdk-manager's sendUserMessage (set by cli.js after import). */
4478
4577
  let _sdkSendUserMessage = null;
4479
4578
  export function setSdkSendUserMessage(fn) { _sdkSendUserMessage = fn; }
@@ -1 +0,0 @@
1
- import{s as styles_default,c as classRenderer_v3_unified_default,a as classDiagram_default,C as ClassDB}from"./chunk-4TB4RGXK-DjZZc11c.js";import{_ as __name}from"./mermaid.core-f1mZHh7u.js";import"./chunk-FMBD7UC4-Dykxux6b.js";import"./chunk-YZCP3GAM-CbReBNg5.js";import"./chunk-55IACEB6-Cw2U09Tu.js";import"./chunk-EDXVE4YY-Cv-G70H7.js";import"./vendor-markdown-BFrYfpb0.js";import"./vendor-mdxeditor-BFiCaYAL.js";import"./vendor-antd-jGWxgt_-.js";import"./vendor-codemirror-9gysGjWj.js";var diagram={parser:classDiagram_default,get db(){return new ClassDB},renderer:classRenderer_v3_unified_default,styles:styles_default,init:__name(cnf=>{cnf.class||(cnf.class={}),cnf.class.arrowMarkerAbsolute=cnf.arrowMarkerAbsolute},"init")};export{diagram};
@@ -1 +0,0 @@
1
- import{s as styles_default,c as classRenderer_v3_unified_default,a as classDiagram_default,C as ClassDB}from"./chunk-4TB4RGXK-DjZZc11c.js";import{_ as __name}from"./mermaid.core-f1mZHh7u.js";import"./chunk-FMBD7UC4-Dykxux6b.js";import"./chunk-YZCP3GAM-CbReBNg5.js";import"./chunk-55IACEB6-Cw2U09Tu.js";import"./chunk-EDXVE4YY-Cv-G70H7.js";import"./vendor-markdown-BFrYfpb0.js";import"./vendor-mdxeditor-BFiCaYAL.js";import"./vendor-antd-jGWxgt_-.js";import"./vendor-codemirror-9gysGjWj.js";var diagram={parser:classDiagram_default,get db(){return new ClassDB},renderer:classRenderer_v3_unified_default,styles:styles_default,init:__name(cnf=>{cnf.class||(cnf.class={}),cnf.class.arrowMarkerAbsolute=cnf.arrowMarkerAbsolute},"init")};export{diagram};
@@ -1 +0,0 @@
1
- import{b as baseClone}from"./graph-BTcI3kpi.js";function clone(value){return baseClone(value,4)}export{clone as c};