@zds-ai/cli 0.1.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 (204) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +497 -0
  3. package/dist/agent/grok-agent.d.ts +250 -0
  4. package/dist/agent/grok-agent.js +2480 -0
  5. package/dist/agent/grok-agent.js.map +1 -0
  6. package/dist/agent/index.d.ts +14 -0
  7. package/dist/agent/index.js +136 -0
  8. package/dist/agent/index.js.map +1 -0
  9. package/dist/commands/mcp.d.ts +2 -0
  10. package/dist/commands/mcp.js +239 -0
  11. package/dist/commands/mcp.js.map +1 -0
  12. package/dist/grok/client.d.ts +55 -0
  13. package/dist/grok/client.js +276 -0
  14. package/dist/grok/client.js.map +1 -0
  15. package/dist/grok/tools.d.ts +8 -0
  16. package/dist/grok/tools.js +878 -0
  17. package/dist/grok/tools.js.map +1 -0
  18. package/dist/hooks/use-enhanced-input.d.ts +38 -0
  19. package/dist/hooks/use-enhanced-input.js +228 -0
  20. package/dist/hooks/use-enhanced-input.js.map +1 -0
  21. package/dist/hooks/use-input-handler.d.ts +36 -0
  22. package/dist/hooks/use-input-handler.js +1099 -0
  23. package/dist/hooks/use-input-handler.js.map +1 -0
  24. package/dist/hooks/use-input-history.d.ts +9 -0
  25. package/dist/hooks/use-input-history.js +61 -0
  26. package/dist/hooks/use-input-history.js.map +1 -0
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.js +869 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/mcp/client.d.ts +41 -0
  31. package/dist/mcp/client.js +224 -0
  32. package/dist/mcp/client.js.map +1 -0
  33. package/dist/mcp/config.d.ts +13 -0
  34. package/dist/mcp/config.js +56 -0
  35. package/dist/mcp/config.js.map +1 -0
  36. package/dist/mcp/transports.d.ts +53 -0
  37. package/dist/mcp/transports.js +256 -0
  38. package/dist/mcp/transports.js.map +1 -0
  39. package/dist/tools/character-tool.d.ts +27 -0
  40. package/dist/tools/character-tool.js +194 -0
  41. package/dist/tools/character-tool.js.map +1 -0
  42. package/dist/tools/clear-cache-tool.d.ts +14 -0
  43. package/dist/tools/clear-cache-tool.js +82 -0
  44. package/dist/tools/clear-cache-tool.js.map +1 -0
  45. package/dist/tools/confirmation-tool.d.ts +16 -0
  46. package/dist/tools/confirmation-tool.js +72 -0
  47. package/dist/tools/confirmation-tool.js.map +1 -0
  48. package/dist/tools/env-tool.d.ts +17 -0
  49. package/dist/tools/env-tool.js +89 -0
  50. package/dist/tools/env-tool.js.map +1 -0
  51. package/dist/tools/file-conversion-tool.d.ts +16 -0
  52. package/dist/tools/file-conversion-tool.js +181 -0
  53. package/dist/tools/file-conversion-tool.js.map +1 -0
  54. package/dist/tools/image-tool.d.ts +22 -0
  55. package/dist/tools/image-tool.js +268 -0
  56. package/dist/tools/image-tool.js.map +1 -0
  57. package/dist/tools/index.d.ts +14 -0
  58. package/dist/tools/index.js +15 -0
  59. package/dist/tools/index.js.map +1 -0
  60. package/dist/tools/internet-tool.d.ts +11 -0
  61. package/dist/tools/internet-tool.js +108 -0
  62. package/dist/tools/internet-tool.js.map +1 -0
  63. package/dist/tools/introspect-tool.d.ts +11 -0
  64. package/dist/tools/introspect-tool.js +243 -0
  65. package/dist/tools/introspect-tool.js.map +1 -0
  66. package/dist/tools/morph-editor.d.ts +38 -0
  67. package/dist/tools/morph-editor.js +318 -0
  68. package/dist/tools/morph-editor.js.map +1 -0
  69. package/dist/tools/restart-tool.d.ts +7 -0
  70. package/dist/tools/restart-tool.js +24 -0
  71. package/dist/tools/restart-tool.js.map +1 -0
  72. package/dist/tools/search.d.ts +71 -0
  73. package/dist/tools/search.js +340 -0
  74. package/dist/tools/search.js.map +1 -0
  75. package/dist/tools/task-tool.d.ts +19 -0
  76. package/dist/tools/task-tool.js +115 -0
  77. package/dist/tools/task-tool.js.map +1 -0
  78. package/dist/tools/text-editor.d.ts +35 -0
  79. package/dist/tools/text-editor.js +669 -0
  80. package/dist/tools/text-editor.js.map +1 -0
  81. package/dist/tools/tool-discovery.d.ts +20 -0
  82. package/dist/tools/tool-discovery.js +45 -0
  83. package/dist/tools/tool-discovery.js.map +1 -0
  84. package/dist/tools/zsh.d.ts +13 -0
  85. package/dist/tools/zsh.js +168 -0
  86. package/dist/tools/zsh.js.map +1 -0
  87. package/dist/types/index.d.ts +31 -0
  88. package/dist/types/index.js +2 -0
  89. package/dist/types/index.js.map +1 -0
  90. package/dist/ui/app.d.ts +7 -0
  91. package/dist/ui/app.js +99 -0
  92. package/dist/ui/app.js.map +1 -0
  93. package/dist/ui/components/active-task-status.d.ts +7 -0
  94. package/dist/ui/components/active-task-status.js +37 -0
  95. package/dist/ui/components/active-task-status.js.map +1 -0
  96. package/dist/ui/components/api-key-input.d.ts +7 -0
  97. package/dist/ui/components/api-key-input.js +80 -0
  98. package/dist/ui/components/api-key-input.js.map +1 -0
  99. package/dist/ui/components/backend-status.d.ts +7 -0
  100. package/dist/ui/components/backend-status.js +85 -0
  101. package/dist/ui/components/backend-status.js.map +1 -0
  102. package/dist/ui/components/chat-history.d.ts +8 -0
  103. package/dist/ui/components/chat-history.js +187 -0
  104. package/dist/ui/components/chat-history.js.map +1 -0
  105. package/dist/ui/components/chat-input.d.ts +9 -0
  106. package/dist/ui/components/chat-input.js +63 -0
  107. package/dist/ui/components/chat-input.js.map +1 -0
  108. package/dist/ui/components/chat-interface.d.ts +9 -0
  109. package/dist/ui/components/chat-interface.js +389 -0
  110. package/dist/ui/components/chat-interface.js.map +1 -0
  111. package/dist/ui/components/command-suggestions.d.ts +17 -0
  112. package/dist/ui/components/command-suggestions.js +22 -0
  113. package/dist/ui/components/command-suggestions.js.map +1 -0
  114. package/dist/ui/components/confirmation-dialog.d.ts +11 -0
  115. package/dist/ui/components/confirmation-dialog.js +105 -0
  116. package/dist/ui/components/confirmation-dialog.js.map +1 -0
  117. package/dist/ui/components/context-status.d.ts +7 -0
  118. package/dist/ui/components/context-status.js +36 -0
  119. package/dist/ui/components/context-status.js.map +1 -0
  120. package/dist/ui/components/diff-renderer.d.ts +13 -0
  121. package/dist/ui/components/diff-renderer.js +206 -0
  122. package/dist/ui/components/diff-renderer.js.map +1 -0
  123. package/dist/ui/components/loading-spinner.d.ts +8 -0
  124. package/dist/ui/components/loading-spinner.js +64 -0
  125. package/dist/ui/components/loading-spinner.js.map +1 -0
  126. package/dist/ui/components/mcp-status.d.ts +5 -0
  127. package/dist/ui/components/mcp-status.js +57 -0
  128. package/dist/ui/components/mcp-status.js.map +1 -0
  129. package/dist/ui/components/model-selection.d.ts +12 -0
  130. package/dist/ui/components/model-selection.js +17 -0
  131. package/dist/ui/components/model-selection.js.map +1 -0
  132. package/dist/ui/components/mood-status.d.ts +7 -0
  133. package/dist/ui/components/mood-status.js +34 -0
  134. package/dist/ui/components/mood-status.js.map +1 -0
  135. package/dist/ui/components/persona-status.d.ts +7 -0
  136. package/dist/ui/components/persona-status.js +34 -0
  137. package/dist/ui/components/persona-status.js.map +1 -0
  138. package/dist/ui/shared/max-sized-box.d.ts +8 -0
  139. package/dist/ui/shared/max-sized-box.js +6 -0
  140. package/dist/ui/shared/max-sized-box.js.map +1 -0
  141. package/dist/ui/utils/code-colorizer.d.ts +2 -0
  142. package/dist/ui/utils/code-colorizer.js +7 -0
  143. package/dist/ui/utils/code-colorizer.js.map +1 -0
  144. package/dist/ui/utils/colors.d.ts +14 -0
  145. package/dist/ui/utils/colors.js +15 -0
  146. package/dist/ui/utils/colors.js.map +1 -0
  147. package/dist/ui/utils/markdown-renderer.d.ts +4 -0
  148. package/dist/ui/utils/markdown-renderer.js +40 -0
  149. package/dist/ui/utils/markdown-renderer.js.map +1 -0
  150. package/dist/utils/auth-helper.d.ts +63 -0
  151. package/dist/utils/auth-helper.js +129 -0
  152. package/dist/utils/auth-helper.js.map +1 -0
  153. package/dist/utils/chat-history-manager-sqlite.d.ts +92 -0
  154. package/dist/utils/chat-history-manager-sqlite.js +334 -0
  155. package/dist/utils/chat-history-manager-sqlite.js.map +1 -0
  156. package/dist/utils/chat-history-manager.d.ts +87 -0
  157. package/dist/utils/chat-history-manager.js +273 -0
  158. package/dist/utils/chat-history-manager.js.map +1 -0
  159. package/dist/utils/chat-history-manager.json-backup.d.ts +69 -0
  160. package/dist/utils/chat-history-manager.json-backup.js +215 -0
  161. package/dist/utils/chat-history-manager.json-backup.js.map +1 -0
  162. package/dist/utils/confirmation-service.d.ts +46 -0
  163. package/dist/utils/confirmation-service.js +165 -0
  164. package/dist/utils/confirmation-service.js.map +1 -0
  165. package/dist/utils/custom-instructions.d.ts +1 -0
  166. package/dist/utils/custom-instructions.js +30 -0
  167. package/dist/utils/custom-instructions.js.map +1 -0
  168. package/dist/utils/database-connection.d.ts +27 -0
  169. package/dist/utils/database-connection.js +81 -0
  170. package/dist/utils/database-connection.js.map +1 -0
  171. package/dist/utils/database-schema.d.ts +17 -0
  172. package/dist/utils/database-schema.js +93 -0
  173. package/dist/utils/database-schema.js.map +1 -0
  174. package/dist/utils/error-logger.d.ts +13 -0
  175. package/dist/utils/error-logger.js +56 -0
  176. package/dist/utils/error-logger.js.map +1 -0
  177. package/dist/utils/hook-executor.d.ts +59 -0
  178. package/dist/utils/hook-executor.js +351 -0
  179. package/dist/utils/hook-executor.js.map +1 -0
  180. package/dist/utils/model-config.d.ts +28 -0
  181. package/dist/utils/model-config.js +42 -0
  182. package/dist/utils/model-config.js.map +1 -0
  183. package/dist/utils/path-utils.d.ts +4 -0
  184. package/dist/utils/path-utils.js +12 -0
  185. package/dist/utils/path-utils.js.map +1 -0
  186. package/dist/utils/settings-manager.d.ts +169 -0
  187. package/dist/utils/settings-manager.js +403 -0
  188. package/dist/utils/settings-manager.js.map +1 -0
  189. package/dist/utils/settings.d.ts +1 -0
  190. package/dist/utils/settings.js +4 -0
  191. package/dist/utils/settings.js.map +1 -0
  192. package/dist/utils/slash-commands.d.ts +25 -0
  193. package/dist/utils/slash-commands.js +454 -0
  194. package/dist/utils/slash-commands.js.map +1 -0
  195. package/dist/utils/startup-hook.d.ts +13 -0
  196. package/dist/utils/startup-hook.js +44 -0
  197. package/dist/utils/startup-hook.js.map +1 -0
  198. package/dist/utils/text-utils.d.ts +80 -0
  199. package/dist/utils/text-utils.js +182 -0
  200. package/dist/utils/text-utils.js.map +1 -0
  201. package/dist/utils/token-counter.d.ts +33 -0
  202. package/dist/utils/token-counter.js +78 -0
  203. package/dist/utils/token-counter.js.map +1 -0
  204. package/package.json +102 -0
