wezard 1.2.22 → 1.2.24

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, parseModalOptions, pickModalAnswer } from "../shared/modal-pane.js";
20
+ import { isModalPane, isAskqSubmitPage, parseModalOptions, pickModalAnswer } from "../shared/modal-pane.js";
21
21
  import { hasMirrorAskq, runMirrorAskqFlow, hasMirrorPlan, mootMirrorPlan, runMirrorPlanFlow } from "./approval.js";
22
22
  import { runTmux as runTmuxCmd, spawnTmuxClaude } from "./spawn-tmux.js";
23
23
  import { recordTool, recordToolResult, recordTurnStart, recordTurnItem, recordTurnUsage, recordTurnClose, recordCloseOpenTurns, buildDetailUrl, buildChatUrl } from "./detail.js";
@@ -1009,6 +1009,64 @@ const detectModalPicker = async (target) => {
1009
1009
  const screen = lines.slice(-MODAL_SCAN_ROWS).join("\n");
1010
1010
  return { ...isModalPane(screen), screen };
1011
1011
  };
1012
+ // 「聊聊这个」收尾: 文本 inject 已把引导语贴进自定义文本行 (❯ N. <text>)。codebuddy
1013
+ // 的 AskUserQuestion 面板把自定义行和 Submit 行做成两个独立行, 光标此刻停在自定义
1014
+ // 行, 直接 Enter 只是在文本里换行, 提交不了。这里读屏确认 Submit 行已渲染
1015
+ // (isAskqSubmitPage) → Down 落到 Submit 行 → Enter 收工 → 确认面板关闭 (!isModalPane)。
1016
+ // 全程 bare `-p` (仅当前屏, 不碰 scrollback 里的旧面板残影)。最多重试
1017
+ // ASKQ_CONFIRM_ENTERS 次, 仍在面板里则判失败, 交回上层告警兜底。
1018
+ const ASKQ_CONFIRM_ENTERS = 3;
1019
+ const ASKQ_CONFIRM_POLL_MS = 150;
1020
+ const ASKQ_CONFIRM_STAGE_MS = 2500; // 每阶段 (到提交页 / 面板关闭) 的轮询上限
1021
+ const confirmAskqSubmit = async (target, paneAlive) => {
1022
+ const capture = async () => {
1023
+ const r = await tmuxRun(["capture-pane", "-t", target, "-p"]);
1024
+ return r.ok ? r.stdout.replace(/\s+$/u, "") : "";
1025
+ };
1026
+ // 轮询到 pred 为真; 期间 pane 死了立即失败。
1027
+ const pollUntil = async (pred, timeoutMs) => {
1028
+ const t0 = Date.now();
1029
+ for (;;) {
1030
+ if (!(await paneAlive()))
1031
+ return false;
1032
+ if (pred(await capture()))
1033
+ return true;
1034
+ if (Date.now() - t0 >= timeoutMs)
1035
+ return false;
1036
+ await sleepMs(ASKQ_CONFIRM_POLL_MS);
1037
+ }
1038
+ };
1039
+ const sendKey = (key) => tmuxRun(["send-keys", "-t", target, key]);
1040
+ for (let attempt = 0; attempt < ASKQ_CONFIRM_ENTERS; attempt++) {
1041
+ if (!(await paneAlive()))
1042
+ return { ok: false, reason: "pane_dead" };
1043
+ const cur = await capture();
1044
+ // 面板已经不在了 = 已成功收工。
1045
+ if (!isModalPane(cur).modal)
1046
+ return { ok: true };
1047
+ // 还没出现 Submit 行则等它渲染 (文本刚落下 / TUI 重绘)。
1048
+ if (!isAskqSubmitPage(cur) && !(await pollUntil(isAskqSubmitPage, ASKQ_CONFIRM_STAGE_MS))) {
1049
+ // 连 Submit 行都没有 —— 可能文本没落下, 兜底按一次 Enter 试推进。
1050
+ const e = await sendKey("Enter");
1051
+ if (!e.ok)
1052
+ return { ok: false, reason: `send-keys Enter: ${e.stderr.slice(-200) || e.code}` };
1053
+ continue;
1054
+ }
1055
+ // Submit 行已出现, 但光标停在自定义文本行 (❯ N. <text>) —— 面板结构是
1056
+ // 「自定义行 + 独立 Submit 行」, 直接 Enter 只会在自定义行换行。先 Down 落到
1057
+ // Submit 行, 再 Enter 收工。
1058
+ const d = await sendKey("Down");
1059
+ if (!d.ok)
1060
+ return { ok: false, reason: `send-keys Down: ${d.stderr.slice(-200) || d.code}` };
1061
+ await sleepMs(ASKQ_CONFIRM_POLL_MS); // 等光标移动重绘
1062
+ const e = await sendKey("Enter");
1063
+ if (!e.ok)
1064
+ return { ok: false, reason: `send-keys Enter: ${e.stderr.slice(-200) || e.code}` };
1065
+ if (await pollUntil((p) => !isModalPane(p).modal, ASKQ_CONFIRM_STAGE_MS))
1066
+ return { ok: true };
1067
+ }
1068
+ return { ok: false, reason: "面板未在确认后关闭 (可能停在提交页/自定义行)" };
1069
+ };
1012
1070
  // Two fingerprints, derived from different ends of `text`:
