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,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
- }
@@ -1,66 +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
- maxFps: 60,
63
- incrementalRendering: true,
64
- },
65
- );
66
- }
@@ -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
- });
@@ -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 };
@@ -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
- };
@@ -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;