wave-code 0.0.2

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 (131) hide show
  1. package/README.md +120 -0
  2. package/bin/wave-code.js +16 -0
  3. package/dist/cli.d.ts +6 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +62 -0
  6. package/dist/components/App.d.ts +8 -0
  7. package/dist/components/App.d.ts.map +1 -0
  8. package/dist/components/App.js +10 -0
  9. package/dist/components/BashHistorySelector.d.ts +10 -0
  10. package/dist/components/BashHistorySelector.d.ts.map +1 -0
  11. package/dist/components/BashHistorySelector.js +83 -0
  12. package/dist/components/BashShellManager.d.ts +6 -0
  13. package/dist/components/BashShellManager.d.ts.map +1 -0
  14. package/dist/components/BashShellManager.js +116 -0
  15. package/dist/components/ChatInterface.d.ts +3 -0
  16. package/dist/components/ChatInterface.d.ts.map +1 -0
  17. package/dist/components/ChatInterface.js +31 -0
  18. package/dist/components/CommandOutputDisplay.d.ts +9 -0
  19. package/dist/components/CommandOutputDisplay.d.ts.map +1 -0
  20. package/dist/components/CommandOutputDisplay.js +40 -0
  21. package/dist/components/CommandSelector.d.ts +11 -0
  22. package/dist/components/CommandSelector.d.ts.map +1 -0
  23. package/dist/components/CommandSelector.js +60 -0
  24. package/dist/components/CompressDisplay.d.ts +9 -0
  25. package/dist/components/CompressDisplay.d.ts.map +1 -0
  26. package/dist/components/CompressDisplay.js +17 -0
  27. package/dist/components/DiffViewer.d.ts +9 -0
  28. package/dist/components/DiffViewer.d.ts.map +1 -0
  29. package/dist/components/DiffViewer.js +221 -0
  30. package/dist/components/FileSelector.d.ts +13 -0
  31. package/dist/components/FileSelector.d.ts.map +1 -0
  32. package/dist/components/FileSelector.js +48 -0
  33. package/dist/components/InputBox.d.ts +23 -0
  34. package/dist/components/InputBox.d.ts.map +1 -0
  35. package/dist/components/InputBox.js +124 -0
  36. package/dist/components/McpManager.d.ts +10 -0
  37. package/dist/components/McpManager.d.ts.map +1 -0
  38. package/dist/components/McpManager.js +123 -0
  39. package/dist/components/MemoryDisplay.d.ts +8 -0
  40. package/dist/components/MemoryDisplay.d.ts.map +1 -0
  41. package/dist/components/MemoryDisplay.js +25 -0
  42. package/dist/components/MemoryTypeSelector.d.ts +8 -0
  43. package/dist/components/MemoryTypeSelector.d.ts.map +1 -0
  44. package/dist/components/MemoryTypeSelector.js +38 -0
  45. package/dist/components/MessageList.d.ts +12 -0
  46. package/dist/components/MessageList.d.ts.map +1 -0
  47. package/dist/components/MessageList.js +36 -0
  48. package/dist/components/ToolResultDisplay.d.ts +9 -0
  49. package/dist/components/ToolResultDisplay.d.ts.map +1 -0
  50. package/dist/components/ToolResultDisplay.js +52 -0
  51. package/dist/contexts/useAppConfig.d.ts +11 -0
  52. package/dist/contexts/useAppConfig.d.ts.map +1 -0
  53. package/dist/contexts/useAppConfig.js +13 -0
  54. package/dist/contexts/useChat.d.ts +36 -0
  55. package/dist/contexts/useChat.d.ts.map +1 -0
  56. package/dist/contexts/useChat.js +208 -0
  57. package/dist/hooks/useBashHistorySelector.d.ts +15 -0
  58. package/dist/hooks/useBashHistorySelector.d.ts.map +1 -0
  59. package/dist/hooks/useBashHistorySelector.js +61 -0
  60. package/dist/hooks/useCommandSelector.d.ts +24 -0
  61. package/dist/hooks/useCommandSelector.d.ts.map +1 -0
  62. package/dist/hooks/useCommandSelector.js +98 -0
  63. package/dist/hooks/useFileSelector.d.ts +16 -0
  64. package/dist/hooks/useFileSelector.d.ts.map +1 -0
  65. package/dist/hooks/useFileSelector.js +174 -0
  66. package/dist/hooks/useImageManager.d.ts +13 -0
  67. package/dist/hooks/useImageManager.d.ts.map +1 -0
  68. package/dist/hooks/useImageManager.js +46 -0
  69. package/dist/hooks/useInputHistory.d.ts +11 -0
  70. package/dist/hooks/useInputHistory.d.ts.map +1 -0
  71. package/dist/hooks/useInputHistory.js +64 -0
  72. package/dist/hooks/useInputKeyboardHandler.d.ts +83 -0
  73. package/dist/hooks/useInputKeyboardHandler.d.ts.map +1 -0
  74. package/dist/hooks/useInputKeyboardHandler.js +507 -0
  75. package/dist/hooks/useInputState.d.ts +14 -0
  76. package/dist/hooks/useInputState.d.ts.map +1 -0
  77. package/dist/hooks/useInputState.js +57 -0
  78. package/dist/hooks/useMemoryTypeSelector.d.ts +9 -0
  79. package/dist/hooks/useMemoryTypeSelector.d.ts.map +1 -0
  80. package/dist/hooks/useMemoryTypeSelector.js +27 -0
  81. package/dist/hooks/usePagination.d.ts +20 -0
  82. package/dist/hooks/usePagination.d.ts.map +1 -0
  83. package/dist/hooks/usePagination.js +168 -0
  84. package/dist/index.d.ts +5 -0
  85. package/dist/index.d.ts.map +1 -0
  86. package/dist/index.js +91 -0
  87. package/dist/plain-cli.d.ts +7 -0
  88. package/dist/plain-cli.d.ts.map +1 -0
  89. package/dist/plain-cli.js +49 -0
  90. package/dist/utils/clipboard.d.ts +22 -0
  91. package/dist/utils/clipboard.d.ts.map +1 -0
  92. package/dist/utils/clipboard.js +347 -0
  93. package/dist/utils/constants.d.ts +17 -0
  94. package/dist/utils/constants.d.ts.map +1 -0
  95. package/dist/utils/constants.js +18 -0
  96. package/dist/utils/logger.d.ts +72 -0
  97. package/dist/utils/logger.d.ts.map +1 -0
  98. package/dist/utils/logger.js +245 -0
  99. package/package.json +60 -0
  100. package/src/cli.tsx +82 -0
  101. package/src/components/App.tsx +31 -0
  102. package/src/components/BashHistorySelector.tsx +163 -0
  103. package/src/components/BashShellManager.tsx +306 -0
  104. package/src/components/ChatInterface.tsx +88 -0
  105. package/src/components/CommandOutputDisplay.tsx +81 -0
  106. package/src/components/CommandSelector.tsx +144 -0
  107. package/src/components/CompressDisplay.tsx +58 -0
  108. package/src/components/DiffViewer.tsx +321 -0
  109. package/src/components/FileSelector.tsx +137 -0
  110. package/src/components/InputBox.tsx +310 -0
  111. package/src/components/McpManager.tsx +328 -0
  112. package/src/components/MemoryDisplay.tsx +62 -0
  113. package/src/components/MemoryTypeSelector.tsx +96 -0
  114. package/src/components/MessageList.tsx +215 -0
  115. package/src/components/ToolResultDisplay.tsx +138 -0
  116. package/src/contexts/useAppConfig.tsx +32 -0
  117. package/src/contexts/useChat.tsx +300 -0
  118. package/src/hooks/useBashHistorySelector.ts +77 -0
  119. package/src/hooks/useCommandSelector.ts +131 -0
  120. package/src/hooks/useFileSelector.ts +227 -0
  121. package/src/hooks/useImageManager.ts +64 -0
  122. package/src/hooks/useInputHistory.ts +74 -0
  123. package/src/hooks/useInputKeyboardHandler.ts +778 -0
  124. package/src/hooks/useInputState.ts +66 -0
  125. package/src/hooks/useMemoryTypeSelector.ts +40 -0
  126. package/src/hooks/usePagination.ts +203 -0
  127. package/src/index.ts +108 -0
  128. package/src/plain-cli.ts +66 -0
  129. package/src/utils/clipboard.ts +384 -0
  130. package/src/utils/constants.ts +22 -0
  131. package/src/utils/logger.ts +301 -0
