mu-coding 0.8.0 → 0.10.0
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/package.json +4 -4
- package/src/cli/install.ts +18 -3
- package/src/plugin.ts +33 -5
- package/src/runtime/createRegistry.test.ts +4 -3
- package/src/runtime/createRegistry.ts +34 -2
- package/src/runtime/fileMentionProvider.ts +117 -0
- package/src/runtime/pluginLoader.ts +37 -6
- package/src/tui/channel/tuiChannel.ts +14 -1
- package/src/tui/chat/useAbort.ts +5 -0
- package/src/tui/chat/useChat.ts +7 -0
- package/src/tui/chat/useChatPanel.ts +24 -3
- package/src/tui/chat/useChatSession.ts +105 -7
- package/src/tui/chat/useModels.ts +25 -1
- package/src/tui/chat/useSessionPersistence.ts +27 -11
- package/src/tui/chat/useStatusSegments.ts +26 -6
- package/src/tui/chat/useSubagentBrowser.ts +133 -0
- package/src/tui/components/chat/ChatPanel.tsx +16 -1
- package/src/tui/components/chat/ChatPanelBody.tsx +21 -0
- package/src/tui/components/chat/SubagentBrowserPanel.tsx +145 -0
- package/src/tui/components/messages/EditOutput.tsx +11 -5
- package/src/tui/components/messages/ReadOutput.tsx +1 -1
- package/src/tui/components/messages/ToolHeader.tsx +6 -4
- package/src/tui/components/messages/WriteOutput.tsx +12 -4
- package/src/tui/components/messages/assistantMessage.tsx +43 -10
- package/src/tui/components/messages/markdown.tsx +407 -0
- package/src/tui/components/messages/reasoningBlock.tsx +8 -6
- package/src/tui/components/messages/streamingOutput.tsx +1 -1
- package/src/tui/components/messages/toolCallBlock.tsx +2 -2
- package/src/tui/components/messages/userMessage.tsx +3 -3
- package/src/tui/components/primitives/toast.tsx +38 -7
- package/src/tui/components/statusBar.tsx +24 -15
- package/src/tui/hooks/useChordKeyboard.ts +87 -0
- package/src/tui/hooks/useInputInfoSegments.ts +22 -0
- package/src/tui/input/InputBoxView.tsx +71 -15
- package/src/tui/input/commands.ts +5 -0
- package/src/tui/input/useInputBox.ts +29 -3
- package/src/tui/input/useInputHandler.ts +1 -0
- package/src/tui/input/useMentionPicker.ts +26 -14
- package/src/tui/renderApp.tsx +29 -8
- package/src/tui/theme/presets.ts +12 -1
- package/src/tui/theme/types.ts +22 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { InputInfoSegment } from 'mu-core';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { useChatContext } from '../chat/ChatContext';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Subscribe to the aggregated input-info segments published by plugins via
|
|
7
|
+
* `PluginContext.setInputInfo`. Returns the live snapshot; re-renders on
|
|
8
|
+
* every push from any plugin.
|
|
9
|
+
*/
|
|
10
|
+
export function useInputInfoSegments(): InputInfoSegment[] {
|
|
11
|
+
const { registry } = useChatContext();
|
|
12
|
+
const [segments, setSegments] = useState<InputInfoSegment[]>(() => registry.getInputInfoSegments());
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const unsub = registry.onInputInfoChange(() => {
|
|
16
|
+
setSegments(registry.getInputInfoSegments());
|
|
17
|
+
});
|
|
18
|
+
return unsub;
|
|
19
|
+
}, [registry]);
|
|
20
|
+
|
|
21
|
+
return segments;
|
|
22
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Box, Text } from 'ink';
|
|
2
|
-
import type { MentionCompletion } from 'mu-core';
|
|
2
|
+
import type { InputInfoSegment, MentionCompletion } from 'mu-core';
|
|
3
3
|
import { useTheme } from '../context/ThemeContext';
|
|
4
4
|
import type { Theme } from '../theme/types';
|
|
5
5
|
import type { SlashCommand } from './commands';
|
|
@@ -19,6 +19,7 @@ export interface InputBoxViewProps {
|
|
|
19
19
|
streaming: boolean;
|
|
20
20
|
isActive: boolean;
|
|
21
21
|
model: string;
|
|
22
|
+
infoSegments: InputInfoSegment[];
|
|
22
23
|
attachmentName: string | null;
|
|
23
24
|
attachmentError: string | null;
|
|
24
25
|
mentions: MentionPickerView | null;
|
|
@@ -51,22 +52,69 @@ function CommandHints({
|
|
|
51
52
|
);
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Render a label with the substring matching `partial` (case-insensitive)
|
|
57
|
+
* highlighted using the same accent color the picker uses for the selected
|
|
58
|
+
* row. Preserves the original casing of the label since we only slice it.
|
|
59
|
+
*/
|
|
60
|
+
function renderHighlightedLabel(label: string, partial: string, theme: Theme) {
|
|
61
|
+
if (!partial) return <>{label}</>;
|
|
62
|
+
const idx = label.toLowerCase().indexOf(partial.toLowerCase());
|
|
63
|
+
if (idx < 0) return <>{label}</>;
|
|
64
|
+
const head = label.slice(0, idx);
|
|
65
|
+
const match = label.slice(idx, idx + partial.length);
|
|
66
|
+
const tail = label.slice(idx + partial.length);
|
|
67
|
+
return (
|
|
68
|
+
<>
|
|
69
|
+
{head}
|
|
70
|
+
<Text color={theme.input.commandHighlight} bold={true}>
|
|
71
|
+
{match}
|
|
72
|
+
</Text>
|
|
73
|
+
{tail}
|
|
74
|
+
</>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
54
78
|
function MentionHints({ mentions, theme }: { mentions: MentionPickerView; theme: Theme }) {
|
|
55
79
|
if (!mentions.completions.length) {
|
|
56
80
|
return null;
|
|
57
81
|
}
|
|
82
|
+
// Group completions by category while preserving the global index so
|
|
83
|
+
// ↑/↓ navigation still maps to the correct entry. When only one
|
|
84
|
+
// category is present we hide the header to keep the dropdown compact.
|
|
85
|
+
const grouped = new Map<string, { c: MentionCompletion; i: number }[]>();
|
|
86
|
+
mentions.completions.forEach((c, i) => {
|
|
87
|
+
const key = c.category ?? '';
|
|
88
|
+
const arr = grouped.get(key);
|
|
89
|
+
if (arr) arr.push({ c, i });
|
|
90
|
+
else grouped.set(key, [{ c, i }]);
|
|
91
|
+
});
|
|
92
|
+
const showHeaders = grouped.size > 1;
|
|
93
|
+
const sections = Array.from(grouped.entries());
|
|
58
94
|
return (
|
|
59
95
|
<Box flexDirection="column" marginBottom={1}>
|
|
60
|
-
{
|
|
61
|
-
<Box key={
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
{
|
|
96
|
+
{sections.map(([category, items]) => (
|
|
97
|
+
<Box key={category || 'default'} flexDirection="column">
|
|
98
|
+
{showHeaders && category && (
|
|
99
|
+
<Box paddingX={1}>
|
|
100
|
+
<Text dimColor={true} bold={true}>
|
|
101
|
+
{category}
|
|
102
|
+
</Text>
|
|
103
|
+
</Box>
|
|
104
|
+
)}
|
|
105
|
+
{items.map(({ c, i }) => {
|
|
106
|
+
const selected = i === mentions.selectedIndex;
|
|
107
|
+
const labelText = c.label ?? c.value;
|
|
108
|
+
return (
|
|
109
|
+
<Box key={`${category}:${c.value}`} paddingX={1}>
|
|
110
|
+
<Text wrap="truncate-start" color={selected ? theme.input.commandHighlight : undefined} bold={selected}>
|
|
111
|
+
{selected ? '▸ @' : ' @'}
|
|
112
|
+
{renderHighlightedLabel(labelText, mentions.partial, theme)}
|
|
113
|
+
</Text>
|
|
114
|
+
{c.description && <Text dimColor={true}> {c.description}</Text>}
|
|
115
|
+
</Box>
|
|
116
|
+
);
|
|
117
|
+
})}
|
|
70
118
|
</Box>
|
|
71
119
|
))}
|
|
72
120
|
</Box>
|
|
@@ -75,6 +123,7 @@ function MentionHints({ mentions, theme }: { mentions: MentionPickerView; theme:
|
|
|
75
123
|
|
|
76
124
|
function InputFooter({
|
|
77
125
|
model,
|
|
126
|
+
infoSegments,
|
|
78
127
|
attachmentName,
|
|
79
128
|
attachmentError,
|
|
80
129
|
hasContent,
|
|
@@ -83,6 +132,7 @@ function InputFooter({
|
|
|
83
132
|
theme,
|
|
84
133
|
}: {
|
|
85
134
|
model: string;
|
|
135
|
+
infoSegments: InputInfoSegment[];
|
|
86
136
|
attachmentName: string | null;
|
|
87
137
|
attachmentError: string | null;
|
|
88
138
|
hasContent: boolean;
|
|
@@ -91,16 +141,21 @@ function InputFooter({
|
|
|
91
141
|
theme: Theme;
|
|
92
142
|
}) {
|
|
93
143
|
const hint = hasMentions
|
|
94
|
-
? '↑↓
|
|
144
|
+
? '↑↓ · Tab accept'
|
|
95
145
|
: hasContent
|
|
96
146
|
? isCommandMode
|
|
97
|
-
? '↑↓
|
|
98
|
-
: '
|
|
99
|
-
: '
|
|
147
|
+
? '↑↓ · Enter run'
|
|
148
|
+
: ''
|
|
149
|
+
: '/ commands · @ mentions';
|
|
100
150
|
|
|
101
151
|
return (
|
|
102
152
|
<Box justifyContent="space-between">
|
|
103
153
|
<Box gap={1}>
|
|
154
|
+
{infoSegments.map((seg) => (
|
|
155
|
+
<Text key={seg.key} color={seg.color} bold={seg.bold}>
|
|
156
|
+
{seg.text}
|
|
157
|
+
</Text>
|
|
158
|
+
))}
|
|
104
159
|
{model && (
|
|
105
160
|
<Text color={theme.input.modelLabel} bold={true}>
|
|
106
161
|
{model}
|
|
@@ -225,6 +280,7 @@ export function InputBoxView(props: InputBoxViewProps) {
|
|
|
225
280
|
</Box>
|
|
226
281
|
<InputFooter
|
|
227
282
|
model={props.model}
|
|
283
|
+
infoSegments={props.infoSegments}
|
|
228
284
|
attachmentName={props.attachmentName}
|
|
229
285
|
attachmentError={props.attachmentError}
|
|
230
286
|
hasContent={props.value.length > 0}
|
|
@@ -20,6 +20,11 @@ export const BUILTIN_COMMANDS: SlashCommand[] = [
|
|
|
20
20
|
{ name: '/model', description: 'Select a model', invoke: (a) => a.onTogglePicker?.() },
|
|
21
21
|
{ name: '/sessions', description: 'List project sessions', invoke: (a) => a.onToggleSessionPicker?.() },
|
|
22
22
|
{ name: '/new', description: 'New conversation', invoke: (a) => a.onNew?.() },
|
|
23
|
+
{
|
|
24
|
+
name: '/compact',
|
|
25
|
+
description: 'Summarize the conversation and replace history with the summary (frees context)',
|
|
26
|
+
invoke: (a) => a.onCompact?.(),
|
|
27
|
+
},
|
|
23
28
|
{
|
|
24
29
|
name: '/context',
|
|
25
30
|
description: 'Show the LLM context (system prompt, messages, tools) as plain text',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { InputInfoSegment } from 'mu-core';
|
|
1
2
|
import { useCallback, useMemo, useRef } from 'react';
|
|
2
3
|
import { useChatContext } from '../chat/ChatContext';
|
|
3
4
|
import { dumpContext } from './dumpContext';
|
|
@@ -14,6 +15,12 @@ export interface InputBoxProps {
|
|
|
14
15
|
isActive?: boolean;
|
|
15
16
|
model?: string;
|
|
16
17
|
history?: string[];
|
|
18
|
+
/**
|
|
19
|
+
* Extra info chips rendered in the footer (before the model). Generic
|
|
20
|
+
* mechanism for upstream consumers to surface context (active agent,
|
|
21
|
+
* branch, ...) without `InputBox` knowing what they are.
|
|
22
|
+
*/
|
|
23
|
+
infoSegments?: InputInfoSegment[];
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
interface BufferDraft {
|
|
@@ -24,7 +31,9 @@ interface BufferDraft {
|
|
|
24
31
|
/**
|
|
25
32
|
* Replace `[triggerStart, cursor)` (the `<trigger><partial>` token) with the
|
|
26
33
|
* chosen completion value plus a trailing space, so the user is left in a
|
|
27
|
-
* sensible position for further input.
|
|
34
|
+
* sensible position for further input. File completions drop the trigger
|
|
35
|
+
* (`@`) entirely — the path stands alone in the prompt — while other
|
|
36
|
+
* categories (agents) keep the `@` prefix as a visible marker.
|
|
28
37
|
*/
|
|
29
38
|
function applyMention(
|
|
30
39
|
value: string,
|
|
@@ -32,10 +41,12 @@ function applyMention(
|
|
|
32
41
|
cursor: number,
|
|
33
42
|
trigger: string,
|
|
34
43
|
completion: string,
|
|
44
|
+
category: string | undefined,
|
|
35
45
|
): BufferDraft {
|
|
36
46
|
const before = value.slice(0, triggerStart);
|
|
37
47
|
const after = value.slice(cursor);
|
|
38
|
-
const
|
|
48
|
+
const keepTrigger = category !== 'files';
|
|
49
|
+
const insertion = `${keepTrigger ? trigger : ''}${completion} `;
|
|
39
50
|
return { value: before + insertion + after, cursor: triggerStart + insertion.length };
|
|
40
51
|
}
|
|
41
52
|
|
|
@@ -65,7 +76,14 @@ function buildMentionMode(mentions: MentionPickerState, input: InputHandle): Men
|
|
|
65
76
|
const completion = mentions.completions[mentions.selectedIndex];
|
|
66
77
|
const trig = mentions.trigger;
|
|
67
78
|
if (!(completion && trig)) return;
|
|
68
|
-
const draft = applyMention(
|
|
79
|
+
const draft = applyMention(
|
|
80
|
+
input.value,
|
|
81
|
+
mentions.triggerStart,
|
|
82
|
+
input.cursor,
|
|
83
|
+
trig,
|
|
84
|
+
completion.value,
|
|
85
|
+
completion.category,
|
|
86
|
+
);
|
|
69
87
|
input.setBuffer(draft.value, draft.cursor);
|
|
70
88
|
},
|
|
71
89
|
};
|
|
@@ -90,6 +108,9 @@ function useInputActions(deps: ActionDeps): InputActions {
|
|
|
90
108
|
onEsc: abort.onEsc,
|
|
91
109
|
onPaste: attachment.onPaste,
|
|
92
110
|
onNew: session.onNew,
|
|
111
|
+
onCompact: () => {
|
|
112
|
+
void session.onCompact();
|
|
113
|
+
},
|
|
93
114
|
onCycleModel: models.cycleModel,
|
|
94
115
|
onTogglePicker: toggles.onTogglePicker,
|
|
95
116
|
onToggleSessionPicker: toggles.onToggleSessionPicker,
|
|
@@ -103,6 +124,7 @@ function useInputActions(deps: ActionDeps): InputActions {
|
|
|
103
124
|
abort.onEsc,
|
|
104
125
|
attachment.onPaste,
|
|
105
126
|
session.onNew,
|
|
127
|
+
session.onCompact,
|
|
106
128
|
models.cycleModel,
|
|
107
129
|
models.models.length,
|
|
108
130
|
toggles.onTogglePicker,
|
|
@@ -114,6 +136,8 @@ function useInputActions(deps: ActionDeps): InputActions {
|
|
|
114
136
|
);
|
|
115
137
|
}
|
|
116
138
|
|
|
139
|
+
const EMPTY_SEGMENTS: InputInfoSegment[] = [];
|
|
140
|
+
|
|
117
141
|
export function useInputBox({
|
|
118
142
|
onSubmit,
|
|
119
143
|
onScrollUp,
|
|
@@ -121,6 +145,7 @@ export function useInputBox({
|
|
|
121
145
|
isActive = true,
|
|
122
146
|
model = '',
|
|
123
147
|
history = [],
|
|
148
|
+
infoSegments = EMPTY_SEGMENTS,
|
|
124
149
|
}: InputBoxProps): InputBoxViewProps {
|
|
125
150
|
const { config, session, toggles, attachment, models, abort, registry, uiService } = useChatContext();
|
|
126
151
|
// Ref pattern: the mention controls depend on the input handler's
|
|
@@ -193,6 +218,7 @@ export function useInputBox({
|
|
|
193
218
|
streaming: session.streaming,
|
|
194
219
|
isActive,
|
|
195
220
|
model,
|
|
221
|
+
infoSegments,
|
|
196
222
|
attachmentName: attachment.attachment?.name ?? null,
|
|
197
223
|
attachmentError: attachment.attachmentError,
|
|
198
224
|
mentions:
|
|
@@ -79,25 +79,37 @@ export function useMentionPicker(registry: PluginRegistry, value: string, cursor
|
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
81
81
|
const partial = value.slice(match.start + 1, cursor);
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
82
|
+
const matching = providers.filter((p) => p.trigger === match.trigger);
|
|
83
|
+
if (matching.length === 0) {
|
|
84
84
|
setBase(EMPTY);
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
87
|
let cancelled = false;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
88
|
+
// Run every provider that registered for this trigger and concatenate
|
|
89
|
+
// results in registration order. Providers tag completions with
|
|
90
|
+
// `category` so the picker can render section headers (e.g. agents
|
|
91
|
+
// first, then files) without the picker hard-coding any plugin id.
|
|
92
|
+
Promise.all(
|
|
93
|
+
matching.map(async (entry) => {
|
|
94
|
+
try {
|
|
95
|
+
const out = await Promise.resolve(entry.provider(partial));
|
|
96
|
+
// Default the category to the plugin name so legacy providers
|
|
97
|
+
// that don't set `category` still get visually grouped.
|
|
98
|
+
return out.map((c) => ({ ...c, category: c.category ?? entry.plugin }));
|
|
99
|
+
} catch {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
}),
|
|
103
|
+
).then((groups) => {
|
|
104
|
+
if (cancelled) return;
|
|
105
|
+
const completions = groups.flat();
|
|
106
|
+
setBase({
|
|
107
|
+
trigger: match.trigger,
|
|
108
|
+
partial,
|
|
109
|
+
completions,
|
|
110
|
+
triggerStart: match.start,
|
|
100
111
|
});
|
|
112
|
+
});
|
|
101
113
|
return () => {
|
|
102
114
|
cancelled = true;
|
|
103
115
|
};
|
package/src/tui/renderApp.tsx
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { type Instance, render } from 'ink';
|
|
2
|
+
import { type SubagentRunRegistry, SubagentRunsProvider } from 'mu-agents';
|
|
2
3
|
import type { ChatMessage, PluginRegistry } from 'mu-core';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
3
5
|
import type { ShutdownFn } from '../app/shutdown';
|
|
4
6
|
import type { AppConfig } from '../config/index';
|
|
7
|
+
import type { SessionPathHolder } from '../runtime/createRegistry';
|
|
5
8
|
import type { HostMessageBus } from '../runtime/messageBus';
|
|
6
9
|
import { ChatPanel } from './components/chat/ChatPanel';
|
|
7
10
|
import { ThemeProvider } from './context/ThemeContext';
|
|
@@ -15,6 +18,19 @@ interface RenderAppOptions {
|
|
|
15
18
|
messageBus: HostMessageBus;
|
|
16
19
|
uiService: InkUIService;
|
|
17
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>;
|
|
18
34
|
}
|
|
19
35
|
|
|
20
36
|
/**
|
|
@@ -26,14 +42,19 @@ export function renderApp(options: RenderAppOptions): Instance {
|
|
|
26
42
|
const theme = resolveTheme(options.config.theme);
|
|
27
43
|
return render(
|
|
28
44
|
<ThemeProvider theme={theme}>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
+
)}
|
|
37
58
|
</ThemeProvider>,
|
|
38
59
|
{
|
|
39
60
|
exitOnCtrlC: false,
|
package/src/tui/theme/presets.ts
CHANGED
|
@@ -28,7 +28,7 @@ export const DEFAULT_THEME: Theme = {
|
|
|
28
28
|
tool: {
|
|
29
29
|
success: 'green',
|
|
30
30
|
error: 'red',
|
|
31
|
-
previewBackground: '#
|
|
31
|
+
previewBackground: '#2a2a2a',
|
|
32
32
|
previewText: 'white',
|
|
33
33
|
summaryDim: 'gray',
|
|
34
34
|
warning: 'yellow',
|
|
@@ -69,6 +69,17 @@ export const DEFAULT_THEME: Theme = {
|
|
|
69
69
|
status: {
|
|
70
70
|
separator: 'gray',
|
|
71
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
|
+
},
|
|
72
83
|
common: {
|
|
73
84
|
error: 'red',
|
|
74
85
|
warning: 'yellow',
|
package/src/tui/theme/types.ts
CHANGED
|
@@ -79,6 +79,27 @@ interface ThemeStatus {
|
|
|
79
79
|
separator: string;
|
|
80
80
|
}
|
|
81
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
|
+
|
|
82
103
|
interface ThemeCommon {
|
|
83
104
|
error: string;
|
|
84
105
|
warning: string;
|
|
@@ -99,6 +120,7 @@ export interface Theme {
|
|
|
99
120
|
dialog: ThemeDialog;
|
|
100
121
|
diff: ThemeDiff;
|
|
101
122
|
status: ThemeStatus;
|
|
123
|
+
markdown: ThemeMarkdown;
|
|
102
124
|
common: ThemeCommon;
|
|
103
125
|
}
|
|
104
126
|
|