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