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
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import type { ContentPart, Message } from 'mu-core';
|
|
2
|
+
import { column, type Component, truncateToWidth, visibleWidth, wrapText } from 'mu-tui';
|
|
3
|
+
import type { AgentSessionEvent } from 'mu-harness';
|
|
4
|
+
import { renderMarkdown } from './markdown';
|
|
5
|
+
import { styleToAnsi, type Theme } from './theme';
|
|
6
|
+
|
|
7
|
+
const RESET = '\x1b[0m';
|
|
8
|
+
const PAD = 1;
|
|
9
|
+
const COLLAPSE_LIMIT = 8;
|
|
10
|
+
|
|
11
|
+
export type Entry =
|
|
12
|
+
| { kind: 'user'; text: string }
|
|
13
|
+
| { kind: 'reasoning'; text: string; closed: boolean }
|
|
14
|
+
| { kind: 'assistant'; text: string }
|
|
15
|
+
| { kind: 'tool_call'; name: string; input: unknown }
|
|
16
|
+
| { kind: 'shell'; cmd: string; output: string; error: boolean }
|
|
17
|
+
| {
|
|
18
|
+
kind: 'subagent';
|
|
19
|
+
agent: string;
|
|
20
|
+
status: SubAgentStatus;
|
|
21
|
+
tools: number;
|
|
22
|
+
activity: string;
|
|
23
|
+
result: string;
|
|
24
|
+
log: string[];
|
|
25
|
+
open: boolean;
|
|
26
|
+
}
|
|
27
|
+
| { kind: 'note'; text: string; error: boolean };
|
|
28
|
+
|
|
29
|
+
type ReasoningEntry = Extract<Entry, { kind: 'reasoning' }>;
|
|
30
|
+
type SubAgentEntry = Extract<Entry, { kind: 'subagent' }>;
|
|
31
|
+
|
|
32
|
+
export type SubAgentStatus = 'running' | 'done' | 'error';
|
|
33
|
+
|
|
34
|
+
export interface SubAgentHandle {
|
|
35
|
+
addTool(label: string): void;
|
|
36
|
+
finish(result: string): void;
|
|
37
|
+
fail(message: string): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const partsToText = (parts: readonly ContentPart[]): string =>
|
|
41
|
+
parts.map((part) => (part.type === 'text' ? part.text : part.type === 'tool_result' ? partsToText(part.content) : ''))
|
|
42
|
+
.join('');
|
|
43
|
+
|
|
44
|
+
const isToolResults = (message: Message): boolean =>
|
|
45
|
+
message.content.length > 0 && message.content.every((part) => part.type === 'tool_result');
|
|
46
|
+
|
|
47
|
+
export class Transcript {
|
|
48
|
+
entries: Entry[] = [];
|
|
49
|
+
expanded = false;
|
|
50
|
+
thinkingVisible = false;
|
|
51
|
+
private pending: { kind: 'assistant'; text: string } | undefined;
|
|
52
|
+
private pendingReasoning: ReasoningEntry | undefined;
|
|
53
|
+
|
|
54
|
+
reset(): void {
|
|
55
|
+
this.entries = [];
|
|
56
|
+
this.pending = undefined;
|
|
57
|
+
this.pendingReasoning = undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private closeReasoning(): void {
|
|
61
|
+
if (this.pendingReasoning) this.pendingReasoning.closed = !this.thinkingVisible;
|
|
62
|
+
this.pendingReasoning = undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
toggleExpanded(): boolean {
|
|
66
|
+
if (!this.entries.some((e) => e.kind === 'shell')) return false;
|
|
67
|
+
this.expanded = !this.expanded;
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
toggleReasoning(): boolean {
|
|
72
|
+
this.thinkingVisible = !this.thinkingVisible;
|
|
73
|
+
for (const entry of this.entries) {
|
|
74
|
+
if (entry.kind === 'reasoning') entry.closed = !this.thinkingVisible;
|
|
75
|
+
}
|
|
76
|
+
return this.thinkingVisible;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
appendUser(text: string): void {
|
|
80
|
+
this.entries.push({ kind: 'user', text });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
note(content: string, error = false): void {
|
|
84
|
+
this.entries.push({ kind: 'note', text: content, error });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
appendSubAgent(agent: string): SubAgentHandle {
|
|
88
|
+
const entry: SubAgentEntry = {
|
|
89
|
+
kind: 'subagent',
|
|
90
|
+
agent,
|
|
91
|
+
status: 'running',
|
|
92
|
+
tools: 0,
|
|
93
|
+
activity: '',
|
|
94
|
+
result: '',
|
|
95
|
+
log: [],
|
|
96
|
+
open: false,
|
|
97
|
+
};
|
|
98
|
+
this.entries.push(entry);
|
|
99
|
+
return {
|
|
100
|
+
addTool: (label) => {
|
|
101
|
+
entry.tools += 1;
|
|
102
|
+
entry.activity = label;
|
|
103
|
+
entry.log.push(label);
|
|
104
|
+
},
|
|
105
|
+
finish: (result) => {
|
|
106
|
+
entry.status = 'done';
|
|
107
|
+
entry.activity = '';
|
|
108
|
+
entry.result = result;
|
|
109
|
+
},
|
|
110
|
+
fail: (message) => {
|
|
111
|
+
entry.status = 'error';
|
|
112
|
+
entry.activity = '';
|
|
113
|
+
entry.result = message;
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
appendShell(cmd: string): { setOutput: (output: string, error?: boolean) => void } {
|
|
119
|
+
const entry: Entry = { kind: 'shell', cmd, output: '', error: false };
|
|
120
|
+
this.entries.push(entry);
|
|
121
|
+
return {
|
|
122
|
+
setOutput: (output, error = false) => {
|
|
123
|
+
entry.output = output;
|
|
124
|
+
entry.error = error;
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
seed(messages: readonly Message[]): void {
|
|
130
|
+
this.reset();
|
|
131
|
+
for (const message of messages) {
|
|
132
|
+
if (message.role === 'system') continue;
|
|
133
|
+
if (message.role === 'assistant') {
|
|
134
|
+
const txt = partsToText(message.content);
|
|
135
|
+
if (txt) this.entries.push({ kind: 'assistant', text: txt });
|
|
136
|
+
for (const part of message.content) {
|
|
137
|
+
if (part.type === 'tool_call') this.entries.push({ kind: 'tool_call', name: part.name, input: part.input });
|
|
138
|
+
}
|
|
139
|
+
} else if (!isToolResults(message)) {
|
|
140
|
+
this.entries.push({ kind: 'user', text: partsToText(message.content) });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
applyEvent(event: AgentSessionEvent): void {
|
|
146
|
+
switch (event.type) {
|
|
147
|
+
case 'turn_start':
|
|
148
|
+
this.pending = undefined;
|
|
149
|
+
this.pendingReasoning = undefined;
|
|
150
|
+
return;
|
|
151
|
+
case 'reasoning': {
|
|
152
|
+
if (!this.pendingReasoning) {
|
|
153
|
+
this.pendingReasoning = { kind: 'reasoning', text: '', closed: !this.thinkingVisible };
|
|
154
|
+
this.entries.push(this.pendingReasoning);
|
|
155
|
+
}
|
|
156
|
+
this.pendingReasoning.text += event.text;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
case 'text': {
|
|
160
|
+
this.closeReasoning();
|
|
161
|
+
if (!this.pending) {
|
|
162
|
+
this.pending = { kind: 'assistant', text: '' };
|
|
163
|
+
this.entries.push(this.pending);
|
|
164
|
+
}
|
|
165
|
+
this.pending.text += event.text;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
case 'tool_call':
|
|
169
|
+
this.closeReasoning();
|
|
170
|
+
if (event.name === 'subagent') return;
|
|
171
|
+
this.entries.push({ kind: 'tool_call', name: event.name, input: event.input });
|
|
172
|
+
return;
|
|
173
|
+
case 'message': {
|
|
174
|
+
const message = event.message;
|
|
175
|
+
if (message.role === 'assistant') {
|
|
176
|
+
const txt = partsToText(message.content);
|
|
177
|
+
if (this.pending) this.pending.text = txt;
|
|
178
|
+
else if (txt) this.entries.push({ kind: 'assistant', text: txt });
|
|
179
|
+
this.pending = undefined;
|
|
180
|
+
this.closeReasoning();
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
case 'turn_end':
|
|
185
|
+
case 'done':
|
|
186
|
+
this.pending = undefined;
|
|
187
|
+
this.closeReasoning();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const stringifyArg = (value: unknown): string => {
|
|
194
|
+
if (value === undefined || value === null) return '';
|
|
195
|
+
if (typeof value === 'string') return value;
|
|
196
|
+
if (Array.isArray(value)) return value.map(stringifyArg).filter(Boolean).join(' ');
|
|
197
|
+
return JSON.stringify(value) ?? '';
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const truncateText = (value: string, max: number): string =>
|
|
201
|
+
value.length > max ? `${value.slice(0, Math.max(0, max - 1))}…` : value;
|
|
202
|
+
|
|
203
|
+
export function formatToolArgs(name: string, input: unknown, max = 120): string {
|
|
204
|
+
if (input === null || typeof input !== 'object') return truncateText(String(input ?? ''), max);
|
|
205
|
+
const args = input as Record<string, unknown>;
|
|
206
|
+
if (name === 'edit' || name === 'write' || name === 'read' || name === 'list_dir') {
|
|
207
|
+
return truncateText(stringifyArg(args.path), max);
|
|
208
|
+
}
|
|
209
|
+
if (name === 'bash') return truncateText(stringifyArg(args.cmd), max);
|
|
210
|
+
if (name === 'subagent') return truncateText(stringifyArg(args.agent), max);
|
|
211
|
+
return truncateText(Object.values(args).map(stringifyArg).filter(Boolean).join(' '), max);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const fit = (line: string, width: number): string => visibleWidth(line) > width ? truncateToWidth(line, width) : line;
|
|
215
|
+
|
|
216
|
+
const SPACER: Component = { render: (s) => s.text(0, 0, '') };
|
|
217
|
+
|
|
218
|
+
const userEntry = (value: string, theme: Theme): Component => ({
|
|
219
|
+
render: (s) => {
|
|
220
|
+
if (s.width <= 0) return;
|
|
221
|
+
const bg = theme.styles.userMessage.bg;
|
|
222
|
+
if (bg) s.fill({ x: 0, y: 0, width: s.width, height: s.height }, bg);
|
|
223
|
+
const muted = styleToAnsi(theme.styles.muted);
|
|
224
|
+
const body = styleToAnsi(theme.styles.userMessage);
|
|
225
|
+
const innerW = Math.max(1, s.width - PAD - 2 - PAD);
|
|
226
|
+
const wrapped = value.split('\n').flatMap((line) => wrapText(line, innerW));
|
|
227
|
+
for (let i = 0; i < wrapped.length; i++) {
|
|
228
|
+
if (i === 0) s.text(PAD, 0, `${muted}❯${RESET}`);
|
|
229
|
+
s.text(PAD + 2, i, `${body}${wrapped[i]}${RESET}`);
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const assistantEntry = (value: string, theme: Theme): Component => ({
|
|
235
|
+
render: (s) => {
|
|
236
|
+
if (s.width <= 0) return;
|
|
237
|
+
const innerW = Math.max(1, s.width - PAD * 2);
|
|
238
|
+
const lines = renderMarkdown(value || '…', innerW, theme);
|
|
239
|
+
for (let i = 0; i < lines.length; i++) s.text(PAD, i, fit(lines[i], innerW));
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const leftClick = (event: { type: string; kind?: string; button?: string }): boolean =>
|
|
244
|
+
event.type === 'mouse' && event.kind === 'press' && event.button === 'left';
|
|
245
|
+
|
|
246
|
+
const reasoningComponent = (entry: ReasoningEntry, theme: Theme): Component => {
|
|
247
|
+
const style = styleToAnsi(theme.styles.reasoning);
|
|
248
|
+
if (entry.closed) {
|
|
249
|
+
return {
|
|
250
|
+
handleInput: (event) => {
|
|
251
|
+
if (leftClick(event)) entry.closed = false;
|
|
252
|
+
},
|
|
253
|
+
render: (s) => {
|
|
254
|
+
if (s.width <= 0) return;
|
|
255
|
+
s.text(PAD, 0, `${style}[thinking]${RESET}`);
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
render: (s) => {
|
|
261
|
+
if (s.width <= 0) return;
|
|
262
|
+
const innerW = Math.max(1, s.width - PAD * 2);
|
|
263
|
+
const wrapped = entry.text.split('\n').flatMap((line) => wrapText(line, innerW));
|
|
264
|
+
for (let i = 0; i < wrapped.length; i++) s.text(PAD, i, `${style}${fit(wrapped[i], innerW)}${RESET}`);
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const toolEntry = (name: string, input: unknown, theme: Theme): Component => ({
|
|
270
|
+
render: (s) => {
|
|
271
|
+
if (s.width <= 0) return;
|
|
272
|
+
const muted = styleToAnsi(theme.styles.muted);
|
|
273
|
+
const args = formatToolArgs(name, input);
|
|
274
|
+
const line = args ? `→ ${name} ${args}` : `→ ${name}`;
|
|
275
|
+
s.text(PAD, 0, `${muted}${fit(line, Math.max(1, s.width - PAD * 2))}${RESET}`);
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const noteEntry = (value: string, error: boolean, theme: Theme): Component => ({
|
|
280
|
+
render: (s) => {
|
|
281
|
+
if (s.width <= 0) return;
|
|
282
|
+
const innerW = Math.max(1, s.width - PAD * 2);
|
|
283
|
+
if (error) {
|
|
284
|
+
const head = styleToAnsi(theme.styles.errorPrefix);
|
|
285
|
+
const tail = styleToAnsi(theme.styles.errorLine);
|
|
286
|
+
s.text(PAD, 0, `${head}! ${RESET}${tail}${fit(value, Math.max(1, innerW - 2))}${RESET}`);
|
|
287
|
+
} else {
|
|
288
|
+
s.text(PAD, 0, `${styleToAnsi(theme.styles.muted)}${fit(value, innerW)}${RESET}`);
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const shellEntry = (cmd: string, output: string, error: boolean, expanded: boolean, theme: Theme): Component => {
|
|
294
|
+
const innerWidthFor = (w: number) => Math.max(1, w - 2);
|
|
295
|
+
return {
|
|
296
|
+
render: (s) => {
|
|
297
|
+
if (s.width <= 0) return;
|
|
298
|
+
const bg = error ? theme.colors.surfaceMuted : theme.colors.surface;
|
|
299
|
+
s.fill({ x: 0, y: 0, width: s.width, height: s.height }, bg);
|
|
300
|
+
const headerStyle = styleToAnsi({ fg: theme.colors.textMuted });
|
|
301
|
+
const outputStyle = styleToAnsi({ fg: theme.colors.text });
|
|
302
|
+
const innerW = innerWidthFor(s.width);
|
|
303
|
+
s.text(1, 1, `${headerStyle}${fit(cmd, innerW)}${RESET}`);
|
|
304
|
+
const all = wrapText(output, innerW);
|
|
305
|
+
const lines = expanded || all.length <= COLLAPSE_LIMIT ? all : all.slice(0, COLLAPSE_LIMIT);
|
|
306
|
+
const truncated = expanded ? 0 : Math.max(0, all.length - COLLAPSE_LIMIT);
|
|
307
|
+
for (let i = 0; i < lines.length; i++) s.text(1, 3 + i, `${outputStyle}${lines[i]}${RESET}`);
|
|
308
|
+
let bottom = 3 + lines.length;
|
|
309
|
+
if (truncated > 0) {
|
|
310
|
+
s.text(1, bottom, `${headerStyle}... ${truncated} more lines (ctrl+o)${RESET}`);
|
|
311
|
+
bottom += 1;
|
|
312
|
+
}
|
|
313
|
+
s.text(0, bottom, '');
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const SUBAGENT_ICON: Record<SubAgentStatus, string> = { running: '◐', done: '✓', error: '✗' };
|
|
319
|
+
|
|
320
|
+
const subAgentEntry = (entry: SubAgentEntry, theme: Theme): Component => {
|
|
321
|
+
const iconColor = entry.status === 'done'
|
|
322
|
+
? theme.colors.success
|
|
323
|
+
: entry.status === 'error'
|
|
324
|
+
? theme.styles.errorPrefix.fg ?? theme.colors.warning
|
|
325
|
+
: theme.colors.accent;
|
|
326
|
+
return {
|
|
327
|
+
handleInput: (event) => {
|
|
328
|
+
if (leftClick(event)) entry.open = !entry.open;
|
|
329
|
+
},
|
|
330
|
+
render: (s) => {
|
|
331
|
+
if (s.width <= 0) return;
|
|
332
|
+
const innerW = Math.max(1, s.width - PAD * 2);
|
|
333
|
+
const icon = styleToAnsi({ fg: iconColor, bold: true });
|
|
334
|
+
const name = styleToAnsi({ fg: theme.colors.accent, bold: true });
|
|
335
|
+
const muted = styleToAnsi(theme.styles.muted);
|
|
336
|
+
const meta = entry.tools > 0 ? ` · ${entry.tools} tool${entry.tools === 1 ? '' : 's'}` : '';
|
|
337
|
+
const header = `${icon}${
|
|
338
|
+
SUBAGENT_ICON[entry.status]
|
|
339
|
+
}${RESET} ${name}@${entry.agent}${RESET}${muted} ${entry.status}${meta}${RESET}`;
|
|
340
|
+
s.text(PAD, 0, fit(header, innerW));
|
|
341
|
+
let row = 1;
|
|
342
|
+
if (entry.open && entry.log.length > 0) {
|
|
343
|
+
for (const label of entry.log) {
|
|
344
|
+
s.text(PAD + 2, row, `${muted}→ ${fit(label, Math.max(1, innerW - 2))}${RESET}`);
|
|
345
|
+
row += 1;
|
|
346
|
+
}
|
|
347
|
+
} else if (entry.status === 'running' && entry.activity) {
|
|
348
|
+
s.text(PAD + 2, row, `${muted}→ ${fit(entry.activity, Math.max(1, innerW - 2))}${RESET}`);
|
|
349
|
+
row += 1;
|
|
350
|
+
}
|
|
351
|
+
if (entry.result) {
|
|
352
|
+
const lines = renderMarkdown(entry.result, innerW, theme);
|
|
353
|
+
const shown = entry.open ? lines : lines.slice(0, COLLAPSE_LIMIT);
|
|
354
|
+
for (const line of shown) {
|
|
355
|
+
s.text(PAD, row, fit(line, innerW));
|
|
356
|
+
row += 1;
|
|
357
|
+
}
|
|
358
|
+
const hidden = lines.length - shown.length;
|
|
359
|
+
if (hidden > 0) {
|
|
360
|
+
s.text(PAD, row, `${muted}… ${hidden} more lines (click)${RESET}`);
|
|
361
|
+
row += 1;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
s.text(0, row, '');
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
export function entryComponent(entry: Entry, theme: Theme, expanded: boolean): Component {
|
|
370
|
+
switch (entry.kind) {
|
|
371
|
+
case 'user':
|
|
372
|
+
return userEntry(entry.text, theme);
|
|
373
|
+
case 'reasoning':
|
|
374
|
+
return reasoningComponent(entry, theme);
|
|
375
|
+
case 'assistant':
|
|
376
|
+
return assistantEntry(entry.text, theme);
|
|
377
|
+
case 'tool_call':
|
|
378
|
+
return toolEntry(entry.name, entry.input, theme);
|
|
379
|
+
case 'shell':
|
|
380
|
+
return shellEntry(entry.cmd, entry.output || '…', entry.error, expanded, theme);
|
|
381
|
+
case 'subagent':
|
|
382
|
+
return subAgentEntry(entry, theme);
|
|
383
|
+
case 'note':
|
|
384
|
+
return noteEntry(entry.text, entry.error, theme);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function transcriptComponent(model: Transcript, theme: Theme): Component {
|
|
389
|
+
return {
|
|
390
|
+
render: (s) => {
|
|
391
|
+
const children: Component[] = [];
|
|
392
|
+
for (const entry of model.entries) {
|
|
393
|
+
children.push(entryComponent(entry, theme, model.expanded));
|
|
394
|
+
children.push(SPACER);
|
|
395
|
+
}
|
|
396
|
+
column(children).render(s);
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
}
|
package/tsconfig.json
ADDED
package/bin/mu.js
DELETED
package/prompts/SYSTEM.md
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
You are mu, a terminal coding agent. Be concise, direct, accurate.
|
|
2
|
-
|
|
3
|
-
## Working style
|
|
4
|
-
- Investigate before editing; don't guess at APIs.
|
|
5
|
-
- Issue independent tool calls in parallel.
|
|
6
|
-
- Ask only when genuinely ambiguous; otherwise proceed.
|
|
7
|
-
- After non-trivial edits, run the project's check command if known (e.g. `bun run check`).
|
|
8
|
-
|
|
9
|
-
## Output
|
|
10
|
-
- Plain terminal text. Backticks for `paths`, `commands`, `identifiers`.
|
|
11
|
-
- Reference code as `path/to/file.ts:LINE`.
|
|
12
|
-
- No filler. Lead with the result or next action.
|
|
13
|
-
|
|
14
|
-
## Safety
|
|
15
|
-
- Never run destructive commands (`rm -rf`, force-push, history rewrites) without explicit request.
|
|
16
|
-
- Never commit, amend, or push unless asked.
|
package/src/app/shutdown.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import type { PluginRegistry } from 'mu-core';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Escape sequences to disable every SGR mouse-tracking mode the TUI may have
|
|
5
|
-
* enabled (or inherited from a stale prior session). Disabling already-off
|
|
6
|
-
* modes is a no-op, so we send all three defensively to avoid leaking mouse
|
|
7
|
-
* tracking into the parent shell after abort.
|
|
8
|
-
* - 1000 = X10/normal (press+release) ← what `useScroll` enables
|
|
9
|
-
* - 1002 = button-event tracking (press+drag) ← legacy, prior versions
|
|
10
|
-
* - 1003 = any-event tracking (all motion) ← belt-and-suspenders
|
|
11
|
-
* - 1006 = SGR-encoded coordinates extension
|
|
12
|
-
*/
|
|
13
|
-
const DISABLE_MOUSE_MODE = '\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l';
|
|
14
|
-
|
|
15
|
-
/** Restore the kitty keyboard protocol stack — symmetric with renderApp's enable. */
|
|
16
|
-
const POP_KITTY_KEYBOARD = '\x1b[<u';
|
|
17
|
-
|
|
18
|
-
export type ShutdownFn = (code?: number) => Promise<void>;
|
|
19
|
-
|
|
20
|
-
let registered = false;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Install graceful-shutdown handlers and return a `shutdown` function the TUI
|
|
24
|
-
* can invoke directly when the user requests a quit.
|
|
25
|
-
*
|
|
26
|
-
* Coverage:
|
|
27
|
-
* - terminal close / external kill (SIGHUP, SIGTERM)
|
|
28
|
-
* - normal Node shutdown via `beforeExit`
|
|
29
|
-
* - uncaught exceptions / unhandled rejections (best-effort terminal restore)
|
|
30
|
-
* - explicit quit from `useAbort` (calls the returned `shutdown` directly)
|
|
31
|
-
*
|
|
32
|
-
* SIGINT is intentionally NOT trapped: Ink owns Ctrl+C through the `useInput`
|
|
33
|
-
* hook (`exitOnCtrlC: false` in renderApp) and `useAbort` implements the
|
|
34
|
-
* double-press quit UX, calling the returned function on confirmation.
|
|
35
|
-
*
|
|
36
|
-
* The function is idempotent — concurrent invocations resolve to the same
|
|
37
|
-
* outcome and the handlers fire only once.
|
|
38
|
-
*
|
|
39
|
-
* `getRegistry` is a thunk so the shutdown handle can be created BEFORE the
|
|
40
|
-
* registry (the registry consumes `shutdown` in its plugin context, creating
|
|
41
|
-
* a cycle if both were eager).
|
|
42
|
-
*/
|
|
43
|
-
export function registerShutdown(getRegistry: () => PluginRegistry | null): ShutdownFn {
|
|
44
|
-
let shuttingDown: Promise<void> | null = null;
|
|
45
|
-
|
|
46
|
-
const shutdown: ShutdownFn = (code = 0) => {
|
|
47
|
-
if (shuttingDown) {
|
|
48
|
-
return shuttingDown;
|
|
49
|
-
}
|
|
50
|
-
shuttingDown = (async () => {
|
|
51
|
-
try {
|
|
52
|
-
const registry = getRegistry();
|
|
53
|
-
if (registry) {
|
|
54
|
-
await registry.shutdown();
|
|
55
|
-
}
|
|
56
|
-
} catch (err) {
|
|
57
|
-
console.error('Shutdown error:', err instanceof Error ? err.message : err);
|
|
58
|
-
} finally {
|
|
59
|
-
restoreTerminal();
|
|
60
|
-
// `process.exit` from inside an `async` function still terminates
|
|
61
|
-
// synchronously after the current microtask queue drains.
|
|
62
|
-
process.exit(code);
|
|
63
|
-
}
|
|
64
|
-
})();
|
|
65
|
-
return shuttingDown;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
if (!registered) {
|
|
69
|
-
registered = true;
|
|
70
|
-
process.once('SIGTERM', () => void shutdown(143));
|
|
71
|
-
process.once('SIGHUP', () => void shutdown(129));
|
|
72
|
-
process.once('beforeExit', (code) => void shutdown(code));
|
|
73
|
-
process.once('uncaughtException', (err) => {
|
|
74
|
-
restoreTerminal();
|
|
75
|
-
console.error(err);
|
|
76
|
-
process.exit(1);
|
|
77
|
-
});
|
|
78
|
-
process.once('unhandledRejection', (err) => {
|
|
79
|
-
restoreTerminal();
|
|
80
|
-
console.error(err);
|
|
81
|
-
process.exit(1);
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return shutdown;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function restoreTerminal(): void {
|
|
89
|
-
try {
|
|
90
|
-
process.stdout.write(`${DISABLE_MOUSE_MODE}${POP_KITTY_KEYBOARD}`);
|
|
91
|
-
} catch {
|
|
92
|
-
// stdout may already be closed during teardown — nothing to do.
|
|
93
|
-
}
|
|
94
|
-
}
|
package/src/app/startApp.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import type { PluginRegistry } from 'mu-core';
|
|
2
|
-
import { parseArgs, resolveInitialMessages } from '../cli/args';
|
|
3
|
-
import { handleSubcommand } from '../cli/subcommands';
|
|
4
|
-
import { loadConfig } from '../config/index';
|
|
5
|
-
import { createRegistry } from '../runtime/createRegistry';
|
|
6
|
-
import { checkForUpdatesInBackground } from '../runtime/startupUpdateCheck';
|
|
7
|
-
import { InkUIService } from '../tui/plugins/InkUIService';
|
|
8
|
-
import { registerShutdown } from './shutdown';
|
|
9
|
-
|
|
10
|
-
async function runApp(): Promise<void> {
|
|
11
|
-
if (await handleSubcommand()) return;
|
|
12
|
-
|
|
13
|
-
const cliArgs = parseArgs();
|
|
14
|
-
const config = loadConfig(cliArgs.model);
|
|
15
|
-
const uiService = new InkUIService();
|
|
16
|
-
|
|
17
|
-
// Create the shutdown handle BEFORE the registry so we can pass it into the
|
|
18
|
-
// plugin context. The registry is bound through a thunk, filled in once
|
|
19
|
-
// construction completes.
|
|
20
|
-
let registryRef: PluginRegistry | null = null;
|
|
21
|
-
const shutdown = registerShutdown(() => registryRef);
|
|
22
|
-
|
|
23
|
-
const initialMessages = resolveInitialMessages(cliArgs);
|
|
24
|
-
const { registry, channels } = await createRegistry({
|
|
25
|
-
cwd: process.cwd(),
|
|
26
|
-
config,
|
|
27
|
-
uiService,
|
|
28
|
-
initialMessages,
|
|
29
|
-
shutdown,
|
|
30
|
-
});
|
|
31
|
-
registryRef = registry;
|
|
32
|
-
|
|
33
|
-
// Fire-and-forget npm registry probe — surfaces a toast via uiService.notify
|
|
34
|
-
// if mu or an installed npm plugin has a newer version. Cached for 24h to
|
|
35
|
-
// avoid hammering the registry; disable with MU_NO_UPDATE_CHECK=1.
|
|
36
|
-
void checkForUpdatesInBackground(uiService);
|
|
37
|
-
|
|
38
|
-
// The TUI is registered as a `Channel` by `createCodingPlugin`. Starting
|
|
39
|
-
// it mounts Ink with the same options that were captured at activation
|
|
40
|
-
// time (config, initialMessages, registry, messageBus, uiService, shutdown).
|
|
41
|
-
await channels.startAll();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function startApp(): void {
|
|
45
|
-
runApp().catch((err) => {
|
|
46
|
-
console.error(err);
|
|
47
|
-
process.exit(1);
|
|
48
|
-
});
|
|
49
|
-
}
|