codeep 1.0.105 → 1.0.107

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React, { useState, useEffect, useCallback } from 'react';
2
+ import { useState, useEffect, useCallback } from 'react';
3
3
  import { Box, Text, useApp, useInput, useStdout } from 'ink';
4
4
  import clipboardy from 'clipboardy';
5
5
  import { logger } from './utils/logger.js';
@@ -92,25 +92,6 @@ export const App = () => {
92
92
  const [agentThinking, setAgentThinking] = useState('');
93
93
  const [agentResult, setAgentResult] = useState(null);
94
94
  const [agentDryRun, setAgentDryRun] = useState(false);
95
- // Track LiveCodeStream height to render placeholder after agent finishes (prevents ghost content)
96
- const [lastStreamHeight, setLastStreamHeight] = useState(0);
97
- // Clear ghost content when agent finishes by erasing lines above cursor
98
- const prevAgentRunning = React.useRef(isAgentRunning);
99
- useEffect(() => {
100
- if (prevAgentRunning.current === true && isAgentRunning === false && lastStreamHeight > 0) {
101
- // Erase N lines above without scrolling: move up, clear each line, move back down
102
- const linesToClear = lastStreamHeight;
103
- let escapeSeq = '';
104
- escapeSeq += `\x1b[${linesToClear}A`; // Move cursor up N lines
105
- for (let i = 0; i < linesToClear; i++) {
106
- escapeSeq += '\x1b[2K\x1b[B'; // Clear line, move down
107
- }
108
- escapeSeq += `\x1b[${linesToClear}A`; // Move back up to original position
109
- stdout?.write(escapeSeq);
110
- setLastStreamHeight(0);
111
- }
112
- prevAgentRunning.current = isAgentRunning;
113
- }, [isAgentRunning, lastStreamHeight, stdout]);
114
95
  // Load API keys for ALL providers on startup and check if current provider is configured
115
96
  useEffect(() => {
116
97
  loadAllApiKeys().then(() => {
@@ -209,7 +190,6 @@ export const App = () => {
209
190
  clearCodeBlocks();
210
191
  setAgentResult(null);
211
192
  setAgentActions([]);
212
- setLastStreamHeight(0);
213
193
  const newSessId = startNewSession();
214
194
  setSessionId(newSessId);
215
195
  setClearInputTrigger(prev => prev + 1); // Trigger input clear
@@ -301,7 +281,6 @@ export const App = () => {
301
281
  setAgentActions([]);
302
282
  setAgentThinking('');
303
283
  setAgentResult(null);
304
- setLastStreamHeight(0);
305
284
  setAgentDryRun(dryRun);
306
285
  // Add user message
307
286
  const userMessage = {
@@ -348,12 +327,6 @@ export const App = () => {
348
327
  timestamp: Date.now(),
349
328
  };
350
329
  setAgentActions(prev => [...prev, actionLog]);
351
- // Track stream height for ghost content cleanup
352
- if (details) {
353
- const lineCount = details.split('\n').length;
354
- // LiveCodeStream shows: 1 header + lines + 1 loading indicator + 1 footer = ~lineCount + 3
355
- setLastStreamHeight(Math.min(lineCount, 50) + 5);
356
- }
357
330
  },
358
331
  onToolResult: (result, toolCall) => {
359
332
  // Replace the last action with the complete one
@@ -414,7 +387,6 @@ export const App = () => {
414
387
  if (agentResult) {
415
388
  setAgentResult(null);
416
389
  setAgentActions([]);
417
- setLastStreamHeight(0);
418
390
  }
419
391
  // Validate input
420
392
  const validation = validateInput(input);
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Agent progress display component
3
+ * Optimized with isolated spinner animation and memoization
3
4
  */
4
5
  import React from 'react';
5
6
  import { ActionLog } from '../utils/tools';
@@ -1,22 +1,27 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /**
3
3
  * Agent progress display component
4
+ * Optimized with isolated spinner animation and memoization
4
5
  */
5
- import { useState, useEffect, useRef } from 'react';
6
+ import { useState, useEffect, useRef, memo } from 'react';
6
7
  import { Box, Text } from 'ink';
7
8
  // Spinner frames for animation (no emojis)
8
9
  const SPINNER_FRAMES = ['/', '-', '\\', '|'];
9
- export const AgentProgress = ({ isRunning, iteration, maxIterations, actions, currentThinking, dryRun, }) => {
10
- const [spinnerFrame, setSpinnerFrame] = useState(0);
11
- // Animate spinner when running
10
+ /**
11
+ * Isolated spinner component - animation doesn't cause parent re-renders
12
+ */
13
+ const AgentSpinner = memo(({ color = '#f02a30' }) => {
14
+ const [frame, setFrame] = useState(0);
12
15
  useEffect(() => {
13
- if (!isRunning)
14
- return;
15
16
  const timer = setInterval(() => {
16
- setSpinnerFrame(f => (f + 1) % SPINNER_FRAMES.length);
17
+ setFrame(f => (f + 1) % SPINNER_FRAMES.length);
17
18
  }, 150);
18
19
  return () => clearInterval(timer);
19
- }, [isRunning]);
20
+ }, []);
21
+ return _jsxs(Text, { color: color, children: ["[", SPINNER_FRAMES[frame], "]"] });
22
+ });
23
+ AgentSpinner.displayName = 'AgentSpinner';
24
+ export const AgentProgress = memo(({ isRunning, iteration, maxIterations, actions, currentThinking, dryRun, }) => {
20
25
  // Don't show anything if not running and no actions
21
26
  if (!isRunning && actions.length === 0) {
22
27
  return null;
@@ -40,8 +45,9 @@ export const AgentProgress = ({ isRunning, iteration, maxIterations, actions, cu
40
45
  deleted: actions.filter(a => a.type === 'delete' && a.result === 'success').length,
41
46
  };
42
47
  const totalFileChanges = fileChanges.created + fileChanges.modified + fileChanges.deleted;
43
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: dryRun ? 'yellow' : '#f02a30', paddingX: 1, marginY: 1, children: [_jsx(Box, { children: isRunning ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: dryRun ? 'yellow' : '#f02a30', children: ["[", SPINNER_FRAMES[spinnerFrame], "]"] }), _jsxs(Text, { color: dryRun ? 'yellow' : '#f02a30', bold: true, children: [' ', dryRun ? 'DRY RUN' : 'AGENT', ' '] }), _jsx(Text, { color: "cyan", children: "|" }), _jsxs(Text, { color: "cyan", children: [" step ", iteration] }), _jsx(Text, { color: "cyan", children: " | " }), actionCounts.reads > 0 && _jsxs(Text, { color: "blue", children: [actionCounts.reads, "R "] }), actionCounts.writes > 0 && _jsxs(Text, { color: "green", children: [actionCounts.writes, "W "] }), actionCounts.edits > 0 && _jsxs(Text, { color: "yellow", children: [actionCounts.edits, "E "] }), actionCounts.commands > 0 && _jsxs(Text, { color: "magenta", children: [actionCounts.commands, "C "] }), actionCounts.searches > 0 && _jsxs(Text, { color: "cyan", children: [actionCounts.searches, "S "] }), actions.length === 0 && _jsx(Text, { color: "cyan", children: "0 actions" })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "green", bold: true, children: "[DONE] " }), _jsx(Text, { children: "Agent completed" }), _jsx(Text, { color: "cyan", children: " | " }), _jsxs(Text, { color: "white", children: [actions.length, " actions"] })] })) }), isRunning && currentAction && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "white", bold: true, children: "Now: " }), _jsxs(Text, { color: getActionColor(currentAction.type), children: [getActionLabel(currentAction.type), " "] }), _jsx(Text, { color: "white", children: formatTarget(currentAction.target) })] })), _jsx(Text, { color: "cyan", children: '─'.repeat(50) }), recentActions.length > 1 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", dimColor: true, children: "Recent:" }), recentActions.slice(0, -1).map((action, i) => (_jsx(ActionItem, { action: action }, i)))] })), isRunning && totalFileChanges > 0 && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: "Changes: " }), fileChanges.created > 0 && _jsxs(Text, { color: "green", children: ["+", fileChanges.created, " "] }), fileChanges.modified > 0 && _jsxs(Text, { color: "yellow", children: ["~", fileChanges.modified, " "] }), fileChanges.deleted > 0 && _jsxs(Text, { color: "red", children: ["-", fileChanges.deleted] })] })), isRunning && currentThinking && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "cyan", wrap: "truncate-end", children: ["> ", currentThinking.slice(0, 80), currentThinking.length > 80 ? '...' : ''] }) })), isRunning && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: "Press " }), _jsx(Text, { color: "#f02a30", children: "Esc" }), _jsx(Text, { color: "cyan", children: " to stop" })] })), !isRunning && totalFileChanges > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "File Changes:" }), fileChanges.created > 0 && (_jsxs(Text, { color: "green", children: [" + ", fileChanges.created, " file(s) created"] })), fileChanges.modified > 0 && (_jsxs(Text, { color: "yellow", children: [" ~ ", fileChanges.modified, " file(s) modified"] })), fileChanges.deleted > 0 && (_jsxs(Text, { color: "red", children: [" - ", fileChanges.deleted, " file(s) deleted"] }))] }))] }));
44
- };
48
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: dryRun ? 'yellow' : '#f02a30', paddingX: 1, marginY: 1, children: [_jsx(Box, { children: isRunning ? (_jsxs(_Fragment, { children: [_jsx(AgentSpinner, { color: dryRun ? 'yellow' : '#f02a30' }), _jsxs(Text, { color: dryRun ? 'yellow' : '#f02a30', bold: true, children: [' ', dryRun ? 'DRY RUN' : 'AGENT', ' '] }), _jsx(Text, { color: "cyan", children: "|" }), _jsxs(Text, { color: "cyan", children: [" step ", iteration] }), _jsx(Text, { color: "cyan", children: " | " }), actionCounts.reads > 0 && _jsxs(Text, { color: "blue", children: [actionCounts.reads, "R "] }), actionCounts.writes > 0 && _jsxs(Text, { color: "green", children: [actionCounts.writes, "W "] }), actionCounts.edits > 0 && _jsxs(Text, { color: "yellow", children: [actionCounts.edits, "E "] }), actionCounts.commands > 0 && _jsxs(Text, { color: "magenta", children: [actionCounts.commands, "C "] }), actionCounts.searches > 0 && _jsxs(Text, { color: "cyan", children: [actionCounts.searches, "S "] }), actions.length === 0 && _jsx(Text, { color: "cyan", children: "0 actions" })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "green", bold: true, children: "[DONE] " }), _jsx(Text, { children: "Agent completed" }), _jsx(Text, { color: "cyan", children: " | " }), _jsxs(Text, { color: "white", children: [actions.length, " actions"] })] })) }), isRunning && currentAction && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "white", bold: true, children: "Now: " }), _jsxs(Text, { color: getActionColor(currentAction.type), children: [getActionLabel(currentAction.type), " "] }), _jsx(Text, { color: "white", children: formatTarget(currentAction.target) })] })), _jsx(Text, { color: "cyan", children: '─'.repeat(50) }), recentActions.length > 1 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", dimColor: true, children: "Recent:" }), recentActions.slice(0, -1).map((action, i) => (_jsx(ActionItem, { action: action }, i)))] })), isRunning && totalFileChanges > 0 && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: "Changes: " }), fileChanges.created > 0 && _jsxs(Text, { color: "green", children: ["+", fileChanges.created, " "] }), fileChanges.modified > 0 && _jsxs(Text, { color: "yellow", children: ["~", fileChanges.modified, " "] }), fileChanges.deleted > 0 && _jsxs(Text, { color: "red", children: ["-", fileChanges.deleted] })] })), isRunning && currentThinking && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "cyan", wrap: "truncate-end", children: ["> ", currentThinking.slice(0, 80), currentThinking.length > 80 ? '...' : ''] }) })), isRunning && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: "Press " }), _jsx(Text, { color: "#f02a30", children: "Esc" }), _jsx(Text, { color: "cyan", children: " to stop" })] })), !isRunning && totalFileChanges > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "File Changes:" }), fileChanges.created > 0 && (_jsxs(Text, { color: "green", children: [" + ", fileChanges.created, " file(s) created"] })), fileChanges.modified > 0 && (_jsxs(Text, { color: "yellow", children: [" ~ ", fileChanges.modified, " file(s) modified"] })), fileChanges.deleted > 0 && (_jsxs(Text, { color: "red", children: [" - ", fileChanges.deleted, " file(s) deleted"] }))] }))] }));
49
+ });
50
+ AgentProgress.displayName = 'AgentProgress';
45
51
  // Get file extension for language detection
