wave-code 1.6.2 → 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/dist/bundle/wave.mjs +621 -796
- package/package.json +3 -3
- package/src/daemon/commands.ts +9 -2
- package/src/remote/notificationCoalescer.ts +3 -1
- package/src/stdio/agentBridge.ts +72 -6
- package/src/utils/workspaceDiff.ts +2 -4
- package/src/utils/worktree.ts +244 -175
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-code",
|
|
3
|
-
"version": "1.6.
|
|
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.2"
|
|
60
|
+
"zod": "^3.23.8"
|
|
61
61
|
},
|
|
62
62
|
"engines": {
|
|
63
63
|
"node": ">=22"
|
package/src/daemon/commands.ts
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* must be destroyed via the envelope sessionId returned by `initialize`.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/src/stdio/agentBridge.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 {
|
|
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
|
|
98
|
+
const { stdout } = await execFileAsyncHidden("git", ["-C", cwd, ...args], {
|
|
101
99
|
encoding: "utf-8" as const,
|
|
102
100
|
maxBuffer: GIT_BUFFER,
|
|
103
101
|
});
|