@ridit/milo 0.1.0
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/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +111 -0
- package/LICENSE +21 -0
- package/README.md +122 -0
- package/dist/index.mjs +106603 -0
- package/package.json +64 -0
- package/src/commands/clear.ts +18 -0
- package/src/commands/crimes.ts +48 -0
- package/src/commands/feed.ts +20 -0
- package/src/commands/genz.ts +33 -0
- package/src/commands/help.ts +25 -0
- package/src/commands/init.ts +65 -0
- package/src/commands/mode.ts +22 -0
- package/src/commands/pet.ts +35 -0
- package/src/commands/provider.ts +46 -0
- package/src/commands/roast.ts +40 -0
- package/src/commands/vibe.ts +42 -0
- package/src/commands.ts +43 -0
- package/src/components/AsciiLogo.tsx +25 -0
- package/src/components/CommandSuggestions.tsx +78 -0
- package/src/components/Header.tsx +68 -0
- package/src/components/HighlightedCode.tsx +23 -0
- package/src/components/Message.tsx +43 -0
- package/src/components/ProviderWizard.tsx +278 -0
- package/src/components/Spinner.tsx +76 -0
- package/src/components/StatusBar.tsx +85 -0
- package/src/components/StructuredDiff.tsx +194 -0
- package/src/components/TextInput.tsx +144 -0
- package/src/components/messages/AssistantMessage.tsx +68 -0
- package/src/components/messages/ToolCallMessage.tsx +77 -0
- package/src/components/messages/ToolResultMessage.tsx +181 -0
- package/src/components/messages/UserMessage.tsx +32 -0
- package/src/components/permissions/PermissionCard.tsx +152 -0
- package/src/history.ts +27 -0
- package/src/hooks/useArrowKeyHistory.ts +0 -0
- package/src/hooks/useChat.ts +271 -0
- package/src/hooks/useDoublePress.ts +35 -0
- package/src/hooks/useTerminalSize.ts +24 -0
- package/src/hooks/useTextInput.ts +263 -0
- package/src/icons.ts +31 -0
- package/src/index.tsx +5 -0
- package/src/multi-agent/agent/agent.ts +33 -0
- package/src/multi-agent/orchestrator/orchestrator.ts +103 -0
- package/src/multi-agent/schemas.ts +12 -0
- package/src/multi-agent/types.ts +8 -0
- package/src/permissions.ts +54 -0
- package/src/pet.ts +239 -0
- package/src/screens/REPL.tsx +261 -0
- package/src/shortcuts.ts +37 -0
- package/src/skills/backend.ts +76 -0
- package/src/skills/cicd.ts +57 -0
- package/src/skills/colors.ts +72 -0
- package/src/skills/database.ts +55 -0
- package/src/skills/docker.ts +74 -0
- package/src/skills/frontend.ts +70 -0
- package/src/skills/git.ts +52 -0
- package/src/skills/testing.ts +73 -0
- package/src/skills/typography.ts +57 -0
- package/src/skills/uiux.ts +43 -0
- package/src/tools/AgentTool/prompt.ts +17 -0
- package/src/tools/AgentTool/tool.ts +22 -0
- package/src/tools/BashTool/prompt.ts +82 -0
- package/src/tools/BashTool/tool.ts +54 -0
- package/src/tools/FileEditTool/prompt.ts +13 -0
- package/src/tools/FileEditTool/tool.ts +39 -0
- package/src/tools/FileReadTool/prompt.ts +5 -0
- package/src/tools/FileReadTool/tool.ts +34 -0
- package/src/tools/FileWriteTool/prompt.ts +19 -0
- package/src/tools/FileWriteTool/tool.ts +34 -0
- package/src/tools/GlobTool/prompt.ts +11 -0
- package/src/tools/GlobTool/tool.ts +34 -0
- package/src/tools/GrepTool/prompt.ts +13 -0
- package/src/tools/GrepTool/tool.ts +41 -0
- package/src/tools/MemoryEditTool/prompt.ts +10 -0
- package/src/tools/MemoryEditTool/tool.ts +38 -0
- package/src/tools/MemoryReadTool/prompt.ts +9 -0
- package/src/tools/MemoryReadTool/tool.ts +47 -0
- package/src/tools/MemoryWriteTool/prompt.ts +10 -0
- package/src/tools/MemoryWriteTool/tool.ts +30 -0
- package/src/tools/OrchestratorTool/prompt.ts +26 -0
- package/src/tools/OrchestratorTool/tool.ts +20 -0
- package/src/tools/RecallTool/prompt.ts +13 -0
- package/src/tools/RecallTool/tool.ts +47 -0
- package/src/tools/ThinkTool/tool.ts +16 -0
- package/src/tools/WebFetchTool/prompt.ts +7 -0
- package/src/tools/WebFetchTool/tool.ts +33 -0
- package/src/tools/WebSearchTool/prompt.ts +8 -0
- package/src/tools/WebSearchTool/tool.ts +49 -0
- package/src/types.ts +124 -0
- package/src/utils/Cursor.ts +423 -0
- package/src/utils/PersistentShell.ts +306 -0
- package/src/utils/agent.ts +21 -0
- package/src/utils/chat.ts +21 -0
- package/src/utils/compaction.ts +71 -0
- package/src/utils/env.ts +11 -0
- package/src/utils/file.ts +42 -0
- package/src/utils/format.ts +46 -0
- package/src/utils/imagePaste.ts +78 -0
- package/src/utils/json.ts +10 -0
- package/src/utils/llm.ts +65 -0
- package/src/utils/markdown.ts +258 -0
- package/src/utils/messages.ts +81 -0
- package/src/utils/model.ts +16 -0
- package/src/utils/plan.ts +26 -0
- package/src/utils/providers.ts +100 -0
- package/src/utils/ripgrep.ts +175 -0
- package/src/utils/session.ts +100 -0
- package/src/utils/skills.ts +26 -0
- package/src/utils/systemPrompt.ts +218 -0
- package/src/utils/theme.ts +110 -0
- package/src/utils/tools.ts +58 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import type { StructuredPatchHunk } from "diff";
|
|
4
|
+
import { getTheme } from "../utils/theme";
|
|
5
|
+
import { useMemo } from "react";
|
|
6
|
+
import { wrapText } from "../utils/format";
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
patch: StructuredPatchHunk;
|
|
10
|
+
dim: boolean;
|
|
11
|
+
width: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type DiffLine = {
|
|
15
|
+
code: string;
|
|
16
|
+
type: string;
|
|
17
|
+
i: number;
|
|
18
|
+
empty: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function StructuredDiff({ patch, dim, width }: Props): React.ReactNode {
|
|
22
|
+
const diff = useMemo(
|
|
23
|
+
() => formatDiff(patch.lines, patch.oldStart, width, dim),
|
|
24
|
+
[patch.lines, patch.oldStart, width, dim],
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
return diff.map((_, i) => <Box key={i}>{_}</Box>);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function formatDiff(
|
|
31
|
+
lines: string[],
|
|
32
|
+
startingLineNumber: number,
|
|
33
|
+
width: number,
|
|
34
|
+
dim: boolean,
|
|
35
|
+
): React.ReactNode[] {
|
|
36
|
+
const theme = getTheme();
|
|
37
|
+
|
|
38
|
+
const ls = numberDiffLines(
|
|
39
|
+
lines.map((code) => {
|
|
40
|
+
const content = code.slice(1);
|
|
41
|
+
if (code.startsWith("+")) {
|
|
42
|
+
return {
|
|
43
|
+
code: "+ " + content,
|
|
44
|
+
i: 0,
|
|
45
|
+
type: "add",
|
|
46
|
+
empty: !content.trim(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (code.startsWith("-")) {
|
|
50
|
+
return {
|
|
51
|
+
code: "- " + content,
|
|
52
|
+
i: 0,
|
|
53
|
+
type: "remove",
|
|
54
|
+
empty: !content.trim(),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
code: " " + code,
|
|
59
|
+
i: 0,
|
|
60
|
+
type: "nochange",
|
|
61
|
+
empty: !code.trim(),
|
|
62
|
+
};
|
|
63
|
+
}),
|
|
64
|
+
startingLineNumber,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const maxLineNumber = Math.max(...ls.map(({ i }) => i));
|
|
68
|
+
const maxWidth = maxLineNumber.toString().length;
|
|
69
|
+
|
|
70
|
+
return ls.flatMap(({ type, code, i, empty }) => {
|
|
71
|
+
const wrappedLines = wrapText(code, width - maxWidth);
|
|
72
|
+
const renderLines =
|
|
73
|
+
wrappedLines.length && wrappedLines[0] !== "" ? wrappedLines : [" "];
|
|
74
|
+
|
|
75
|
+
return renderLines.map((line, lineIndex) => {
|
|
76
|
+
const key = `${type}-${i}-${lineIndex}`;
|
|
77
|
+
switch (type) {
|
|
78
|
+
case "add":
|
|
79
|
+
return (
|
|
80
|
+
<Box key={key} width={width - maxWidth}>
|
|
81
|
+
<LineNumber
|
|
82
|
+
i={lineIndex === 0 ? i : undefined}
|
|
83
|
+
width={maxWidth}
|
|
84
|
+
/>
|
|
85
|
+
<Box flexGrow={1}>
|
|
86
|
+
<Text
|
|
87
|
+
color={theme.text}
|
|
88
|
+
backgroundColor={
|
|
89
|
+
empty
|
|
90
|
+
? undefined
|
|
91
|
+
: dim
|
|
92
|
+
? theme.diff.addedDimmed
|
|
93
|
+
: theme.diff.added
|
|
94
|
+
}
|
|
95
|
+
dimColor={dim}
|
|
96
|
+
>
|
|
97
|
+
{empty ? line : line.padEnd(width - maxWidth - 4)}
|
|
98
|
+
</Text>
|
|
99
|
+
</Box>
|
|
100
|
+
</Box>
|
|
101
|
+
);
|
|
102
|
+
case "remove":
|
|
103
|
+
return (
|
|
104
|
+
<Box key={key} width={width - maxWidth}>
|
|
105
|
+
<LineNumber
|
|
106
|
+
i={lineIndex === 0 ? i : undefined}
|
|
107
|
+
width={maxWidth}
|
|
108
|
+
/>
|
|
109
|
+
<Box flexGrow={1}>
|
|
110
|
+
<Text
|
|
111
|
+
color={theme.text}
|
|
112
|
+
backgroundColor={
|
|
113
|
+
empty
|
|
114
|
+
? undefined
|
|
115
|
+
: dim
|
|
116
|
+
? theme.diff.removedDimmed
|
|
117
|
+
: theme.diff.removed
|
|
118
|
+
}
|
|
119
|
+
dimColor={dim}
|
|
120
|
+
>
|
|
121
|
+
{empty ? line : line.padEnd(width - maxWidth - 4)}
|
|
122
|
+
</Text>
|
|
123
|
+
</Box>
|
|
124
|
+
</Box>
|
|
125
|
+
);
|
|
126
|
+
case "nochange":
|
|
127
|
+
return (
|
|
128
|
+
<Box key={key} width={width - maxWidth}>
|
|
129
|
+
<LineNumber
|
|
130
|
+
i={lineIndex === 0 ? i : undefined}
|
|
131
|
+
width={maxWidth}
|
|
132
|
+
/>
|
|
133
|
+
<Text color={theme.text} dimColor={dim}>
|
|
134
|
+
{line}
|
|
135
|
+
</Text>
|
|
136
|
+
</Box>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function LineNumber({
|
|
144
|
+
i,
|
|
145
|
+
width,
|
|
146
|
+
}: {
|
|
147
|
+
i: number | undefined;
|
|
148
|
+
width: number;
|
|
149
|
+
}): React.ReactNode {
|
|
150
|
+
return (
|
|
151
|
+
<Text color={getTheme().secondaryText}>
|
|
152
|
+
{i !== undefined ? i.toString().padStart(width) : " ".repeat(width)}{" "}
|
|
153
|
+
</Text>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function numberDiffLines(
|
|
158
|
+
diff: { code: string; type: string; empty: boolean }[],
|
|
159
|
+
startLine: number,
|
|
160
|
+
): DiffLine[] {
|
|
161
|
+
let i = startLine;
|
|
162
|
+
const result: DiffLine[] = [];
|
|
163
|
+
const queue = [...diff];
|
|
164
|
+
|
|
165
|
+
while (queue.length > 0) {
|
|
166
|
+
const { code, type, empty } = queue.shift()!;
|
|
167
|
+
const line = { code, type, i, empty };
|
|
168
|
+
|
|
169
|
+
switch (type) {
|
|
170
|
+
case "nochange":
|
|
171
|
+
i++;
|
|
172
|
+
result.push(line);
|
|
173
|
+
break;
|
|
174
|
+
case "add":
|
|
175
|
+
i++;
|
|
176
|
+
result.push(line);
|
|
177
|
+
break;
|
|
178
|
+
case "remove": {
|
|
179
|
+
result.push(line);
|
|
180
|
+
let numRemoved = 0;
|
|
181
|
+
while (queue[0]?.type === "remove") {
|
|
182
|
+
i++;
|
|
183
|
+
const { code, type, empty } = queue.shift()!;
|
|
184
|
+
result.push({ code, type, i, empty });
|
|
185
|
+
numRemoved++;
|
|
186
|
+
}
|
|
187
|
+
i -= numRemoved;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import React, { type JSX } from "react";
|
|
2
|
+
import { Text, useInput } from "ink";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { useTextInput } from "../hooks/useTextInput";
|
|
5
|
+
import { getTheme } from "../utils/theme";
|
|
6
|
+
import { type Key } from "ink";
|
|
7
|
+
|
|
8
|
+
export type Props = {
|
|
9
|
+
readonly onHistoryUp?: () => void;
|
|
10
|
+
readonly onHistoryDown?: () => void;
|
|
11
|
+
readonly placeholder?: string;
|
|
12
|
+
readonly multiline?: boolean;
|
|
13
|
+
readonly focus?: boolean;
|
|
14
|
+
readonly mask?: string;
|
|
15
|
+
readonly showCursor?: boolean;
|
|
16
|
+
readonly highlightPastedText?: boolean;
|
|
17
|
+
readonly value: string;
|
|
18
|
+
readonly onChange: (value: string) => void;
|
|
19
|
+
readonly onSubmit?: (value: string) => void;
|
|
20
|
+
readonly onExit?: () => void;
|
|
21
|
+
readonly onExitMessage?: (show: boolean, key?: string) => void;
|
|
22
|
+
readonly onMessage?: (show: boolean, message?: string) => void;
|
|
23
|
+
readonly onHistoryReset?: () => void;
|
|
24
|
+
readonly columns: number;
|
|
25
|
+
readonly onImagePaste?: (base64Image: string) => void;
|
|
26
|
+
readonly onPaste?: (text: string) => void;
|
|
27
|
+
readonly isDimmed?: boolean;
|
|
28
|
+
readonly disableCursorMovementForUpDownKeys?: boolean;
|
|
29
|
+
readonly cursorOffset: number;
|
|
30
|
+
readonly onChangeCursorOffset: (offset: number) => void;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default function TextInput({
|
|
34
|
+
value: originalValue,
|
|
35
|
+
placeholder = "",
|
|
36
|
+
focus = true,
|
|
37
|
+
mask,
|
|
38
|
+
multiline = false,
|
|
39
|
+
highlightPastedText = false,
|
|
40
|
+
showCursor = true,
|
|
41
|
+
onChange,
|
|
42
|
+
onSubmit,
|
|
43
|
+
onExit,
|
|
44
|
+
onHistoryUp,
|
|
45
|
+
onHistoryDown,
|
|
46
|
+
onExitMessage,
|
|
47
|
+
onMessage,
|
|
48
|
+
onHistoryReset,
|
|
49
|
+
columns,
|
|
50
|
+
onImagePaste,
|
|
51
|
+
onPaste,
|
|
52
|
+
isDimmed = false,
|
|
53
|
+
disableCursorMovementForUpDownKeys = false,
|
|
54
|
+
cursorOffset,
|
|
55
|
+
onChangeCursorOffset,
|
|
56
|
+
}: Props): JSX.Element {
|
|
57
|
+
const { onInput, renderedValue } = useTextInput({
|
|
58
|
+
value: originalValue,
|
|
59
|
+
onChange,
|
|
60
|
+
onSubmit,
|
|
61
|
+
onExit,
|
|
62
|
+
onExitMessage,
|
|
63
|
+
onMessage,
|
|
64
|
+
onHistoryReset,
|
|
65
|
+
onHistoryUp,
|
|
66
|
+
onHistoryDown,
|
|
67
|
+
mask,
|
|
68
|
+
multiline,
|
|
69
|
+
cursorChar: showCursor ? " " : "",
|
|
70
|
+
invert: chalk.inverse,
|
|
71
|
+
columns,
|
|
72
|
+
onImagePaste,
|
|
73
|
+
disableCursorMovementForUpDownKeys,
|
|
74
|
+
externalOffset: cursorOffset,
|
|
75
|
+
onOffsetChange: onChangeCursorOffset,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const [pasteState, setPasteState] = React.useState<{
|
|
79
|
+
chunks: string[];
|
|
80
|
+
timeoutId: ReturnType<typeof setTimeout> | null;
|
|
81
|
+
}>({ chunks: [], timeoutId: null });
|
|
82
|
+
|
|
83
|
+
const resetPasteTimeout = (
|
|
84
|
+
currentTimeoutId: ReturnType<typeof setTimeout> | null,
|
|
85
|
+
) => {
|
|
86
|
+
if (currentTimeoutId) clearTimeout(currentTimeoutId);
|
|
87
|
+
return setTimeout(() => {
|
|
88
|
+
setPasteState(({ chunks }) => {
|
|
89
|
+
const pastedText = chunks.join("");
|
|
90
|
+
Promise.resolve().then(() => onPaste!(pastedText));
|
|
91
|
+
return { chunks: [], timeoutId: null };
|
|
92
|
+
});
|
|
93
|
+
}, 100);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const wrappedOnInput = (input: string, key: Key): void => {
|
|
97
|
+
const isSpecialKey =
|
|
98
|
+
key.backspace ||
|
|
99
|
+
key.delete ||
|
|
100
|
+
key.leftArrow ||
|
|
101
|
+
key.rightArrow ||
|
|
102
|
+
key.upArrow ||
|
|
103
|
+
key.downArrow ||
|
|
104
|
+
key.return ||
|
|
105
|
+
key.escape ||
|
|
106
|
+
key.ctrl ||
|
|
107
|
+
key.meta;
|
|
108
|
+
|
|
109
|
+
if (
|
|
110
|
+
onPaste &&
|
|
111
|
+
!isSpecialKey &&
|
|
112
|
+
(input.length > 800 || pasteState.timeoutId)
|
|
113
|
+
) {
|
|
114
|
+
setPasteState(({ chunks, timeoutId }) => ({
|
|
115
|
+
chunks: [...chunks, input],
|
|
116
|
+
timeoutId: resetPasteTimeout(timeoutId),
|
|
117
|
+
}));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
onInput(input, key);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
useInput(wrappedOnInput, { isActive: focus });
|
|
124
|
+
|
|
125
|
+
let renderedPlaceholder = placeholder
|
|
126
|
+
? chalk.hex(getTheme().secondaryText)(placeholder)
|
|
127
|
+
: undefined;
|
|
128
|
+
|
|
129
|
+
if (showCursor && focus) {
|
|
130
|
+
renderedPlaceholder =
|
|
131
|
+
placeholder.length > 0
|
|
132
|
+
? chalk.inverse(placeholder[0]) +
|
|
133
|
+
chalk.hex(getTheme().secondaryText)(placeholder.slice(1))
|
|
134
|
+
: chalk.inverse(" ");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const showPlaceholder = originalValue.length === 0 && placeholder;
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<Text wrap="truncate-end" dimColor={isDimmed}>
|
|
141
|
+
{showPlaceholder ? renderedPlaceholder : renderedValue}
|
|
142
|
+
</Text>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { getTheme } from "../../utils/theme";
|
|
4
|
+
import { useTerminalSize } from "../../hooks/useTerminalSize";
|
|
5
|
+
import { applyMarkdown } from "../../utils/markdown";
|
|
6
|
+
import { HighlightedCode } from "../HighlightedCode";
|
|
7
|
+
import { bullet } from "../../icons";
|
|
8
|
+
|
|
9
|
+
type Props = {
|
|
10
|
+
text: string;
|
|
11
|
+
addMargin?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type Segment =
|
|
15
|
+
| { type: "text"; content: string }
|
|
16
|
+
| { type: "code"; lang: string; code: string };
|
|
17
|
+
|
|
18
|
+
function parseSegments(text: string): Segment[] {
|
|
19
|
+
const segments: Segment[] = [];
|
|
20
|
+
const regex = /```(\w*)\n([\s\S]*?)```/g;
|
|
21
|
+
let last = 0;
|
|
22
|
+
let match;
|
|
23
|
+
|
|
24
|
+
while ((match = regex.exec(text)) !== null) {
|
|
25
|
+
if (match.index > last) {
|
|
26
|
+
segments.push({ type: "text", content: text.slice(last, match.index) });
|
|
27
|
+
}
|
|
28
|
+
segments.push({
|
|
29
|
+
type: "code",
|
|
30
|
+
lang: match[1] || "markdown",
|
|
31
|
+
code: match[2] ?? "",
|
|
32
|
+
});
|
|
33
|
+
last = match.index + match[0].length;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (last < text.length) {
|
|
37
|
+
segments.push({ type: "text", content: text.slice(last) });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return segments;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function AssistantMessage({
|
|
44
|
+
text,
|
|
45
|
+
addMargin = false,
|
|
46
|
+
}: Props): React.ReactNode {
|
|
47
|
+
const { columns } = useTerminalSize();
|
|
48
|
+
if (!text) return null;
|
|
49
|
+
|
|
50
|
+
const segments = parseSegments(text);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Box flexDirection="row" marginTop={addMargin ? 1 : 0} width="100%">
|
|
54
|
+
<Box minWidth={2} width={2}>
|
|
55
|
+
<Text color={getTheme().primary}>{bullet}</Text>
|
|
56
|
+
</Box>
|
|
57
|
+
<Box flexDirection="column" width={columns - 4}>
|
|
58
|
+
{segments.map((seg, i) =>
|
|
59
|
+
seg.type === "code" ? (
|
|
60
|
+
<HighlightedCode key={i} code={seg.code} language={seg.lang} />
|
|
61
|
+
) : (
|
|
62
|
+
<Text key={i}>{applyMarkdown(seg.content)}</Text>
|
|
63
|
+
),
|
|
64
|
+
)}
|
|
65
|
+
</Box>
|
|
66
|
+
</Box>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { getTheme } from "../../utils/theme";
|
|
4
|
+
import { SimpleSpinner } from "../Spinner";
|
|
5
|
+
import { cornerBottomLeft, line } from "../../icons";
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
toolName: string;
|
|
9
|
+
input?: unknown;
|
|
10
|
+
addMargin?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function getCommand(toolName: string, input: unknown): string {
|
|
14
|
+
if (!input || typeof input !== "object") return toolName;
|
|
15
|
+
const a = input as Record<string, unknown>;
|
|
16
|
+
switch (toolName) {
|
|
17
|
+
case "FileReadTool":
|
|
18
|
+
return `cat ${a.path ?? ""}`;
|
|
19
|
+
case "FileWriteTool":
|
|
20
|
+
return `write ${a.path ?? ""}`;
|
|
21
|
+
case "FileEditTool":
|
|
22
|
+
return `edit ${a.path ?? ""}`;
|
|
23
|
+
case "BashTool":
|
|
24
|
+
return String(a.command ?? "");
|
|
25
|
+
case "GrepTool":
|
|
26
|
+
return `grep ${a.pattern ?? ""}`;
|
|
27
|
+
case "GlobTool":
|
|
28
|
+
return `glob ${a.pattern ?? ""}`;
|
|
29
|
+
case "RecallTool":
|
|
30
|
+
return `recall ${a.query ?? ""}`;
|
|
31
|
+
case "WebSearchTool":
|
|
32
|
+
return `search ${String((a as any).query ?? "")}`;
|
|
33
|
+
case "WebFetchTool":
|
|
34
|
+
return `fetch ${String((a as any).url ?? "")}`;
|
|
35
|
+
case "MemoryReadTool":
|
|
36
|
+
return `memory read`;
|
|
37
|
+
case "MemoryWriteTool":
|
|
38
|
+
return `memory write`;
|
|
39
|
+
case "MemoryEditTool":
|
|
40
|
+
return `memory edit`;
|
|
41
|
+
case "ThinkTool":
|
|
42
|
+
return `think`;
|
|
43
|
+
case "AgentTool":
|
|
44
|
+
return `agent · ${String((a as any).task ?? (a as any).subtask ?? "").slice(0, 50)}`;
|
|
45
|
+
case "OrchestratorTool":
|
|
46
|
+
return `orchestrate · ${String((a as any).goal ?? "").slice(0, 50)}`;
|
|
47
|
+
default:
|
|
48
|
+
return toolName;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function ToolCallMessage({
|
|
53
|
+
toolName,
|
|
54
|
+
input,
|
|
55
|
+
addMargin = false,
|
|
56
|
+
}: Props): React.ReactNode {
|
|
57
|
+
const command = getCommand(toolName, input);
|
|
58
|
+
const preview = command.length > 60 ? command.slice(0, 60) + "…" : command;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Box flexDirection="column" marginTop={addMargin ? 1 : 0}>
|
|
62
|
+
<Box flexDirection="row">
|
|
63
|
+
<Box minWidth={2} width={2}>
|
|
64
|
+
<SimpleSpinner />
|
|
65
|
+
</Box>
|
|
66
|
+
<Text color={getTheme().secondaryText}>Running 1 tool…</Text>
|
|
67
|
+
</Box>
|
|
68
|
+
<Box flexDirection="row" marginLeft={2}>
|
|
69
|
+
<Text color={getTheme().secondaryText} dimColor>
|
|
70
|
+
{cornerBottomLeft}
|
|
71
|
+
{line}{" "}
|
|
72
|
+
</Text>
|
|
73
|
+
<Text color={getTheme().secondary}>$ {preview}</Text>
|
|
74
|
+
</Box>
|
|
75
|
+
</Box>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { parsePatch } from "diff";
|
|
4
|
+
import type { StructuredPatchHunk } from "diff";
|
|
5
|
+
import { getTheme } from "../../utils/theme";
|
|
6
|
+
import { useTerminalSize } from "../../hooks/useTerminalSize";
|
|
7
|
+
import { StructuredDiff } from "../StructuredDiff";
|
|
8
|
+
import { star, cornerBottomLeft, line, dot } from "../../icons";
|
|
9
|
+
|
|
10
|
+
type Props = {
|
|
11
|
+
toolName: string;
|
|
12
|
+
input?: unknown;
|
|
13
|
+
output: unknown;
|
|
14
|
+
success: boolean;
|
|
15
|
+
addMargin?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function getAction(toolName: string, input: unknown): string {
|
|
19
|
+
if (!input || typeof input !== "object") return toolName;
|
|
20
|
+
const a = input as Record<string, unknown>;
|
|
21
|
+
switch (toolName) {
|
|
22
|
+
case "FileReadTool":
|
|
23
|
+
return `cat ${a.path ?? ""}`;
|
|
24
|
+
case "FileWriteTool":
|
|
25
|
+
return `write ${a.path ?? ""}`;
|
|
26
|
+
case "FileEditTool":
|
|
27
|
+
return `edit ${a.path ?? ""}`;
|
|
28
|
+
case "BashTool":
|
|
29
|
+
return String(a.command ?? "");
|
|
30
|
+
case "GrepTool":
|
|
31
|
+
return `grep ${a.pattern ?? ""}`;
|
|
32
|
+
case "GlobTool":
|
|
33
|
+
return `glob ${a.pattern ?? ""}`;
|
|
34
|
+
case "RecallTool":
|
|
35
|
+
return `recall ${a.query ?? ""}`;
|
|
36
|
+
case "WebSearchTool":
|
|
37
|
+
return `search ${String((a as any).query ?? "")}`;
|
|
38
|
+
case "WebFetchTool":
|
|
39
|
+
return `fetch ${String((a as any).url ?? "")}`;
|
|
40
|
+
case "MemoryReadTool":
|
|
41
|
+
return `memory read`;
|
|
42
|
+
case "MemoryWriteTool":
|
|
43
|
+
return `memory write`;
|
|
44
|
+
case "MemoryEditTool":
|
|
45
|
+
return `memory edit`;
|
|
46
|
+
case "ThinkTool":
|
|
47
|
+
return `think`;
|
|
48
|
+
case "AgentTool":
|
|
49
|
+
return `agent · ${String((a as any).task ?? (a as any).subtask ?? "").slice(0, 50)}`;
|
|
50
|
+
case "OrchestratorTool":
|
|
51
|
+
return `orchestrate · ${String((a as any).goal ?? "").slice(0, 50)}`;
|
|
52
|
+
default:
|
|
53
|
+
return toolName;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getOutputPreview(toolName: string, output: unknown): string | null {
|
|
58
|
+
if (!output || typeof output !== "object") return null;
|
|
59
|
+
const o = output as Record<string, unknown>;
|
|
60
|
+
switch (toolName) {
|
|
61
|
+
case "BashTool": {
|
|
62
|
+
const out = String(o.output ?? "").trim();
|
|
63
|
+
const first = out.split("\n")[0] ?? "";
|
|
64
|
+
return first.length > 60 ? first.slice(0, 60) + "…" : first || null;
|
|
65
|
+
}
|
|
66
|
+
case "FileReadTool": {
|
|
67
|
+
const lines = String(o.content ?? "")
|
|
68
|
+
.trim()
|
|
69
|
+
.split("\n").length;
|
|
70
|
+
return `${lines} lines`;
|
|
71
|
+
}
|
|
72
|
+
case "GrepTool": {
|
|
73
|
+
const matches = Array.isArray(o.matches) ? o.matches.length : 0;
|
|
74
|
+
return matches > 0
|
|
75
|
+
? `${matches} match${matches === 1 ? "" : "es"}`
|
|
76
|
+
: "no matches";
|
|
77
|
+
}
|
|
78
|
+
case "RecallTool": {
|
|
79
|
+
const matches = String(o.output ?? "")
|
|
80
|
+
.trim()
|
|
81
|
+
.split("\n")
|
|
82
|
+
.filter(Boolean).length;
|
|
83
|
+
return matches > 0
|
|
84
|
+
? `${matches} match${matches === 1 ? "" : "es"}`
|
|
85
|
+
: "no matches";
|
|
86
|
+
}
|
|
87
|
+
case "WebSearchTool": {
|
|
88
|
+
const results = (o.results as any[])?.length ?? 0;
|
|
89
|
+
return `${results} result${results === 1 ? "" : "s"}`;
|
|
90
|
+
}
|
|
91
|
+
case "WebFetchTool": {
|
|
92
|
+
const content = String(o.content ?? "").trim();
|
|
93
|
+
return content.length > 0 ? `${content.length} chars` : null;
|
|
94
|
+
}
|
|
95
|
+
case "FileWriteTool":
|
|
96
|
+
case "FileEditTool":
|
|
97
|
+
return o.success ? "saved" : "failed";
|
|
98
|
+
case "ThinkTool":
|
|
99
|
+
return null;
|
|
100
|
+
default:
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getDiff(
|
|
106
|
+
toolName: string,
|
|
107
|
+
output: unknown,
|
|
108
|
+
): StructuredPatchHunk | null {
|
|
109
|
+
if (!output || typeof output !== "object") return null;
|
|
110
|
+
const o = output as Record<string, unknown>;
|
|
111
|
+
if (!o.success) return null;
|
|
112
|
+
|
|
113
|
+
if (toolName === "FileWriteTool") {
|
|
114
|
+
const content = String(o.content ?? "");
|
|
115
|
+
if (!content) return null;
|
|
116
|
+
const lines = content.split("\n").map((l) => "+" + l);
|
|
117
|
+
return {
|
|
118
|
+
oldStart: 1,
|
|
119
|
+
oldLines: 0,
|
|
120
|
+
newStart: 1,
|
|
121
|
+
newLines: lines.length,
|
|
122
|
+
lines,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (toolName === "FileEditTool") {
|
|
127
|
+
const patch = String(o.patch ?? "");
|
|
128
|
+
if (!patch) return null;
|
|
129
|
+
const parsed = parsePatch(patch);
|
|
130
|
+
return parsed[0]?.hunks[0] ?? null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function ToolResultMessage({
|
|
137
|
+
toolName,
|
|
138
|
+
input,
|
|
139
|
+
output,
|
|
140
|
+
success,
|
|
141
|
+
addMargin = false,
|
|
142
|
+
}: Props): React.ReactNode {
|
|
143
|
+
const { columns } = useTerminalSize();
|
|
144
|
+
const action = getAction(toolName, input);
|
|
145
|
+
const preview = action.length > 60 ? action.slice(0, 60) + "…" : action;
|
|
146
|
+
const outputPreview = getOutputPreview(toolName, output);
|
|
147
|
+
const hunk = getDiff(toolName, output);
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<Box flexDirection="row" marginTop={addMargin ? 1 : 0}>
|
|
151
|
+
<Box minWidth={2} width={2}>
|
|
152
|
+
<Text color={success ? getTheme().success : getTheme().error}>
|
|
153
|
+
{star}
|
|
154
|
+
</Text>
|
|
155
|
+
</Box>
|
|
156
|
+
<Box flexDirection="column" width={columns - 4}>
|
|
157
|
+
<Text>{toolName}</Text>
|
|
158
|
+
<Box gap={1} alignItems="center">
|
|
159
|
+
<Text color={getTheme().secondaryText} dimColor>
|
|
160
|
+
{cornerBottomLeft}
|
|
161
|
+
{line} {preview}
|
|
162
|
+
</Text>
|
|
163
|
+
<Text dimColor color={getTheme().secondaryText}>
|
|
164
|
+
{dot}
|
|
165
|
+
</Text>
|
|
166
|
+
{outputPreview && (
|
|
167
|
+
<Text color={getTheme().secondaryText} dimColor>
|
|
168
|
+
{outputPreview}
|
|
169
|
+
</Text>
|
|
170
|
+
)}
|
|
171
|
+
</Box>
|
|
172
|
+
|
|
173
|
+
{hunk && (
|
|
174
|
+
<Box marginTop={1} marginLeft={2} flexDirection="column">
|
|
175
|
+
<StructuredDiff patch={hunk} dim={false} width={columns - 8} />
|
|
176
|
+
</Box>
|
|
177
|
+
)}
|
|
178
|
+
</Box>
|
|
179
|
+
</Box>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { getTheme } from "../../utils/theme";
|
|
4
|
+
import { useTerminalSize } from "../../hooks/useTerminalSize";
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
text: string;
|
|
8
|
+
addMargin?: boolean;
|
|
9
|
+
isFirst?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function UserMessage({
|
|
13
|
+
text,
|
|
14
|
+
addMargin = false,
|
|
15
|
+
isFirst = false,
|
|
16
|
+
}: Props): React.ReactNode {
|
|
17
|
+
const { columns } = useTerminalSize();
|
|
18
|
+
if (!text) return null;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Box flexDirection="row" marginTop={isFirst ? 0 : 1} width="100%">
|
|
22
|
+
<Box minWidth={2} width={2}>
|
|
23
|
+
<Text color={getTheme().secondaryText}>{">"}</Text>
|
|
24
|
+
</Box>
|
|
25
|
+
<Box flexDirection="column" width={columns - 4}>
|
|
26
|
+
<Text color={getTheme().secondaryText} wrap="wrap">
|
|
27
|
+
{text}
|
|
28
|
+
</Text>
|
|
29
|
+
</Box>
|
|
30
|
+
</Box>
|
|
31
|
+
);
|
|
32
|
+
}
|