wezard 1.3.16 → 1.3.18
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.
|
@@ -17,7 +17,7 @@ import { existsSync, readdirSync, statSync, watch, openSync, readSync, closeSync
|
|
|
17
17
|
import { join, dirname } from "node:path";
|
|
18
18
|
import { expandHome, sanitizeId } from "../shared/paths.js";
|
|
19
19
|
import { activeBackends, backendForPath, projectDirFor, projectDirsFor, } from "../shared/cli-backends.js";
|
|
20
|
-
import { isModalPane, isAskqSubmitPage, parseModalOptions, pickModalAnswer, pickAutoAllowAnswer, parseConfirmContext } from "../shared/modal-pane.js";
|
|
20
|
+
import { isModalPane, isAskqSubmitPage, parseModalOptions, pickModalAnswer, pickAutoAllowAnswer, parseConfirmContext, looksLikePicker } from "../shared/modal-pane.js";
|
|
21
21
|
import { hasMirrorAskq, runMirrorAskqFlow, hasMirrorPlan, mootMirrorPlan, runMirrorPlanFlow, hasMirrorPicker, mootMirrorPicker, runMirrorPickerFlow } from "./approval.js";
|
|
22
22
|
import { isAutoWindowActive } from "./session-cache.js";
|
|
23
23
|
import { dangerOf } from "./danger.js";
|
|
@@ -1049,20 +1049,37 @@ const detectModalPicker = async (target) => {
|
|
|
1049
1049
|
const screen = lines.slice(-MODAL_SCAN_ROWS).join("\n");
|
|
1050
1050
|
return { ...isModalPane(screen), screen };
|
|
1051
1051
|
};
|
|
1052
|
-
// 按下确认框的第 index
|
|
1053
|
-
//
|
|
1054
|
-
//
|
|
1052
|
+
// 按下确认框的第 index 号选项。Claude Code 的 picker 数字键即选即确认; CodeBuddy
|
|
1053
|
+
// 的框数字键不一定生效, 且高亮可能停在别的选项上 (实测见过默认高亮在 "Yes, and
|
|
1054
|
+
// don't ask again…" 上) —— 盲补 Enter 会把**高亮项**确认下去, 可能恰是那条放宽
|
|
1055
|
+
// 权限的选项。所以补 Enter 前必须核对高亮就在目标项; 不在就用方向键挪过去、复核
|
|
1056
|
+
// 后再按; 高亮读不出 / 挪不动 / 标题变了 (已是另一个框) 一律罢手, 交调用方兜底。
|
|
1057
|
+
// 返回按完后的屏面 verdict, 由调用方裁决成败。
|
|
1055
1058
|
const MODAL_PRESS_SETTLE_MS = 400;
|
|
1059
|
+
const MODAL_ARROW_SETTLE_MS = 150;
|
|
1060
|
+
const highlightedIndex = (screen) => parseModalOptions(screen).find((o) => o.selected)?.index;
|
|
1056
1061
|
const pressModalIndex = async (pane, before, index) => {
|
|
1057
1062
|
await tmuxRun(["send-keys", "-t", pane, String(index)]);
|
|
1058
1063
|
await sleepMs(MODAL_PRESS_SETTLE_MS);
|
|
1059
1064
|
let after = await detectModalPicker(pane);
|
|
1060
|
-
if (after.modal
|
|
1061
|
-
|
|
1062
|
-
|
|
1065
|
+
if (!after.modal || after.title !== before.title)
|
|
1066
|
+
return after;
|
|
1067
|
+
// 数字键没关掉框: 把高亮挪到目标项。guard 上限盖过任何确认框的选项数。
|
|
1068
|
+
for (let guard = 0; guard < 8; guard++) {
|
|
1069
|
+
const cur = highlightedIndex(after.screen);
|
|
1070
|
+
if (cur === undefined || cur === index)
|
|
1071
|
+
break;
|
|
1072
|
+
await tmuxRun(["send-keys", "-t", pane, cur < index ? "Down" : "Up"]);
|
|
1073
|
+
await sleepMs(MODAL_ARROW_SETTLE_MS);
|
|
1063
1074
|
after = await detectModalPicker(pane);
|
|
1075
|
+
if (!after.modal || after.title !== before.title)
|
|
1076
|
+
return after;
|
|
1064
1077
|
}
|
|
1065
|
-
|
|
1078
|
+
if (highlightedIndex(after.screen) !== index)
|
|
1079
|
+
return after; // 核不准, 绝不盲按
|
|
1080
|
+
await tmuxRun(["send-keys", "-t", pane, "Enter"]);
|
|
1081
|
+
await sleepMs(MODAL_PRESS_SETTLE_MS);
|
|
1082
|
+
return detectModalPicker(pane);
|
|
1066
1083
|
};
|
|
1067
1084
|
// 「聊聊这个」收尾: 文本 inject 已把引导语贴进自定义文本行 (❯ N. <text>)。codebuddy
|
|
1068
1085
|
// 的 AskUserQuestion 面板把自定义行和 Submit 行做成两个独立行, 光标此刻停在自定义
|
|
@@ -1785,6 +1802,13 @@ export const startMirror = (deps) => {
|
|
|
1785
1802
|
clearTimeout(a.standaloneBuf.timer);
|
|
1786
1803
|
flushStandalone(a);
|
|
1787
1804
|
};
|
|
1805
|
+
// Skill 反馈 (/model 的 "Switch model to …" 等) 是用户敲完命令正在等的回执 ——
|
|
1806
|
+
// 不进 standaloneDebounceMs 防抖窗 (默认 30s, 对交互回执就是纯延迟)。先清掉
|
|
1807
|
+
// 在途 debounce 缓冲保序 (同批 drain 里先到的 user 回显不能被反超), 再立即发。
|
|
1808
|
+
const sendStandaloneNow = (a, content) => {
|
|
1809
|
+
flushPendingStandalone(a);
|
|
1810
|
+
sendStandalone(a, content);
|
|
1811
|
+
};
|
|
1788
1812
|
// Path A: a needs-approval tool_use just landed. Aggregate the buffer as one
|
|
1789
1813
|
// standalone, send it BEFORE the approval card race, transition to AWAITING_APPR.
|
|
1790
1814
|
const promoteToStandalone = (a) => {
|
|
@@ -1910,7 +1934,7 @@ export const startMirror = (deps) => {
|
|
|
1910
1934
|
// as a standalone bubble so the user sees the result immediately.
|
|
1911
1935
|
if (item.kind === "skill_output") {
|
|
1912
1936
|
if (!item.quiet)
|
|
1913
|
-
|
|
1937
|
+
sendStandaloneNow(a, item.body);
|
|
1914
1938
|
return;
|
|
1915
1939
|
}
|
|
1916
1940
|
const wasEmpty = out.buf.length === 0;
|
|
@@ -2491,9 +2515,13 @@ export const startMirror = (deps) => {
|
|
|
2491
2515
|
// chat with hundreds of tool bubbles. turn_end = the goal auto-cleared and the
|
|
2492
2516
|
// model finally stopped → leave goal mode; normal brief resumes next turn.
|
|
2493
2517
|
const handleGoalItem = (a, item) => {
|
|
2494
|
-
if (item.kind === "text"
|
|
2495
|
-
|
|
2496
|
-
|
|
2518
|
+
if (item.kind === "text") {
|
|
2519
|
+
enqueueStandalone(a, item.body);
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
if (item.kind === "skill_output") {
|
|
2523
|
+
if (!item.quiet)
|
|
2524
|
+
sendStandaloneNow(a, item.body);
|
|
2497
2525
|
return;
|
|
2498
2526
|
}
|
|
2499
2527
|
if (item.kind === "turn_end") {
|
|
@@ -2905,7 +2933,7 @@ export const startMirror = (deps) => {
|
|
|
2905
2933
|
// an active stream so the result is independently visible.
|
|
2906
2934
|
if (item.kind === "skill_output") {
|
|
2907
2935
|
if (!item.quiet)
|
|
2908
|
-
|
|
2936
|
+
sendStandaloneNow(a, item.body);
|
|
2909
2937
|
return;
|
|
2910
2938
|
}
|
|
2911
2939
|
// Assistant turn truly ended (stop_reason terminal). Finalize the live
|
|
@@ -4444,9 +4472,19 @@ export const startMirror = (deps) => {
|
|
|
4444
4472
|
continue;
|
|
4445
4473
|
const v = await detectModalPicker(a.tmuxPane);
|
|
4446
4474
|
if (!v.modal) {
|
|
4447
|
-
a.pickerSentinelFp = undefined;
|
|
4448
4475
|
// 审批卡在途而框已消失 = 本地先按掉了 → 作废卡等待 (无卡则 no-op)。
|
|
4449
4476
|
mootMirrorPicker(a.sessionId);
|
|
4477
|
+
// 诊断: 屏上有高亮编号行却没判成 modal = 布局盲区候选 (askq/plan 面板
|
|
4478
|
+
// 也长这样, 属正常)。每屏记一次日志, 供远端排查"哨兵为何没反应"。
|
|
4479
|
+
if (looksLikePicker(v.screen)) {
|
|
4480
|
+
if (a.pickerSentinelFp !== v.screen) {
|
|
4481
|
+
a.pickerSentinelFp = v.screen;
|
|
4482
|
+
log.info({ target: a.target, tail: v.screen.split("\n").slice(-8).join("\\n").slice(-600) }, "picker sentinel: numbered picker shape on pane but not judged modal");
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
4485
|
+
else {
|
|
4486
|
+
a.pickerSentinelFp = undefined;
|
|
4487
|
+
}
|
|
4450
4488
|
continue;
|
|
4451
4489
|
}
|
|
4452
4490
|
if (hasMirrorPicker(a.sessionId))
|
|
@@ -4496,6 +4534,11 @@ export const startMirror = (deps) => {
|
|
|
4496
4534
|
danger: danger?.rule,
|
|
4497
4535
|
voteTimeoutMs: cfg.approval.longPollSec * 1000,
|
|
4498
4536
|
press: (what) => pressNativePicker(a, ctx.toolName, what),
|
|
4537
|
+
}).then((sent) => {
|
|
4538
|
+
// 卡没发出去 (WS 断/发送失败): 清指纹, 让下一 tick 对同屏重试发卡,
|
|
4539
|
+
// 否则这只框会被指纹永久静默。
|
|
4540
|
+
if (!sent)
|
|
4541
|
+
a.pickerSentinelFp = undefined;
|
|
4499
4542
|
}).catch((e) => log.warn({ err: e.message, target: a.target }, "mirror picker flow failed"));
|
|
4500
4543
|
}
|
|
4501
4544
|
}
|