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,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model Selection Panel - Interactive model selection with arrow keys.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React, { useState, useEffect, useMemo } from "react";
|
|
6
|
+
import { Box, Text, useInput } from "ink";
|
|
7
|
+
import { Spinner } from "@inkjs/ui";
|
|
8
|
+
import { colors, emoji } from "../theme.js";
|
|
9
|
+
import {
|
|
10
|
+
getModelsByProvider,
|
|
11
|
+
detectAvailableProviders,
|
|
12
|
+
type AvailableModel,
|
|
13
|
+
} from "../utils/model-list.js";
|
|
14
|
+
|
|
15
|
+
interface ModelSelectionPanelProps {
|
|
16
|
+
currentModel?: string;
|
|
17
|
+
/** Callback when a model is selected */
|
|
18
|
+
onModelSelect?: (modelId: string) => void;
|
|
19
|
+
/** Callback to close the panel */
|
|
20
|
+
onClose?: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface LoadingState {
|
|
24
|
+
loading: boolean;
|
|
25
|
+
anthropicModels: AvailableModel[];
|
|
26
|
+
openaiModels: AvailableModel[];
|
|
27
|
+
errors: { provider: string; error: string }[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function ModelSelectionPanel({
|
|
31
|
+
currentModel,
|
|
32
|
+
onModelSelect,
|
|
33
|
+
onClose,
|
|
34
|
+
}: ModelSelectionPanelProps): React.ReactElement {
|
|
35
|
+
const providers = detectAvailableProviders();
|
|
36
|
+
const hasAnyKey = providers.anthropic || providers.openai;
|
|
37
|
+
|
|
38
|
+
const [state, setState] = useState<LoadingState>({
|
|
39
|
+
loading: true,
|
|
40
|
+
anthropicModels: [],
|
|
41
|
+
openaiModels: [],
|
|
42
|
+
errors: [],
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
46
|
+
|
|
47
|
+
// Combine all models into a flat list for navigation
|
|
48
|
+
const allModels = useMemo(() => {
|
|
49
|
+
const models: AvailableModel[] = [];
|
|
50
|
+
if (state.anthropicModels.length > 0) {
|
|
51
|
+
models.push(...state.anthropicModels);
|
|
52
|
+
}
|
|
53
|
+
if (state.openaiModels.length > 0) {
|
|
54
|
+
models.push(...state.openaiModels);
|
|
55
|
+
}
|
|
56
|
+
return models;
|
|
57
|
+
}, [state.anthropicModels, state.openaiModels]);
|
|
58
|
+
|
|
59
|
+
// Find current model index to set initial selection
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (allModels.length > 0 && currentModel) {
|
|
62
|
+
const currentIndex = allModels.findIndex((m) => isCurrentModel(currentModel, m));
|
|
63
|
+
if (currentIndex >= 0) {
|
|
64
|
+
setSelectedIndex(currentIndex);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}, [allModels, currentModel]);
|
|
68
|
+
|
|
69
|
+
// Handle keyboard input
|
|
70
|
+
useInput((input, key) => {
|
|
71
|
+
if (state.loading) return;
|
|
72
|
+
|
|
73
|
+
if (key.upArrow) {
|
|
74
|
+
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : allModels.length - 1));
|
|
75
|
+
} else if (key.downArrow) {
|
|
76
|
+
setSelectedIndex((prev) => (prev < allModels.length - 1 ? prev + 1 : 0));
|
|
77
|
+
} else if (key.return) {
|
|
78
|
+
const selectedModel = allModels[selectedIndex];
|
|
79
|
+
if (selectedModel) {
|
|
80
|
+
onModelSelect?.(selectedModel.id);
|
|
81
|
+
onClose?.();
|
|
82
|
+
}
|
|
83
|
+
} else if (key.escape) {
|
|
84
|
+
onClose?.();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Fetch models on mount
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!hasAnyKey) {
|
|
91
|
+
setState({ loading: false, anthropicModels: [], openaiModels: [], errors: [] });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let cancelled = false;
|
|
96
|
+
|
|
97
|
+
async function loadModels() {
|
|
98
|
+
try {
|
|
99
|
+
const result = await getModelsByProvider();
|
|
100
|
+
if (!cancelled) {
|
|
101
|
+
setState({
|
|
102
|
+
loading: false,
|
|
103
|
+
anthropicModels: result.anthropic || [],
|
|
104
|
+
openaiModels: result.openai || [],
|
|
105
|
+
errors: result.errors,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (!cancelled) {
|
|
110
|
+
setState({
|
|
111
|
+
loading: false,
|
|
112
|
+
anthropicModels: [],
|
|
113
|
+
openaiModels: [],
|
|
114
|
+
errors: [{ provider: "Unknown", error: String(error) }],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
loadModels();
|
|
121
|
+
|
|
122
|
+
return () => {
|
|
123
|
+
cancelled = true;
|
|
124
|
+
};
|
|
125
|
+
}, [hasAnyKey]);
|
|
126
|
+
|
|
127
|
+
// No API keys configured
|
|
128
|
+
if (!hasAnyKey) {
|
|
129
|
+
return (
|
|
130
|
+
<Box
|
|
131
|
+
flexDirection="column"
|
|
132
|
+
borderStyle="single"
|
|
133
|
+
borderColor={colors.warning}
|
|
134
|
+
paddingX={2}
|
|
135
|
+
paddingY={1}
|
|
136
|
+
marginY={1}
|
|
137
|
+
>
|
|
138
|
+
<Text bold color={colors.warning}>
|
|
139
|
+
⚠️ No API Keys Found
|
|
140
|
+
</Text>
|
|
141
|
+
<Box height={1} />
|
|
142
|
+
<Text>Add an API key first to see available models.</Text>
|
|
143
|
+
<Box height={1} />
|
|
144
|
+
<Text color={colors.primary}>Run /apikey to add your API key</Text>
|
|
145
|
+
<Box height={1} />
|
|
146
|
+
<Text dimColor>Supported providers:</Text>
|
|
147
|
+
<Text dimColor> • Anthropic (Claude)</Text>
|
|
148
|
+
<Text dimColor> • OpenAI (GPT)</Text>
|
|
149
|
+
</Box>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Loading state
|
|
154
|
+
if (state.loading) {
|
|
155
|
+
return (
|
|
156
|
+
<Box
|
|
157
|
+
flexDirection="column"
|
|
158
|
+
borderStyle="single"
|
|
159
|
+
borderColor={colors.muted}
|
|
160
|
+
paddingX={2}
|
|
161
|
+
paddingY={1}
|
|
162
|
+
marginY={1}
|
|
163
|
+
>
|
|
164
|
+
<Text bold color={colors.info}>
|
|
165
|
+
{emoji.model} Select Model
|
|
166
|
+
</Text>
|
|
167
|
+
<Box height={1} />
|
|
168
|
+
<Box>
|
|
169
|
+
<Spinner label="Fetching models from API..." />
|
|
170
|
+
</Box>
|
|
171
|
+
</Box>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Check if we have any models
|
|
176
|
+
const hasModels = allModels.length > 0;
|
|
177
|
+
|
|
178
|
+
// No models found (API errors)
|
|
179
|
+
if (!hasModels && state.errors.length > 0) {
|
|
180
|
+
return (
|
|
181
|
+
<Box
|
|
182
|
+
flexDirection="column"
|
|
183
|
+
borderStyle="single"
|
|
184
|
+
borderColor={colors.error}
|
|
185
|
+
paddingX={2}
|
|
186
|
+
paddingY={1}
|
|
187
|
+
marginY={1}
|
|
188
|
+
>
|
|
189
|
+
<Text bold color={colors.error}>
|
|
190
|
+
{emoji.error} Failed to Fetch Models
|
|
191
|
+
</Text>
|
|
192
|
+
<Box height={1} />
|
|
193
|
+
{state.errors.map((err, i) => (
|
|
194
|
+
<Text key={i} color={colors.error}>
|
|
195
|
+
{err.provider}: {err.error}
|
|
196
|
+
</Text>
|
|
197
|
+
))}
|
|
198
|
+
<Box height={1} />
|
|
199
|
+
<Text dimColor>Check your API key and try again with /apikey</Text>
|
|
200
|
+
</Box>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Calculate the offset for each provider section
|
|
205
|
+
let anthropicOffset = 0;
|
|
206
|
+
let openaiOffset = state.anthropicModels.length;
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<Box
|
|
210
|
+
flexDirection="column"
|
|
211
|
+
borderStyle="single"
|
|
212
|
+
borderColor={colors.primary}
|
|
213
|
+
paddingX={2}
|
|
214
|
+
paddingY={1}
|
|
215
|
+
marginY={1}
|
|
216
|
+
>
|
|
217
|
+
<Text bold color={colors.info}>
|
|
218
|
+
{emoji.model} Select Model
|
|
219
|
+
</Text>
|
|
220
|
+
<Box height={1} />
|
|
221
|
+
|
|
222
|
+
{/* Show any errors */}
|
|
223
|
+
{state.errors.length > 0 && (
|
|
224
|
+
<>
|
|
225
|
+
{state.errors.map((err, i) => (
|
|
226
|
+
<Text key={i} color={colors.warning}>
|
|
227
|
+
{emoji.warning} {err.provider}: {err.error}
|
|
228
|
+
</Text>
|
|
229
|
+
))}
|
|
230
|
+
<Box height={1} />
|
|
231
|
+
</>
|
|
232
|
+
)}
|
|
233
|
+
|
|
234
|
+
{/* Anthropic Models */}
|
|
235
|
+
{state.anthropicModels.length > 0 && (
|
|
236
|
+
<>
|
|
237
|
+
<Text bold color={colors.primary}>
|
|
238
|
+
Anthropic Claude
|
|
239
|
+
</Text>
|
|
240
|
+
{state.anthropicModels.map((model, index) => {
|
|
241
|
+
const globalIndex = anthropicOffset + index;
|
|
242
|
+
const isSelected = globalIndex === selectedIndex;
|
|
243
|
+
const isCurrent = isCurrentModel(currentModel, model);
|
|
244
|
+
return (
|
|
245
|
+
<ModelItem
|
|
246
|
+
key={model.id}
|
|
247
|
+
model={model}
|
|
248
|
+
isSelected={isSelected}
|
|
249
|
+
isCurrent={isCurrent}
|
|
250
|
+
/>
|
|
251
|
+
);
|
|
252
|
+
})}
|
|
253
|
+
<Box height={1} />
|
|
254
|
+
</>
|
|
255
|
+
)}
|
|
256
|
+
|
|
257
|
+
{/* OpenAI Models */}
|
|
258
|
+
{state.openaiModels.length > 0 && (
|
|
259
|
+
<>
|
|
260
|
+
<Text bold color={colors.primary}>
|
|
261
|
+
OpenAI GPT
|
|
262
|
+
</Text>
|
|
263
|
+
{state.openaiModels.map((model, index) => {
|
|
264
|
+
const globalIndex = openaiOffset + index;
|
|
265
|
+
const isSelected = globalIndex === selectedIndex;
|
|
266
|
+
const isCurrent = isCurrentModel(currentModel, model);
|
|
267
|
+
return (
|
|
268
|
+
<ModelItem
|
|
269
|
+
key={model.id}
|
|
270
|
+
model={model}
|
|
271
|
+
isSelected={isSelected}
|
|
272
|
+
isCurrent={isCurrent}
|
|
273
|
+
/>
|
|
274
|
+
);
|
|
275
|
+
})}
|
|
276
|
+
<Box height={1} />
|
|
277
|
+
</>
|
|
278
|
+
)}
|
|
279
|
+
|
|
280
|
+
{/* Navigation hint */}
|
|
281
|
+
<Text dimColor>↑/↓ Navigate • Enter Select • Esc Cancel</Text>
|
|
282
|
+
</Box>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Check if a model matches the current model.
|
|
288
|
+
*/
|
|
289
|
+
function isCurrentModel(currentModel: string | undefined, model: AvailableModel): boolean {
|
|
290
|
+
if (!currentModel) return false;
|
|
291
|
+
return (
|
|
292
|
+
currentModel === model.id ||
|
|
293
|
+
currentModel === model.name ||
|
|
294
|
+
currentModel === `${model.provider}/${model.name}` ||
|
|
295
|
+
(currentModel.startsWith(`${model.provider}/`) && currentModel === model.id)
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
interface ModelItemProps {
|
|
300
|
+
model: AvailableModel;
|
|
301
|
+
isSelected: boolean;
|
|
302
|
+
isCurrent: boolean;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function ModelItem({ model, isSelected, isCurrent }: ModelItemProps): React.ReactElement {
|
|
306
|
+
// Determine the indicator
|
|
307
|
+
let indicator = " ";
|
|
308
|
+
let textColor: string | undefined = undefined;
|
|
309
|
+
let isBold = false;
|
|
310
|
+
|
|
311
|
+
if (isSelected) {
|
|
312
|
+
indicator = "▸ ";
|
|
313
|
+
textColor = colors.primary;
|
|
314
|
+
isBold = true;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (isCurrent) {
|
|
318
|
+
indicator = isSelected ? "▸✓" : " ✓";
|
|
319
|
+
textColor = isSelected ? colors.primary : colors.success;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return (
|
|
323
|
+
<Box marginLeft={1}>
|
|
324
|
+
<Text color={isSelected ? colors.primary : isCurrent ? colors.success : undefined}>
|
|
325
|
+
{indicator}
|
|
326
|
+
</Text>
|
|
327
|
+
<Text color={textColor} bold={isBold}>
|
|
328
|
+
{model.id}
|
|
329
|
+
</Text>
|
|
330
|
+
{model.description && (
|
|
331
|
+
<>
|
|
332
|
+
<Text dimColor> - </Text>
|
|
333
|
+
<Text dimColor>{model.description}</Text>
|
|
334
|
+
</>
|
|
335
|
+
)}
|
|
336
|
+
</Box>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slash command autocomplete menu component.
|
|
3
|
+
*/
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { Box, Text } from "ink";
|
|
6
|
+
import { colors, filterCommands, type SlashCommand } from "../theme.js";
|
|
7
|
+
|
|
8
|
+
interface SlashMenuProps {
|
|
9
|
+
/** Current input value to filter commands */
|
|
10
|
+
filter: string;
|
|
11
|
+
/** Maximum number of items to show */
|
|
12
|
+
maxItems?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function SlashMenu({
|
|
16
|
+
filter,
|
|
17
|
+
maxItems = 8,
|
|
18
|
+
}: SlashMenuProps): React.ReactElement | null {
|
|
19
|
+
// Only show menu when input starts with /
|
|
20
|
+
if (!filter.startsWith("/")) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const filtered = filterCommands(filter);
|
|
25
|
+
|
|
26
|
+
if (filtered.length === 0) {
|
|
27
|
+
return (
|
|
28
|
+
<Box paddingLeft={2} marginTop={1}>
|
|
29
|
+
<Text dimColor>No matching commands</Text>
|
|
30
|
+
</Box>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const displayItems = filtered.slice(0, maxItems);
|
|
35
|
+
const hasMore = filtered.length > maxItems;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Box flexDirection="column" marginTop={1} paddingLeft={2}>
|
|
39
|
+
{displayItems.map((cmd) => (
|
|
40
|
+
<SlashMenuItem key={cmd.command} command={cmd} />
|
|
41
|
+
))}
|
|
42
|
+
{hasMore && (
|
|
43
|
+
<Text dimColor>... and {filtered.length - maxItems} more</Text>
|
|
44
|
+
)}
|
|
45
|
+
</Box>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface SlashMenuItemProps {
|
|
50
|
+
command: SlashCommand;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function SlashMenuItem({ command }: SlashMenuItemProps): React.ReactElement {
|
|
54
|
+
const aliases =
|
|
55
|
+
command.aliases.length > 0 ? ` (${command.aliases.join(", ")})` : "";
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Box>
|
|
59
|
+
<Text color={colors.info}>{command.command}</Text>
|
|
60
|
+
<Text dimColor>{aliases}</Text>
|
|
61
|
+
<Text dimColor> - {command.description}</Text>
|
|
62
|
+
</Box>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Full slash menu panel for /help command.
|
|
68
|
+
*/
|
|
69
|
+
export function SlashMenuPanel(): React.ReactElement {
|
|
70
|
+
const commands = filterCommands();
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Box
|
|
74
|
+
flexDirection="column"
|
|
75
|
+
borderStyle="single"
|
|
76
|
+
borderColor={colors.muted}
|
|
77
|
+
paddingX={2}
|
|
78
|
+
paddingY={1}
|
|
79
|
+
marginY={1}
|
|
80
|
+
>
|
|
81
|
+
<Text bold color={colors.info}>
|
|
82
|
+
Available Commands
|
|
83
|
+
</Text>
|
|
84
|
+
<Box height={1} />
|
|
85
|
+
{commands.map((cmd) => (
|
|
86
|
+
<Box key={cmd.command} flexDirection="column" marginBottom={1}>
|
|
87
|
+
<Box>
|
|
88
|
+
<Text color={colors.info}>{cmd.command}</Text>
|
|
89
|
+
{cmd.aliases.length > 0 && (
|
|
90
|
+
<Text dimColor> ({cmd.aliases.join(", ")})</Text>
|
|
91
|
+
)}
|
|
92
|
+
</Box>
|
|
93
|
+
<Box paddingLeft={2}>
|
|
94
|
+
<Text dimColor>{cmd.description}</Text>
|
|
95
|
+
</Box>
|
|
96
|
+
</Box>
|
|
97
|
+
))}
|
|
98
|
+
</Box>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact status bar component.
|
|
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
|
+
|
|
9
|
+
interface FeatureFlags {
|
|
10
|
+
promptCaching: boolean;
|
|
11
|
+
eviction: boolean;
|
|
12
|
+
summarization: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface StatusBarProps {
|
|
16
|
+
/** Current working directory */
|
|
17
|
+
workDir: string;
|
|
18
|
+
/** Current model name */
|
|
19
|
+
model: string;
|
|
20
|
+
/** Optional status indicator (idle, generating, etc.) */
|
|
21
|
+
status?: "idle" | "thinking" | "streaming" | "tool-call" | "subagent" | "done" | "error";
|
|
22
|
+
/** Feature flags to display */
|
|
23
|
+
features?: FeatureFlags;
|
|
24
|
+
/** Whether auto-approve mode is enabled */
|
|
25
|
+
autoApproveEnabled?: boolean;
|
|
26
|
+
/** Current session ID if checkpointing is enabled */
|
|
27
|
+
sessionId?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function StatusBar({
|
|
31
|
+
workDir,
|
|
32
|
+
model,
|
|
33
|
+
status = "idle",
|
|
34
|
+
features,
|
|
35
|
+
autoApproveEnabled = false,
|
|
36
|
+
sessionId,
|
|
37
|
+
}: StatusBarProps): React.ReactElement {
|
|
38
|
+
// Get short model name
|
|
39
|
+
const shortModel = model.split("/").pop() || model;
|
|
40
|
+
|
|
41
|
+
// Status indicator - minimal
|
|
42
|
+
const getStatusDisplay = () => {
|
|
43
|
+
switch (status) {
|
|
44
|
+
case "thinking":
|
|
45
|
+
return <Text color={colors.warning}>●</Text>;
|
|
46
|
+
case "streaming":
|
|
47
|
+
return <Text color={colors.success}>●</Text>;
|
|
48
|
+
case "tool-call":
|
|
49
|
+
return <Text color={colors.tool}>●</Text>;
|
|
50
|
+
case "subagent":
|
|
51
|
+
return <Text color={colors.secondary}>●</Text>;
|
|
52
|
+
case "error":
|
|
53
|
+
return <Text color={colors.error}>●</Text>;
|
|
54
|
+
case "done":
|
|
55
|
+
return <Text color={colors.success}>●</Text>;
|
|
56
|
+
default:
|
|
57
|
+
return <Text dimColor>○</Text>;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Feature badges - compact
|
|
62
|
+
const featureBadges: string[] = [];
|
|
63
|
+
if (features?.promptCaching) featureBadges.push("⚡");
|
|
64
|
+
if (features?.eviction) featureBadges.push("📦");
|
|
65
|
+
if (features?.summarization) featureBadges.push("📝");
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Box marginTop={1}>
|
|
69
|
+
<Text dimColor>
|
|
70
|
+
{getStatusDisplay()} {shortModel}
|
|
71
|
+
{featureBadges.length > 0 && ` ${featureBadges.join(" ")}`}
|
|
72
|
+
{" · "}
|
|
73
|
+
{autoApproveEnabled ? (
|
|
74
|
+
<Text color={colors.success}>🟢 Auto-approve</Text>
|
|
75
|
+
) : (
|
|
76
|
+
<Text color={colors.warning}>🔴 Safe mode</Text>
|
|
77
|
+
)}
|
|
78
|
+
{sessionId && (
|
|
79
|
+
<>
|
|
80
|
+
{" · "}
|
|
81
|
+
<Text dimColor>Session: {sessionId}</Text>
|
|
82
|
+
</>
|
|
83
|
+
)}
|
|
84
|
+
{" · "}? for shortcuts
|
|
85
|
+
</Text>
|
|
86
|
+
</Box>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent status display components.
|
|
3
|
+
*/
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { Box, Text } from "ink";
|
|
6
|
+
import { Spinner, StatusMessage } from "@inkjs/ui";
|
|
7
|
+
import { emoji, colors } from "../theme.js";
|
|
8
|
+
|
|
9
|
+
interface SubagentStartProps {
|
|
10
|
+
/** Subagent name */
|
|
11
|
+
name: string;
|
|
12
|
+
/** Task description */
|
|
13
|
+
task: string;
|
|
14
|
+
/** Maximum task length to display */
|
|
15
|
+
maxTaskLength?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function SubagentStart({
|
|
19
|
+
name,
|
|
20
|
+
task,
|
|
21
|
+
maxTaskLength = 60,
|
|
22
|
+
}: SubagentStartProps): React.ReactElement {
|
|
23
|
+
const shortTask =
|
|
24
|
+
task.length > maxTaskLength
|
|
25
|
+
? task.substring(0, maxTaskLength) + "..."
|
|
26
|
+
: task;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Box flexDirection="column" marginY={1}>
|
|
30
|
+
<Box>
|
|
31
|
+
<Spinner
|
|
32
|
+
label={`${emoji.subagent} Starting subagent: ${name}`}
|
|
33
|
+
/>
|
|
34
|
+
</Box>
|
|
35
|
+
<Box paddingLeft={4}>
|
|
36
|
+
<Text dimColor>└─ {shortTask}</Text>
|
|
37
|
+
</Box>
|
|
38
|
+
</Box>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface SubagentFinishProps {
|
|
43
|
+
/** Subagent name */
|
|
44
|
+
name: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function SubagentFinish({
|
|
48
|
+
name,
|
|
49
|
+
}: SubagentFinishProps): React.ReactElement {
|
|
50
|
+
return (
|
|
51
|
+
<Box marginY={1}>
|
|
52
|
+
<StatusMessage variant="success">
|
|
53
|
+
{emoji.subagent} Subagent {name} completed
|
|
54
|
+
</StatusMessage>
|
|
55
|
+
</Box>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Subagent running indicator (for when a subagent is actively working).
|
|
61
|
+
*/
|
|
62
|
+
interface SubagentRunningProps {
|
|
63
|
+
name: string;
|
|
64
|
+
task: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function SubagentRunning({
|
|
68
|
+
name,
|
|
69
|
+
task,
|
|
70
|
+
}: SubagentRunningProps): React.ReactElement {
|
|
71
|
+
const shortTask = task.length > 50 ? task.substring(0, 50) + "..." : task;
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<Box
|
|
75
|
+
flexDirection="column"
|
|
76
|
+
borderStyle="single"
|
|
77
|
+
borderColor={colors.secondary}
|
|
78
|
+
paddingX={2}
|
|
79
|
+
paddingY={1}
|
|
80
|
+
marginY={1}
|
|
81
|
+
>
|
|
82
|
+
<Box>
|
|
83
|
+
<Spinner label={`${emoji.subagent} ${name}`} />
|
|
84
|
+
</Box>
|
|
85
|
+
<Box paddingLeft={2}>
|
|
86
|
+
<Text dimColor>{shortTask}</Text>
|
|
87
|
+
</Box>
|
|
88
|
+
</Box>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|