@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.
- package/LICENSE +21 -0
- package/README.md +497 -0
- package/dist/agent/grok-agent.d.ts +250 -0
- package/dist/agent/grok-agent.js +2480 -0
- package/dist/agent/grok-agent.js.map +1 -0
- package/dist/agent/index.d.ts +14 -0
- package/dist/agent/index.js +136 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/commands/mcp.d.ts +2 -0
- package/dist/commands/mcp.js +239 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/grok/client.d.ts +55 -0
- package/dist/grok/client.js +276 -0
- package/dist/grok/client.js.map +1 -0
- package/dist/grok/tools.d.ts +8 -0
- package/dist/grok/tools.js +878 -0
- package/dist/grok/tools.js.map +1 -0
- package/dist/hooks/use-enhanced-input.d.ts +38 -0
- package/dist/hooks/use-enhanced-input.js +228 -0
- package/dist/hooks/use-enhanced-input.js.map +1 -0
- package/dist/hooks/use-input-handler.d.ts +36 -0
- package/dist/hooks/use-input-handler.js +1099 -0
- package/dist/hooks/use-input-handler.js.map +1 -0
- package/dist/hooks/use-input-history.d.ts +9 -0
- package/dist/hooks/use-input-history.js +61 -0
- package/dist/hooks/use-input-history.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +869 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/client.d.ts +41 -0
- package/dist/mcp/client.js +224 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/config.d.ts +13 -0
- package/dist/mcp/config.js +56 -0
- package/dist/mcp/config.js.map +1 -0
- package/dist/mcp/transports.d.ts +53 -0
- package/dist/mcp/transports.js +256 -0
- package/dist/mcp/transports.js.map +1 -0
- package/dist/tools/character-tool.d.ts +27 -0
- package/dist/tools/character-tool.js +194 -0
- package/dist/tools/character-tool.js.map +1 -0
- package/dist/tools/clear-cache-tool.d.ts +14 -0
- package/dist/tools/clear-cache-tool.js +82 -0
- package/dist/tools/clear-cache-tool.js.map +1 -0
- package/dist/tools/confirmation-tool.d.ts +16 -0
- package/dist/tools/confirmation-tool.js +72 -0
- package/dist/tools/confirmation-tool.js.map +1 -0
- package/dist/tools/env-tool.d.ts +17 -0
- package/dist/tools/env-tool.js +89 -0
- package/dist/tools/env-tool.js.map +1 -0
- package/dist/tools/file-conversion-tool.d.ts +16 -0
- package/dist/tools/file-conversion-tool.js +181 -0
- package/dist/tools/file-conversion-tool.js.map +1 -0
- package/dist/tools/image-tool.d.ts +22 -0
- package/dist/tools/image-tool.js +268 -0
- package/dist/tools/image-tool.js.map +1 -0
- package/dist/tools/index.d.ts +14 -0
- package/dist/tools/index.js +15 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/internet-tool.d.ts +11 -0
- package/dist/tools/internet-tool.js +108 -0
- package/dist/tools/internet-tool.js.map +1 -0
- package/dist/tools/introspect-tool.d.ts +11 -0
- package/dist/tools/introspect-tool.js +243 -0
- package/dist/tools/introspect-tool.js.map +1 -0
- package/dist/tools/morph-editor.d.ts +38 -0
- package/dist/tools/morph-editor.js +318 -0
- package/dist/tools/morph-editor.js.map +1 -0
- package/dist/tools/restart-tool.d.ts +7 -0
- package/dist/tools/restart-tool.js +24 -0
- package/dist/tools/restart-tool.js.map +1 -0
- package/dist/tools/search.d.ts +71 -0
- package/dist/tools/search.js +340 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/task-tool.d.ts +19 -0
- package/dist/tools/task-tool.js +115 -0
- package/dist/tools/task-tool.js.map +1 -0
- package/dist/tools/text-editor.d.ts +35 -0
- package/dist/tools/text-editor.js +669 -0
- package/dist/tools/text-editor.js.map +1 -0
- package/dist/tools/tool-discovery.d.ts +20 -0
- package/dist/tools/tool-discovery.js +45 -0
- package/dist/tools/tool-discovery.js.map +1 -0
- package/dist/tools/zsh.d.ts +13 -0
- package/dist/tools/zsh.js +168 -0
- package/dist/tools/zsh.js.map +1 -0
- package/dist/types/index.d.ts +31 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/ui/app.d.ts +7 -0
- package/dist/ui/app.js +99 -0
- package/dist/ui/app.js.map +1 -0
- package/dist/ui/components/active-task-status.d.ts +7 -0
- package/dist/ui/components/active-task-status.js +37 -0
- package/dist/ui/components/active-task-status.js.map +1 -0
- package/dist/ui/components/api-key-input.d.ts +7 -0
- package/dist/ui/components/api-key-input.js +80 -0
- package/dist/ui/components/api-key-input.js.map +1 -0
- package/dist/ui/components/backend-status.d.ts +7 -0
- package/dist/ui/components/backend-status.js +85 -0
- package/dist/ui/components/backend-status.js.map +1 -0
- package/dist/ui/components/chat-history.d.ts +8 -0
- package/dist/ui/components/chat-history.js +187 -0
- package/dist/ui/components/chat-history.js.map +1 -0
- package/dist/ui/components/chat-input.d.ts +9 -0
- package/dist/ui/components/chat-input.js +63 -0
- package/dist/ui/components/chat-input.js.map +1 -0
- package/dist/ui/components/chat-interface.d.ts +9 -0
- package/dist/ui/components/chat-interface.js +389 -0
- package/dist/ui/components/chat-interface.js.map +1 -0
- package/dist/ui/components/command-suggestions.d.ts +17 -0
- package/dist/ui/components/command-suggestions.js +22 -0
- package/dist/ui/components/command-suggestions.js.map +1 -0
- package/dist/ui/components/confirmation-dialog.d.ts +11 -0
- package/dist/ui/components/confirmation-dialog.js +105 -0
- package/dist/ui/components/confirmation-dialog.js.map +1 -0
- package/dist/ui/components/context-status.d.ts +7 -0
- package/dist/ui/components/context-status.js +36 -0
- package/dist/ui/components/context-status.js.map +1 -0
- package/dist/ui/components/diff-renderer.d.ts +13 -0
- package/dist/ui/components/diff-renderer.js +206 -0
- package/dist/ui/components/diff-renderer.js.map +1 -0
- package/dist/ui/components/loading-spinner.d.ts +8 -0
- package/dist/ui/components/loading-spinner.js +64 -0
- package/dist/ui/components/loading-spinner.js.map +1 -0
- package/dist/ui/components/mcp-status.d.ts +5 -0
- package/dist/ui/components/mcp-status.js +57 -0
- package/dist/ui/components/mcp-status.js.map +1 -0
- package/dist/ui/components/model-selection.d.ts +12 -0
- package/dist/ui/components/model-selection.js +17 -0
- package/dist/ui/components/model-selection.js.map +1 -0
- package/dist/ui/components/mood-status.d.ts +7 -0
- package/dist/ui/components/mood-status.js +34 -0
- package/dist/ui/components/mood-status.js.map +1 -0
- package/dist/ui/components/persona-status.d.ts +7 -0
- package/dist/ui/components/persona-status.js +34 -0
- package/dist/ui/components/persona-status.js.map +1 -0
- package/dist/ui/shared/max-sized-box.d.ts +8 -0
- package/dist/ui/shared/max-sized-box.js +6 -0
- package/dist/ui/shared/max-sized-box.js.map +1 -0
- package/dist/ui/utils/code-colorizer.d.ts +2 -0
- package/dist/ui/utils/code-colorizer.js +7 -0
- package/dist/ui/utils/code-colorizer.js.map +1 -0
- package/dist/ui/utils/colors.d.ts +14 -0
- package/dist/ui/utils/colors.js +15 -0
- package/dist/ui/utils/colors.js.map +1 -0
- package/dist/ui/utils/markdown-renderer.d.ts +4 -0
- package/dist/ui/utils/markdown-renderer.js +40 -0
- package/dist/ui/utils/markdown-renderer.js.map +1 -0
- package/dist/utils/auth-helper.d.ts +63 -0
- package/dist/utils/auth-helper.js +129 -0
- package/dist/utils/auth-helper.js.map +1 -0
- package/dist/utils/chat-history-manager-sqlite.d.ts +92 -0
- package/dist/utils/chat-history-manager-sqlite.js +334 -0
- package/dist/utils/chat-history-manager-sqlite.js.map +1 -0
- package/dist/utils/chat-history-manager.d.ts +87 -0
- package/dist/utils/chat-history-manager.js +273 -0
- package/dist/utils/chat-history-manager.js.map +1 -0
- package/dist/utils/chat-history-manager.json-backup.d.ts +69 -0
- package/dist/utils/chat-history-manager.json-backup.js +215 -0
- package/dist/utils/chat-history-manager.json-backup.js.map +1 -0
- package/dist/utils/confirmation-service.d.ts +46 -0
- package/dist/utils/confirmation-service.js +165 -0
- package/dist/utils/confirmation-service.js.map +1 -0
- package/dist/utils/custom-instructions.d.ts +1 -0
- package/dist/utils/custom-instructions.js +30 -0
- package/dist/utils/custom-instructions.js.map +1 -0
- package/dist/utils/database-connection.d.ts +27 -0
- package/dist/utils/database-connection.js +81 -0
- package/dist/utils/database-connection.js.map +1 -0
- package/dist/utils/database-schema.d.ts +17 -0
- package/dist/utils/database-schema.js +93 -0
- package/dist/utils/database-schema.js.map +1 -0
- package/dist/utils/error-logger.d.ts +13 -0
- package/dist/utils/error-logger.js +56 -0
- package/dist/utils/error-logger.js.map +1 -0
- package/dist/utils/hook-executor.d.ts +59 -0
- package/dist/utils/hook-executor.js +351 -0
- package/dist/utils/hook-executor.js.map +1 -0
- package/dist/utils/model-config.d.ts +28 -0
- package/dist/utils/model-config.js +42 -0
- package/dist/utils/model-config.js.map +1 -0
- package/dist/utils/path-utils.d.ts +4 -0
- package/dist/utils/path-utils.js +12 -0
- package/dist/utils/path-utils.js.map +1 -0
- package/dist/utils/settings-manager.d.ts +169 -0
- package/dist/utils/settings-manager.js +403 -0
- package/dist/utils/settings-manager.js.map +1 -0
- package/dist/utils/settings.d.ts +1 -0
- package/dist/utils/settings.js +4 -0
- package/dist/utils/settings.js.map +1 -0
- package/dist/utils/slash-commands.d.ts +25 -0
- package/dist/utils/slash-commands.js +454 -0
- package/dist/utils/slash-commands.js.map +1 -0
- package/dist/utils/startup-hook.d.ts +13 -0
- package/dist/utils/startup-hook.js +44 -0
- package/dist/utils/startup-hook.js.map +1 -0
- package/dist/utils/text-utils.d.ts +80 -0
- package/dist/utils/text-utils.js +182 -0
- package/dist/utils/text-utils.js.map +1 -0
- package/dist/utils/token-counter.d.ts +33 -0
- package/dist/utils/token-counter.js +78 -0
- package/dist/utils/token-counter.js.map +1 -0
- 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
|