mu-coding 0.13.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -123
- package/bin/coding-agent.ts +95 -0
- package/package.json +10 -21
- package/src/config.ts +122 -0
- package/src/harness.test.ts +159 -0
- package/src/main.ts +53 -3
- package/src/plugins.ts +49 -0
- package/src/systemPrompt.ts +22 -0
- package/src/ui/ChatApp.ts +959 -0
- package/src/ui/commands.ts +35 -0
- package/src/ui/editor.ts +166 -0
- package/src/ui/markdown.ts +363 -0
- package/src/ui/picker.ts +126 -0
- package/src/ui/status.ts +61 -0
- package/src/ui/theme.ts +241 -0
- package/src/ui/transcript.test.ts +121 -0
- package/src/ui/transcript.ts +399 -0
- package/tsconfig.json +8 -0
- package/bin/mu.js +0 -2
- package/prompts/SYSTEM.md +0 -16
- package/src/app/shutdown.ts +0 -94
- package/src/app/startApp.ts +0 -49
- package/src/cli/args.ts +0 -133
- package/src/cli/install.ts +0 -107
- package/src/cli/subcommands.ts +0 -29
- package/src/cli/update.ts +0 -205
- package/src/config/index.test.ts +0 -77
- package/src/config/index.ts +0 -199
- package/src/plugin.ts +0 -124
- package/src/runtime/codingTools/bash.ts +0 -114
- package/src/runtime/codingTools/edit-file.ts +0 -60
- package/src/runtime/codingTools/index.ts +0 -39
- package/src/runtime/codingTools/read-file.ts +0 -83
- package/src/runtime/codingTools/utils.ts +0 -21
- package/src/runtime/codingTools/write-file.ts +0 -42
- package/src/runtime/createRegistry.test.ts +0 -147
- package/src/runtime/createRegistry.ts +0 -195
- package/src/runtime/fileMentionProvider.ts +0 -117
- package/src/runtime/messageBus.test.ts +0 -62
- package/src/runtime/messageBus.ts +0 -78
- package/src/runtime/pluginLoader.ts +0 -153
- package/src/runtime/startupUpdateCheck.ts +0 -163
- package/src/runtime/updateCheck.ts +0 -136
- package/src/sessions/index.test.ts +0 -66
- package/src/sessions/index.ts +0 -183
- package/src/sessions/peek.test.ts +0 -88
- package/src/sessions/project.ts +0 -51
- package/src/tui/channel/tuiChannel.test.ts +0 -107
- package/src/tui/channel/tuiChannel.ts +0 -62
- package/src/tui/chat/ChatContext.ts +0 -10
- package/src/tui/chat/MessageRendererContext.ts +0 -44
- package/src/tui/chat/ToolDisplayContext.ts +0 -33
- package/src/tui/chat/useAbort.ts +0 -85
- package/src/tui/chat/useAttachment.ts +0 -74
- package/src/tui/chat/useChat.ts +0 -113
- package/src/tui/chat/useChatPanel.ts +0 -119
- package/src/tui/chat/useChatSession.ts +0 -382
- package/src/tui/chat/useModels.ts +0 -83
- package/src/tui/chat/usePluginStatus.ts +0 -44
- package/src/tui/chat/useSessionPersistence.ts +0 -84
- package/src/tui/chat/useStatusSegments.ts +0 -82
- package/src/tui/chat/useSubagentBrowser.ts +0 -133
- package/src/tui/components/chat/ChatPanel.tsx +0 -54
- package/src/tui/components/chat/ChatPanelBody.tsx +0 -86
- package/src/tui/components/chat/Pickers.tsx +0 -44
- package/src/tui/components/chat/SubagentBrowserPanel.tsx +0 -145
- package/src/tui/components/messageView.tsx +0 -72
- package/src/tui/components/messages/EditOutput.tsx +0 -112
- package/src/tui/components/messages/ReadOutput.tsx +0 -48
- package/src/tui/components/messages/ToolHeader.tsx +0 -30
- package/src/tui/components/messages/WebFetchOutput.tsx +0 -30
- package/src/tui/components/messages/WriteOutput.tsx +0 -64
- package/src/tui/components/messages/assistantMessage.tsx +0 -72
- package/src/tui/components/messages/markdown.tsx +0 -407
- package/src/tui/components/messages/messageItem.tsx +0 -43
- package/src/tui/components/messages/reasoningBlock.tsx +0 -18
- package/src/tui/components/messages/streamingOutput.tsx +0 -18
- package/src/tui/components/messages/toolCallBlock.tsx +0 -125
- package/src/tui/components/messages/userMessage.tsx +0 -44
- package/src/tui/components/primitives/dropdown.tsx +0 -125
- package/src/tui/components/primitives/modal.tsx +0 -47
- package/src/tui/components/primitives/pickerModal.tsx +0 -47
- package/src/tui/components/primitives/scrollbar.tsx +0 -27
- package/src/tui/components/primitives/toast.tsx +0 -100
- package/src/tui/components/statusBar.tsx +0 -41
- package/src/tui/components/ui/dialogLayer.tsx +0 -175
- package/src/tui/context/ThemeContext.tsx +0 -18
- package/src/tui/hooks/useChordKeyboard.ts +0 -87
- package/src/tui/hooks/useInputInfoSegments.ts +0 -22
- package/src/tui/hooks/useScroll.ts +0 -64
- package/src/tui/hooks/useTerminal.ts +0 -40
- package/src/tui/hooks/useUI.ts +0 -15
- package/src/tui/input/InputBox.tsx +0 -6
- package/src/tui/input/InputBoxView.tsx +0 -293
- package/src/tui/input/commands.test.ts +0 -71
- package/src/tui/input/commands.ts +0 -55
- package/src/tui/input/cursor.test.ts +0 -136
- package/src/tui/input/cursor.ts +0 -214
- package/src/tui/input/dumpContext.ts +0 -107
- package/src/tui/input/sanitize.ts +0 -33
- package/src/tui/input/useCommandExecutor.ts +0 -32
- package/src/tui/input/useInputBox.ts +0 -265
- package/src/tui/input/useInputHandler.ts +0 -455
- package/src/tui/input/useMentionPicker.ts +0 -133
- package/src/tui/input/usePluginShortcuts.ts +0 -29
- package/src/tui/plugins/InkApprovalChannel.test.ts +0 -51
- package/src/tui/plugins/InkApprovalChannel.ts +0 -30
- package/src/tui/plugins/InkUIService.ts +0 -188
- package/src/tui/renderApp.tsx +0 -64
- package/src/tui/theme/index.ts +0 -1
- package/src/tui/theme/merge.test.ts +0 -49
- package/src/tui/theme/merge.ts +0 -43
- package/src/tui/theme/presets.ts +0 -90
- package/src/tui/theme/types.ts +0 -138
- package/src/tui/update/runUpdateInTui.ts +0 -127
- package/src/utils/clipboard.ts +0 -97
- package/src/utils/diff.test.ts +0 -56
- package/src/utils/diff.ts +0 -81
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import type { UIService } from 'mu-core';
|
|
2
|
-
|
|
3
|
-
export type DialogType = 'confirm' | 'select' | 'input';
|
|
4
|
-
|
|
5
|
-
export interface DialogRequest {
|
|
6
|
-
id: number;
|
|
7
|
-
type: DialogType;
|
|
8
|
-
title: string;
|
|
9
|
-
message?: string;
|
|
10
|
-
options?: string[];
|
|
11
|
-
placeholder?: string;
|
|
12
|
-
resolve: (value: unknown) => void;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface ToastRequest {
|
|
16
|
-
message: string;
|
|
17
|
-
level: 'info' | 'success' | 'warning' | 'error';
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
let nextDialogId = 0;
|
|
21
|
-
|
|
22
|
-
type ToastListener = (toast: ToastRequest) => void;
|
|
23
|
-
type StatusListener = (entries: Map<string, string>) => void;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* InkUIService bridges plugin UI requests with Ink's React rendering.
|
|
27
|
-
*
|
|
28
|
-
* All event channels (dialogs, toasts, status) follow the same multi-listener
|
|
29
|
-
* pattern: `subscribe`/`onToast`/`onStatusChange` return an unsubscribe
|
|
30
|
-
* function. This lets multiple components observe the same service safely
|
|
31
|
-
* (e.g. during hot-reload or component swaps) without one handler clobbering
|
|
32
|
-
* another.
|
|
33
|
-
*
|
|
34
|
-
* Toasts emitted before any listener subscribes are buffered and replayed
|
|
35
|
-
* to the first subscriber; this avoids losing plugin-load errors emitted
|
|
36
|
-
* before the TUI mounts.
|
|
37
|
-
*
|
|
38
|
-
* Implements `UIService` from `mu-agents` — gives nominal typing so a
|
|
39
|
-
* change on either side fails the build.
|
|
40
|
-
*/
|
|
41
|
-
export class InkUIService implements UIService {
|
|
42
|
-
private dialogQueue: DialogRequest[] = [];
|
|
43
|
-
private dialogSubscribers: Set<() => void> = new Set();
|
|
44
|
-
private toastListeners: Set<ToastListener> = new Set();
|
|
45
|
-
private pendingToasts: ToastRequest[] = [];
|
|
46
|
-
private statusMap: Map<string, string> = new Map();
|
|
47
|
-
private statusListeners: Set<StatusListener> = new Set();
|
|
48
|
-
|
|
49
|
-
// ─── Dialog Subscription (used by DialogLayer) ──────────────────────────
|
|
50
|
-
|
|
51
|
-
subscribe(fn: () => void): () => void {
|
|
52
|
-
this.dialogSubscribers.add(fn);
|
|
53
|
-
return () => {
|
|
54
|
-
this.dialogSubscribers.delete(fn);
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
private notifyDialogSubscribers(): void {
|
|
59
|
-
for (const fn of this.dialogSubscribers) {
|
|
60
|
-
fn();
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Get the current dialog at the front of the queue */
|
|
65
|
-
currentDialog(): DialogRequest | null {
|
|
66
|
-
return this.dialogQueue[0] ?? null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Resolve the current dialog and advance the queue */
|
|
70
|
-
resolveDialog(value: unknown): void {
|
|
71
|
-
const dialog = this.dialogQueue.shift();
|
|
72
|
-
if (dialog) {
|
|
73
|
-
dialog.resolve(value);
|
|
74
|
-
this.notifyDialogSubscribers();
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Cancel/dismiss the current dialog */
|
|
79
|
-
cancelDialog(): void {
|
|
80
|
-
const dialog = this.dialogQueue.shift();
|
|
81
|
-
if (dialog) {
|
|
82
|
-
dialog.resolve(dialog.type === 'confirm' ? false : null);
|
|
83
|
-
this.notifyDialogSubscribers();
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ─── Toast ──────────────────────────────────────────────────────────────
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Subscribe to toast events. Returns an unsubscribe function. If toasts
|
|
91
|
-
* were emitted before any listener attached, they are replayed to the
|
|
92
|
-
* first subscriber once.
|
|
93
|
-
*/
|
|
94
|
-
onToast(callback: ToastListener): () => void {
|
|
95
|
-
this.toastListeners.add(callback);
|
|
96
|
-
if (this.pendingToasts.length > 0) {
|
|
97
|
-
const buffered = this.pendingToasts;
|
|
98
|
-
this.pendingToasts = [];
|
|
99
|
-
for (const toast of buffered) {
|
|
100
|
-
callback(toast);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return () => {
|
|
104
|
-
this.toastListeners.delete(callback);
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// ─── Status ─────────────────────────────────────────────────────────────
|
|
109
|
-
|
|
110
|
-
onStatusChange(callback: StatusListener): () => void {
|
|
111
|
-
this.statusListeners.add(callback);
|
|
112
|
-
return () => {
|
|
113
|
-
this.statusListeners.delete(callback);
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
private emitStatus(): void {
|
|
118
|
-
for (const fn of this.statusListeners) {
|
|
119
|
-
fn(this.statusMap);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
getStatusEntries(): Map<string, string> {
|
|
124
|
-
return new Map(this.statusMap);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// ─── Plugin UI Methods ──────────────────────────────────────────────────
|
|
128
|
-
|
|
129
|
-
notify(message: string, level?: 'info' | 'success' | 'warning' | 'error'): void {
|
|
130
|
-
const toast: ToastRequest = { message, level: level ?? 'info' };
|
|
131
|
-
if (this.toastListeners.size === 0) {
|
|
132
|
-
this.pendingToasts.push(toast);
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
for (const listener of this.toastListeners) {
|
|
136
|
-
listener(toast);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
confirm(title: string, message: string): Promise<boolean> {
|
|
141
|
-
return new Promise((resolve) => {
|
|
142
|
-
this.dialogQueue.push({
|
|
143
|
-
id: nextDialogId++,
|
|
144
|
-
type: 'confirm',
|
|
145
|
-
title,
|
|
146
|
-
message,
|
|
147
|
-
resolve: resolve as (value: unknown) => void,
|
|
148
|
-
});
|
|
149
|
-
this.notifyDialogSubscribers();
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
select(title: string, options: string[]): Promise<string | null> {
|
|
154
|
-
return new Promise((resolve) => {
|
|
155
|
-
this.dialogQueue.push({
|
|
156
|
-
id: nextDialogId++,
|
|
157
|
-
type: 'select',
|
|
158
|
-
title,
|
|
159
|
-
options,
|
|
160
|
-
resolve: resolve as (value: unknown) => void,
|
|
161
|
-
});
|
|
162
|
-
this.notifyDialogSubscribers();
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
input(title: string, placeholder?: string): Promise<string | null> {
|
|
167
|
-
return new Promise((resolve) => {
|
|
168
|
-
this.dialogQueue.push({
|
|
169
|
-
id: nextDialogId++,
|
|
170
|
-
type: 'input',
|
|
171
|
-
title,
|
|
172
|
-
placeholder,
|
|
173
|
-
resolve: resolve as (value: unknown) => void,
|
|
174
|
-
});
|
|
175
|
-
this.notifyDialogSubscribers();
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
setStatus(key: string, text: string): void {
|
|
180
|
-
this.statusMap.set(key, text);
|
|
181
|
-
this.emitStatus();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
clearStatus(key: string): void {
|
|
185
|
-
this.statusMap.delete(key);
|
|
186
|
-
this.emitStatus();
|
|
187
|
-
}
|
|
188
|
-
}
|
package/src/tui/renderApp.tsx
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { type Instance, render } from 'ink';
|
|
2
|
-
import { type SubagentRunRegistry, SubagentRunsProvider } from 'mu-agents';
|
|
3
|
-
import type { ChatMessage, PluginRegistry } from 'mu-core';
|
|
4
|
-
import type { ReactNode } from 'react';
|
|
5
|
-
import type { ShutdownFn } from '../app/shutdown';
|
|
6
|
-
import type { AppConfig } from '../config/index';
|
|
7
|
-
import type { SessionPathHolder } from '../runtime/createRegistry';
|
|
8
|
-
import type { HostMessageBus } from '../runtime/messageBus';
|
|
9
|
-
import { ChatPanel } from './components/chat/ChatPanel';
|
|
10
|
-
import { ThemeProvider } from './context/ThemeContext';
|
|
11
|
-
import type { InkUIService } from './plugins/InkUIService';
|
|
12
|
-
import { resolveTheme } from './theme';
|
|
13
|
-
|
|
14
|
-
interface RenderAppOptions {
|
|
15
|
-
config: AppConfig;
|
|
16
|
-
initialMessages?: ChatMessage[];
|
|
17
|
-
registry: PluginRegistry;
|
|
18
|
-
messageBus: HostMessageBus;
|
|
19
|
-
uiService: InkUIService;
|
|
20
|
-
shutdown: ShutdownFn;
|
|
21
|
-
sessionPathHolder?: SessionPathHolder;
|
|
22
|
-
subagentRuns?: SubagentRunRegistry;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Optionally wrap children with the subagent-runs provider so the
|
|
27
|
-
* `↳ subagent` header renderer can subscribe to live status updates.
|
|
28
|
-
* Wrapping is conditional because hosts that disabled the agent plugin
|
|
29
|
-
* have no registry to provide.
|
|
30
|
-
*/
|
|
31
|
-
function withSubagentProvider(runs: SubagentRunRegistry | undefined, children: ReactNode): ReactNode {
|
|
32
|
-
if (!runs) return <>{children}</>;
|
|
33
|
-
return <SubagentRunsProvider registry={runs}>{children}</SubagentRunsProvider>;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Renders the chat TUI and returns the Ink `Instance` so callers (the TUI
|
|
38
|
-
* channel, tests) can unmount it explicitly. Ink stays mounted until the
|
|
39
|
-
* caller invokes `instance.unmount()` or the process exits.
|
|
40
|
-
*/
|
|
41
|
-
export function renderApp(options: RenderAppOptions): Instance {
|
|
42
|
-
const theme = resolveTheme(options.config.theme);
|
|
43
|
-
return render(
|
|
44
|
-
<ThemeProvider theme={theme}>
|
|
45
|
-
{withSubagentProvider(
|
|
46
|
-
options.subagentRuns,
|
|
47
|
-
<ChatPanel
|
|
48
|
-
config={options.config}
|
|
49
|
-
initialMessages={options.initialMessages}
|
|
50
|
-
registry={options.registry}
|
|
51
|
-
messageBus={options.messageBus}
|
|
52
|
-
uiService={options.uiService}
|
|
53
|
-
shutdown={options.shutdown}
|
|
54
|
-
sessionPathHolder={options.sessionPathHolder}
|
|
55
|
-
subagentRuns={options.subagentRuns}
|
|
56
|
-
/>,
|
|
57
|
-
)}
|
|
58
|
-
</ThemeProvider>,
|
|
59
|
-
{
|
|
60
|
-
exitOnCtrlC: false,
|
|
61
|
-
kittyKeyboard: { mode: 'enabled' },
|
|
62
|
-
},
|
|
63
|
-
);
|
|
64
|
-
}
|
package/src/tui/theme/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { resolveTheme } from './merge';
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'bun:test';
|
|
2
|
-
import { mergeTheme, resolveTheme } from './merge';
|
|
3
|
-
import { DEFAULT_THEME } from './presets';
|
|
4
|
-
|
|
5
|
-
describe('mergeTheme', () => {
|
|
6
|
-
it('returns the base theme untouched when no override is provided', () => {
|
|
7
|
-
expect(mergeTheme(DEFAULT_THEME)).toBe(DEFAULT_THEME);
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it('merges leaf overrides without dropping siblings', () => {
|
|
11
|
-
const merged = mergeTheme(DEFAULT_THEME, { input: { cursor: '#ff00ff' } });
|
|
12
|
-
expect(merged.input.cursor).toBe('#ff00ff');
|
|
13
|
-
expect(merged.input.background).toBe(DEFAULT_THEME.input.background);
|
|
14
|
-
expect(merged.user).toEqual(DEFAULT_THEME.user);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('does not mutate the base theme', () => {
|
|
18
|
-
const before = DEFAULT_THEME.input.cursor;
|
|
19
|
-
mergeTheme(DEFAULT_THEME, { input: { cursor: '#000000' } });
|
|
20
|
-
expect(DEFAULT_THEME.input.cursor).toBe(before);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('ignores non-object sections defensively', () => {
|
|
24
|
-
// biome-ignore lint/suspicious/noExplicitAny: testing malformed user input on purpose
|
|
25
|
-
const merged = mergeTheme(DEFAULT_THEME, { input: 'red' as any });
|
|
26
|
-
expect(merged.input).toEqual(DEFAULT_THEME.input);
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe('resolveTheme', () => {
|
|
31
|
-
it('returns the default theme when config is undefined', () => {
|
|
32
|
-
expect(resolveTheme(undefined)).toBe(DEFAULT_THEME);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('applies overrides on top of the default theme', () => {
|
|
36
|
-
const theme = resolveTheme({ user: { border: 'magenta' } });
|
|
37
|
-
expect(theme.user.border).toBe('magenta');
|
|
38
|
-
expect(theme.input).toEqual(DEFAULT_THEME.input);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('returns default for non-object input', () => {
|
|
42
|
-
// biome-ignore lint/suspicious/noExplicitAny: malformed value
|
|
43
|
-
expect(resolveTheme(42 as any)).toBe(DEFAULT_THEME);
|
|
44
|
-
// biome-ignore lint/suspicious/noExplicitAny: malformed value
|
|
45
|
-
expect(resolveTheme(null as any)).toBe(DEFAULT_THEME);
|
|
46
|
-
// biome-ignore lint/suspicious/noExplicitAny: string presets are no longer supported
|
|
47
|
-
expect(resolveTheme('dark' as any)).toBe(DEFAULT_THEME);
|
|
48
|
-
});
|
|
49
|
-
});
|
package/src/tui/theme/merge.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_THEME } from './presets';
|
|
2
|
-
import type { PartialTheme, Theme, ThemeConfig } from './types';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Two-level deep merge tailored to the `Theme` shape: each top-level key maps
|
|
6
|
-
* to an object of color leaves (strings), so we never need recursion beyond
|
|
7
|
-
* one nesting level. Keeping it flat avoids accidentally merging into nested
|
|
8
|
-
* structures users haven't opted into and also avoids `any`/recursion that
|
|
9
|
-
* would trip Biome's complexity rules.
|
|
10
|
-
*/
|
|
11
|
-
function mergeTheme(base: Theme, override?: PartialTheme): Theme {
|
|
12
|
-
if (!override) return base;
|
|
13
|
-
const out: Theme = { ...base };
|
|
14
|
-
const keys = Object.keys(override) as (keyof Theme)[];
|
|
15
|
-
for (const key of keys) {
|
|
16
|
-
mergeSection(out, base, override, key);
|
|
17
|
-
}
|
|
18
|
-
return out;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function mergeSection<K extends keyof Theme>(out: Theme, base: Theme, override: PartialTheme, key: K): void {
|
|
22
|
-
const section = override[key];
|
|
23
|
-
if (section && typeof section === 'object') {
|
|
24
|
-
out[key] = { ...base[key], ...section };
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
29
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Resolve the user-supplied `theme` field from config.json into a fully
|
|
34
|
-
* populated `Theme`. Tolerates malformed input (wrong type) by silently
|
|
35
|
-
* falling back to the default — config corruption should never crash the TUI.
|
|
36
|
-
*/
|
|
37
|
-
export function resolveTheme(config: ThemeConfig | undefined): Theme {
|
|
38
|
-
if (config === undefined) return DEFAULT_THEME;
|
|
39
|
-
if (!isPlainObject(config)) return DEFAULT_THEME;
|
|
40
|
-
return mergeTheme(DEFAULT_THEME, config as PartialTheme);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export { mergeTheme };
|
package/src/tui/theme/presets.ts
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import type { Theme } from './types';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The single built-in theme. Reproduces the exact hard-coded colors that
|
|
5
|
-
* lived inline in the components prior to the theme extraction — using it
|
|
6
|
-
* must be a visual no-op. Users may still override individual leaves via
|
|
7
|
-
* the `theme` field in their config.
|
|
8
|
-
*/
|
|
9
|
-
export const DEFAULT_THEME: Theme = {
|
|
10
|
-
input: {
|
|
11
|
-
background: '#222222',
|
|
12
|
-
text: 'white',
|
|
13
|
-
cursor: 'white',
|
|
14
|
-
commandHighlight: 'green',
|
|
15
|
-
footerHint: 'gray',
|
|
16
|
-
attachmentName: 'cyan',
|
|
17
|
-
attachmentError: 'red',
|
|
18
|
-
modelLabel: 'white',
|
|
19
|
-
},
|
|
20
|
-
user: {
|
|
21
|
-
background: '#1a1a1a',
|
|
22
|
-
border: 'yellow',
|
|
23
|
-
attachment: 'cyan',
|
|
24
|
-
},
|
|
25
|
-
assistant: {
|
|
26
|
-
text: 'white',
|
|
27
|
-
},
|
|
28
|
-
tool: {
|
|
29
|
-
success: 'green',
|
|
30
|
-
error: 'red',
|
|
31
|
-
previewBackground: '#2a2a2a',
|
|
32
|
-
previewText: 'white',
|
|
33
|
-
summaryDim: 'gray',
|
|
34
|
-
warning: 'yellow',
|
|
35
|
-
},
|
|
36
|
-
reasoning: {
|
|
37
|
-
title: 'yellow',
|
|
38
|
-
body: 'gray',
|
|
39
|
-
},
|
|
40
|
-
modal: {
|
|
41
|
-
background: '#1a1a1a',
|
|
42
|
-
hint: 'gray',
|
|
43
|
-
},
|
|
44
|
-
toast: {
|
|
45
|
-
background: '#1a1a1a',
|
|
46
|
-
defaultColor: 'green',
|
|
47
|
-
closeHint: 'gray',
|
|
48
|
-
},
|
|
49
|
-
dropdown: {
|
|
50
|
-
selected: 'green',
|
|
51
|
-
description: 'gray',
|
|
52
|
-
placeholder: 'gray',
|
|
53
|
-
cursor: 'white',
|
|
54
|
-
empty: 'gray',
|
|
55
|
-
},
|
|
56
|
-
dialog: {
|
|
57
|
-
confirmYes: 'green',
|
|
58
|
-
confirmNo: 'red',
|
|
59
|
-
hint: 'gray',
|
|
60
|
-
cursor: 'white',
|
|
61
|
-
placeholder: 'gray',
|
|
62
|
-
},
|
|
63
|
-
diff: {
|
|
64
|
-
added: 'green',
|
|
65
|
-
removed: 'red',
|
|
66
|
-
context: 'gray',
|
|
67
|
-
warning: 'yellow',
|
|
68
|
-
},
|
|
69
|
-
status: {
|
|
70
|
-
separator: 'gray',
|
|
71
|
-
},
|
|
72
|
-
markdown: {
|
|
73
|
-
heading: 'cyan',
|
|
74
|
-
codeBackground: '#2a2a2a',
|
|
75
|
-
codeText: 'yellow',
|
|
76
|
-
codeBlockBackground: '#2a2a2a',
|
|
77
|
-
codeBlockText: 'white',
|
|
78
|
-
link: 'cyan',
|
|
79
|
-
blockquote: 'gray',
|
|
80
|
-
bullet: 'cyan',
|
|
81
|
-
tableBorder: 'gray',
|
|
82
|
-
},
|
|
83
|
-
common: {
|
|
84
|
-
error: 'red',
|
|
85
|
-
warning: 'yellow',
|
|
86
|
-
success: 'green',
|
|
87
|
-
accent: 'cyan',
|
|
88
|
-
info: 'blue',
|
|
89
|
-
},
|
|
90
|
-
};
|
package/src/tui/theme/types.ts
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
// Theme type definitions. Kept free of any Ink import so the type can travel
|
|
2
|
-
// to `config/index.ts` without dragging the renderer into the config layer.
|
|
3
|
-
//
|
|
4
|
-
// Color values are plain strings: either an Ink-supported color name
|
|
5
|
-
// ("red", "cyan", "yellow"...) or a hex code ("#1a1a1a"). All optional fields
|
|
6
|
-
// in `PartialTheme` mirror this so users can override one leaf at a time
|
|
7
|
-
// without having to redeclare a full theme.
|
|
8
|
-
|
|
9
|
-
interface ThemeInput {
|
|
10
|
-
background: string;
|
|
11
|
-
text: string;
|
|
12
|
-
cursor: string;
|
|
13
|
-
commandHighlight: string;
|
|
14
|
-
footerHint: string;
|
|
15
|
-
attachmentName: string;
|
|
16
|
-
attachmentError: string;
|
|
17
|
-
modelLabel: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface ThemeUser {
|
|
21
|
-
background: string;
|
|
22
|
-
border: string;
|
|
23
|
-
attachment: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface ThemeAssistant {
|
|
27
|
-
text: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface ThemeTool {
|
|
31
|
-
success: string;
|
|
32
|
-
error: string;
|
|
33
|
-
previewBackground: string;
|
|
34
|
-
previewText: string;
|
|
35
|
-
summaryDim: string;
|
|
36
|
-
warning: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface ThemeReasoning {
|
|
40
|
-
title: string;
|
|
41
|
-
body: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface ThemeModal {
|
|
45
|
-
background: string;
|
|
46
|
-
hint: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
interface ThemeToast {
|
|
50
|
-
background: string;
|
|
51
|
-
defaultColor: string;
|
|
52
|
-
closeHint: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
interface ThemeDropdown {
|
|
56
|
-
selected: string;
|
|
57
|
-
description: string;
|
|
58
|
-
placeholder: string;
|
|
59
|
-
cursor: string;
|
|
60
|
-
empty: string;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
interface ThemeDialog {
|
|
64
|
-
confirmYes: string;
|
|
65
|
-
confirmNo: string;
|
|
66
|
-
hint: string;
|
|
67
|
-
cursor: string;
|
|
68
|
-
placeholder: string;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
interface ThemeDiff {
|
|
72
|
-
added: string;
|
|
73
|
-
removed: string;
|
|
74
|
-
context: string;
|
|
75
|
-
warning: string;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
interface ThemeStatus {
|
|
79
|
-
separator: string;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
interface ThemeMarkdown {
|
|
83
|
-
/** Heading text color (h1/h2/h3 share this color, h1 is also bold). */
|
|
84
|
-
heading: string;
|
|
85
|
-
/** Inline code background. */
|
|
86
|
-
codeBackground: string;
|
|
87
|
-
/** Inline code text color. */
|
|
88
|
-
codeText: string;
|
|
89
|
-
/** Fenced code-block background. */
|
|
90
|
-
codeBlockBackground: string;
|
|
91
|
-
/** Fenced code-block text color. */
|
|
92
|
-
codeBlockText: string;
|
|
93
|
-
/** Link `[label](url)` rendering — label color. */
|
|
94
|
-
link: string;
|
|
95
|
-
/** Blockquote (`> …`) text color (also dimmed). */
|
|
96
|
-
blockquote: string;
|
|
97
|
-
/** List bullet color. */
|
|
98
|
-
bullet: string;
|
|
99
|
-
/** Table border color. */
|
|
100
|
-
tableBorder: string;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
interface ThemeCommon {
|
|
104
|
-
error: string;
|
|
105
|
-
warning: string;
|
|
106
|
-
success: string;
|
|
107
|
-
accent: string;
|
|
108
|
-
info: string;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export interface Theme {
|
|
112
|
-
input: ThemeInput;
|
|
113
|
-
user: ThemeUser;
|
|
114
|
-
assistant: ThemeAssistant;
|
|
115
|
-
tool: ThemeTool;
|
|
116
|
-
reasoning: ThemeReasoning;
|
|
117
|
-
modal: ThemeModal;
|
|
118
|
-
toast: ThemeToast;
|
|
119
|
-
dropdown: ThemeDropdown;
|
|
120
|
-
dialog: ThemeDialog;
|
|
121
|
-
diff: ThemeDiff;
|
|
122
|
-
status: ThemeStatus;
|
|
123
|
-
markdown: ThemeMarkdown;
|
|
124
|
-
common: ThemeCommon;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
type DeepPartial<T> = {
|
|
128
|
-
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
export type PartialTheme = DeepPartial<Theme>;
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Shape stored in `~/.config/mu/config.json` under the `theme` key. An object
|
|
135
|
-
* with per-leaf overrides on top of the default theme. Kept loose on purpose:
|
|
136
|
-
* malformed input falls back to the default theme rather than throwing.
|
|
137
|
-
*/
|
|
138
|
-
export type ThemeConfig = PartialTheme;
|
|
@@ -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
|
-
}
|