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.
Files changed (102) 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/BangDisplay.d.ts +9 -0
  8. package/dist/components/BangDisplay.d.ts.map +1 -0
  9. package/dist/components/{CommandOutputDisplay.js → BangDisplay.js} +1 -1
  10. package/dist/components/ChatInterface.d.ts.map +1 -1
  11. package/dist/components/ChatInterface.js +3 -2
  12. package/dist/components/CommandSelector.d.ts.map +1 -1
  13. package/dist/components/CommandSelector.js +18 -2
  14. package/dist/components/ConfirmationSelector.d.ts.map +1 -1
  15. package/dist/components/ConfirmationSelector.js +105 -8
  16. package/dist/components/HelpView.d.ts.map +1 -1
  17. package/dist/components/HelpView.js +2 -0
  18. package/dist/components/InputBox.d.ts.map +1 -1
  19. package/dist/components/InputBox.js +9 -3
  20. package/dist/components/MarketplaceAddForm.d.ts.map +1 -1
  21. package/dist/components/MarketplaceAddForm.js +13 -6
  22. package/dist/components/MarketplaceDetail.d.ts.map +1 -1
  23. package/dist/components/MarketplaceDetail.js +8 -3
  24. package/dist/components/MessageBlockItem.js +2 -2
  25. package/dist/components/MessageList.d.ts +4 -1
  26. package/dist/components/MessageList.d.ts.map +1 -1
  27. package/dist/components/MessageList.js +15 -8
  28. package/dist/components/PluginDetail.d.ts.map +1 -1
  29. package/dist/components/PluginDetail.js +14 -3
  30. package/dist/components/PluginManagerShell.d.ts.map +1 -1
  31. package/dist/components/PluginManagerShell.js +3 -3
  32. package/dist/components/PluginManagerTypes.d.ts +2 -0
  33. package/dist/components/PluginManagerTypes.d.ts.map +1 -1
  34. package/dist/components/SessionSelector.d.ts.map +1 -1
  35. package/dist/components/SessionSelector.js +5 -5
  36. package/dist/components/StatusCommand.d.ts +6 -0
  37. package/dist/components/StatusCommand.d.ts.map +1 -0
  38. package/dist/components/StatusCommand.js +28 -0
  39. package/dist/components/WorktreeExitPrompt.d.ts +13 -0
  40. package/dist/components/WorktreeExitPrompt.d.ts.map +1 -0
  41. package/dist/components/WorktreeExitPrompt.js +26 -0
  42. package/dist/contexts/useChat.d.ts +9 -5
  43. package/dist/contexts/useChat.d.ts.map +1 -1
  44. package/dist/contexts/useChat.js +38 -8
  45. package/dist/contracts/status.d.ts +8 -0
  46. package/dist/contracts/status.d.ts.map +1 -0
  47. package/dist/contracts/status.js +1 -0
  48. package/dist/hooks/useInputManager.d.ts +2 -0
  49. package/dist/hooks/useInputManager.d.ts.map +1 -1
  50. package/dist/hooks/useInputManager.js +12 -0
  51. package/dist/hooks/usePluginManager.d.ts.map +1 -1
  52. package/dist/hooks/usePluginManager.js +41 -13
  53. package/dist/hooks/useTasks.js +2 -2
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +53 -4
  56. package/dist/managers/InputManager.d.ts +6 -0
  57. package/dist/managers/InputManager.d.ts.map +1 -1
  58. package/dist/managers/InputManager.js +32 -13
  59. package/dist/print-cli.d.ts +2 -4
  60. package/dist/print-cli.d.ts.map +1 -1
  61. package/dist/print-cli.js +31 -2
  62. package/dist/session-selector-cli.d.ts +3 -1
  63. package/dist/session-selector-cli.d.ts.map +1 -1
  64. package/dist/session-selector-cli.js +2 -2
  65. package/dist/types.d.ts +11 -0
  66. package/dist/types.d.ts.map +1 -0
  67. package/dist/types.js +1 -0
  68. package/dist/utils/worktree.d.ts +23 -0
  69. package/dist/utils/worktree.d.ts.map +1 -0
  70. package/dist/utils/worktree.js +135 -0
  71. package/package.json +2 -2
  72. package/src/cli.tsx +36 -59
  73. package/src/components/App.tsx +99 -11
  74. package/src/components/{CommandOutputDisplay.tsx → BangDisplay.tsx} +4 -4
  75. package/src/components/ChatInterface.tsx +8 -0
  76. package/src/components/CommandSelector.tsx +18 -1
  77. package/src/components/ConfirmationSelector.tsx +118 -9
  78. package/src/components/HelpView.tsx +2 -0
  79. package/src/components/InputBox.tsx +11 -1
  80. package/src/components/MarketplaceAddForm.tsx +21 -8
  81. package/src/components/MarketplaceDetail.tsx +19 -4
  82. package/src/components/MessageBlockItem.tsx +3 -3
  83. package/src/components/MessageList.tsx +47 -23
  84. package/src/components/PluginDetail.tsx +30 -6
  85. package/src/components/PluginManagerShell.tsx +24 -6
  86. package/src/components/PluginManagerTypes.ts +2 -0
  87. package/src/components/SessionSelector.tsx +33 -16
  88. package/src/components/StatusCommand.tsx +94 -0
  89. package/src/components/WorktreeExitPrompt.tsx +86 -0
  90. package/src/contexts/useChat.tsx +57 -13
  91. package/src/contracts/status.ts +7 -0
  92. package/src/hooks/useInputManager.ts +12 -0
  93. package/src/hooks/usePluginManager.ts +47 -13
  94. package/src/hooks/useTasks.ts +2 -2
  95. package/src/index.ts +71 -12
  96. package/src/managers/InputManager.ts +37 -15
  97. package/src/print-cli.ts +48 -5
  98. package/src/session-selector-cli.tsx +6 -2
  99. package/src/types.ts +11 -0
  100. package/src/utils/worktree.ts +164 -0
  101. package/dist/components/CommandOutputDisplay.d.ts +0 -9
  102. 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.7.2",
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.2"
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
  );
@@ -1,13 +1,13 @@
1
1
  import React, { useState, useEffect } from "react";
2
2
  import { Box, Text } from "ink";
3
- import type { CommandOutputBlock } from "wave-agent-sdk";
3
+ import type { BangBlock } from "wave-agent-sdk";
4
4
 
5
- interface CommandOutputDisplayProps {
6
- block: CommandOutputBlock;
5
+ interface BangDisplayProps {
6
+ block: BangBlock;
7
7
  isExpanded?: boolean;
8
8
  }
9
9
 
10
- export const CommandOutputDisplay: React.FC<CommandOutputDisplayProps> = ({
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 = [...commands, ...AVAILABLE_COMMANDS];
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
- return {
137
- ...prev,
138
- currentQuestionIndex: prev.currentQuestionIndex + 1,
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(newAnswers),
209
+ message: JSON.stringify(finalAnswers),
149
210
  });
150
211
  return {
151
212
  ...prev,
152
- userAnswers: newAnswers,
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",