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.
Files changed (108) hide show
  1. package/dist/cli.d.ts +2 -4
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +24 -52
  4. package/dist/components/App.d.ts +3 -4
  5. package/dist/components/App.d.ts.map +1 -1
  6. package/dist/components/App.js +49 -6
  7. package/dist/components/BackgroundTaskManager.d.ts.map +1 -1
  8. package/dist/components/BackgroundTaskManager.js +12 -20
  9. package/dist/components/BangDisplay.d.ts +9 -0
  10. package/dist/components/BangDisplay.d.ts.map +1 -0
  11. package/dist/components/{CommandOutputDisplay.js → BangDisplay.js} +1 -1
  12. package/dist/components/ChatInterface.d.ts.map +1 -1
  13. package/dist/components/ChatInterface.js +3 -2
  14. package/dist/components/CommandSelector.d.ts.map +1 -1
  15. package/dist/components/CommandSelector.js +18 -2
  16. package/dist/components/ConfirmationSelector.d.ts.map +1 -1
  17. package/dist/components/ConfirmationSelector.js +105 -8
  18. package/dist/components/HelpView.d.ts.map +1 -1
  19. package/dist/components/HelpView.js +2 -0
  20. package/dist/components/HistorySearch.d.ts.map +1 -1
  21. package/dist/components/HistorySearch.js +19 -25
  22. package/dist/components/InputBox.d.ts.map +1 -1
  23. package/dist/components/InputBox.js +9 -3
  24. package/dist/components/MarketplaceAddForm.d.ts.map +1 -1
  25. package/dist/components/MarketplaceAddForm.js +13 -6
  26. package/dist/components/MarketplaceDetail.d.ts.map +1 -1
  27. package/dist/components/MarketplaceDetail.js +8 -3
  28. package/dist/components/MessageBlockItem.js +2 -2
  29. package/dist/components/MessageList.d.ts +4 -1
  30. package/dist/components/MessageList.d.ts.map +1 -1
  31. package/dist/components/MessageList.js +15 -8
  32. package/dist/components/PluginDetail.d.ts.map +1 -1
  33. package/dist/components/PluginDetail.js +14 -3
  34. package/dist/components/PluginManagerShell.d.ts.map +1 -1
  35. package/dist/components/PluginManagerShell.js +3 -3
  36. package/dist/components/PluginManagerTypes.d.ts +2 -0
  37. package/dist/components/PluginManagerTypes.d.ts.map +1 -1
  38. package/dist/components/SessionSelector.d.ts.map +1 -1
  39. package/dist/components/SessionSelector.js +10 -13
  40. package/dist/components/StatusCommand.d.ts +6 -0
  41. package/dist/components/StatusCommand.d.ts.map +1 -0
  42. package/dist/components/StatusCommand.js +28 -0
  43. package/dist/components/WorktreeExitPrompt.d.ts +13 -0
  44. package/dist/components/WorktreeExitPrompt.d.ts.map +1 -0
  45. package/dist/components/WorktreeExitPrompt.js +26 -0
  46. package/dist/contexts/useChat.d.ts +9 -5
  47. package/dist/contexts/useChat.d.ts.map +1 -1
  48. package/dist/contexts/useChat.js +38 -8
  49. package/dist/contracts/status.d.ts +8 -0
  50. package/dist/contracts/status.d.ts.map +1 -0
  51. package/dist/contracts/status.js +1 -0
  52. package/dist/hooks/useInputManager.d.ts +2 -0
  53. package/dist/hooks/useInputManager.d.ts.map +1 -1
  54. package/dist/hooks/useInputManager.js +12 -0
  55. package/dist/hooks/usePluginManager.d.ts.map +1 -1
  56. package/dist/hooks/usePluginManager.js +41 -13
  57. package/dist/hooks/useTasks.js +2 -2
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +53 -4
  60. package/dist/managers/InputManager.d.ts +6 -0
  61. package/dist/managers/InputManager.d.ts.map +1 -1
  62. package/dist/managers/InputManager.js +32 -13
  63. package/dist/print-cli.d.ts +2 -4
  64. package/dist/print-cli.d.ts.map +1 -1
  65. package/dist/print-cli.js +31 -2
  66. package/dist/session-selector-cli.d.ts +3 -1
  67. package/dist/session-selector-cli.d.ts.map +1 -1
  68. package/dist/session-selector-cli.js +2 -2
  69. package/dist/types.d.ts +11 -0
  70. package/dist/types.d.ts.map +1 -0
  71. package/dist/types.js +1 -0
  72. package/dist/utils/worktree.d.ts +23 -0
  73. package/dist/utils/worktree.d.ts.map +1 -0
  74. package/dist/utils/worktree.js +135 -0
  75. package/package.json +2 -2
  76. package/src/cli.tsx +36 -59
  77. package/src/components/App.tsx +99 -11
  78. package/src/components/BackgroundTaskManager.tsx +12 -20
  79. package/src/components/{CommandOutputDisplay.tsx → BangDisplay.tsx} +4 -4
  80. package/src/components/ChatInterface.tsx +8 -0
  81. package/src/components/CommandSelector.tsx +18 -1
  82. package/src/components/ConfirmationSelector.tsx +118 -9
  83. package/src/components/HelpView.tsx +2 -0
  84. package/src/components/HistorySearch.tsx +25 -30
  85. package/src/components/InputBox.tsx +11 -1
  86. package/src/components/MarketplaceAddForm.tsx +21 -8
  87. package/src/components/MarketplaceDetail.tsx +19 -4
  88. package/src/components/MessageBlockItem.tsx +3 -3
  89. package/src/components/MessageList.tsx +47 -23
  90. package/src/components/PluginDetail.tsx +30 -6
  91. package/src/components/PluginManagerShell.tsx +24 -6
  92. package/src/components/PluginManagerTypes.ts +2 -0
  93. package/src/components/SessionSelector.tsx +38 -24
  94. package/src/components/StatusCommand.tsx +94 -0
  95. package/src/components/WorktreeExitPrompt.tsx +86 -0
  96. package/src/contexts/useChat.tsx +57 -13
  97. package/src/contracts/status.ts +7 -0
  98. package/src/hooks/useInputManager.ts +12 -0
  99. package/src/hooks/usePluginManager.ts +47 -13
  100. package/src/hooks/useTasks.ts +2 -2
  101. package/src/index.ts +71 -12
  102. package/src/managers/InputManager.ts +37 -15
  103. package/src/print-cli.ts +48 -5
  104. package/src/session-selector-cli.tsx +6 -2
  105. package/src/types.ts +11 -0
  106. package/src/utils/worktree.ts +164 -0
  107. package/dist/components/CommandOutputDisplay.d.ts +0 -9
  108. 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 === "tasks") {
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.showBackgroundTaskManager &&
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.showFileSelector ||
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
- // Task manager, MCP manager, Rewind and Help don't need to handle input, handled by component itself
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
  }
