wave-code 0.7.1 → 0.8.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/cli.d.ts +2 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +24 -52
- package/dist/components/App.d.ts +3 -4
- package/dist/components/App.d.ts.map +1 -1
- package/dist/components/App.js +49 -6
- package/dist/components/BackgroundTaskManager.d.ts.map +1 -1
- package/dist/components/BackgroundTaskManager.js +12 -20
- package/dist/components/BangDisplay.d.ts +9 -0
- package/dist/components/BangDisplay.d.ts.map +1 -0
- package/dist/components/{CommandOutputDisplay.js → BangDisplay.js} +1 -1
- package/dist/components/ChatInterface.d.ts.map +1 -1
- package/dist/components/ChatInterface.js +3 -2
- package/dist/components/CommandSelector.d.ts.map +1 -1
- package/dist/components/CommandSelector.js +18 -2
- package/dist/components/ConfirmationSelector.d.ts.map +1 -1
- package/dist/components/ConfirmationSelector.js +105 -8
- package/dist/components/HelpView.d.ts.map +1 -1
- package/dist/components/HelpView.js +2 -0
- package/dist/components/HistorySearch.d.ts.map +1 -1
- package/dist/components/HistorySearch.js +19 -25
- package/dist/components/InputBox.d.ts.map +1 -1
- package/dist/components/InputBox.js +9 -3
- package/dist/components/MarketplaceAddForm.d.ts.map +1 -1
- package/dist/components/MarketplaceAddForm.js +13 -6
- package/dist/components/MarketplaceDetail.d.ts.map +1 -1
- package/dist/components/MarketplaceDetail.js +8 -3
- package/dist/components/MessageBlockItem.js +2 -2
- package/dist/components/MessageList.d.ts +4 -1
- package/dist/components/MessageList.d.ts.map +1 -1
- package/dist/components/MessageList.js +15 -8
- package/dist/components/PluginDetail.d.ts.map +1 -1
- package/dist/components/PluginDetail.js +14 -3
- package/dist/components/PluginManagerShell.d.ts.map +1 -1
- package/dist/components/PluginManagerShell.js +3 -3
- package/dist/components/PluginManagerTypes.d.ts +2 -0
- package/dist/components/PluginManagerTypes.d.ts.map +1 -1
- package/dist/components/SessionSelector.d.ts.map +1 -1
- package/dist/components/SessionSelector.js +10 -13
- package/dist/components/StatusCommand.d.ts +6 -0
- package/dist/components/StatusCommand.d.ts.map +1 -0
- package/dist/components/StatusCommand.js +28 -0
- package/dist/components/WorktreeExitPrompt.d.ts +13 -0
- package/dist/components/WorktreeExitPrompt.d.ts.map +1 -0
- package/dist/components/WorktreeExitPrompt.js +26 -0
- package/dist/contexts/useChat.d.ts +9 -5
- package/dist/contexts/useChat.d.ts.map +1 -1
- package/dist/contexts/useChat.js +38 -8
- package/dist/contracts/status.d.ts +8 -0
- package/dist/contracts/status.d.ts.map +1 -0
- package/dist/contracts/status.js +1 -0
- package/dist/hooks/useInputManager.d.ts +2 -0
- package/dist/hooks/useInputManager.d.ts.map +1 -1
- package/dist/hooks/useInputManager.js +12 -0
- package/dist/hooks/usePluginManager.d.ts.map +1 -1
- package/dist/hooks/usePluginManager.js +41 -13
- package/dist/hooks/useTasks.js +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +53 -4
- package/dist/managers/InputManager.d.ts +6 -0
- package/dist/managers/InputManager.d.ts.map +1 -1
- package/dist/managers/InputManager.js +32 -13
- package/dist/print-cli.d.ts +2 -4
- package/dist/print-cli.d.ts.map +1 -1
- package/dist/print-cli.js +31 -2
- package/dist/session-selector-cli.d.ts +3 -1
- package/dist/session-selector-cli.d.ts.map +1 -1
- package/dist/session-selector-cli.js +2 -2
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils/worktree.d.ts +23 -0
- package/dist/utils/worktree.d.ts.map +1 -0
- package/dist/utils/worktree.js +135 -0
- package/package.json +2 -2
- package/src/cli.tsx +36 -59
- package/src/components/App.tsx +99 -11
- package/src/components/BackgroundTaskManager.tsx +12 -20
- package/src/components/{CommandOutputDisplay.tsx → BangDisplay.tsx} +4 -4
- package/src/components/ChatInterface.tsx +8 -0
- package/src/components/CommandSelector.tsx +18 -1
- package/src/components/ConfirmationSelector.tsx +118 -9
- package/src/components/HelpView.tsx +2 -0
- package/src/components/HistorySearch.tsx +25 -30
- package/src/components/InputBox.tsx +11 -1
- package/src/components/MarketplaceAddForm.tsx +21 -8
- package/src/components/MarketplaceDetail.tsx +19 -4
- package/src/components/MessageBlockItem.tsx +3 -3
- package/src/components/MessageList.tsx +47 -23
- package/src/components/PluginDetail.tsx +30 -6
- package/src/components/PluginManagerShell.tsx +24 -6
- package/src/components/PluginManagerTypes.ts +2 -0
- package/src/components/SessionSelector.tsx +38 -24
- package/src/components/StatusCommand.tsx +94 -0
- package/src/components/WorktreeExitPrompt.tsx +86 -0
- package/src/contexts/useChat.tsx +57 -13
- package/src/contracts/status.ts +7 -0
- package/src/hooks/useInputManager.ts +12 -0
- package/src/hooks/usePluginManager.ts +47 -13
- package/src/hooks/useTasks.ts +2 -2
- package/src/index.ts +71 -12
- package/src/managers/InputManager.ts +37 -15
- package/src/print-cli.ts +48 -5
- package/src/session-selector-cli.tsx +6 -2
- package/src/types.ts +11 -0
- package/src/utils/worktree.ts +164 -0
- package/dist/components/CommandOutputDisplay.d.ts +0 -9
- package/dist/components/CommandOutputDisplay.d.ts.map +0 -1
|
@@ -34,6 +34,7 @@ export class InputManager {
|
|
|
34
34
|
this.showMcpManager = false;
|
|
35
35
|
this.showRewindManager = false;
|
|
36
36
|
this.showHelp = false;
|
|
37
|
+
this.showStatusCommand = false;
|
|
37
38
|
// Permission mode state
|
|
38
39
|
this.permissionMode = "default";
|
|
39
40
|
// Flag to prevent handleInput conflicts when selector selection occurs
|
|
@@ -208,7 +209,11 @@ export class InputManager {
|
|
|
208
209
|
}
|
|
209
210
|
// If not an agent command or execution failed, check local commands
|
|
210
211
|
if (!commandExecuted) {
|
|
211
|
-
if (command === "
|
|
212
|
+
if (command === "clear") {
|
|
213
|
+
this.callbacks.onClearMessages?.();
|
|
214
|
+
commandExecuted = true;
|
|
215
|
+
}
|
|
216
|
+
else if (command === "tasks") {
|
|
212
217
|
this.setShowBackgroundTaskManager(true);
|
|
213
218
|
commandExecuted = true;
|
|
214
219
|
}
|
|
@@ -224,6 +229,10 @@ export class InputManager {
|
|
|
224
229
|
this.setShowHelp(true);
|
|
225
230
|
commandExecuted = true;
|
|
226
231
|
}
|
|
232
|
+
else if (command === "status") {
|
|
233
|
+
this.setShowStatusCommand(true);
|
|
234
|
+
commandExecuted = true;
|
|
235
|
+
}
|
|
227
236
|
}
|
|
228
237
|
})();
|
|
229
238
|
this.handleCancelCommandSelect();
|
|
@@ -473,10 +482,27 @@ export class InputManager {
|
|
|
473
482
|
this.showHelp = show;
|
|
474
483
|
this.callbacks.onHelpStateChange?.(show);
|
|
475
484
|
}
|
|
485
|
+
getShowStatusCommand() {
|
|
486
|
+
return this.showStatusCommand;
|
|
487
|
+
}
|
|
488
|
+
setShowStatusCommand(show) {
|
|
489
|
+
this.showStatusCommand = show;
|
|
490
|
+
this.callbacks.onStatusCommandStateChange?.(show);
|
|
491
|
+
}
|
|
476
492
|
// Permission mode methods
|
|
477
493
|
getPermissionMode() {
|
|
478
494
|
return this.permissionMode;
|
|
479
495
|
}
|
|
496
|
+
isAnySelectorOrManagerActive() {
|
|
497
|
+
return (this.showFileSelector ||
|
|
498
|
+
this.showCommandSelector ||
|
|
499
|
+
this.showHistorySearch ||
|
|
500
|
+
this.showBackgroundTaskManager ||
|
|
501
|
+
this.showMcpManager ||
|
|
502
|
+
this.showRewindManager ||
|
|
503
|
+
this.showHelp ||
|
|
504
|
+
this.showStatusCommand);
|
|
505
|
+
}
|
|
480
506
|
setPermissionMode(mode) {
|
|
481
507
|
this.permissionMode = mode;
|
|
482
508
|
}
|
|
@@ -664,9 +690,7 @@ export class InputManager {
|
|
|
664
690
|
// Handle interrupt request - use Esc key to interrupt AI request or command
|
|
665
691
|
if (key.escape &&
|
|
666
692
|
(isLoading || isCommandRunning) &&
|
|
667
|
-
!this.
|
|
668
|
-
!this.showMcpManager &&
|
|
669
|
-
!this.showRewindManager) {
|
|
693
|
+
!this.isAnySelectorOrManagerActive()) {
|
|
670
694
|
// Unified interrupt for AI message generation and command execution
|
|
671
695
|
this.callbacks.onAbortMessage?.();
|
|
672
696
|
return true;
|
|
@@ -678,18 +702,13 @@ export class InputManager {
|
|
|
678
702
|
return true;
|
|
679
703
|
}
|
|
680
704
|
// Check if any selector is active
|
|
681
|
-
if (this.
|
|
682
|
-
this.showCommandSelector ||
|
|
683
|
-
this.showHistorySearch ||
|
|
684
|
-
this.showBackgroundTaskManager ||
|
|
685
|
-
this.showMcpManager ||
|
|
686
|
-
this.showRewindManager ||
|
|
687
|
-
this.showHelp) {
|
|
705
|
+
if (this.isAnySelectorOrManagerActive()) {
|
|
688
706
|
if (this.showBackgroundTaskManager ||
|
|
689
707
|
this.showMcpManager ||
|
|
690
708
|
this.showRewindManager ||
|
|
691
|
-
this.showHelp
|
|
692
|
-
|
|
709
|
+
this.showHelp ||
|
|
710
|
+
this.showStatusCommand) {
|
|
711
|
+
// Task manager, MCP manager, Rewind, Help and Status don't need to handle input, handled by component itself
|
|
693
712
|
// Return true to indicate we've "handled" it (by ignoring it) so it doesn't leak to normal input
|
|
694
713
|
return true;
|
|
695
714
|
}
|
package/dist/print-cli.d.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
import { BaseAppProps } from "./types.js";
|
|
2
|
+
export interface PrintCliOptions extends BaseAppProps {
|
|
2
3
|
restoreSessionId?: string;
|
|
3
4
|
continueLastSession?: boolean;
|
|
4
5
|
message?: string;
|
|
5
6
|
showStats?: boolean;
|
|
6
|
-
bypassPermissions?: boolean;
|
|
7
|
-
pluginDirs?: string[];
|
|
8
|
-
tools?: string[];
|
|
9
7
|
}
|
|
10
8
|
export declare function startPrintCli(options: PrintCliOptions): Promise<void>;
|
|
11
9
|
//# sourceMappingURL=print-cli.d.ts.map
|
package/dist/print-cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"print-cli.d.ts","sourceRoot":"","sources":["../src/print-cli.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"print-cli.d.ts","sourceRoot":"","sources":["../src/print-cli.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C,MAAM,WAAW,eAAgB,SAAQ,YAAY;IACnD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAgBD,wBAAsB,aAAa,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAsM3E"}
|
package/dist/print-cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Agent } from "wave-agent-sdk";
|
|
1
|
+
import { Agent, hasUncommittedChanges, hasNewCommits, getDefaultRemoteBranch, } from "wave-agent-sdk";
|
|
2
2
|
import { displayUsageSummary } from "./utils/usageSummary.js";
|
|
3
|
+
import { removeWorktree } from "./utils/worktree.js";
|
|
3
4
|
function displayTimingInfo(startTime, showStats) {
|
|
4
5
|
// Skip timing info in test environment or if stats are disabled
|
|
5
6
|
if (process.env.NODE_ENV === "test" || process.env.VITEST || !showStats) {
|
|
@@ -13,7 +14,7 @@ function displayTimingInfo(startTime, showStats) {
|
|
|
13
14
|
}
|
|
14
15
|
export async function startPrintCli(options) {
|
|
15
16
|
const startTime = new Date();
|
|
16
|
-
const { restoreSessionId, continueLastSession, message, showStats = false, bypassPermissions, pluginDirs, tools, } = options;
|
|
17
|
+
const { restoreSessionId, continueLastSession, message, showStats = false, bypassPermissions, pluginDirs, tools, worktreeSession, workdir, model, } = options;
|
|
17
18
|
if ((!message || message.trim() === "") &&
|
|
18
19
|
!continueLastSession &&
|
|
19
20
|
!restoreSessionId) {
|
|
@@ -98,6 +99,8 @@ export async function startPrintCli(options) {
|
|
|
98
99
|
permissionMode: bypassPermissions ? "bypassPermissions" : undefined,
|
|
99
100
|
plugins: pluginDirs?.map((path) => ({ type: "local", path })),
|
|
100
101
|
tools,
|
|
102
|
+
workdir,
|
|
103
|
+
model,
|
|
101
104
|
// 保持流式模式以获得更好的命令行用户体验
|
|
102
105
|
});
|
|
103
106
|
// Send message if provided and not empty
|
|
@@ -119,6 +122,19 @@ export async function startPrintCli(options) {
|
|
|
119
122
|
displayTimingInfo(startTime, showStats);
|
|
120
123
|
// Destroy agent and exit after sendMessage completes
|
|
121
124
|
await agent.destroy();
|
|
125
|
+
// Handle worktree cleanup for print mode
|
|
126
|
+
if (worktreeSession) {
|
|
127
|
+
const cwd = workdir || worktreeSession.path;
|
|
128
|
+
const baseBranch = getDefaultRemoteBranch(cwd);
|
|
129
|
+
const hasChanges = hasUncommittedChanges(cwd);
|
|
130
|
+
const hasCommits = hasNewCommits(cwd, baseBranch);
|
|
131
|
+
if (!hasChanges && !hasCommits) {
|
|
132
|
+
removeWorktree(worktreeSession);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
process.stdout.write(`\n⚠️ Worktree '${worktreeSession.name}' has changes or commits. Keeping it at: ${worktreeSession.path}\n`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
122
138
|
process.exit(0);
|
|
123
139
|
}
|
|
124
140
|
catch (error) {
|
|
@@ -138,6 +154,19 @@ export async function startPrintCli(options) {
|
|
|
138
154
|
// Display timing information even on error
|
|
139
155
|
displayTimingInfo(startTime, showStats);
|
|
140
156
|
await agent.destroy();
|
|
157
|
+
// Handle worktree cleanup for print mode even on error
|
|
158
|
+
if (worktreeSession) {
|
|
159
|
+
const cwd = workdir || worktreeSession.path;
|
|
160
|
+
const baseBranch = getDefaultRemoteBranch(cwd);
|
|
161
|
+
const hasChanges = hasUncommittedChanges(cwd);
|
|
162
|
+
const hasCommits = hasNewCommits(cwd, baseBranch);
|
|
163
|
+
if (!hasChanges && !hasCommits) {
|
|
164
|
+
removeWorktree(worktreeSession);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
process.stdout.write(`\n⚠️ Worktree '${worktreeSession.name}' has changes or commits. Keeping it at: ${worktreeSession.path}\n`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
141
170
|
}
|
|
142
171
|
process.exit(1);
|
|
143
172
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-selector-cli.d.ts","sourceRoot":"","sources":["../src/session-selector-cli.tsx"],"names":[],"mappings":"AAKA,wBAAsB,uBAAuB,
|
|
1
|
+
{"version":3,"file":"session-selector-cli.d.ts","sourceRoot":"","sources":["../src/session-selector-cli.tsx"],"names":[],"mappings":"AAKA,wBAAsB,uBAAuB,CAAC,EAC5C,OAAO,GACR,GAAE;IACD,OAAO,CAAC,EAAE,MAAM,CAAC;CACb,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA+B9B"}
|
|
@@ -2,8 +2,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { render, Box } from "ink";
|
|
3
3
|
import { listSessions, truncateContent } from "wave-agent-sdk";
|
|
4
4
|
import { SessionSelector } from "./components/SessionSelector.js";
|
|
5
|
-
export async function startSessionSelectorCli() {
|
|
6
|
-
const currentWorkdir = process.cwd();
|
|
5
|
+
export async function startSessionSelectorCli({ workdir, } = {}) {
|
|
6
|
+
const currentWorkdir = workdir || process.cwd();
|
|
7
7
|
const sessions = await listSessions(currentWorkdir);
|
|
8
8
|
if (sessions.length === 0) {
|
|
9
9
|
console.log(`No sessions found for workdir: ${currentWorkdir}`);
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { WorktreeSession } from "./utils/worktree.js";
|
|
2
|
+
export interface BaseAppProps {
|
|
3
|
+
bypassPermissions?: boolean;
|
|
4
|
+
pluginDirs?: string[];
|
|
5
|
+
tools?: string[];
|
|
6
|
+
worktreeSession?: WorktreeSession;
|
|
7
|
+
workdir?: string;
|
|
8
|
+
version?: string;
|
|
9
|
+
model?: string;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD,MAAM,WAAW,YAAY;IAC3B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface WorktreeSession {
|
|
2
|
+
name: string;
|
|
3
|
+
path: string;
|
|
4
|
+
branch: string;
|
|
5
|
+
repoRoot: string;
|
|
6
|
+
hasUncommittedChanges: boolean;
|
|
7
|
+
hasNewCommits: boolean;
|
|
8
|
+
isNew: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare const WORKTREE_DIR = ".wave/worktrees";
|
|
11
|
+
/**
|
|
12
|
+
* Create a new git worktree
|
|
13
|
+
* @param name Worktree name
|
|
14
|
+
* @param cwd Current working directory
|
|
15
|
+
* @returns Worktree session details
|
|
16
|
+
*/
|
|
17
|
+
export declare function createWorktree(name: string, cwd: string): WorktreeSession;
|
|
18
|
+
/**
|
|
19
|
+
* Remove a git worktree and its associated branch
|
|
20
|
+
* @param session Worktree session details
|
|
21
|
+
*/
|
|
22
|
+
export declare function removeWorktree(session: WorktreeSession): void;
|
|
23
|
+
//# sourceMappingURL=worktree.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worktree.d.ts","sourceRoot":"","sources":["../../src/utils/worktree.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,qBAAqB,EAAE,OAAO,CAAC;IAC/B,aAAa,EAAE,OAAO,CAAC;IACvB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,eAAO,MAAM,YAAY,oBAAoB,CAAC;AAE9C;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,eAAe,CAyEzE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CA6D7D"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import { getGitRepoRoot, getDefaultRemoteBranch } from "wave-agent-sdk";
|
|
5
|
+
export const WORKTREE_DIR = ".wave/worktrees";
|
|
6
|
+
/**
|
|
7
|
+
* Create a new git worktree
|
|
8
|
+
* @param name Worktree name
|
|
9
|
+
* @param cwd Current working directory
|
|
10
|
+
* @returns Worktree session details
|
|
11
|
+
*/
|
|
12
|
+
export function createWorktree(name, cwd) {
|
|
13
|
+
const repoRoot = getGitRepoRoot(cwd);
|
|
14
|
+
const worktreePath = path.join(repoRoot, WORKTREE_DIR, name);
|
|
15
|
+
const branchName = `worktree-${name}`;
|
|
16
|
+
const baseBranch = getDefaultRemoteBranch(cwd);
|
|
17
|
+
// Ensure parent directory exists
|
|
18
|
+
const parentDir = path.dirname(worktreePath);
|
|
19
|
+
if (!fs.existsSync(parentDir)) {
|
|
20
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
// Check if worktree already exists
|
|
23
|
+
if (fs.existsSync(worktreePath)) {
|
|
24
|
+
// If it exists, we assume it's already set up correctly
|
|
25
|
+
return {
|
|
26
|
+
name,
|
|
27
|
+
path: worktreePath,
|
|
28
|
+
branch: branchName,
|
|
29
|
+
repoRoot,
|
|
30
|
+
hasUncommittedChanges: false,
|
|
31
|
+
hasNewCommits: false,
|
|
32
|
+
isNew: false,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
// Create worktree and branch
|
|
37
|
+
execSync(`git worktree add -b ${branchName} "${worktreePath}" ${baseBranch}`, {
|
|
38
|
+
cwd: repoRoot,
|
|
39
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
name,
|
|
43
|
+
path: worktreePath,
|
|
44
|
+
branch: branchName,
|
|
45
|
+
repoRoot,
|
|
46
|
+
hasUncommittedChanges: false,
|
|
47
|
+
hasNewCommits: false,
|
|
48
|
+
isNew: true,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
const stderr = error.stderr?.toString() || "";
|
|
53
|
+
if (stderr.includes("already exists")) {
|
|
54
|
+
// If branch already exists, try to add worktree without -b
|
|
55
|
+
try {
|
|
56
|
+
execSync(`git worktree add "${worktreePath}" ${branchName}`, {
|
|
57
|
+
cwd: repoRoot,
|
|
58
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
name,
|
|
62
|
+
path: worktreePath,
|
|
63
|
+
branch: branchName,
|
|
64
|
+
repoRoot,
|
|
65
|
+
hasUncommittedChanges: false,
|
|
66
|
+
hasNewCommits: false,
|
|
67
|
+
isNew: true,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (innerError) {
|
|
71
|
+
throw new Error(`Failed to add existing worktree branch: ${innerError.message}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
throw new Error(`Failed to create worktree: ${error.message}\n${stderr}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Remove a git worktree and its associated branch
|
|
79
|
+
* @param session Worktree session details
|
|
80
|
+
*/
|
|
81
|
+
export function removeWorktree(session) {
|
|
82
|
+
const repoRoot = session.repoRoot;
|
|
83
|
+
try {
|
|
84
|
+
// Get current branch in worktree before removing it
|
|
85
|
+
let currentBranch;
|
|
86
|
+
try {
|
|
87
|
+
currentBranch = execSync(`git rev-parse --abbrev-ref HEAD`, {
|
|
88
|
+
cwd: session.path,
|
|
89
|
+
encoding: "utf8",
|
|
90
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
91
|
+
}).trim();
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Ignore errors getting current branch
|
|
95
|
+
}
|
|
96
|
+
// Remove worktree
|
|
97
|
+
execSync(`git worktree remove --force "${session.path}"`, {
|
|
98
|
+
cwd: repoRoot,
|
|
99
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
100
|
+
});
|
|
101
|
+
// Delete original branch
|
|
102
|
+
try {
|
|
103
|
+
execSync(`git branch -D ${session.branch}`, {
|
|
104
|
+
cwd: repoRoot,
|
|
105
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Ignore errors deleting original branch
|
|
110
|
+
}
|
|
111
|
+
// Delete current branch if it's different and not a protected branch
|
|
112
|
+
if (currentBranch &&
|
|
113
|
+
currentBranch !== session.branch &&
|
|
114
|
+
currentBranch !== "HEAD") {
|
|
115
|
+
const defaultRemoteBranch = getDefaultRemoteBranch(repoRoot);
|
|
116
|
+
const defaultBranchName = defaultRemoteBranch.split("/").pop();
|
|
117
|
+
if (currentBranch !== defaultBranchName &&
|
|
118
|
+
currentBranch !== "main" &&
|
|
119
|
+
currentBranch !== "master") {
|
|
120
|
+
try {
|
|
121
|
+
execSync(`git branch -D ${currentBranch}`, {
|
|
122
|
+
cwd: repoRoot,
|
|
123
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Ignore errors deleting current branch
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
console.error(`Failed to remove worktree or branch: ${error.message}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "CLI-based code assistant powered by AI, built with React and Ink",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"react": "^19.2.4",
|
|
40
40
|
"react-dom": "19.2.4",
|
|
41
41
|
"yargs": "^17.7.2",
|
|
42
|
-
"wave-agent-sdk": "0.
|
|
42
|
+
"wave-agent-sdk": "0.8.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/react": "^19.1.8",
|
package/src/cli.tsx
CHANGED
|
@@ -2,13 +2,12 @@ import React from "react";
|
|
|
2
2
|
import { render } from "ink";
|
|
3
3
|
import { App } from "./components/App.js";
|
|
4
4
|
import { cleanupLogs } from "./utils/logger.js";
|
|
5
|
+
import { removeWorktree } from "./utils/worktree.js";
|
|
6
|
+
import { BaseAppProps } from "./types.js";
|
|
5
7
|
|
|
6
|
-
export interface CliOptions {
|
|
8
|
+
export interface CliOptions extends BaseAppProps {
|
|
7
9
|
restoreSessionId?: string;
|
|
8
10
|
continueLastSession?: boolean;
|
|
9
|
-
bypassPermissions?: boolean;
|
|
10
|
-
pluginDirs?: string[];
|
|
11
|
-
tools?: string[];
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
export async function startCli(options: CliOptions): Promise<void> {
|
|
@@ -18,77 +17,55 @@ export async function startCli(options: CliOptions): Promise<void> {
|
|
|
18
17
|
bypassPermissions,
|
|
19
18
|
pluginDirs,
|
|
20
19
|
tools,
|
|
20
|
+
worktreeSession,
|
|
21
|
+
workdir,
|
|
22
|
+
version,
|
|
23
|
+
model,
|
|
21
24
|
} = options;
|
|
22
25
|
|
|
23
26
|
// Continue with ink-based UI for normal mode
|
|
24
|
-
|
|
25
|
-
let isCleaningUp = false;
|
|
26
|
-
let appUnmounted = false;
|
|
27
|
+
let shouldRemoveWorktree = false;
|
|
27
28
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
console.log("\nShutting down gracefully...");
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
// Clean up old log files
|
|
36
|
-
await cleanupLogs().catch((error) => {
|
|
37
|
-
console.warn("Failed to cleanup old logs:", error);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// Unmount the React app to trigger cleanup
|
|
41
|
-
if (!appUnmounted) {
|
|
42
|
-
unmount();
|
|
43
|
-
appUnmounted = true;
|
|
44
|
-
// Give React time to cleanup
|
|
45
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
process.exit(0);
|
|
49
|
-
} catch (error: unknown) {
|
|
50
|
-
console.error("Error during cleanup:", error);
|
|
51
|
-
process.exit(1);
|
|
52
|
-
}
|
|
29
|
+
const handleExit = (shouldRemove: boolean) => {
|
|
30
|
+
shouldRemoveWorktree = shouldRemove;
|
|
31
|
+
unmount();
|
|
53
32
|
};
|
|
54
33
|
|
|
55
|
-
// Handle process signals
|
|
56
|
-
process.on("SIGINT", cleanup);
|
|
57
|
-
process.on("SIGTERM", cleanup);
|
|
58
|
-
|
|
59
|
-
// Handle uncaught exceptions
|
|
60
|
-
process.on("uncaughtException", (error) => {
|
|
61
|
-
console.error("Uncaught exception:", error);
|
|
62
|
-
cleanup();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
process.on("unhandledRejection", (reason, promise) => {
|
|
66
|
-
console.error("Unhandled rejection at:", promise, "reason:", reason);
|
|
67
|
-
cleanup();
|
|
68
|
-
});
|
|
69
|
-
|
|
70
34
|
// Render the application
|
|
71
|
-
const { unmount } = render(
|
|
35
|
+
const { unmount, waitUntilExit } = render(
|
|
72
36
|
<App
|
|
73
37
|
restoreSessionId={restoreSessionId}
|
|
74
38
|
continueLastSession={continueLastSession}
|
|
75
39
|
bypassPermissions={bypassPermissions}
|
|
76
40
|
pluginDirs={pluginDirs}
|
|
77
41
|
tools={tools}
|
|
42
|
+
worktreeSession={worktreeSession}
|
|
43
|
+
workdir={workdir}
|
|
44
|
+
version={version}
|
|
45
|
+
model={model}
|
|
46
|
+
onExit={handleExit}
|
|
78
47
|
/>,
|
|
48
|
+
{ exitOnCtrlC: false },
|
|
79
49
|
);
|
|
80
50
|
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
51
|
+
// Wait for the app to finish unmounting
|
|
52
|
+
await waitUntilExit();
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
// Clean up old log files
|
|
56
|
+
await cleanupLogs().catch((error) => {
|
|
57
|
+
console.warn("Failed to cleanup old logs:", error);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Cleanup worktree if requested
|
|
61
|
+
if (shouldRemoveWorktree && worktreeSession) {
|
|
62
|
+
process.chdir(worktreeSession.repoRoot);
|
|
63
|
+
removeWorktree(worktreeSession);
|
|
89
64
|
}
|
|
90
|
-
});
|
|
91
65
|
|
|
92
|
-
|
|
93
|
-
|
|
66
|
+
process.exit(0);
|
|
67
|
+
} catch (error: unknown) {
|
|
68
|
+
console.error("Error during cleanup:", error);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
94
71
|
}
|
package/src/components/App.tsx
CHANGED
|
@@ -1,27 +1,105 @@
|
|
|
1
|
-
import React, { useState, useEffect, useRef } from "react";
|
|
2
|
-
import { useStdout } from "ink";
|
|
1
|
+
import React, { useState, useEffect, useRef, useCallback } from "react";
|
|
2
|
+
import { useStdout, useInput } from "ink";
|
|
3
3
|
import { ChatInterface } from "./ChatInterface.js";
|
|
4
4
|
import { ChatProvider, useChat } from "../contexts/useChat.js";
|
|
5
5
|
import { AppProvider } from "../contexts/useAppConfig.js";
|
|
6
|
+
import { WorktreeExitPrompt } from "./WorktreeExitPrompt.js";
|
|
7
|
+
import {
|
|
8
|
+
hasUncommittedChanges,
|
|
9
|
+
hasNewCommits,
|
|
10
|
+
getDefaultRemoteBranch,
|
|
11
|
+
} from "wave-agent-sdk";
|
|
12
|
+
import { BaseAppProps } from "../types.js";
|
|
6
13
|
|
|
7
|
-
interface AppProps {
|
|
14
|
+
interface AppProps extends BaseAppProps {
|
|
8
15
|
restoreSessionId?: string;
|
|
9
16
|
continueLastSession?: boolean;
|
|
10
|
-
|
|
11
|
-
pluginDirs?: string[];
|
|
12
|
-
tools?: string[];
|
|
17
|
+
onExit: (shouldRemove: boolean) => void;
|
|
13
18
|
}
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
interface AppWithProvidersProps extends BaseAppProps {
|
|
21
|
+
onExit: (shouldRemove: boolean) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const AppWithProviders: React.FC<AppWithProvidersProps> = ({
|
|
25
|
+
bypassPermissions,
|
|
26
|
+
pluginDirs,
|
|
27
|
+
tools,
|
|
28
|
+
worktreeSession,
|
|
29
|
+
workdir,
|
|
30
|
+
version,
|
|
31
|
+
model,
|
|
32
|
+
onExit,
|
|
33
|
+
}) => {
|
|
34
|
+
const [isExiting, setIsExiting] = useState(false);
|
|
35
|
+
const [worktreeStatus, setWorktreeStatus] = useState<{
|
|
36
|
+
hasUncommittedChanges: boolean;
|
|
37
|
+
hasNewCommits: boolean;
|
|
38
|
+
} | null>(null);
|
|
39
|
+
|
|
40
|
+
const handleSignal = useCallback(async () => {
|
|
41
|
+
if (worktreeSession) {
|
|
42
|
+
const cwd = workdir || worktreeSession.path;
|
|
43
|
+
const baseBranch = getDefaultRemoteBranch(cwd);
|
|
44
|
+
const hasChanges = hasUncommittedChanges(cwd);
|
|
45
|
+
const hasCommits = hasNewCommits(cwd, baseBranch);
|
|
46
|
+
|
|
47
|
+
if (hasChanges || hasCommits) {
|
|
48
|
+
setWorktreeStatus({
|
|
49
|
+
hasUncommittedChanges: hasChanges,
|
|
50
|
+
hasNewCommits: hasCommits,
|
|
51
|
+
});
|
|
52
|
+
setIsExiting(true);
|
|
53
|
+
} else {
|
|
54
|
+
onExit(true);
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
onExit(false);
|
|
58
|
+
}
|
|
59
|
+
}, [worktreeSession, workdir, onExit]);
|
|
60
|
+
|
|
61
|
+
useInput((input, key) => {
|
|
62
|
+
if (input === "c" && key.ctrl) {
|
|
63
|
+
handleSignal();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const onSigInt = () => handleSignal();
|
|
69
|
+
const onSigTerm = () => handleSignal();
|
|
70
|
+
|
|
71
|
+
process.on("SIGINT", onSigInt);
|
|
72
|
+
process.on("SIGTERM", onSigTerm);
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
process.off("SIGINT", onSigInt);
|
|
76
|
+
process.off("SIGTERM", onSigTerm);
|
|
77
|
+
};
|
|
78
|
+
}, [handleSignal]);
|
|
79
|
+
|
|
80
|
+
if (isExiting && worktreeSession && worktreeStatus) {
|
|
81
|
+
return (
|
|
82
|
+
<WorktreeExitPrompt
|
|
83
|
+
name={worktreeSession.name}
|
|
84
|
+
path={worktreeSession.path}
|
|
85
|
+
hasUncommittedChanges={worktreeStatus.hasUncommittedChanges}
|
|
86
|
+
hasNewCommits={worktreeStatus.hasNewCommits}
|
|
87
|
+
onKeep={() => onExit(false)}
|
|
88
|
+
onRemove={() => onExit(true)}
|
|
89
|
+
onCancel={() => setIsExiting(false)}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
20
94
|
return (
|
|
21
95
|
<ChatProvider
|
|
22
96
|
bypassPermissions={bypassPermissions}
|
|
23
97
|
pluginDirs={pluginDirs}
|
|
24
98
|
tools={tools}
|
|
99
|
+
workdir={workdir}
|
|
100
|
+
worktreeSession={worktreeSession}
|
|
101
|
+
version={version}
|
|
102
|
+
model={model}
|
|
25
103
|
>
|
|
26
104
|
<ChatInterfaceWithRemount />
|
|
27
105
|
</ChatProvider>
|
|
@@ -81,6 +159,11 @@ export const App: React.FC<AppProps> = ({
|
|
|
81
159
|
bypassPermissions,
|
|
82
160
|
pluginDirs,
|
|
83
161
|
tools,
|
|
162
|
+
worktreeSession,
|
|
163
|
+
workdir,
|
|
164
|
+
version,
|
|
165
|
+
model,
|
|
166
|
+
onExit,
|
|
84
167
|
}) => {
|
|
85
168
|
return (
|
|
86
169
|
<AppProvider
|
|
@@ -91,6 +174,11 @@ export const App: React.FC<AppProps> = ({
|
|
|
91
174
|
bypassPermissions={bypassPermissions}
|
|
92
175
|
pluginDirs={pluginDirs}
|
|
93
176
|
tools={tools}
|
|
177
|
+
worktreeSession={worktreeSession}
|
|
178
|
+
workdir={workdir}
|
|
179
|
+
version={version}
|
|
180
|
+
model={model}
|
|
181
|
+
onExit={onExit}
|
|
94
182
|
/>
|
|
95
183
|
</AppProvider>
|
|
96
184
|
);
|