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.
Files changed (118) hide show
  1. package/README.md +9 -123
  2. package/bin/coding-agent.ts +95 -0
  3. package/package.json +10 -21
  4. package/src/config.ts +122 -0
  5. package/src/harness.test.ts +159 -0
  6. package/src/main.ts +53 -3
  7. package/src/plugins.ts +49 -0
  8. package/src/systemPrompt.ts +22 -0
  9. package/src/ui/ChatApp.ts +959 -0
  10. package/src/ui/commands.ts +35 -0
  11. package/src/ui/editor.ts +166 -0
  12. package/src/ui/markdown.ts +363 -0
  13. package/src/ui/picker.ts +126 -0
  14. package/src/ui/status.ts +61 -0
  15. package/src/ui/theme.ts +241 -0
  16. package/src/ui/transcript.test.ts +121 -0
  17. package/src/ui/transcript.ts +399 -0
  18. package/tsconfig.json +8 -0
  19. package/bin/mu.js +0 -2
  20. package/prompts/SYSTEM.md +0 -16
  21. package/src/app/shutdown.ts +0 -94
  22. package/src/app/startApp.ts +0 -49
  23. package/src/cli/args.ts +0 -133
  24. package/src/cli/install.ts +0 -107
  25. package/src/cli/subcommands.ts +0 -29
  26. package/src/cli/update.ts +0 -205
  27. package/src/config/index.test.ts +0 -77
  28. package/src/config/index.ts +0 -199
  29. package/src/plugin.ts +0 -124
  30. package/src/runtime/codingTools/bash.ts +0 -114
  31. package/src/runtime/codingTools/edit-file.ts +0 -60
  32. package/src/runtime/codingTools/index.ts +0 -39
  33. package/src/runtime/codingTools/read-file.ts +0 -83
  34. package/src/runtime/codingTools/utils.ts +0 -21
  35. package/src/runtime/codingTools/write-file.ts +0 -42
  36. package/src/runtime/createRegistry.test.ts +0 -147
  37. package/src/runtime/createRegistry.ts +0 -195
  38. package/src/runtime/fileMentionProvider.ts +0 -117
  39. package/src/runtime/messageBus.test.ts +0 -62
  40. package/src/runtime/messageBus.ts +0 -78
  41. package/src/runtime/pluginLoader.ts +0 -153
  42. package/src/runtime/startupUpdateCheck.ts +0 -163
  43. package/src/runtime/updateCheck.ts +0 -136
  44. package/src/sessions/index.test.ts +0 -66
  45. package/src/sessions/index.ts +0 -183
  46. package/src/sessions/peek.test.ts +0 -88
  47. package/src/sessions/project.ts +0 -51
  48. package/src/tui/channel/tuiChannel.test.ts +0 -107
  49. package/src/tui/channel/tuiChannel.ts +0 -62
  50. package/src/tui/chat/ChatContext.ts +0 -10
  51. package/src/tui/chat/MessageRendererContext.ts +0 -44
  52. package/src/tui/chat/ToolDisplayContext.ts +0 -33
  53. package/src/tui/chat/useAbort.ts +0 -85
  54. package/src/tui/chat/useAttachment.ts +0 -74
  55. package/src/tui/chat/useChat.ts +0 -113
  56. package/src/tui/chat/useChatPanel.ts +0 -119
  57. package/src/tui/chat/useChatSession.ts +0 -382
  58. package/src/tui/chat/useModels.ts +0 -83
  59. package/src/tui/chat/usePluginStatus.ts +0 -44
  60. package/src/tui/chat/useSessionPersistence.ts +0 -84
  61. package/src/tui/chat/useStatusSegments.ts +0 -82
  62. package/src/tui/chat/useSubagentBrowser.ts +0 -133
  63. package/src/tui/components/chat/ChatPanel.tsx +0 -54
  64. package/src/tui/components/chat/ChatPanelBody.tsx +0 -86
  65. package/src/tui/components/chat/Pickers.tsx +0 -44
  66. package/src/tui/components/chat/SubagentBrowserPanel.tsx +0 -145
  67. package/src/tui/components/messageView.tsx +0 -72
  68. package/src/tui/components/messages/EditOutput.tsx +0 -112
  69. package/src/tui/components/messages/ReadOutput.tsx +0 -48
  70. package/src/tui/components/messages/ToolHeader.tsx +0 -30
  71. package/src/tui/components/messages/WebFetchOutput.tsx +0 -30
  72. package/src/tui/components/messages/WriteOutput.tsx +0 -64
  73. package/src/tui/components/messages/assistantMessage.tsx +0 -72
  74. package/src/tui/components/messages/markdown.tsx +0 -407
  75. package/src/tui/components/messages/messageItem.tsx +0 -43
  76. package/src/tui/components/messages/reasoningBlock.tsx +0 -18
  77. package/src/tui/components/messages/streamingOutput.tsx +0 -18
  78. package/src/tui/components/messages/toolCallBlock.tsx +0 -125
  79. package/src/tui/components/messages/userMessage.tsx +0 -44
  80. package/src/tui/components/primitives/dropdown.tsx +0 -125
  81. package/src/tui/components/primitives/modal.tsx +0 -47
  82. package/src/tui/components/primitives/pickerModal.tsx +0 -47
  83. package/src/tui/components/primitives/scrollbar.tsx +0 -27
  84. package/src/tui/components/primitives/toast.tsx +0 -100
  85. package/src/tui/components/statusBar.tsx +0 -41
  86. package/src/tui/components/ui/dialogLayer.tsx +0 -175
  87. package/src/tui/context/ThemeContext.tsx +0 -18
  88. package/src/tui/hooks/useChordKeyboard.ts +0 -87
  89. package/src/tui/hooks/useInputInfoSegments.ts +0 -22
  90. package/src/tui/hooks/useScroll.ts +0 -64
  91. package/src/tui/hooks/useTerminal.ts +0 -40
  92. package/src/tui/hooks/useUI.ts +0 -15
  93. package/src/tui/input/InputBox.tsx +0 -6
  94. package/src/tui/input/InputBoxView.tsx +0 -293
  95. package/src/tui/input/commands.test.ts +0 -71
  96. package/src/tui/input/commands.ts +0 -55
  97. package/src/tui/input/cursor.test.ts +0 -136
  98. package/src/tui/input/cursor.ts +0 -214
  99. package/src/tui/input/dumpContext.ts +0 -107
  100. package/src/tui/input/sanitize.ts +0 -33
  101. package/src/tui/input/useCommandExecutor.ts +0 -32
  102. package/src/tui/input/useInputBox.ts +0 -265
  103. package/src/tui/input/useInputHandler.ts +0 -455
  104. package/src/tui/input/useMentionPicker.ts +0 -133
  105. package/src/tui/input/usePluginShortcuts.ts +0 -29
  106. package/src/tui/plugins/InkApprovalChannel.test.ts +0 -51
  107. package/src/tui/plugins/InkApprovalChannel.ts +0 -30
  108. package/src/tui/plugins/InkUIService.ts +0 -188
  109. package/src/tui/renderApp.tsx +0 -64
  110. package/src/tui/theme/index.ts +0 -1
  111. package/src/tui/theme/merge.test.ts +0 -49
  112. package/src/tui/theme/merge.ts +0 -43
  113. package/src/tui/theme/presets.ts +0 -90
  114. package/src/tui/theme/types.ts +0 -138
  115. package/src/tui/update/runUpdateInTui.ts +0 -127
  116. package/src/utils/clipboard.ts +0 -97
  117. package/src/utils/diff.test.ts +0 -56
  118. 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
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }
package/bin/mu.js DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env bun
2
- import '../src/main.ts';
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.
@@ -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
- }
@@ -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
- }