@@ -1,11 +1,9 @@
1
- export interface PrintCliOptions {
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
@@ -1 +1 @@
1
- {"version":3,"file":"print-cli.d.ts","sourceRoot":"","sources":["../src/print-cli.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,eAAe;IAC9B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAgBD,wBAAsB,aAAa,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAgK3E"}
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,2 +1,4 @@
1
- export declare function startSessionSelectorCli(): Promise<string | null>;
1
+ export declare function startSessionSelectorCli({ workdir, }?: {
2
+ workdir?: string;
3
+ }): Promise<string | null>;
2
4
  //# sourceMappingURL=session-selector-cli.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"session-selector-cli.d.ts","sourceRoot":"","sources":["../src/session-selector-cli.tsx"],"names":[],"mappings":"AAKA,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA+BtE"}
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}`);
@@ -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.7.1",
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.7.1"
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
- // Global cleanup tracker
25
- let isCleaningUp = false;
26
- let appUnmounted = false;
27
+ let shouldRemoveWorktree = false;
27
28
 
28
- const cleanup = async () => {
29
- if (isCleaningUp) return;
30
- isCleaningUp = true;
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
- // Store unmount function for cleanup when process exits normally
82
- process.on("exit", () => {
83
- if (!appUnmounted) {
84
- try {
85
- unmount();
86
- } catch {
87
- // Ignore errors during unmount
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
- // Return a promise that never resolves to keep the CLI running
93
- return new Promise(() => {});
66
+ process.exit(0);
67
+ } catch (error: unknown) {
68
+ console.error("Error during cleanup:", error);
69
+ process.exit(1);
70
+ }
94
71
  }
@@ -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
- bypassPermissions?: boolean;
11
- pluginDirs?: string[];
12
- tools?: string[];
17
+ onExit: (shouldRemove: boolean) => void;
13
18
  }
14
19
 
15
- const AppWithProviders: React.FC<{
16
- bypassPermissions?: boolean;
17
- pluginDirs?: string[];
18
- tools?: string[];
19
- }> = ({ bypassPermissions, pluginDirs, tools }) => {
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
  );