wave-code 0.7.2 → 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/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/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 +5 -5
- 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/{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/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 +33 -16
- 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
|
@@ -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
|
);
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import React, { useState, useEffect } from "react";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import type {
|
|
3
|
+
import type { BangBlock } from "wave-agent-sdk";
|
|
4
4
|
|
|
5
|
-
interface
|
|
6
|
-
block:
|
|
5
|
+
interface BangDisplayProps {
|
|
6
|
+
block: BangBlock;
|
|
7
7
|
isExpanded?: boolean;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export const
|
|
10
|
+
export const BangDisplay: React.FC<BangDisplayProps> = ({
|
|
11
11
|
block,
|
|
12
12
|
isExpanded = false,
|
|
13
13
|
}) => {
|
|
@@ -36,8 +36,13 @@ export const ChatInterface: React.FC = () => {
|
|
|
36
36
|
handleConfirmationDecision,
|
|
37
37
|
handleConfirmationCancel: originalHandleConfirmationCancel,
|
|
38
38
|
setWasLastDetailsTooTall,
|
|
39
|
+
version,
|
|
40
|
+
workdir,
|
|
41
|
+
getModelConfig,
|
|
39
42
|
} = useChat();
|
|
40
43
|
|
|
44
|
+
const model = getModelConfig().model;
|
|
45
|
+
|
|
41
46
|
const handleDetailsHeightMeasured = useCallback((height: number) => {
|
|
42
47
|
setDetailsHeight(height);
|
|
43
48
|
}, []);
|
|
@@ -91,6 +96,9 @@ export const ChatInterface: React.FC = () => {
|
|
|
91
96
|
messages={messages}
|
|
92
97
|
isExpanded={isExpanded}
|
|
93
98
|
hideDynamicBlocks={isConfirmationVisible}
|
|
99
|
+
version={version}
|
|
100
|
+
workdir={workdir}
|
|
101
|
+
model={model}
|
|
94
102
|
/>
|
|
95
103
|
|
|
96
104
|
{(isLoading || isCommandRunning || isCompressing) &&
|
|
@@ -3,6 +3,12 @@ import { Box, Text, useInput } from "ink";
|
|
|
3
3
|
import type { SlashCommand } from "wave-agent-sdk";
|
|
4
4
|
|
|
5
5
|
const AVAILABLE_COMMANDS: SlashCommand[] = [
|
|
6
|
+
{
|
|
7
|
+
id: "clear",
|
|
8
|
+
name: "clear",
|
|
9
|
+
description: "Clear the chat session and terminal",
|
|
10
|
+
handler: () => {}, // Handler here won't be used, actual processing is in the hook
|
|
11
|
+
},
|
|
6
12
|
{
|
|
7
13
|
id: "tasks",
|
|
8
14
|
name: "tasks",
|
|
@@ -28,6 +34,12 @@ const AVAILABLE_COMMANDS: SlashCommand[] = [
|
|
|
28
34
|
description: "Show help and key bindings",
|
|
29
35
|
handler: () => {}, // Handler here won't be used, actual processing is in the hook
|
|
30
36
|
},
|
|
37
|
+
{
|
|
38
|
+
id: "status",
|
|
39
|
+
name: "status",
|
|
40
|
+
description: "Show agent status and configuration",
|
|
41
|
+
handler: () => {}, // Handler here won't be used, actual processing is in the hook
|
|
42
|
+
},
|
|
31
43
|
];
|
|
32
44
|
|
|
33
45
|
export interface CommandSelectorProps {
|
|
@@ -48,8 +60,13 @@ export const CommandSelector: React.FC<CommandSelectorProps> = ({
|
|
|
48
60
|
const MAX_VISIBLE_ITEMS = 3;
|
|
49
61
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
50
62
|
|
|
63
|
+
// Reset selected index when search query changes
|
|
64
|
+
React.useEffect(() => {
|
|
65
|
+
setSelectedIndex(0);
|
|
66
|
+
}, [searchQuery]);
|
|
67
|
+
|
|
51
68
|
// Merge agent commands and local commands
|
|
52
|
-
const allCommands = [...
|
|
69
|
+
const allCommands = [...AVAILABLE_COMMANDS, ...commands];
|
|
53
70
|
|
|
54
71
|
// Filter command list
|
|
55
72
|
const filteredCommands = allCommands.filter(
|
|
@@ -70,6 +70,15 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
70
70
|
userAnswers: {} as Record<string, string>,
|
|
71
71
|
otherText: "",
|
|
72
72
|
otherCursorPosition: 0,
|
|
73
|
+
savedStates: {} as Record<
|
|
74
|
+
number,
|
|
75
|
+
{
|
|
76
|
+
selectedOptionIndex: number;
|
|
77
|
+
selectedOptionIndices: Set<number>;
|
|
78
|
+
otherText: string;
|
|
79
|
+
otherCursorPosition: number;
|
|
80
|
+
}
|
|
81
|
+
>,
|
|
73
82
|
});
|
|
74
83
|
|
|
75
84
|
const questions =
|
|
@@ -133,23 +142,75 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
133
142
|
};
|
|
134
143
|
|
|
135
144
|
if (prev.currentQuestionIndex < questions.length - 1) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
145
|
+
const nextIndex = prev.currentQuestionIndex + 1;
|
|
146
|
+
const savedStates = {
|
|
147
|
+
...prev.savedStates,
|
|
148
|
+
[prev.currentQuestionIndex]: {
|
|
149
|
+
selectedOptionIndex: prev.selectedOptionIndex,
|
|
150
|
+
selectedOptionIndices: prev.selectedOptionIndices,
|
|
151
|
+
otherText: prev.otherText,
|
|
152
|
+
otherCursorPosition: prev.otherCursorPosition,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const nextState = savedStates[nextIndex] || {
|
|
139
157
|
selectedOptionIndex: 0,
|
|
140
|
-
selectedOptionIndices: new Set(),
|
|
141
|
-
userAnswers: newAnswers,
|
|
158
|
+
selectedOptionIndices: new Set<number>(),
|
|
142
159
|
otherText: "",
|
|
143
160
|
otherCursorPosition: 0,
|
|
144
161
|
};
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
...prev,
|
|
165
|
+
currentQuestionIndex: nextIndex,
|
|
166
|
+
...nextState,
|
|
167
|
+
userAnswers: newAnswers,
|
|
168
|
+
savedStates,
|
|
169
|
+
};
|
|
145
170
|
} else {
|
|
171
|
+
const finalAnswers = { ...newAnswers };
|
|
172
|
+
// Also collect from savedStates for any questions that were skipped via Tab
|
|
173
|
+
for (const [idxStr, s] of Object.entries(prev.savedStates)) {
|
|
174
|
+
const idx = parseInt(idxStr);
|
|
175
|
+
const q = questions[idx];
|
|
176
|
+
if (q && !finalAnswers[q.question]) {
|
|
177
|
+
const opts = [...q.options, { label: "Other" }];
|
|
178
|
+
let a = "";
|
|
179
|
+
if (q.multiSelect) {
|
|
180
|
+
const selectedLabels = Array.from(s.selectedOptionIndices)
|
|
181
|
+
.filter((i) => i < q.options.length)
|
|
182
|
+
.map((i) => q.options[i].label);
|
|
183
|
+
const isOtherChecked = s.selectedOptionIndices.has(
|
|
184
|
+
opts.length - 1,
|
|
185
|
+
);
|
|
186
|
+
if (isOtherChecked && s.otherText.trim()) {
|
|
187
|
+
selectedLabels.push(s.otherText.trim());
|
|
188
|
+
}
|
|
189
|
+
a = selectedLabels.join(", ");
|
|
190
|
+
} else {
|
|
191
|
+
if (s.selectedOptionIndex === opts.length - 1) {
|
|
192
|
+
a = s.otherText.trim();
|
|
193
|
+
} else {
|
|
194
|
+
a = opts[s.selectedOptionIndex].label;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (a) finalAnswers[q.question] = a;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Only submit if all questions have been answered
|
|
202
|
+
const allAnswered = questions.every(
|
|
203
|
+
(q) => finalAnswers[q.question],
|
|
204
|
+
);
|
|
205
|
+
if (!allAnswered) return prev;
|
|
206
|
+
|
|
146
207
|
onDecision({
|
|
147
208
|
behavior: "allow",
|
|
148
|
-
message: JSON.stringify(
|
|
209
|
+
message: JSON.stringify(finalAnswers),
|
|
149
210
|
});
|
|
150
211
|
return {
|
|
151
212
|
...prev,
|
|
152
|
-
userAnswers:
|
|
213
|
+
userAnswers: finalAnswers,
|
|
153
214
|
};
|
|
154
215
|
}
|
|
155
216
|
});
|
|
@@ -196,6 +257,41 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
196
257
|
}));
|
|
197
258
|
return;
|
|
198
259
|
}
|
|
260
|
+
if (key.tab) {
|
|
261
|
+
setQuestionState((prev) => {
|
|
262
|
+
const direction = key.shift ? -1 : 1;
|
|
263
|
+
let nextIndex = prev.currentQuestionIndex + direction;
|
|
264
|
+
if (nextIndex < 0) nextIndex = questions.length - 1;
|
|
265
|
+
if (nextIndex >= questions.length) nextIndex = 0;
|
|
266
|
+
|
|
267
|
+
if (nextIndex === prev.currentQuestionIndex) return prev;
|
|
268
|
+
|
|
269
|
+
const savedStates = {
|
|
270
|
+
...prev.savedStates,
|
|
271
|
+
[prev.currentQuestionIndex]: {
|
|
272
|
+
selectedOptionIndex: prev.selectedOptionIndex,
|
|
273
|
+
selectedOptionIndices: prev.selectedOptionIndices,
|
|
274
|
+
otherText: prev.otherText,
|
|
275
|
+
otherCursorPosition: prev.otherCursorPosition,
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const nextState = savedStates[nextIndex] || {
|
|
280
|
+
selectedOptionIndex: 0,
|
|
281
|
+
selectedOptionIndices: new Set<number>(),
|
|
282
|
+
otherText: "",
|
|
283
|
+
otherCursorPosition: 0,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
...prev,
|
|
288
|
+
currentQuestionIndex: nextIndex,
|
|
289
|
+
...nextState,
|
|
290
|
+
savedStates,
|
|
291
|
+
};
|
|
292
|
+
});
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
199
295
|
|
|
200
296
|
setQuestionState((prev) => {
|
|
201
297
|
const isOtherFocused = prev.selectedOptionIndex === options.length - 1;
|
|
@@ -321,6 +417,19 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
321
417
|
return;
|
|
322
418
|
}
|
|
323
419
|
|
|
420
|
+
if (key.tab) {
|
|
421
|
+
const currentIndex = availableOptions.indexOf(state.selectedOption);
|
|
422
|
+
const direction = key.shift ? -1 : 1;
|
|
423
|
+
let nextIndex = currentIndex + direction;
|
|
424
|
+
if (nextIndex < 0) nextIndex = availableOptions.length - 1;
|
|
425
|
+
if (nextIndex >= availableOptions.length) nextIndex = 0;
|
|
426
|
+
setState((prev) => ({
|
|
427
|
+
...prev,
|
|
428
|
+
selectedOption: availableOptions[nextIndex],
|
|
429
|
+
}));
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
324
433
|
if (input && !key.ctrl && !key.meta && !("alt" in key && key.alt)) {
|
|
325
434
|
setState((prev) => {
|
|
326
435
|
const nextText =
|
|
@@ -436,7 +545,7 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
436
545
|
Question {questionState.currentQuestionIndex + 1} of{" "}
|
|
437
546
|
{questions.length} •
|
|
438
547
|
{currentQuestion.multiSelect ? " Space to toggle •" : ""} Use ↑↓
|
|
439
|
-
to navigate • Enter to confirm
|
|
548
|
+
or Tab to navigate • Enter to confirm
|
|
440
549
|
</Text>
|
|
441
550
|
</Box>
|
|
442
551
|
</Box>
|
|
@@ -531,7 +640,7 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
531
640
|
</Box>
|
|
532
641
|
</Box>
|
|
533
642
|
<Box marginTop={1}>
|
|
534
|
-
<Text dimColor>Use ↑↓ to navigate • ESC to cancel</Text>
|
|
643
|
+
<Text dimColor>Use ↑↓ or Tab to navigate • ESC to cancel</Text>
|
|
535
644
|
</Box>
|
|
536
645
|
</>
|
|
537
646
|
)}
|
|
@@ -21,6 +21,8 @@ export const HelpView: React.FC<HelpViewProps> = ({ onCancel }) => {
|
|
|
21
21
|
{ key: "Ctrl+B", description: "Background current task" },
|
|
22
22
|
{ key: "Ctrl+V", description: "Paste image" },
|
|
23
23
|
{ key: "Shift+Tab", description: "Cycle permission mode" },
|
|
24
|
+
{ key: "/status", description: "Show agent status and configuration" },
|
|
25
|
+
{ key: "/clear", description: "Clear the chat session and terminal" },
|
|
24
26
|
{
|
|
25
27
|
key: "Esc",
|
|
26
28
|
description: "Interrupt AI or command / Cancel selector / Close help",
|