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.
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 -120
  57. package/src/tui/chat/useChatSession.ts +0 -384
  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 -85
  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 -66
  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
@@ -1,127 +0,0 @@
1
- /**
2
- * TUI-friendly update runner — same semantics as `mu update` from the CLI,
3
- * but never writes to stdout/stderr (Ink owns the terminal). Progress and
4
- * results are surfaced as toasts via `uiService.notify`.
5
- *
6
- * Mirrors `cli/update.ts` but uses `child_process.execFile` with `stdio:
7
- * 'pipe'` so subprocess output is buffered, not streamed to the TUI's tty.
8
- */
9
-
10
- import { execFile } from 'node:child_process';
11
- import { realpathSync } from 'node:fs';
12
- import { promisify } from 'node:util';
13
- import { ensureDataDir } from '../../cli/install';
14
- import { invalidateUpdateCheckCache } from '../../runtime/startupUpdateCheck';
15
- import { listConfiguredNpmPlugins, PACKAGE_NAME } from '../../runtime/updateCheck';
16
- import type { InkUIService } from '../plugins/InkUIService';
17
-
18
- const execFileAsync = promisify(execFile);
19
-
20
- export type UpdateScope = 'all' | 'plugins' | 'self';
21
-
22
- interface SelfInstallStrategy {
23
- manager: 'bun' | 'npm' | 'pnpm' | 'yarn' | 'unknown';
24
- command?: [string, string[]];
25
- }
26
-
27
- function detectSelfInstall(): SelfInstallStrategy {
28
- let bin = process.argv[1] ?? '';
29
- try {
30
- bin = realpathSync(bin);
31
- } catch {
32
- // keep raw argv
33
- }
34
- const norm = bin.replace(/\\/g, '/');
35
- if (norm.includes('/.bun/') || norm.includes('/bun/install/')) {
36
- return { manager: 'bun', command: ['bun', ['add', '-g', `${PACKAGE_NAME}@latest`]] };
37
- }
38
- if (norm.includes('/pnpm/')) {
39
- return { manager: 'pnpm', command: ['pnpm', ['add', '-g', `${PACKAGE_NAME}@latest`]] };
40
- }
41
- if (norm.includes('/.yarn/') || norm.includes('/yarn/global/')) {
42
- return { manager: 'yarn', command: ['yarn', ['global', 'add', `${PACKAGE_NAME}@latest`]] };
43
- }
44
- if (norm.includes('/npm/') || norm.includes('node_modules/.bin/mu')) {
45
- return { manager: 'npm', command: ['npm', ['i', '-g', `${PACKAGE_NAME}@latest`]] };
46
- }
47
- return { manager: 'unknown' };
48
- }
49
-
50
- async function updatePlugin(name: string, dataDir: string): Promise<boolean> {
51
- try {
52
- await execFileAsync('bun', ['update', '--latest', name], { cwd: dataDir });
53
- return true;
54
- } catch {
55
- return false;
56
- }
57
- }
58
-
59
- async function updatePlugins(ui: InkUIService): Promise<{ ok: number; failed: number; total: number }> {
60
- const dataDir = ensureDataDir();
61
- const names = listConfiguredNpmPlugins();
62
- if (names.length === 0) return { ok: 0, failed: 0, total: 0 };
63
-
64
- let ok = 0;
65
- let failed = 0;
66
- for (const name of names) {
67
- ui.notify(`Updating ${name}…`, 'info');
68
- if (await updatePlugin(name, dataDir)) ok += 1;
69
- else {
70
- failed += 1;
71
- ui.notify(`Failed to update ${name}`, 'error');
72
- }
73
- }
74
- return { ok, failed, total: names.length };
75
- }
76
-
77
- async function updateSelf(ui: InkUIService): Promise<boolean> {
78
- const strategy = detectSelfInstall();
79
- if (!strategy.command) {
80
- ui.notify(`Cannot auto-detect mu's installer. Re-install manually: bun add -g ${PACKAGE_NAME}@latest`, 'warning');
81
- return false;
82
- }
83
- const [bin, args] = strategy.command;
84
- ui.notify(`Updating mu via ${strategy.manager}…`, 'info');
85
- try {
86
- await execFileAsync(bin, args);
87
- ui.notify('mu updated. Restart your session to pick up the new version.', 'success');
88
- return true;
89
- } catch (err) {
90
- const message = err instanceof Error ? err.message.split('\n')[0] : String(err);
91
- ui.notify(`Failed to update mu: ${message}`, 'error');
92
- return false;
93
- }
94
- }
95
-
96
- /**
97
- * Entry point used by the `/update` slash command. Fire-and-forget — the
98
- * caller voids the returned promise. All progress / errors land in toasts.
99
- */
100
- export async function runUpdateInTui(scope: UpdateScope, ui: InkUIService): Promise<void> {
101
- if (scope === 'plugins') {
102
- const { ok, failed, total } = await updatePlugins(ui);
103
- if (total === 0) ui.notify('No npm plugins configured.', 'info');
104
- else if (failed === 0) ui.notify(`Updated ${ok}/${total} plugin${total === 1 ? '' : 's'}.`, 'success');
105
- else ui.notify(`Plugins: ${ok} updated, ${failed} failed.`, 'warning');
106
- invalidateUpdateCheckCache();
107
- return;
108
- }
109
-
110
- if (scope === 'self') {
111
- await updateSelf(ui);
112
- invalidateUpdateCheckCache();
113
- return;
114
- }
115
-
116
- // 'all'
117
- const plugins = await updatePlugins(ui);
118
- const selfOk = await updateSelf(ui);
119
- if (plugins.total === 0 && selfOk) {
120
- // mu-only success already toasted; nothing to add
121
- } else if (plugins.failed === 0 && selfOk) {
122
- ui.notify('Update complete.', 'success');
123
- } else {
124
- ui.notify('Update finished with errors — see prior messages.', 'warning');
125
- }
126
- invalidateUpdateCheckCache();
127
- }
@@ -1,97 +0,0 @@
1
- import { execFileSync, execSync } from 'node:child_process';
2
- import { existsSync, readFileSync, statSync, unlinkSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
- import type { ImageAttachment } from 'mu-core';
6
-
7
- const CLIPBOARD_TIMEOUT = 3000;
8
-
9
- function tryExecFile(file: string, args: string[]): boolean {
10
- try {
11
- execFileSync(file, args, { stdio: 'pipe', timeout: CLIPBOARD_TIMEOUT });
12
- return true;
13
- } catch {
14
- return false;
15
- }
16
- }
17
-
18
- function tryShell(command: string): boolean {
19
- try {
20
- execSync(command, { stdio: 'pipe', timeout: CLIPBOARD_TIMEOUT });
21
- return true;
22
- } catch {
23
- return false;
24
- }
25
- }
26
-
27
- function commandExists(name: string): boolean {
28
- return tryExecFile('which', [name]);
29
- }
30
-
31
- function extractMacImage(tmpFile: string): boolean {
32
- if (commandExists('pngpaste')) {
33
- return tryExecFile('pngpaste', [tmpFile]);
34
- }
35
- // Each `-e` is a discrete script line — passing as separate args avoids shell quoting traps.
36
- return tryExecFile('osascript', [
37
- '-e',
38
- 'tell application "System Events"',
39
- '-e',
40
- 'set imgData to the clipboard as «class PNGf»',
41
- '-e',
42
- `set fp to open for access POSIX file "${tmpFile}" with write permission`,
43
- '-e',
44
- 'write imgData to fp',
45
- '-e',
46
- 'close access fp',
47
- '-e',
48
- 'end tell',
49
- ]);
50
- }
51
-
52
- function extractLinuxImage(tmpFile: string): boolean {
53
- // xclip / wl-paste write to stdout; redirection still requires a shell.
54
- // tmpFile is constructed from tmpdir() + a numeric timestamp, so quoting is safe.
55
- if (tryShell(`xclip -selection clipboard -t image/png -o > "${tmpFile}"`)) {
56
- return true;
57
- }
58
- return tryShell(`wl-paste --type image/png > "${tmpFile}"`);
59
- }
60
-
61
- function extractPlatformImage(tmpFile: string): boolean {
62
- if (process.platform === 'darwin') {
63
- return extractMacImage(tmpFile);
64
- }
65
- if (process.platform === 'linux') {
66
- return extractLinuxImage(tmpFile);
67
- }
68
- return false;
69
- }
70
-
71
- function readTmpAsAttachment(tmpFile: string): ImageAttachment | null {
72
- if (!existsSync(tmpFile)) {
73
- return null;
74
- }
75
- if (statSync(tmpFile).size === 0) {
76
- unlinkSync(tmpFile);
77
- return null;
78
- }
79
- const buffer = readFileSync(tmpFile);
80
- unlinkSync(tmpFile);
81
- return { data: buffer.toString('base64'), mimeType: 'image/png', name: 'clipboard.png' };
82
- }
83
-
84
- export function readClipboardImage(): ImageAttachment | null {
85
- const tmpFile = join(tmpdir(), `mu-clip-${Date.now()}.png`);
86
- try {
87
- if (!extractPlatformImage(tmpFile)) {
88
- return null;
89
- }
90
- return readTmpAsAttachment(tmpFile);
91
- } catch {
92
- if (existsSync(tmpFile)) {
93
- unlinkSync(tmpFile);
94
- }
95
- return null;
96
- }
97
- }
@@ -1,56 +0,0 @@
1
- import { describe, expect, it } from 'bun:test';
2
- import { computeDiff, renderDiff } from './diff';
3
-
4
- describe('computeDiff', () => {
5
- it('emits only context lines when texts are identical', () => {
6
- const diff = computeDiff('a\nb\nc', 'a\nb\nc');
7
- expect(diff.lines.every((l) => l.type === 'context')).toBe(true);
8
- expect(diff.lines.find((l) => l.type === 'old')).toBeUndefined();
9
- expect(diff.lines.find((l) => l.type === 'new')).toBeUndefined();
10
- expect(diff.totalOldLines).toBe(3);
11
- expect(diff.totalNewLines).toBe(3);
12
- });
13
-
14
- it('detects an added line in the middle', () => {
15
- const diff = computeDiff('a\nb\nc', 'a\nx\nb\nc');
16
- const types = diff.lines.map((l) => l.type);
17
- expect(types).toContain('new');
18
- expect(diff.lines.find((l) => l.type === 'new')?.value).toBe('x');
19
- });
20
-
21
- it('detects a removed line', () => {
22
- const diff = computeDiff('a\nb\nc', 'a\nc');
23
- expect(diff.lines.find((l) => l.type === 'old')?.value).toBe('b');
24
- });
25
-
26
- it('returns no lines when either side exceeds the size limit', () => {
27
- const huge = Array.from({ length: 600 }, (_, i) => `line${i}`).join('\n');
28
- const diff = computeDiff(huge, huge.replace('line0', 'lineX'));
29
- expect(diff.lines).toEqual([]);
30
- expect(diff.totalOldLines).toBe(600);
31
- });
32
-
33
- it('emits up to CONTEXT_LINES of leading/trailing context', () => {
34
- const diff = computeDiff('a\nb\nc\nd\ne', 'a\nb\nc\nX\ne');
35
- const contextValues = diff.lines.filter((l) => l.type === 'context').map((l) => l.value);
36
- expect(contextValues).toContain('c');
37
- expect(contextValues).toContain('e');
38
- });
39
- });
40
-
41
- describe('renderDiff', () => {
42
- it('prefixes added/removed/context lines and reports truncation', () => {
43
- const diff = computeDiff('a\nb', 'a\nc');
44
- const { lines, truncated } = renderDiff(diff, 100);
45
- expect(lines.some((l) => l.startsWith('-'))).toBe(true);
46
- expect(lines.some((l) => l.startsWith('+'))).toBe(true);
47
- expect(truncated).toBe(false);
48
- });
49
-
50
- it('reports truncated when diff exceeds maxLines', () => {
51
- const diff = computeDiff('a', Array.from({ length: 50 }, (_, i) => `n${i}`).join('\n'));
52
- const { lines, truncated } = renderDiff(diff, 5);
53
- expect(lines).toHaveLength(5);
54
- expect(truncated).toBe(true);
55
- });
56
- });
package/src/utils/diff.ts DELETED
@@ -1,81 +0,0 @@
1
- // Lightweight diff for edit_file tool output.
2
- // Uses prefix/suffix matching — sufficient for small, localized edits.
3
-
4
- export interface DiffLine {
5
- type: 'context' | 'old' | 'new';
6
- value: string;
7
- }
8
-
9
- export interface DiffResult {
10
- lines: DiffLine[];
11
- totalOldLines: number;
12
- totalNewLines: number;
13
- }
14
-
15
- const CONTEXT_LINES = 3;
16
- const MAX_LINES = 500;
17
-
18
- export function computeDiff(oldText: string, newText: string): DiffResult {
19
- const oldLines = oldText.split('\n');
20
- const newLines = newText.split('\n');
21
-
22
- if (oldLines.length > MAX_LINES || newLines.length > MAX_LINES) {
23
- return { lines: [], totalOldLines: oldLines.length, totalNewLines: newLines.length };
24
- }
25
-
26
- // Find common prefix
27
- let prefixLen = 0;
28
- const minLen = Math.min(oldLines.length, newLines.length);
29
- while (prefixLen < minLen && oldLines[prefixLen] === newLines[prefixLen]) {
30
- prefixLen++;
31
- }
32
-
33
- // Find common suffix (don't overlap with prefix)
34
- let suffixLen = 0;
35
- const maxSuffix = Math.min(oldLines.length - prefixLen, newLines.length - prefixLen);
36
- while (
37
- suffixLen < maxSuffix &&
38
- oldLines[oldLines.length - 1 - suffixLen] === newLines[newLines.length - 1 - suffixLen]
39
- ) {
40
- suffixLen++;
41
- }
42
-
43
- const result: DiffLine[] = [];
44
-
45
- // Context from prefix (last N lines)
46
- const ctxStart = Math.max(0, prefixLen - CONTEXT_LINES);
47
- for (let i = ctxStart; i < prefixLen; i++) {
48
- result.push({ type: 'context', value: oldLines[i] });
49
- }
50
-
51
- // Removed lines
52
- for (let i = prefixLen; i < oldLines.length - suffixLen; i++) {
53
- result.push({ type: 'old', value: oldLines[i] });
54
- }
55
-
56
- // Added lines
57
- for (let i = prefixLen; i < newLines.length - suffixLen; i++) {
58
- result.push({ type: 'new', value: newLines[i] });
59
- }
60
-
61
- // Context from suffix (first N lines)
62
- const ctxEnd = Math.min(suffixLen, CONTEXT_LINES);
63
- for (let i = 0; i < ctxEnd; i++) {
64
- const idx = oldLines.length - suffixLen + i;
65
- result.push({ type: 'context', value: oldLines[idx] });
66
- }
67
-
68
- return { lines: result, totalOldLines: oldLines.length, totalNewLines: newLines.length };
69
- }
70
-
71
- export function renderDiff(diff: DiffResult, maxLines: number): { lines: string[]; truncated: boolean } {
72
- const result: string[] = [];
73
- const capped = diff.lines.slice(0, maxLines);
74
-
75
- for (const line of capped) {
76
- const prefix = line.type === 'old' ? '-' : line.type === 'new' ? '+' : ' ';
77
- result.push(`${prefix} ${line.value}`);
78
- }
79
-
80
- return { lines: result, truncated: diff.lines.length > maxLines };
81
- }