@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.
Files changed (111) hide show
  1. package/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +111 -0
  2. package/LICENSE +21 -0
  3. package/README.md +122 -0
  4. package/dist/index.mjs +106603 -0
  5. package/package.json +64 -0
  6. package/src/commands/clear.ts +18 -0
  7. package/src/commands/crimes.ts +48 -0
  8. package/src/commands/feed.ts +20 -0
  9. package/src/commands/genz.ts +33 -0
  10. package/src/commands/help.ts +25 -0
  11. package/src/commands/init.ts +65 -0
  12. package/src/commands/mode.ts +22 -0
  13. package/src/commands/pet.ts +35 -0
  14. package/src/commands/provider.ts +46 -0
  15. package/src/commands/roast.ts +40 -0
  16. package/src/commands/vibe.ts +42 -0
  17. package/src/commands.ts +43 -0
  18. package/src/components/AsciiLogo.tsx +25 -0
  19. package/src/components/CommandSuggestions.tsx +78 -0
  20. package/src/components/Header.tsx +68 -0
  21. package/src/components/HighlightedCode.tsx +23 -0
  22. package/src/components/Message.tsx +43 -0
  23. package/src/components/ProviderWizard.tsx +278 -0
  24. package/src/components/Spinner.tsx +76 -0
  25. package/src/components/StatusBar.tsx +85 -0
  26. package/src/components/StructuredDiff.tsx +194 -0
  27. package/src/components/TextInput.tsx +144 -0
  28. package/src/components/messages/AssistantMessage.tsx +68 -0
  29. package/src/components/messages/ToolCallMessage.tsx +77 -0
  30. package/src/components/messages/ToolResultMessage.tsx +181 -0
  31. package/src/components/messages/UserMessage.tsx +32 -0
  32. package/src/components/permissions/PermissionCard.tsx +152 -0
  33. package/src/history.ts +27 -0
  34. package/src/hooks/useArrowKeyHistory.ts +0 -0
  35. package/src/hooks/useChat.ts +271 -0
  36. package/src/hooks/useDoublePress.ts +35 -0
  37. package/src/hooks/useTerminalSize.ts +24 -0
  38. package/src/hooks/useTextInput.ts +263 -0
  39. package/src/icons.ts +31 -0
  40. package/src/index.tsx +5 -0
  41. package/src/multi-agent/agent/agent.ts +33 -0
  42. package/src/multi-agent/orchestrator/orchestrator.ts +103 -0
  43. package/src/multi-agent/schemas.ts +12 -0
  44. package/src/multi-agent/types.ts +8 -0
  45. package/src/permissions.ts +54 -0
  46. package/src/pet.ts +239 -0
  47. package/src/screens/REPL.tsx +261 -0
  48. package/src/shortcuts.ts +37 -0
  49. package/src/skills/backend.ts +76 -0
  50. package/src/skills/cicd.ts +57 -0
  51. package/src/skills/colors.ts +72 -0
  52. package/src/skills/database.ts +55 -0
  53. package/src/skills/docker.ts +74 -0
  54. package/src/skills/frontend.ts +70 -0
  55. package/src/skills/git.ts +52 -0
  56. package/src/skills/testing.ts +73 -0
  57. package/src/skills/typography.ts +57 -0
  58. package/src/skills/uiux.ts +43 -0
  59. package/src/tools/AgentTool/prompt.ts +17 -0
  60. package/src/tools/AgentTool/tool.ts +22 -0
  61. package/src/tools/BashTool/prompt.ts +82 -0
  62. package/src/tools/BashTool/tool.ts +54 -0
  63. package/src/tools/FileEditTool/prompt.ts +13 -0
  64. package/src/tools/FileEditTool/tool.ts +39 -0
  65. package/src/tools/FileReadTool/prompt.ts +5 -0
  66. package/src/tools/FileReadTool/tool.ts +34 -0
  67. package/src/tools/FileWriteTool/prompt.ts +19 -0
  68. package/src/tools/FileWriteTool/tool.ts +34 -0
  69. package/src/tools/GlobTool/prompt.ts +11 -0
  70. package/src/tools/GlobTool/tool.ts +34 -0
  71. package/src/tools/GrepTool/prompt.ts +13 -0
  72. package/src/tools/GrepTool/tool.ts +41 -0
  73. package/src/tools/MemoryEditTool/prompt.ts +10 -0
  74. package/src/tools/MemoryEditTool/tool.ts +38 -0
  75. package/src/tools/MemoryReadTool/prompt.ts +9 -0
  76. package/src/tools/MemoryReadTool/tool.ts +47 -0
  77. package/src/tools/MemoryWriteTool/prompt.ts +10 -0
  78. package/src/tools/MemoryWriteTool/tool.ts +30 -0
  79. package/src/tools/OrchestratorTool/prompt.ts +26 -0
  80. package/src/tools/OrchestratorTool/tool.ts +20 -0
  81. package/src/tools/RecallTool/prompt.ts +13 -0
  82. package/src/tools/RecallTool/tool.ts +47 -0
  83. package/src/tools/ThinkTool/tool.ts +16 -0
  84. package/src/tools/WebFetchTool/prompt.ts +7 -0
  85. package/src/tools/WebFetchTool/tool.ts +33 -0
  86. package/src/tools/WebSearchTool/prompt.ts +8 -0
  87. package/src/tools/WebSearchTool/tool.ts +49 -0
  88. package/src/types.ts +124 -0
  89. package/src/utils/Cursor.ts +423 -0
  90. package/src/utils/PersistentShell.ts +306 -0
  91. package/src/utils/agent.ts +21 -0
  92. package/src/utils/chat.ts +21 -0
  93. package/src/utils/compaction.ts +71 -0
  94. package/src/utils/env.ts +11 -0
  95. package/src/utils/file.ts +42 -0
  96. package/src/utils/format.ts +46 -0
  97. package/src/utils/imagePaste.ts +78 -0
  98. package/src/utils/json.ts +10 -0
  99. package/src/utils/llm.ts +65 -0
  100. package/src/utils/markdown.ts +258 -0
  101. package/src/utils/messages.ts +81 -0
  102. package/src/utils/model.ts +16 -0
  103. package/src/utils/plan.ts +26 -0
  104. package/src/utils/providers.ts +100 -0
  105. package/src/utils/ripgrep.ts +175 -0
  106. package/src/utils/session.ts +100 -0
  107. package/src/utils/skills.ts +26 -0
  108. package/src/utils/systemPrompt.ts +218 -0
  109. package/src/utils/theme.ts +110 -0
  110. package/src/utils/tools.ts +58 -0
  111. 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
+ }