mu-coding 0.5.0 → 0.8.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/README.md +49 -3
- package/package.json +9 -4
- package/prompts/SYSTEM.md +16 -0
- package/src/app/shutdown.ts +1 -1
- package/src/app/startApp.ts +11 -8
- package/src/cli/args.ts +14 -11
- package/src/config/index.test.ts +26 -0
- package/src/config/index.ts +25 -7
- package/src/plugin.ts +96 -0
- package/src/runtime/codingTools/bash.ts +114 -0
- package/src/runtime/codingTools/edit-file.ts +60 -0
- package/src/runtime/codingTools/index.ts +39 -0
- package/src/runtime/codingTools/read-file.ts +83 -0
- package/src/runtime/codingTools/utils.ts +21 -0
- package/src/runtime/codingTools/write-file.ts +42 -0
- package/src/runtime/createRegistry.test.ts +146 -0
- package/src/runtime/createRegistry.ts +128 -23
- package/src/runtime/messageBus.test.ts +62 -0
- package/src/runtime/messageBus.ts +78 -0
- package/src/runtime/pluginLoader.ts +22 -9
- package/src/sessions/index.ts +2 -9
- package/src/tui/channel/tuiChannel.test.ts +107 -0
- package/src/tui/channel/tuiChannel.ts +49 -0
- package/src/tui/chat/MessageRendererContext.ts +44 -0
- package/src/tui/chat/ToolDisplayContext.ts +1 -1
- package/src/tui/chat/useAttachment.ts +1 -1
- package/src/tui/chat/useChat.ts +31 -3
- package/src/tui/chat/useChatPanel.ts +7 -5
- package/src/tui/chat/useChatSession.ts +222 -53
- package/src/tui/chat/useModels.ts +2 -1
- package/src/tui/chat/usePluginStatus.ts +1 -1
- package/src/tui/chat/useSessionPersistence.ts +25 -14
- package/src/tui/chat/useStatusSegments.ts +17 -4
- package/src/tui/components/chat/ChatPanel.tsx +10 -4
- package/src/tui/components/chat/ChatPanelBody.tsx +1 -1
- package/src/tui/components/messageView.tsx +4 -2
- package/src/tui/components/messages/EditOutput.tsx +6 -4
- package/src/tui/components/messages/ToolHeader.tsx +3 -1
- package/src/tui/components/messages/assistantMessage.tsx +17 -2
- package/src/tui/components/messages/messageItem.tsx +19 -1
- package/src/tui/components/messages/reasoningBlock.tsx +4 -2
- package/src/tui/components/messages/streamingOutput.tsx +5 -1
- package/src/tui/components/messages/toolCallBlock.tsx +6 -5
- package/src/tui/components/messages/userMessage.tsx +21 -6
- package/src/tui/components/primitives/dropdown.tsx +8 -4
- package/src/tui/components/primitives/modal.tsx +4 -2
- package/src/tui/components/primitives/pickerModal.tsx +3 -1
- package/src/tui/components/primitives/toast.tsx +5 -3
- package/src/tui/components/statusBar.tsx +8 -1
- package/src/tui/components/ui/dialogLayer.tsx +11 -6
- package/src/tui/context/ThemeContext.tsx +18 -0
- package/src/tui/input/InputBoxView.tsx +135 -26
- package/src/tui/input/commands.test.ts +3 -1
- package/src/tui/input/commands.ts +6 -1
- package/src/tui/input/cursor.test.ts +136 -0
- package/src/tui/input/cursor.ts +214 -0
- package/src/tui/input/dumpContext.ts +107 -0
- package/src/tui/input/sanitize.ts +1 -1
- package/src/tui/input/useCommandExecutor.ts +1 -1
- package/src/tui/input/useInputBox.ts +134 -15
- package/src/tui/input/useInputHandler.ts +316 -126
- package/src/tui/input/useMentionPicker.ts +121 -0
- package/src/tui/input/usePluginShortcuts.ts +29 -0
- package/src/tui/plugins/InkApprovalChannel.test.ts +51 -0
- package/src/tui/plugins/InkApprovalChannel.ts +30 -0
- package/src/tui/plugins/InkUIService.ts +1 -1
- package/src/tui/renderApp.tsx +26 -13
- package/src/tui/theme/index.ts +1 -0
- package/src/tui/theme/merge.test.ts +49 -0
- package/src/tui/theme/merge.ts +43 -0
- package/src/tui/theme/presets.ts +79 -0
- package/src/tui/theme/types.ts +116 -0
- package/src/utils/clipboard.ts +1 -1
- package/src/tui/chat/useStreamConsumer.ts +0 -118
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readdirSync } from 'node:fs';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
|
-
import type { Plugin, PluginRegistry } from 'mu-
|
|
4
|
+
import type { Plugin, PluginRegistry } from 'mu-core';
|
|
5
5
|
import { getDataDir, getPluginsDir, parseBareNpmSpec } from '../config/index';
|
|
6
6
|
import type { InkUIService } from '../tui/plugins/InkUIService';
|
|
7
7
|
|
|
@@ -71,36 +71,49 @@ function extractPlugin(mod: Record<string, unknown>, pluginConfig: Record<string
|
|
|
71
71
|
return null;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Loader variant that *resolves* (imports + extracts) a plugin without
|
|
76
|
+
* registering it. Used by hosts driving `startMu({ resolvePlugin })`. Errors
|
|
77
|
+
* surface via `uiService` (or are swallowed when omitted) so the host's
|
|
78
|
+
* boot log behaviour matches `loadConfiguredPlugin`.
|
|
79
|
+
*/
|
|
80
|
+
export async function resolveConfiguredPlugin(
|
|
76
81
|
name: string,
|
|
77
82
|
pluginConfig?: Record<string, unknown>,
|
|
78
83
|
uiService?: InkUIService,
|
|
79
|
-
): Promise<
|
|
84
|
+
): Promise<Plugin | null> {
|
|
80
85
|
const config = pluginConfig ?? {};
|
|
81
86
|
let target: string;
|
|
82
87
|
try {
|
|
83
88
|
target = name.startsWith('npm:') ? resolveNpmPlugin(name) : name;
|
|
84
89
|
} catch (err) {
|
|
85
90
|
uiService?.notify(formatPluginError(name, err), 'error');
|
|
86
|
-
return;
|
|
91
|
+
return null;
|
|
87
92
|
}
|
|
88
|
-
|
|
89
93
|
let mod: Record<string, unknown>;
|
|
90
94
|
try {
|
|
91
95
|
mod = (await import(target)) as Record<string, unknown>;
|
|
92
96
|
} catch (err) {
|
|
93
97
|
uiService?.notify(formatPluginError(name, err), 'error');
|
|
94
|
-
return;
|
|
98
|
+
return null;
|
|
95
99
|
}
|
|
96
|
-
|
|
97
100
|
const plugin = extractPlugin(mod, config);
|
|
98
101
|
if (!plugin) {
|
|
99
102
|
const exportKeys = Object.keys(mod).join(', ') || '(none)';
|
|
100
103
|
uiService?.notify(`Plugin "${name}": no plugin export found. Exports: [${exportKeys}]`, 'error');
|
|
101
|
-
return;
|
|
104
|
+
return null;
|
|
102
105
|
}
|
|
106
|
+
return plugin;
|
|
107
|
+
}
|
|
103
108
|
|
|
109
|
+
export async function loadConfiguredPlugin(
|
|
110
|
+
registry: PluginRegistry,
|
|
111
|
+
name: string,
|
|
112
|
+
pluginConfig?: Record<string, unknown>,
|
|
113
|
+
uiService?: InkUIService,
|
|
114
|
+
): Promise<void> {
|
|
115
|
+
const plugin = await resolveConfiguredPlugin(name, pluginConfig, uiService);
|
|
116
|
+
if (!plugin) return;
|
|
104
117
|
try {
|
|
105
118
|
await registry.register(plugin);
|
|
106
119
|
} catch (err) {
|
package/src/sessions/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { createReadStream, mkdirSync, readdirSync, readFileSync
|
|
1
|
+
import { createReadStream, mkdirSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { createInterface } from 'node:readline';
|
|
5
|
-
import type { ChatMessage } from 'mu-
|
|
5
|
+
import type { ChatMessage } from 'mu-core';
|
|
6
6
|
import { getDataDir } from '../config/index';
|
|
7
7
|
import { getProjectId, getProjectName } from './project';
|
|
8
8
|
|
|
@@ -181,10 +181,3 @@ export async function listSessionsAsync(): Promise<SessionInfo[]> {
|
|
|
181
181
|
|
|
182
182
|
return results.filter((s): s is SessionInfo => s !== null);
|
|
183
183
|
}
|
|
184
|
-
|
|
185
|
-
// Sync helper preserved for legacy/test callers — wraps the same data path
|
|
186
|
-
// without the streaming optimization. New code should prefer `listSessionsAsync`.
|
|
187
|
-
export function saveSessionSync(path: string, messages: ChatMessage[]): void {
|
|
188
|
-
const content = messages.length > 0 ? `${messages.map((m) => JSON.stringify(m)).join('\n')}\n` : '';
|
|
189
|
-
writeFileSync(path, content, 'utf-8');
|
|
190
|
-
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, expect, it, mock } from 'bun:test';
|
|
2
|
+
import type { ChatMessage, PluginRegistry } from 'mu-core';
|
|
3
|
+
import type { ShutdownFn } from '../../app/shutdown';
|
|
4
|
+
import type { AppConfig } from '../../config/index';
|
|
5
|
+
import type { HostMessageBus } from '../../runtime/messageBus';
|
|
6
|
+
import type { InkUIService } from '../plugins/InkUIService';
|
|
7
|
+
import { createTuiChannel } from './tuiChannel';
|
|
8
|
+
|
|
9
|
+
// Stub renderApp by mocking the import surface. We can't actually mount Ink
|
|
10
|
+
// in a non-TTY test environment, but we can verify the channel structure
|
|
11
|
+
// and that the registry passed in has the methods the TUI subscribes to.
|
|
12
|
+
const noop = (): void => {
|
|
13
|
+
/* stub */
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const renderArgs: Array<{ registry: PluginRegistry; config: AppConfig }> = [];
|
|
17
|
+
mock.module('../renderApp', () => ({
|
|
18
|
+
renderApp: (opts: { registry: PluginRegistry; config: AppConfig }) => {
|
|
19
|
+
renderArgs.push(opts);
|
|
20
|
+
return {
|
|
21
|
+
unmount: noop,
|
|
22
|
+
waitUntilExit: async () => {
|
|
23
|
+
/* stub */
|
|
24
|
+
},
|
|
25
|
+
rerender: noop,
|
|
26
|
+
cleanup: noop,
|
|
27
|
+
clear: noop,
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const fakeOpts = {
|
|
33
|
+
config: {} as AppConfig,
|
|
34
|
+
initialMessages: [] as ChatMessage[],
|
|
35
|
+
registry: {} as PluginRegistry,
|
|
36
|
+
messageBus: {} as HostMessageBus,
|
|
37
|
+
uiService: {} as InkUIService,
|
|
38
|
+
shutdown: (async () => {
|
|
39
|
+
/* test shutdown stub */
|
|
40
|
+
}) as ShutdownFn,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
describe('createTuiChannel', () => {
|
|
44
|
+
it('exposes id="tui"', () => {
|
|
45
|
+
const ch = createTuiChannel(fakeOpts);
|
|
46
|
+
expect(ch.id).toBe('tui');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('start is idempotent — second start is a no-op', async () => {
|
|
50
|
+
const ch = createTuiChannel(fakeOpts);
|
|
51
|
+
await ch.start();
|
|
52
|
+
await ch.start(); // should not throw / re-mount
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('stop without start is a no-op', async () => {
|
|
56
|
+
const ch = createTuiChannel(fakeOpts);
|
|
57
|
+
await ch.stop?.();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('start → stop → start cycles cleanly', async () => {
|
|
61
|
+
const ch = createTuiChannel(fakeOpts);
|
|
62
|
+
await ch.start();
|
|
63
|
+
await ch.stop?.();
|
|
64
|
+
await ch.start();
|
|
65
|
+
await ch.stop?.();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('createTuiChannel — registry shape contract', () => {
|
|
70
|
+
it('forwards a registry that exposes the subscription methods the TUI relies on', async () => {
|
|
71
|
+
// Build a registry mock whose methods are all functions; the channel's
|
|
72
|
+
// `start()` calls renderApp which (in production) mounts components that
|
|
73
|
+
// immediately invoke onStatusChange / onRenderersChange / etc.
|
|
74
|
+
const stubFn = (): (() => void) => () => {
|
|
75
|
+
/* unsub */
|
|
76
|
+
};
|
|
77
|
+
const fakeRegistry: Record<string, unknown> = {
|
|
78
|
+
getTools: () => [],
|
|
79
|
+
getFilteredTools: async () => [],
|
|
80
|
+
getHooks: () => [],
|
|
81
|
+
getStatusSegments: () => new Map(),
|
|
82
|
+
onStatusChange: stubFn(),
|
|
83
|
+
getRenderers: () => [],
|
|
84
|
+
onRenderersChange: stubFn(),
|
|
85
|
+
getShortcuts: () => [],
|
|
86
|
+
onShortcutsChange: stubFn(),
|
|
87
|
+
getCommands: () => [],
|
|
88
|
+
};
|
|
89
|
+
const ch = createTuiChannel({ ...fakeOpts, registry: fakeRegistry as unknown as PluginRegistry });
|
|
90
|
+
renderArgs.length = 0;
|
|
91
|
+
await ch.start();
|
|
92
|
+
expect(renderArgs).toHaveLength(1);
|
|
93
|
+
const seen = renderArgs[0].registry as unknown as Record<string, unknown>;
|
|
94
|
+
for (const method of [
|
|
95
|
+
'onStatusChange',
|
|
96
|
+
'getStatusSegments',
|
|
97
|
+
'onRenderersChange',
|
|
98
|
+
'getRenderers',
|
|
99
|
+
'onShortcutsChange',
|
|
100
|
+
'getShortcuts',
|
|
101
|
+
'getCommands',
|
|
102
|
+
]) {
|
|
103
|
+
expect(typeof seen[method]).toBe('function');
|
|
104
|
+
}
|
|
105
|
+
await ch.stop?.();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI Channel — wraps Ink rendering inside the mu-core `Channel` contract.
|
|
3
|
+
* `start()` mounts the app and captures the Ink instance; `stop()` unmounts
|
|
4
|
+
* it cleanly so `channels.stopAll()` restores the terminal.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Instance } from 'ink';
|
|
8
|
+
import type { Channel, ChatMessage, PluginRegistry } from 'mu-core';
|
|
9
|
+
import type { ShutdownFn } from '../../app/shutdown';
|
|
10
|
+
import type { AppConfig } from '../../config/index';
|
|
11
|
+
import type { HostMessageBus } from '../../runtime/messageBus';
|
|
12
|
+
import type { InkUIService } from '../plugins/InkUIService';
|
|
13
|
+
import { renderApp } from '../renderApp';
|
|
14
|
+
|
|
15
|
+
export interface TuiChannelOptions {
|
|
16
|
+
config: AppConfig;
|
|
17
|
+
initialMessages?: ChatMessage[];
|
|
18
|
+
registry: PluginRegistry;
|
|
19
|
+
messageBus: HostMessageBus;
|
|
20
|
+
uiService: InkUIService;
|
|
21
|
+
shutdown: ShutdownFn;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createTuiChannel(opts: TuiChannelOptions): Channel {
|
|
25
|
+
let instance: Instance | null = null;
|
|
26
|
+
return {
|
|
27
|
+
id: 'tui',
|
|
28
|
+
async start() {
|
|
29
|
+
// Idempotent: re-starting after a stop remounts; re-starting while
|
|
30
|
+
// mounted is a no-op.
|
|
31
|
+
if (instance) return;
|
|
32
|
+
instance = renderApp(opts);
|
|
33
|
+
},
|
|
34
|
+
async stop() {
|
|
35
|
+
if (!instance) return;
|
|
36
|
+
try {
|
|
37
|
+
instance.unmount();
|
|
38
|
+
// Wait for Ink's exit promise so `stopAll()` callers know the
|
|
39
|
+
// terminal has been restored before they continue (e.g. emitting
|
|
40
|
+
// a final shutdown message to stdout).
|
|
41
|
+
await instance.waitUntilExit().catch(() => {
|
|
42
|
+
/* unmount-induced exit rejects with the cause; we don't care */
|
|
43
|
+
});
|
|
44
|
+
} finally {
|
|
45
|
+
instance = null;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ChatMessage, PluginRegistry } from 'mu-core';
|
|
2
|
+
import { createContext, type ReactNode, useContext, useEffect, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Plugin renderers are typed `unknown` in mu-agents (kept renderer-agnostic);
|
|
6
|
+
* the host narrows to React at the boundary so renderer authors can return
|
|
7
|
+
* any `ReactNode`.
|
|
8
|
+
*/
|
|
9
|
+
type ReactMessageRenderer = (msg: ChatMessage) => ReactNode;
|
|
10
|
+
|
|
11
|
+
type RendererMap = Map<string, ReactMessageRenderer>;
|
|
12
|
+
|
|
13
|
+
const MessageRendererContext = createContext<RendererMap>(new Map());
|
|
14
|
+
|
|
15
|
+
export const MessageRendererProvider = MessageRendererContext.Provider;
|
|
16
|
+
|
|
17
|
+
/** Hook used by `MessageItem` to look up custom renderers by `customType`. */
|
|
18
|
+
export function useMessageRenderer(customType: string | undefined): ReactMessageRenderer | undefined {
|
|
19
|
+
const map = useContext(MessageRendererContext);
|
|
20
|
+
if (!customType) return undefined;
|
|
21
|
+
return map.get(customType);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Track the registry's custom renderer set. Re-builds the map whenever a
|
|
26
|
+
* plugin registers or unregisters one. The cast from `unknown` to ReactNode
|
|
27
|
+
* happens here so descendant components stay strictly typed.
|
|
28
|
+
*/
|
|
29
|
+
export function useRegistryRenderers(registry: PluginRegistry): RendererMap {
|
|
30
|
+
const [map, setMap] = useState<RendererMap>(() => buildMap(registry));
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
setMap(buildMap(registry));
|
|
33
|
+
return registry.onRenderersChange(() => setMap(buildMap(registry)));
|
|
34
|
+
}, [registry]);
|
|
35
|
+
return map;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildMap(registry: PluginRegistry): RendererMap {
|
|
39
|
+
const out: RendererMap = new Map();
|
|
40
|
+
for (const [customType, renderer] of registry.getRenderers()) {
|
|
41
|
+
out.set(customType, (msg) => renderer(msg) as ReactNode);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
package/src/tui/chat/useChat.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { useApp } from 'ink';
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import {
|
|
3
|
+
type ChatMessage,
|
|
4
|
+
createSessionManager,
|
|
5
|
+
type PluginRegistry,
|
|
6
|
+
type ProviderConfig,
|
|
7
|
+
type SessionManager,
|
|
8
|
+
} from 'mu-core';
|
|
9
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
5
10
|
import type { ShutdownFn } from '../../app/shutdown';
|
|
11
|
+
import type { HostMessageBus } from '../../runtime/messageBus';
|
|
6
12
|
import { listSessionsAsync, type SessionInfo } from '../../sessions/index';
|
|
13
|
+
import type { InkUIService } from '../plugins/InkUIService';
|
|
7
14
|
import { type AbortState, useAbort } from './useAbort';
|
|
8
15
|
import { type AttachmentState, type TogglesState, useAttachment, useToggles } from './useAttachment';
|
|
9
16
|
import { type ChatSessionState, useChatSession } from './useChatSession';
|
|
@@ -14,12 +21,15 @@ const ABORT_TIMEOUT_MS = 2000;
|
|
|
14
21
|
export interface ChatContextValue {
|
|
15
22
|
config: ProviderConfig;
|
|
16
23
|
session: ChatSessionState;
|
|
24
|
+
sessionManager: SessionManager;
|
|
17
25
|
toggles: TogglesState;
|
|
18
26
|
attachment: AttachmentState;
|
|
19
27
|
models: ModelListState;
|
|
20
28
|
abort: AbortState;
|
|
21
29
|
sessions: SessionInfo[];
|
|
22
30
|
registry: PluginRegistry;
|
|
31
|
+
uiService?: InkUIService;
|
|
32
|
+
messageBus?: HostMessageBus;
|
|
23
33
|
}
|
|
24
34
|
|
|
25
35
|
export function useChat(
|
|
@@ -27,19 +37,34 @@ export function useChat(
|
|
|
27
37
|
registry: PluginRegistry,
|
|
28
38
|
initialMessages?: ChatMessage[],
|
|
29
39
|
shutdown?: ShutdownFn,
|
|
40
|
+
uiService?: InkUIService,
|
|
41
|
+
messageBus?: HostMessageBus,
|
|
30
42
|
): ChatContextValue {
|
|
31
43
|
const { exit } = useApp();
|
|
32
44
|
const controllerRef = useRef<AbortController | null>(null);
|
|
33
45
|
const attachment = useAttachment();
|
|
34
46
|
const toggles = useToggles();
|
|
35
47
|
const models = useModelList(config.baseUrl, config.model);
|
|
48
|
+
// Stable SessionManager + Session for the lifetime of the chat hook. Model
|
|
49
|
+
// updates flow through `runTurn(options)` per call, so we don't need to
|
|
50
|
+
// re-instantiate on every change.
|
|
51
|
+
const sessionManager = useMemo(
|
|
52
|
+
() => createSessionManager({ registry, config, model: models.currentModel || config.model || 'unknown' }),
|
|
53
|
+
[registry, config, models.currentModel],
|
|
54
|
+
);
|
|
55
|
+
const muSession = useMemo(
|
|
56
|
+
() => sessionManager.getOrCreate('tui', { initialMessages }),
|
|
57
|
+
[sessionManager, initialMessages],
|
|
58
|
+
);
|
|
36
59
|
const session = useChatSession({
|
|
60
|
+
session: muSession,
|
|
37
61
|
config,
|
|
38
62
|
currentModel: models.currentModel,
|
|
39
63
|
attachment,
|
|
40
64
|
controllerRef,
|
|
41
65
|
initialMessages,
|
|
42
66
|
registry,
|
|
67
|
+
messageBus,
|
|
43
68
|
});
|
|
44
69
|
const abort = useAbort(session.streaming, controllerRef, exit, ABORT_TIMEOUT_MS, shutdown);
|
|
45
70
|
|
|
@@ -68,11 +93,14 @@ export function useChat(
|
|
|
68
93
|
return {
|
|
69
94
|
config,
|
|
70
95
|
session,
|
|
96
|
+
sessionManager,
|
|
71
97
|
toggles,
|
|
72
98
|
attachment,
|
|
73
99
|
models,
|
|
74
100
|
abort,
|
|
75
101
|
sessions,
|
|
76
102
|
registry,
|
|
103
|
+
uiService,
|
|
104
|
+
messageBus,
|
|
77
105
|
};
|
|
78
106
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type DOMElement as InkDOMElement, useInput } from 'ink';
|
|
2
|
-
import type { PluginRegistry } from 'mu-
|
|
3
|
-
import type { ChatMessage, ProviderConfig } from 'mu-provider';
|
|
2
|
+
import type { ChatMessage, PluginRegistry, ProviderConfig } from 'mu-core';
|
|
4
3
|
import { useEffect, useMemo, useRef } from 'react';
|
|
5
4
|
import type { ShutdownFn } from '../../app/shutdown';
|
|
5
|
+
import type { HostMessageBus } from '../../runtime/messageBus';
|
|
6
6
|
import type { ChatPanelBodyProps } from '../components/chat/ChatPanelBody';
|
|
7
7
|
import { useToast } from '../components/primitives/toast';
|
|
8
8
|
import { useScroll } from '../hooks/useScroll';
|
|
@@ -23,13 +23,14 @@ interface UseChatPanelOptions {
|
|
|
23
23
|
config: ProviderConfig;
|
|
24
24
|
initialMessages?: ChatMessage[];
|
|
25
25
|
registry: PluginRegistry;
|
|
26
|
+
messageBus?: HostMessageBus;
|
|
26
27
|
uiService?: InkUIService;
|
|
27
28
|
shutdown?: ShutdownFn;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export function useChatPanel(options: UseChatPanelOptions) {
|
|
31
|
-
const { config, initialMessages, registry, uiService, shutdown } = options;
|
|
32
|
-
const ctx = useChat(config, registry, initialMessages, shutdown);
|
|
32
|
+
const { config, initialMessages, registry, messageBus, uiService, shutdown } = options;
|
|
33
|
+
const ctx = useChat(config, registry, initialMessages, shutdown, uiService, messageBus);
|
|
33
34
|
const { width, height } = useTerminalSize();
|
|
34
35
|
const viewRef = useRef<InkDOMElement>(null);
|
|
35
36
|
const contentRef = useRef<InkDOMElement>(null);
|
|
@@ -64,7 +65,8 @@ export function useChatPanel(options: UseChatPanelOptions) {
|
|
|
64
65
|
quitWarning: ctx.abort.quitWarning,
|
|
65
66
|
error: ctx.session.error,
|
|
66
67
|
modelError: ctx.models.modelError,
|
|
67
|
-
|
|
68
|
+
totalTokens: ctx.session.stream.totalTokens,
|
|
69
|
+
cachedTokens: ctx.session.stream.cachedTokens,
|
|
68
70
|
pluginStatus,
|
|
69
71
|
});
|
|
70
72
|
|