mu-coding 0.15.0 → 0.16.1
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/README.md +9 -123
- package/bin/coding-agent.ts +95 -0
- package/package.json +10 -21
- package/src/config.ts +122 -0
- package/src/harness.test.ts +159 -0
- package/src/main.ts +53 -3
- package/src/plugins.ts +49 -0
- package/src/systemPrompt.ts +22 -0
- package/src/ui/ChatApp.ts +959 -0
- package/src/ui/commands.ts +35 -0
- package/src/ui/editor.ts +166 -0
- package/src/ui/markdown.ts +363 -0
- package/src/ui/picker.ts +126 -0
- package/src/ui/status.ts +61 -0
- package/src/ui/theme.ts +241 -0
- package/src/ui/transcript.test.ts +121 -0
- package/src/ui/transcript.ts +399 -0
- package/tsconfig.json +8 -0
- package/bin/mu.js +0 -2
- package/prompts/SYSTEM.md +0 -16
- package/src/app/shutdown.ts +0 -94
- package/src/app/startApp.ts +0 -49
- package/src/cli/args.ts +0 -133
- package/src/cli/install.ts +0 -107
- package/src/cli/subcommands.ts +0 -29
- package/src/cli/update.ts +0 -205
- package/src/config/index.test.ts +0 -77
- package/src/config/index.ts +0 -199
- package/src/plugin.ts +0 -124
- package/src/runtime/codingTools/bash.ts +0 -114
- package/src/runtime/codingTools/edit-file.ts +0 -60
- package/src/runtime/codingTools/index.ts +0 -39
- package/src/runtime/codingTools/read-file.ts +0 -83
- package/src/runtime/codingTools/utils.ts +0 -21
- package/src/runtime/codingTools/write-file.ts +0 -42
- package/src/runtime/createRegistry.test.ts +0 -147
- package/src/runtime/createRegistry.ts +0 -195
- package/src/runtime/fileMentionProvider.ts +0 -117
- package/src/runtime/messageBus.test.ts +0 -62
- package/src/runtime/messageBus.ts +0 -78
- package/src/runtime/pluginLoader.ts +0 -153
- package/src/runtime/startupUpdateCheck.ts +0 -163
- package/src/runtime/updateCheck.ts +0 -136
- package/src/sessions/index.test.ts +0 -66
- package/src/sessions/index.ts +0 -183
- package/src/sessions/peek.test.ts +0 -88
- package/src/sessions/project.ts +0 -51
- package/src/tui/channel/tuiChannel.test.ts +0 -107
- package/src/tui/channel/tuiChannel.ts +0 -62
- package/src/tui/chat/ChatContext.ts +0 -10
- package/src/tui/chat/MessageRendererContext.ts +0 -44
- package/src/tui/chat/ToolDisplayContext.ts +0 -33
- package/src/tui/chat/useAbort.ts +0 -85
- package/src/tui/chat/useAttachment.ts +0 -74
- package/src/tui/chat/useChat.ts +0 -113
- package/src/tui/chat/useChatPanel.ts +0 -120
- package/src/tui/chat/useChatSession.ts +0 -384
- package/src/tui/chat/useModels.ts +0 -83
- package/src/tui/chat/usePluginStatus.ts +0 -44
- package/src/tui/chat/useSessionPersistence.ts +0 -84
- package/src/tui/chat/useStatusSegments.ts +0 -85
- package/src/tui/chat/useSubagentBrowser.ts +0 -133
- package/src/tui/components/chat/ChatPanel.tsx +0 -54
- package/src/tui/components/chat/ChatPanelBody.tsx +0 -86
- package/src/tui/components/chat/Pickers.tsx +0 -44
- package/src/tui/components/chat/SubagentBrowserPanel.tsx +0 -145
- package/src/tui/components/messageView.tsx +0 -72
- package/src/tui/components/messages/EditOutput.tsx +0 -112
- package/src/tui/components/messages/ReadOutput.tsx +0 -48
- package/src/tui/components/messages/ToolHeader.tsx +0 -30
- package/src/tui/components/messages/WebFetchOutput.tsx +0 -30
- package/src/tui/components/messages/WriteOutput.tsx +0 -64
- package/src/tui/components/messages/assistantMessage.tsx +0 -72
- package/src/tui/components/messages/markdown.tsx +0 -407
- package/src/tui/components/messages/messageItem.tsx +0 -43
- package/src/tui/components/messages/reasoningBlock.tsx +0 -18
- package/src/tui/components/messages/streamingOutput.tsx +0 -18
- package/src/tui/components/messages/toolCallBlock.tsx +0 -125
- package/src/tui/components/messages/userMessage.tsx +0 -44
- package/src/tui/components/primitives/dropdown.tsx +0 -125
- package/src/tui/components/primitives/modal.tsx +0 -47
- package/src/tui/components/primitives/pickerModal.tsx +0 -47
- package/src/tui/components/primitives/scrollbar.tsx +0 -27
- package/src/tui/components/primitives/toast.tsx +0 -100
- package/src/tui/components/statusBar.tsx +0 -41
- package/src/tui/components/ui/dialogLayer.tsx +0 -175
- package/src/tui/context/ThemeContext.tsx +0 -18
- package/src/tui/hooks/useChordKeyboard.ts +0 -87
- package/src/tui/hooks/useInputInfoSegments.ts +0 -22
- package/src/tui/hooks/useScroll.ts +0 -64
- package/src/tui/hooks/useTerminal.ts +0 -40
- package/src/tui/hooks/useUI.ts +0 -15
- package/src/tui/input/InputBox.tsx +0 -6
- package/src/tui/input/InputBoxView.tsx +0 -293
- package/src/tui/input/commands.test.ts +0 -71
- package/src/tui/input/commands.ts +0 -55
- package/src/tui/input/cursor.test.ts +0 -136
- package/src/tui/input/cursor.ts +0 -214
- package/src/tui/input/dumpContext.ts +0 -107
- package/src/tui/input/sanitize.ts +0 -33
- package/src/tui/input/useCommandExecutor.ts +0 -32
- package/src/tui/input/useInputBox.ts +0 -265
- package/src/tui/input/useInputHandler.ts +0 -455
- package/src/tui/input/useMentionPicker.ts +0 -133
- package/src/tui/input/usePluginShortcuts.ts +0 -29
- package/src/tui/plugins/InkApprovalChannel.test.ts +0 -51
- package/src/tui/plugins/InkApprovalChannel.ts +0 -30
- package/src/tui/plugins/InkUIService.ts +0 -188
- package/src/tui/renderApp.tsx +0 -66
- package/src/tui/theme/index.ts +0 -1
- package/src/tui/theme/merge.test.ts +0 -49
- package/src/tui/theme/merge.ts +0 -43
- package/src/tui/theme/presets.ts +0 -90
- package/src/tui/theme/types.ts +0 -138
- package/src/tui/update/runUpdateInTui.ts +0 -127
- package/src/utils/clipboard.ts +0 -97
- package/src/utils/diff.test.ts +0 -56
- package/src/utils/diff.ts +0 -81
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import type { DOMElement } from 'ink';
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
3
|
-
import type { ChatMessage } from 'mu-core';
|
|
4
|
-
import { type RefObject, useMemo } from 'react';
|
|
5
|
-
import type { StreamState } from '../chat/useChatSession';
|
|
6
|
-
import { useTheme } from '../context/ThemeContext';
|
|
7
|
-
import { MessageItem } from './messages/messageItem';
|
|
8
|
-
import { StreamingOutput } from './messages/streamingOutput';
|
|
9
|
-
import { Scrollbar } from './primitives/scrollbar';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Walk `messages` once and group every assistant-with-tool-calls index to
|
|
13
|
-
* its trailing `tool` messages. Avoids the previous O(n²) scan where each
|
|
14
|
-
* `MessageItem` re-walked the array forward to find its tool replies.
|
|
15
|
-
*/
|
|
16
|
-
function indexToolMessages(messages: ChatMessage[]): Map<number, ChatMessage[]> {
|
|
17
|
-
const map = new Map<number, ChatMessage[]>();
|
|
18
|
-
let activeAssistant = -1;
|
|
19
|
-
for (let i = 0; i < messages.length; i++) {
|
|
20
|
-
const msg = messages[i];
|
|
21
|
-
if (msg.role === 'assistant' && msg.toolCalls?.length) {
|
|
22
|
-
activeAssistant = i;
|
|
23
|
-
map.set(i, []);
|
|
24
|
-
} else if (msg.role === 'tool' && activeAssistant !== -1) {
|
|
25
|
-
map.get(activeAssistant)?.push(msg);
|
|
26
|
-
} else {
|
|
27
|
-
activeAssistant = -1;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return map;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function MessageView({
|
|
34
|
-
viewRef,
|
|
35
|
-
contentRef,
|
|
36
|
-
messages,
|
|
37
|
-
streaming,
|
|
38
|
-
stream,
|
|
39
|
-
error,
|
|
40
|
-
scrollOffset,
|
|
41
|
-
viewHeight,
|
|
42
|
-
contentHeight,
|
|
43
|
-
}: {
|
|
44
|
-
viewRef: RefObject<DOMElement | null>;
|
|
45
|
-
contentRef: RefObject<DOMElement | null>;
|
|
46
|
-
messages: ChatMessage[];
|
|
47
|
-
streaming: boolean;
|
|
48
|
-
stream: StreamState;
|
|
49
|
-
error: string | null;
|
|
50
|
-
scrollOffset: number;
|
|
51
|
-
viewHeight: number;
|
|
52
|
-
contentHeight: number;
|
|
53
|
-
}) {
|
|
54
|
-
const theme = useTheme();
|
|
55
|
-
const toolMessageIndex = useMemo(() => indexToolMessages(messages), [messages]);
|
|
56
|
-
|
|
57
|
-
return (
|
|
58
|
-
<Box flexGrow={1} overflow="hidden">
|
|
59
|
-
<Box ref={viewRef} flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
|
|
60
|
-
<Box ref={contentRef} flexDirection="column" flexShrink={0} marginTop={-scrollOffset}>
|
|
61
|
-
{messages.map((msg, i) => (
|
|
62
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: messages have no stable id
|
|
63
|
-
<MessageItem key={i} msg={msg} toolMessages={toolMessageIndex.get(i)} />
|
|
64
|
-
))}
|
|
65
|
-
{streaming && <StreamingOutput currentText={stream.text} currentReasoning={stream.reasoning} />}
|
|
66
|
-
{error && <Text color={theme.common.error}>Error: {error}</Text>}
|
|
67
|
-
</Box>
|
|
68
|
-
</Box>
|
|
69
|
-
<Scrollbar viewHeight={viewHeight} contentHeight={contentHeight} scrollOffset={scrollOffset} />
|
|
70
|
-
</Box>
|
|
71
|
-
);
|
|
72
|
-
}
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { Box, Text } from 'ink';
|
|
2
|
-
import type { ToolDisplayHint } from 'mu-core';
|
|
3
|
-
import { computeDiff, renderDiff } from '../../../utils/diff';
|
|
4
|
-
import { useTheme } from '../../context/ThemeContext';
|
|
5
|
-
import { ToolHeader } from './ToolHeader';
|
|
6
|
-
|
|
7
|
-
interface EditOutputProps {
|
|
8
|
-
args: string;
|
|
9
|
-
content: string;
|
|
10
|
-
error: boolean;
|
|
11
|
-
/**
|
|
12
|
-
* Display hint from the tool's plugin. Used to resolve which JSON arg field
|
|
13
|
-
* holds the path / from-string / to-string, so a plugin can register a
|
|
14
|
-
* diff-kind tool with arbitrary field names.
|
|
15
|
-
*/
|
|
16
|
-
hint?: ToolDisplayHint;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
interface ParsedEditArgs {
|
|
20
|
-
path: string;
|
|
21
|
-
before: string;
|
|
22
|
-
after: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const MAX_DIFF_LINES = 30;
|
|
26
|
-
|
|
27
|
-
function parseEditArgs(args: string, hint: ToolDisplayHint | undefined): ParsedEditArgs {
|
|
28
|
-
const fields = hint?.fields ?? {};
|
|
29
|
-
const pathField = fields.path ?? 'path';
|
|
30
|
-
const fromField = fields.from ?? 'old_string';
|
|
31
|
-
const toField = fields.to ?? 'new_string';
|
|
32
|
-
try {
|
|
33
|
-
const parsed = JSON.parse(args);
|
|
34
|
-
return {
|
|
35
|
-
path: parsed[pathField] ?? '(unknown)',
|
|
36
|
-
before: parsed[fromField] ?? '',
|
|
37
|
-
after: parsed[toField] ?? '',
|
|
38
|
-
};
|
|
39
|
-
} catch {
|
|
40
|
-
return { path: '(unknown)', before: '', after: '' };
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function EditOutput({ args, content, error, hint }: EditOutputProps) {
|
|
45
|
-
const theme = useTheme();
|
|
46
|
-
const { path, before, after } = parseEditArgs(args, hint);
|
|
47
|
-
const verb = hint?.verb ?? 'edit_file';
|
|
48
|
-
|
|
49
|
-
if (error) {
|
|
50
|
-
return (
|
|
51
|
-
<Box flexDirection="column" flexShrink={0} marginBottom={0}>
|
|
52
|
-
<ToolHeader name={verb} subtitle={path} error={true} />
|
|
53
|
-
<Text dimColor={true} wrap="wrap">
|
|
54
|
-
{content}
|
|
55
|
-
</Text>
|
|
56
|
-
</Box>
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const diff = computeDiff(before, after);
|
|
61
|
-
|
|
62
|
-
if (diff.lines.length === 0 && diff.totalOldLines > 0 && diff.totalNewLines > 0) {
|
|
63
|
-
return (
|
|
64
|
-
<Box flexDirection="column" flexShrink={0} marginBottom={0}>
|
|
65
|
-
<Text color={theme.diff.warning} bold={true}>
|
|
66
|
-
! {verb}
|
|
67
|
-
</Text>
|
|
68
|
-
<Text dimColor={true}> {path}</Text>
|
|
69
|
-
<Text dimColor={true}>
|
|
70
|
-
Diff too large to display ({diff.totalOldLines} → {diff.totalNewLines} lines)
|
|
71
|
-
</Text>
|
|
72
|
-
</Box>
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (diff.lines.length === 0) {
|
|
77
|
-
return (
|
|
78
|
-
<Box flexDirection="column" flexShrink={0} marginBottom={0}>
|
|
79
|
-
<ToolHeader name={verb} subtitle={path} />
|
|
80
|
-
<Text dimColor={true}>No changes (content identical)</Text>
|
|
81
|
-
</Box>
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const { lines, truncated } = renderDiff(diff, MAX_DIFF_LINES);
|
|
86
|
-
|
|
87
|
-
return (
|
|
88
|
-
<Box flexDirection="column" flexShrink={0} marginBottom={0}>
|
|
89
|
-
<ToolHeader name={verb} subtitle={path} />
|
|
90
|
-
<Box
|
|
91
|
-
flexDirection="column"
|
|
92
|
-
flexShrink={0}
|
|
93
|
-
backgroundColor={theme.tool.previewBackground}
|
|
94
|
-
paddingX={1}
|
|
95
|
-
paddingY={0}
|
|
96
|
-
>
|
|
97
|
-
{lines.map((line, i) => {
|
|
98
|
-
let color: string | undefined;
|
|
99
|
-
if (line.startsWith('-')) color = theme.diff.removed;
|
|
100
|
-
else if (line.startsWith('+')) color = theme.diff.added;
|
|
101
|
-
return (
|
|
102
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: diff lines may repeat (blank lines, braces); index disambiguates
|
|
103
|
-
<Text key={`${i}-${line}`} color={color} dimColor={color === undefined} wrap="wrap">
|
|
104
|
-
{line}
|
|
105
|
-
</Text>
|
|
106
|
-
);
|
|
107
|
-
})}
|
|
108
|
-
{truncated && <Text dimColor={true}>… (truncated, {MAX_DIFF_LINES} line limit)</Text>}
|
|
109
|
-
</Box>
|
|
110
|
-
</Box>
|
|
111
|
-
);
|
|
112
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { Box, Text } from 'ink';
|
|
2
|
-
import { ToolHeader } from './ToolHeader';
|
|
3
|
-
|
|
4
|
-
interface ReadOutputProps {
|
|
5
|
-
args: string;
|
|
6
|
-
error: boolean;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
interface ReadArgs {
|
|
10
|
-
paths: string[];
|
|
11
|
-
startLine?: number;
|
|
12
|
-
endLine?: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function parseReadArgs(args: string): ReadArgs {
|
|
16
|
-
try {
|
|
17
|
-
const parsed = JSON.parse(args);
|
|
18
|
-
const p = parsed.path;
|
|
19
|
-
return {
|
|
20
|
-
paths: Array.isArray(p) ? p : [p ?? '(unknown)'],
|
|
21
|
-
startLine: typeof parsed.start === 'number' ? parsed.start : undefined,
|
|
22
|
-
endLine: typeof parsed.end === 'number' ? parsed.end : undefined,
|
|
23
|
-
};
|
|
24
|
-
} catch {
|
|
25
|
-
return { paths: ['(unknown)'] };
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function ReadOutput({ args, error }: ReadOutputProps) {
|
|
30
|
-
const { paths, startLine, endLine } = parseReadArgs(args);
|
|
31
|
-
const rangeLabel = startLine != null && endLine != null ? ` (lines ${startLine}-${endLine})` : '';
|
|
32
|
-
const subtitle = paths.length === 1 ? `${paths[0]}${rangeLabel}` : `${paths.length} files${rangeLabel}`;
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<Box flexDirection="column" flexShrink={0} marginBottom={0}>
|
|
36
|
-
<ToolHeader name="read_file" subtitle={subtitle} error={error} />
|
|
37
|
-
{paths.length > 1 && (
|
|
38
|
-
<Box flexDirection="column" flexShrink={0}>
|
|
39
|
-
{paths.map((p) => (
|
|
40
|
-
<Text key={p} dimColor={true} wrap="wrap">
|
|
41
|
-
{` • ${p}`}
|
|
42
|
-
</Text>
|
|
43
|
-
))}
|
|
44
|
-
</Box>
|
|
45
|
-
)}
|
|
46
|
-
</Box>
|
|
47
|
-
);
|
|
48
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { Box, Text } from 'ink';
|
|
2
|
-
import { useTheme } from '../../context/ThemeContext';
|
|
3
|
-
|
|
4
|
-
interface ToolHeaderProps {
|
|
5
|
-
/** The tool name shown after the status icon. */
|
|
6
|
-
name: string;
|
|
7
|
-
/** Optional subtitle (typically the file path or command). */
|
|
8
|
-
subtitle?: string;
|
|
9
|
-
/** When true, render with the failure styling. */
|
|
10
|
-
error?: boolean;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Shared header used by every tool-output renderer (read/write/edit/bash).
|
|
15
|
-
* Centralizes the ✓/✗ glyphs, color choice, and subtitle formatting so each
|
|
16
|
-
* specific component doesn't have to re-implement the same layout.
|
|
17
|
-
*/
|
|
18
|
-
export function ToolHeader({ name, subtitle, error = false }: ToolHeaderProps) {
|
|
19
|
-
const theme = useTheme();
|
|
20
|
-
return (
|
|
21
|
-
<Box flexShrink={0}>
|
|
22
|
-
<Text wrap="truncate-end">
|
|
23
|
-
<Text color={error ? theme.tool.error : theme.tool.success} bold={true}>
|
|
24
|
-
{error ? '✗' : '✓'} {name}
|
|
25
|
-
</Text>
|
|
26
|
-
{subtitle && <Text dimColor={true}> {subtitle}</Text>}
|
|
27
|
-
</Text>
|
|
28
|
-
</Box>
|
|
29
|
-
);
|
|
30
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { Box } from 'ink';
|
|
2
|
-
import { ToolHeader } from './ToolHeader';
|
|
3
|
-
|
|
4
|
-
interface WebFetchOutputProps {
|
|
5
|
-
args: string;
|
|
6
|
-
error: boolean;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function parseUrl(args: string): string {
|
|
10
|
-
try {
|
|
11
|
-
const parsed = JSON.parse(args);
|
|
12
|
-
return typeof parsed.url === 'string' ? parsed.url : '(unknown)';
|
|
13
|
-
} catch {
|
|
14
|
-
return '(unknown)';
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Compact renderer for the `webfetch` tool — shows a one-line header with the
|
|
20
|
-
* fetched URL and elides the (often huge) response body so it doesn't fill
|
|
21
|
-
* the transcript. Mirrors `ReadOutput`'s minimal layout.
|
|
22
|
-
*/
|
|
23
|
-
export function WebFetchOutput({ args, error }: WebFetchOutputProps) {
|
|
24
|
-
const url = parseUrl(args);
|
|
25
|
-
return (
|
|
26
|
-
<Box flexDirection="column" flexShrink={0} marginBottom={0}>
|
|
27
|
-
<ToolHeader name="webfetch" subtitle={url} error={error} />
|
|
28
|
-
</Box>
|
|
29
|
-
);
|
|
30
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { Box, Text } from 'ink';
|
|
2
|
-
import { useTheme } from '../../context/ThemeContext';
|
|
3
|
-
import { ToolHeader } from './ToolHeader';
|
|
4
|
-
|
|
5
|
-
const PREVIEW_LINES = 30;
|
|
6
|
-
|
|
7
|
-
interface WriteOutputProps {
|
|
8
|
-
args: string;
|
|
9
|
-
content: string;
|
|
10
|
-
error: boolean;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function parsePath(args: string): string {
|
|
14
|
-
try {
|
|
15
|
-
const parsed = JSON.parse(args);
|
|
16
|
-
return parsed.path ?? '(unknown)';
|
|
17
|
-
} catch {
|
|
18
|
-
return '(unknown)';
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function WriteOutput({ args, content, error }: WriteOutputProps) {
|
|
23
|
-
const theme = useTheme();
|
|
24
|
-
const path = parsePath(args);
|
|
25
|
-
|
|
26
|
-
if (error) {
|
|
27
|
-
return (
|
|
28
|
-
<Box flexDirection="column" flexShrink={0} marginBottom={0}>
|
|
29
|
-
<ToolHeader name="write_file" error={true} />
|
|
30
|
-
<Text dimColor={true} wrap="wrap">
|
|
31
|
-
{content}
|
|
32
|
-
</Text>
|
|
33
|
-
</Box>
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const lines = content.split('\n');
|
|
38
|
-
const totalLines = lines.length;
|
|
39
|
-
const preview = lines.slice(0, PREVIEW_LINES).join('\n');
|
|
40
|
-
const hasMore = totalLines > PREVIEW_LINES;
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<Box flexDirection="column" flexShrink={0} marginBottom={0}>
|
|
44
|
-
<ToolHeader name="write_file" subtitle={path} />
|
|
45
|
-
<Box flexDirection="column" flexShrink={0}>
|
|
46
|
-
<Text dimColor={true}>
|
|
47
|
-
{totalLines} line{totalLines !== 1 ? 's' : ''}
|
|
48
|
-
</Text>
|
|
49
|
-
<Box
|
|
50
|
-
flexDirection="column"
|
|
51
|
-
flexShrink={0}
|
|
52
|
-
backgroundColor={theme.tool.previewBackground}
|
|
53
|
-
paddingX={1}
|
|
54
|
-
paddingY={0}
|
|
55
|
-
>
|
|
56
|
-
<Text color={theme.tool.previewText} wrap="wrap">
|
|
57
|
-
{hasMore ? preview : content}
|
|
58
|
-
</Text>
|
|
59
|
-
{hasMore && <Text dimColor={true}>… ({totalLines - PREVIEW_LINES} more lines)</Text>}
|
|
60
|
-
</Box>
|
|
61
|
-
</Box>
|
|
62
|
-
</Box>
|
|
63
|
-
);
|
|
64
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { Box, Text } from 'ink';
|
|
2
|
-
import type { ChatMessage } from 'mu-core';
|
|
3
|
-
import React from 'react';
|
|
4
|
-
import { MarkdownContent } from './markdown';
|
|
5
|
-
import { ReasoningBlock } from './reasoningBlock';
|
|
6
|
-
import { ToolCallBlock } from './toolCallBlock';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Tool names whose calls are already represented in the transcript by the
|
|
10
|
-
* `mu-agents.subagent` custom message renderer (`SubagentMessage`).
|
|
11
|
-
* Filtering them out here prevents a redundant `✓ subagent` block from
|
|
12
|
-
* rendering the same body the SubagentMessage already shows.
|
|
13
|
-
*
|
|
14
|
-
* Reload caveat: the `SubagentRunRegistry` is in-memory only, so after a
|
|
15
|
-
* session reload the SubagentMessage block has no live run and shows
|
|
16
|
-
* just the `↳ <name>` glyph without a body. The wrapped tool result
|
|
17
|
-
* still lives in the persisted transcript (and the LLM payload), so the
|
|
18
|
-
* parent agent's relay paragraph is intact; the user just can't see the
|
|
19
|
-
* raw subagent output inline after reopening the session. Accepted
|
|
20
|
-
* trade-off; revisit if/when run hydration is wired into reload.
|
|
21
|
-
*/
|
|
22
|
-
const SUBAGENT_TOOL_NAMES = new Set(['subagent', 'subagent_parallel']);
|
|
23
|
-
|
|
24
|
-
export const AssistantMessage: React.FC<{
|
|
25
|
-
msg: ChatMessage;
|
|
26
|
-
toolMessages?: ChatMessage[];
|
|
27
|
-
}> = React.memo(function AssistantMessage({ msg, toolMessages }) {
|
|
28
|
-
const badge = msg.display?.badge;
|
|
29
|
-
const prefix = msg.display?.prefix;
|
|
30
|
-
const color = msg.display?.color;
|
|
31
|
-
|
|
32
|
-
// Filter subagent tool calls out of the visible list, dropping their
|
|
33
|
-
// matching `toolMessages` entries in lock-step so positional indexing
|
|
34
|
-
// stays correct for the surviving calls.
|
|
35
|
-
const visibleEntries = (msg.toolCalls ?? []).flatMap((tc, i) =>
|
|
36
|
-
SUBAGENT_TOOL_NAMES.has(tc.function.name) ? [] : [{ tc, toolMsg: toolMessages?.[i] }],
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
// If every renderable surface on this assistant message is empty after
|
|
40
|
-
// filtering, suppress the entire block — otherwise we'd render a
|
|
41
|
-
// dangling badge bubble for assistant turns that were nothing but a
|
|
42
|
-
// subagent dispatch.
|
|
43
|
-
const hasAnything = visibleEntries.length > 0 || !!msg.content || !!msg.reasoning;
|
|
44
|
-
if (!hasAnything) return null;
|
|
45
|
-
|
|
46
|
-
const hasVisibleToolCalls = visibleEntries.length > 0;
|
|
47
|
-
return (
|
|
48
|
-
<Box flexDirection="column" flexShrink={0} marginBottom={hasVisibleToolCalls ? 0 : 1}>
|
|
49
|
-
{badge && (
|
|
50
|
-
<Box>
|
|
51
|
-
<Text color={color} bold={true}>
|
|
52
|
-
{badge.charAt(0).toUpperCase() + badge.slice(1)}
|
|
53
|
-
</Text>
|
|
54
|
-
</Box>
|
|
55
|
-
)}
|
|
56
|
-
{msg.reasoning && <ReasoningBlock reasoning={msg.reasoning} />}
|
|
57
|
-
{hasVisibleToolCalls ? (
|
|
58
|
-
<Box flexDirection="column">
|
|
59
|
-
{visibleEntries.map(({ tc, toolMsg }) => (
|
|
60
|
-
<ToolCallBlock key={tc.id} toolCall={tc} toolMsg={toolMsg} />
|
|
61
|
-
))}
|
|
62
|
-
</Box>
|
|
63
|
-
) : null}
|
|
64
|
-
{msg.content && (
|
|
65
|
-
<Box flexDirection="column">
|
|
66
|
-
{prefix && <Text color={color}>{prefix}</Text>}
|
|
67
|
-
<MarkdownContent content={msg.content} color={color} />
|
|
68
|
-
</Box>
|
|
69
|
-
)}
|
|
70
|
-
</Box>
|
|
71
|
-
);
|
|
72
|
-
});
|