@@ -0,0 +1,1099 @@
1
+ import { useState, useMemo, useEffect, useRef } from "react";
2
+ import { useInput } from "ink";
3
+ import { ConfirmationService } from "../utils/confirmation-service.js";
4
+ import { useEnhancedInput } from "./use-enhanced-input.js";
5
+ import { filterCommandSuggestions } from "../ui/components/command-suggestions.js";
6
+ import { loadModelConfig, updateCurrentModel } from "../utils/model-config.js";
7
+ export function useInputHandler({ agent, chatHistory, setChatHistory, setIsProcessing, setIsStreaming, setTokenCount, setTotalTokenUsage, setProcessingTime, processingStartTime, isProcessing, isStreaming, isConfirmationActive = false, totalTokenUsage, }) {
8
+ // Track current token count for accumulation
9
+ const currentTokenCount = useRef(0);
10
+ const [showCommandSuggestions, setShowCommandSuggestions] = useState(false);
11
+ const [selectedCommandIndex, setSelectedCommandIndex] = useState(0);
12
+ const [showModelSelection, setShowModelSelection] = useState(false);
13
+ const [selectedModelIndex, setSelectedModelIndex] = useState(0);
14
+ const [autoEditEnabled, setAutoEditEnabled] = useState(() => {
15
+ const confirmationService = ConfirmationService.getInstance();
16
+ const sessionFlags = confirmationService.getSessionFlags();
17
+ return sessionFlags.allOperations;
18
+ });
19
+ const lastEscapeTime = useRef(0);
20
+ const wasCancelled = useRef(false);
21
+ const handleEscape = () => {
22
+ const now = Date.now();
23
+ if (now - lastEscapeTime.current < 500) {
24
+ // Double ESC: clear input
25
+ setInput("");
26
+ setCursorPosition(0);
27
+ lastEscapeTime.current = 0;
28
+ return;
29
+ }
30
+ lastEscapeTime.current = now;
31
+ // Single ESC: cancel current action or close menus
32
+ if (showCommandSuggestions) {
33
+ setShowCommandSuggestions(false);
34
+ setSelectedCommandIndex(0);
35
+ return;
36
+ }
37
+ if (showModelSelection) {
38
+ setShowModelSelection(false);
39
+ setSelectedModelIndex(0);
40
+ return;
41
+ }
42
+ if (isProcessing || isStreaming) {
43
+ wasCancelled.current = true;
44
+ agent.abortCurrentOperation();
45
+ setIsProcessing(false);
46
+ setIsStreaming(false);
47
+ setTokenCount(0);
48
+ setProcessingTime(0);
49
+ processingStartTime.current = 0;
50
+ return;
51
+ }
52
+ // Otherwise, just return to input (no-op)
53
+ };
54
+ const handleSpecialKey = (key) => {
55
+ // Don't handle input if confirmation dialog is active
56
+ if (isConfirmationActive) {
57
+ return true; // Prevent default handling
58
+ }
59
+ // Handle shift+tab to toggle auto-edit mode
60
+ if (key.shift && key.tab) {
61
+ const newAutoEditState = !autoEditEnabled;
62
+ setAutoEditEnabled(newAutoEditState);
63
+ const confirmationService = ConfirmationService.getInstance();
64
+ if (newAutoEditState) {
65
+ // Enable auto-edit: set all operations to be accepted
66
+ confirmationService.setSessionFlag("allOperations", true);
67
+ }
68
+ else {
69
+ // Disable auto-edit: reset session flags
70
+ confirmationService.resetSession();
71
+ }
72
+ return true; // Handled
73
+ }
74
+ // Handle command suggestions navigation
75
+ if (showCommandSuggestions) {
76
+ const filteredSuggestions = filterCommandSuggestions(commandSuggestions, input);
77
+ if (filteredSuggestions.length === 0) {
78
+ setShowCommandSuggestions(false);
79
+ setSelectedCommandIndex(0);
80
+ return false; // Continue processing
81
+ }
82
+ else {
83
+ if (key.upArrow) {
84
+ setSelectedCommandIndex((prev) => prev === 0 ? filteredSuggestions.length - 1 : prev - 1);
85
+ return true;
86
+ }
87
+ if (key.downArrow) {
88
+ setSelectedCommandIndex((prev) => (prev + 1) % filteredSuggestions.length);
89
+ return true;
90
+ }
91
+ if (key.tab || key.return) {
92
+ // Check if Enter was pressed and input exactly matches a command
93
+ // If so, allow it to submit instead of forcing autocomplete selection
94
+ if (key.return) {
95
+ const exactMatch = commandSuggestions.some((cmd) => cmd.command === input.trim());
96
+ if (exactMatch) {
97
+ // Let Enter submit the command
98
+ setShowCommandSuggestions(false);
99
+ setSelectedCommandIndex(0);
100
+ return false; // Allow normal Enter handling to proceed
101
+ }
102
+ }
103
+ // Otherwise, autocomplete with selected suggestion
104
+ const safeIndex = Math.min(selectedCommandIndex, filteredSuggestions.length - 1);
105
+ const selectedCommand = filteredSuggestions[safeIndex];
106
+ const newInput = selectedCommand.command + " ";
107
+ setInput(newInput);
108
+ setCursorPosition(newInput.length);
109
+ setShowCommandSuggestions(false);
110
+ setSelectedCommandIndex(0);
111
+ return true;
112
+ }
113
+ }
114
+ }
115
+ // Handle model selection navigation
116
+ if (showModelSelection) {
117
+ if (key.upArrow) {
118
+ setSelectedModelIndex((prev) => prev === 0 ? availableModels.length - 1 : prev - 1);
119
+ return true;
120
+ }
121
+ if (key.downArrow) {
122
+ setSelectedModelIndex((prev) => (prev + 1) % availableModels.length);
123
+ return true;
124
+ }
125
+ if (key.tab || key.return) {
126
+ const selectedModel = availableModels[selectedModelIndex];
127
+ agent.setModel(selectedModel.model);
128
+ updateCurrentModel(selectedModel.model);
129
+ const confirmEntry = {
130
+ type: "assistant",
131
+ content: `✓ Switched to model: ${selectedModel.model}`,
132
+ timestamp: new Date(),
133
+ };
134
+ setChatHistory((prev) => [...prev, confirmEntry]);
135
+ setShowModelSelection(false);
136
+ setSelectedModelIndex(0);
137
+ return true;
138
+ }
139
+ }
140
+ return false; // Let default handling proceed
141
+ };
142
+ const handleInputSubmit = async (userInput) => {
143
+ if (userInput === "exit" || userInput === "quit") {
144
+ process.exit(0);
145
+ return;
146
+ }
147
+ // Check for pending context edit confirmation (stored in agent, survives re-renders)
148
+ const pendingEdit = agent.getPendingContextEdit();
149
+ if (pendingEdit) {
150
+ const trimmed = userInput.trim().toLowerCase();
151
+ const { tmpJsonPath, contextFilePath } = pendingEdit;
152
+ if (trimmed === "y" || trimmed === "yes") {
153
+ // User confirmed - replace context
154
+ const fs = await import("fs");
155
+ const { ChatHistoryManager } = await import("../utils/chat-history-manager.js");
156
+ fs.copyFileSync(tmpJsonPath, contextFilePath);
157
+ // Reload context from file
158
+ const historyManager = ChatHistoryManager.getInstance();
159
+ const { systemPrompt: reloadedSystemPrompt, chatHistory: reloadedHistory } = historyManager.loadContext();
160
+ // Update agent's chat history
161
+ agent.setChatHistory(reloadedHistory);
162
+ // Update system prompt - regenerate if empty
163
+ if (reloadedSystemPrompt && reloadedSystemPrompt.trim()) {
164
+ agent.setSystemPrompt(reloadedSystemPrompt);
165
+ }
166
+ else {
167
+ await agent.buildSystemMessage();
168
+ }
169
+ // Sync UI with reloaded context
170
+ setChatHistory(agent.getChatHistory());
171
+ const successEntry = {
172
+ type: "system",
173
+ content: "✓ Context replaced with edited version",
174
+ timestamp: new Date(),
175
+ };
176
+ setChatHistory((prev) => [...prev, successEntry]);
177
+ // Clean up temp file
178
+ try {
179
+ fs.unlinkSync(tmpJsonPath);
180
+ }
181
+ catch (err) {
182
+ // Ignore cleanup errors
183
+ }
184
+ }
185
+ else {
186
+ // User cancelled or said no
187
+ const fs = await import("fs");
188
+ const cancelEntry = {
189
+ type: "system",
190
+ content: "Context edit cancelled",
191
+ timestamp: new Date(),
192
+ };
193
+ setChatHistory((prev) => [...prev, cancelEntry]);
194
+ // Clean up temp file
195
+ try {
196
+ fs.unlinkSync(tmpJsonPath);
197
+ }
198
+ catch (err) {
199
+ // Ignore cleanup errors
200
+ }
201
+ }
202
+ // Clear pending state
203
+ agent.clearPendingContextEdit();
204
+ clearInput();
205
+ return;
206
+ }
207
+ if (userInput.trim()) {
208
+ wasCancelled.current = false;
209
+ const directCommandResult = await handleDirectCommand(userInput);
210
+ if (!directCommandResult) {
211
+ await processUserMessage(userInput);
212
+ }
213
+ }
214
+ };
215
+ const handleInputChange = (newInput) => {
216
+ // Update command suggestions based on input
217
+ if (newInput.startsWith("/")) {
218
+ setShowCommandSuggestions(true);
219
+ setSelectedCommandIndex(0);
220
+ }
221
+ else {
222
+ setShowCommandSuggestions(false);
223
+ setSelectedCommandIndex(0);
224
+ }
225
+ };
226
+ const { input, cursorPosition, setInput, setCursorPosition, clearInput, resetHistory, handleInput, } = useEnhancedInput({
227
+ onSubmit: handleInputSubmit,
228
+ onSpecialKey: handleSpecialKey,
229
+ onEscape: handleEscape,
230
+ onCtrlC: handleEscape,
231
+ disabled: isConfirmationActive,
232
+ });
233
+ // Hook up the actual input handling
234
+ useInput((inputChar, key) => {
235
+ handleInput(inputChar, key);
236
+ });
237
+ // Additional input handler specifically for abort operations (always active)
238
+ useInput((inputChar, key) => {
239
+ // Handle ESC and Ctrl+C during streaming/processing (bypass normal input handling)
240
+ if ((isProcessing || isStreaming) && (key.escape || (key.ctrl && inputChar === "c") || inputChar === "\x03")) {
241
+ handleEscape();
242
+ }
243
+ });
244
+ // Update command suggestions when input changes
245
+ useEffect(() => {
246
+ handleInputChange(input);
247
+ }, [input]);
248
+ const commandSuggestions = [
249
+ { command: "/help", description: "Show help information" },
250
+ { command: "/clear", description: "Clear chat history" },
251
+ { command: "/context", description: "Show context usage info" },
252
+ { command: "/context view", description: "View context in pager" },
253
+ { command: "/context edit", description: "Edit context JSON" },
254
+ { command: "/ink", description: "Switch to Ink UI mode (restart required)" },
255
+ { command: "/introspect", description: "Show available tools" },
256
+ { command: "/models", description: "Switch Grok Model" },
257
+ { command: "/no-ink", description: "Switch to plain console mode (restart required)" },
258
+ { command: "/persona", description: "Set persona text (e.g., /persona debugging red)" },
259
+ { command: "/mood", description: "Set mood text (e.g., /mood focused green)" },
260
+ { command: "/commit-and-push", description: "AI commit & push to remote" },
261
+ { command: "/restart", description: "Restart the application (exit code 51)" },
262
+ { command: "/exit", description: "Exit the application" },
263
+ ];
264
+ // Load models from configuration with fallback to defaults
265
+ const availableModels = useMemo(() => {
266
+ return loadModelConfig(); // Return directly, interface already matches
267
+ }, []);
268
+ const handleDirectCommand = async (input) => {
269
+ const trimmedInput = input.trim();
270
+ // Handle !<command> - execute and add to context, but don't send to LLM
271
+ if (trimmedInput.startsWith("!")) {
272
+ const command = trimmedInput.substring(1).trim();
273
+ if (command) {
274
+ // Add user message to show the command in chat history
275
+ const userEntry = {
276
+ type: "user",
277
+ content: trimmedInput,
278
+ timestamp: new Date(),
279
+ };
280
+ setChatHistory((prev) => [...prev, userEntry]);
281
+ // Execute the command and add result to chat history
282
+ const result = await agent.executeCommand(command, true); // skip confirmation
283
+ const commandEntry = {
284
+ type: "tool_result",
285
+ content: result.success
286
+ ? result.output || "Command completed"
287
+ : result.error || "Command failed",
288
+ timestamp: new Date(),
289
+ toolCall: {
290
+ id: `user_execute_${Date.now()}`,
291
+ type: "function",
292
+ function: {
293
+ name: "execute",
294
+ arguments: JSON.stringify({ command: command }),
295
+ },
296
+ },
297
+ toolResult: result,
298
+ };
299
+ setChatHistory((prev) => [...prev, commandEntry]);
300
+ // Return true - command executed, don't send to LLM as prompt
301
+ return true;
302
+ }
303
+ return true; // Empty command after !
304
+ }
305
+ if (trimmedInput === "/clear") {
306
+ try {
307
+ // Set processing to true temporarily to prevent sync during clear
308
+ setIsProcessing(true);
309
+ // Clear agent's internal context (messages array + chat history)
310
+ await agent.clearCache();
311
+ // Reset UI chat history to match cleared agent state
312
+ setChatHistory(agent.getChatHistory());
313
+ // Reset total token usage
314
+ setTotalTokenUsage(0);
315
+ // Reset processing states
316
+ setIsProcessing(false);
317
+ setIsStreaming(false);
318
+ setTokenCount(0);
319
+ setProcessingTime(0);
320
+ processingStartTime.current = 0;
321
+ // Reset confirmation service session flags
322
+ const confirmationService = ConfirmationService.getInstance();
323
+ confirmationService.resetSession();
324
+ clearInput();
325
+ resetHistory();
326
+ return true;
327
+ }
328
+ catch (error) {
329
+ console.error("Error during /clear command:", error);
330
+ setIsProcessing(false);
331
+ setIsStreaming(false);
332
+ // Show error to user
333
+ const errorEntry = {
334
+ type: "system",
335
+ content: `ERROR: Failed to clear cache: ${error instanceof Error ? error.message : String(error)}`,
336
+ timestamp: new Date(),
337
+ };
338
+ setChatHistory((prev) => [...prev, errorEntry]);
339
+ return true;
340
+ }
341
+ }
342
+ if (trimmedInput.startsWith("/introspect")) {
343
+ const parts = trimmedInput.split(" ");
344
+ const target = parts[1] || "help";
345
+ const toolResult = await agent["introspect"].introspect(target);
346
+ const introspectEntry = {
347
+ type: "assistant",
348
+ content: toolResult.success ? toolResult.output : toolResult.error,
349
+ timestamp: new Date(),
350
+ preserveFormatting: true,
351
+ };
352
+ setChatHistory((prev) => [...prev, introspectEntry]);
353
+ clearInput();
354
+ return true;
355
+ }
356
+ if (trimmedInput === "/help") {
357
+ const helpEntry = {
358
+ type: "assistant",
359
+ content: `Grok CLI Help:
360
+
361
+ Built-in Commands:
362
+ /clear - Clear chat history (current session + persisted)
363
+ /context - Show context usage info
364
+ /context view - View full context in pager (markdown format)
365
+ /context edit - Edit context JSON file (opens in $EDITOR)
366
+ /help - Show this help
367
+ /ink - Switch to Ink UI mode (restart required)
368
+ /introspect - Show available tools (internal and MCP)
369
+ /models - Switch between available models
370
+ /no-ink - Switch to plain console mode (restart required)
371
+ /restart - Restart the application (exit code 51)
372
+ /exit - Exit application
373
+ exit, quit - Exit application
374
+
375
+ CLI Options:
376
+ --fresh - Start with a fresh session (don't load previous history)
377
+
378
+ Git Commands:
379
+ /commit-and-push - AI-generated commit + push to remote
380
+
381
+ Enhanced Input Features:
382
+ ↑/↓ Arrow - Navigate command history
383
+ Ctrl+C - Clear input (press twice to exit)
384
+ Ctrl+D - Exit on blank line
385
+ Ctrl+←/→ - Move by word
386
+ Ctrl+A/E - Move to line start/end
387
+ Ctrl+W - Delete word before cursor
388
+ Ctrl+K - Delete to end of line
389
+ Ctrl+U - Delete to start of line
390
+ ESC - Cancel current action / close menus
391
+ ESC (twice) - Clear input line
392
+ Shift+Tab - Toggle auto-edit mode (bypass confirmations)
393
+
394
+ Direct Commands (executed immediately):
395
+ !command - Execute any shell command directly
396
+ ls [path] - List directory contents
397
+ pwd - Show current directory
398
+ cd <path> - Change directory
399
+ cat <file> - View file contents
400
+ mkdir <dir> - Create directory
401
+ touch <file>- Create empty file
402
+
403
+ Model Configuration:
404
+ Edit ~/.grok/models.json to add custom models (Claude, GPT, Gemini, etc.)
405
+
406
+ History Persistence:
407
+ Chat history is automatically saved and restored between sessions.
408
+ Use /clear to reset both current and persisted history.
409
+
410
+ For complex operations, just describe what you want in natural language.
411
+ Examples:
412
+ "edit package.json and add a new script"
413
+ "create a new React component called Header"
414
+ "show me all TypeScript files in this project"`,
415
+ timestamp: new Date(),
416
+ };
417
+ setChatHistory((prev) => [...prev, helpEntry]);
418
+ clearInput();
419
+ return true;
420
+ }
421
+ if (trimmedInput === "/restart") {
422
+ // Call the restart tool which exits with code 51
423
+ await agent["restartTool"].restart();
424
+ return true; // This line won't be reached but TypeScript needs it
425
+ }
426
+ if (trimmedInput === "/ink") {
427
+ // Already in ink mode - show message
428
+ const alreadyEntry = {
429
+ type: "system",
430
+ content: "You are already in Ink UI mode",
431
+ timestamp: new Date(),
432
+ };
433
+ setChatHistory((prev) => [...prev, alreadyEntry]);
434
+ clearInput();
435
+ return true;
436
+ }
437
+ if (trimmedInput === "/no-ink") {
438
+ // Switch to plain console mode
439
+ const switchEntry = {
440
+ type: "assistant",
441
+ content: "Switching to plain console mode...",
442
+ timestamp: new Date(),
443
+ };
444
+ setChatHistory((prev) => [...prev, switchEntry]);
445
+ // Exit with code 53 - wrapper will add --no-ink and restart
446
+ process.exit(53);
447
+ return true;
448
+ }
449
+ if (trimmedInput === "/exit") {
450
+ process.exit(0);
451
+ return true;
452
+ }
453
+ if (trimmedInput === "/models") {
454
+ setShowModelSelection(true);
455
+ setSelectedModelIndex(0);
456
+ clearInput();
457
+ return true;
458
+ }
459
+ if (trimmedInput.startsWith("/models ")) {
460
+ const modelArg = trimmedInput.split(" ")[1];
461
+ const modelNames = availableModels.map((m) => m.model);
462
+ if (modelNames.includes(modelArg)) {
463
+ agent.setModel(modelArg);
464
+ updateCurrentModel(modelArg); // Update project current model
465
+ const confirmEntry = {
466
+ type: "assistant",
467
+ content: `✓ Switched to model: ${modelArg}`,
468
+ timestamp: new Date(),
469
+ };
470
+ setChatHistory((prev) => [...prev, confirmEntry]);
471
+ }
472
+ else {
473
+ const errorEntry = {
474
+ type: "assistant",
475
+ content: `Invalid model: ${modelArg}
476
+
477
+ Available models: ${modelNames.join(", ")}`,
478
+ timestamp: new Date(),
479
+ };
480
+ setChatHistory((prev) => [...prev, errorEntry]);
481
+ }
482
+ clearInput();
483
+ return true;
484
+ }
485
+ if (trimmedInput.startsWith("/context")) {
486
+ const parts = trimmedInput.split(" ");
487
+ const subcommand = parts[1];
488
+ if (subcommand === "view") {
489
+ // View context as markdown in pager
490
+ try {
491
+ const { spawn } = await import("child_process");
492
+ const fs = await import("fs");
493
+ const path = await import("path");
494
+ // Get context file path
495
+ const { ChatHistoryManager } = await import("../utils/chat-history-manager.js");
496
+ const historyManager = ChatHistoryManager.getInstance();
497
+ const contextFilePath = historyManager.getContextFilePath();
498
+ // Create temp files
499
+ const tmpDir = path.dirname(contextFilePath);
500
+ const tmpMdPath = `${contextFilePath}.md.tmp`;
501
+ // Convert context to markdown
502
+ const markdown = await agent.convertContextToMarkdown();
503
+ fs.writeFileSync(tmpMdPath, markdown, "utf-8");
504
+ // Get viewer command and check if we need to suspend Ink UI
505
+ const settings = await import("../utils/settings-manager.js");
506
+ const settingsManager = settings.getSettingsManager();
507
+ const viewerCommand = settingsManager.getContextViewHelper();
508
+ // Determine if we're in text mode (need to suspend Ink) or GUI mode (don't suspend)
509
+ const inkInstance = global.inkInstance;
510
+ const needsSuspend = inkInstance && !settingsManager.isGuiAvailable();
511
+ if (needsSuspend) {
512
+ // Unmount Ink UI before spawning text-mode viewer
513
+ inkInstance.unmount();
514
+ inkInstance.waitUntilExit();
515
+ }
516
+ // Spawn viewer as blocking process
517
+ const viewerProcess = spawn(viewerCommand, [tmpMdPath], {
518
+ stdio: "inherit",
519
+ shell: true,
520
+ });
521
+ await new Promise((resolve) => {
522
+ viewerProcess.on("close", () => {
523
+ // Clean up temp file
524
+ try {
525
+ fs.unlinkSync(tmpMdPath);
526
+ }
527
+ catch (err) {
528
+ // Ignore cleanup errors
529
+ }
530
+ resolve();
531
+ });
532
+ });
533
+ // Re-render Ink UI after external process exits (only if we suspended it)
534
+ if (needsSuspend) {
535
+ const React = await import("react");
536
+ const { render } = await import("ink");
537
+ const { default: ChatInterface } = await import("../ui/components/chat-interface.js");
538
+ // Clear screen
539
+ process.stdout.write('\x1b[2J\x1b[0f');
540
+ // Re-render with current state (don't reload from file)
541
+ const newInstance = render(React.createElement(ChatInterface, {
542
+ agent,
543
+ initialMessage: undefined,
544
+ fresh: true
545
+ }));
546
+ // Update global instance
547
+ global.inkInstance = newInstance;
548
+ }
549
+ clearInput();
550
+ return true;
551
+ }
552
+ catch (error) {
553
+ const errorEntry = {
554
+ type: "system",
555
+ content: `ERROR: Failed to view context: ${error instanceof Error ? error.message : String(error)}`,
556
+ timestamp: new Date(),
557
+ };
558
+ setChatHistory((prev) => [...prev, errorEntry]);
559
+ clearInput();
560
+ return true;
561
+ }
562
+ }
563
+ else if (subcommand === "edit") {
564
+ // Edit context JSON in editor
565
+ try {
566
+ const { spawn } = await import("child_process");
567
+ const fs = await import("fs");
568
+ const path = await import("path");
569
+ // Get context file path
570
+ const { ChatHistoryManager } = await import("../utils/chat-history-manager.js");
571
+ const historyManager = ChatHistoryManager.getInstance();
572
+ const contextFilePath = historyManager.getContextFilePath();
573
+ // Create temp copy
574
+ const tmpJsonPath = `${contextFilePath}.tmp`;
575
+ fs.copyFileSync(contextFilePath, tmpJsonPath);
576
+ // Get editor command and check if we need to suspend Ink UI
577
+ const settings = await import("../utils/settings-manager.js");
578
+ const settingsManager = settings.getSettingsManager();
579
+ const editorCommand = settingsManager.getContextEditHelper();
580
+ // Determine if we're in text mode (need to suspend Ink) or GUI mode (don't suspend)
581
+ const inkInstance = global.inkInstance;
582
+ const isGui = settingsManager.isGuiAvailable();
583
+ const needsSuspend = inkInstance && !isGui;
584
+ if (needsSuspend) {
585
+ // Unmount Ink UI before spawning text-mode editor
586
+ inkInstance.unmount();
587
+ inkInstance.waitUntilExit();
588
+ }
589
+ // Spawn editor as blocking process
590
+ const editorProcess = spawn(editorCommand, [tmpJsonPath], {
591
+ stdio: "inherit",
592
+ shell: true,
593
+ });
594
+ await new Promise((resolve) => {
595
+ editorProcess.on("close", () => {
596
+ resolve();
597
+ });
598
+ });
599
+ // Validate edited JSON BEFORE re-rendering
600
+ let isValid = false;
601
+ let validationError = "";
602
+ try {
603
+ const editedContent = fs.readFileSync(tmpJsonPath, "utf-8");
604
+ JSON.parse(editedContent); // Will throw if invalid
605
+ isValid = true;
606
+ }
607
+ catch (error) {
608
+ validationError = error instanceof Error ? error.message : String(error);
609
+ }
610
+ if (!isValid) {
611
+ // Re-render Ink UI first (only if we suspended it)
612
+ if (needsSuspend) {
613
+ const React = await import("react");
614
+ const { render } = await import("ink");
615
+ const { default: ChatInterface } = await import("../ui/components/chat-interface.js");
616
+ process.stdout.write('\x1b[2J\x1b[0f');
617
+ const newInstance = render(React.createElement(ChatInterface, {
618
+ agent,
619
+ initialMessage: undefined,
620
+ fresh: true // Don't reload from file
621
+ }));
622
+ global.inkInstance = newInstance;
623
+ }
624
+ const errorEntry = {
625
+ type: "system",
626
+ content: `ERROR: Edited context file contains invalid JSON: ${validationError}`,
627
+ timestamp: new Date(),
628
+ };
629
+ setChatHistory((prev) => [...prev, errorEntry]);
630
+ // Clean up temp file
631
+ try {
632
+ fs.unlinkSync(tmpJsonPath);
633
+ }
634
+ catch (err) {
635
+ // Ignore cleanup errors
636
+ }
637
+ clearInput();
638
+ return true;
639
+ }
640
+ // Store pending edit info in agent (survives re-renders)
641
+ agent.setPendingContextEdit(tmpJsonPath, contextFilePath);
642
+ // Add prompt to both agent history and React state BEFORE re-rendering
643
+ const promptEntry = {
644
+ type: "system",
645
+ content: "Editor closed. Replace context with edited version? (y/n)",
646
+ timestamp: new Date(),
647
+ };
648
+ // Add to agent's history first
649
+ const agentHistory = agent.getChatHistory();
650
+ agent.setChatHistory([...agentHistory, promptEntry]);
651
+ // Re-render Ink UI with updated history (only if we suspended it)
652
+ if (needsSuspend) {
653
+ const React = await import("react");
654
+ const { render } = await import("ink");
655
+ const { default: ChatInterface } = await import("../ui/components/chat-interface.js");
656
+ // Clear screen
657
+ process.stdout.write('\x1b[2J\x1b[0f');
658
+ // Re-render - ChatInterface should not reload from file (we have pending state)
659
+ const newInstance = render(React.createElement(ChatInterface, {
660
+ agent,
661
+ initialMessage: undefined,
662
+ fresh: true // Don't reload from file - use agent's current history
663
+ }));
664
+ // Update global instance
665
+ global.inkInstance = newInstance;
666
+ }
667
+ else {
668
+ // GUI mode or no Ink - just update React state
669
+ setChatHistory(agent.getChatHistory());
670
+ }
671
+ // Don't clean up temp file yet - we need it for confirmation
672
+ // Cleanup will happen after user confirms or denies
673
+ clearInput();
674
+ return true;
675
+ }
676
+ catch (error) {
677
+ const errorEntry = {
678
+ type: "system",
679
+ content: `ERROR: Failed to edit context: ${error instanceof Error ? error.message : String(error)}`,
680
+ timestamp: new Date(),
681
+ };
682
+ setChatHistory((prev) => [...prev, errorEntry]);
683
+ clearInput();
684
+ return true;
685
+ }
686
+ }
687
+ else {
688
+ // Default: show context usage info (redirect to /introspect context)
689
+ const toolResult = await agent["introspect"].introspect("context");
690
+ const contextEntry = {
691
+ type: "assistant",
692
+ content: toolResult.success ? toolResult.output : toolResult.error,
693
+ timestamp: new Date(),
694
+ };
695
+ setChatHistory((prev) => [...prev, contextEntry]);
696
+ clearInput();
697
+ return true;
698
+ }
699
+ }
700
+ if (trimmedInput.startsWith("/persona")) {
701
+ const parts = trimmedInput.split(" ");
702
+ if (parts.length < 2) {
703
+ const helpEntry = {
704
+ type: "assistant",
705
+ content: "Usage: /persona <text> [color]\nExample: /persona debugging red",
706
+ timestamp: new Date(),
707
+ };
708
+ setChatHistory((prev) => [...prev, helpEntry]);
709
+ clearInput();
710
+ return true;
711
+ }
712
+ const persona = parts[1];
713
+ const color = parts[2];
714
+ agent.setPersona(persona, color);
715
+ const confirmEntry = {
716
+ type: "assistant",
717
+ content: `Persona set to: ${persona}${color ? ` (${color})` : ''}`,
718
+ timestamp: new Date(),
719
+ };
720
+ setChatHistory((prev) => [...prev, confirmEntry]);
721
+ clearInput();
722
+ return true;
723
+ }
724
+ if (trimmedInput.startsWith("/mood")) {
725
+ const parts = trimmedInput.split(" ");
726
+ if (parts.length < 2) {
727
+ const helpEntry = {
728
+ type: "assistant",
729
+ content: "Usage: /mood <text> [color]\nExample: /mood focused green",
730
+ timestamp: new Date(),
731
+ };
732
+ setChatHistory((prev) => [...prev, helpEntry]);
733
+ clearInput();
734
+ return true;
735
+ }
736
+ const mood = parts[1];
737
+ const color = parts[2];
738
+ const result = await agent.setMood(mood, color);
739
+ const confirmEntry = {
740
+ type: "assistant",
741
+ content: result.success
742
+ ? `Mood set to: ${mood}${color ? ` (${color})` : ''}`
743
+ : `Failed to set mood: ${result.error || 'Unknown error'}`,
744
+ timestamp: new Date(),
745
+ };
746
+ setChatHistory((prev) => [...prev, confirmEntry]);
747
+ clearInput();
748
+ return true;
749
+ }
750
+ if (trimmedInput === "/commit-and-push") {
751
+ const userEntry = {
752
+ type: "user",
753
+ content: "/commit-and-push",
754
+ timestamp: new Date(),
755
+ };
756
+ setChatHistory((prev) => [...prev, userEntry]);
757
+ setIsProcessing(true);
758
+ setIsStreaming(true);
759
+ try {
760
+ // First check if there are any changes at all
761
+ const initialStatusResult = await agent.executeCommand("git status --porcelain");
762
+ if (!initialStatusResult.success ||
763
+ !initialStatusResult.output?.trim()) {
764
+ const noChangesEntry = {
765
+ type: "assistant",
766
+ content: "No changes to commit. Working directory is clean.",
767
+ timestamp: new Date(),
768
+ };
769
+ setChatHistory((prev) => [...prev, noChangesEntry]);
770
+ setIsProcessing(false);
771
+ setIsStreaming(false);
772
+ setInput("");
773
+ return true;
774
+ }
775
+ // Add all changes
776
+ const addResult = await agent.executeCommand("git add .");
777
+ if (!addResult.success) {
778
+ const addErrorEntry = {
779
+ type: "assistant",
780
+ content: `Failed to stage changes: ${addResult.error || "Unknown error"}`,
781
+ timestamp: new Date(),
782
+ };
783
+ setChatHistory((prev) => [...prev, addErrorEntry]);
784
+ setIsProcessing(false);
785
+ setIsStreaming(false);
786
+ setInput("");
787
+ return true;
788
+ }
789
+ // Show that changes were staged
790
+ const addEntry = {
791
+ type: "tool_result",
792
+ content: "Changes staged successfully",
793
+ timestamp: new Date(),
794
+ toolCall: {
795
+ id: `git_add_${Date.now()}`,
796
+ type: "function",
797
+ function: {
798
+ name: "bash",
799
+ arguments: JSON.stringify({ command: "git add ." }),
800
+ },
801
+ },
802
+ toolResult: addResult,
803
+ };
804
+ setChatHistory((prev) => [...prev, addEntry]);
805
+ // Get staged changes for commit message generation
806
+ const diffResult = await agent.executeCommand("git diff --cached");
807
+ // Generate commit message using AI
808
+ const commitPrompt = `Generate a concise, professional git commit message for these changes:
809
+
810
+ Git Status:
811
+ ${initialStatusResult.output}
812
+
813
+ Git Diff (staged changes):
814
+ ${diffResult.output || "No staged changes shown"}
815
+
816
+ Follow conventional commit format (feat:, fix:, docs:, etc.) and keep it under 72 characters.
817
+ Respond with ONLY the commit message, no additional text.`;
818
+ let commitMessage = "";
819
+ let streamingEntry = null;
820
+ for await (const chunk of agent.processUserMessageStream(commitPrompt)) {
821
+ if (chunk.type === "content" && chunk.content) {
822
+ if (!streamingEntry) {
823
+ const newEntry = {
824
+ type: "assistant",
825
+ content: `Generating commit message...\n\n${chunk.content}`,
826
+ timestamp: new Date(),
827
+ isStreaming: true,
828
+ };
829
+ setChatHistory((prev) => [...prev, newEntry]);
830
+ streamingEntry = newEntry;
831
+ commitMessage = chunk.content;
832
+ }
833
+ else {
834
+ commitMessage += chunk.content;
835
+ setChatHistory((prev) => prev.map((entry, idx) => idx === prev.length - 1 && entry.isStreaming
836
+ ? {
837
+ ...entry,
838
+ content: `Generating commit message...\n\n${commitMessage}`,
839
+ }
840
+ : entry));
841
+ }
842
+ }
843
+ else if (chunk.type === "done") {
844
+ if (streamingEntry) {
845
+ setChatHistory((prev) => prev.map((entry) => entry.isStreaming
846
+ ? {
847
+ ...entry,
848
+ content: `Generated commit message: "${commitMessage.trim()}"`,
849
+ isStreaming: false,
850
+ }
851
+ : entry));
852
+ }
853
+ break;
854
+ }
855
+ }
856
+ // Execute the commit
857
+ const cleanCommitMessage = commitMessage
858
+ .trim()
859
+ .replace(/^["']|["']$/g, "");
860
+ const commitCommand = `git commit -m "${cleanCommitMessage}"`;
861
+ const commitResult = await agent.executeCommand(commitCommand);
862
+ const commitEntry = {
863
+ type: "tool_result",
864
+ content: commitResult.success
865
+ ? commitResult.output || "Commit successful"
866
+ : commitResult.error || "Commit failed",
867
+ timestamp: new Date(),
868
+ toolCall: {
869
+ id: `git_commit_${Date.now()}`,
870
+ type: "function",
871
+ function: {
872
+ name: "bash",
873
+ arguments: JSON.stringify({ command: commitCommand }),
874
+ },
875
+ },
876
+ toolResult: commitResult,
877
+ };
878
+ setChatHistory((prev) => [...prev, commitEntry]);
879
+ // If commit was successful, push to remote
880
+ if (commitResult.success) {
881
+ // First try regular push, if it fails try with upstream setup
882
+ let pushResult = await agent.executeCommand("git push");
883
+ let pushCommand = "git push";
884
+ if (!pushResult.success &&
885
+ pushResult.error?.includes("no upstream branch")) {
886
+ pushCommand = "git push -u origin HEAD";
887
+ pushResult = await agent.executeCommand(pushCommand);
888
+ }
889
+ const pushEntry = {
890
+ type: "tool_result",
891
+ content: pushResult.success
892
+ ? pushResult.output || "Push successful"
893
+ : pushResult.error || "Push failed",
894
+ timestamp: new Date(),
895
+ toolCall: {
896
+ id: `git_push_${Date.now()}`,
897
+ type: "function",
898
+ function: {
899
+ name: "bash",
900
+ arguments: JSON.stringify({ command: pushCommand }),
901
+ },
902
+ },
903
+ toolResult: pushResult,
904
+ };
905
+ setChatHistory((prev) => [...prev, pushEntry]);
906
+ }
907
+ }
908
+ catch (error) {
909
+ const errorEntry = {
910
+ type: "assistant",
911
+ content: `Error during commit and push: ${error.message}`,
912
+ timestamp: new Date(),
913
+ };
914
+ setChatHistory((prev) => [...prev, errorEntry]);
915
+ }
916
+ setIsProcessing(false);
917
+ setIsStreaming(false);
918
+ clearInput();
919
+ return true;
920
+ }
921
+ return false;
922
+ };
923
+ const processUserMessage = async (userInput) => {
924
+ setIsProcessing(true);
925
+ clearInput();
926
+ try {
927
+ setIsStreaming(true);
928
+ let streamingEntry = null;
929
+ let userMessageAdded = false; // Track if we've added the user message
930
+ for await (const chunk of agent.processUserMessageStream(userInput)) {
931
+ // Check if user cancelled - stop immediately
932
+ if (wasCancelled.current) {
933
+ setIsProcessing(false);
934
+ setIsStreaming(false);
935
+ return;
936
+ }
937
+ switch (chunk.type) {
938
+ case "user_message":
939
+ // Add user message to UI immediately when agent yields it
940
+ // Only add if not already in history (prevents duplicates)
941
+ if (chunk.userEntry && !userMessageAdded) {
942
+ setChatHistory((prev) => {
943
+ // Check if this exact message is already in history
944
+ const alreadyExists = prev.some(entry => entry.type === "user" &&
945
+ entry.content === chunk.userEntry.content);
946
+ if (!alreadyExists) {
947
+ userMessageAdded = true;
948
+ return [...prev, chunk.userEntry];
949
+ }
950
+ return prev;
951
+ });
952
+ }
953
+ break;
954
+ case "content":
955
+ if (chunk.content) {
956
+ if (!streamingEntry) {
957
+ const newStreamingEntry = {
958
+ type: "assistant",
959
+ content: chunk.content,
960
+ timestamp: new Date(),
961
+ isStreaming: true,
962
+ };
963
+ setChatHistory((prev) => [...prev, newStreamingEntry]);
964
+ streamingEntry = newStreamingEntry;
965
+ }
966
+ else {
967
+ setChatHistory((prev) => prev.map((entry, idx) => idx === prev.length - 1 && entry.isStreaming
968
+ ? { ...entry, content: (entry.content || "") + chunk.content }
969
+ : entry));
970
+ }
971
+ }
972
+ break;
973
+ case "token_count":
974
+ if (chunk.tokenCount !== undefined) {
975
+ currentTokenCount.current = chunk.tokenCount;
976
+ setTokenCount(chunk.tokenCount);
977
+ }
978
+ break;
979
+ case "tool_calls":
980
+ if (chunk.tool_calls) {
981
+ // Stop streaming for the current assistant message
982
+ setChatHistory((prev) => prev.map((entry) => entry.isStreaming
983
+ ? {
984
+ ...entry,
985
+ isStreaming: false,
986
+ tool_calls: chunk.tool_calls,
987
+ }
988
+ : entry));
989
+ streamingEntry = null;
990
+ // Add individual tool call entries to show tools are being executed
991
+ chunk.tool_calls.forEach((toolCall) => {
992
+ const toolCallEntry = {
993
+ type: "tool_call",
994
+ content: "Executing...",
995
+ timestamp: new Date(),
996
+ toolCall: toolCall,
997
+ };
998
+ setChatHistory((prev) => [...prev, toolCallEntry]);
999
+ });
1000
+ }
1001
+ break;
1002
+ case "tool_result":
1003
+ if (chunk.toolCall && chunk.toolResult) {
1004
+ setChatHistory((prev) => {
1005
+ const updated = prev.map((entry) => {
1006
+ if (entry.isStreaming) {
1007
+ return { ...entry, isStreaming: false };
1008
+ }
1009
+ // Update the existing tool_call entry with the result
1010
+ if (entry.type === "tool_call" &&
1011
+ entry.toolCall?.id === chunk.toolCall?.id) {
1012
+ return {
1013
+ ...entry,
1014
+ type: "tool_result",
1015
+ toolCall: chunk.toolCall, // Use the new toolCall from chunk with complete arguments
1016
+ content: chunk.toolResult.success
1017
+ ? chunk.toolResult.output || "Success"
1018
+ : chunk.toolResult.error || "Error occurred",
1019
+ toolResult: chunk.toolResult,
1020
+ };
1021
+ }
1022
+ return entry;
1023
+ });
1024
+ // Add any system messages that came with this tool result (from hooks)
1025
+ if (chunk.systemMessages && chunk.systemMessages.length > 0) {
1026
+ return [...updated, ...chunk.systemMessages];
1027
+ }
1028
+ return updated;
1029
+ });
1030
+ streamingEntry = null;
1031
+ }
1032
+ break;
1033
+ case "done":
1034
+ if (streamingEntry) {
1035
+ setChatHistory((prev) => prev.map((entry) => entry.isStreaming ? { ...entry, isStreaming: false } : entry));
1036
+ }
1037
+ // Note: System messages are now added immediately with tool_result chunks
1038
+ // Only sync system messages that weren't already added during tool execution
1039
+ const agentHistory = agent.getChatHistory();
1040
+ const agentSystemMessages = agentHistory.filter(e => e.type === "system");
1041
+ setChatHistory((prev) => {
1042
+ // If agent has exactly 1 system message, ensure UI has only that one
1043
+ // This handles system message regeneration (e.g., when model doesn't support tools)
1044
+ if (agentSystemMessages.length === 1) {
1045
+ const firstSystemIndex = prev.findIndex(e => e.type === "system");
1046
+ if (firstSystemIndex >= 0) {
1047
+ // Replace first system message with agent's version, remove any others
1048
+ const nonSystemEntries = prev.filter(e => e.type !== "system");
1049
+ return [
1050
+ ...prev.slice(0, firstSystemIndex),
1051
+ agentSystemMessages[0],
1052
+ ...nonSystemEntries.slice(firstSystemIndex)
1053
+ ];
1054
+ }
1055
+ }
1056
+ // For other cases (multiple system messages from hooks, etc.), use original logic
1057
+ // Find system messages from agent that UI doesn't have
1058
+ const newSystemMessages = agentSystemMessages.filter(agentMsg => !prev.some(uiMsg => uiMsg.type === "system" &&
1059
+ uiMsg.content === agentMsg.content));
1060
+ // Only add if there are truly new messages (e.g., from startup hooks)
1061
+ if (newSystemMessages.length > 0) {
1062
+ return [...prev, ...newSystemMessages];
1063
+ }
1064
+ return prev;
1065
+ });
1066
+ setIsStreaming(false);
1067
+ // Use the tokenCount ref that was set during streaming
1068
+ setTotalTokenUsage(prev => prev + currentTokenCount.current);
1069
+ currentTokenCount.current = 0; // Reset for next request
1070
+ break;
1071
+ }
1072
+ }
1073
+ }
1074
+ catch (error) {
1075
+ const errorEntry = {
1076
+ type: "assistant",
1077
+ content: `Error: ${error.message}`,
1078
+ timestamp: new Date(),
1079
+ };
1080
+ setChatHistory((prev) => [...prev, errorEntry]);
1081
+ setIsStreaming(false);
1082
+ }
1083
+ setIsProcessing(false);
1084
+ processingStartTime.current = 0;
1085
+ };
1086
+ return {
1087
+ input,
1088
+ cursorPosition,
1089
+ showCommandSuggestions,
1090
+ selectedCommandIndex,
1091
+ showModelSelection,
1092
+ selectedModelIndex,
1093
+ commandSuggestions,
1094
+ availableModels,
1095
+ agent,
1096
+ autoEditEnabled,
1097
+ };
1098
+ }
1099
+ //# sourceMappingURL=use-input-handler.js.map