deepagentsdk 0.9.2
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/LICENSE +21 -0
- package/README.md +159 -0
- package/package.json +95 -0
- package/src/agent.ts +1230 -0
- package/src/backends/composite.ts +273 -0
- package/src/backends/filesystem.ts +692 -0
- package/src/backends/index.ts +22 -0
- package/src/backends/local-sandbox.ts +175 -0
- package/src/backends/persistent.ts +593 -0
- package/src/backends/sandbox.ts +510 -0
- package/src/backends/state.ts +244 -0
- package/src/backends/utils.ts +287 -0
- package/src/checkpointer/file-saver.ts +98 -0
- package/src/checkpointer/index.ts +5 -0
- package/src/checkpointer/kv-saver.ts +82 -0
- package/src/checkpointer/memory-saver.ts +82 -0
- package/src/checkpointer/types.ts +125 -0
- package/src/cli/components/ApiKeyInput.tsx +300 -0
- package/src/cli/components/FilePreview.tsx +237 -0
- package/src/cli/components/Input.tsx +277 -0
- package/src/cli/components/Message.tsx +93 -0
- package/src/cli/components/ModelSelection.tsx +338 -0
- package/src/cli/components/SlashMenu.tsx +101 -0
- package/src/cli/components/StatusBar.tsx +89 -0
- package/src/cli/components/Subagent.tsx +91 -0
- package/src/cli/components/TodoList.tsx +133 -0
- package/src/cli/components/ToolApproval.tsx +70 -0
- package/src/cli/components/ToolCall.tsx +144 -0
- package/src/cli/components/ToolCallSummary.tsx +175 -0
- package/src/cli/components/Welcome.tsx +75 -0
- package/src/cli/components/index.ts +24 -0
- package/src/cli/hooks/index.ts +12 -0
- package/src/cli/hooks/useAgent.ts +933 -0
- package/src/cli/index.tsx +1066 -0
- package/src/cli/theme.ts +205 -0
- package/src/cli/utils/model-list.ts +365 -0
- package/src/constants/errors.ts +29 -0
- package/src/constants/limits.ts +195 -0
- package/src/index.ts +176 -0
- package/src/middleware/agent-memory.ts +330 -0
- package/src/prompts.ts +196 -0
- package/src/skills/index.ts +2 -0
- package/src/skills/load.ts +191 -0
- package/src/skills/types.ts +53 -0
- package/src/tools/execute.ts +167 -0
- package/src/tools/filesystem.ts +418 -0
- package/src/tools/index.ts +39 -0
- package/src/tools/subagent.ts +443 -0
- package/src/tools/todos.ts +101 -0
- package/src/tools/web.ts +567 -0
- package/src/types/backend.ts +177 -0
- package/src/types/core.ts +220 -0
- package/src/types/events.ts +429 -0
- package/src/types/index.ts +94 -0
- package/src/types/structured-output.ts +43 -0
- package/src/types/subagent.ts +96 -0
- package/src/types.ts +22 -0
- package/src/utils/approval.ts +213 -0
- package/src/utils/events.ts +416 -0
- package/src/utils/eviction.ts +181 -0
- package/src/utils/index.ts +34 -0
- package/src/utils/model-parser.ts +38 -0
- package/src/utils/patch-tool-calls.ts +233 -0
- package/src/utils/project-detection.ts +32 -0
- package/src/utils/summarization.ts +254 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File preview component with line numbers.
|
|
3
|
+
*/
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { Box, Text } from "ink";
|
|
6
|
+
import { emoji, colors } from "../theme.js";
|
|
7
|
+
import type { FileInfo } from "../../types.js";
|
|
8
|
+
|
|
9
|
+
interface FilePreviewProps {
|
|
10
|
+
/** File path */
|
|
11
|
+
path: string;
|
|
12
|
+
/** File content */
|
|
13
|
+
content: string;
|
|
14
|
+
/** Maximum lines to show */
|
|
15
|
+
maxLines?: number;
|
|
16
|
+
/** Whether this is a write preview (vs read) */
|
|
17
|
+
isWrite?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function FilePreview({
|
|
21
|
+
path,
|
|
22
|
+
content,
|
|
23
|
+
maxLines = 20,
|
|
24
|
+
isWrite = false,
|
|
25
|
+
}: FilePreviewProps): React.ReactElement {
|
|
26
|
+
const lines = content.split("\n");
|
|
27
|
+
const totalLines = lines.length;
|
|
28
|
+
const displayLines = lines.slice(0, maxLines);
|
|
29
|
+
const truncated = totalLines > maxLines;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Box
|
|
33
|
+
flexDirection="column"
|
|
34
|
+
borderStyle="single"
|
|
35
|
+
borderColor={colors.muted}
|
|
36
|
+
marginY={1}
|
|
37
|
+
>
|
|
38
|
+
{/* Header */}
|
|
39
|
+
<Box paddingX={2} paddingY={1} borderBottom>
|
|
40
|
+
<Text color={colors.info}>
|
|
41
|
+
{emoji.file} {isWrite ? "Writing:" : "Reading:"}{" "}
|
|
42
|
+
</Text>
|
|
43
|
+
<Text color={colors.file}>{path}</Text>
|
|
44
|
+
<Text dimColor> ({totalLines} lines)</Text>
|
|
45
|
+
</Box>
|
|
46
|
+
|
|
47
|
+
{/* Content with line numbers */}
|
|
48
|
+
<Box flexDirection="column" paddingX={2} paddingY={1}>
|
|
49
|
+
{displayLines.map((line, index) => (
|
|
50
|
+
<Box key={index}>
|
|
51
|
+
<Text dimColor>{String(index + 1).padStart(4, " ")} </Text>
|
|
52
|
+
<Text>{truncateLine(line, 70)}</Text>
|
|
53
|
+
</Box>
|
|
54
|
+
))}
|
|
55
|
+
{truncated && (
|
|
56
|
+
<Box marginTop={1}>
|
|
57
|
+
<Text dimColor>
|
|
58
|
+
... {totalLines - maxLines} more lines ...
|
|
59
|
+
</Text>
|
|
60
|
+
</Box>
|
|
61
|
+
)}
|
|
62
|
+
</Box>
|
|
63
|
+
</Box>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Truncate a line if too long.
|
|
69
|
+
*/
|
|
70
|
+
function truncateLine(line: string, maxLength: number): string {
|
|
71
|
+
if (line.length <= maxLength) {
|
|
72
|
+
return line;
|
|
73
|
+
}
|
|
74
|
+
return line.substring(0, maxLength - 3) + "...";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Compact file written notification.
|
|
79
|
+
*/
|
|
80
|
+
interface FileWrittenProps {
|
|
81
|
+
path: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function FileWritten({ path }: FileWrittenProps): React.ReactElement {
|
|
85
|
+
return (
|
|
86
|
+
<Box>
|
|
87
|
+
<Text color={colors.success}>✓ Wrote: </Text>
|
|
88
|
+
<Text color={colors.file}>{path}</Text>
|
|
89
|
+
</Box>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Compact file edited notification.
|
|
95
|
+
*/
|
|
96
|
+
interface FileEditedProps {
|
|
97
|
+
path: string;
|
|
98
|
+
occurrences: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function FileEdited({
|
|
102
|
+
path,
|
|
103
|
+
occurrences,
|
|
104
|
+
}: FileEditedProps): React.ReactElement {
|
|
105
|
+
return (
|
|
106
|
+
<Box>
|
|
107
|
+
<Text color={colors.success}>{emoji.edit} Edited: </Text>
|
|
108
|
+
<Text color={colors.file}>{path}</Text>
|
|
109
|
+
<Text dimColor>
|
|
110
|
+
{" "}
|
|
111
|
+
({occurrences} change{occurrences === 1 ? "" : "s"})
|
|
112
|
+
</Text>
|
|
113
|
+
</Box>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Compact file read notification.
|
|
119
|
+
*/
|
|
120
|
+
interface FileReadProps {
|
|
121
|
+
path: string;
|
|
122
|
+
lines: number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function FileRead({ path, lines }: FileReadProps): React.ReactElement {
|
|
126
|
+
return (
|
|
127
|
+
<Box>
|
|
128
|
+
<Text color={colors.info}>📖 Read: </Text>
|
|
129
|
+
<Text color={colors.file}>{path}</Text>
|
|
130
|
+
<Text dimColor> ({lines} lines)</Text>
|
|
131
|
+
</Box>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Compact ls result notification.
|
|
137
|
+
*/
|
|
138
|
+
interface LsResultProps {
|
|
139
|
+
path: string;
|
|
140
|
+
count: number;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function LsResult({ path, count }: LsResultProps): React.ReactElement {
|
|
144
|
+
return (
|
|
145
|
+
<Box>
|
|
146
|
+
<Text color={colors.info}>📂 Listed: </Text>
|
|
147
|
+
<Text color={colors.file}>{path}</Text>
|
|
148
|
+
<Text dimColor> ({count} item{count === 1 ? "" : "s"})</Text>
|
|
149
|
+
</Box>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Compact glob result notification.
|
|
155
|
+
*/
|
|
156
|
+
interface GlobResultProps {
|
|
157
|
+
pattern: string;
|
|
158
|
+
count: number;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function GlobResult({ pattern, count }: GlobResultProps): React.ReactElement {
|
|
162
|
+
return (
|
|
163
|
+
<Box>
|
|
164
|
+
<Text color={colors.info}>🔍 Glob: </Text>
|
|
165
|
+
<Text color={colors.tool}>{pattern}</Text>
|
|
166
|
+
<Text dimColor> ({count} match{count === 1 ? "" : "es"})</Text>
|
|
167
|
+
</Box>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Compact grep result notification.
|
|
173
|
+
*/
|
|
174
|
+
interface GrepResultProps {
|
|
175
|
+
pattern: string;
|
|
176
|
+
count: number;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function GrepResult({ pattern, count }: GrepResultProps): React.ReactElement {
|
|
180
|
+
return (
|
|
181
|
+
<Box>
|
|
182
|
+
<Text color={colors.info}>🔎 Grep: </Text>
|
|
183
|
+
<Text color={colors.tool}>{pattern}</Text>
|
|
184
|
+
<Text dimColor> ({count} match{count === 1 ? "" : "es"})</Text>
|
|
185
|
+
</Box>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* File list panel for /files command.
|
|
191
|
+
*/
|
|
192
|
+
interface FileListProps {
|
|
193
|
+
files: FileInfo[];
|
|
194
|
+
workDir?: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function FileList({
|
|
198
|
+
files,
|
|
199
|
+
workDir,
|
|
200
|
+
}: FileListProps): React.ReactElement {
|
|
201
|
+
return (
|
|
202
|
+
<Box
|
|
203
|
+
flexDirection="column"
|
|
204
|
+
borderStyle="single"
|
|
205
|
+
borderColor={colors.muted}
|
|
206
|
+
paddingX={2}
|
|
207
|
+
paddingY={1}
|
|
208
|
+
marginY={1}
|
|
209
|
+
>
|
|
210
|
+
<Box marginBottom={1}>
|
|
211
|
+
<Text bold color={colors.info}>
|
|
212
|
+
{emoji.file} Files
|
|
213
|
+
</Text>
|
|
214
|
+
{workDir && <Text dimColor> in {workDir}</Text>}
|
|
215
|
+
</Box>
|
|
216
|
+
|
|
217
|
+
{files.length === 0 ? (
|
|
218
|
+
<Box paddingLeft={2}>
|
|
219
|
+
<Text dimColor>No files found.</Text>
|
|
220
|
+
</Box>
|
|
221
|
+
) : (
|
|
222
|
+
<Box flexDirection="column">
|
|
223
|
+
{files.map((file) => (
|
|
224
|
+
<Box key={file.path} paddingLeft={2}>
|
|
225
|
+
<Text>{file.is_dir ? "📁" : "📄"} </Text>
|
|
226
|
+
<Text color={colors.file}>{file.path}</Text>
|
|
227
|
+
{file.size !== undefined && (
|
|
228
|
+
<Text dimColor> ({file.size} bytes)</Text>
|
|
229
|
+
)}
|
|
230
|
+
</Box>
|
|
231
|
+
))}
|
|
232
|
+
</Box>
|
|
233
|
+
)}
|
|
234
|
+
</Box>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text input component with slash command suggestions.
|
|
3
|
+
* Clean, minimal design inspired by Claude Code and OpenAI Codex.
|
|
4
|
+
*/
|
|
5
|
+
import React, { useState, useRef } from "react";
|
|
6
|
+
import { Box, Text, useInput } from "ink";
|
|
7
|
+
import { colors } from "../theme.js";
|
|
8
|
+
import { SlashMenu } from "./SlashMenu.js";
|
|
9
|
+
|
|
10
|
+
interface InputProps {
|
|
11
|
+
/** Called when user submits input */
|
|
12
|
+
onSubmit: (value: string) => void;
|
|
13
|
+
/** Whether input is disabled (e.g., during generation) */
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
/** Placeholder text */
|
|
16
|
+
placeholder?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Module-level history storage to persist across re-renders
|
|
20
|
+
const inputHistory: string[] = [];
|
|
21
|
+
const MAX_HISTORY = 100;
|
|
22
|
+
|
|
23
|
+
export function Input({
|
|
24
|
+
onSubmit,
|
|
25
|
+
disabled = false,
|
|
26
|
+
placeholder = "Plan, search, build anything",
|
|
27
|
+
}: InputProps): React.ReactElement {
|
|
28
|
+
const [value, setValue] = useState("");
|
|
29
|
+
const [cursorPos, setCursorPos] = useState(0);
|
|
30
|
+
const showMenu = value.startsWith("/") && !disabled;
|
|
31
|
+
|
|
32
|
+
// History navigation state
|
|
33
|
+
// -1 means we're at the current input (not browsing history)
|
|
34
|
+
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
35
|
+
// Store the current input when user starts navigating history
|
|
36
|
+
const savedInputRef = useRef("");
|
|
37
|
+
|
|
38
|
+
// Helper function to delete the previous word from cursor position
|
|
39
|
+
const deleteWord = () => {
|
|
40
|
+
if (cursorPos === 0) return;
|
|
41
|
+
|
|
42
|
+
let end = cursorPos;
|
|
43
|
+
// Skip trailing spaces
|
|
44
|
+
while (end > 0 && value[end - 1] === " ") {
|
|
45
|
+
end--;
|
|
46
|
+
}
|
|
47
|
+
// Find start of current word
|
|
48
|
+
while (end > 0 && value[end - 1] !== " ") {
|
|
49
|
+
end--;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const newValue = value.slice(0, end) + value.slice(cursorPos);
|
|
53
|
+
setValue(newValue);
|
|
54
|
+
setCursorPos(end);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
useInput(
|
|
58
|
+
(input, key) => {
|
|
59
|
+
if (disabled) return;
|
|
60
|
+
|
|
61
|
+
// Handle Enter - submit
|
|
62
|
+
if (key.return) {
|
|
63
|
+
if (value.trim()) {
|
|
64
|
+
// Add to history (avoid duplicates of the last entry)
|
|
65
|
+
if (inputHistory.length === 0 || inputHistory[0] !== value) {
|
|
66
|
+
inputHistory.unshift(value);
|
|
67
|
+
if (inputHistory.length > MAX_HISTORY) {
|
|
68
|
+
inputHistory.pop();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
onSubmit(value);
|
|
72
|
+
setValue("");
|
|
73
|
+
setCursorPos(0);
|
|
74
|
+
setHistoryIndex(-1);
|
|
75
|
+
savedInputRef.current = "";
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Handle up arrow - navigate to older history
|
|
81
|
+
if (key.upArrow) {
|
|
82
|
+
if (inputHistory.length === 0) return;
|
|
83
|
+
|
|
84
|
+
if (historyIndex === -1) {
|
|
85
|
+
// Save current input before navigating history
|
|
86
|
+
savedInputRef.current = value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const newIndex = Math.min(historyIndex + 1, inputHistory.length - 1);
|
|
90
|
+
if (newIndex !== historyIndex) {
|
|
91
|
+
const historyValue = inputHistory[newIndex];
|
|
92
|
+
if (historyValue !== undefined) {
|
|
93
|
+
setHistoryIndex(newIndex);
|
|
94
|
+
setValue(historyValue);
|
|
95
|
+
setCursorPos(historyValue.length);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Handle down arrow - navigate to newer history
|
|
102
|
+
if (key.downArrow) {
|
|
103
|
+
if (historyIndex === -1) return;
|
|
104
|
+
|
|
105
|
+
const newIndex = historyIndex - 1;
|
|
106
|
+
if (newIndex === -1) {
|
|
107
|
+
// Return to saved current input
|
|
108
|
+
setHistoryIndex(-1);
|
|
109
|
+
setValue(savedInputRef.current);
|
|
110
|
+
setCursorPos(savedInputRef.current.length);
|
|
111
|
+
} else {
|
|
112
|
+
const historyValue = inputHistory[newIndex];
|
|
113
|
+
if (historyValue !== undefined) {
|
|
114
|
+
setHistoryIndex(newIndex);
|
|
115
|
+
setValue(historyValue);
|
|
116
|
+
setCursorPos(historyValue.length);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Handle left arrow - move cursor left
|
|
123
|
+
if (key.leftArrow) {
|
|
124
|
+
if (key.meta || key.ctrl) {
|
|
125
|
+
// Option/Ctrl+Left: jump to start of previous word
|
|
126
|
+
let pos = cursorPos;
|
|
127
|
+
// Skip spaces
|
|
128
|
+
while (pos > 0 && value[pos - 1] === " ") {
|
|
129
|
+
pos--;
|
|
130
|
+
}
|
|
131
|
+
// Skip word characters
|
|
132
|
+
while (pos > 0 && value[pos - 1] !== " ") {
|
|
133
|
+
pos--;
|
|
134
|
+
}
|
|
135
|
+
setCursorPos(pos);
|
|
136
|
+
} else {
|
|
137
|
+
setCursorPos((prev) => Math.max(0, prev - 1));
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Handle right arrow - move cursor right
|
|
143
|
+
if (key.rightArrow) {
|
|
144
|
+
if (key.meta || key.ctrl) {
|
|
145
|
+
// Option/Ctrl+Right: jump to end of next word
|
|
146
|
+
let pos = cursorPos;
|
|
147
|
+
// Skip current word characters
|
|
148
|
+
while (pos < value.length && value[pos] !== " ") {
|
|
149
|
+
pos++;
|
|
150
|
+
}
|
|
151
|
+
// Skip spaces
|
|
152
|
+
while (pos < value.length && value[pos] === " ") {
|
|
153
|
+
pos++;
|
|
154
|
+
}
|
|
155
|
+
setCursorPos(pos);
|
|
156
|
+
} else {
|
|
157
|
+
setCursorPos((prev) => Math.min(value.length, prev + 1));
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Handle Ctrl+A - move to start of line
|
|
163
|
+
if (key.ctrl && input === "a") {
|
|
164
|
+
setCursorPos(0);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Handle Ctrl+E - move to end of line
|
|
169
|
+
if (key.ctrl && input === "e") {
|
|
170
|
+
setCursorPos(value.length);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Handle Option+Backspace (Alt+Backspace) - delete previous word
|
|
175
|
+
if ((key.backspace || key.delete) && key.meta) {
|
|
176
|
+
deleteWord();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Handle Ctrl+W - delete previous word (Unix-style)
|
|
181
|
+
if (key.ctrl && input === "w") {
|
|
182
|
+
deleteWord();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Handle Ctrl+U - delete from start to cursor
|
|
187
|
+
if (key.ctrl && input === "u") {
|
|
188
|
+
setValue(value.slice(cursorPos));
|
|
189
|
+
setCursorPos(0);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Handle Ctrl+K - delete from cursor to end
|
|
194
|
+
if (key.ctrl && input === "k") {
|
|
195
|
+
setValue(value.slice(0, cursorPos));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Handle Backspace/Delete - single character
|
|
200
|
+
if (key.backspace || key.delete) {
|
|
201
|
+
if (cursorPos > 0) {
|
|
202
|
+
setValue((prev) => prev.slice(0, cursorPos - 1) + prev.slice(cursorPos));
|
|
203
|
+
setCursorPos((prev) => prev - 1);
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Tab for autocomplete - complete first matching command
|
|
209
|
+
if (key.tab && value.startsWith("/")) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Ignore other control keys
|
|
214
|
+
if (key.ctrl || key.meta || key.escape || key.tab) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Handle pasted text or typed characters
|
|
219
|
+
// Filter to only printable characters
|
|
220
|
+
if (input) {
|
|
221
|
+
const printable = input
|
|
222
|
+
.split("")
|
|
223
|
+
.filter((char) => char >= " " || char === "\t")
|
|
224
|
+
.join("");
|
|
225
|
+
|
|
226
|
+
if (printable) {
|
|
227
|
+
// Reset history navigation when user types
|
|
228
|
+
if (historyIndex !== -1) {
|
|
229
|
+
setHistoryIndex(-1);
|
|
230
|
+
savedInputRef.current = "";
|
|
231
|
+
}
|
|
232
|
+
setValue((prev) => prev.slice(0, cursorPos) + printable + prev.slice(cursorPos));
|
|
233
|
+
setCursorPos((prev) => prev + printable.length);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
{ isActive: !disabled }
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// Render text with cursor at the correct position
|
|
241
|
+
const renderTextWithCursor = () => {
|
|
242
|
+
if (!value) {
|
|
243
|
+
return (
|
|
244
|
+
<Text>
|
|
245
|
+
<Text color={colors.primary}>▌</Text>
|
|
246
|
+
<Text dimColor>{placeholder}</Text>
|
|
247
|
+
</Text>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const beforeCursor = value.slice(0, cursorPos);
|
|
252
|
+
const afterCursor = value.slice(cursorPos);
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
<Text>
|
|
256
|
+
{beforeCursor}
|
|
257
|
+
<Text color={colors.primary}>▌</Text>
|
|
258
|
+
{afterCursor}
|
|
259
|
+
</Text>
|
|
260
|
+
);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<Box flexDirection="column">
|
|
265
|
+
<Box>
|
|
266
|
+
<Text color={colors.muted}>{"→ "}</Text>
|
|
267
|
+
{disabled ? (
|
|
268
|
+
<Text dimColor>...</Text>
|
|
269
|
+
) : (
|
|
270
|
+
renderTextWithCursor()
|
|
271
|
+
)}
|
|
272
|
+
</Box>
|
|
273
|
+
{showMenu && <SlashMenu filter={value} />}
|
|
274
|
+
</Box>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message display component for user and assistant messages.
|
|
3
|
+
* Clean, minimal design inspired by Claude Code and OpenAI Codex.
|
|
4
|
+
*/
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { Box, Text } from "ink";
|
|
7
|
+
import { colors } from "../theme.js";
|
|
8
|
+
import { ToolCallSummary } from "./ToolCallSummary.js";
|
|
9
|
+
|
|
10
|
+
export type MessageRole = "user" | "assistant";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Data for a single tool call.
|
|
14
|
+
*/
|
|
15
|
+
export interface ToolCallData {
|
|
16
|
+
/** Tool name that was called */
|
|
17
|
+
toolName: string;
|
|
18
|
+
/** Arguments passed to the tool */
|
|
19
|
+
args?: unknown;
|
|
20
|
+
/** Result returned by the tool */
|
|
21
|
+
result?: unknown;
|
|
22
|
+
/** Whether the tool call succeeded or failed */
|
|
23
|
+
status: "success" | "error";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface MessageData {
|
|
27
|
+
id: string;
|
|
28
|
+
role: MessageRole;
|
|
29
|
+
content: string;
|
|
30
|
+
timestamp?: Date;
|
|
31
|
+
/** Tool calls made during this message (for assistant messages) */
|
|
32
|
+
toolCalls?: ToolCallData[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface MessageProps {
|
|
36
|
+
message: MessageData;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function Message({ message }: MessageProps): React.ReactElement {
|
|
40
|
+
const isUser = message.role === "user";
|
|
41
|
+
const hasToolCalls = message.toolCalls && message.toolCalls.length > 0;
|
|
42
|
+
|
|
43
|
+
if (isUser) {
|
|
44
|
+
// User message: "> message" style (like Claude Code)
|
|
45
|
+
return (
|
|
46
|
+
<Box marginBottom={1}>
|
|
47
|
+
<Text color={colors.muted} bold>{"> "}</Text>
|
|
48
|
+
<Text bold>{message.content}</Text>
|
|
49
|
+
</Box>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Assistant message: "● message" style (like Claude Code)
|
|
54
|
+
return (
|
|
55
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
56
|
+
<Box>
|
|
57
|
+
<Text color={colors.success}>{"● "}</Text>
|
|
58
|
+
<Text>{message.content}</Text>
|
|
59
|
+
</Box>
|
|
60
|
+
{/* Show tool calls summary for assistant messages */}
|
|
61
|
+
{hasToolCalls && (
|
|
62
|
+
<Box marginLeft={2}>
|
|
63
|
+
<ToolCallSummary toolCalls={message.toolCalls!} />
|
|
64
|
+
</Box>
|
|
65
|
+
)}
|
|
66
|
+
</Box>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface StreamingMessageProps {
|
|
71
|
+
/** Current streamed text content */
|
|
72
|
+
content: string;
|
|
73
|
+
/** Whether the message is still streaming */
|
|
74
|
+
isStreaming?: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function StreamingMessage({
|
|
78
|
+
content,
|
|
79
|
+
isStreaming = true,
|
|
80
|
+
}: StreamingMessageProps): React.ReactElement {
|
|
81
|
+
return (
|
|
82
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
83
|
+
<Box>
|
|
84
|
+
<Text color={colors.success}>{"● "}</Text>
|
|
85
|
+
<Text>
|
|
86
|
+
{content}
|
|
87
|
+
{isStreaming && <Text color={colors.muted}>▌</Text>}
|
|
88
|
+
</Text>
|
|
89
|
+
</Box>
|
|
90
|
+
</Box>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|