wave-code 0.19.9 → 1.0.0
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/components/ChatInterface.js +1 -1
- package/dist/components/HelpView.js +6 -0
- package/dist/components/InputBox.d.ts +2 -0
- package/dist/components/InputBox.js +11 -2
- package/dist/components/RewindCommand.js +4 -2
- package/dist/hooks/useInputManager.d.ts +1 -0
- package/dist/hooks/useInputManager.js +23 -20
- package/dist/managers/inputHandlers.js +5 -8
- package/dist/managers/inputReducer.d.ts +10 -20
- package/dist/managers/inputReducer.js +309 -173
- package/dist/print-cli.js +36 -10
- package/dist/stdio/agentBridge.js +25 -2
- package/dist/utils/rewindCheckpoints.d.ts +8 -0
- package/dist/utils/rewindCheckpoints.js +15 -0
- package/dist/utils/worktree.d.ts +8 -0
- package/dist/utils/worktree.js +32 -1
- package/package.json +2 -2
- package/src/components/ChatInterface.tsx +2 -0
- package/src/components/HelpView.tsx +6 -0
- package/src/components/InputBox.tsx +19 -0
- package/src/components/RewindCommand.tsx +4 -2
- package/src/hooks/useInputManager.ts +27 -21
- package/src/managers/inputHandlers.ts +6 -10
- package/src/managers/inputReducer.ts +381 -208
- package/src/print-cli.ts +48 -11
- package/src/stdio/agentBridge.ts +25 -1
- package/src/utils/rewindCheckpoints.ts +15 -0
- package/src/utils/worktree.ts +50 -1
package/src/print-cli.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
hasUncommittedChanges,
|
|
5
5
|
hasNewCommits,
|
|
6
6
|
getDefaultRemoteBranch,
|
|
7
|
+
validateWorktreeRemovalPath,
|
|
7
8
|
type WorktreeSession,
|
|
8
9
|
} from "wave-agent-sdk";
|
|
9
10
|
import { displayUsageSummary } from "./utils/usageSummary.js";
|
|
@@ -183,18 +184,18 @@ export async function startPrintCli(options: PrintCliOptions): Promise<void> {
|
|
|
183
184
|
// Display timing information
|
|
184
185
|
displayTimingInfo(startTime, showStats);
|
|
185
186
|
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
// Handle worktree cleanup for print mode
|
|
187
|
+
// Trigger WorktreeRemove hook (before destroy — it needs a live agent) and
|
|
188
|
+
// decide whether the worktree is clean enough to remove
|
|
189
|
+
let cleanWorktree = false;
|
|
190
190
|
if (worktreeSession) {
|
|
191
191
|
const cwd = workdir || worktreeSession.path;
|
|
192
192
|
const baseBranch = getDefaultRemoteBranch(cwd);
|
|
193
193
|
const hasChanges = hasUncommittedChanges(cwd);
|
|
194
194
|
const hasCommits = hasNewCommits(cwd, baseBranch);
|
|
195
|
+
cleanWorktree = !hasChanges && !hasCommits;
|
|
195
196
|
|
|
196
|
-
if (
|
|
197
|
-
await
|
|
197
|
+
if (cleanWorktree) {
|
|
198
|
+
await agent.triggerWorktreeRemoveHook(worktreeSession.path);
|
|
198
199
|
} else {
|
|
199
200
|
process.stdout.write(
|
|
200
201
|
`\n⚠️ Worktree '${worktreeSession.name}' has changes or commits. Keeping it at: ${worktreeSession.path}\n`,
|
|
@@ -202,6 +203,25 @@ export async function startPrintCli(options: PrintCliOptions): Promise<void> {
|
|
|
202
203
|
}
|
|
203
204
|
}
|
|
204
205
|
|
|
206
|
+
// Destroy agent and exit after sendMessage completes
|
|
207
|
+
await agent.destroy();
|
|
208
|
+
|
|
209
|
+
// Handle worktree cleanup for print mode (git removal stays after destroy)
|
|
210
|
+
if (worktreeSession && cleanWorktree) {
|
|
211
|
+
try {
|
|
212
|
+
validateWorktreeRemovalPath(
|
|
213
|
+
worktreeSession.path,
|
|
214
|
+
worktreeSession.repoRoot,
|
|
215
|
+
);
|
|
216
|
+
await removeWorktree(worktreeSession);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
// Never block print-mode exit on worktree cleanup failures
|
|
219
|
+
process.stdout.write(
|
|
220
|
+
`\n⚠️ Skipping worktree removal: ${(error as Error).message}\n`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
205
225
|
process.exit(0);
|
|
206
226
|
} catch (error) {
|
|
207
227
|
console.error("Failed to send message:", error);
|
|
@@ -220,23 +240,40 @@ export async function startPrintCli(options: PrintCliOptions): Promise<void> {
|
|
|
220
240
|
// Display timing information even on error
|
|
221
241
|
displayTimingInfo(startTime, showStats);
|
|
222
242
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
// Handle worktree cleanup for print mode even on error
|
|
243
|
+
// Trigger WorktreeRemove hook (before destroy) when the worktree is clean
|
|
244
|
+
let cleanWorktree = false;
|
|
226
245
|
if (worktreeSession) {
|
|
227
246
|
const cwd = workdir || worktreeSession.path;
|
|
228
247
|
const baseBranch = getDefaultRemoteBranch(cwd);
|
|
229
248
|
const hasChanges = hasUncommittedChanges(cwd);
|
|
230
249
|
const hasCommits = hasNewCommits(cwd, baseBranch);
|
|
250
|
+
cleanWorktree = !hasChanges && !hasCommits;
|
|
231
251
|
|
|
232
|
-
if (
|
|
233
|
-
await
|
|
252
|
+
if (cleanWorktree) {
|
|
253
|
+
await agent.triggerWorktreeRemoveHook(worktreeSession.path);
|
|
234
254
|
} else {
|
|
235
255
|
process.stdout.write(
|
|
236
256
|
`\n⚠️ Worktree '${worktreeSession.name}' has changes or commits. Keeping it at: ${worktreeSession.path}\n`,
|
|
237
257
|
);
|
|
238
258
|
}
|
|
239
259
|
}
|
|
260
|
+
|
|
261
|
+
await agent.destroy();
|
|
262
|
+
|
|
263
|
+
// Handle worktree cleanup for print mode even on error
|
|
264
|
+
if (worktreeSession && cleanWorktree) {
|
|
265
|
+
try {
|
|
266
|
+
validateWorktreeRemovalPath(
|
|
267
|
+
worktreeSession.path,
|
|
268
|
+
worktreeSession.repoRoot,
|
|
269
|
+
);
|
|
270
|
+
await removeWorktree(worktreeSession);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
process.stdout.write(
|
|
273
|
+
`\n⚠️ Skipping worktree removal: ${(error as Error).message}\n`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
240
277
|
}
|
|
241
278
|
process.exit(1);
|
|
242
279
|
}
|
package/src/stdio/agentBridge.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
PromptHistoryManager,
|
|
41
41
|
AuthService,
|
|
42
42
|
PluginCore,
|
|
43
|
+
validateWorktreeRemovalPath,
|
|
43
44
|
type SlashCommand,
|
|
44
45
|
} from "wave-agent-sdk";
|
|
45
46
|
import {
|
|
@@ -50,6 +51,7 @@ import {
|
|
|
50
51
|
import { execFileSync } from "node:child_process";
|
|
51
52
|
import { createWorktree, removeWorktree } from "../utils/worktree.js";
|
|
52
53
|
import { logger } from "../utils/logger.js";
|
|
54
|
+
import { isUserCheckpointMessage } from "../utils/rewindCheckpoints.js";
|
|
53
55
|
|
|
54
56
|
export type NotificationEmitter = (
|
|
55
57
|
method: string,
|
|
@@ -526,6 +528,28 @@ export class AgentBridge {
|
|
|
526
528
|
branch: string;
|
|
527
529
|
repoRoot: string;
|
|
528
530
|
}): Promise<{ ok: true }> {
|
|
531
|
+
// Align with Claude Code v2.1.216+: refuse to remove a worktree whose path
|
|
532
|
+
// is a symlink or resolves outside the repo root. Already-removed (missing)
|
|
533
|
+
// paths pass validation so removal stays idempotent.
|
|
534
|
+
try {
|
|
535
|
+
validateWorktreeRemovalPath(params.path, params.repoRoot);
|
|
536
|
+
} catch (e) {
|
|
537
|
+
throw new RpcError(PROTOCOL_INTERNAL_ERROR, (e as Error).message);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Trigger the WorktreeRemove hook (before git removal, non-blocking) using
|
|
541
|
+
// the session that runs in this worktree, if it is still registered.
|
|
542
|
+
for (const entry of this.sessions.values()) {
|
|
543
|
+
if (entry.agent.workingDirectory === params.path) {
|
|
544
|
+
try {
|
|
545
|
+
await entry.agent.triggerWorktreeRemoveHook(params.path);
|
|
546
|
+
} catch (e) {
|
|
547
|
+
logger.warn("WorktreeRemove hooks execution failed:", e);
|
|
548
|
+
}
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
529
553
|
// removeWorktree is best-effort/idempotent: already-removed worktrees or
|
|
530
554
|
// branches only log, never throw.
|
|
531
555
|
await removeWorktree({
|
|
@@ -684,7 +708,7 @@ export class AgentBridge {
|
|
|
684
708
|
const entry = this.requireSession(sessionId);
|
|
685
709
|
const { messages } = await entry.agent.getFullMessageThread();
|
|
686
710
|
const checkpoints = messages
|
|
687
|
-
.filter((m) => m
|
|
711
|
+
.filter((m) => isUserCheckpointMessage(m) && m.id)
|
|
688
712
|
.map((m) => ({
|
|
689
713
|
id: m.id as string,
|
|
690
714
|
content: getMessageContent(m).replace(/\s+/g, " ").trim(),
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Message } from "wave-agent-sdk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 判断一条 user 消息能否作为 /rewind 检查点。
|
|
5
|
+
* 后台任务通知(task_notification)与 hook 注入的消息(source: "hook")
|
|
6
|
+
* 都是系统生成、用户不可见的,不能作为回滚点。
|
|
7
|
+
* CLI 交互式选择器与 stdio listRewindCheckpoints 共用此判定,避免两处漂移。
|
|
8
|
+
*/
|
|
9
|
+
export function isUserCheckpointMessage(m: Message): boolean {
|
|
10
|
+
if (m.role !== "user" || m.isMeta) return false;
|
|
11
|
+
if (m.blocks.some((b) => b.type === "task_notification")) return false;
|
|
12
|
+
if (m.blocks.some((b) => b.type === "text" && b.source === "hook"))
|
|
13
|
+
return false;
|
|
14
|
+
return true;
|
|
15
|
+
}
|
package/src/utils/worktree.ts
CHANGED
|
@@ -2,7 +2,11 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
getDefaultRemoteBranch,
|
|
7
|
+
getGitMainRepoRoot,
|
|
8
|
+
performPostCreationSetup,
|
|
9
|
+
} from "wave-agent-sdk";
|
|
6
10
|
|
|
7
11
|
// Never use execFileSync here: the shared `wave --stdio` process handles all
|
|
8
12
|
// desktop sessions, so a synchronous git call (especially a multi-second
|
|
@@ -19,6 +23,38 @@ export interface WorktreeSession {
|
|
|
19
23
|
isNew: boolean;
|
|
20
24
|
}
|
|
21
25
|
|
|
26
|
+
// --- Worktree name validation ------------------------------------------------
|
|
27
|
+
|
|
28
|
+
const VALID_WORKTREE_SLUG_SEGMENT = /^[a-zA-Z0-9._-]+$/;
|
|
29
|
+
const MAX_WORKTREE_SLUG_LENGTH = 64;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Validate a worktree name before any side effects. Names are slash-separated
|
|
33
|
+
* slugs: every segment must be non-empty and contain only letters, digits,
|
|
34
|
+
* dots, underscores, and dashes. The total length is capped at 64 characters
|
|
35
|
+
* and "." / ".." segments are rejected (path traversal protection).
|
|
36
|
+
* @throws {Error} When the name is not a valid slug
|
|
37
|
+
*/
|
|
38
|
+
export function validateWorktreeSlug(name: string): void {
|
|
39
|
+
if (name.length > MAX_WORKTREE_SLUG_LENGTH) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Invalid worktree name: "${name}" must be ${MAX_WORKTREE_SLUG_LENGTH} characters or fewer (got ${name.length})`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
for (const segment of name.split("/")) {
|
|
45
|
+
if (segment === "." || segment === "..") {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Invalid worktree name: "${name}" must not contain "." or ".." path segments`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
if (segment.length === 0 || !VALID_WORKTREE_SLUG_SEGMENT.test(segment)) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Invalid worktree name: "${name}" each "/"-separated segment must be non-empty and contain only letters, digits, dots, underscores, and dashes`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
22
58
|
/**
|
|
23
59
|
* Create a new git worktree
|
|
24
60
|
* @param name Worktree name
|
|
@@ -32,6 +68,19 @@ export async function createWorktree(
|
|
|
32
68
|
name: string,
|
|
33
69
|
cwd: string,
|
|
34
70
|
options?: { baseRef?: "fresh" | "head"; baseBranch?: string },
|
|
71
|
+
): Promise<WorktreeSession> {
|
|
72
|
+
validateWorktreeSlug(name);
|
|
73
|
+
const session = await createWorktreeInternal(name, cwd, options);
|
|
74
|
+
if (session.isNew) {
|
|
75
|
+
await performPostCreationSetup(session.path, session.repoRoot);
|
|
76
|
+
}
|
|
77
|
+
return session;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function createWorktreeInternal(
|
|
81
|
+
name: string,
|
|
82
|
+
cwd: string,
|
|
83
|
+
options?: { baseRef?: "fresh" | "head"; baseBranch?: string },
|
|
35
84
|
): Promise<WorktreeSession> {
|
|
36
85
|
const repoRoot = getGitMainRepoRoot(cwd);
|
|
37
86
|
const worktreePath = path.join(repoRoot, ".wave", "worktrees", name);
|