46
52
  const getFileExtension = (filename) => {
47
53
  const parts = filename.split('.');
@@ -140,7 +146,7 @@ const isSectionBreak = (line, prevLine) => {
140
146
  return false;
141
147
  };
142
148
  const LINES_PER_CHUNK = 10; // Show 10 lines at a time
143
- export const LiveCodeStream = ({ actions, isRunning, terminalWidth = 80 }) => {
149
+ export const LiveCodeStream = memo(({ actions, isRunning, terminalWidth = 80 }) => {
144
150
  // Track how many lines we've shown so far
145
151
  const [visibleLineCount, setVisibleLineCount] = useState(LINES_PER_CHUNK);
146
152
  const lastActionIdRef = useRef('');
@@ -199,7 +205,8 @@ export const LiveCodeStream = ({ actions, isRunning, terminalWidth = 80 }) => {
199
205
  const lineNum = i + 1;
200
206
  return (_jsxs(Text, { children: [_jsxs(Text, { color: "cyan", dimColor: true, children: [String(lineNum).padStart(4, ' '), " \u2502", ' '] }), _jsx(Text, { color: getCodeColor(line, ext), children: line.slice(0, 80) }), line.length > 80 && _jsx(Text, { color: "cyan", children: "\u2026" })] }, `line-${i}`));
201
207
  }), hasMoreLines && (_jsxs(Text, { color: "cyan", dimColor: true, children: [' ', "\u2502 ... ", totalLines - visibleLineCount, " more lines loading..."] })), _jsx(Text, { color: actionColor, children: '─'.repeat(lineWidth) })] }));
