@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,68 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import React, { useState, useEffect } from "react";
|
|
3
|
+
import { getTheme } from "../utils/theme";
|
|
4
|
+
import { useTerminalSize } from "../hooks/useTerminalSize";
|
|
5
|
+
import { getModel } from "../utils/model";
|
|
6
|
+
import { AsciiLogo } from "./AsciiLogo";
|
|
7
|
+
import { bullet, dot, lineVertical } from "../icons";
|
|
8
|
+
import { cwd } from "process";
|
|
9
|
+
|
|
10
|
+
const TIPS = [
|
|
11
|
+
"run /init to generate MILO.md",
|
|
12
|
+
"use /mode chat for read-only",
|
|
13
|
+
"use /mode plan for big tasks",
|
|
14
|
+
"esc to interrupt at any time",
|
|
15
|
+
"/help to see all commands",
|
|
16
|
+
"ctrl+t to switch mode",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export function Header(props: Record<string, unknown> = {}): React.ReactNode {
|
|
20
|
+
const { columns } = useTerminalSize();
|
|
21
|
+
const tip = TIPS[Math.floor(Math.random() * TIPS.length)];
|
|
22
|
+
const [modelLabel, setModelLabel] = useState("no model");
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
getModel()
|
|
26
|
+
.then(({ modelId }) => setModelLabel(modelId))
|
|
27
|
+
.catch(() => {});
|
|
28
|
+
}, [JSON.stringify(props)]);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Box
|
|
32
|
+
width={columns}
|
|
33
|
+
flexDirection="row"
|
|
34
|
+
marginTop={1}
|
|
35
|
+
marginBottom={1}
|
|
36
|
+
justifyContent="space-between"
|
|
37
|
+
gap={2}
|
|
38
|
+
>
|
|
39
|
+
<Box
|
|
40
|
+
borderStyle="round"
|
|
41
|
+
borderColor={getTheme().border}
|
|
42
|
+
paddingX={1}
|
|
43
|
+
gap={1}
|
|
44
|
+
>
|
|
45
|
+
<Box flexDirection="column">
|
|
46
|
+
<Text color={getTheme().secondaryText}>{modelLabel}</Text>
|
|
47
|
+
</Box>
|
|
48
|
+
<Box flexDirection="column" alignItems="center" paddingX={1}>
|
|
49
|
+
<Text color={getTheme().border} dimColor>
|
|
50
|
+
{Array(4).fill(lineVertical).join("\n")}
|
|
51
|
+
</Text>
|
|
52
|
+
</Box>
|
|
53
|
+
<Box flexDirection="column">
|
|
54
|
+
<Text color={getTheme().primary} bold>
|
|
55
|
+
getting started
|
|
56
|
+
</Text>
|
|
57
|
+
<Text color={getTheme().secondaryText} dimColor wrap="wrap">
|
|
58
|
+
{tip}
|
|
59
|
+
</Text>
|
|
60
|
+
</Box>
|
|
61
|
+
</Box>
|
|
62
|
+
|
|
63
|
+
<Box flexDirection="column" justifyContent="center">
|
|
64
|
+
<AsciiLogo />
|
|
65
|
+
</Box>
|
|
66
|
+
</Box>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { highlight, supportsLanguage } from "cli-highlight";
|
|
2
|
+
import { Text } from "ink";
|
|
3
|
+
import React, { useMemo } from "react";
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
code: string;
|
|
7
|
+
language: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function HighlightedCode({ code, language }: Props): React.ReactElement {
|
|
11
|
+
const highlightedCode = useMemo(() => {
|
|
12
|
+
try {
|
|
13
|
+
if (supportsLanguage(language)) {
|
|
14
|
+
return highlight(code, { language });
|
|
15
|
+
}
|
|
16
|
+
return highlight(code, { language: "markdown" });
|
|
17
|
+
} catch {
|
|
18
|
+
return highlight(code, { language: "markdown" });
|
|
19
|
+
}
|
|
20
|
+
}, [code, language]);
|
|
21
|
+
|
|
22
|
+
return <Text>{highlightedCode}</Text>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ChatMessage } from "../types";
|
|
3
|
+
import { UserMessage } from "./messages/UserMessage";
|
|
4
|
+
import { AssistantMessage } from "./messages/AssistantMessage";
|
|
5
|
+
import { ToolCallMessage } from "./messages/ToolCallMessage";
|
|
6
|
+
import { ToolResultMessage } from "./messages/ToolResultMessage";
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
msg: ChatMessage;
|
|
10
|
+
addMargin?: boolean;
|
|
11
|
+
isFirst?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const Message = React.memo(function Message({
|
|
15
|
+
msg,
|
|
16
|
+
addMargin = false,
|
|
17
|
+
isFirst = false,
|
|
18
|
+
}: Props): React.ReactNode {
|
|
19
|
+
switch (msg.type) {
|
|
20
|
+
case "user":
|
|
21
|
+
return <UserMessage text={msg.text} addMargin={true} isFirst={isFirst} />;
|
|
22
|
+
case "assistant":
|
|
23
|
+
return <AssistantMessage text={msg.text} addMargin={addMargin} />;
|
|
24
|
+
case "tool_call":
|
|
25
|
+
return (
|
|
26
|
+
<ToolCallMessage
|
|
27
|
+
toolName={msg.toolName}
|
|
28
|
+
input={msg.input}
|
|
29
|
+
addMargin={addMargin}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
case "tool_result":
|
|
33
|
+
return (
|
|
34
|
+
<ToolResultMessage
|
|
35
|
+
toolName={msg.toolName}
|
|
36
|
+
input={msg.input}
|
|
37
|
+
output={msg.output}
|
|
38
|
+
success={msg.success}
|
|
39
|
+
addMargin={addMargin}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import { getTheme } from "../utils/theme";
|
|
4
|
+
import {
|
|
5
|
+
addProvider,
|
|
6
|
+
setActiveProvider,
|
|
7
|
+
removeProvider,
|
|
8
|
+
readProviders,
|
|
9
|
+
type ProviderType,
|
|
10
|
+
type ProviderConfig,
|
|
11
|
+
} from "../utils/providers";
|
|
12
|
+
import { useTerminalSize } from "../hooks/useTerminalSize";
|
|
13
|
+
import { arrowLeft, arrowRight, tick, upDownArrow } from "../icons";
|
|
14
|
+
|
|
15
|
+
type WizardMode = "add" | "edit" | "remove" | "list";
|
|
16
|
+
|
|
17
|
+
type Props = {
|
|
18
|
+
mode: WizardMode;
|
|
19
|
+
onDone: (message?: string) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type ProvidersData = Awaited<ReturnType<typeof readProviders>>;
|
|
23
|
+
|
|
24
|
+
const PROVIDERS: ProviderType[] = ["groq", "openai", "anthropic", "ollama"];
|
|
25
|
+
|
|
26
|
+
const FIELDS = ["name", "provider", "model", "apiKey", "baseURL"] as const;
|
|
27
|
+
type Field = (typeof FIELDS)[number];
|
|
28
|
+
|
|
29
|
+
const LABELS: Record<Field, string> = {
|
|
30
|
+
name: "provider name",
|
|
31
|
+
provider: "provider type",
|
|
32
|
+
model: "model id",
|
|
33
|
+
apiKey: "api key",
|
|
34
|
+
baseURL: "base url (optional)",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function ProviderWizard({ mode, onDone }: Props): React.ReactNode {
|
|
38
|
+
const theme = getTheme();
|
|
39
|
+
const [step, setStep] = useState(0);
|
|
40
|
+
const [providerIndex, setProviderIndex] = useState(0);
|
|
41
|
+
const [values, setValues] = useState<Record<Field, string>>({
|
|
42
|
+
name: "",
|
|
43
|
+
provider: "groq",
|
|
44
|
+
model: "",
|
|
45
|
+
apiKey: "",
|
|
46
|
+
baseURL: "",
|
|
47
|
+
});
|
|
48
|
+
const [inputBuffer, setInputBuffer] = useState("");
|
|
49
|
+
const [providers, setProviders] = useState<ProvidersData | null>(null);
|
|
50
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
51
|
+
const [error, setError] = useState("");
|
|
52
|
+
|
|
53
|
+
React.useEffect(() => {
|
|
54
|
+
readProviders()
|
|
55
|
+
.then((p) => {
|
|
56
|
+
setProviders(p);
|
|
57
|
+
const activeIdx = p.providers.findIndex((x) => x.name === p.active);
|
|
58
|
+
if (activeIdx >= 0) setSelectedIndex(activeIdx);
|
|
59
|
+
})
|
|
60
|
+
.catch(() => {});
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
useInput((input, key) => {
|
|
64
|
+
setError("");
|
|
65
|
+
|
|
66
|
+
if (key.escape) {
|
|
67
|
+
onDone();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (mode === "remove" || mode === "list") {
|
|
72
|
+
if (!providers) return;
|
|
73
|
+
if (key.upArrow) {
|
|
74
|
+
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (key.downArrow) {
|
|
78
|
+
setSelectedIndex((i) =>
|
|
79
|
+
Math.min(providers.providers.length - 1, i + 1),
|
|
80
|
+
);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (key.return) {
|
|
84
|
+
const target = providers.providers[selectedIndex];
|
|
85
|
+
if (!target) return;
|
|
86
|
+
if (mode === "remove") {
|
|
87
|
+
removeProvider(target.name)
|
|
88
|
+
.then(() => onDone(`removed "${target.name}" 🫡`))
|
|
89
|
+
.catch((e) => setError(String(e)));
|
|
90
|
+
} else {
|
|
91
|
+
setActiveProvider(target.name)
|
|
92
|
+
.then(() => onDone(`switched to "${target.name}" 🫡`))
|
|
93
|
+
.catch((e) => setError(String(e)));
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (mode === "add") {
|
|
101
|
+
const currentField = FIELDS[step];
|
|
102
|
+
|
|
103
|
+
if (currentField === "provider") {
|
|
104
|
+
if (key.leftArrow) {
|
|
105
|
+
const next = Math.max(0, providerIndex - 1);
|
|
106
|
+
setProviderIndex(next);
|
|
107
|
+
setValues((v) => ({ ...v, provider: PROVIDERS[next]! }));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (key.rightArrow) {
|
|
111
|
+
const next = Math.min(PROVIDERS.length - 1, providerIndex + 1);
|
|
112
|
+
setProviderIndex(next);
|
|
113
|
+
setValues((v) => ({ ...v, provider: PROVIDERS[next]! }));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (key.return) {
|
|
117
|
+
setValues((v) => ({ ...v, provider: PROVIDERS[providerIndex]! }));
|
|
118
|
+
setStep((s) => s + 1);
|
|
119
|
+
setInputBuffer("");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (key.return) {
|
|
126
|
+
const isOptional =
|
|
127
|
+
currentField === "baseURL" || currentField === "apiKey";
|
|
128
|
+
if (!isOptional && inputBuffer.trim() === "") {
|
|
129
|
+
setError("this field is required");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const newValues = { ...values, [currentField!]: inputBuffer.trim() };
|
|
133
|
+
setValues(newValues);
|
|
134
|
+
|
|
135
|
+
if (step === FIELDS.length - 1) {
|
|
136
|
+
addProvider({
|
|
137
|
+
name: newValues.name,
|
|
138
|
+
provider: newValues.provider as ProviderType,
|
|
139
|
+
model: newValues.model,
|
|
140
|
+
apiKey: newValues.apiKey || undefined,
|
|
141
|
+
baseURL: newValues.baseURL || undefined,
|
|
142
|
+
})
|
|
143
|
+
.then(() => onDone(`added "${newValues.name}" 🫡`))
|
|
144
|
+
.catch((e) => setError(String(e)));
|
|
145
|
+
} else {
|
|
146
|
+
setStep((s) => s + 1);
|
|
147
|
+
setInputBuffer("");
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (key.backspace || key.delete) {
|
|
153
|
+
setInputBuffer((b) => b.slice(0, -1));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!key.ctrl && !key.meta && input) {
|
|
158
|
+
setInputBuffer((b) => b + input);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (mode === "list" || mode === "remove") {
|
|
164
|
+
return (
|
|
165
|
+
<Box flexDirection="column" paddingX={1} paddingY={1}>
|
|
166
|
+
<Box flexDirection="row" marginBottom={1} gap={2}>
|
|
167
|
+
<Text color={theme.primary}>
|
|
168
|
+
{mode === "remove" ? "remove provider" : "switch provider"}
|
|
169
|
+
</Text>
|
|
170
|
+
<Text color={theme.secondaryText} dimColor>
|
|
171
|
+
{upDownArrow} select · enter confirm · esc cancel
|
|
172
|
+
</Text>
|
|
173
|
+
</Box>
|
|
174
|
+
|
|
175
|
+
<Box flexDirection="column">
|
|
176
|
+
{providers?.providers.map((p: ProviderConfig, i: number) => {
|
|
177
|
+
const isSelected = i === selectedIndex;
|
|
178
|
+
const isActive = p.name === providers.active;
|
|
179
|
+
return (
|
|
180
|
+
<Box key={p.name} flexDirection="row" gap={2} paddingX={1}>
|
|
181
|
+
<Text
|
|
182
|
+
color={isSelected ? theme.secondary : theme.secondaryText}
|
|
183
|
+
>
|
|
184
|
+
{isSelected ? arrowRight : " "}
|
|
185
|
+
</Text>
|
|
186
|
+
<Box flexDirection="row" gap={1}>
|
|
187
|
+
<Text
|
|
188
|
+
color={isSelected ? theme.secondary : theme.secondaryText}
|
|
189
|
+
>
|
|
190
|
+
{p.name}
|
|
191
|
+
</Text>
|
|
192
|
+
<Text color={theme.secondaryText} dimColor>
|
|
193
|
+
{p.provider} · {p.model}
|
|
194
|
+
</Text>
|
|
195
|
+
{isActive && <Text color={theme.success}>active</Text>}
|
|
196
|
+
</Box>
|
|
197
|
+
</Box>
|
|
198
|
+
);
|
|
199
|
+
})}
|
|
200
|
+
</Box>
|
|
201
|
+
|
|
202
|
+
{error && (
|
|
203
|
+
<Box marginTop={1}>
|
|
204
|
+
<Text color={theme.error}>{error}</Text>
|
|
205
|
+
</Box>
|
|
206
|
+
)}
|
|
207
|
+
|
|
208
|
+
<Box marginTop={1}>
|
|
209
|
+
<Text color={theme.secondaryText} dimColor>
|
|
210
|
+
{upDownArrow} to select · enter to confirm · esc to cancel
|
|
211
|
+
</Text>
|
|
212
|
+
</Box>
|
|
213
|
+
</Box>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const currentField = FIELDS[step];
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<Box flexDirection="column" paddingX={1} paddingY={1}>
|
|
221
|
+
<Box flexDirection="row" marginBottom={1} gap={2}>
|
|
222
|
+
<Text color={theme.primary}>add provider</Text>
|
|
223
|
+
<Text color={theme.secondaryText} dimColor>
|
|
224
|
+
esc to cancel
|
|
225
|
+
</Text>
|
|
226
|
+
</Box>
|
|
227
|
+
|
|
228
|
+
{FIELDS.slice(0, step).map((f) => (
|
|
229
|
+
<Box key={f} flexDirection="row" gap={2} paddingX={1}>
|
|
230
|
+
<Text color={theme.success}>{tick}</Text>
|
|
231
|
+
<Text color={theme.secondaryText}>{LABELS[f]}</Text>
|
|
232
|
+
<Text color={theme.text}>
|
|
233
|
+
{f === "apiKey" ? "•".repeat(8) : values[f] || "—"}
|
|
234
|
+
</Text>
|
|
235
|
+
</Box>
|
|
236
|
+
))}
|
|
237
|
+
|
|
238
|
+
<Box flexDirection="row" gap={2} paddingX={1} marginTop={1}>
|
|
239
|
+
<Text color={theme.primary}>{arrowRight}</Text>
|
|
240
|
+
<Text color={theme.secondary}>{LABELS[currentField!]}</Text>
|
|
241
|
+
{currentField === "provider" ? (
|
|
242
|
+
<Box flexDirection="row" gap={1}>
|
|
243
|
+
{PROVIDERS.map((p, i) => (
|
|
244
|
+
<Text
|
|
245
|
+
key={p}
|
|
246
|
+
color={i === providerIndex ? theme.error : theme.secondaryText}
|
|
247
|
+
>
|
|
248
|
+
{p}
|
|
249
|
+
</Text>
|
|
250
|
+
))}
|
|
251
|
+
</Box>
|
|
252
|
+
) : (
|
|
253
|
+
<Text color={theme.text}>
|
|
254
|
+
{currentField === "apiKey"
|
|
255
|
+
? "•".repeat(inputBuffer.length) + "█"
|
|
256
|
+
: inputBuffer + "█"}
|
|
257
|
+
</Text>
|
|
258
|
+
)}
|
|
259
|
+
</Box>
|
|
260
|
+
|
|
261
|
+
<Box marginTop={1} marginLeft={4}>
|
|
262
|
+
<Text color={theme.secondaryText} dimColor>
|
|
263
|
+
{currentField === "provider"
|
|
264
|
+
? `${arrowLeft} ${arrowRight} to select · enter to confirm`
|
|
265
|
+
: currentField === "baseURL" || currentField === "apiKey"
|
|
266
|
+
? "enter to confirm · or skip"
|
|
267
|
+
: "enter to confirm"}
|
|
268
|
+
</Text>
|
|
269
|
+
</Box>
|
|
270
|
+
|
|
271
|
+
{error && (
|
|
272
|
+
<Box marginTop={1}>
|
|
273
|
+
<Text color={theme.error}>{error}</Text>
|
|
274
|
+
</Box>
|
|
275
|
+
)}
|
|
276
|
+
</Box>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import InkSpinner from "ink-spinner";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
import { getTheme } from "../utils/theme";
|
|
6
|
+
import { sample } from "lodash-es";
|
|
7
|
+
import { cornerBottomLeft, line } from "../icons";
|
|
8
|
+
import { readPet, getSpinnerPool } from "../pet";
|
|
9
|
+
|
|
10
|
+
const TIPS = [
|
|
11
|
+
"use agent mode for file tasks",
|
|
12
|
+
"use plan mode for large multi-step tasks",
|
|
13
|
+
"esc to interrupt at any time",
|
|
14
|
+
"milo remembers your preferences",
|
|
15
|
+
"use RecallTool to search past sessions",
|
|
16
|
+
"milo reads MILO.md for project context",
|
|
17
|
+
"chain tasks in one prompt for best results",
|
|
18
|
+
"milo can run bash commands on your behalf",
|
|
19
|
+
"use memory to persist facts across sessions",
|
|
20
|
+
"the more context you give, the better the output",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export function Spinner(): React.ReactNode {
|
|
24
|
+
const [elapsedTime, setElapsedTime] = useState(0);
|
|
25
|
+
const message = useRef<string>("Sniffing… 🐱");
|
|
26
|
+
const tip = useRef(sample(TIPS));
|
|
27
|
+
const startTime = useRef(Date.now());
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
readPet()
|
|
31
|
+
.then((pet) => {
|
|
32
|
+
const pool = getSpinnerPool(pet.level);
|
|
33
|
+
message.current = sample(pool) ?? "Sniffing… 🐱";
|
|
34
|
+
})
|
|
35
|
+
.catch(() => {});
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const timer = setInterval(() => {
|
|
40
|
+
setElapsedTime(Math.floor((Date.now() - startTime.current) / 1000));
|
|
41
|
+
}, 1000);
|
|
42
|
+
return () => clearInterval(timer);
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Box flexDirection="column" height={2}>
|
|
47
|
+
<Box flexDirection="row">
|
|
48
|
+
<Box flexWrap="nowrap" height={1} width={2}>
|
|
49
|
+
<Text color={getTheme().primary}>
|
|
50
|
+
<InkSpinner type="star" />
|
|
51
|
+
</Text>
|
|
52
|
+
</Box>
|
|
53
|
+
<Text color={getTheme().primary}> {message.current} </Text>
|
|
54
|
+
<Text color={getTheme().secondaryText}>
|
|
55
|
+
({elapsedTime}s · <Text bold>esc</Text> to interrupt)
|
|
56
|
+
</Text>
|
|
57
|
+
</Box>
|
|
58
|
+
<Box flexDirection="row" marginLeft={1}>
|
|
59
|
+
<Text color={getTheme().secondaryText} dimColor>
|
|
60
|
+
{cornerBottomLeft}
|
|
61
|
+
{line} tip: {tip.current}
|
|
62
|
+
</Text>
|
|
63
|
+
</Box>
|
|
64
|
+
</Box>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function SimpleSpinner(): React.ReactNode {
|
|
69
|
+
return (
|
|
70
|
+
<Box flexWrap="nowrap" height={1} width={2}>
|
|
71
|
+
<Text color={getTheme().primary}>
|
|
72
|
+
<InkSpinner type="star" />
|
|
73
|
+
</Text>
|
|
74
|
+
</Box>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { getTheme } from "../utils/theme";
|
|
4
|
+
import { useTerminalSize } from "../hooks/useTerminalSize";
|
|
5
|
+
import type { Mode } from "../types";
|
|
6
|
+
import { bullet, diamond, star } from "../icons";
|
|
7
|
+
import { readPet } from "../pet";
|
|
8
|
+
import type { Pet } from "../types";
|
|
9
|
+
import Spinner from "ink-spinner";
|
|
10
|
+
|
|
11
|
+
type Props = {
|
|
12
|
+
model: string;
|
|
13
|
+
mode: Mode;
|
|
14
|
+
thinking: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function getModeColor(mode: Mode): string {
|
|
18
|
+
switch (mode) {
|
|
19
|
+
case "agent":
|
|
20
|
+
return getTheme().secondary;
|
|
21
|
+
case "plan":
|
|
22
|
+
return getTheme().warning;
|
|
23
|
+
case "chat":
|
|
24
|
+
return getTheme().success;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function StatusBar({
|
|
29
|
+
model,
|
|
30
|
+
mode,
|
|
31
|
+
thinking = false,
|
|
32
|
+
}: Props): React.ReactNode {
|
|
33
|
+
const { columns } = useTerminalSize();
|
|
34
|
+
const [pet, setPet] = useState<Pet | null>(null);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
readPet()
|
|
38
|
+
.then(setPet)
|
|
39
|
+
.catch(() => {});
|
|
40
|
+
|
|
41
|
+
const interval = setInterval(() => {
|
|
42
|
+
readPet()
|
|
43
|
+
.then(setPet)
|
|
44
|
+
.catch(() => {});
|
|
45
|
+
}, 5000);
|
|
46
|
+
return () => clearInterval(interval);
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
const icon = mode === "agent" ? diamond : mode === "plan" ? star : bullet;
|
|
50
|
+
const modeBg = getModeColor(mode);
|
|
51
|
+
|
|
52
|
+
const modelPart = ` ${model} `;
|
|
53
|
+
const modePart = ` ${icon} ${mode} mode `;
|
|
54
|
+
|
|
55
|
+
const levelPart = pet ? ` lv.${pet.level} ` : "";
|
|
56
|
+
const xpPart = pet ? ` ${pet.xp}/${pet.xpToNext}xp ` : "";
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Box width={columns} justifyContent="space-between">
|
|
60
|
+
<Box gap={2}>
|
|
61
|
+
{thinking ? (
|
|
62
|
+
<Box>
|
|
63
|
+
<Spinner type="bluePulse" />
|
|
64
|
+
<Text>Thinking</Text>
|
|
65
|
+
</Box>
|
|
66
|
+
) : (
|
|
67
|
+
<>
|
|
68
|
+
<Box>
|
|
69
|
+
<Text color={getTheme().error}>{star}</Text>
|
|
70
|
+
<Text color={getTheme().secondary}>{levelPart}</Text>
|
|
71
|
+
</Box>
|
|
72
|
+
<Box>
|
|
73
|
+
<Text color={getTheme().warning}>{diamond}</Text>
|
|
74
|
+
<Text color={getTheme().success}>{xpPart}</Text>
|
|
75
|
+
</Box>
|
|
76
|
+
</>
|
|
77
|
+
)}
|
|
78
|
+
</Box>
|
|
79
|
+
<Text color={getTheme().secondaryText}>{modelPart}</Text>
|
|
80
|
+
<Text backgroundColor={modeBg} color="#000000">
|
|
81
|
+
{modePart}
|
|
82
|
+
</Text>
|
|
83
|
+
</Box>
|
|
84
|
+
);
|
|
85
|
+
}
|