@@ -0,0 +1,138 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import type { ToolBlock } from "wave-agent-sdk";
4
+
5
+ interface ToolResultDisplayProps {
6
+ block: ToolBlock;
7
+ isExpanded?: boolean;
8
+ }
9
+
10
+ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
11
+ block,
12
+ isExpanded = false,
13
+ }) => {
14
+ const { parameters, result, compactParams, isRunning, success, error, name } =
15
+ block;
16
+
17
+ // Directly use compactParams
18
+ // (no change needed as we destructured it above)
19
+
20
+ const getStatusColor = () => {
21
+ if (isRunning) return "yellow";
22
+ if (success) return "green";
23
+ if (error || success === false) return "red";
24
+ return "gray"; // Unknown state or no state information
25
+ };
26
+
27
+ const getStatusText = () => {
28
+ if (isRunning) return "🔄";
29
+ if (success) return "";
30
+ if (error || success === false) return "❌ Failed";
31
+ return ""; // Don't display text for unknown state
32
+ };
33
+
34
+ const hasImages = () => {
35
+ return block.images && block.images.length > 0;
36
+ };
37
+
38
+ const getImageIndicator = () => {
39
+ if (!hasImages()) return "";
40
+ const imageCount = block.images!.length;
41
+ return imageCount === 1 ? "🖼️" : `🖼️×${imageCount}`;
42
+ };
43
+
44
+ const toolName = name ? String(name) : "Tool";
45
+
46
+ // Get shortResult, if not available show last 5 lines of result
47
+ const getShortResult = () => {
48
+ if (block.shortResult) {
49
+ return block.shortResult;
50
+ }
51
+
52
+ // If no shortResult but has result, return last 5 lines
53
+ if (block.result) {
54
+ const lines = block.result.split("\n");
55
+ if (lines.length > 5) {
56
+ return lines.slice(-5).join("\n");
57
+ }
58
+ return block.result;
59
+ }
60
+
61
+ return null;
62
+ };
63
+
64
+ const shortResult = getShortResult();
65
+
66
+ return (
67
+ <Box flexDirection="column" gap={1}>
68
+ <Box>
69
+ <Text color="magenta">🔧 </Text>
70
+ <Text color="white">{toolName}</Text>
71
+ {/* Display compactParams in collapsed state */}
72
+ {!isExpanded && compactParams && (
73
+ <Text color="gray"> ({compactParams})</Text>
74
+ )}
75
+ <Text color={getStatusColor()}> {getStatusText()}</Text>
76
+ {/* Display image indicator */}
77
+ {hasImages() && <Text color="blue"> {getImageIndicator()}</Text>}
78
+ </Box>
79
+
80
+ {/* Display shortResult in collapsed state */}
81
+ {!isExpanded && shortResult && (
82
+ <Box
83
+ paddingLeft={2}
84
+ borderLeft
85
+ borderColor="gray"
86
+ flexDirection="column"
87
+ >
88
+ {shortResult.split("\n").map((line, index) => (
89
+ <Text key={index} color="white">
90
+ {line}
91
+ </Text>
92
+ ))}
93
+ </Box>
94
+ )}
95
+
96
+ {/* Display complete parameters in expanded state */}
97
+ {isExpanded && parameters && (
98
+ <Box
99
+ paddingLeft={2}
100
+ borderLeft
101
+ borderColor="gray"
102
+ flexDirection="column"
103
+ >
104
+ <Text color="cyan" bold>
105
+ Parameters:
106
+ </Text>
107
+ <Text color="gray">{parameters}</Text>
108
+ </Box>
109
+ )}
110
+
111
+ {/* Display complete result in expanded state */}
112
+ {isExpanded && result && (
113
+ <Box flexDirection="column">
114
+ <Box
115
+ paddingLeft={2}
116
+ borderLeft
117
+ borderColor="green"
118
+ flexDirection="column"
119
+ >
120
+ <Text color="cyan" bold>
121
+ Result:
122
+ </Text>
123
+ <Text color="white">{result}</Text>
124
+ </Box>
125
+ </Box>
126
+ )}
127
+
128
+ {/* Error information always displayed */}
129
+ {error && (
130
+ <Box>
131
+ <Text color="red">
132
+ Error: {typeof error === "string" ? error : String(error)}
133
+ </Text>
134
+ </Box>
135
+ )}
136
+ </Box>
137
+ );
138
+ };
@@ -0,0 +1,32 @@
1
+ import React, { createContext, useContext } from "react";
2
+
3
+ export interface AppConfig {
4
+ restoreSessionId?: string;
5
+ continueLastSession?: boolean;
6
+ }
7
+
8
+ const AppContext = createContext<AppConfig | null>(null);
9
+
10
+ export const useAppConfig = () => {
11
+ const context = useContext(AppContext);
12
+ if (!context) {
13
+ throw new Error("useAppConfig must be used within AppProvider");
14
+ }
15
+ return context;
16
+ };
17
+
18
+ export interface AppProviderProps extends AppConfig {
19
+ children: React.ReactNode;
20
+ }
21
+
22
+ export const AppProvider: React.FC<AppProviderProps> = ({
23
+ restoreSessionId,
24
+ continueLastSession,
25
+ children,
26
+ }) => {
27
+ return (
28
+ <AppContext.Provider value={{ restoreSessionId, continueLastSession }}>
29
+ {children}
30
+ </AppContext.Provider>
31
+ );
32
+ };
@@ -0,0 +1,300 @@
1
+ import React, {
2
+ createContext,
3
+ useContext,
4
+ useCallback,
5
+ useRef,
6
+ useEffect,
7
+ useState,
8
+ } from "react";
9
+ import { useInput } from "ink";
10
+ import { useAppConfig } from "./useAppConfig.js";
11
+ import type {
12
+ Message,
13
+ McpServerStatus,
14
+ BackgroundShell,
15
+ SlashCommand,
16
+ } from "wave-agent-sdk";
17
+ import { Agent, AgentCallbacks } from "wave-agent-sdk";
18
+ import { logger } from "../utils/logger.js";
19
+
20
+ // Main Chat Context
21
+ export interface ChatContextType {
22
+ messages: Message[];
23
+ isLoading: boolean;
24
+ isCommandRunning: boolean;
25
+ isCompressing: boolean;
26
+ userInputHistory: string[];
27
+ // Message display state
28
+ isExpanded: boolean;
29
+ // AI functionality
30
+ sessionId: string;
31
+ sendMessage: (
32
+ content: string,
33
+ images?: Array<{ path: string; mimeType: string }>,
34
+ ) => Promise<void>;
35
+ abortMessage: () => void;
36
+ latestTotalTokens: number;
37
+ // Memory functionality
38
+ saveMemory: (message: string, type: "project" | "user") => Promise<void>;
39
+ // MCP functionality
40
+ mcpServers: McpServerStatus[];
41
+ connectMcpServer: (serverName: string) => Promise<boolean>;
42
+ disconnectMcpServer: (serverName: string) => Promise<boolean>;
43
+ // Background bash shells
44
+ backgroundShells: BackgroundShell[];
45
+ getBackgroundShellOutput: (
46
+ shellId: string,
47
+ ) => { stdout: string; stderr: string; status: string } | null;
48
+ killBackgroundShell: (shellId: string) => boolean;
49
+ // Slash Command functionality
50
+ slashCommands: SlashCommand[];
51
+ hasSlashCommand: (commandId: string) => boolean;
52
+ }
53
+
54
+ const ChatContext = createContext<ChatContextType | null>(null);
55
+
56
+ export const useChat = () => {
57
+ const context = useContext(ChatContext);
58
+ if (!context) {
59
+ throw new Error("useChat must be used within ChatProvider");
60
+ }
61
+ return context;
62
+ };
63
+
64
+ export interface ChatProviderProps {
65
+ children: React.ReactNode;
66
+ }
67
+
68
+ export const ChatProvider: React.FC<ChatProviderProps> = ({ children }) => {
69
+ const { restoreSessionId, continueLastSession } = useAppConfig();
70
+
71
+ // Message Display State
72
+ const [isExpanded, setIsExpanded] = useState(false);
73
+
74
+ // AI State
75
+ const [messages, setMessages] = useState<Message[]>([]);
76
+ const [isLoading, setIsLoading] = useState(false);
77
+ const [latestTotalTokens, setlatestTotalTokens] = useState(0);
78
+ const [sessionId, setSessionId] = useState("");
79
+ const [isCommandRunning, setIsCommandRunning] = useState(false);
80
+ const [isCompressing, setIsCompressing] = useState(false);
81
+ const [userInputHistory, setUserInputHistory] = useState<string[]>([]);
82
+
83
+ // MCP State
84
+ const [mcpServers, setMcpServers] = useState<McpServerStatus[]>([]);
85
+
86
+ // Background bash shells state
87
+ const [backgroundShells, setBackgroundShells] = useState<BackgroundShell[]>(
88
+ [],
89
+ );
90
+
91
+ // Command state
92
+ const [slashCommands, setSlashCommands] = useState<SlashCommand[]>([]);
93
+
94
+ const agentRef = useRef<Agent | null>(null);
95
+
96
+ // Listen for Ctrl+O hotkey to toggle collapse/expand state
97
+ useInput((input, key) => {
98
+ if (key.ctrl && input === "o") {
99
+ setIsExpanded((prev) => !prev);
100
+ }
101
+ });
102
+
103
+ // Initialize AI manager
104
+ useEffect(() => {
105
+ const initializeAgent = async () => {
106
+ const callbacks: AgentCallbacks = {
107
+ onMessagesChange: (newMessages) => {
108
+ setMessages([...newMessages]);
109
+ },
110
+ onServersChange: (servers) => {
111
+ setMcpServers([...servers]);
112
+ },
113
+ onSessionIdChange: (sessionId) => {
114
+ setSessionId(sessionId);
115
+ },
116
+ onLatestTotalTokensChange: (tokens) => {
117
+ setlatestTotalTokens(tokens);
118
+ },
119
+ onUserInputHistoryChange: (history) => {
120
+ setUserInputHistory([...history]);
121
+ },
122
+ onCompressionStateChange: (isCompressingState) => {
123
+ setIsCompressing(isCompressingState);
124
+ },
125
+ onShellsChange: (shells) => {
126
+ setBackgroundShells([...shells]);
127
+ },
128
+ };
129
+
130
+ try {
131
+ const agent = await Agent.create({
132
+ callbacks,
133
+ restoreSessionId,
134
+ continueLastSession,
135
+ logger,
136
+ });
137
+
138
+ agentRef.current = agent;
139
+
140
+ // Get initial state
141
+ setSessionId(agent.sessionId);
142
+ setMessages(agent.messages);
143
+ setIsLoading(agent.isLoading);
144
+ setlatestTotalTokens(agent.latestTotalTokens);
145
+ setIsCommandRunning(agent.isCommandRunning);
146
+ setIsCompressing(agent.isCompressing);
147
+ setUserInputHistory(agent.userInputHistory);
148
+
149
+ // Get initial MCP servers state
150
+ const mcpServers = agent.getMcpServers?.() || [];
151
+ setMcpServers(mcpServers);
152
+
153
+ // Get initial commands
154
+ const agentSlashCommands = agent.getSlashCommands?.() || [];
155
+ setSlashCommands(agentSlashCommands);
156
+ } catch (error) {
157
+ console.error("Failed to initialize AI manager:", error);
158
+ }
159
+ };
160
+
161
+ initializeAgent();
162
+ }, [restoreSessionId, continueLastSession]);
163
+
164
+ // Cleanup on unmount
165
+ useEffect(() => {
166
+ return () => {
167
+ if (agentRef.current) {
168
+ agentRef.current.destroy();
169
+ }
170
+ };
171
+ }, []);
172
+
173
+ // Send message function (including judgment logic)
174
+ const sendMessage = useCallback(
175
+ async (
176
+ content: string,
177
+ images?: Array<{ path: string; mimeType: string }>,
178
+ ) => {
179
+ // Check if there's content to send (text content or image attachments)
180
+ const hasTextContent = content.trim();
181
+ const hasImageAttachments = images && images.length > 0;
182
+
183
+ if (!hasTextContent && !hasImageAttachments) return;
184
+
185
+ try {
186
+ // Handle memory mode - check if it's a memory message (starts with # and only one line)
187
+ if (content.startsWith("#") && !content.includes("\n")) {
188
+ const memoryText = content.substring(1).trim();
189
+ if (!memoryText) return;
190
+
191
+ // In memory mode, don't add user message, only wait for user to choose memory type then add assistant message
192
+ // Don't auto-save, wait for user to choose memory type
193
+ return;
194
+ }
195
+
196
+ // Handle bash mode - check if it's a bash command (starts with ! and only one line)
197
+ if (content.startsWith("!") && !content.includes("\n")) {
198
+ const command = content.substring(1).trim();
199
+ if (!command) return;
200
+
201
+ // In bash mode, don't add user message to UI, directly execute command
202
+ // Executing bash command will automatically add assistant message
203
+
204
+ // Set command running state
205
+ setIsCommandRunning(true);
206
+
207
+ try {
208
+ await agentRef.current?.executeBashCommand(command);
209
+ } finally {
210
+ // Clear command running state
211
+ setIsCommandRunning(false);
212
+ }
213
+
214
+ return;
215
+ }
216
+
217
+ // Handle normal AI message and slash commands
218
+ // Slash commands are now handled internally in agent.sendMessage
219
+
220
+ // Set loading state
221
+ setIsLoading(true);
222
+
223
+ try {
224
+ await agentRef.current?.sendMessage(content, images);
225
+ } finally {
226
+ // Clear loading state
227
+ setIsLoading(false);
228
+ }
229
+ } catch (error) {
230
+ console.error("Failed to send message:", error);
231
+ // Loading state will be automatically updated by the useEffect that watches messages
232
+ }
233
+ },
234
+ [],
235
+ );
236
+
237
+ // Unified interrupt method, interrupt both AI messages and command execution
238
+ const abortMessage = useCallback(() => {
239
+ agentRef.current?.abortMessage();
240
+ }, []);
241
+
242
+ // Memory save function - delegate to Agent
243
+ const saveMemory = useCallback(
244
+ async (message: string, type: "project" | "user") => {
245
+ await agentRef.current?.saveMemory(message, type);
246
+ },
247
+ [],
248
+ );
249
+
250
+ // MCP management methods - delegate to Agent
251
+ const connectMcpServer = useCallback(async (serverName: string) => {
252
+ return (await agentRef.current?.connectMcpServer(serverName)) ?? false;
253
+ }, []);
254
+
255
+ const disconnectMcpServer = useCallback(async (serverName: string) => {
256
+ return (await agentRef.current?.disconnectMcpServer(serverName)) ?? false;
257
+ }, []);
258
+
259
+ // Background bash management methods - delegate to Agent
260
+ const getBackgroundShellOutput = useCallback((shellId: string) => {
261
+ if (!agentRef.current) return null;
262
+ return agentRef.current.getBackgroundShellOutput(shellId);
263
+ }, []);
264
+
265
+ const killBackgroundShell = useCallback((shellId: string) => {
266
+ if (!agentRef.current) return false;
267
+ return agentRef.current.killBackgroundShell(shellId);
268
+ }, []);
269
+
270
+ const hasSlashCommand = useCallback((commandId: string) => {
271
+ if (!agentRef.current) return false;
272
+ return agentRef.current.hasSlashCommand(commandId);
273
+ }, []);
274
+
275
+ const contextValue: ChatContextType = {
276
+ messages,
277
+ isLoading,
278
+ isCommandRunning,
279
+ userInputHistory,
280
+ isExpanded,
281
+ sessionId,
282
+ sendMessage,
283
+ abortMessage,
284
+ latestTotalTokens,
285
+ isCompressing,
286
+ saveMemory,
287
+ mcpServers,
288
+ connectMcpServer,
289
+ disconnectMcpServer,
290
+ backgroundShells,
291
+ getBackgroundShellOutput,
292
+ killBackgroundShell,
293
+ slashCommands,
294
+ hasSlashCommand,
295
+ };
296
+
297
+ return (
298
+ <ChatContext.Provider value={contextValue}>{children}</ChatContext.Provider>
299
+ );
300
+ };
@@ -0,0 +1,77 @@
1
+ import { useState, useCallback } from "react";
2
+
3
+ export const useBashHistorySelector = () => {
4
+ const [showBashHistorySelector, setShowBashHistorySelector] = useState(false);
5
+ const [exclamationPosition, setExclamationPosition] = useState(-1);
6
+ const [bashHistorySearchQuery, setBashHistorySearchQuery] = useState("");
7
+
8
+ const activateBashHistorySelector = useCallback((position: number) => {
9
+ setShowBashHistorySelector(true);
10
+ setExclamationPosition(position);
11
+ setBashHistorySearchQuery("");
12
+ }, []);
13
+
14
+ const handleBashHistorySelect = useCallback(
15
+ (command: string, inputText: string, cursorPosition: number) => {
16
+ if (exclamationPosition >= 0) {
17
+ // Replace ! and search query with selected command
18
+ const beforeExclamation = inputText.substring(0, exclamationPosition);
19
+ const afterQuery = inputText.substring(cursorPosition);
20
+ const newInput = beforeExclamation + `!${command}` + afterQuery;
21
+ const newCursorPosition = beforeExclamation.length + command.length + 1;
22
+
23
+ setShowBashHistorySelector(false);
24
+ setExclamationPosition(-1);
25
+ setBashHistorySearchQuery("");
26
+
27
+ return { newInput, newCursorPosition };
28
+ }
29
+ return { newInput: inputText, newCursorPosition: cursorPosition };
30
+ },
31
+ [exclamationPosition],
32
+ );
33
+
34
+ const handleCancelBashHistorySelect = useCallback(() => {
35
+ setShowBashHistorySelector(false);
36
+ setExclamationPosition(-1);
37
+ setBashHistorySearchQuery("");
38
+ }, []);
39
+
40
+ const handleBashHistoryExecute = useCallback((command: string) => {
41
+ setShowBashHistorySelector(false);
42
+ setExclamationPosition(-1);
43
+ setBashHistorySearchQuery("");
44
+ return command; // Return command to execute
45
+ }, []);
46
+
47
+ const updateBashHistorySearchQuery = useCallback((query: string) => {
48
+ setBashHistorySearchQuery(query);
49
+ }, []);
50
+
51
+ const checkForExclamationDeletion = useCallback(
52
+ (cursorPosition: number) => {
53
+ if (showBashHistorySelector && cursorPosition <= exclamationPosition) {
54
+ handleCancelBashHistorySelect();
55
+ return true;
56
+ }
57
+ return false;
58
+ },
59
+ [
60
+ showBashHistorySelector,
61
+ exclamationPosition,
62
+ handleCancelBashHistorySelect,
63
+ ],
64
+ );
65
+
66
+ return {
67
+ showBashHistorySelector,
68
+ bashHistorySearchQuery,
69
+ activateBashHistorySelector,
70
+ handleBashHistorySelect,
71
+ handleBashHistoryExecute,
72
+ handleCancelBashHistorySelect,
73
+ updateBashHistorySearchQuery,
74
+ checkForExclamationDeletion,
75
+ exclamationPosition,
76
+ };
77
+ };
@@ -0,0 +1,131 @@
1
+ import { useState, useCallback } from "react";
2
+
3
+ export interface UseCommandSelectorParams {
4
+ onShowBashManager?: () => void;
5
+ onShowMcpManager?: () => void;
6
+ sendMessage?: (content: string) => Promise<void>; // Use sendMessage instead of executeSlashCommand
7
+ hasSlashCommand?: (commandId: string) => boolean; // Function to check if command exists
8
+ }
9
+
10
+ export const useCommandSelector = ({
11
+ onShowBashManager,
12
+ onShowMcpManager,
13
+ sendMessage,
14
+ hasSlashCommand,
15
+ }: UseCommandSelectorParams) => {
16
+ const [showCommandSelector, setShowCommandSelector] = useState(false);
17
+ const [slashPosition, setSlashPosition] = useState(-1);
18
+ const [commandSearchQuery, setCommandSearchQuery] = useState("");
19
+
20
+ const activateCommandSelector = useCallback((position: number) => {
21
+ setShowCommandSelector(true);
22
+ setSlashPosition(position);
23
+ setCommandSearchQuery("");
24
+ }, []);
25
+
26
+ const handleCommandInsert = useCallback(
27
+ (command: string, inputText: string, cursorPosition: number) => {
28
+ if (slashPosition >= 0) {
29
+ // Replace content from / to current cursor position with /command_name + space
30
+ const beforeSlash = inputText.substring(0, slashPosition);
31
+ const afterQuery = inputText.substring(cursorPosition);
32
+ const newInput = beforeSlash + `/${command} ` + afterQuery;
33
+ const newCursorPosition = beforeSlash.length + command.length + 2; // +2 for "/" and " "
34
+
35
+ setShowCommandSelector(false);
36
+ setSlashPosition(-1);
37
+ setCommandSearchQuery("");
38
+
39
+ return { newInput, newCursorPosition };
40
+ }
41
+ return { newInput: inputText, newCursorPosition: cursorPosition };
42
+ },
43
+ [slashPosition],
44
+ );
45
+
46
+ const handleCommandSelect = useCallback(
47
+ (command: string, inputText: string, cursorPosition: number) => {
48
+ if (slashPosition >= 0) {
49
+ // Replace command part, keep other content
50
+ const beforeSlash = inputText.substring(0, slashPosition);
51
+ const afterQuery = inputText.substring(cursorPosition);
52
+ const newInput = beforeSlash + afterQuery;
53
+ const newCursorPosition = beforeSlash.length;
54
+
55
+ // Execute command asynchronously
56
+ (async () => {
57
+ // First check if it's an agent command
58
+ let commandExecuted = false;
59
+ if (sendMessage && hasSlashCommand && hasSlashCommand(command)) {
60
+ // Execute complete command (replace partial input with complete command name)
61
+ const fullCommand = `/${command}`;
62
+ try {
63
+ await sendMessage(fullCommand);
64
+ commandExecuted = true;
65
+ } catch (error) {
66
+ console.error("Failed to execute slash command:", error);
67
+ }
68
+ }
69
+
70
+ // If not an agent command or execution failed, check local commands
71
+ if (!commandExecuted) {
72
+ if (command === "bashes" && onShowBashManager) {
73
+ onShowBashManager();
74
+ commandExecuted = true;
75
+ } else if (command === "mcp" && onShowMcpManager) {
76
+ onShowMcpManager();
77
+ commandExecuted = true;
78
+ }
79
+ }
80
+ })();
81
+
82
+ setShowCommandSelector(false);
83
+ setSlashPosition(-1);
84
+ setCommandSearchQuery("");
85
+
86
+ return { newInput, newCursorPosition };
87
+ }
88
+ return { newInput: inputText, newCursorPosition: cursorPosition };
89
+ },
90
+ [
91
+ slashPosition,
92
+ onShowBashManager,
93
+ onShowMcpManager,
94
+ sendMessage,
95
+ hasSlashCommand,
96
+ ],
97
+ );
98
+
99
+ const handleCancelCommandSelect = useCallback(() => {
100
+ setShowCommandSelector(false);
101
+ setSlashPosition(-1);
102
+ setCommandSearchQuery("");
103
+ }, []);
104
+
105
+ const updateCommandSearchQuery = useCallback((query: string) => {
106
+ setCommandSearchQuery(query);
107
+ }, []);
108
+
109
+ const checkForSlashDeletion = useCallback(
110
+ (cursorPosition: number) => {
111
+ if (showCommandSelector && cursorPosition <= slashPosition) {
112
+ handleCancelCommandSelect();
113
+ return true;
114
+ }
115
+ return false;
116
+ },
117
+ [showCommandSelector, slashPosition, handleCancelCommandSelect],
118
+ );
119
+
120
+ return {
121
+ showCommandSelector,
122
+ commandSearchQuery,
123
+ activateCommandSelector,
124
+ handleCommandSelect,
125
+ handleCommandInsert,
126
+ handleCancelCommandSelect,
127
+ updateCommandSearchQuery,
128
+ checkForSlashDeletion,
129
+ slashPosition,
130
+ };
131
+ };