codeep 1.0.0

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