wezard 1.1.0 → 1.1.4

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,6 +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 { hasMirrorAskq, runMirrorAskqFlow, hasMirrorPlan, mootMirrorPlan, runMirrorPlanFlow } from "./approval.js";
20
21
  import { spawnTmuxClaude } from "./spawn-tmux.js";
21
22
  import { recordTool, recordToolResult, recordTurnStart, recordTurnItem, recordTurnUsage, recordTurnClose, recordCloseOpenTurns, buildDetailUrl, buildChatUrl } from "./detail.js";
22
23
  import { labelFor, tagOfKey, baseOfKey, withTagHeader } from "../shared/session-label.js";
@@ -1920,6 +1921,10 @@ export const startMirror = (deps) => {
1920
1921
  a.softEnd = setTimeout(() => fireSoftTurnEnd(a), SOFT_TURN_END_MS);
1921
1922
  return;
1922
1923
  }
1924
+ // codebuddy: ExitPlanMode 若被本地先答, result 落盘即作废挂着的计划审批卡
1925
+ // (卡 pending 期间 model 阻塞在本地对话框, 不可能有其它 tool_result)。
1926
+ if (item.kind === "tool_result")
1927
+ mootMirrorPlan(a.sessionId);
1923
1928
  // Record tool_use signatures unconditionally (before any state branching),
1924
1929
  // so flushBeforeCard's poll-drain can detect that the to-be-approved tool
1925
1930
  // is now persisted in the jsonl regardless of DEFERRED/STREAMING/IDLE.
@@ -1934,6 +1939,82 @@ export const startMirror = (deps) => {
1934
1939
  break;
1935
1940
  a.recentToolSigs.delete(oldest);
1936
1941
  }
1942
+ // codebuddy 的 ExitPlanMode 完全不过 PreToolUse hook (实测: 由
1943
+ // interruption-service 本地对话框裁决, HookExecutor 零调用)。mirror 从
1944
+ // jsonl 看到 function_call → 发计划审批卡; 点选后 send-keys 裁决本地
1945
+ // 对话框 (Enter=同意, 默认高亮 Yes / Escape=选项 2 标注的快捷键)。
1946
+ // claude 系 hook 即时触发, 不走此路 (否则双重发卡)。
1947
+ if (c.name === "ExitPlanMode"
1948
+ && backendForPath(a.jsonlPath).name === "codebuddy"
1949
+ && !hasMirrorPlan(a.sessionId)) {
1950
+ void runMirrorPlanFlow({
1951
+ log: log.child({ sessionId: a.sessionId }),
1952
+ client,
1953
+ sessionId: a.sessionId,
1954
+ chatKey: a.target,
1955
+ cwd: a.runningCwd,
1956
+ jsonlPath: a.jsonlPath,
1957
+ voteTimeoutMs: cfg.approval.longPollSec * 1000,
1958
+ sendKey: async (key) => {
1959
+ if (!a.tmuxPane)
1960
+ return { ok: false, reason: "no_live_pane" };
1961
+ if (!(await tmuxPaneAlive(a.tmuxPane)))
1962
+ return { ok: false, reason: "pane_dead" };
1963
+ const r = await tmuxRun(["send-keys", "-t", a.tmuxPane, key]);
1964
+ return r.code === 0 ? { ok: true } : { ok: false, reason: `send-keys ${key} failed: ${r.stdout.slice(-200) || r.code}` };
1965
+ },
1966
+ });
1967
+ }
1968
+ // codebuddy 对 AskUserQuestion 不在提问时触发 PreToolUse hook — 先弹本地
1969
+ // 面板, hook 只在面板被提交后才到达 (实测可延迟数小时)。这里从 jsonl 提前
1970
+ // 看到 function_call, 直接驱动 vote 卡让远端可答; 点选后注入一段触发文本
1971
+ // 提交本地面板, hook 到达时以 deny+reason 覆盖答案, 与 claude 路径同产物。
1972
+ // claude 系 hook 即时触发, 不走此路 (否则双重发卡)。
1973
+ if (c.name === "AskUserQuestion"
1974
+ && backendForPath(a.jsonlPath).name === "codebuddy"
1975
+ && !hasMirrorAskq(a.sessionId)) {
1976
+ void runMirrorAskqFlow({
1977
+ log: log.child({ sessionId: a.sessionId }),
1978
+ client,
1979
+ sessionId: a.sessionId,
1980
+ chatKey: a.target,
1981
+ toolInput: c.input,
1982
+ voteTimeoutMs: cfg.wrc.mirror.askqVoteTimeoutSec * 1000,
1983
+ drive: async (acts) => {
1984
+ // 面板只活在 TTY 里 — 无 pane 无法驱动 (spawn 注入会变成新 prompt)。
1985
+ if (!a.tmuxPane)
1986
+ return { ok: false, reason: "no_live_pane" };
1987
+ if (!(await tmuxPaneAlive(a.tmuxPane)))
1988
+ return { ok: false, reason: "pane_dead" };
1989
+ for (const act of acts) {
1990
+ if (act.kind === "text") {
1991
+ // 贴文本到自定义输入行: 复用 inject 的 bracketed-paste + Enter。
1992
+ const r = await inject({
1993
+ text: act.text,
1994
+ cfg,
1995
+ log: log.child({ principal: a.target, sessionId: a.sessionId }),
1996
+ sessionId: a.sessionId,
1997
+ jsonlPath: a.jsonlPath,
1998
+ tmuxTarget: a.tmuxPane,
1999
+ freshSpawn: false,
2000
+ });
2001
+ if (!r.ok)
2002
+ return r;
2003
+ }
2004
+ else {
2005
+ for (const key of act.keys) {
2006
+ const r = await tmuxRun(["send-keys", "-t", a.tmuxPane, key]);
2007
+ if (r.code !== 0)
2008
+ return { ok: false, reason: `send-keys ${key}: ${r.stdout.slice(-200) || r.code}` };
2009
+ await sleepMs(120); // 键间距 — 等 TUI 重渲染, 防吞键
2010
+ }
2011
+ }
2012
+ await sleepMs(300); // 题间/阶段间隔 — 等面板翻页
2013
+ }
2014
+ return { ok: true };
2015
+ },
2016
+ });
2017
+ }
1937
2018
  }
1938
2019
  }
1939
2020
  // /goal mode overrides everything below: the session-scoped Stop hook
@@ -2714,7 +2795,7 @@ export const startMirror = (deps) => {
2714
2795
  if (pending && pending !== running) {
2715
2796
  lines.push(`下次切换: \`${pending}\` (使用 /new 或 /clear 生效)`);
2716
2797
  }
2717
- lines.push("> 切换其他项目: 在对话中告诉 AI 调用 `cd` MCP 工具");
2798
+ lines.push("> 切换其他项目: 在对话中告诉 AI 调用 `enter` MCP 工具");
2718
2799
  // Session-boundary footer — `/new` and `/clear` are the only two callers,
2719
2800
  // so the tip lands exactly once per fresh context, never mid-conversation.
2720
2801
  lines.push(randomTip());