codeep 1.2.11 → 1.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/bin/codeep.js +13 -1
  2. package/dist/config/providers.d.ts +6 -0
  3. package/dist/config/providers.js +41 -2
  4. package/dist/config/providers.test.js +31 -2
  5. package/dist/utils/agent.js +2 -2
  6. package/dist/utils/tools.d.ts +64 -4
  7. package/dist/utils/tools.js +209 -4
  8. package/dist/utils/tools.test.js +12 -7
  9. package/package.json +2 -1
  10. package/dist/app.d.ts +0 -2
  11. package/dist/app.js +0 -1501
  12. package/dist/components/AgentActions.d.ts +0 -18
  13. package/dist/components/AgentActions.js +0 -122
  14. package/dist/components/AgentProgress.d.ts +0 -59
  15. package/dist/components/AgentProgress.js +0 -368
  16. package/dist/components/Export.d.ts +0 -8
  17. package/dist/components/Export.js +0 -27
  18. package/dist/components/Help.d.ts +0 -6
  19. package/dist/components/Help.js +0 -7
  20. package/dist/components/Input.d.ts +0 -9
  21. package/dist/components/Input.js +0 -334
  22. package/dist/components/Loading.d.ts +0 -17
  23. package/dist/components/Loading.js +0 -52
  24. package/dist/components/Login.d.ts +0 -7
  25. package/dist/components/Login.js +0 -77
  26. package/dist/components/Logo.d.ts +0 -8
  27. package/dist/components/Logo.js +0 -89
  28. package/dist/components/LogoutPicker.d.ts +0 -8
  29. package/dist/components/LogoutPicker.js +0 -61
  30. package/dist/components/Message.d.ts +0 -10
  31. package/dist/components/Message.js +0 -242
  32. package/dist/components/MessageList.d.ts +0 -10
  33. package/dist/components/MessageList.js +0 -42
  34. package/dist/components/ProjectPermission.d.ts +0 -7
  35. package/dist/components/ProjectPermission.js +0 -65
  36. package/dist/components/Search.d.ts +0 -10
  37. package/dist/components/Search.js +0 -30
  38. package/dist/components/SessionPicker.d.ts +0 -9
  39. package/dist/components/SessionPicker.js +0 -88
  40. package/dist/components/Sessions.d.ts +0 -12
  41. package/dist/components/Sessions.js +0 -119
  42. package/dist/components/Settings.d.ts +0 -9
  43. package/dist/components/Settings.js +0 -198
  44. package/dist/components/Spinner.d.ts +0 -34
  45. package/dist/components/Spinner.js +0 -38
  46. package/dist/components/Status.d.ts +0 -2
  47. package/dist/components/Status.js +0 -13
  48. package/dist/components/StreamingMessage.d.ts +0 -14
  49. package/dist/components/StreamingMessage.js +0 -19
  50. package/dist/index.d.ts +0 -2
  51. 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
- };