mu-coding 0.13.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 -119
- package/src/tui/chat/useChatSession.ts +0 -382
- 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 -82
- 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 -64
- 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,407 +0,0 @@
|
|
|
1
|
-
import { Box, Text } from 'ink';
|
|
2
|
-
import type React from 'react';
|
|
3
|
-
import { useTheme } from '../../context/ThemeContext';
|
|
4
|
-
import type { Theme } from '../../theme/types';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Lightweight Markdown renderer for assistant messages. Intentionally
|
|
8
|
-
* pragmatic — covers headings, lists, tables, blockquotes, code blocks,
|
|
9
|
-
* and the common inline tokens (bold, italic, inline code, links).
|
|
10
|
-
*
|
|
11
|
-
* Implementation is two-pass:
|
|
12
|
-
* - `parseBlocks` splits the raw text into a list of typed blocks
|
|
13
|
-
* - each block has its own React renderer that delegates inline
|
|
14
|
-
* formatting to `renderInline` for paragraph / list / heading content.
|
|
15
|
-
*
|
|
16
|
-
* No external markdown dependency: the renderer must work in any host
|
|
17
|
-
* shipping `mu-coding` without forcing them to ship a parser.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
// ─── Block model ──────────────────────────────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
type Block =
|
|
23
|
-
| { type: 'heading'; level: 1 | 2 | 3; text: string }
|
|
24
|
-
| { type: 'paragraph'; text: string }
|
|
25
|
-
| { type: 'list'; ordered: boolean; items: string[] }
|
|
26
|
-
| { type: 'code'; lang: string; lines: string[] }
|
|
27
|
-
| { type: 'quote'; lines: string[] }
|
|
28
|
-
| { type: 'table'; header: string[]; rows: string[][] }
|
|
29
|
-
| { type: 'hr' };
|
|
30
|
-
|
|
31
|
-
const FENCE = /^```(.*)$/;
|
|
32
|
-
const HEADING = /^(#{1,3})\s+(.*)$/;
|
|
33
|
-
const UL_ITEM = /^(\s*)[-*+]\s+(.*)$/;
|
|
34
|
-
const OL_ITEM = /^(\s*)\d+\.\s+(.*)$/;
|
|
35
|
-
const QUOTE = /^>\s?(.*)$/;
|
|
36
|
-
const HR = /^\s*(---|\*\*\*|___)\s*$/;
|
|
37
|
-
const TABLE_SEP = /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)+\|?\s*$/;
|
|
38
|
-
const TABLE_ROW = /^\s*\|.*\|\s*$/;
|
|
39
|
-
|
|
40
|
-
function splitTableRow(line: string): string[] {
|
|
41
|
-
// Trim outer pipes then split. Handles `| a | b |` and `a | b`.
|
|
42
|
-
const trimmed = line.trim().replace(/^\|/, '').replace(/\|$/, '');
|
|
43
|
-
return trimmed.split('|').map((c) => c.trim());
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: block parsing is dispatch-heavy
|
|
47
|
-
// biome-ignore lint/complexity/noExcessiveLinesPerFunction: single parser dispatch loop — every block kind consumed inline keeps the cursor (`i`) local; splitting would scatter it.
|
|
48
|
-
function parseBlocks(input: string): Block[] {
|
|
49
|
-
const lines = input.split('\n');
|
|
50
|
-
const blocks: Block[] = [];
|
|
51
|
-
let i = 0;
|
|
52
|
-
|
|
53
|
-
while (i < lines.length) {
|
|
54
|
-
const line = lines[i];
|
|
55
|
-
|
|
56
|
-
// Code fence — capture until matching closing fence (or EOF).
|
|
57
|
-
const fence = line.match(FENCE);
|
|
58
|
-
if (fence) {
|
|
59
|
-
const lang = fence[1].trim();
|
|
60
|
-
const codeLines: string[] = [];
|
|
61
|
-
i++;
|
|
62
|
-
while (i < lines.length && !FENCE.test(lines[i])) {
|
|
63
|
-
codeLines.push(lines[i]);
|
|
64
|
-
i++;
|
|
65
|
-
}
|
|
66
|
-
if (i < lines.length) i++; // skip closing fence
|
|
67
|
-
blocks.push({ type: 'code', lang, lines: codeLines });
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (line.trim() === '') {
|
|
72
|
-
i++;
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (HR.test(line)) {
|
|
77
|
-
blocks.push({ type: 'hr' });
|
|
78
|
-
i++;
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const heading = line.match(HEADING);
|
|
83
|
-
if (heading) {
|
|
84
|
-
blocks.push({ type: 'heading', level: heading[1].length as 1 | 2 | 3, text: heading[2] });
|
|
85
|
-
i++;
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Quote — consume consecutive `> ` lines.
|
|
90
|
-
if (QUOTE.test(line)) {
|
|
91
|
-
const quoteLines: string[] = [];
|
|
92
|
-
while (i < lines.length && QUOTE.test(lines[i])) {
|
|
93
|
-
quoteLines.push((lines[i].match(QUOTE) as RegExpMatchArray)[1]);
|
|
94
|
-
i++;
|
|
95
|
-
}
|
|
96
|
-
blocks.push({ type: 'quote', lines: quoteLines });
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Table — header row, separator row, then body rows.
|
|
101
|
-
if (TABLE_ROW.test(line) && i + 1 < lines.length && TABLE_SEP.test(lines[i + 1])) {
|
|
102
|
-
const header = splitTableRow(line);
|
|
103
|
-
i += 2; // skip header + separator
|
|
104
|
-
const rows: string[][] = [];
|
|
105
|
-
while (i < lines.length && TABLE_ROW.test(lines[i])) {
|
|
106
|
-
rows.push(splitTableRow(lines[i]));
|
|
107
|
-
i++;
|
|
108
|
-
}
|
|
109
|
-
blocks.push({ type: 'table', header, rows });
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// List — ordered or unordered, contiguous items only.
|
|
114
|
-
const ulMatch = line.match(UL_ITEM);
|
|
115
|
-
const olMatch = line.match(OL_ITEM);
|
|
116
|
-
if (ulMatch || olMatch) {
|
|
117
|
-
const ordered = !!olMatch;
|
|
118
|
-
const re = ordered ? OL_ITEM : UL_ITEM;
|
|
119
|
-
const items: string[] = [];
|
|
120
|
-
while (i < lines.length) {
|
|
121
|
-
const m = lines[i].match(re);
|
|
122
|
-
if (!m) break;
|
|
123
|
-
items.push(m[2]);
|
|
124
|
-
i++;
|
|
125
|
-
}
|
|
126
|
-
blocks.push({ type: 'list', ordered, items });
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Paragraph — contiguous non-empty, non-block lines.
|
|
131
|
-
const paraLines: string[] = [];
|
|
132
|
-
while (i < lines.length) {
|
|
133
|
-
const cur = lines[i];
|
|
134
|
-
if (cur.trim() === '') break;
|
|
135
|
-
if (FENCE.test(cur) || HEADING.test(cur) || QUOTE.test(cur) || HR.test(cur)) break;
|
|
136
|
-
if (UL_ITEM.test(cur) || OL_ITEM.test(cur)) break;
|
|
137
|
-
if (TABLE_ROW.test(cur) && i + 1 < lines.length && TABLE_SEP.test(lines[i + 1])) break;
|
|
138
|
-
paraLines.push(cur);
|
|
139
|
-
i++;
|
|
140
|
-
}
|
|
141
|
-
if (paraLines.length > 0) {
|
|
142
|
-
blocks.push({ type: 'paragraph', text: paraLines.join(' ') });
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return blocks;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// ─── Inline rendering ─────────────────────────────────────────────────────────
|
|
150
|
-
|
|
151
|
-
interface InlineToken {
|
|
152
|
-
kind: 'text' | 'bold' | 'italic' | 'code' | 'link';
|
|
153
|
-
text: string;
|
|
154
|
-
href?: string;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const INLINE_PATTERNS: { kind: InlineToken['kind']; re: RegExp; capture: number; href?: number }[] = [
|
|
158
|
-
// Order matters: longer / more specific patterns first.
|
|
159
|
-
{ kind: 'code', re: /`([^`\n]+)`/, capture: 1 },
|
|
160
|
-
{ kind: 'link', re: /\[([^\]]+)\]\(([^)]+)\)/, capture: 1, href: 2 },
|
|
161
|
-
{ kind: 'bold', re: /\*\*([^*\n]+)\*\*/, capture: 1 },
|
|
162
|
-
{ kind: 'bold', re: /__([^_\n]+)__/, capture: 1 },
|
|
163
|
-
{ kind: 'italic', re: /\*([^*\n]+)\*/, capture: 1 },
|
|
164
|
-
{ kind: 'italic', re: /_([^_\n]+)_/, capture: 1 },
|
|
165
|
-
];
|
|
166
|
-
|
|
167
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: greedy inline tokenizer — earliest-match across patterns then advance; the branching is the algorithm.
|
|
168
|
-
function tokenizeInline(input: string): InlineToken[] {
|
|
169
|
-
const out: InlineToken[] = [];
|
|
170
|
-
let cursor = 0;
|
|
171
|
-
while (cursor < input.length) {
|
|
172
|
-
let bestIdx = -1;
|
|
173
|
-
let bestMatch: { kind: InlineToken['kind']; text: string; href?: string; raw: string } | null = null;
|
|
174
|
-
for (const p of INLINE_PATTERNS) {
|
|
175
|
-
const sub = input.slice(cursor);
|
|
176
|
-
const m = sub.match(p.re);
|
|
177
|
-
if (m && m.index !== undefined) {
|
|
178
|
-
if (bestIdx === -1 || m.index < bestIdx) {
|
|
179
|
-
bestIdx = m.index;
|
|
180
|
-
bestMatch = {
|
|
181
|
-
kind: p.kind,
|
|
182
|
-
text: m[p.capture],
|
|
183
|
-
href: p.href !== undefined ? m[p.href] : undefined,
|
|
184
|
-
raw: m[0],
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
if (!bestMatch || bestIdx === -1) {
|
|
190
|
-
out.push({ kind: 'text', text: input.slice(cursor) });
|
|
191
|
-
break;
|
|
192
|
-
}
|
|
193
|
-
if (bestIdx > 0) {
|
|
194
|
-
out.push({ kind: 'text', text: input.slice(cursor, cursor + bestIdx) });
|
|
195
|
-
}
|
|
196
|
-
out.push({ kind: bestMatch.kind, text: bestMatch.text, href: bestMatch.href });
|
|
197
|
-
cursor += bestIdx + bestMatch.raw.length;
|
|
198
|
-
}
|
|
199
|
-
return out;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function renderInline(text: string, theme: Theme, baseColor?: string): React.ReactNode[] {
|
|
203
|
-
const tokens = tokenizeInline(text);
|
|
204
|
-
return tokens.map((tok, i) => {
|
|
205
|
-
const key = `${i}-${tok.kind}`;
|
|
206
|
-
if (tok.kind === 'text') {
|
|
207
|
-
return (
|
|
208
|
-
<Text key={key} color={baseColor}>
|
|
209
|
-
{tok.text}
|
|
210
|
-
</Text>
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
if (tok.kind === 'bold') {
|
|
214
|
-
return (
|
|
215
|
-
<Text key={key} color={baseColor} bold={true}>
|
|
216
|
-
{tok.text}
|
|
217
|
-
</Text>
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
if (tok.kind === 'italic') {
|
|
221
|
-
return (
|
|
222
|
-
<Text key={key} color={baseColor} italic={true}>
|
|
223
|
-
{tok.text}
|
|
224
|
-
</Text>
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
if (tok.kind === 'code') {
|
|
228
|
-
return (
|
|
229
|
-
<Text key={key} color={theme.markdown.codeText} backgroundColor={theme.markdown.codeBackground}>
|
|
230
|
-
{` ${tok.text} `}
|
|
231
|
-
</Text>
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
if (tok.kind === 'link') {
|
|
235
|
-
return (
|
|
236
|
-
<Text key={key} color={theme.markdown.link} underline={true}>
|
|
237
|
-
{tok.text}
|
|
238
|
-
{tok.href ? (
|
|
239
|
-
<Text color={theme.markdown.link} dimColor={true}>
|
|
240
|
-
{' '}
|
|
241
|
-
({tok.href})
|
|
242
|
-
</Text>
|
|
243
|
-
) : null}
|
|
244
|
-
</Text>
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
return null;
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// ─── Block renderers ──────────────────────────────────────────────────────────
|
|
252
|
-
|
|
253
|
-
function HeadingBlock({ block, theme }: { block: Extract<Block, { type: 'heading' }>; theme: Theme }) {
|
|
254
|
-
const prefix = block.level === 1 ? '# ' : block.level === 2 ? '## ' : '### ';
|
|
255
|
-
return (
|
|
256
|
-
<Box marginBottom={1}>
|
|
257
|
-
<Text color={theme.markdown.heading} bold={block.level <= 2}>
|
|
258
|
-
{prefix}
|
|
259
|
-
{block.text}
|
|
260
|
-
</Text>
|
|
261
|
-
</Box>
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function ParagraphBlock({
|
|
266
|
-
block,
|
|
267
|
-
theme,
|
|
268
|
-
color,
|
|
269
|
-
}: {
|
|
270
|
-
block: Extract<Block, { type: 'paragraph' }>;
|
|
271
|
-
theme: Theme;
|
|
272
|
-
color?: string;
|
|
273
|
-
}) {
|
|
274
|
-
return (
|
|
275
|
-
<Box marginBottom={1}>
|
|
276
|
-
<Text wrap="wrap" color={color}>
|
|
277
|
-
{renderInline(block.text, theme, color)}
|
|
278
|
-
</Text>
|
|
279
|
-
</Box>
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function ListBlock({ block, theme, color }: { block: Extract<Block, { type: 'list' }>; theme: Theme; color?: string }) {
|
|
284
|
-
return (
|
|
285
|
-
<Box flexDirection="column" marginBottom={1}>
|
|
286
|
-
{block.items.map((item, idx) => {
|
|
287
|
-
const marker = block.ordered ? `${idx + 1}.` : '•';
|
|
288
|
-
return (
|
|
289
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: list items have no stable id
|
|
290
|
-
<Box key={idx}>
|
|
291
|
-
<Text color={theme.markdown.bullet}>{` ${marker} `}</Text>
|
|
292
|
-
<Box flexShrink={1} flexGrow={1}>
|
|
293
|
-
<Text wrap="wrap" color={color}>
|
|
294
|
-
{renderInline(item, theme, color)}
|
|
295
|
-
</Text>
|
|
296
|
-
</Box>
|
|
297
|
-
</Box>
|
|
298
|
-
);
|
|
299
|
-
})}
|
|
300
|
-
</Box>
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function CodeBlock({ block, theme }: { block: Extract<Block, { type: 'code' }>; theme: Theme }) {
|
|
305
|
-
return (
|
|
306
|
-
<Box flexDirection="column" marginBottom={1} paddingX={1} backgroundColor={theme.markdown.codeBlockBackground}>
|
|
307
|
-
{block.lang && (
|
|
308
|
-
<Text dimColor={true} color={theme.markdown.codeBlockText}>
|
|
309
|
-
{block.lang}
|
|
310
|
-
</Text>
|
|
311
|
-
)}
|
|
312
|
-
{block.lines.map((ln, i) => (
|
|
313
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: code lines have no stable id and may repeat
|
|
314
|
-
<Text key={`${i}-${ln}`} color={theme.markdown.codeBlockText}>
|
|
315
|
-
{ln || ' '}
|
|
316
|
-
</Text>
|
|
317
|
-
))}
|
|
318
|
-
</Box>
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function QuoteBlock({ block, theme }: { block: Extract<Block, { type: 'quote' }>; theme: Theme }) {
|
|
323
|
-
return (
|
|
324
|
-
<Box flexDirection="column" marginBottom={1}>
|
|
325
|
-
{block.lines.map((ln, i) => (
|
|
326
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: blockquote lines render in document order and never reorder — index is the only stable identifier.
|
|
327
|
-
<Box key={`${i}-${ln}`}>
|
|
328
|
-
<Text color={theme.markdown.blockquote}> │ </Text>
|
|
329
|
-
<Box flexShrink={1} flexGrow={1}>
|
|
330
|
-
<Text wrap="wrap" color={theme.markdown.blockquote} italic={true}>
|
|
331
|
-
{renderInline(ln, theme, theme.markdown.blockquote)}
|
|
332
|
-
</Text>
|
|
333
|
-
</Box>
|
|
334
|
-
</Box>
|
|
335
|
-
))}
|
|
336
|
-
</Box>
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function TableBlock({ block, theme }: { block: Extract<Block, { type: 'table' }>; theme: Theme }) {
|
|
341
|
-
// Compute column widths based on the longest cell per column.
|
|
342
|
-
const colCount = Math.max(block.header.length, ...block.rows.map((r) => r.length));
|
|
343
|
-
const widths: number[] = new Array(colCount).fill(0);
|
|
344
|
-
for (let c = 0; c < colCount; c++) {
|
|
345
|
-
widths[c] = (block.header[c] ?? '').length;
|
|
346
|
-
for (const row of block.rows) {
|
|
347
|
-
widths[c] = Math.max(widths[c], (row[c] ?? '').length);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
const renderRow = (cells: string[], bold: boolean, key: string) => (
|
|
351
|
-
<Box key={key}>
|
|
352
|
-
{Array.from({ length: colCount }, (_, c) => (
|
|
353
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: table columns have fixed positions for the lifetime of the row; reordering is impossible.
|
|
354
|
-
<Box key={c} marginRight={c === colCount - 1 ? 0 : 2}>
|
|
355
|
-
<Text bold={bold}>{(cells[c] ?? '').padEnd(widths[c])}</Text>
|
|
356
|
-
</Box>
|
|
357
|
-
))}
|
|
358
|
-
</Box>
|
|
359
|
-
);
|
|
360
|
-
const sep = (
|
|
361
|
-
<Box>
|
|
362
|
-
{Array.from({ length: colCount }, (_, c) => (
|
|
363
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: separator cells map 1:1 to columns — column index is the natural key.
|
|
364
|
-
<Box key={c} marginRight={c === colCount - 1 ? 0 : 2}>
|
|
365
|
-
<Text color={theme.markdown.tableBorder}>{'─'.repeat(widths[c])}</Text>
|
|
366
|
-
</Box>
|
|
367
|
-
))}
|
|
368
|
-
</Box>
|
|
369
|
-
);
|
|
370
|
-
return (
|
|
371
|
-
<Box flexDirection="column" marginBottom={1}>
|
|
372
|
-
{renderRow(block.header, true, 'header')}
|
|
373
|
-
{sep}
|
|
374
|
-
{block.rows.map((r, i) => renderRow(r, false, `r-${i}`))}
|
|
375
|
-
</Box>
|
|
376
|
-
);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function HrBlock({ theme }: { theme: Theme }) {
|
|
380
|
-
return (
|
|
381
|
-
<Box marginBottom={1}>
|
|
382
|
-
<Text color={theme.markdown.tableBorder}>{'─'.repeat(40)}</Text>
|
|
383
|
-
</Box>
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// ─── Public renderer ─────────────────────────────────────────────────────────
|
|
388
|
-
|
|
389
|
-
export function MarkdownContent({ content, color }: { content: string; color?: string }) {
|
|
390
|
-
const theme = useTheme();
|
|
391
|
-
const blocks = parseBlocks(content);
|
|
392
|
-
return (
|
|
393
|
-
<Box flexDirection="column">
|
|
394
|
-
{blocks.map((b, i) => {
|
|
395
|
-
const key = `${i}-${b.type}`;
|
|
396
|
-
if (b.type === 'heading') return <HeadingBlock key={key} block={b} theme={theme} />;
|
|
397
|
-
if (b.type === 'paragraph') return <ParagraphBlock key={key} block={b} theme={theme} color={color} />;
|
|
398
|
-
if (b.type === 'list') return <ListBlock key={key} block={b} theme={theme} color={color} />;
|
|
399
|
-
if (b.type === 'code') return <CodeBlock key={key} block={b} theme={theme} />;
|
|
400
|
-
if (b.type === 'quote') return <QuoteBlock key={key} block={b} theme={theme} />;
|
|
401
|
-
if (b.type === 'table') return <TableBlock key={key} block={b} theme={theme} />;
|
|
402
|
-
if (b.type === 'hr') return <HrBlock key={key} theme={theme} />;
|
|
403
|
-
return null;
|
|
404
|
-
})}
|
|
405
|
-
</Box>
|
|
406
|
-
);
|
|
407
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { ChatMessage } from 'mu-core';
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { useMessageRenderer } from '../../chat/MessageRendererContext';
|
|
4
|
-
import { AssistantMessage } from './assistantMessage';
|
|
5
|
-
import { UserMessage } from './userMessage';
|
|
6
|
-
|
|
7
|
-
export const MessageItem: React.FC<{
|
|
8
|
-
msg: ChatMessage;
|
|
9
|
-
toolMessages?: ChatMessage[];
|
|
10
|
-
}> = React.memo(function MessageItem({ msg, toolMessages }) {
|
|
11
|
-
const customRenderer = useMessageRenderer(msg.customType);
|
|
12
|
-
|
|
13
|
-
// Plugins may flag a message as `hidden` to keep it in the LLM transcript
|
|
14
|
-
// while suppressing on-screen rendering (e.g. system reminders carried with
|
|
15
|
-
// the user's next turn).
|
|
16
|
-
if (msg.display?.hidden) {
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Custom-typed messages always defer to a registered renderer when one is
|
|
21
|
-
// available; otherwise fall through to the role-default rendering so a
|
|
22
|
-
// plugin can ship messages whose renderer isn't loaded yet without losing
|
|
23
|
-
// their content.
|
|
24
|
-
if (customRenderer) {
|
|
25
|
-
return <>{customRenderer(msg)}</>;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Tool result messages are rendered inline within ToolCallBlock via the
|
|
29
|
-
// pre-built index passed from MessageView; suppress them at the top level.
|
|
30
|
-
if (msg.role === 'tool') {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (msg.role === 'assistant' && msg.toolCalls?.length) {
|
|
35
|
-
return <AssistantMessage msg={msg} toolMessages={toolMessages} />;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (msg.role === 'user') {
|
|
39
|
-
return <UserMessage msg={msg} />;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return <AssistantMessage msg={msg} />;
|
|
43
|
-
});
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { Box, Text } from 'ink';
|
|
2
|
-
import { useTheme } from '../../context/ThemeContext';
|
|
3
|
-
|
|
4
|
-
export function ReasoningBlock({ reasoning }: { reasoning: string }) {
|
|
5
|
-
const theme = useTheme();
|
|
6
|
-
return (
|
|
7
|
-
<Box marginBottom={0}>
|
|
8
|
-
<Text wrap="wrap">
|
|
9
|
-
<Text color={theme.reasoning.title} italic={true}>
|
|
10
|
-
thinking:{' '}
|
|
11
|
-
</Text>
|
|
12
|
-
<Text color={theme.reasoning.body} italic={true}>
|
|
13
|
-
{reasoning}
|
|
14
|
-
</Text>
|
|
15
|
-
</Text>
|
|
16
|
-
</Box>
|
|
17
|
-
);
|
|
18
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { Box, Text } from 'ink';
|
|
2
|
-
import { useTheme } from '../../context/ThemeContext';
|
|
3
|
-
import { ReasoningBlock } from './reasoningBlock';
|
|
4
|
-
|
|
5
|
-
export function StreamingOutput({ currentText, currentReasoning }: { currentText: string; currentReasoning: string }) {
|
|
6
|
-
const theme = useTheme();
|
|
7
|
-
return (
|
|
8
|
-
<Box flexDirection="column" flexShrink={0} marginBottom={0}>
|
|
9
|
-
{currentReasoning && <ReasoningBlock reasoning={currentReasoning} />}
|
|
10
|
-
<Text wrap="wrap">
|
|
11
|
-
{currentText}
|
|
12
|
-
<Text color={theme.input.cursor} inverse={true}>
|
|
13
|
-
▎
|
|
14
|
-
</Text>
|
|
15
|
-
</Text>
|
|
16
|
-
</Box>
|
|
17
|
-
);
|
|
18
|
-
}
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { Box, Text } from 'ink';
|
|
2
|
-
import type { ChatMessage, ToolDisplayHint } from 'mu-core';
|
|
3
|
-
import { useToolDisplay } from '../../chat/ToolDisplayContext';
|
|
4
|
-
import { useTheme } from '../../context/ThemeContext';
|
|
5
|
-
import { useSpinner } from '../../hooks/useUI';
|
|
6
|
-
import { EditOutput } from './EditOutput';
|
|
7
|
-
import { ReadOutput } from './ReadOutput';
|
|
8
|
-
import { WebFetchOutput } from './WebFetchOutput';
|
|
9
|
-
import { WriteOutput } from './WriteOutput';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Render a tool call. Display behaviour is driven by the optional
|
|
13
|
-
* `ToolDisplayHint` the plugin attached to its tool — `kind` selects the
|
|
14
|
-
* dedicated renderer (file-read / file-write / diff / shell), and `verb`
|
|
15
|
-
* shows in the spinner line. Tools without a hint fall back to a generic
|
|
16
|
-
* preview block, so plugin-registered tools "just work" without UI changes.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
function getArgSummary(args: string, hint: ToolDisplayHint | undefined): string {
|
|
20
|
-
if (!hint?.fields) return args;
|
|
21
|
-
// For shell-like tools the most useful preview is the command itself;
|
|
22
|
-
// generic tools show the raw JSON.
|
|
23
|
-
const commandField = hint.fields.command;
|
|
24
|
-
if (!commandField) return args;
|
|
25
|
-
try {
|
|
26
|
-
const parsed = JSON.parse(args);
|
|
27
|
-
return parsed[commandField] ?? args;
|
|
28
|
-
} catch {
|
|
29
|
-
return args;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function ToolCallBlock({
|
|
34
|
-
toolCall,
|
|
35
|
-
toolMsg,
|
|
36
|
-
}: {
|
|
37
|
-
toolCall: { id: string; function: { name: string; arguments: string } };
|
|
38
|
-
toolMsg?: ChatMessage;
|
|
39
|
-
}) {
|
|
40
|
-
const name = toolCall.function.name;
|
|
41
|
-
const args = toolCall.function.arguments;
|
|
42
|
-
const hint = useToolDisplay(name);
|
|
43
|
-
|
|
44
|
-
const result = toolMsg?.toolResult;
|
|
45
|
-
const hasResult = result !== undefined;
|
|
46
|
-
const spinner = useSpinner(!hasResult);
|
|
47
|
-
const verb = hint?.verb ?? 'executing';
|
|
48
|
-
const argSummary = getArgSummary(args, hint);
|
|
49
|
-
|
|
50
|
-
return (
|
|
51
|
-
<Box flexDirection="column" flexShrink={0} marginTop={1} marginBottom={1}>
|
|
52
|
-
{!hasResult ? (
|
|
53
|
-
<Box>
|
|
54
|
-
<Text dimColor={true}>
|
|
55
|
-
{' '}
|
|
56
|
-
{spinner} {verb}... <Text dimColor={true}>{argSummary}</Text>
|
|
57
|
-
</Text>
|
|
58
|
-
</Box>
|
|
59
|
-
) : (
|
|
60
|
-
renderToolOutput(name, args, result.content, result.error ?? false, hint)
|
|
61
|
-
)}
|
|
62
|
-
</Box>
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function renderToolOutput(
|
|
67
|
-
name: string,
|
|
68
|
-
args: string,
|
|
69
|
-
content: string,
|
|
70
|
-
error: boolean,
|
|
71
|
-
hint: ToolDisplayHint | undefined,
|
|
72
|
-
) {
|
|
73
|
-
switch (hint?.kind) {
|
|
74
|
-
case 'file-read':
|
|
75
|
-
return <ReadOutput args={args} error={error} />;
|
|
76
|
-
case 'file-write':
|
|
77
|
-
return <WriteOutput args={args} content={content} error={error} />;
|
|
78
|
-
case 'diff':
|
|
79
|
-
return <EditOutput args={args} content={content} error={error} hint={hint} />;
|
|
80
|
-
case 'webfetch':
|
|
81
|
-
return <WebFetchOutput args={args} error={error} />;
|
|
82
|
-
default:
|
|
83
|
-
return <GenericToolOutput name={name} args={args} content={content} error={error} hint={hint} />;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
interface GenericProps {
|
|
88
|
-
name: string;
|
|
89
|
-
args: string;
|
|
90
|
-
content: string;
|
|
91
|
-
error: boolean;
|
|
92
|
-
hint: ToolDisplayHint | undefined;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function GenericToolOutput({ name, args, content, error, hint }: GenericProps) {
|
|
96
|
-
const theme = useTheme();
|
|
97
|
-
let summary = '';
|
|
98
|
-
const commandField = hint?.fields?.command;
|
|
99
|
-
if (commandField) {
|
|
100
|
-
try {
|
|
101
|
-
const parsed = JSON.parse(args);
|
|
102
|
-
summary = parsed[commandField] ?? '';
|
|
103
|
-
} catch {
|
|
104
|
-
// ignore
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const preview = content.length > 200 ? `${content.slice(0, 200)}…` : content;
|
|
109
|
-
return (
|
|
110
|
-
<Box flexDirection="column" flexShrink={0}>
|
|
111
|
-
<Text color={error ? theme.tool.error : theme.tool.success} bold={true}>
|
|
112
|
-
{error ? '✗' : '✓'} {name}
|
|
113
|
-
{summary && (
|
|
114
|
-
<>
|
|
115
|
-
{' '}
|
|
116
|
-
<Text dimColor={true}>{summary}</Text>
|
|
117
|
-
</>
|
|
118
|
-
)}
|
|
119
|
-
</Text>
|
|
120
|
-
<Box flexDirection="column" backgroundColor={theme.tool.previewBackground} paddingX={1} paddingY={0}>
|
|
121
|
-
<Text color={theme.tool.previewText}>{preview}</Text>
|
|
122
|
-
</Box>
|
|
123
|
-
</Box>
|
|
124
|
-
);
|
|
125
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { Box, Text } from 'ink';
|
|
2
|
-
import type { ChatMessage } from 'mu-core';
|
|
3
|
-
import { useTheme } from '../../context/ThemeContext';
|
|
4
|
-
|
|
5
|
-
export function UserMessage({ msg }: { msg: ChatMessage }) {
|
|
6
|
-
const theme = useTheme();
|
|
7
|
-
const borderColor = msg.display?.color ?? theme.user.border;
|
|
8
|
-
const badge = msg.display?.badge;
|
|
9
|
-
const prefix = msg.display?.prefix;
|
|
10
|
-
return (
|
|
11
|
-
<Box
|
|
12
|
-
flexDirection="column"
|
|
13
|
-
flexShrink={0}
|
|
14
|
-
marginBottom={1}
|
|
15
|
-
backgroundColor={theme.user.background}
|
|
16
|
-
paddingX={1}
|
|
17
|
-
paddingY={1}
|
|
18
|
-
borderLeft={true}
|
|
19
|
-
borderTop={false}
|
|
20
|
-
borderBottom={false}
|
|
21
|
-
borderRight={false}
|
|
22
|
-
borderColor={borderColor}
|
|
23
|
-
borderStyle="single"
|
|
24
|
-
>
|
|
25
|
-
{badge && (
|
|
26
|
-
<Box>
|
|
27
|
-
<Text color={msg.display?.color} bold={true}>
|
|
28
|
-
{badge.charAt(0).toUpperCase() + badge.slice(1)}
|
|
29
|
-
</Text>
|
|
30
|
-
</Box>
|
|
31
|
-
)}
|
|
32
|
-
{msg.images && msg.images.length > 0 && (
|
|
33
|
-
<Box>
|
|
34
|
-
<Text color={theme.user.attachment}>📷 </Text>
|
|
35
|
-
<Text color={theme.user.attachment}>{msg.images.map((img) => img.name).join(', ')}</Text>
|
|
36
|
-
</Box>
|
|
37
|
-
)}
|
|
38
|
-
<Text wrap="wrap">
|
|
39
|
-
{prefix && <Text color={msg.display?.color}>{prefix}</Text>}
|
|
40
|
-
{msg.content}
|
|
41
|
-
</Text>
|
|
42
|
-
</Box>
|
|
43
|
-
);
|
|
44
|
-
}
|