202
- };
208
+ });
209
+ LiveCodeStream.displayName = 'LiveCodeStream';
203
210
  // Helper functions for action display
204
211
  const getActionColor = (type) => {
205
212
  switch (type) {
@@ -257,7 +264,7 @@ const ActionItem = ({ action }) => {
257
264
  };
258
265
  return (_jsxs(Text, { children: [getStatusIndicator(), ' ', _jsx(Text, { color: getActionColor(action.type), children: getActionLabel(action.type).padEnd(10) }), ' ', _jsx(Text, { color: "cyan", children: formatTarget(action.target) })] }));
259
266
  };
260
- export const AgentSummary = ({ success, iterations, actions, error, aborted, }) => {
267
+ export const AgentSummary = memo(({ success, iterations, actions, error, aborted, }) => {
261
268
  const filesWritten = actions.filter(a => a.type === 'write' && a.result === 'success');
262
269
  const filesEdited = actions.filter(a => a.type === 'edit' && a.result === 'success');
263
270
  const filesDeleted = actions.filter(a => a.type === 'delete' && a.result === 'success');
@@ -267,7 +274,8 @@ export const AgentSummary = ({ success, iterations, actions, error, aborted, })
267
274
  const hasFileChanges = filesWritten.length > 0 || filesEdited.length > 0 ||
268
275
  filesDeleted.length > 0 || dirsCreated.length > 0;
269
276
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: success ? 'green' : aborted ? 'yellow' : 'red', padding: 1, marginY: 1, children: [_jsxs(Box, { children: [success ? (_jsx(Text, { color: "green", bold: true, children: "[OK] Agent completed" })) : aborted ? (_jsx(Text, { color: "yellow", bold: true, children: "[--] Agent stopped" })) : (_jsx(Text, { color: "red", bold: true, children: "[!!] Agent failed" })), _jsxs(Text, { color: "cyan", children: [" | ", iterations, " iterations | ", actions.length, " actions"] })] }), error && (_jsxs(Text, { color: "red", children: ["Error: ", error] })), hasFileChanges && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "cyan", children: '─'.repeat(40) }), _jsx(Text, { bold: true, children: "Changes:" }), filesWritten.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "green", children: ["+ Created (", filesWritten.length, "):"] }), filesWritten.map((f, i) => (_jsxs(Text, { color: "green", children: [" ", f.target] }, i)))] })), filesEdited.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: ["~ Modified (", filesEdited.length, "):"] }), filesEdited.map((f, i) => (_jsxs(Text, { color: "yellow", children: [" ", f.target] }, i)))] })), filesDeleted.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", children: ["- Deleted (", filesDeleted.length, "):"] }), filesDeleted.map((f, i) => (_jsxs(Text, { color: "red", children: [" ", f.target] }, i)))] })), dirsCreated.length > 0 && (_jsxs(Text, { color: "blue", children: ["+ ", dirsCreated.length, " director(ies) created"] }))] })), commandsRun.length > 0 && (_jsxs(Text, { color: "magenta", children: [commandsRun.length, " command(s) executed"] })), errors.length > 0 && (_jsxs(Text, { color: "red", children: [errors.length, " error(s) occurred"] }))] }));
270
- };
277
+ });
278
+ AgentSummary.displayName = 'AgentSummary';
271
279
  export const ChangesList = ({ actions }) => {
272
280
  const writes = actions.filter(a => a.type === 'write' && a.result === 'success');
273
281
  const edits = actions.filter(a => a.type === 'edit' && a.result === 'success');
@@ -2,7 +2,15 @@ import React from 'react';
2
2
  interface LoadingProps {
3
3
  isStreaming?: boolean;
4
4
  }
5
+ /**
6
+ * Loading indicator with isolated animation
7
+ * Parent component won't re-render when spinner animates
8
+ */
5
9
  export declare const Loading: React.FC<LoadingProps>;
10
+ /**
11
+ * Simple inline spinner for smaller spaces
12
+ * Also uses isolated animation
13
+ */
6
14
  export declare const InlineSpinner: React.FC<{
7
15
  text?: string;
8
16
  }>;
@@ -1,31 +1,52 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- import { useState, useEffect } from 'react';
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect, memo } from 'react';
3
3
  import { Text, Box } from 'ink';
