codeep 1.0.106 → 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/components/AgentProgress.d.ts +1 -0
- package/dist/components/AgentProgress.js +22 -14
- package/dist/components/Loading.d.ts +8 -0
- package/dist/components/Loading.js +41 -20
- package/dist/components/MessageList.d.ts +7 -2
- package/dist/components/MessageList.js +22 -5
- package/dist/components/Spinner.d.ts +34 -0
- package/dist/components/Spinner.js +38 -0
- package/dist/components/StreamingMessage.d.ts +14 -0
- package/dist/components/StreamingMessage.js +19 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/useAgent.d.ts +29 -0
- package/dist/hooks/useAgent.js +148 -0
- package/dist/utils/terminal.d.ts +42 -0
- package/dist/utils/terminal.js +121 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
17
|
+
setFrame(f => (f + 1) % SPINNER_FRAMES.length);
|
|
17
18
|
}, 150);
|
|
18
19
|
return () => clearInterval(timer);
|
|
19
|
-
}, [
|
|
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: [
|
|
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 {
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
14
|
+
setFrame(f => (f + 1) % SPINNER.length);
|
|
12
15
|
}, 100);
|
|
13
16
|
return () => clearInterval(timer);
|
|
14
17
|
}, []);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const [
|
|
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
|
-
|
|
27
|
-
},
|
|
29
|
+
setCount(c => (c + 1) % 4);
|
|
30
|
+
}, 300);
|
|
28
31
|
return () => clearInterval(timer);
|
|
29
32
|
}, []);
|
|
30
|
-
return (
|
|
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
|
|
7
|
-
terminalHeight
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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,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.
|
|
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",
|