@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,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
+ }