codeep 1.0.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 +201 -0
- package/README.md +576 -0
- package/dist/api/index.d.ts +8 -0
- package/dist/api/index.js +421 -0
- package/dist/app.d.ts +2 -0
- package/dist/app.js +1406 -0
- package/dist/components/AgentProgress.d.ts +33 -0
- package/dist/components/AgentProgress.js +97 -0
- package/dist/components/Export.d.ts +8 -0
- package/dist/components/Export.js +27 -0
- package/dist/components/Help.d.ts +2 -0
- package/dist/components/Help.js +3 -0
- package/dist/components/Input.d.ts +9 -0
- package/dist/components/Input.js +89 -0
- package/dist/components/Loading.d.ts +9 -0
- package/dist/components/Loading.js +31 -0
- package/dist/components/Login.d.ts +7 -0
- package/dist/components/Login.js +77 -0
- package/dist/components/Logo.d.ts +8 -0
- package/dist/components/Logo.js +89 -0
- package/dist/components/LogoutPicker.d.ts +8 -0
- package/dist/components/LogoutPicker.js +61 -0
- package/dist/components/Message.d.ts +10 -0
- package/dist/components/Message.js +234 -0
- package/dist/components/MessageList.d.ts +10 -0
- package/dist/components/MessageList.js +8 -0
- package/dist/components/ProjectPermission.d.ts +7 -0
- package/dist/components/ProjectPermission.js +52 -0
- package/dist/components/Search.d.ts +10 -0
- package/dist/components/Search.js +30 -0
- package/dist/components/SessionPicker.d.ts +9 -0
- package/dist/components/SessionPicker.js +88 -0
- package/dist/components/Sessions.d.ts +12 -0
- package/dist/components/Sessions.js +102 -0
- package/dist/components/Settings.d.ts +7 -0
- package/dist/components/Settings.js +162 -0
- package/dist/components/Status.d.ts +2 -0
- package/dist/components/Status.js +12 -0
- package/dist/config/config.test.d.ts +1 -0
- package/dist/config/config.test.js +157 -0
- package/dist/config/index.d.ts +121 -0
- package/dist/config/index.js +555 -0
- package/dist/config/providers.d.ts +43 -0
- package/dist/config/providers.js +82 -0
- package/dist/config/providers.test.d.ts +1 -0
- package/dist/config/providers.test.js +132 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +38 -0
- package/dist/utils/agent.d.ts +37 -0
- package/dist/utils/agent.js +627 -0
- package/dist/utils/codeReview.d.ts +36 -0
- package/dist/utils/codeReview.js +390 -0
- package/dist/utils/context.d.ts +49 -0
- package/dist/utils/context.js +216 -0
- package/dist/utils/diffPreview.d.ts +57 -0
- package/dist/utils/diffPreview.js +335 -0
- package/dist/utils/export.d.ts +19 -0
- package/dist/utils/export.js +94 -0
- package/dist/utils/git.d.ts +85 -0
- package/dist/utils/git.js +399 -0
- package/dist/utils/git.test.d.ts +1 -0
- package/dist/utils/git.test.js +193 -0
- package/dist/utils/history.d.ts +93 -0
- package/dist/utils/history.js +348 -0
- package/dist/utils/interactive.d.ts +34 -0
- package/dist/utils/interactive.js +206 -0
- package/dist/utils/keychain.d.ts +17 -0
- package/dist/utils/keychain.js +160 -0
- package/dist/utils/learning.d.ts +89 -0
- package/dist/utils/learning.js +330 -0
- package/dist/utils/logger.d.ts +33 -0
- package/dist/utils/logger.js +130 -0
- package/dist/utils/project.d.ts +86 -0
- package/dist/utils/project.js +415 -0
- package/dist/utils/project.test.d.ts +1 -0
- package/dist/utils/project.test.js +212 -0
- package/dist/utils/ratelimit.d.ts +26 -0
- package/dist/utils/ratelimit.js +132 -0
- package/dist/utils/ratelimit.test.d.ts +1 -0
- package/dist/utils/ratelimit.test.js +131 -0
- package/dist/utils/retry.d.ts +28 -0
- package/dist/utils/retry.js +109 -0
- package/dist/utils/retry.test.d.ts +1 -0
- package/dist/utils/retry.test.js +163 -0
- package/dist/utils/search.d.ts +11 -0
- package/dist/utils/search.js +29 -0
- package/dist/utils/shell.d.ts +45 -0
- package/dist/utils/shell.js +242 -0
- package/dist/utils/skills.d.ts +144 -0
- package/dist/utils/skills.js +1137 -0
- package/dist/utils/smartContext.d.ts +29 -0
- package/dist/utils/smartContext.js +441 -0
- package/dist/utils/tools.d.ts +224 -0
- package/dist/utils/tools.js +731 -0
- package/dist/utils/update.d.ts +22 -0
- package/dist/utils/update.js +128 -0
- package/dist/utils/validation.d.ts +28 -0
- package/dist/utils/validation.js +141 -0
- package/dist/utils/validation.test.d.ts +1 -0
- package/dist/utils/validation.test.js +164 -0
- package/dist/utils/verify.d.ts +78 -0
- package/dist/utils/verify.js +464 -0
- package/package.json +68 -0
package/dist/app.js
ADDED
|
@@ -0,0 +1,1406 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
4
|
+
import clipboardy from 'clipboardy';
|
|
5
|
+
import { logger } from './utils/logger';
|
|
6
|
+
import { Logo, IntroAnimation } from './components/Logo';
|
|
7
|
+
import { Loading } from './components/Loading';
|
|
8
|
+
import { getCodeBlock, clearCodeBlocks } from './components/Message';
|
|
9
|
+
import { ChatInput } from './components/Input';
|
|
10
|
+
import { Help } from './components/Help';
|
|
11
|
+
import { Status } from './components/Status';
|
|
12
|
+
import { Login } from './components/Login';
|
|
13
|
+
import { Sessions } from './components/Sessions';
|
|
14
|
+
import { SessionPicker } from './components/SessionPicker';
|
|
15
|
+
import { LogoutPicker } from './components/LogoutPicker';
|
|
16
|
+
import { Settings } from './components/Settings';
|
|
17
|
+
import { ProjectPermission } from './components/ProjectPermission';
|
|
18
|
+
import { Search } from './components/Search';
|
|
19
|
+
import { Export } from './components/Export';
|
|
20
|
+
import { MessageList } from './components/MessageList';
|
|
21
|
+
import { chat } from './api/index';
|
|
22
|
+
import { config, loadApiKey, loadAllApiKeys, PROTOCOLS, LANGUAGES, autoSaveSession, startNewSession, getCurrentSessionId, renameSession, deleteSession, hasReadPermission, hasWritePermission, setProjectPermission, setProvider, getCurrentProvider, getModelsForCurrentProvider, PROVIDERS } from './config/index';
|
|
23
|
+
import { getProviderList } from './config/providers';
|
|
24
|
+
import { isProjectDirectory, getProjectContext, detectFilePaths, readProjectFile, parseFileChanges, writeProjectFile, deleteProjectFile } from './utils/project';
|
|
25
|
+
import { logStartup, setLogProjectPath } from './utils/logger';
|
|
26
|
+
import { searchMessages } from './utils/search';
|
|
27
|
+
import { exportMessages, saveExport } from './utils/export';
|
|
28
|
+
import { checkForUpdates, formatVersionInfo, getCurrentVersion } from './utils/update';
|
|
29
|
+
import { getGitDiff, getGitStatus, suggestCommitMessage, formatDiffForDisplay } from './utils/git';
|
|
30
|
+
import { validateInput } from './utils/validation';
|
|
31
|
+
import { checkApiRateLimit, checkCommandRateLimit } from './utils/ratelimit';
|
|
32
|
+
import { runAgent, formatAgentResult, undoLastAction, undoAllActions, getRecentSessions } from './utils/agent';
|
|
33
|
+
import { autoCommitAgentChanges } from './utils/git';
|
|
34
|
+
import { saveContext, loadContext, clearContext, mergeContext } from './utils/context';
|
|
35
|
+
import { performCodeReview, formatReviewResult } from './utils/codeReview';
|
|
36
|
+
import { learnFromProject, addCustomRule, getLearningStatus } from './utils/learning';
|
|
37
|
+
import { getAllSkills, findSkill, formatSkillsList, formatSkillHelp, generateSkillPrompt, saveCustomSkill, deleteCustomSkill, parseSkillChain, parseSkillArgs, searchSkills, trackSkillUsage, getSkillStats } from './utils/skills';
|
|
38
|
+
import { AgentProgress, AgentSummary } from './components/AgentProgress';
|
|
39
|
+
export const App = () => {
|
|
40
|
+
const { exit } = useApp();
|
|
41
|
+
const { stdout } = useStdout();
|
|
42
|
+
// Start with 'chat' screen, will switch to login if needed after loading API key
|
|
43
|
+
const [screen, setScreen] = useState('chat');
|
|
44
|
+
const [messages, setMessages] = useState([]);
|
|
45
|
+
const [inputHistory, setInputHistory] = useState([]);
|
|
46
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
47
|
+
const [streamingContent, setStreamingContent] = useState('');
|
|
48
|
+
const [notification, setNotification] = useState('');
|
|
49
|
+
const [abortController, setAbortController] = useState(null);
|
|
50
|
+
const [sessionId, setSessionId] = useState(getCurrentSessionId());
|
|
51
|
+
const [showIntro, setShowIntro] = useState(true);
|
|
52
|
+
const [clearInputTrigger, setClearInputTrigger] = useState(0);
|
|
53
|
+
// Project context
|
|
54
|
+
const [projectPath] = useState(process.cwd());
|
|
55
|
+
// Log application startup and set project path for logging
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
logStartup('1.0.0');
|
|
58
|
+
setLogProjectPath(projectPath);
|
|
59
|
+
}, [projectPath]);
|
|
60
|
+
const [projectContext, setProjectContext] = useState(null);
|
|
61
|
+
const [hasProjectAccess, setHasProjectAccess] = useState(false);
|
|
62
|
+
const [hasWriteAccess, setHasWriteAccess] = useState(false);
|
|
63
|
+
const [permissionChecked, setPermissionChecked] = useState(false);
|
|
64
|
+
// Load previous session on startup (after intro)
|
|
65
|
+
const [sessionLoaded, setSessionLoaded] = useState(false);
|
|
66
|
+
// Search state
|
|
67
|
+
const [searchResults, setSearchResults] = useState([]);
|
|
68
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
69
|
+
const [exportFormat, setExportFormat] = useState('md');
|
|
70
|
+
// Removed pagination state - terminal handles scrolling natively
|
|
71
|
+
// Update check state
|
|
72
|
+
const [updateInfo, setUpdateInfo] = useState(null);
|
|
73
|
+
// File changes prompt state
|
|
74
|
+
const [pendingFileChanges, setPendingFileChanges] = useState([]);
|
|
75
|
+
// Agent mode state
|
|
76
|
+
const [isAgentRunning, setIsAgentRunning] = useState(false);
|
|
77
|
+
const [agentIteration, setAgentIteration] = useState(0);
|
|
78
|
+
const [agentActions, setAgentActions] = useState([]);
|
|
79
|
+
const [agentThinking, setAgentThinking] = useState('');
|
|
80
|
+
const [agentResult, setAgentResult] = useState(null);
|
|
81
|
+
const [agentDryRun, setAgentDryRun] = useState(false);
|
|
82
|
+
// Load API keys for ALL providers on startup and check if current provider is configured
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
loadAllApiKeys().then(() => {
|
|
85
|
+
// After loading all keys, check if current provider has an API key
|
|
86
|
+
return loadApiKey();
|
|
87
|
+
}).then(key => {
|
|
88
|
+
if (!key || key.length === 0) {
|
|
89
|
+
setScreen('login');
|
|
90
|
+
}
|
|
91
|
+
// else: stay on chat screen (default)
|
|
92
|
+
}).catch(() => {
|
|
93
|
+
setScreen('login');
|
|
94
|
+
});
|
|
95
|
+
}, []);
|
|
96
|
+
// Check project permission after intro
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (!showIntro && !permissionChecked && screen !== 'login') {
|
|
99
|
+
const isProject = isProjectDirectory(projectPath);
|
|
100
|
+
if (isProject) {
|
|
101
|
+
const hasRead = hasReadPermission(projectPath);
|
|
102
|
+
if (hasRead) {
|
|
103
|
+
// Already has permission, load context
|
|
104
|
+
setHasProjectAccess(true);
|
|
105
|
+
const hasWrite = hasWritePermission(projectPath);
|
|
106
|
+
setHasWriteAccess(hasWrite);
|
|
107
|
+
const ctx = getProjectContext(projectPath);
|
|
108
|
+
if (ctx) {
|
|
109
|
+
ctx.hasWriteAccess = hasWrite;
|
|
110
|
+
}
|
|
111
|
+
setProjectContext(ctx);
|
|
112
|
+
setPermissionChecked(true);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// Need to ask for permission
|
|
116
|
+
setScreen('permission');
|
|
117
|
+
setPermissionChecked(true);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
setPermissionChecked(true);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}, [showIntro, permissionChecked, projectPath, screen]);
|
|
125
|
+
// Show session picker after permission is handled (instead of auto-loading)
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (!showIntro && permissionChecked && !sessionLoaded && screen !== 'permission' && screen !== 'login') {
|
|
128
|
+
// If we already have messages (e.g., from a previous action), skip picker
|
|
129
|
+
if (messages.length > 0) {
|
|
130
|
+
setSessionLoaded(true);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// Show session picker instead of auto-loading
|
|
134
|
+
setScreen('session-picker');
|
|
135
|
+
}
|
|
136
|
+
}, [showIntro, permissionChecked, sessionLoaded, screen, messages.length]);
|
|
137
|
+
// Check for updates on startup (once per session, after intro)
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (!showIntro && sessionLoaded && !updateInfo) {
|
|
140
|
+
checkForUpdates()
|
|
141
|
+
.then((info) => {
|
|
142
|
+
setUpdateInfo(info);
|
|
143
|
+
if (info.hasUpdate) {
|
|
144
|
+
setNotification(`Update available: ${info.current} → ${info.latest}. Type /update for info.`);
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
.catch(() => {
|
|
148
|
+
// Silent fail - update check is non-critical
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}, [showIntro, sessionLoaded, updateInfo]);
|
|
152
|
+
// Clear notification after delay
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (notification) {
|
|
155
|
+
const timer = setTimeout(() => setNotification(''), 3000);
|
|
156
|
+
return () => clearTimeout(timer);
|
|
157
|
+
}
|
|
158
|
+
}, [notification]);
|
|
159
|
+
// Handle keyboard shortcuts
|
|
160
|
+
useInput((input, key) => {
|
|
161
|
+
// Ctrl+L to clear chat (F5 doesn't work reliably in all terminals)
|
|
162
|
+
if (key.ctrl && input === 'l') {
|
|
163
|
+
if (!isLoading && screen === 'chat') {
|
|
164
|
+
// Clear terminal screen
|
|
165
|
+
stdout?.write('\x1b[2J\x1b[H');
|
|
166
|
+
setMessages([]);
|
|
167
|
+
clearCodeBlocks();
|
|
168
|
+
const newSessId = startNewSession();
|
|
169
|
+
setSessionId(newSessId);
|
|
170
|
+
setClearInputTrigger(prev => prev + 1); // Trigger input clear
|
|
171
|
+
notify('Chat cleared, new session started');
|
|
172
|
+
}
|
|
173
|
+
return; // Prevent further processing
|
|
174
|
+
}
|
|
175
|
+
// Escape to cancel agent or request
|
|
176
|
+
if (key.escape && isAgentRunning) {
|
|
177
|
+
abortController?.abort();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// Escape to cancel request
|
|
181
|
+
if (key.escape && isLoading) {
|
|
182
|
+
abortController?.abort();
|
|
183
|
+
setIsLoading(false);
|
|
184
|
+
setAbortController(null);
|
|
185
|
+
setClearInputTrigger(prev => prev + 1); // Clear input after cancel
|
|
186
|
+
// Save partial response if there is any
|
|
187
|
+
if (streamingContent && streamingContent.trim().length > 0) {
|
|
188
|
+
const partialMessage = {
|
|
189
|
+
role: 'assistant',
|
|
190
|
+
content: streamingContent.trim() + '\n\n*(Response cancelled - partial)*',
|
|
191
|
+
};
|
|
192
|
+
setMessages(prev => [...prev, partialMessage]);
|
|
193
|
+
setStreamingContent('');
|
|
194
|
+
notify('Request cancelled - partial response saved');
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
// No content yet, remove the user message
|
|
198
|
+
setMessages(prev => prev.slice(0, -1));
|
|
199
|
+
setStreamingContent('');
|
|
200
|
+
notify('Request cancelled');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Escape to close modals
|
|
204
|
+
if (key.escape && screen !== 'chat' && screen !== 'login') {
|
|
205
|
+
setScreen('chat');
|
|
206
|
+
}
|
|
207
|
+
// Handle file changes prompt (Y/n)
|
|
208
|
+
if (pendingFileChanges.length > 0 && !isLoading) {
|
|
209
|
+
if (input.toLowerCase() === 'y' || key.return) {
|
|
210
|
+
// Apply changes
|
|
211
|
+
let applied = 0;
|
|
212
|
+
for (const change of pendingFileChanges) {
|
|
213
|
+
let result;
|
|
214
|
+
if (change.action === 'delete') {
|
|
215
|
+
result = deleteProjectFile(change.path);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
result = writeProjectFile(change.path, change.content);
|
|
219
|
+
}
|
|
220
|
+
if (result.success) {
|
|
221
|
+
applied++;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
notify(`Error: ${result.error || 'Failed to apply change'}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
notify(`Applied ${applied}/${pendingFileChanges.length} file change(s)`);
|
|
228
|
+
setPendingFileChanges([]);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (input.toLowerCase() === 'n' || key.escape) {
|
|
232
|
+
// Reject changes
|
|
233
|
+
notify('File changes rejected');
|
|
234
|
+
setPendingFileChanges([]);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
const notify = useCallback((msg) => {
|
|
240
|
+
setNotification(msg);
|
|
241
|
+
}, []);
|
|
242
|
+
// Start agent execution
|
|
243
|
+
const startAgent = useCallback(async (prompt, dryRun = false) => {
|
|
244
|
+
if (!projectContext) {
|
|
245
|
+
notify('Agent mode requires project context. Run in a project directory.');
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (!hasWriteAccess && !dryRun) {
|
|
249
|
+
notify('Agent mode requires write access. Grant permission first or use /agent-dry');
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
// Reset agent state
|
|
253
|
+
setIsAgentRunning(true);
|
|
254
|
+
setAgentIteration(0);
|
|
255
|
+
setAgentActions([]);
|
|
256
|
+
setAgentThinking('');
|
|
257
|
+
setAgentResult(null);
|
|
258
|
+
setAgentDryRun(dryRun);
|
|
259
|
+
// Add user message
|
|
260
|
+
const userMessage = {
|
|
261
|
+
role: 'user',
|
|
262
|
+
content: dryRun ? `[DRY RUN] ${prompt}` : `[AGENT] ${prompt}`
|
|
263
|
+
};
|
|
264
|
+
setMessages(prev => [...prev, userMessage]);
|
|
265
|
+
const controller = new AbortController();
|
|
266
|
+
setAbortController(controller);
|
|
267
|
+
try {
|
|
268
|
+
const result = await runAgent(prompt, projectContext, {
|
|
269
|
+
maxIterations: 20,
|
|
270
|
+
maxDuration: 5 * 60 * 1000, // 5 minutes
|
|
271
|
+
dryRun,
|
|
272
|
+
onIteration: (iteration, message) => {
|
|
273
|
+
setAgentIteration(iteration);
|
|
274
|
+
},
|
|
275
|
+
onToolCall: (tool) => {
|
|
276
|
+
setAgentActions(prev => [...prev, {
|
|
277
|
+
type: tool.tool,
|
|
278
|
+
target: tool.parameters.path || tool.parameters.command || 'unknown',
|
|
279
|
+
result: 'success', // Will be updated by onToolResult
|
|
280
|
+
timestamp: Date.now(),
|
|
281
|
+
}]);
|
|
282
|
+
},
|
|
283
|
+
onToolResult: (result) => {
|
|
284
|
+
setAgentActions(prev => {
|
|
285
|
+
const updated = [...prev];
|
|
286
|
+
if (updated.length > 0) {
|
|
287
|
+
updated[updated.length - 1].result = result.success ? 'success' : 'error';
|
|
288
|
+
updated[updated.length - 1].details = result.success ? result.output.slice(0, 100) : result.error;
|
|
289
|
+
}
|
|
290
|
+
return updated;
|
|
291
|
+
});
|
|
292
|
+
},
|
|
293
|
+
onThinking: (text) => {
|
|
294
|
+
setAgentThinking(prev => prev + text);
|
|
295
|
+
},
|
|
296
|
+
abortSignal: controller.signal,
|
|
297
|
+
});
|
|
298
|
+
setAgentResult(result);
|
|
299
|
+
// Add agent summary as assistant message
|
|
300
|
+
const summaryMessage = {
|
|
301
|
+
role: 'assistant',
|
|
302
|
+
content: result.finalResponse || formatAgentResult(result),
|
|
303
|
+
};
|
|
304
|
+
setMessages(prev => [...prev, summaryMessage]);
|
|
305
|
+
// Auto-save session
|
|
306
|
+
autoSaveSession([...messages, userMessage, summaryMessage], projectPath);
|
|
307
|
+
if (result.success) {
|
|
308
|
+
notify(`Agent completed: ${result.actions.length} action(s)`);
|
|
309
|
+
}
|
|
310
|
+
else if (result.aborted) {
|
|
311
|
+
notify('Agent stopped by user');
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
notify(`Agent failed: ${result.error}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
const err = error;
|
|
319
|
+
notify(`Agent error: ${err.message}`);
|
|
320
|
+
}
|
|
321
|
+
finally {
|
|
322
|
+
setIsAgentRunning(false);
|
|
323
|
+
setAbortController(null);
|
|
324
|
+
setAgentThinking('');
|
|
325
|
+
}
|
|
326
|
+
}, [projectContext, hasWriteAccess, messages, projectPath, notify]);
|
|
327
|
+
const handleSubmit = async (input) => {
|
|
328
|
+
logger.debug(`[handleSubmit] Called with input, current messages.length: ${messages.length}`);
|
|
329
|
+
// Clear previous agent result when user sends new message
|
|
330
|
+
if (agentResult) {
|
|
331
|
+
setAgentResult(null);
|
|
332
|
+
setAgentActions([]);
|
|
333
|
+
}
|
|
334
|
+
// Validate input
|
|
335
|
+
const validation = validateInput(input);
|
|
336
|
+
if (!validation.valid) {
|
|
337
|
+
notify(`Invalid input: ${validation.error}`);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
// Use sanitized input
|
|
341
|
+
const sanitizedInput = validation.sanitized || input;
|
|
342
|
+
// Add to input history (limit to last 100 entries to prevent memory leak)
|
|
343
|
+
const MAX_HISTORY = 100;
|
|
344
|
+
setInputHistory(h => [...h.slice(-(MAX_HISTORY - 1)), sanitizedInput]);
|
|
345
|
+
// Check for commands
|
|
346
|
+
if (sanitizedInput.startsWith('/')) {
|
|
347
|
+
// Rate limit commands
|
|
348
|
+
const commandLimit = checkCommandRateLimit();
|
|
349
|
+
if (!commandLimit.allowed) {
|
|
350
|
+
notify(commandLimit.message || 'Too many commands');
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
handleCommand(sanitizedInput);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
// Rate limit API calls
|
|
357
|
+
const apiLimit = checkApiRateLimit();
|
|
358
|
+
if (!apiLimit.allowed) {
|
|
359
|
+
notify(apiLimit.message || 'Rate limit exceeded');
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
// Auto-agent mode: if enabled and we have write access, use agent
|
|
363
|
+
const agentMode = config.get('agentMode');
|
|
364
|
+
logger.debug(`[handleSubmit] agentMode=${agentMode}, hasWriteAccess=${hasWriteAccess}, hasProjectContext=${!!projectContext}`);
|
|
365
|
+
if (agentMode === 'auto' && hasWriteAccess && projectContext) {
|
|
366
|
+
notify('Using agent mode (change in /settings)');
|
|
367
|
+
startAgent(sanitizedInput, false);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
// Auto-detect file paths and enrich message
|
|
371
|
+
let enrichedInput = sanitizedInput;
|
|
372
|
+
if (hasProjectAccess) {
|
|
373
|
+
const detectedPaths = detectFilePaths(sanitizedInput, projectPath);
|
|
374
|
+
if (detectedPaths.length > 0) {
|
|
375
|
+
const fileContents = [];
|
|
376
|
+
for (const filePath of detectedPaths) {
|
|
377
|
+
const file = readProjectFile(filePath);
|
|
378
|
+
if (file) {
|
|
379
|
+
const ext = filePath.split('.').pop() || '';
|
|
380
|
+
fileContents.push(`\n\n--- File: ${filePath} ---\n\`\`\`${ext}\n${file.content}\n\`\`\``);
|
|
381
|
+
if (file.truncated) {
|
|
382
|
+
notify(`Note: ${filePath} was truncated (too large)`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (fileContents.length > 0) {
|
|
387
|
+
enrichedInput = input + fileContents.join('');
|
|
388
|
+
notify(`Attached ${fileContents.length} file(s)`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// Regular message
|
|
393
|
+
const userMessage = { role: 'user', content: enrichedInput };
|
|
394
|
+
// Display sanitized input to user, but send enriched
|
|
395
|
+
const displayMessage = { role: 'user', content: sanitizedInput };
|
|
396
|
+
// Create updated messages array with user message
|
|
397
|
+
const messagesWithUser = [...messages, displayMessage];
|
|
398
|
+
logger.debug(`[handleSubmit] Current messages: ${messages.length}`);
|
|
399
|
+
logger.debug(`[handleSubmit] Messages with user: ${messagesWithUser.length}`);
|
|
400
|
+
setMessages(messagesWithUser);
|
|
401
|
+
setIsLoading(true);
|
|
402
|
+
setStreamingContent('');
|
|
403
|
+
const controller = new AbortController();
|
|
404
|
+
setAbortController(controller);
|
|
405
|
+
try {
|
|
406
|
+
// Clean agent markers from history to prevent model confusion
|
|
407
|
+
// When switching from agent to manual mode, history may contain [AGENT] prefixes
|
|
408
|
+
const cleanedHistory = messages.map(msg => {
|
|
409
|
+
if (msg.role === 'user' && (msg.content.startsWith('[AGENT] ') || msg.content.startsWith('[DRY RUN] '))) {
|
|
410
|
+
return {
|
|
411
|
+
...msg,
|
|
412
|
+
content: msg.content.replace(/^\[(AGENT|DRY RUN)\] /, ''),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
return msg;
|
|
416
|
+
});
|
|
417
|
+
logger.debug(`[handleSubmit] Calling chat API with messages.length: ${cleanedHistory.length}`);
|
|
418
|
+
const response = await chat(enrichedInput, cleanedHistory, // Send cleaned conversation history WITHOUT the user message we just added
|
|
419
|
+
(chunk) => {
|
|
420
|
+
// Don't update streaming content if request was aborted
|
|
421
|
+
if (!controller.signal.aborted) {
|
|
422
|
+
setStreamingContent(c => c + chunk);
|
|
423
|
+
}
|
|
424
|
+
}, undefined, projectContext, controller.signal);
|
|
425
|
+
logger.debug(`[handleSubmit] Response received, length: ${response?.length || 0}`);
|
|
426
|
+
logger.debug(`[handleSubmit] Controller aborted? ${controller.signal.aborted}`);
|
|
427
|
+
// Check if request was aborted before updating messages
|
|
428
|
+
if (!controller.signal.aborted) {
|
|
429
|
+
const finalMessages = [...messagesWithUser, { role: 'assistant', content: response }];
|
|
430
|
+
logger.debug(`[handleSubmit] Final messages array length: ${finalMessages.length}`);
|
|
431
|
+
setMessages(finalMessages);
|
|
432
|
+
// Check for file changes in response if write access enabled
|
|
433
|
+
if (hasWriteAccess && response) {
|
|
434
|
+
const fileChanges = parseFileChanges(response);
|
|
435
|
+
if (fileChanges.length > 0) {
|
|
436
|
+
setPendingFileChanges(fileChanges);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// Auto-save session
|
|
440
|
+
autoSaveSession(finalMessages, projectPath);
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
// Revert to messages without user input on abort
|
|
444
|
+
setMessages(messages);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
// Revert to messages without user input on error
|
|
449
|
+
setMessages(messages);
|
|
450
|
+
// Don't show error if request was aborted by user
|
|
451
|
+
const err = error;
|
|
452
|
+
const isAborted = err.name === 'AbortError' ||
|
|
453
|
+
err.message?.includes('aborted') ||
|
|
454
|
+
err.message?.includes('abort') ||
|
|
455
|
+
controller.signal.aborted;
|
|
456
|
+
if (!isAborted) {
|
|
457
|
+
notify(`Error: ${err.message || 'Unknown error'}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
finally {
|
|
461
|
+
setIsLoading(false);
|
|
462
|
+
setStreamingContent('');
|
|
463
|
+
setAbortController(null);
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
const handleCommand = (cmd) => {
|
|
467
|
+
const parts = cmd.split(' ');
|
|
468
|
+
const command = parts[0].toLowerCase();
|
|
469
|
+
const args = parts.slice(1);
|
|
470
|
+
switch (command) {
|
|
471
|
+
case '/exit':
|
|
472
|
+
case '/quit':
|
|
473
|
+
exit();
|
|
474
|
+
break;
|
|
475
|
+
case '/help':
|
|
476
|
+
setScreen('help');
|
|
477
|
+
break;
|
|
478
|
+
case '/status':
|
|
479
|
+
setScreen('status');
|
|
480
|
+
break;
|
|
481
|
+
case '/version': {
|
|
482
|
+
const version = getCurrentVersion();
|
|
483
|
+
const provider = getCurrentProvider();
|
|
484
|
+
const providers = getProviderList();
|
|
485
|
+
const providerInfo = providers.find(p => p.id === provider.id);
|
|
486
|
+
const providerName = providerInfo?.name || 'Unknown';
|
|
487
|
+
notify(`Codeep v${version} • Provider: ${providerName} • Model: ${config.get('model')}`);
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
case '/update': {
|
|
491
|
+
// Check for updates
|
|
492
|
+
notify('Checking for updates...');
|
|
493
|
+
checkForUpdates()
|
|
494
|
+
.then((info) => {
|
|
495
|
+
setUpdateInfo(info);
|
|
496
|
+
const message = formatVersionInfo(info);
|
|
497
|
+
// Split into multiple notifications for better display
|
|
498
|
+
message.split('\n').forEach((line, i) => {
|
|
499
|
+
setTimeout(() => notify(line), i * 100);
|
|
500
|
+
});
|
|
501
|
+
})
|
|
502
|
+
.catch(() => {
|
|
503
|
+
notify('Failed to check for updates. Please try again later.');
|
|
504
|
+
});
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
case '/clear':
|
|
508
|
+
setMessages([]);
|
|
509
|
+
clearCodeBlocks();
|
|
510
|
+
const newId = startNewSession();
|
|
511
|
+
setSessionId(newId);
|
|
512
|
+
notify('Chat cleared, new session started');
|
|
513
|
+
break;
|
|
514
|
+
case '/model': {
|
|
515
|
+
const models = getModelsForCurrentProvider();
|
|
516
|
+
if (args[0] && models[args[0]]) {
|
|
517
|
+
config.set('model', args[0]);
|
|
518
|
+
notify(`Model: ${args[0]}`);
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
setScreen('model');
|
|
522
|
+
}
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
case '/provider':
|
|
526
|
+
if (args[0] && PROVIDERS[args[0].toLowerCase()]) {
|
|
527
|
+
if (setProvider(args[0].toLowerCase())) {
|
|
528
|
+
notify(`Provider: ${getCurrentProvider().name}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
setScreen('provider');
|
|
533
|
+
}
|
|
534
|
+
break;
|
|
535
|
+
case '/protocol':
|
|
536
|
+
if (args[0] && PROTOCOLS[args[0].toLowerCase()]) {
|
|
537
|
+
config.set('protocol', args[0].toLowerCase());
|
|
538
|
+
notify(`Protocol: ${args[0]}`);
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
setScreen('protocol');
|
|
542
|
+
}
|
|
543
|
+
break;
|
|
544
|
+
case '/sessions':
|
|
545
|
+
// Handle /sessions delete
|
|
546
|
+
if (args[0]?.toLowerCase() === 'delete') {
|
|
547
|
+
if (args[1]) {
|
|
548
|
+
// Delete specific session by name
|
|
549
|
+
const sessionName = args.slice(1).join(' ');
|
|
550
|
+
if (deleteSession(sessionName, projectPath)) {
|
|
551
|
+
notify(`Deleted: ${sessionName}`);
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
notify(`Session not found: ${sessionName}`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
// Open delete picker
|
|
559
|
+
setScreen('sessions-delete');
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
setScreen('sessions');
|
|
564
|
+
}
|
|
565
|
+
break;
|
|
566
|
+
case '/settings':
|
|
567
|
+
setScreen('settings');
|
|
568
|
+
break;
|
|
569
|
+
case '/login':
|
|
570
|
+
setScreen('login');
|
|
571
|
+
break;
|
|
572
|
+
case '/lang':
|
|
573
|
+
case '/language':
|
|
574
|
+
if (args[0] && LANGUAGES[args[0].toLowerCase()]) {
|
|
575
|
+
config.set('language', args[0].toLowerCase());
|
|
576
|
+
notify(`Language: ${LANGUAGES[args[0].toLowerCase()]}`);
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
setScreen('language');
|
|
580
|
+
}
|
|
581
|
+
break;
|
|
582
|
+
case '/logout':
|
|
583
|
+
setScreen('logout');
|
|
584
|
+
break;
|
|
585
|
+
case '/rename': {
|
|
586
|
+
const newName = args.join(' ').trim();
|
|
587
|
+
if (!newName) {
|
|
588
|
+
notify('Usage: /rename <new-name>');
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
// Validate name (no special characters that could cause file issues)
|
|
592
|
+
if (!/^[\w\s-]+$/.test(newName)) {
|
|
593
|
+
notify('Invalid name. Use only letters, numbers, spaces, and hyphens.');
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
const currentId = getCurrentSessionId();
|
|
597
|
+
if (renameSession(currentId, newName, projectPath)) {
|
|
598
|
+
setSessionId(newName);
|
|
599
|
+
notify(`Session renamed to: ${newName}`);
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
notify('Failed to rename session');
|
|
603
|
+
}
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
case '/apply': {
|
|
607
|
+
// Apply file changes from last AI response
|
|
608
|
+
if (!hasWriteAccess) {
|
|
609
|
+
notify('Write access not granted. Enable it in project permissions.');
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
const lastMessage = messages[messages.length - 1];
|
|
613
|
+
if (!lastMessage || lastMessage.role !== 'assistant') {
|
|
614
|
+
notify('No AI response to apply changes from.');
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
const fileChanges = parseFileChanges(lastMessage.content);
|
|
618
|
+
if (fileChanges.length === 0) {
|
|
619
|
+
notify('No file changes found in last response.');
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
// Apply all changes
|
|
623
|
+
let successCount = 0;
|
|
624
|
+
let errorCount = 0;
|
|
625
|
+
for (const change of fileChanges) {
|
|
626
|
+
const result = writeProjectFile(change.path, change.content);
|
|
627
|
+
if (result.success) {
|
|
628
|
+
successCount++;
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
errorCount++;
|
|
632
|
+
notify(`Failed to write ${change.path}: ${result.error}`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (successCount > 0) {
|
|
636
|
+
notify(`Applied ${successCount} file change(s)${errorCount > 0 ? `, ${errorCount} failed` : ''}`);
|
|
637
|
+
}
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
case '/search': {
|
|
641
|
+
const term = args.join(' ').trim();
|
|
642
|
+
if (!term) {
|
|
643
|
+
notify('Usage: /search <term>');
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
if (messages.length === 0) {
|
|
647
|
+
notify('No messages to search');
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
const results = searchMessages(messages, term);
|
|
651
|
+
setSearchResults(results);
|
|
652
|
+
setSearchTerm(term);
|
|
653
|
+
setScreen('search');
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
case '/export': {
|
|
657
|
+
if (messages.length === 0) {
|
|
658
|
+
notify('No messages to export');
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
setScreen('export');
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
case '/diff': {
|
|
665
|
+
const staged = args.includes('--staged') || args.includes('-s');
|
|
666
|
+
const result = getGitDiff(staged, projectPath);
|
|
667
|
+
if (!result.success) {
|
|
668
|
+
notify(result.error || 'Failed to get diff');
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
if (!result.diff) {
|
|
672
|
+
notify(staged ? 'No staged changes' : 'No unstaged changes');
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
// Add a clean user message first
|
|
676
|
+
const userMessage = {
|
|
677
|
+
role: 'user',
|
|
678
|
+
content: `/diff ${staged ? '--staged' : ''}\nRequesting review of ${staged ? 'staged' : 'unstaged'} changes`,
|
|
679
|
+
};
|
|
680
|
+
setMessages(prev => [...prev, userMessage]);
|
|
681
|
+
// Format and send to AI with full diff in background
|
|
682
|
+
const diffPreview = formatDiffForDisplay(result.diff, 100);
|
|
683
|
+
const aiPrompt = `Review this git diff:\n\n\`\`\`diff\n${diffPreview}\n\`\`\`\n\nPlease provide feedback and suggestions.`;
|
|
684
|
+
// Send to AI without adding another user message
|
|
685
|
+
setIsLoading(true);
|
|
686
|
+
setStreamingContent('');
|
|
687
|
+
const controller = new AbortController();
|
|
688
|
+
setAbortController(controller);
|
|
689
|
+
(async () => {
|
|
690
|
+
try {
|
|
691
|
+
const response = await chat(aiPrompt, messages, (chunk) => {
|
|
692
|
+
if (!controller.signal.aborted) {
|
|
693
|
+
setStreamingContent(c => c + chunk);
|
|
694
|
+
}
|
|
695
|
+
}, undefined, projectContext, controller.signal);
|
|
696
|
+
if (!controller.signal.aborted) {
|
|
697
|
+
const finalMessages = [...messages, userMessage, { role: 'assistant', content: response }];
|
|
698
|
+
setMessages(finalMessages);
|
|
699
|
+
autoSaveSession(finalMessages, projectPath);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
catch (error) {
|
|
703
|
+
const err = error;
|
|
704
|
+
const isAborted = err.name === 'AbortError' ||
|
|
705
|
+
err.message?.includes('aborted') ||
|
|
706
|
+
err.message?.includes('abort') ||
|
|
707
|
+
controller.signal.aborted;
|
|
708
|
+
if (!isAborted) {
|
|
709
|
+
notify(`Error: ${err.message || 'Unknown error'}`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
finally {
|
|
713
|
+
setIsLoading(false);
|
|
714
|
+
setStreamingContent('');
|
|
715
|
+
setAbortController(null);
|
|
716
|
+
}
|
|
717
|
+
})();
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
case '/commit': {
|
|
721
|
+
const status = getGitStatus(projectPath);
|
|
722
|
+
if (!status.isRepo) {
|
|
723
|
+
notify('Not a git repository');
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
const diff = getGitDiff(true, projectPath); // Get staged diff
|
|
727
|
+
if (!diff.success || !diff.diff) {
|
|
728
|
+
notify('No staged changes. Use `git add` first.');
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
// Ask AI to generate commit message
|
|
732
|
+
const suggestion = suggestCommitMessage(diff.diff);
|
|
733
|
+
const commitPrompt = `Generate a conventional commit message for these changes:\n\n\`\`\`diff\n${formatDiffForDisplay(diff.diff, 50)}\n\`\`\`\n\nSuggested: "${suggestion}"\n\nProvide an improved commit message following conventional commits format.`;
|
|
734
|
+
notify('Generating commit message...');
|
|
735
|
+
handleSubmit(commitPrompt);
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
case '/copy': {
|
|
739
|
+
// Copy code block to clipboard
|
|
740
|
+
const blockIndex = args[0] ? parseInt(args[0], 10) : -1;
|
|
741
|
+
const code = getCodeBlock(blockIndex);
|
|
742
|
+
if (code) {
|
|
743
|
+
try {
|
|
744
|
+
clipboardy.writeSync(code);
|
|
745
|
+
notify(`Code block ${blockIndex === -1 ? '(last)' : `[${blockIndex}]`} copied to clipboard`);
|
|
746
|
+
}
|
|
747
|
+
catch {
|
|
748
|
+
notify('Failed to copy to clipboard');
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
else {
|
|
752
|
+
notify('No code block found');
|
|
753
|
+
}
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
case '/agent': {
|
|
757
|
+
const prompt = args.join(' ').trim();
|
|
758
|
+
if (!prompt) {
|
|
759
|
+
notify('Usage: /agent <task description>');
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
if (isAgentRunning) {
|
|
763
|
+
notify('Agent is already running. Press Escape to stop it first.');
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
startAgent(prompt, false);
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
case '/agent-dry': {
|
|
770
|
+
const prompt = args.join(' ').trim();
|
|
771
|
+
if (!prompt) {
|
|
772
|
+
notify('Usage: /agent-dry <task description>');
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
if (isAgentRunning) {
|
|
776
|
+
notify('Agent is already running. Press Escape to stop it first.');
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
startAgent(prompt, true);
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
case '/agent-stop': {
|
|
783
|
+
if (!isAgentRunning) {
|
|
784
|
+
notify('No agent is running');
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
abortController?.abort();
|
|
788
|
+
notify('Stopping agent...');
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
case '/undo': {
|
|
792
|
+
const result = undoLastAction();
|
|
793
|
+
if (result.success) {
|
|
794
|
+
notify(`Undo: ${result.message}`);
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
notify(`Cannot undo: ${result.message}`);
|
|
798
|
+
}
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
case '/undo-all': {
|
|
802
|
+
const result = undoAllActions();
|
|
803
|
+
if (result.success) {
|
|
804
|
+
notify(`Undone ${result.results.length} action(s)`);
|
|
805
|
+
}
|
|
806
|
+
else {
|
|
807
|
+
notify(result.results.join('\n'));
|
|
808
|
+
}
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
case '/history': {
|
|
812
|
+
const sessions = getRecentSessions(5);
|
|
813
|
+
if (sessions.length === 0) {
|
|
814
|
+
notify('No agent history');
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
const formatted = sessions.map(s => {
|
|
818
|
+
const date = new Date(s.startTime).toLocaleString();
|
|
819
|
+
return `${date}: ${s.prompt.slice(0, 40)}... (${s.actions.length} actions)`;
|
|
820
|
+
}).join('\n');
|
|
821
|
+
notify(`Recent agent sessions:\n${formatted}`);
|
|
822
|
+
}
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
case '/changes': {
|
|
826
|
+
// Show all file changes from current agent session
|
|
827
|
+
if (agentActions.length === 0) {
|
|
828
|
+
notify('No changes in current session. Run an agent task first.');
|
|
829
|
+
}
|
|
830
|
+
else {
|
|
831
|
+
// Filter to only file changes
|
|
832
|
+
const fileChanges = agentActions.filter(a => ['write', 'edit', 'delete', 'mkdir'].includes(a.type) &&
|
|
833
|
+
a.result === 'success');
|
|
834
|
+
if (fileChanges.length === 0) {
|
|
835
|
+
notify('No file changes in current session.');
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
// Format changes for display
|
|
839
|
+
const writes = fileChanges.filter(a => a.type === 'write');
|
|
840
|
+
const edits = fileChanges.filter(a => a.type === 'edit');
|
|
841
|
+
const deletes = fileChanges.filter(a => a.type === 'delete');
|
|
842
|
+
const mkdirs = fileChanges.filter(a => a.type === 'mkdir');
|
|
843
|
+
let changesText = '# Session Changes\n\n';
|
|
844
|
+
if (writes.length > 0) {
|
|
845
|
+
changesText += `## Created (${writes.length})\n`;
|
|
846
|
+
writes.forEach(w => changesText += `+ ${w.target}\n`);
|
|
847
|
+
changesText += '\n';
|
|
848
|
+
}
|
|
849
|
+
if (edits.length > 0) {
|
|
850
|
+
changesText += `## Modified (${edits.length})\n`;
|
|
851
|
+
edits.forEach(e => changesText += `~ ${e.target}\n`);
|
|
852
|
+
changesText += '\n';
|
|
853
|
+
}
|
|
854
|
+
if (deletes.length > 0) {
|
|
855
|
+
changesText += `## Deleted (${deletes.length})\n`;
|
|
856
|
+
deletes.forEach(d => changesText += `- ${d.target}\n`);
|
|
857
|
+
changesText += '\n';
|
|
858
|
+
}
|
|
859
|
+
if (mkdirs.length > 0) {
|
|
860
|
+
changesText += `## Directories (${mkdirs.length})\n`;
|
|
861
|
+
mkdirs.forEach(m => changesText += `+ ${m.target}/\n`);
|
|
862
|
+
}
|
|
863
|
+
setMessages(prev => [...prev, {
|
|
864
|
+
role: 'assistant',
|
|
865
|
+
content: changesText,
|
|
866
|
+
}]);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
break;
|
|
870
|
+
}
|
|
871
|
+
case '/git-commit': {
|
|
872
|
+
if (!projectContext) {
|
|
873
|
+
notify('No project context');
|
|
874
|
+
break;
|
|
875
|
+
}
|
|
876
|
+
const commitResult = autoCommitAgentChanges(args.join(' ') || 'Agent changes', [], projectContext.root);
|
|
877
|
+
if (commitResult.success) {
|
|
878
|
+
notify(`Committed: ${commitResult.hash}`);
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
notify(`Commit failed: ${commitResult.error}`);
|
|
882
|
+
}
|
|
883
|
+
break;
|
|
884
|
+
}
|
|
885
|
+
case '/context-save': {
|
|
886
|
+
if (!projectContext) {
|
|
887
|
+
notify('No project context');
|
|
888
|
+
break;
|
|
889
|
+
}
|
|
890
|
+
const saved = saveContext(projectContext.root, messages);
|
|
891
|
+
notify(saved ? 'Context saved' : 'Failed to save context');
|
|
892
|
+
break;
|
|
893
|
+
}
|
|
894
|
+
case '/context-load': {
|
|
895
|
+
if (!projectContext) {
|
|
896
|
+
notify('No project context');
|
|
897
|
+
break;
|
|
898
|
+
}
|
|
899
|
+
const loaded = loadContext(projectContext.root);
|
|
900
|
+
if (loaded) {
|
|
901
|
+
setMessages(mergeContext(loaded, []));
|
|
902
|
+
notify(`Loaded context with ${loaded.messages.length} messages`);
|
|
903
|
+
}
|
|
904
|
+
else {
|
|
905
|
+
notify('No saved context for this project');
|
|
906
|
+
}
|
|
907
|
+
break;
|
|
908
|
+
}
|
|
909
|
+
case '/context-clear': {
|
|
910
|
+
if (!projectContext) {
|
|
911
|
+
notify('No project context');
|
|
912
|
+
break;
|
|
913
|
+
}
|
|
914
|
+
clearContext(projectContext.root);
|
|
915
|
+
notify('Context cleared');
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
case '/review': {
|
|
919
|
+
if (!projectContext) {
|
|
920
|
+
notify('No project context');
|
|
921
|
+
break;
|
|
922
|
+
}
|
|
923
|
+
const reviewFiles = args.length > 0 ? args : undefined;
|
|
924
|
+
const reviewResult = performCodeReview(projectContext, reviewFiles);
|
|
925
|
+
const formatted = formatReviewResult(reviewResult);
|
|
926
|
+
setMessages(prev => [...prev, {
|
|
927
|
+
role: 'assistant',
|
|
928
|
+
content: formatted,
|
|
929
|
+
}]);
|
|
930
|
+
break;
|
|
931
|
+
}
|
|
932
|
+
case '/learn': {
|
|
933
|
+
if (!projectContext) {
|
|
934
|
+
notify('No project context');
|
|
935
|
+
break;
|
|
936
|
+
}
|
|
937
|
+
if (args[0] === 'status') {
|
|
938
|
+
const status = getLearningStatus(projectContext.root);
|
|
939
|
+
notify(status);
|
|
940
|
+
}
|
|
941
|
+
else if (args[0] === 'rule' && args.length > 1) {
|
|
942
|
+
const rule = args.slice(1).join(' ');
|
|
943
|
+
addCustomRule(rule, projectContext.root);
|
|
944
|
+
notify(`Added rule: ${rule}`);
|
|
945
|
+
}
|
|
946
|
+
else {
|
|
947
|
+
// Trigger learning from project files
|
|
948
|
+
const prefs = learnFromProject(projectContext.root, projectContext.keyFiles);
|
|
949
|
+
notify(`Learned from ${prefs.sampleCount} files. Use /learn status to see preferences.`);
|
|
950
|
+
}
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
case '/skills': {
|
|
954
|
+
// Show all available skills, search, or stats
|
|
955
|
+
if (args[0] === 'stats') {
|
|
956
|
+
// Show skill usage statistics
|
|
957
|
+
const stats = getSkillStats();
|
|
958
|
+
const statsMessage = `# Skill Usage Statistics
|
|
959
|
+
|
|
960
|
+
- **Total skill executions:** ${stats.totalUsage}
|
|
961
|
+
- **Unique skills used:** ${stats.uniqueSkills}
|
|
962
|
+
- **Success rate:** ${stats.successRate}%`;
|
|
963
|
+
setMessages(prev => [...prev, {
|
|
964
|
+
role: 'assistant',
|
|
965
|
+
content: statsMessage,
|
|
966
|
+
}]);
|
|
967
|
+
}
|
|
968
|
+
else if (args.length > 0) {
|
|
969
|
+
// Search skills
|
|
970
|
+
const query = args.join(' ');
|
|
971
|
+
const results = searchSkills(query);
|
|
972
|
+
if (results.length === 0) {
|
|
973
|
+
notify(`No skills found matching: ${query}`);
|
|
974
|
+
}
|
|
975
|
+
else {
|
|
976
|
+
const formatted = formatSkillsList(results);
|
|
977
|
+
setMessages(prev => [...prev, {
|
|
978
|
+
role: 'assistant',
|
|
979
|
+
content: `# Search Results for "${query}"\n\n${formatted}`,
|
|
980
|
+
}]);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
else {
|
|
984
|
+
const skills = getAllSkills();
|
|
985
|
+
const formatted = formatSkillsList(skills);
|
|
986
|
+
setMessages(prev => [...prev, {
|
|
987
|
+
role: 'assistant',
|
|
988
|
+
content: formatted,
|
|
989
|
+
}]);
|
|
990
|
+
}
|
|
991
|
+
break;
|
|
992
|
+
}
|
|
993
|
+
case '/skill': {
|
|
994
|
+
// Execute or show info about a specific skill
|
|
995
|
+
if (args.length === 0) {
|
|
996
|
+
notify('Usage: /skill <name> [args] or /skills to list all');
|
|
997
|
+
break;
|
|
998
|
+
}
|
|
999
|
+
const skillName = args[0];
|
|
1000
|
+
const skillArgs = args.slice(1).join(' ');
|
|
1001
|
+
// Special subcommands
|
|
1002
|
+
if (skillName === 'create' && args.length > 1) {
|
|
1003
|
+
// Create a new custom skill template
|
|
1004
|
+
const newSkillName = args[1];
|
|
1005
|
+
const template = {
|
|
1006
|
+
name: newSkillName,
|
|
1007
|
+
description: 'Add description here',
|
|
1008
|
+
shortcut: '',
|
|
1009
|
+
category: 'custom',
|
|
1010
|
+
steps: [
|
|
1011
|
+
{ type: 'prompt', content: 'Add your prompt here' },
|
|
1012
|
+
],
|
|
1013
|
+
};
|
|
1014
|
+
try {
|
|
1015
|
+
saveCustomSkill(template);
|
|
1016
|
+
notify(`Created skill template: ~/.codeep/skills/${newSkillName}.json\nEdit it to customize.`);
|
|
1017
|
+
}
|
|
1018
|
+
catch (e) {
|
|
1019
|
+
notify(`Failed to create skill: ${e.message}`);
|
|
1020
|
+
}
|
|
1021
|
+
break;
|
|
1022
|
+
}
|
|
1023
|
+
if (skillName === 'delete' && args.length > 1) {
|
|
1024
|
+
const toDelete = args[1];
|
|
1025
|
+
if (deleteCustomSkill(toDelete)) {
|
|
1026
|
+
notify(`Deleted skill: ${toDelete}`);
|
|
1027
|
+
}
|
|
1028
|
+
else {
|
|
1029
|
+
notify(`Skill not found or is built-in: ${toDelete}`);
|
|
1030
|
+
}
|
|
1031
|
+
break;
|
|
1032
|
+
}
|
|
1033
|
+
if (skillName === 'help' && args.length > 1) {
|
|
1034
|
+
const helpSkill = findSkill(args[1]);
|
|
1035
|
+
if (helpSkill) {
|
|
1036
|
+
const help = formatSkillHelp(helpSkill);
|
|
1037
|
+
setMessages(prev => [...prev, { role: 'assistant', content: help }]);
|
|
1038
|
+
}
|
|
1039
|
+
else {
|
|
1040
|
+
notify(`Skill not found: ${args[1]}`);
|
|
1041
|
+
}
|
|
1042
|
+
break;
|
|
1043
|
+
}
|
|
1044
|
+
// Find and execute skill
|
|
1045
|
+
const skill = findSkill(skillName);
|
|
1046
|
+
if (!skill) {
|
|
1047
|
+
notify(`Skill not found: ${skillName}. Use /skills to list all.`);
|
|
1048
|
+
break;
|
|
1049
|
+
}
|
|
1050
|
+
// Parse parameters
|
|
1051
|
+
const params = parseSkillArgs(skillArgs, skill);
|
|
1052
|
+
// Check required parameters
|
|
1053
|
+
if (skill.parameters) {
|
|
1054
|
+
for (const param of skill.parameters) {
|
|
1055
|
+
if (param.required && !params[param.name]) {
|
|
1056
|
+
notify(`Missing required parameter: ${param.name}. Usage: /skill ${skill.name} <${param.name}>`);
|
|
1057
|
+
break;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
// Check requirements
|
|
1062
|
+
if (skill.requiresWriteAccess && !hasWriteAccess) {
|
|
1063
|
+
notify(`Skill "${skill.name}" requires write access. Grant permission first.`);
|
|
1064
|
+
break;
|
|
1065
|
+
}
|
|
1066
|
+
if (skill.requiresGit) {
|
|
1067
|
+
const status = getGitStatus(projectPath);
|
|
1068
|
+
if (!status.isRepo) {
|
|
1069
|
+
notify(`Skill "${skill.name}" requires a git repository.`);
|
|
1070
|
+
break;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
// Execute skill based on step types
|
|
1074
|
+
const hasAgentStep = skill.steps.some(s => s.type === 'agent');
|
|
1075
|
+
// Track skill usage
|
|
1076
|
+
trackSkillUsage(skill.name);
|
|
1077
|
+
if (hasAgentStep && projectContext) {
|
|
1078
|
+
// Use agent mode for skills with agent steps
|
|
1079
|
+
const prompt = generateSkillPrompt(skill, projectContext, skillArgs, params);
|
|
1080
|
+
startAgent(prompt, false);
|
|
1081
|
+
}
|
|
1082
|
+
else if (projectContext) {
|
|
1083
|
+
// Use regular chat for prompt-only skills
|
|
1084
|
+
const prompt = generateSkillPrompt(skill, projectContext, skillArgs, params);
|
|
1085
|
+
handleSubmit(prompt);
|
|
1086
|
+
}
|
|
1087
|
+
else {
|
|
1088
|
+
notify('Skill requires project context');
|
|
1089
|
+
}
|
|
1090
|
+
break;
|
|
1091
|
+
}
|
|
1092
|
+
default: {
|
|
1093
|
+
// Check for skill chaining (e.g., /commit+push)
|
|
1094
|
+
const commandWithoutSlash = command.slice(1);
|
|
1095
|
+
const chain = parseSkillChain(commandWithoutSlash);
|
|
1096
|
+
if (chain) {
|
|
1097
|
+
// Execute skill chain
|
|
1098
|
+
if (!projectContext) {
|
|
1099
|
+
notify('Skill chain requires project context');
|
|
1100
|
+
break;
|
|
1101
|
+
}
|
|
1102
|
+
// Build combined prompt for all skills in chain
|
|
1103
|
+
const chainPrompt = [];
|
|
1104
|
+
chainPrompt.push('# Skill Chain');
|
|
1105
|
+
chainPrompt.push(`Execute the following skills in order. Stop if any fails.`);
|
|
1106
|
+
chainPrompt.push('');
|
|
1107
|
+
for (const skillName of chain.skills) {
|
|
1108
|
+
const skill = findSkill(skillName);
|
|
1109
|
+
if (!skill)
|
|
1110
|
+
continue;
|
|
1111
|
+
// Check requirements
|
|
1112
|
+
if (skill.requiresWriteAccess && !hasWriteAccess) {
|
|
1113
|
+
notify(`Skill chain requires write access (${skill.name})`);
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
if (skill.requiresGit) {
|
|
1117
|
+
const status = getGitStatus(projectPath);
|
|
1118
|
+
if (!status.isRepo) {
|
|
1119
|
+
notify(`Skill chain requires git repository (${skill.name})`);
|
|
1120
|
+
break;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
chainPrompt.push(`## Step: ${skill.name}`);
|
|
1124
|
+
chainPrompt.push(skill.description);
|
|
1125
|
+
for (const step of skill.steps) {
|
|
1126
|
+
if (step.type === 'prompt' || step.type === 'agent') {
|
|
1127
|
+
chainPrompt.push(step.content);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
chainPrompt.push('');
|
|
1131
|
+
}
|
|
1132
|
+
// Track all skills in chain
|
|
1133
|
+
for (const skillName of chain.skills) {
|
|
1134
|
+
trackSkillUsage(skillName);
|
|
1135
|
+
}
|
|
1136
|
+
// Execute chain as agent
|
|
1137
|
+
const fullPrompt = chainPrompt.join('\n');
|
|
1138
|
+
startAgent(fullPrompt, false);
|
|
1139
|
+
break;
|
|
1140
|
+
}
|
|
1141
|
+
// Check if it's a skill shortcut (e.g., /c for commit)
|
|
1142
|
+
const skillByShortcut = findSkill(commandWithoutSlash);
|
|
1143
|
+
if (skillByShortcut) {
|
|
1144
|
+
const skillArgs = args.join(' ');
|
|
1145
|
+
const params = parseSkillArgs(skillArgs, skillByShortcut);
|
|
1146
|
+
// Check required parameters
|
|
1147
|
+
if (skillByShortcut.parameters) {
|
|
1148
|
+
let missingParam = false;
|
|
1149
|
+
for (const param of skillByShortcut.parameters) {
|
|
1150
|
+
if (param.required && !params[param.name]) {
|
|
1151
|
+
notify(`Missing required parameter: ${param.name}. Usage: /${skillByShortcut.name} <${param.name}>`);
|
|
1152
|
+
missingParam = true;
|
|
1153
|
+
break;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
if (missingParam)
|
|
1157
|
+
break;
|
|
1158
|
+
}
|
|
1159
|
+
// Check requirements
|
|
1160
|
+
if (skillByShortcut.requiresWriteAccess && !hasWriteAccess) {
|
|
1161
|
+
notify(`Skill "${skillByShortcut.name}" requires write access.`);
|
|
1162
|
+
break;
|
|
1163
|
+
}
|
|
1164
|
+
if (skillByShortcut.requiresGit) {
|
|
1165
|
+
const status = getGitStatus(projectPath);
|
|
1166
|
+
if (!status.isRepo) {
|
|
1167
|
+
notify(`Skill "${skillByShortcut.name}" requires a git repository.`);
|
|
1168
|
+
break;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
const hasAgentStep = skillByShortcut.steps.some(s => s.type === 'agent');
|
|
1172
|
+
// Track skill usage
|
|
1173
|
+
trackSkillUsage(skillByShortcut.name);
|
|
1174
|
+
if (hasAgentStep && projectContext) {
|
|
1175
|
+
const prompt = generateSkillPrompt(skillByShortcut, projectContext, skillArgs, params);
|
|
1176
|
+
startAgent(prompt, false);
|
|
1177
|
+
}
|
|
1178
|
+
else if (projectContext) {
|
|
1179
|
+
const prompt = generateSkillPrompt(skillByShortcut, projectContext, skillArgs, params);
|
|
1180
|
+
handleSubmit(prompt);
|
|
1181
|
+
}
|
|
1182
|
+
else {
|
|
1183
|
+
notify('Skill requires project context');
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
else {
|
|
1187
|
+
notify(`Unknown command: ${command}`);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
const handleLogin = () => {
|
|
1193
|
+
setScreen('chat');
|
|
1194
|
+
notify('Logged in successfully!');
|
|
1195
|
+
};
|
|
1196
|
+
const handleSessionLoad = (history, name) => {
|
|
1197
|
+
setMessages(history);
|
|
1198
|
+
setScreen('chat');
|
|
1199
|
+
notify(`Loaded: ${name}`);
|
|
1200
|
+
};
|
|
1201
|
+
const handlePermissionComplete = (granted, permanent, writeGranted = false) => {
|
|
1202
|
+
if (granted) {
|
|
1203
|
+
setHasProjectAccess(true);
|
|
1204
|
+
setHasWriteAccess(writeGranted);
|
|
1205
|
+
const ctx = getProjectContext(projectPath);
|
|
1206
|
+
if (ctx) {
|
|
1207
|
+
ctx.hasWriteAccess = writeGranted;
|
|
1208
|
+
}
|
|
1209
|
+
setProjectContext(ctx);
|
|
1210
|
+
if (permanent) {
|
|
1211
|
+
// Save permission to local .codeep/config.json (already saved by component if write granted)
|
|
1212
|
+
if (!writeGranted) {
|
|
1213
|
+
setProjectPermission(projectPath, true, false); // read only
|
|
1214
|
+
}
|
|
1215
|
+
notify(writeGranted
|
|
1216
|
+
? 'Project access granted (read + write, permanent)'
|
|
1217
|
+
: 'Project access granted (read-only, permanent)');
|
|
1218
|
+
}
|
|
1219
|
+
else {
|
|
1220
|
+
notify(writeGranted
|
|
1221
|
+
? 'Project access granted (read + write, this session)'
|
|
1222
|
+
: 'Project access granted (read-only, this session)');
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
else {
|
|
1226
|
+
notify('Project access denied');
|
|
1227
|
+
}
|
|
1228
|
+
setScreen('chat');
|
|
1229
|
+
};
|
|
1230
|
+
// Render based on screen
|
|
1231
|
+
// Show intro only once on first load (not when messages are cleared)
|
|
1232
|
+
if (showIntro && screen === 'chat' && messages.length === 0 && !sessionLoaded) {
|
|
1233
|
+
return (_jsx(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", children: _jsx(IntroAnimation, { onComplete: () => setShowIntro(false) }) }));
|
|
1234
|
+
}
|
|
1235
|
+
if (screen === 'login') {
|
|
1236
|
+
return _jsx(Login, { onLogin: handleLogin, onCancel: () => setScreen('chat') });
|
|
1237
|
+
}
|
|
1238
|
+
if (screen === 'permission') {
|
|
1239
|
+
return (_jsx(ProjectPermission, { projectPath: projectPath, onComplete: handlePermissionComplete }));
|
|
1240
|
+
}
|
|
1241
|
+
if (screen === 'help') {
|
|
1242
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Help, {}), _jsx(Text, { children: "Press Escape to close" })] }));
|
|
1243
|
+
}
|
|
1244
|
+
if (screen === 'status') {
|
|
1245
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Status, {}), _jsx(Text, { children: "Press Escape to close" })] }));
|
|
1246
|
+
}
|
|
1247
|
+
if (screen === 'session-picker') {
|
|
1248
|
+
return (_jsx(SessionPicker, { projectPath: projectPath, onSelect: (loadedMessages, sessionName) => {
|
|
1249
|
+
setMessages(loadedMessages);
|
|
1250
|
+
setSessionId(sessionName);
|
|
1251
|
+
setSessionLoaded(true);
|
|
1252
|
+
setScreen('chat');
|
|
1253
|
+
setNotification(`Loaded: ${sessionName}`);
|
|
1254
|
+
}, onNewSession: () => {
|
|
1255
|
+
setSessionLoaded(true);
|
|
1256
|
+
setScreen('chat');
|
|
1257
|
+
} }));
|
|
1258
|
+
}
|
|
1259
|
+
if (screen === 'sessions') {
|
|
1260
|
+
return (_jsx(Sessions, { history: messages, onLoad: handleSessionLoad, onClose: () => setScreen('chat'), projectPath: projectPath }));
|
|
1261
|
+
}
|
|
1262
|
+
if (screen === 'sessions-delete') {
|
|
1263
|
+
return (_jsx(Sessions, { history: messages, onLoad: handleSessionLoad, onClose: () => setScreen('chat'), onDelete: (name) => {
|
|
1264
|
+
notify(`Deleted: ${name}`);
|
|
1265
|
+
setScreen('chat');
|
|
1266
|
+
}, deleteMode: true, projectPath: projectPath }));
|
|
1267
|
+
}
|
|
1268
|
+
if (screen === 'logout') {
|
|
1269
|
+
return (_jsx(LogoutPicker, { onLogout: (providerId) => {
|
|
1270
|
+
notify(`Logged out from ${providerId}`);
|
|
1271
|
+
// If logged out from current provider, go to login
|
|
1272
|
+
if (providerId === config.get('provider')) {
|
|
1273
|
+
setMessages([]);
|
|
1274
|
+
setScreen('login');
|
|
1275
|
+
}
|
|
1276
|
+
else {
|
|
1277
|
+
setScreen('chat');
|
|
1278
|
+
}
|
|
1279
|
+
}, onLogoutAll: () => {
|
|
1280
|
+
notify('Logged out from all providers');
|
|
1281
|
+
setMessages([]);
|
|
1282
|
+
setScreen('login');
|
|
1283
|
+
}, onCancel: () => setScreen('chat') }));
|
|
1284
|
+
}
|
|
1285
|
+
if (screen === 'settings') {
|
|
1286
|
+
return (_jsx(Settings, { onClose: () => setScreen('chat'), notify: notify }));
|
|
1287
|
+
}
|
|
1288
|
+
if (screen === 'search') {
|
|
1289
|
+
return (_jsx(Search, { results: searchResults, searchTerm: searchTerm, onClose: () => setScreen('chat'), onSelectMessage: (index) => {
|
|
1290
|
+
// Just close search for now - message is already in chat history
|
|
1291
|
+
notify(`Message #${index + 1}`);
|
|
1292
|
+
} }));
|
|
1293
|
+
}
|
|
1294
|
+
if (screen === 'export') {
|
|
1295
|
+
return (_jsx(Export, { onExport: (format) => {
|
|
1296
|
+
const content = exportMessages(messages, {
|
|
1297
|
+
format,
|
|
1298
|
+
sessionName: sessionId || 'chat',
|
|
1299
|
+
});
|
|
1300
|
+
const result = saveExport(content, format, process.cwd(), sessionId || undefined);
|
|
1301
|
+
if (result.success) {
|
|
1302
|
+
notify(`Exported to ${result.filePath}`);
|
|
1303
|
+
}
|
|
1304
|
+
else {
|
|
1305
|
+
notify(`Export failed: ${result.error}`);
|
|
1306
|
+
}
|
|
1307
|
+
setScreen('chat');
|
|
1308
|
+
}, onCancel: () => setScreen('chat') }));
|
|
1309
|
+
}
|
|
1310
|
+
if (screen === 'model') {
|
|
1311
|
+
return (_jsx(ModelSelect, { onClose: () => setScreen('chat'), notify: notify }));
|
|
1312
|
+
}
|
|
1313
|
+
if (screen === 'provider') {
|
|
1314
|
+
return (_jsx(ProviderSelect, { onClose: () => setScreen('chat'), notify: notify }));
|
|
1315
|
+
}
|
|
1316
|
+
if (screen === 'protocol') {
|
|
1317
|
+
return (_jsx(ProtocolSelect, { onClose: () => setScreen('chat'), notify: notify }));
|
|
1318
|
+
}
|
|
1319
|
+
if (screen === 'language') {
|
|
1320
|
+
return (_jsx(LanguageSelect, { onClose: () => setScreen('chat'), notify: notify }));
|
|
1321
|
+
}
|
|
1322
|
+
// Main chat screen
|
|
1323
|
+
return (_jsxs(Box, { flexDirection: "column", children: [messages.length === 0 && !isLoading && _jsx(Logo, {}), messages.length === 0 && !isLoading && (_jsx(Box, { justifyContent: "center", children: _jsxs(Text, { children: ["Connected to ", _jsx(Text, { color: "#f02a30", children: config.get('model') }), ". Type ", _jsx(Text, { color: "#f02a30", children: "/help" }), " for commands."] }) })), _jsx(MessageList, { messages: messages, streamingContent: streamingContent, scrollOffset: 0, terminalHeight: stdout.rows || 24 }, sessionId), isLoading && !isAgentRunning && _jsx(Loading, { isStreaming: !!streamingContent }), isAgentRunning && (_jsx(AgentProgress, { isRunning: true, iteration: agentIteration, maxIterations: 20, actions: agentActions, currentThinking: agentThinking, dryRun: agentDryRun })), !isAgentRunning && agentResult && (_jsx(AgentSummary, { success: agentResult.success, iterations: agentResult.iterations, actions: agentActions, error: agentResult.error, aborted: agentResult.aborted })), pendingFileChanges.length > 0 && !isLoading && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#f02a30", padding: 1, marginY: 1, children: [_jsxs(Text, { color: "#f02a30", bold: true, children: ["\u2713 Detected ", pendingFileChanges.length, " file change(s):"] }), pendingFileChanges.map((change, i) => {
|
|
1324
|
+
const actionColor = change.action === 'delete' ? 'red' : change.action === 'edit' ? 'yellow' : 'green';
|
|
1325
|
+
const actionLabel = change.action === 'delete' ? 'DELETE' : change.action === 'edit' ? 'EDIT' : 'CREATE';
|
|
1326
|
+
return (_jsxs(Text, { children: ["\u2022 ", _jsxs(Text, { color: actionColor, children: ["[", actionLabel, "]"] }), " ", change.path, change.action !== 'delete' && change.content.includes('\n') && ` (${change.content.split('\n').length} lines)`] }, i));
|
|
1327
|
+
}), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["Apply changes? ", _jsx(Text, { color: "#f02a30", bold: true, children: "[Y/n]" })] }), _jsx(Text, { color: "gray", children: "Press Y to apply, N or Esc to reject" })] })), notification && (_jsx(Box, { justifyContent: "center", children: _jsx(Text, { color: "cyan", children: notification }) })), _jsx(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, children: _jsx(ChatInput, { onSubmit: handleSubmit, disabled: isLoading || pendingFileChanges.length > 0, history: inputHistory, clearTrigger: clearInputTrigger }) }), _jsx(Box, { children: _jsxs(Text, { children: [_jsx(Text, { color: "#f02a30", children: "Ctrl+L" }), " Clear", _jsx(Text, { color: "#f02a30", children: " Esc" }), " Cancel", _jsx(Text, { color: "#f02a30", children: " /help" }), " Commands"] }) })] }));
|
|
1328
|
+
};
|
|
1329
|
+
// Model selection component
|
|
1330
|
+
const ModelSelect = ({ onClose, notify }) => {
|
|
1331
|
+
const [selected, setSelected] = useState(0);
|
|
1332
|
+
const models = Object.entries(getModelsForCurrentProvider());
|
|
1333
|
+
const provider = getCurrentProvider();
|
|
1334
|
+
useInput((input, key) => {
|
|
1335
|
+
if (key.escape)
|
|
1336
|
+
onClose();
|
|
1337
|
+
if (key.upArrow)
|
|
1338
|
+
setSelected(s => Math.max(0, s - 1));
|
|
1339
|
+
if (key.downArrow)
|
|
1340
|
+
setSelected(s => Math.min(models.length - 1, s + 1));
|
|
1341
|
+
if (key.return) {
|
|
1342
|
+
config.set('model', models[selected][0]);
|
|
1343
|
+
notify(`Model: ${models[selected][0]}`);
|
|
1344
|
+
onClose();
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#f02a30", padding: 1, children: [_jsx(Text, { color: "#f02a30", bold: true, children: "Select Model" }), _jsxs(Text, { children: ["Provider: ", provider.name] }), _jsx(Text, { children: " " }), models.map(([key, desc], i) => (_jsxs(Text, { children: [i === selected ? _jsx(Text, { color: "#f02a30", children: "\u25B8 " }) : ' ', _jsx(Text, { color: i === selected ? '#f02a30' : undefined, children: key }), _jsxs(Text, { children: [" - ", desc] }), key === config.get('model') && _jsx(Text, { color: "green", children: " \u25CF" })] }, key))), _jsx(Text, { children: " " }), _jsx(Text, { children: "Enter to select, Escape to close" })] }));
|
|
1348
|
+
};
|
|
1349
|
+
// Provider selection component
|
|
1350
|
+
const ProviderSelect = ({ onClose, notify }) => {
|
|
1351
|
+
const [selected, setSelected] = useState(0);
|
|
1352
|
+
const providers = getProviderList();
|
|
1353
|
+
const currentProvider = getCurrentProvider();
|
|
1354
|
+
useInput((input, key) => {
|
|
1355
|
+
if (key.escape)
|
|
1356
|
+
onClose();
|
|
1357
|
+
if (key.upArrow)
|
|
1358
|
+
setSelected(s => Math.max(0, s - 1));
|
|
1359
|
+
if (key.downArrow)
|
|
1360
|
+
setSelected(s => Math.min(providers.length - 1, s + 1));
|
|
1361
|
+
if (key.return) {
|
|
1362
|
+
setProvider(providers[selected].id);
|
|
1363
|
+
notify(`Provider: ${providers[selected].name}`);
|
|
1364
|
+
onClose();
|
|
1365
|
+
}
|
|
1366
|
+
});
|
|
1367
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#f02a30", padding: 1, children: [_jsx(Text, { color: "#f02a30", bold: true, children: "Select AI Provider" }), _jsx(Text, { children: " " }), providers.map((provider, i) => (_jsxs(Text, { children: [i === selected ? _jsx(Text, { color: "#f02a30", children: "\u25B8 " }) : ' ', _jsx(Text, { color: i === selected ? '#f02a30' : undefined, children: provider.name }), _jsxs(Text, { children: [" - ", provider.description] }), provider.id === currentProvider.id && _jsx(Text, { color: "green", children: " \u25CF" })] }, provider.id))), _jsx(Text, { children: " " }), _jsx(Text, { children: "Enter to select, Escape to close" }), _jsx(Text, { color: "#f02a30", children: "Note: You may need to /login with a new API key" })] }));
|
|
1368
|
+
};
|
|
1369
|
+
// Protocol selection component
|
|
1370
|
+
const ProtocolSelect = ({ onClose, notify }) => {
|
|
1371
|
+
const [selected, setSelected] = useState(0);
|
|
1372
|
+
const protocols = Object.entries(PROTOCOLS);
|
|
1373
|
+
useInput((input, key) => {
|
|
1374
|
+
if (key.escape)
|
|
1375
|
+
onClose();
|
|
1376
|
+
if (key.upArrow)
|
|
1377
|
+
setSelected(s => Math.max(0, s - 1));
|
|
1378
|
+
if (key.downArrow)
|
|
1379
|
+
setSelected(s => Math.min(protocols.length - 1, s + 1));
|
|
1380
|
+
if (key.return) {
|
|
1381
|
+
config.set('protocol', protocols[selected][0]);
|
|
1382
|
+
notify(`Protocol: ${protocols[selected][0]}`);
|
|
1383
|
+
onClose();
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1386
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#f02a30", padding: 1, children: [_jsx(Text, { color: "#f02a30", bold: true, children: "Select Protocol" }), _jsx(Text, { children: " " }), protocols.map(([key, desc], i) => (_jsxs(Text, { children: [i === selected ? _jsx(Text, { color: "#f02a30", children: "\u25B8 " }) : ' ', _jsx(Text, { color: i === selected ? '#f02a30' : undefined, children: key }), _jsxs(Text, { children: [" - ", desc] }), key === config.get('protocol') && _jsx(Text, { color: "green", children: " \u25CF" })] }, key))), _jsx(Text, { children: " " }), _jsx(Text, { children: "Enter to select, Escape to close" })] }));
|
|
1387
|
+
};
|
|
1388
|
+
// Language selection component
|
|
1389
|
+
const LanguageSelect = ({ onClose, notify }) => {
|
|
1390
|
+
const [selected, setSelected] = useState(0);
|
|
1391
|
+
const languages = Object.entries(LANGUAGES);
|
|
1392
|
+
useInput((input, key) => {
|
|
1393
|
+
if (key.escape)
|
|
1394
|
+
onClose();
|
|
1395
|
+
if (key.upArrow)
|
|
1396
|
+
setSelected(s => Math.max(0, s - 1));
|
|
1397
|
+
if (key.downArrow)
|
|
1398
|
+
setSelected(s => Math.min(languages.length - 1, s + 1));
|
|
1399
|
+
if (key.return) {
|
|
1400
|
+
config.set('language', languages[selected][0]);
|
|
1401
|
+
notify(`Language: ${languages[selected][1]}`);
|
|
1402
|
+
onClose();
|
|
1403
|
+
}
|
|
1404
|
+
});
|
|
1405
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#f02a30", padding: 1, children: [_jsx(Text, { color: "#f02a30", bold: true, children: "Select Response Language" }), _jsx(Text, { children: " " }), languages.map(([key, name], i) => (_jsxs(Text, { children: [i === selected ? _jsx(Text, { color: "#f02a30", children: "\u25B8 " }) : ' ', _jsx(Text, { color: i === selected ? '#f02a30' : undefined, children: name }), key === config.get('language') && _jsx(Text, { color: "green", children: " \u25CF" })] }, key))), _jsx(Text, { children: " " }), _jsx(Text, { children: "Enter to select, Escape to close" })] }));
|
|
1406
|
+
};
|