4
- // Spinner frames
4
+ // Spinner frames - Braille pattern for smooth animation
5
5
  const SPINNER = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'];
6
- export const Loading = ({ isStreaming = false }) => {
7
- const [tick, setTick] = useState(0);
6
+ /**
7
+ * Isolated spinner animation component
8
+ * Only this component re-renders during animation, not the parent
9
+ */
10
+ const AnimatedSpinner = memo(() => {
11
+ const [frame, setFrame] = useState(0);
8
12
  useEffect(() => {
9
- // Force re-render every 100ms to animate even when parent doesn't update
10
13
  const timer = setInterval(() => {
11
- setTick(t => t + 1);
14
+ setFrame(f => (f + 1) % SPINNER.length);
12
15
  }, 100);
13
16
  return () => clearInterval(timer);
14
17
  }, []);
15
- const spinnerFrame = tick % SPINNER.length;
16
- const dotsCount = (Math.floor(tick / 3) % 4); // 0, 1, 2, 3 dots cycling
17
- const dots = '.'.repeat(dotsCount);
18
- const message = isStreaming ? 'Writing' : 'Thinking';
19
- return (_jsx(Box, { paddingLeft: 2, paddingY: 0, children: _jsxs(Text, { color: "#f02a30", bold: true, children: [SPINNER[spinnerFrame], " ", message, dots] }) }));
20
- };
21
- // Simple inline spinner for smaller spaces
22
- export const InlineSpinner = ({ text = 'Loading' }) => {
23
- const [tick, setTick] = useState(0);
18
+ return _jsx(Text, { color: "#f02a30", children: SPINNER[frame] });
19
+ });
20
+ AnimatedSpinner.displayName = 'AnimatedSpinner';
21
+ /**
22
+ * Animated dots component
23
+ * Isolated animation state
24
+ */
25
+ const AnimatedDots = memo(() => {
26
+ const [count, setCount] = useState(0);
24
27
  useEffect(() => {
25
28
  const timer = setInterval(() => {
26
- setTick(t => t + 1);
27
- }, 80);
29
+ setCount(c => (c + 1) % 4);
30
+ }, 300);
28
31
  return () => clearInterval(timer);
29
32
  }, []);
30
- return (_jsxs(Text, { children: [_jsxs(Text, { color: "#f02a30", children: [SPINNER[tick % SPINNER.length], " "] }), _jsx(Text, { children: text })] }));
31
- };
33
+ return _jsx(Text, { color: "#f02a30", children: '.'.repeat(count) });
34
+ });
35
+ AnimatedDots.displayName = 'AnimatedDots';
36
+ /**
37
+ * Loading indicator with isolated animation
38
+ * Parent component won't re-render when spinner animates
39
+ */
40
+ export const Loading = memo(({ isStreaming = false }) => {
41
+ const message = isStreaming ? 'Writing' : 'Thinking';
42
+ return (_jsxs(Box, { paddingLeft: 2, paddingY: 0, children: [_jsx(AnimatedSpinner, {}), _jsxs(Text, { color: "#f02a30", bold: true, children: [" ", message] }), _jsx(AnimatedDots, {})] }));
43
+ });
44
+ Loading.displayName = 'Loading';
45
+ /**
46
+ * Simple inline spinner for smaller spaces
47
+ * Also uses isolated animation
48
+ */
49
+ export const InlineSpinner = memo(({ text = 'Loading' }) => {
50
+ return (_jsxs(Text, { children: [_jsx(AnimatedSpinner, {}), _jsxs(Text, { children: [" ", text] })] }));
51
+ });
52
+ InlineSpinner.displayName = 'InlineSpinner';
@@ -3,8 +3,13 @@ import { Message } from '../config/index';
3
3
  interface MessageListProps {
4
4
  messages: Message[];
5
5
  streamingContent?: string;
6
- scrollOffset: number;
7
- terminalHeight: number;
6
+ scrollOffset?: number;
7
+ terminalHeight?: number;
8
8
  }
