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