cc-viewer 1.6.262 → 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.
- package/README.md +80 -81
- package/cli.js +1 -0
- package/dist/assets/{App-DfhQt_ed.js → App-CX6bF6ke.js} +1 -1
- package/dist/assets/{MdxEditorPanel-j9aQWwCJ.js → MdxEditorPanel--reKHew0.js} +1 -1
- package/dist/assets/{Mobile-0ZF71DQy.js → Mobile-YwIGAQWc.js} +1 -1
- package/dist/assets/{index-DX4SlYho.js → index-DHUf_c1w.js} +2 -2
- package/dist/assets/{index-Dzkxj8m_.css → index-_4BCXKKF.css} +1 -1
- package/dist/assets/seqResourceLoaders-B9D4RGth.js +2 -0
- package/dist/assets/{seqResourceLoaders-DSKrKxVy.css → seqResourceLoaders-DZvMjXCl.css} +2 -2
- package/dist/index.html +2 -2
- package/lib/ask-bridge.js +19 -1
- package/lib/sdk-manager.js +48 -5
- package/package.json +1 -1
- package/server.js +110 -11
- package/dist/assets/seqResourceLoaders-CH1DqmCg.js +0 -2
package/lib/ask-bridge.js
CHANGED
|
@@ -85,7 +85,7 @@ for (const q of questions) {
|
|
|
85
85
|
// 缺失时 server 会 fallback 到自生成 ask_${ts}_${rand}(向后兼容老 Claude Code 版本)。
|
|
86
86
|
const toolUseId = payload?.tool_use_id || null;
|
|
87
87
|
|
|
88
|
-
const TIMEOUT_MS =
|
|
88
|
+
const TIMEOUT_MS = 60 * 60 * 1000; // 60 minutes(与 server.js HOOK_TIMEOUT 同源)
|
|
89
89
|
|
|
90
90
|
function postToViewer() {
|
|
91
91
|
return new Promise((resolve, reject) => {
|
|
@@ -129,6 +129,24 @@ function postToViewer() {
|
|
|
129
129
|
|
|
130
130
|
try {
|
|
131
131
|
const data = await postToViewer();
|
|
132
|
+
// 用户在 cc-viewer web UI 主动取消(点 Cancel 按钮 / 在输入框打字打断 pending ask)。
|
|
133
|
+
// server.js 的 ask-cancel handler 会给 hook res 回 200 + { cancelled: true, reason }。
|
|
134
|
+
// 输出 PreToolUse hook deny 让 Claude Code 走兜底链:toolExecution.ts 把 deny.message 包装
|
|
135
|
+
// 成 tool_result.is_error=true,配对完整后下一轮 API 不会 400,主循环就绪接收新 prompt。
|
|
136
|
+
if (data.cancelled === true) {
|
|
137
|
+
const reason = typeof data.reason === 'string' && data.reason.length > 0 ? data.reason : 'User aborted by cc-viewer';
|
|
138
|
+
// 加 [cc-viewer:cancel] 前缀作为协议级 sentinel — toolResultBuilder.js 用前缀匹配
|
|
139
|
+
// 区分 cancelled vs rejected,避免靠自然语言模糊匹配(SDK 升级换文案就会失效)。
|
|
140
|
+
const output = {
|
|
141
|
+
hookSpecificOutput: {
|
|
142
|
+
hookEventName: 'PreToolUse',
|
|
143
|
+
permissionDecision: 'deny',
|
|
144
|
+
permissionDecisionReason: '[cc-viewer:cancel] ' + reason,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
process.stdout.write(JSON.stringify(output) + '\n');
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
132
150
|
if (!data.answers || typeof data.answers !== 'object' || Array.isArray(data.answers)) {
|
|
133
151
|
// No valid answers → fall back to terminal UI
|
|
134
152
|
process.stderr.write('ask-bridge: No answers in response (falling back to terminal UI)\n');
|
package/lib/sdk-manager.js
CHANGED
|
@@ -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,
|
|
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
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 =
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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; }
|