1013
1071
  // headFp — first 8 non-ws chars; used for "did paste land" against a wide
1014
1072
  // window because long pastes wrap and the head sits near the top of the
@@ -1139,13 +1197,24 @@ const injectViaTmuxText = async (target, text, log, freshSpawn) => {
1139
1197
  };
1140
1198
  let pasteSeen = await sawHead(PASTE_VERIFY_MS);
1141
1199
  if (!pasteSeen) {
1142
- // Paste fired before the TUI was reading re-paste once after a back-off.
1143
- log.warn({ target, headFp }, "mirror inject: paste headFp not seen, re-pasting");
1144
- await sleepMs(RETRY_SETTLE_MS);
1145
- r = await loadAndPaste();
1146
- if (!r.ok)
1147
- return r;
1148
- pasteSeen = await sawHead(PASTE_VERIFY_MS);
1200
+ // headFp not seen but on a cold fresh-spawn the TUI can render the paste
1201
+ // just outside our capture window / after our budget, so `sawHead` yields a
1202
+ // FALSE negative even though the text is sitting in the input box. Blindly
1203
+ // re-pasting then stacks a SECOND copy → the classic doubled prompt. Guard:
1204
+ // re-paste only if the box genuinely lacks our tail; otherwise the first
1205
+ // paste landed and we just proceed to submit.
1206
+ if (await inputBoxStillHasTail()) {
1207
+ log.warn({ target, headFp }, "mirror inject: headFp not seen but tail present in box — first paste landed, skipping re-paste");
1208
+ pasteSeen = true;
1209
+ }
1210
+ else {
1211
+ log.warn({ target, headFp }, "mirror inject: paste headFp not seen, re-pasting");
1212
+ await sleepMs(RETRY_SETTLE_MS);
1213
+ r = await loadAndPaste();
1214
+ if (!r.ok)
1215
+ return r;
1216
+ pasteSeen = await sawHead(PASTE_VERIFY_MS);
1217
+ }
1149
1218
  }
1150
1219
  // Bracketed-paste end + TUI catch-up. Warm pane: 400ms is invisible.
1151
1220
  // Fresh respawn: 1500ms+ — claude --resume is still loading the transcript.
@@ -2419,6 +2488,12 @@ export const startMirror = (deps) => {
2419
2488
  if (!r.ok)
2420
2489
  return r;
2421
2490
  }
2491
+ else if (act.kind === "confirm_submit") {
2492
+ // 读屏确认收尾: 到提交页 → Enter → 面板关闭, 取代盲发 Enter。
2493
+ const r = await confirmAskqSubmit(a.tmuxPane, () => tmuxPaneAlive(a.tmuxPane));
2494
+ if (!r.ok)
2495
+ return r;
2496
+ }
2422
2497
  else {
2423
2498
  for (const key of act.keys) {
2424
2499
  const r = await tmuxRun(["send-keys", "-t", a.tmuxPane, key]);