codeep 1.0.106 → 1.0.108

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.
@@ -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('');
@@ -174,7 +180,7 @@ export const LiveCodeStream = ({ actions, isRunning, terminalWidth = 80 }) => {
174
180
  return () => clearTimeout(timer);
175
181
  }
176
182
  }, [isRunning, currentAction, visibleLineCount]);
177
- // Only show for write/edit actions with content while running
183
+ // Only show for write/edit actions with content
178
184
  if (!isRunning || !currentAction)
179
185
  return null;
180
186
  if (currentAction.type !== 'write' && currentAction.type !== 'edit')
@@ -188,6 +194,18 @@ export const LiveCodeStream = ({ actions, isRunning, terminalWidth = 80 }) => {
188
194
  const langLabel = getLanguageLabel(ext);
189
195
  const allLines = code.split('\n');
190
196
  const totalLines = allLines.length;
197
+ // When action completes, show compact summary with blank lines to overwrite ghost
198
+ const isCompleted = currentAction.result === 'success' || currentAction.result === 'error';
199
+ if (isCompleted) {
200
+ const statusIcon = currentAction.result === 'success' ? '✓' : '✗';
201
+ const statusColor = currentAction.result === 'success' ? 'green' : 'red';
202
+ const actionVerb = currentAction.type === 'write' ? 'Created' : 'Edited';
203
+ // Calculate how many blank lines we need to overwrite the previous code display
204
+ // Header (2) + code lines shown + footer (1) + loading indicator (1)
205
+ const previousHeight = Math.min(visibleLineCount, totalLines) + 4;
206
+ const blankLines = Math.max(0, previousHeight - 1); // -1 for our summary line
207
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: statusColor, children: [statusIcon, " ", actionVerb, " ", filename, " (", totalLines, " lines)"] }), Array.from({ length: blankLines }).map((_, i) => (_jsx(Text, { children: " " }, i)))] }));
208
+ }
191
209
  const actionLabel = currentAction.type === 'write' ? '✨ Creating' : '✏️ Editing';
192
210
  const actionColor = currentAction.type === 'write' ? 'green' : 'yellow';
193
211
  // Show lines up to visibleLineCount
@@ -199,7 +217,8 @@ export const LiveCodeStream = ({ actions, isRunning, terminalWidth = 80 }) => {
199
217
  const lineNum = i + 1;
200
218
  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
219
  }), hasMoreLines && (_jsxs(Text, { color: "cyan", dimColor: true, children: [' ', "\u2502 ... ", totalLines - visibleLineCount, " more lines loading..."] })), _jsx(Text, { color: actionColor, children: '─'.repeat(lineWidth) })] }));
202
- };
220
+ });
221
+ LiveCodeStream.displayName = 'LiveCodeStream';
203
222
  // Helper functions for action display
204
223
  const getActionColor = (type) => {
205
224
  switch (type) {
@@ -257,7 +276,7 @@ const ActionItem = ({ action }) => {
257
276
  };
258
277
  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
278
  };
260
- export const AgentSummary = ({ success, iterations, actions, error, aborted, }) => {
279
+ export const AgentSummary = memo(({ success, iterations, actions, error, aborted, }) => {
261
280
  const filesWritten = actions.filter(a => a.type === 'write' && a.result === 'success');
262
281
  const filesEdited = actions.filter(a => a.type === 'edit' && a.result === 'success');
263
282
  const filesDeleted = actions.filter(a => a.type === 'delete' && a.result === 'success');
@@ -267,7 +286,8 @@ export const AgentSummary = ({ success, iterations, actions, error, aborted, })
267
286
  const hasFileChanges = filesWritten.length > 0 || filesEdited.length > 0 ||
268
287
  filesDeleted.length > 0 || dirsCreated.length > 0;
269
288
  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
- };
289
+ });
290
+ AgentSummary.displayName = 'AgentSummary';
271
291
  export const ChangesList = ({ actions }) => {
272
292
  const writes = actions.filter(a => a.type === 'write' && a.result === 'success');
273
293
  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.106",
3
+ "version": "1.0.108",
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",