wave-code 1.6.3 → 1.6.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-code",
3
- "version": "1.6.3",
3
+ "version": "1.6.4",
4
4
  "description": "CLI-based code assistant powered by AI, built with React and Ink",
5
5
  "repository": {
6
6
  "type": "git",
@@ -54,10 +54,10 @@
54
54
  "string-width": "^8.2.2",
55
55
  "tsx": "^4.20.4",
56
56
  "vitest": "^5.1.0",
57
+ "wave-agent-sdk": "1.6.4",
57
58
  "wrap-ansi": "^10.0.0",
58
59
  "yargs": "^17.7.2",
59
- "zod": "^3.23.8",
60
- "wave-agent-sdk": "1.6.3"
60
+ "zod": "^3.23.8"
61
61
  },
62
62
  "engines": {
63
63
  "node": ">=22"
@@ -28,7 +28,7 @@
28
28
  * must be destroyed via the envelope sessionId returned by `initialize`.
29
29
  */
30
30
 
31
- import { execFile, spawn, type ChildProcess } from "node:child_process";
31
+ import { spawn, type ChildProcess } from "node:child_process";
32
32
  import * as fs from "node:fs";
33
33
  import net from "node:net";
34
34
  import path from "node:path";
@@ -36,6 +36,7 @@ import {
36
36
  ASK_USER_QUESTION_TOOL_NAME,
37
37
  ENTER_PLAN_MODE_TOOL_NAME,
38
38
  EXIT_PLAN_MODE_TOOL_NAME,
39
+ execFileHidden,
39
40
  getGitMainRepoRoot,
40
41
  getMessageContent,
41
42
  hasWorktreeCreateHook,
@@ -126,6 +127,12 @@ function startDaemon(socketPath: string): LaunchedDaemon {
126
127
  const log = openDaemonLogForChild();
127
128
  const child = spawn(process.execPath, [entry, "--daemon", socketPath], {
128
129
  detached: true,
130
+ // No-op here, kept for intent only: `windowsHide` reaches the child as
131
+ // CREATE_NO_WINDOW, which Windows ignores when DETACHED_PROCESS is also
132
+ // set — and `detached` is exactly that. The daemon's console window (if it
133
+ // ever shows one) cannot be suppressed from here; that needs ConPTY,
134
+ // `Start-Process -WindowStyle Hidden`, or a native CreateProcess call.
135
+ windowsHide: true,
129
136
  stdio: ["ignore", "ignore", log ? log.fd : "ignore"],
130
137
  });
131
138
  if (log) {
@@ -1425,7 +1432,7 @@ export interface DestroyOptions {
1425
1432
  /** Run one git command and return trimmed stdout (rejects on git failure). */
1426
1433
  function runGit(cwd: string, args: string[]): Promise<string> {
1427
1434
  return new Promise((resolve, reject) => {
1428
- execFile("git", args, { cwd, windowsHide: true }, (err, stdout) => {
1435
+ execFileHidden("git", args, { cwd }, (err, stdout) => {
1429
1436
  if (err) reject(err);
1430
1437
  else resolve(stdout.trim());
1431
1438
  });
@@ -93,7 +93,9 @@ function mergeSpec(method: string, params: unknown): MergeSpec | undefined {
93
93
  }
94
94
  return {
95
95
  field: "content",
96
- key: `b\u0000${p.question}\u0000${typeof p.type === "string" ? p.type : ""}`,
96
+ // 归属键里有 requestId:多轮追问之后同一个问题可以同时在途两次(从主输入框
97
+ // 连发两次 `/btw 同样的话`),只按 question 合并会把两轮的增量揉成一团。
98
+ key: `b\u0000${typeof p.requestId === "string" ? p.requestId : ""}\u0000${p.question}\u0000${typeof p.type === "string" ? p.type : ""}`,
97
99
  };
98
100
  }
99
101
 
@@ -21,6 +21,7 @@ import {
21
21
  type AgentOptions,
22
22
  type BackgroundTask,
23
23
  type BackgroundTaskSummary,
24
+ type BtwHistoryTurn,
24
25
  type SerializableWorkflowRun,
25
26
  type Message,
26
27
  type PermissionDecision,
@@ -441,7 +442,12 @@ export class AgentBridge {
441
442
  case "bang":
442
443
  return this.bang(p.command as string, sessionId);
443
444
  case "askBtw":
444
- return this.askBtw(p.question as string, sessionId);
445
+ return this.askBtw(
446
+ p.question as string,
447
+ sessionId,
448
+ p.history as BtwHistoryTurn[] | undefined,
449
+ p.requestId as string | undefined,
450
+ );
445
451
  case "abortMessage":
446
452
  return this.abortMessage(sessionId);
447
453
  case "clearMessages":
@@ -839,6 +845,19 @@ export class AgentBridge {
839
845
  if (entry) {
840
846
  this.pendingPermissions.delete(p.requestId);
841
847
  entry.resolve(p.decision);
848
+ // 这条请求从此不再是待审批 —— 告诉**所有**客户端,不只是答话的这个。
849
+ // 审批卡是每个客户端各挂一份的(桌面端在 `pendingConfirmations` 里、
850
+ // 手机那页在 `shownApprovals` 里),而这台机器上的请求只有一份:从手机
851
+ // 上答掉的那几条,桌面窗口只能靠这条通知撤卡,否则用户从手机回来看到
852
+ // 一叠已经答过的卡(点它只会发一条 requestId 已消失的 `permissionResponse`,
853
+ // 上面那个 `if (entry)` 静默忽略)。放在 `resolve` 之后:答复本身先落地,
854
+ // 撤卡失败/迟到都不影响回合继续(spec ui/confirm-ui.md「同一条审批只有
855
+ // 一份,谁答都算数」)。
856
+ this.emit(
857
+ "permissionResolved",
858
+ { requestId: p.requestId },
859
+ entry.sessionId,
860
+ );
842
861
  // 不带反馈的拒绝 = 用户关掉了确认卡(关闭键 / Esc),本轮在用户这边就
843
862
  // 结束:与桌面端 / VS Code / JetBrains 的 `denied → abortMessage()` 是
844
863
  // 同一条语义,也与 Claude Code 的 `cancelAndAbort`(无 feedback 直接
@@ -869,6 +888,12 @@ export class AgentBridge {
869
888
  currentWorkdir: string;
870
889
  sessionCwd: string;
871
890
  repoRoot?: string;
891
+ worktree?: {
892
+ path: string;
893
+ branch?: string;
894
+ repoRoot: string;
895
+ hookBased: boolean;
896
+ };
872
897
  permissionMode: PermissionMode;
873
898
  latestTotalTokens: number;
874
899
  }> {
@@ -878,7 +903,12 @@ export class AgentBridge {
878
903
  if (params.restoreSessionId) {
879
904
  const live = this.sessions.get(params.restoreSessionId);
880
905
  if (live) {
881
- return this.initializeReply(live.agent, live.createdWorkdir);
906
+ return this.initializeReply(
907
+ live.agent,
908
+ live.createdWorkdir,
909
+ deriveWorktreeInfo(live.createdWorkdir),
910
+ live.createdWorktreeBranch,
911
+ );
882
912
  }
883
913
  }
884
914
 
@@ -958,7 +988,12 @@ export class AgentBridge {
958
988
  createdWorktreeBranch,
959
989
  });
960
990
 
961
- return this.initializeReply(agent, createdWorkdir, worktree);
991
+ return this.initializeReply(
992
+ agent,
993
+ createdWorkdir,
994
+ worktree,
995
+ createdWorktreeBranch,
996
+ );
962
997
  }
963
998
 
964
999
  /**
@@ -1054,12 +1089,20 @@ export class AgentBridge {
1054
1089
  // Derived once by the caller: this reply and the creation-branch snapshot
1055
1090
  // both need it, and it reads config files.
1056
1091
  worktree = deriveWorktreeInfo(sessionCwd),
1092
+ /** Branch the worktree was created on — what removal may delete. */
1093
+ createdBranch?: string,
1057
1094
  ): {
1058
1095
  sessionId: string;
1059
1096
  currentWorkdir: string;
1060
1097
  sessionCwd: string;
1061
1098
  workspaceRoot: string;
1062
1099
  repoRoot?: string;
1100
+ worktree?: {
1101
+ path: string;
1102
+ branch?: string;
1103
+ repoRoot: string;
1104
+ hookBased: boolean;
1105
+ };
1063
1106
  permissionMode: PermissionMode;
1064
1107
  latestTotalTokens: number;
1065
1108
  } {
@@ -1073,6 +1116,19 @@ export class AgentBridge {
1073
1116
  // tree, `@` search).
1074
1117
  workspaceRoot: agent.workspaceRoot,
1075
1118
  ...(worktree ? { repoRoot: worktree.repoRoot } : {}),
1119
+ // The full record, not just the root: a host with no creation record of
1120
+ // its own (a `/resume` pick off disk) has nothing else to remove the
1121
+ // checkout and its branch with when the user deletes that session.
1122
+ ...(worktree
1123
+ ? {
1124
+ worktree: {
1125
+ path: worktree.path,
1126
+ ...(createdBranch ? { branch: createdBranch } : {}),
1127
+ repoRoot: worktree.repoRoot,
1128
+ hookBased: worktree.hookBased,
1129
+ },
1130
+ }
1131
+ : {}),
1076
1132
  permissionMode: agent.getPermissionMode(),
1077
1133
  latestTotalTokens: agent.latestTotalTokens,
1078
1134
  };
@@ -2102,7 +2158,12 @@ export class AgentBridge {
2102
2158
  return null;
2103
2159
  }
2104
2160
 
2105
- private async askBtw(question: string, sessionId?: string): Promise<string> {
2161
+ private async askBtw(
2162
+ question: string,
2163
+ sessionId?: string,
2164
+ history?: BtwHistoryTurn[],
2165
+ requestId?: string,
2166
+ ): Promise<string> {
2106
2167
  const entry = this.requireSession(sessionId);
2107
2168
  return entry.agent.askBtw(
2108
2169
  question,
@@ -2110,11 +2171,15 @@ export class AgentBridge {
2110
2171
  // Stream partial content to the client so webview hosts can render the
2111
2172
  // answer incrementally (thinking and content travel on separate
2112
2173
  // channels so the panel can drop thinking text once content starts).
2174
+ // `requestId` is echoed back so the host can attribute the chunk to the
2175
+ // side-chat turn that asked (spec commands/btw-command.md「Webview 端继续
2176
+ // 提问」: repeated questions within one session are otherwise
2177
+ // indistinguishable).
2113
2178
  (content) => {
2114
2179
  if (sessionId) {
2115
2180
  this.emit(
2116
2181
  "btwContent",
2117
- { question, content, type: "content" },
2182
+ { question, requestId, content, type: "content" },
2118
2183
  sessionId,
2119
2184
  );
2120
2185
  }
@@ -2123,11 +2188,12 @@ export class AgentBridge {
2123
2188
  if (sessionId) {
2124
2189
  this.emit(
2125
2190
  "btwContent",
2126
- { question, content, type: "thinking" },
2191
+ { question, requestId, content, type: "thinking" },
2127
2192
  sessionId,
2128
2193
  );
2129
2194
  }
2130
2195
  },
2196
+ history,
2131
2197
  );
2132
2198
  }
2133
2199
 
@@ -9,14 +9,12 @@
9
9
  * connection handshakes for a single panel refresh.
10
10
  */
11
11
 
12
- import { execFile } from "node:child_process";
13
12
  import * as fs from "node:fs";
14
13
  import * as path from "node:path";
15
- import { promisify } from "node:util";
14
+ import { execFileAsyncHidden } from "wave-agent-sdk";
16
15
 
17
16
  // Never execFileSync: the shared `wave --stdio` process handles every desktop
18
17
  // session, so a synchronous git call would freeze them all (see utils/worktree).
19
- const execFileAsync = promisify(execFile);
20
18
 
21
19
  export type WorkspaceFileStatus =
22
20
  | "added"
@@ -97,7 +95,7 @@ const GIT_BUFFER = 16 * 1024 * 1024;
97
95
 
98
96
  /** Run git in `cwd` and return stdout. */
99
97
  async function git(cwd: string, args: string[]): Promise<string> {
100
- const { stdout } = await execFileAsync("git", ["-C", cwd, ...args], {
98
+ const { stdout } = await execFileAsyncHidden("git", ["-C", cwd, ...args], {
101
99
  encoding: "utf-8" as const,
102
100
  maxBuffer: GIT_BUFFER,
103
101
  });