9
+ /**
10
+ * Message list with optimized rendering
11
+ * Uses Static component for stable scroll position
12
+ * Uses content-based keys instead of index for better React reconciliation
13
+ */
9
14
  export declare const MessageList: React.FC<MessageListProps>;
10
15
  export {};
@@ -1,8 +1,25 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { memo } from 'react';
2
3
  import { Box, Static } from 'ink';
3
4
  import { MessageView } from './Message.js';
4
- export const MessageList = ({ messages, }) => {
5
- // Use Static component to prevent messages from re-rendering on every keystroke
6
- // This keeps the scroll position stable when typing in input field
7
- return (_jsx(Box, { flexDirection: "column", children: _jsx(Static, { items: messages, children: (msg, index) => (_jsx(MessageView, { role: msg.role, content: msg.content }, index)) }) }));
5
+ import { StreamingMessage } from './StreamingMessage.js';
6
+ /**
7
+ * Generate unique key for message based on content and position
8
+ * More stable than index-based keys
9
+ */
10
+ const getMessageKey = (msg, index) => {
11
+ // Use hash of first 50 chars + role + index for uniqueness
12
+ const contentHash = msg.content.slice(0, 50).split('').reduce((acc, char) => {
13
+ return ((acc << 5) - acc) + char.charCodeAt(0);
14
+ }, 0);
15
+ return `${msg.role}-${index}-${Math.abs(contentHash)}`;
8
16
  };
17
+ /**
18
+ * Message list with optimized rendering
19
+ * Uses Static component for stable scroll position
20
+ * Uses content-based keys instead of index for better React reconciliation
21
+ */
22
+ export const MessageList = memo(({ messages, streamingContent, }) => {
23
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: messages, children: (msg, index) => (_jsx(MessageView, { role: msg.role, content: msg.content }, getMessageKey(msg, index))) }), streamingContent && (_jsx(StreamingMessage, { content: streamingContent }))] }));
24
+ });
25
+ MessageList.displayName = 'MessageList';
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Isolated Spinner component
3
+ * Animation state is local - doesn't cause parent re-renders
4
+ */
5
+ import React from 'react';
6
+ declare const SPINNERS: {
7
+ dots: string[];
8
+ line: string[];
9
+ simple: string[];
10
+ arrow: string[];
11
+ bounce: string[];
12
+ };
13
+ type SpinnerType = keyof typeof SPINNERS;
14
+ interface SpinnerProps {
15
+ type?: SpinnerType;
16
+ color?: string;
17
+ interval?: number;
18
+ prefix?: string;
19
+ suffix?: string;
20
+ }
21
+ /**
22
+ * Spinner with isolated animation state
23
+ * Parent component won't re-render when spinner frame changes
24
+ */
25
+ export declare const Spinner: React.FC<SpinnerProps>;
26
+ /**
27
+ * Static spinner character (no animation)
28
+ * Use when you want consistent display without flickering
29
+ */
30
+ export declare const StaticSpinner: React.FC<{
31
+ char?: string;
32
+ color?: string;
33
+ }>;
34
+ export default Spinner;
@@ -0,0 +1,38 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Isolated Spinner component
4
+ * Animation state is local - doesn't cause parent re-renders
5
+ */
6
+ import { useState, useEffect, memo } from 'react';
7
+ import { Text } from 'ink';
8
+ // Different spinner styles
9
+ const SPINNERS = {
10
+ dots: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
11
+ line: ['/', '-', '\\', '|'],
12
+ simple: ['·', '•', '●', '•'],
13
+ arrow: ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙'],
14
+ bounce: ['⠁', '⠂', '⠄', '⠂'],
15
+ };
16
+ /**
17
+ * Spinner with isolated animation state
18
+ * Parent component won't re-render when spinner frame changes
19
+ */
20
+ export const Spinner = memo(({ type = 'line', color = '#f02a30', interval = 100, prefix = '', suffix = '', }) => {
21
+ const [frame, setFrame] = useState(0);
22
+ const frames = SPINNERS[type];
23
+ useEffect(() => {
24
+ const timer = setInterval(() => {
25
+ setFrame(f => (f + 1) % frames.length);
26
+ }, interval);
27
+ return () => clearInterval(timer);
28
+ }, [frames.length, interval]);
29
+ return (_jsxs(Text, { children: [prefix, _jsx(Text, { color: color, children: frames[frame] }), suffix] }));
30
+ });
31
+ Spinner.displayName = 'Spinner';
32
+ /**
33
+ * Static spinner character (no animation)
34
+ * Use when you want consistent display without flickering
35
+ */
36
+ export const StaticSpinner = memo(({ char = '●', color = '#f02a30', }) => (_jsx(Text, { color: color, children: char })));
37
+ StaticSpinner.displayName = 'StaticSpinner';
38
+ export default Spinner;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Streaming message component
3
+ * Isolates streaming state from main App to reduce re-renders
4
+ */
5
+ import React from 'react';
6
+ interface StreamingMessageProps {
7
+ content: string;
8
+ }
9
+ /**
10
+ * Displays streaming content as it arrives
11
+ * Wrapped in memo to prevent unnecessary re-renders when content hasn't changed
12
+ */
13
+ export declare const StreamingMessage: React.FC<StreamingMessageProps>;
14
+ export default StreamingMessage;
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Streaming message component
4
+ * Isolates streaming state from main App to reduce re-renders
5
+ */
6
+ import { memo } from 'react';
7
+ import { Box } from 'ink';
8
+ import { MessageView } from './Message.js';
9
+ /**
10
+ * Displays streaming content as it arrives
11
+ * Wrapped in memo to prevent unnecessary re-renders when content hasn't changed
12
+ */
13
+ export const StreamingMessage = memo(({ content }) => {
14
+ if (!content)
15
+ return null;
16
+ return (_jsx(Box, { flexDirection: "column", children: _jsx(MessageView, { role: "assistant", content: content }) }));
17
+ });
18
+ StreamingMessage.displayName = 'StreamingMessage';
19
+ export default StreamingMessage;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Custom hooks for Codeep
3
+ */
4
+ export { useAgent } from './useAgent';
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Custom hooks for Codeep
3
+ */
4
+ export { useAgent } from './useAgent.js';
@@ -0,0 +1,29 @@
1
+ /**
2
+ * useAgent hook - isolates agent state management from main App
3
+ * Reduces re-renders in App component when agent state changes
4
+ */
5
+ import { AgentResult } from '../utils/agent';
6
+ import { ActionLog } from '../utils/tools';
7
+ import { ProjectContext } from '../utils/project';
8
+ import { Message } from '../config/index';
9
+ interface UseAgentOptions {
10
+ projectContext: ProjectContext | null;
11
+ hasWriteAccess: boolean;
12
+ messages: Message[];
13
+ projectPath: string;
14
+ onMessageAdd: (message: Message) => void;
15
+ notify: (msg: string, duration?: number) => void;
16
+ }
17
+ interface UseAgentReturn {
18
+ isAgentRunning: boolean;
19
+ agentIteration: number;
20
+ agentActions: ActionLog[];
21
+ agentThinking: string;
22
+ agentResult: AgentResult | null;
23
+ agentDryRun: boolean;
24
+ startAgent: (prompt: string, dryRun?: boolean) => Promise<void>;
25
+ stopAgent: () => void;
26
+ clearAgentState: () => void;
27
+ }
28
+ export declare function useAgent({ projectContext, hasWriteAccess, messages, projectPath, onMessageAdd, notify, }: UseAgentOptions): UseAgentReturn;
29
+ export default useAgent;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * useAgent hook - isolates agent state management from main App
3
+ * Reduces re-renders in App component when agent state changes
4
+ */
5
+ import { useState, useCallback, useRef } from 'react';
6
+ import { runAgent, formatAgentResult } from '../utils/agent.js';
7
+ import { createActionLog } from '../utils/tools.js';
8
+ import { autoSaveSession } from '../config/index.js';
9
+ export function useAgent({ projectContext, hasWriteAccess, messages, projectPath, onMessageAdd, notify, }) {
10
+ const [isAgentRunning, setIsAgentRunning] = useState(false);
11
+ const [agentIteration, setAgentIteration] = useState(0);
12
+ const [agentActions, setAgentActions] = useState([]);
13
+ const [agentThinking, setAgentThinking] = useState('');
14
+ const [agentResult, setAgentResult] = useState(null);
15
+ const [agentDryRun, setAgentDryRun] = useState(false);
16
+ const abortControllerRef = useRef(null);
17
+ const clearAgentState = useCallback(() => {
18
+ setAgentResult(null);
19
+ setAgentActions([]);
20
+ setAgentThinking('');
21
+ setAgentIteration(0);
22
+ }, []);
23
+ const stopAgent = useCallback(() => {
24
+ abortControllerRef.current?.abort();
25
+ }, []);
26
+ const startAgent = useCallback(async (prompt, dryRun = false) => {
27
+ if (!projectContext) {
28
+ notify('Agent mode requires project context. Run in a project directory.');
29
+ return;
30
+ }
31
+ if (!hasWriteAccess && !dryRun) {
32
+ notify('Agent mode requires write access. Grant permission first or use /agent-dry');
33
+ return;
34
+ }
35
+ // Reset agent state
36
+ setIsAgentRunning(true);
37
+ setAgentIteration(0);
38
+ setAgentActions([]);
39
+ setAgentThinking('');
40
+ setAgentResult(null);
41
+ setAgentDryRun(dryRun);
42
+ // Add user message
43
+ const userMessage = {
44
+ role: 'user',
45
+ content: dryRun ? `[DRY RUN] ${prompt}` : `[AGENT] ${prompt}`
46
+ };
47
+ onMessageAdd(userMessage);
48
+ const controller = new AbortController();
49
+ abortControllerRef.current = controller;
50
+ try {
51
+ const result = await runAgent(prompt, projectContext, {
52
+ dryRun,
53
+ onIteration: (iteration) => {
54
+ setAgentIteration(iteration);
55
+ },
56
+ onToolCall: (tool) => {
57
+ const toolName = tool.tool.toLowerCase().replace(/-/g, '_');
58
+ let details;
59
+ if (toolName === 'write_file' && tool.parameters.content) {
60
+ details = tool.parameters.content;
61
+ }
62
+ else if (toolName === 'edit_file' && tool.parameters.new_text) {
63
+ details = tool.parameters.new_text;
64
+ }
65
+ const actionLog = {
66
+ type: toolName === 'write_file' ? 'write' :
67
+ toolName === 'edit_file' ? 'edit' :
68
+ toolName === 'read_file' ? 'read' :
69
+ toolName === 'delete_file' ? 'delete' :
70
+ toolName === 'execute_command' ? 'command' :
71
+ toolName === 'search_code' ? 'search' :
72
+ toolName === 'list_files' ? 'list' :
73
+ toolName === 'create_directory' ? 'mkdir' :
74
+ toolName === 'fetch_url' ? 'fetch' : 'command',
75
+ target: tool.parameters.path ||
76
+ tool.parameters.command ||
77
+ tool.parameters.pattern ||
78
+ tool.parameters.url || 'unknown',
79
+ result: 'success',
80
+ details,
81
+ timestamp: Date.now(),
82
+ };
83
+ setAgentActions(prev => [...prev, actionLog]);
84
+ },
85
+ onToolResult: (result, toolCall) => {
86
+ const actionLog = createActionLog(toolCall, result);
87
+ setAgentActions(prev => {
88
+ const updated = [...prev];
89
+ if (updated.length > 0) {
90
+ updated[updated.length - 1] = actionLog;
91
+ }
92
+ return updated;
93
+ });
94
+ },
95
+ onThinking: (text) => {
96
+ const cleanText = text
97
+ .replace(/<think>[\s\S]*?<\/think>/gi, '')
98
+ .replace(/<tool_call>[\s\S]*?<\/tool_call>/gi, '')
99
+ .replace(/<toolcall>[\s\S]*?<\/toolcall>/gi, '')
100
+ .trim();
101
+ if (cleanText) {
102
+ setAgentThinking(prev => prev + cleanText);
103
+ }
104
+ },
105
+ abortSignal: controller.signal,
106
+ });
107
+ setAgentResult(result);
108
+ // Add agent summary as assistant message
109
+ const summaryMessage = {
110
+ role: 'assistant',
111
+ content: result.finalResponse || formatAgentResult(result),
112
+ };
113
+ onMessageAdd(summaryMessage);
114
+ // Auto-save session
115
+ autoSaveSession([...messages, userMessage, summaryMessage], projectPath);
116
+ if (result.success) {
117
+ notify(`Agent completed: ${result.actions.length} action(s)`);
118
+ }
119
+ else if (result.aborted) {
120
+ notify('Agent stopped by user');
121
+ }
122
+ else {
123
+ notify(`Agent failed: ${result.error}`);
124
+ }
125
+ }
126
+ catch (error) {
127
+ const err = error;
128
+ notify(`Agent error: ${err.message}`);
129
+ }
130
+ finally {
131
+ setIsAgentRunning(false);
132
+ abortControllerRef.current = null;
133
+ setAgentThinking('');
134
+ }
135
+ }, [projectContext, hasWriteAccess, messages, projectPath, onMessageAdd, notify]);
136
+ return {
137
+ isAgentRunning,
138
+ agentIteration,
139
+ agentActions,
140
+ agentThinking,
141
+ agentResult,
142
+ agentDryRun,
143
+ startAgent,
144
+ stopAgent,
145
+ clearAgentState,
146
+ };
147
+ }
148
+ export default useAgent;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Terminal utilities for better rendering control
3
+ * Implements DEC Mode 2026 (Synchronized Output) and other optimizations
4
+ */
5
+ import { WriteStream } from 'tty';
6
+ /**
7
+ * Check if terminal supports synchronized output (DEC 2026)
8
+ * Modern terminals: Ghostty, iTerm2 3.5+, Kitty, WezTerm, VSCode 1.80+
9
+ */
10
+ export declare function supportsSynchronizedOutput(): boolean;
11
+ /**
12
+ * Wrap stdout.write to use synchronized output when available
13
+ */
14
+ export declare function createSyncWriter(stdout: WriteStream | undefined): {
15
+ startSync: () => void;
16
+ endSync: () => void;
17
+ write: (data: string) => void;
18
+ };
19
+ /**
20
+ * Hide cursor during heavy rendering operations
21
+ */
22
+ export declare function hideCursor(stdout: WriteStream | undefined): void;
23
+ /**
24
+ * Show cursor after rendering
25
+ */
26
+ export declare function showCursor(stdout: WriteStream | undefined): void;
27
+ /**
28
+ * Clear N lines above cursor without scrolling
29
+ * More targeted than full screen clear
30
+ */
31
+ export declare function clearLinesAbove(stdout: WriteStream | undefined, lines: number): void;
32
+ /**
33
+ * Move cursor to specific line (relative to current position)
34
+ */
35
+ export declare function moveCursor(stdout: WriteStream | undefined, lines: number): void;
36
+ /**
37
+ * Request terminal size (for responsive layouts)
38
+ */
39
+ export declare function getTerminalSize(stdout: WriteStream | undefined): {
40
+ columns: number;
41
+ rows: number;
42
+ };
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Terminal utilities for better rendering control
3
+ * Implements DEC Mode 2026 (Synchronized Output) and other optimizations
4
+ */
5
+ // DEC Mode 2026 - Synchronized Output
6
+ // Tells terminal to batch all output until end marker
7
+ const SYNC_START = '\x1b[?2026h';
8
+ const SYNC_END = '\x1b[?2026l';
9
+ // Alternate screen buffer (not used - loses scroll history)
10
+ // const ALT_SCREEN_ON = '\x1b[?1049h';
11
+ // const ALT_SCREEN_OFF = '\x1b[?1049l';
12
+ // Cursor visibility
13
+ const CURSOR_HIDE = '\x1b[?25l';
14
+ const CURSOR_SHOW = '\x1b[?25h';
15
+ // Screen clearing (avoid - causes scroll jump)
16
+ // const CLEAR_SCREEN = '\x1b[2J';
17
+ // const CLEAR_SCROLLBACK = '\x1b[3J';
18
+ /**
19
+ * Check if terminal supports synchronized output (DEC 2026)
20
+ * Modern terminals: Ghostty, iTerm2 3.5+, Kitty, WezTerm, VSCode 1.80+
21
+ */
22
+ export function supportsSynchronizedOutput() {
23
+ const term = process.env.TERM_PROGRAM?.toLowerCase() || '';
24
+ const termEnv = process.env.TERM?.toLowerCase() || '';
25
+ // Known supported terminals
26
+ const supportedTerminals = [
27
+ 'ghostty',
28
+ 'iterm.app',
29
+ 'iterm2',
30
+ 'kitty',
31
+ 'wezterm',
32
+ 'vscode',
33
+ 'alacritty', // 0.13+
34
+ ];
35
+ // Check TERM_PROGRAM
36
+ if (supportedTerminals.some(t => term.includes(t))) {
37
+ return true;
38
+ }
39
+ // Check for xterm-256color with modern terminal
40
+ if (termEnv.includes('xterm') || termEnv.includes('256color')) {
41
+ // Most modern terminals report as xterm-256color
42
+ // We'll enable sync output and let it gracefully degrade if not supported
43
+ return true;
44
+ }
45
+ return false;
46
+ }
47
+ /**
48
+ * Wrap stdout.write to use synchronized output when available
49
+ */
50
+ export function createSyncWriter(stdout) {
51
+ const syncSupported = supportsSynchronizedOutput();
52
+ let inSync = false;
53
+ return {
54
+ startSync: () => {
55
+ if (syncSupported && stdout && !inSync) {
56
+ stdout.write(SYNC_START);
57
+ inSync = true;
58
+ }
59
+ },
60
+ endSync: () => {
61
+ if (syncSupported && stdout && inSync) {
62
+ stdout.write(SYNC_END);
63
+ inSync = false;
64
+ }
65
+ },
66
+ write: (data) => {
67
+ stdout?.write(data);
68
+ },
69
+ };
70
+ }
71
+ /**
72
+ * Hide cursor during heavy rendering operations
73
+ */
74
+ export function hideCursor(stdout) {
75
+ stdout?.write(CURSOR_HIDE);
76
+ }
77
+ /**
78
+ * Show cursor after rendering
79
+ */
80
+ export function showCursor(stdout) {
81
+ stdout?.write(CURSOR_SHOW);
82
+ }
83
+ /**
84
+ * Clear N lines above cursor without scrolling
85
+ * More targeted than full screen clear
86
+ */
87
+ export function clearLinesAbove(stdout, lines) {
88
+ if (!stdout || lines <= 0)
89
+ return;
90
+ let seq = '';
91
+ seq += `\x1b[${lines}A`; // Move up N lines
92
+ for (let i = 0; i < lines; i++) {
93
+ seq += '\x1b[2K'; // Clear line
94
+ if (i < lines - 1)
95
+ seq += '\x1b[B'; // Move down (except last)
96
+ }
97
+ seq += `\x1b[${lines - 1}A`; // Move back to top of cleared area
98
+ stdout.write(seq);
99
+ }
100
+ /**
101
+ * Move cursor to specific line (relative to current position)
102
+ */
103
+ export function moveCursor(stdout, lines) {
104
+ if (!stdout || lines === 0)
105
+ return;
106
+ if (lines > 0) {
107
+ stdout.write(`\x1b[${lines}B`); // Move down
108
+ }
109
+ else {
110
+ stdout.write(`\x1b[${Math.abs(lines)}A`); // Move up
111
+ }
112
+ }
113
+ /**
114
+ * Request terminal size (for responsive layouts)
115
+ */
116
+ export function getTerminalSize(stdout) {
117
+ return {
118
+ columns: stdout?.columns || 80,
119
+ rows: stdout?.rows || 24,
120
+ };
121
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeep",
3
- "version": "1.0.105",
3
+ "version": "1.0.107",
4
4
  "description": "AI-powered coding assistant built for the terminal. Multiple LLM providers, project-aware context, and a seamless development workflow.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",