mirai-cli 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/bin/config.ts +99 -0
- package/bin/mirai.js +17 -0
- package/bin/mirai.ts +4 -0
- package/bin/provider.ts +149 -0
- package/bin/router.ts +134 -0
- package/dist/mirai.mjs +28316 -0
- package/package.json +29 -0
- package/src/app/index.tsx +274 -0
- package/src/components/chat.tsx +254 -0
- package/src/components/dialog/help-dialog.tsx +101 -0
- package/src/components/dialog/index.ts +3 -0
- package/src/components/dialog/provider.tsx +96 -0
- package/src/components/header/index.tsx +78 -0
- package/src/components/input/command-palette.tsx +129 -0
- package/src/components/input/commands.ts +46 -0
- package/src/components/input/index.tsx +284 -0
- package/src/components/matrix-rain/index.tsx +122 -0
- package/src/components/permission-modal.tsx +66 -0
- package/src/components/scroll-bar/index.tsx +56 -0
- package/src/components/status-bar/index.tsx +43 -0
- package/src/components/tool-result.tsx +11 -0
- package/src/hooks/use-chat.ts +208 -0
- package/src/hooks/use-mouse.tsx +121 -0
- package/src/hooks/use-permission.ts +35 -0
- package/src/hooks/use-runtime.ts +99 -0
- package/src/hooks/use-scroll-bar-drag.ts +115 -0
- package/src/hooks/use-scroll.ts +70 -0
- package/src/index.ts +39 -0
- package/src/renderers/builtins/BashResult.tsx +65 -0
- package/src/renderers/builtins/EditFileResult.tsx +69 -0
- package/src/renderers/builtins/GenericToolResult.tsx +39 -0
- package/src/renderers/builtins/GlobSearchResult.tsx +40 -0
- package/src/renderers/builtins/GrepSearchResult.tsx +49 -0
- package/src/renderers/builtins/ReadFileResult.tsx +54 -0
- package/src/renderers/builtins/WriteFileResult.tsx +24 -0
- package/src/renderers/constants.ts +7 -0
- package/src/renderers/register-builtins.ts +27 -0
- package/src/renderers/registry.ts +37 -0
- package/src/renderers/status.ts +22 -0
- package/src/renderers/utils.ts +70 -0
- package/src/services/hit-test.ts +49 -0
- package/src/services/mouse-input.ts +237 -0
- package/src/services/scroll-registry.ts +64 -0
- package/src/services/tui-permission-provider.ts +35 -0
- package/src/theme.ts +38 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from "react";
|
|
2
|
+
import { detectProvider } from "@mirai/llm";
|
|
3
|
+
import { ConversationRuntime } from "@mirai/runtime";
|
|
4
|
+
import { toolRegistry } from "@mirai/tools";
|
|
5
|
+
import { DEFAULT_MODEL } from "@mirai/core/constants";
|
|
6
|
+
import type { Message, ToolEntry } from "@mirai/core/types";
|
|
7
|
+
import { buildToolContext } from "@mirai/workspace";
|
|
8
|
+
import type { TuiPermissionProvider } from "../services/tui-permission-provider.js";
|
|
9
|
+
import type { RuleStore } from "@mirai/permission";
|
|
10
|
+
import type { ExecutionContext } from "@mirai/runtime";
|
|
11
|
+
|
|
12
|
+
export interface RuntimeChatState {
|
|
13
|
+
messages: Message[];
|
|
14
|
+
isStreaming: boolean;
|
|
15
|
+
error: string | null;
|
|
16
|
+
toolFeed: ToolEntry[];
|
|
17
|
+
tokensUsed: number;
|
|
18
|
+
cost: number;
|
|
19
|
+
startTime: number;
|
|
20
|
+
submit(prompt: string, ctx?: ExecutionContext): Promise<void>;
|
|
21
|
+
clearMessages(): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface RuntimeChatDeps {
|
|
25
|
+
permissionProvider: TuiPermissionProvider;
|
|
26
|
+
ruleStore: RuleStore;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useRuntimeChat(deps: RuntimeChatDeps): RuntimeChatState {
|
|
30
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
31
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
32
|
+
const [error, setError] = useState<string | null>(null);
|
|
33
|
+
const [toolFeed, setToolFeed] = useState<ToolEntry[]>([]);
|
|
34
|
+
const [tokensUsed, setTokensUsed] = useState(0);
|
|
35
|
+
const [cost, setCost] = useState(0);
|
|
36
|
+
const startTimeRef = useRef(Date.now());
|
|
37
|
+
const runtimeRef = useRef<ConversationRuntime | null>(null);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const provider = detectProvider(DEFAULT_MODEL);
|
|
41
|
+
runtimeRef.current = new ConversationRuntime(
|
|
42
|
+
provider,
|
|
43
|
+
toolRegistry,
|
|
44
|
+
deps.ruleStore,
|
|
45
|
+
deps.permissionProvider,
|
|
46
|
+
buildToolContext(),
|
|
47
|
+
{
|
|
48
|
+
systemPrompt:
|
|
49
|
+
"You are a helpful coding assistant. Use tools when needed.",
|
|
50
|
+
maxIterations: 10,
|
|
51
|
+
model: DEFAULT_MODEL,
|
|
52
|
+
},
|
|
53
|
+
(messages) => setMessages([...messages]),
|
|
54
|
+
);
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
const submit = useCallback(async (prompt: string, ctx?: ExecutionContext) => {
|
|
58
|
+
const runtime = runtimeRef.current;
|
|
59
|
+
if (!runtime) return;
|
|
60
|
+
|
|
61
|
+
setError(null);
|
|
62
|
+
setIsStreaming(true);
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const result = await runtime.runTurn(prompt, ctx);
|
|
66
|
+
|
|
67
|
+
if (result.usage) {
|
|
68
|
+
setTokensUsed((prev) => prev + result.usage!.totalTokens);
|
|
69
|
+
setCost(
|
|
70
|
+
(prev) =>
|
|
71
|
+
prev +
|
|
72
|
+
result.usage!.inputTokens * 0.000002 +
|
|
73
|
+
result.usage!.outputTokens * 0.00001,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
78
|
+
} finally {
|
|
79
|
+
setIsStreaming(false);
|
|
80
|
+
}
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
const clearMessages = useCallback(() => {
|
|
84
|
+
setMessages([]);
|
|
85
|
+
startTimeRef.current = Date.now();
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
messages,
|
|
90
|
+
isStreaming,
|
|
91
|
+
error,
|
|
92
|
+
toolFeed,
|
|
93
|
+
tokensUsed,
|
|
94
|
+
cost,
|
|
95
|
+
startTime: startTimeRef.current,
|
|
96
|
+
submit,
|
|
97
|
+
clearMessages,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useRef, useEffect } from "react";
|
|
2
|
+
import { mouseInput } from "../services/mouse-input.js";
|
|
3
|
+
|
|
4
|
+
export interface DragHandle {
|
|
5
|
+
isDragging: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface DragState {
|
|
9
|
+
isDragging: boolean;
|
|
10
|
+
dragStartY: number;
|
|
11
|
+
dragStartOffset: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ScrollBarDimensions {
|
|
15
|
+
thumbPos: number;
|
|
16
|
+
thumbHeight: number;
|
|
17
|
+
barHeight: number;
|
|
18
|
+
barTop: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useScrollBarDrag(
|
|
22
|
+
barRef: React.RefObject<any>,
|
|
23
|
+
scrollRef: React.RefObject<any>,
|
|
24
|
+
dims: ScrollBarDimensions,
|
|
25
|
+
): DragHandle {
|
|
26
|
+
const dragRef = useRef<DragState>({
|
|
27
|
+
isDragging: false,
|
|
28
|
+
dragStartY: 0,
|
|
29
|
+
dragStartOffset: 0,
|
|
30
|
+
});
|
|
31
|
+
const wasPressedRef = useRef(false);
|
|
32
|
+
const dimsRef = useRef(dims);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
dimsRef.current = dims;
|
|
36
|
+
}, [dims]);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const unsub = mouseInput.on((state) => {
|
|
40
|
+
const isPressed = state.isPressed;
|
|
41
|
+
const drag = dragRef.current;
|
|
42
|
+
|
|
43
|
+
// 1. PRESS Transition: !wasPressed -> isPressed
|
|
44
|
+
if (!wasPressedRef.current && isPressed) {
|
|
45
|
+
const currentDims = dimsRef.current;
|
|
46
|
+
const relY = state.y - currentDims.barTop;
|
|
47
|
+
|
|
48
|
+
// Check if press occurred on the thumb
|
|
49
|
+
if (relY >= currentDims.thumbPos && relY < currentDims.thumbPos + currentDims.thumbHeight) {
|
|
50
|
+
const currentOffset = scrollRef.current?.getScrollOffset() ?? 0;
|
|
51
|
+
drag.isDragging = true;
|
|
52
|
+
drag.dragStartY = state.y;
|
|
53
|
+
drag.dragStartOffset = currentOffset;
|
|
54
|
+
} else {
|
|
55
|
+
// Click on track -> Jump to position
|
|
56
|
+
const ref = scrollRef.current;
|
|
57
|
+
if (ref) {
|
|
58
|
+
const content = ref.getContentHeight();
|
|
59
|
+
const viewport = ref.getViewportHeight();
|
|
60
|
+
if (content && content > viewport) {
|
|
61
|
+
const fraction = Math.max(0, Math.min(1, relY / currentDims.barHeight));
|
|
62
|
+
const targetOffset = Math.round(fraction * (content - viewport));
|
|
63
|
+
ref.scrollBy(targetOffset - ref.getScrollOffset());
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 2. DRAG Logic
|
|
70
|
+
if (drag.isDragging) {
|
|
71
|
+
const ref = scrollRef.current;
|
|
72
|
+
if (!ref) {
|
|
73
|
+
drag.isDragging = false;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const content = ref.getContentHeight();
|
|
78
|
+
const viewport = ref.getViewportHeight();
|
|
79
|
+
if (!content || content <= viewport) {
|
|
80
|
+
drag.isDragging = false;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Relative drag calculation
|
|
85
|
+
const currentDims = dimsRef.current;
|
|
86
|
+
const deltaY = state.y - drag.dragStartY;
|
|
87
|
+
const fraction = deltaY / currentDims.barHeight;
|
|
88
|
+
const targetOffset = drag.dragStartOffset + fraction * (content - viewport);
|
|
89
|
+
|
|
90
|
+
// Clamp and apply
|
|
91
|
+
const maxOffset = content - viewport;
|
|
92
|
+
const clampedOffset = Math.max(0, Math.min(maxOffset, targetOffset));
|
|
93
|
+
|
|
94
|
+
if (Math.round(clampedOffset) !== ref.getScrollOffset()) {
|
|
95
|
+
ref.scrollBy(Math.round(clampedOffset) - ref.getScrollOffset());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Handle release while dragging
|
|
99
|
+
if (!isPressed) {
|
|
100
|
+
drag.isDragging = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 3. RELEASE Transition: wasPressed -> !isPressed
|
|
105
|
+
if (wasPressedRef.current && !isPressed) {
|
|
106
|
+
drag.isDragging = false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
wasPressedRef.current = isPressed;
|
|
110
|
+
});
|
|
111
|
+
return unsub;
|
|
112
|
+
}, [barRef, scrollRef]);
|
|
113
|
+
|
|
114
|
+
return { isDragging: dragRef.current.isDragging };
|
|
115
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useLayoutEffect } from "react";
|
|
2
|
+
import { useStdout } from "ink";
|
|
3
|
+
|
|
4
|
+
export interface ScrollState {
|
|
5
|
+
offset: number;
|
|
6
|
+
content: number;
|
|
7
|
+
viewport: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function useWindowSize() {
|
|
11
|
+
const { stdout } = useStdout();
|
|
12
|
+
const [size, setSize] = useState({
|
|
13
|
+
rows: stdout?.rows ?? 24,
|
|
14
|
+
columns: stdout?.columns ?? 80,
|
|
15
|
+
});
|
|
16
|
+
useLayoutEffect(() => {
|
|
17
|
+
const onResize = () =>
|
|
18
|
+
setSize({ rows: stdout?.rows ?? 24, columns: stdout?.columns ?? 80 });
|
|
19
|
+
stdout?.on("resize", onResize);
|
|
20
|
+
return () => { stdout?.off("resize", onResize); };
|
|
21
|
+
}, [stdout]);
|
|
22
|
+
return size;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function useScroll(scrollRef: React.RefObject<any>, terminalRows: number) {
|
|
26
|
+
const [scrollPos, setScrollPos] = useState<ScrollState>({ offset: 0, content: 0, viewport: 0 });
|
|
27
|
+
const [isNearBottom, setIsNearBottom] = useState(true);
|
|
28
|
+
const prevOffsetRef = useRef(0);
|
|
29
|
+
const scrollHeight = Math.max(10, terminalRows - 5);
|
|
30
|
+
|
|
31
|
+
const updatePos = useCallback(() => {
|
|
32
|
+
const ref = scrollRef.current;
|
|
33
|
+
if (!ref) return;
|
|
34
|
+
const offset = ref.getScrollOffset();
|
|
35
|
+
const viewport = ref.getViewportHeight();
|
|
36
|
+
const content = ref.getContentHeight();
|
|
37
|
+
setScrollPos({ offset, content, viewport });
|
|
38
|
+
if (offset + viewport >= content - 3)
|
|
39
|
+
setIsNearBottom(true);
|
|
40
|
+
}, [scrollRef]);
|
|
41
|
+
|
|
42
|
+
const handleScroll = useCallback((offset: number) => {
|
|
43
|
+
const ref = scrollRef.current;
|
|
44
|
+
if (!ref) return;
|
|
45
|
+
const viewport = ref.getViewportHeight();
|
|
46
|
+
const content = ref.getContentHeight();
|
|
47
|
+
if (offset < prevOffsetRef.current && offset + viewport < content - 3)
|
|
48
|
+
setIsNearBottom(false);
|
|
49
|
+
if (offset + viewport >= content - 3)
|
|
50
|
+
setIsNearBottom(true);
|
|
51
|
+
prevOffsetRef.current = offset;
|
|
52
|
+
setScrollPos({ offset, content, viewport });
|
|
53
|
+
}, [scrollRef]);
|
|
54
|
+
|
|
55
|
+
const scrollToLatest = useCallback(() => {
|
|
56
|
+
queueMicrotask(() => {
|
|
57
|
+
scrollRef.current?.scrollToBottom();
|
|
58
|
+
updatePos();
|
|
59
|
+
});
|
|
60
|
+
}, [scrollRef, updatePos]);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
scrollPos,
|
|
64
|
+
scrollHeight,
|
|
65
|
+
isNearBottom,
|
|
66
|
+
handleScroll,
|
|
67
|
+
scrollToLatest,
|
|
68
|
+
updatePos, // expose for manual trigger
|
|
69
|
+
};
|
|
70
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { render } from "ink";
|
|
2
|
+
import { createElement } from "react";
|
|
3
|
+
import App from "./app/index.js";
|
|
4
|
+
import { mouseInput } from "./services/mouse-input.js";
|
|
5
|
+
|
|
6
|
+
export interface BootOptions {
|
|
7
|
+
model?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function bootTui(options?: BootOptions): Promise<void> {
|
|
11
|
+
mouseInput.onExit(() => process.exit(0));
|
|
12
|
+
mouseInput.enable();
|
|
13
|
+
const stdin = Object.assign(mouseInput.stdin, {
|
|
14
|
+
isTTY: true as const,
|
|
15
|
+
setRawMode: (mode: boolean) => process.stdin.setRawMode?.(mode),
|
|
16
|
+
ref: () => process.stdin.ref?.(),
|
|
17
|
+
unref: () => process.stdin.unref?.(),
|
|
18
|
+
}) as unknown as NodeJS.ReadStream;
|
|
19
|
+
|
|
20
|
+
const { waitUntilExit } = render(
|
|
21
|
+
createElement(App, { model: options?.model }),
|
|
22
|
+
{
|
|
23
|
+
stdin,
|
|
24
|
+
interactive: true,
|
|
25
|
+
alternateScreen: true,
|
|
26
|
+
maxFps: 140,
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
await waitUntilExit();
|
|
30
|
+
mouseInput.disable();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Direct entry point when run via tsx src/index.ts (legacy)
|
|
34
|
+
const isDirectRun = process.argv[1]
|
|
35
|
+
?.replace(/\\/g, "/")
|
|
36
|
+
.endsWith("src/index.ts");
|
|
37
|
+
if (isDirectRun) {
|
|
38
|
+
bootTui();
|
|
39
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { truncateOutputSimple } from "../utils.js";
|
|
3
|
+
import { theme } from "../../theme.js";
|
|
4
|
+
import type { ToolResultBlock } from "@mirai/core/types";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
block: ToolResultBlock;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const BashResult = ({ block }: Props) => {
|
|
11
|
+
const data = block.metadata.structuredData ?? {};
|
|
12
|
+
|
|
13
|
+
const command = (data.command as string | undefined) ?? "";
|
|
14
|
+
const stdout = (data.stdout as string | undefined) ?? "";
|
|
15
|
+
const stderr = (data.stderr as string | undefined) ?? "";
|
|
16
|
+
const exitCode = (data.exitCode as number | undefined) ?? 0;
|
|
17
|
+
const backgroundTaskId = (data.backgroundTaskId as string | undefined) ?? "";
|
|
18
|
+
const returnCodeInterpretation =
|
|
19
|
+
(data.returnCodeInterpretation as string | undefined) ?? "";
|
|
20
|
+
|
|
21
|
+
const stdoutPreview = truncateOutputSimple(stdout, 60, 4000);
|
|
22
|
+
const stderrPreview = truncateOutputSimple(stderr, 60, 4000);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Box flexDirection="column">
|
|
26
|
+
{command && (
|
|
27
|
+
<Box marginLeft={2} marginTop={1} marginBottom={1}>
|
|
28
|
+
<Text color={theme.text.dim} backgroundColor={theme.bg.code}>
|
|
29
|
+
{" "}
|
|
30
|
+
$ {command}{" "}
|
|
31
|
+
</Text>
|
|
32
|
+
</Box>
|
|
33
|
+
)}
|
|
34
|
+
{backgroundTaskId && (
|
|
35
|
+
<Box marginLeft={2}>
|
|
36
|
+
<Text color={theme.warning.primary}>
|
|
37
|
+
backgrounded ({backgroundTaskId})
|
|
38
|
+
</Text>
|
|
39
|
+
</Box>
|
|
40
|
+
)}
|
|
41
|
+
{returnCodeInterpretation && (
|
|
42
|
+
<Box marginLeft={2} marginTop={1}>
|
|
43
|
+
<Text color={theme.warning.primary}>{returnCodeInterpretation}</Text>
|
|
44
|
+
</Box>
|
|
45
|
+
)}
|
|
46
|
+
{stdoutPreview.content && (
|
|
47
|
+
<Box marginLeft={2}>
|
|
48
|
+
<Text color={theme.text.dim}>{stdoutPreview.content}</Text>
|
|
49
|
+
</Box>
|
|
50
|
+
)}
|
|
51
|
+
{stderrPreview.content && (
|
|
52
|
+
<Box marginLeft={2} marginTop={1}>
|
|
53
|
+
<Text color={theme.error.primary}>{stderrPreview.content}</Text>
|
|
54
|
+
</Box>
|
|
55
|
+
)}
|
|
56
|
+
{(stdoutPreview.truncated || stderrPreview.truncated) && (
|
|
57
|
+
<Box marginLeft={2} marginTop={1}>
|
|
58
|
+
<Text dimColor>
|
|
59
|
+
… output truncated for display; full result preserved in session.
|
|
60
|
+
</Text>
|
|
61
|
+
</Box>
|
|
62
|
+
)}
|
|
63
|
+
</Box>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
import type { ToolResultBlock } from "@mirai/core/types";
|
|
4
|
+
import type { EditFileContract } from "@mirai/adapter";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
block: ToolResultBlock;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const EditFileResult = ({ block }: Props) => {
|
|
11
|
+
const data = block.metadata.structuredData as unknown as EditFileContract;
|
|
12
|
+
|
|
13
|
+
const path = data.path ?? "unknown";
|
|
14
|
+
const replaceAll = data.replaceAll ?? false;
|
|
15
|
+
const diff = data.diff ?? "";
|
|
16
|
+
const oldText = data.oldText ?? "";
|
|
17
|
+
const newText = data.newText ?? "";
|
|
18
|
+
|
|
19
|
+
const suffix = replaceAll ? " (replace all)" : "";
|
|
20
|
+
const hasDiff = !!diff;
|
|
21
|
+
const hasOldNew = !!oldText || !!newText;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Box flexDirection="column">
|
|
25
|
+
<Text dimColor color={theme.text.dim}>
|
|
26
|
+
📝 Edited {path}
|
|
27
|
+
{suffix}
|
|
28
|
+
</Text>
|
|
29
|
+
{(hasDiff || hasOldNew) && (
|
|
30
|
+
<Box marginLeft={2} marginTop={1}>
|
|
31
|
+
{hasDiff && (
|
|
32
|
+
<Box>
|
|
33
|
+
<Text color={theme.text.dim}>Diff:</Text>
|
|
34
|
+
{diff.split("\n").map((line, i) => (
|
|
35
|
+
<Box key={i} marginLeft={2}>
|
|
36
|
+
<Text
|
|
37
|
+
color={
|
|
38
|
+
line.startsWith("+")
|
|
39
|
+
? theme.success.primary
|
|
40
|
+
: line.startsWith("-")
|
|
41
|
+
? theme.error.primary
|
|
42
|
+
: theme.text.dim
|
|
43
|
+
}
|
|
44
|
+
>
|
|
45
|
+
{line}
|
|
46
|
+
</Text>
|
|
47
|
+
</Box>
|
|
48
|
+
))}
|
|
49
|
+
</Box>
|
|
50
|
+
)}
|
|
51
|
+
{hasOldNew && !hasDiff && (
|
|
52
|
+
<Box>
|
|
53
|
+
{oldText && (
|
|
54
|
+
<Box marginLeft={2}>
|
|
55
|
+
<Text color={theme.error.primary}>- {oldText}</Text>
|
|
56
|
+
</Box>
|
|
57
|
+
)}
|
|
58
|
+
{newText && (
|
|
59
|
+
<Box marginLeft={2}>
|
|
60
|
+
<Text color={theme.success.primary}>+ {newText}</Text>
|
|
61
|
+
</Box>
|
|
62
|
+
)}
|
|
63
|
+
</Box>
|
|
64
|
+
)}
|
|
65
|
+
</Box>
|
|
66
|
+
)}
|
|
67
|
+
</Box>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { DISPLAY_LIMITS, DISPLAY_TRUNCATION_NOTICE } from "../constants.js";
|
|
3
|
+
import { truncateOutputSimple } from "../utils.js";
|
|
4
|
+
import { theme } from "../../theme.js";
|
|
5
|
+
import type { ToolResultBlock } from "@mirai/core/types";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
block: ToolResultBlock;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const GenericToolResult = ({ block }: Props) => {
|
|
12
|
+
const preview = truncateOutputSimple(
|
|
13
|
+
block.output,
|
|
14
|
+
DISPLAY_LIMITS.default.maxLines,
|
|
15
|
+
DISPLAY_LIMITS.default.maxChars,
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Box flexDirection="column">
|
|
20
|
+
{preview.truncated && (
|
|
21
|
+
<Box>
|
|
22
|
+
<Text dimColor>{DISPLAY_TRUNCATION_NOTICE}</Text>
|
|
23
|
+
</Box>
|
|
24
|
+
)}
|
|
25
|
+
{block.metadata.truncated && (
|
|
26
|
+
<Box>
|
|
27
|
+
<Text dimColor>
|
|
28
|
+
Tool output was truncated. Use read_file for full content.
|
|
29
|
+
</Text>
|
|
30
|
+
</Box>
|
|
31
|
+
)}
|
|
32
|
+
{preview.content && (
|
|
33
|
+
<Box>
|
|
34
|
+
<Text color={theme.text.dim}>{preview.content}</Text>
|
|
35
|
+
</Box>
|
|
36
|
+
)}
|
|
37
|
+
</Box>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
import type { ToolResultBlock } from "@mirai/core/types";
|
|
4
|
+
import type { GlobSearchContract } from "@mirai/adapter";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
block: ToolResultBlock;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const GlobSearchResult = ({ block }: Props) => {
|
|
11
|
+
const data = block.metadata.structuredData as unknown as GlobSearchContract;
|
|
12
|
+
|
|
13
|
+
const files = data.files ?? [];
|
|
14
|
+
const numFiles = data.numFiles ?? files.length;
|
|
15
|
+
const displayFiles = files.slice(0, 8);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Box flexDirection="column">
|
|
19
|
+
<Text dimColor color={theme.text.dim}>
|
|
20
|
+
Matched {numFiles} files
|
|
21
|
+
</Text>
|
|
22
|
+
{displayFiles.length > 0 && (
|
|
23
|
+
<Box marginLeft={2} marginTop={1}>
|
|
24
|
+
{displayFiles.map((file, i) => (
|
|
25
|
+
<Box key={i} marginLeft={2}>
|
|
26
|
+
<Text color={theme.text.dim}>{file}</Text>
|
|
27
|
+
</Box>
|
|
28
|
+
))}
|
|
29
|
+
{files.length > 8 && (
|
|
30
|
+
<Box marginLeft={2} marginTop={1}>
|
|
31
|
+
<Text color={theme.text.dim}>
|
|
32
|
+
... and {files.length - 8} more
|
|
33
|
+
</Text>
|
|
34
|
+
</Box>
|
|
35
|
+
)}
|
|
36
|
+
</Box>
|
|
37
|
+
)}
|
|
38
|
+
</Box>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
import type { ToolResultBlock } from "@mirai/core/types";
|
|
4
|
+
import type { GrepSearchContract } from "@mirai/adapter";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
block: ToolResultBlock;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const GrepSearchResult = ({ block }: Props) => {
|
|
11
|
+
const data = block.metadata.structuredData as unknown as GrepSearchContract;
|
|
12
|
+
|
|
13
|
+
const matchCount = data.matchCount ?? 0;
|
|
14
|
+
const fileCount = data.fileCount ?? 0;
|
|
15
|
+
const matches = data.matches ?? [];
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Box flexDirection="column">
|
|
19
|
+
<Text dimColor color={theme.text.dim}>
|
|
20
|
+
{matchCount} matches across {fileCount} files
|
|
21
|
+
</Text>
|
|
22
|
+
{matches.length > 0 && (
|
|
23
|
+
<Box marginLeft={2} marginTop={1}>
|
|
24
|
+
{matches.slice(0, 20).map((match, i) => (
|
|
25
|
+
<Box key={i} marginLeft={2}>
|
|
26
|
+
<Text color={theme.text.dim}>
|
|
27
|
+
{match.path}:{match.line}: {match.text}
|
|
28
|
+
</Text>
|
|
29
|
+
</Box>
|
|
30
|
+
))}
|
|
31
|
+
{matches.length > 20 && (
|
|
32
|
+
<Box marginLeft={2} marginTop={1}>
|
|
33
|
+
<Text color={theme.text.dim}>
|
|
34
|
+
… and {matches.length - 20} more matches
|
|
35
|
+
</Text>
|
|
36
|
+
</Box>
|
|
37
|
+
)}
|
|
38
|
+
</Box>
|
|
39
|
+
)}
|
|
40
|
+
{block.metadata.truncated && (
|
|
41
|
+
<Box marginLeft={2} marginTop={1}>
|
|
42
|
+
<Text dimColor>
|
|
43
|
+
… output truncated for display; full result preserved in session.
|
|
44
|
+
</Text>
|
|
45
|
+
</Box>
|
|
46
|
+
)}
|
|
47
|
+
</Box>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { DISPLAY_LIMITS } from "../constants.js";
|
|
3
|
+
import { truncateOutputSimple } from "../utils.js";
|
|
4
|
+
import { theme } from "../../theme.js";
|
|
5
|
+
import type { ToolResultBlock } from "@mirai/core/types";
|
|
6
|
+
import type { ReadFileContract } from "@mirai/adapter";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
block: ToolResultBlock;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const ReadFileResult = ({ block }: Props) => {
|
|
13
|
+
const data = block.metadata.structuredData as unknown as ReadFileContract;
|
|
14
|
+
|
|
15
|
+
const path = data.path ?? "unknown";
|
|
16
|
+
const startLine = data.startLine ?? 1;
|
|
17
|
+
const endLine = data.endLine ?? 0;
|
|
18
|
+
const content = data.content ?? block.output;
|
|
19
|
+
|
|
20
|
+
const range = endLine > 0 ? ` (lines ${startLine}-${endLine})` : "";
|
|
21
|
+
const preview = truncateOutputSimple(
|
|
22
|
+
content,
|
|
23
|
+
DISPLAY_LIMITS.readFile.maxLines,
|
|
24
|
+
DISPLAY_LIMITS.readFile.maxChars,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Box flexDirection="column">
|
|
29
|
+
<Text dimColor color={theme.text.dim}>
|
|
30
|
+
Read {path}
|
|
31
|
+
{range}
|
|
32
|
+
</Text>
|
|
33
|
+
{preview.content && (
|
|
34
|
+
<Box marginLeft={2} marginTop={1}>
|
|
35
|
+
<Text color={theme.text.dim}>{preview.content}</Text>
|
|
36
|
+
</Box>
|
|
37
|
+
)}
|
|
38
|
+
{preview.truncated && (
|
|
39
|
+
<Box marginLeft={2} marginTop={1}>
|
|
40
|
+
<Text dimColor>
|
|
41
|
+
… output truncated for display; full result preserved in session.
|
|
42
|
+
</Text>
|
|
43
|
+
</Box>
|
|
44
|
+
)}
|
|
45
|
+
{block.metadata.truncated && (
|
|
46
|
+
<Box marginLeft={2} marginTop={1}>
|
|
47
|
+
<Text dimColor>
|
|
48
|
+
Tool output was truncated. Use read_file for full content.
|
|
49
|
+
</Text>
|
|
50
|
+
</Box>
|
|
51
|
+
)}
|
|
52
|
+
</Box>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
import type { ToolResultBlock } from "@mirai/core/types";
|
|
4
|
+
import type { WriteFileContract } from "@mirai/adapter";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
block: ToolResultBlock;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const WriteFileResult = ({ block }: Props) => {
|
|
11
|
+
const data = block.metadata.structuredData as unknown as WriteFileContract;
|
|
12
|
+
|
|
13
|
+
const path = data.path ?? "unknown";
|
|
14
|
+
const bytesWritten = data.bytesWritten ?? 0;
|
|
15
|
+
const action = block.status === "success" ? "Wrote" : "Failed";
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Box flexDirection="column">
|
|
19
|
+
<Text dimColor color={theme.text.dim}>
|
|
20
|
+
✏️ {action} {path} ({bytesWritten} bytes)
|
|
21
|
+
</Text>
|
|
22
|
+
</Box>
|
|
23
|
+
);
|
|
24
|
+
};
|