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
- // Enter (个别布局要显式确认), 标题变了说明弹的已是另一个框 —— 绝不盲按, 免得替
1054
- // 用户确认了他没看过的东西。返回按完后的屏面 verdict, 由调用方裁决成败。
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 && after.title === before.title) {
1061
- await tmuxRun(["send-keys", "-t", pane, "Enter"]);
1062
- await sleepMs(MODAL_PRESS_SETTLE_MS);
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
- return after;
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
- enqueueStandalone(a, item.body);
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" || item.kind === "skill_output") {
2495
- if (item.kind !== "skill_output" || !item.quiet)
2496
- enqueueStandalone(a, item.body);
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
- enqueueStandalone(a, item.body);
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
  }