mu-harness 0.23.0 → 0.24.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/esm/channels/ws/client.js +9 -0
- package/esm/channels/ws/protocol.d.ts +4 -0
- package/esm/channels/ws/server.js +13 -6
- package/esm/tui/chat/ChatApp.d.ts +4 -0
- package/esm/tui/chat/ChatApp.js +18 -0
- package/package.json +3 -3
- package/script/channels/ws/client.js +9 -0
- package/script/channels/ws/protocol.d.ts +4 -0
- package/script/channels/ws/server.js +13 -6
- package/script/tui/chat/ChatApp.d.ts +4 -0
- package/script/tui/chat/ChatApp.js +18 -0
|
@@ -38,6 +38,7 @@ export async function connectHarness(opts) {
|
|
|
38
38
|
const subSessions = new Map();
|
|
39
39
|
const approvalListeners = new Set();
|
|
40
40
|
const subAgentListeners = new Set();
|
|
41
|
+
const modelLoadingListeners = new Set();
|
|
41
42
|
const forkWaiters = new Map();
|
|
42
43
|
const subagentWaiters = new Map();
|
|
43
44
|
const listWaiters = [];
|
|
@@ -144,6 +145,10 @@ export async function connectHarness(opts) {
|
|
|
144
145
|
features.vision = frame.vision;
|
|
145
146
|
features.audio = frame.audio;
|
|
146
147
|
return;
|
|
148
|
+
case 'model_loading':
|
|
149
|
+
for (const l of [...modelLoadingListeners])
|
|
150
|
+
l(frame.model, frame.loading);
|
|
151
|
+
return;
|
|
147
152
|
case 'models:listed': {
|
|
148
153
|
models = frame.models.map((m) => ({ id: m.id, ownedBy: m.ownedBy }));
|
|
149
154
|
modelSelected = frame.selected;
|
|
@@ -300,6 +305,10 @@ export async function connectHarness(opts) {
|
|
|
300
305
|
saveThinking: opts.saveThinking ?? (() => { }),
|
|
301
306
|
history: opts.history,
|
|
302
307
|
features,
|
|
308
|
+
subscribeModelLoading: (listener) => {
|
|
309
|
+
modelLoadingListeners.add(listener);
|
|
310
|
+
return () => modelLoadingListeners.delete(listener);
|
|
311
|
+
},
|
|
303
312
|
banner: opts.banner,
|
|
304
313
|
minimal: opts.minimal,
|
|
305
314
|
commands: () => commands.map((c) => ({ name: c.command.replace(/^\//, ''), description: c.description })),
|
|
@@ -164,12 +164,19 @@ export function webSocketAdapter(opts) {
|
|
|
164
164
|
const frame = await modelsFrame();
|
|
165
165
|
if (frame)
|
|
166
166
|
push(frame);
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
167
|
+
// Detecting the new model's modalities loads it (a /props round-trip can be a
|
|
168
|
+
// 10-30s cold start) — surface that as a loading state to every channel.
|
|
169
|
+
push({ type: 'model_loading', model: msg.ref, loading: true });
|
|
170
|
+
try {
|
|
171
|
+
const modalities = await harness.models.capabilities(msg.ref).catch(() => undefined);
|
|
172
|
+
if (modalities) {
|
|
173
|
+
caps.vision = modalities.vision;
|
|
174
|
+
caps.audio = modalities.audio;
|
|
175
|
+
push({ type: 'capabilities', vision: caps.vision, audio: caps.audio });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
push({ type: 'model_loading', model: msg.ref, loading: false });
|
|
173
180
|
}
|
|
174
181
|
return;
|
|
175
182
|
}
|
|
@@ -41,6 +41,8 @@ export interface ChatHost {
|
|
|
41
41
|
append(text: string): void;
|
|
42
42
|
};
|
|
43
43
|
features?: ChatFeatures;
|
|
44
|
+
/** Subscribe to model load/unload state (cold-start). Optional — only WS-backed hosts emit it. */
|
|
45
|
+
subscribeModelLoading?(listener: (model: string, loading: boolean) => void): () => void;
|
|
44
46
|
banner?: string;
|
|
45
47
|
minimal?: boolean;
|
|
46
48
|
commands?(): {
|
|
@@ -72,6 +74,7 @@ export declare class ChatApp {
|
|
|
72
74
|
private unsubscribe;
|
|
73
75
|
private unsubscribeTheme;
|
|
74
76
|
private unsubscribeSubAgents;
|
|
77
|
+
private unsubscribeModelLoading;
|
|
75
78
|
private readonly runUnsubs;
|
|
76
79
|
private readonly activeRuns;
|
|
77
80
|
private mentionAc;
|
|
@@ -120,6 +123,7 @@ export declare class ChatApp {
|
|
|
120
123
|
private tryDispatch;
|
|
121
124
|
private dispatchMention;
|
|
122
125
|
private swapSession;
|
|
126
|
+
private onModelLoading;
|
|
123
127
|
private handleEvent;
|
|
124
128
|
private applyUsage;
|
|
125
129
|
private onTurnComplete;
|
package/esm/tui/chat/ChatApp.js
CHANGED
|
@@ -99,6 +99,7 @@ export class ChatApp {
|
|
|
99
99
|
unsubscribe;
|
|
100
100
|
unsubscribeTheme;
|
|
101
101
|
unsubscribeSubAgents;
|
|
102
|
+
unsubscribeModelLoading;
|
|
102
103
|
runUnsubs = new Set();
|
|
103
104
|
activeRuns = new Set();
|
|
104
105
|
mentionAc;
|
|
@@ -194,6 +195,7 @@ export class ChatApp {
|
|
|
194
195
|
this.tui.setToastForeground(this.theme().colors.text);
|
|
195
196
|
this.tui.requestRender(true);
|
|
196
197
|
});
|
|
198
|
+
this.unsubscribeModelLoading = this.host.subscribeModelLoading?.((model, loading) => this.onModelLoading(model, loading));
|
|
197
199
|
this.bindSession();
|
|
198
200
|
if (this.feature('subAgents')) {
|
|
199
201
|
this.unsubscribeSubAgents = this.host.subAgents.subscribe((run) => this.onSubAgentRun(run));
|
|
@@ -219,6 +221,7 @@ export class ChatApp {
|
|
|
219
221
|
this.unsubscribeTheme?.();
|
|
220
222
|
this.unsubscribeSubAgents?.();
|
|
221
223
|
this.unsubscribeApproval?.();
|
|
224
|
+
this.unsubscribeModelLoading?.();
|
|
222
225
|
this.clearRuns();
|
|
223
226
|
this.stopSpinner();
|
|
224
227
|
this.clearError();
|
|
@@ -431,6 +434,21 @@ export class ChatApp {
|
|
|
431
434
|
this.setStatus('ready');
|
|
432
435
|
this.tui.requestRender(true);
|
|
433
436
|
}
|
|
437
|
+
onModelLoading(model, loading) {
|
|
438
|
+
const name = model.split('/').pop() ?? model;
|
|
439
|
+
if (loading) {
|
|
440
|
+
this.status.busy = true;
|
|
441
|
+
this.setStatus(`loading ${name}…`);
|
|
442
|
+
this.startSpinner();
|
|
443
|
+
}
|
|
444
|
+
else if (!this.running) {
|
|
445
|
+
// Don't clobber an in-flight turn's status when the load finishes.
|
|
446
|
+
this.status.busy = false;
|
|
447
|
+
this.stopSpinner();
|
|
448
|
+
this.setStatus('ready');
|
|
449
|
+
}
|
|
450
|
+
this.tui.requestRender();
|
|
451
|
+
}
|
|
434
452
|
handleEvent(event) {
|
|
435
453
|
this.updateSpeaker();
|
|
436
454
|
this.transcript.applyEvent(event);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mu-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Agent harness: createHarness wires mu-core into a host — XDG paths, model registry, plugins, disk-loaded agents & skills, sub-agents, sessions (JSONL + SQLite catalog), slash commands, permission/approval hooks, an optional scheduler, and a composable TUI chat app",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "./script/index.js",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"@swc/wasm-typescript": "^1.15.0",
|
|
24
24
|
"cli-highlight": "^2.1.11",
|
|
25
25
|
"croner": "^9.0.0",
|
|
26
|
-
"mu-core": "^0.
|
|
27
|
-
"mu-tui": "^0.
|
|
26
|
+
"mu-core": "^0.24.0",
|
|
27
|
+
"mu-tui": "^0.24.0",
|
|
28
28
|
"ws": "^8.18.0"
|
|
29
29
|
},
|
|
30
30
|
"_generatedBy": "dnt@dev",
|
|
@@ -41,6 +41,7 @@ async function connectHarness(opts) {
|
|
|
41
41
|
const subSessions = new Map();
|
|
42
42
|
const approvalListeners = new Set();
|
|
43
43
|
const subAgentListeners = new Set();
|
|
44
|
+
const modelLoadingListeners = new Set();
|
|
44
45
|
const forkWaiters = new Map();
|
|
45
46
|
const subagentWaiters = new Map();
|
|
46
47
|
const listWaiters = [];
|
|
@@ -147,6 +148,10 @@ async function connectHarness(opts) {
|
|
|
147
148
|
features.vision = frame.vision;
|
|
148
149
|
features.audio = frame.audio;
|
|
149
150
|
return;
|
|
151
|
+
case 'model_loading':
|
|
152
|
+
for (const l of [...modelLoadingListeners])
|
|
153
|
+
l(frame.model, frame.loading);
|
|
154
|
+
return;
|
|
150
155
|
case 'models:listed': {
|
|
151
156
|
models = frame.models.map((m) => ({ id: m.id, ownedBy: m.ownedBy }));
|
|
152
157
|
modelSelected = frame.selected;
|
|
@@ -303,6 +308,10 @@ async function connectHarness(opts) {
|
|
|
303
308
|
saveThinking: opts.saveThinking ?? (() => { }),
|
|
304
309
|
history: opts.history,
|
|
305
310
|
features,
|
|
311
|
+
subscribeModelLoading: (listener) => {
|
|
312
|
+
modelLoadingListeners.add(listener);
|
|
313
|
+
return () => modelLoadingListeners.delete(listener);
|
|
314
|
+
},
|
|
306
315
|
banner: opts.banner,
|
|
307
316
|
minimal: opts.minimal,
|
|
308
317
|
commands: () => commands.map((c) => ({ name: c.command.replace(/^\//, ''), description: c.description })),
|
|
@@ -167,12 +167,19 @@ function webSocketAdapter(opts) {
|
|
|
167
167
|
const frame = await modelsFrame();
|
|
168
168
|
if (frame)
|
|
169
169
|
push(frame);
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
170
|
+
// Detecting the new model's modalities loads it (a /props round-trip can be a
|
|
171
|
+
// 10-30s cold start) — surface that as a loading state to every channel.
|
|
172
|
+
push({ type: 'model_loading', model: msg.ref, loading: true });
|
|
173
|
+
try {
|
|
174
|
+
const modalities = await harness.models.capabilities(msg.ref).catch(() => undefined);
|
|
175
|
+
if (modalities) {
|
|
176
|
+
caps.vision = modalities.vision;
|
|
177
|
+
caps.audio = modalities.audio;
|
|
178
|
+
push({ type: 'capabilities', vision: caps.vision, audio: caps.audio });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
push({ type: 'model_loading', model: msg.ref, loading: false });
|
|
176
183
|
}
|
|
177
184
|
return;
|
|
178
185
|
}
|
|
@@ -41,6 +41,8 @@ export interface ChatHost {
|
|
|
41
41
|
append(text: string): void;
|
|
42
42
|
};
|
|
43
43
|
features?: ChatFeatures;
|
|
44
|
+
/** Subscribe to model load/unload state (cold-start). Optional — only WS-backed hosts emit it. */
|
|
45
|
+
subscribeModelLoading?(listener: (model: string, loading: boolean) => void): () => void;
|
|
44
46
|
banner?: string;
|
|
45
47
|
minimal?: boolean;
|
|
46
48
|
commands?(): {
|
|
@@ -72,6 +74,7 @@ export declare class ChatApp {
|
|
|
72
74
|
private unsubscribe;
|
|
73
75
|
private unsubscribeTheme;
|
|
74
76
|
private unsubscribeSubAgents;
|
|
77
|
+
private unsubscribeModelLoading;
|
|
75
78
|
private readonly runUnsubs;
|
|
76
79
|
private readonly activeRuns;
|
|
77
80
|
private mentionAc;
|
|
@@ -120,6 +123,7 @@ export declare class ChatApp {
|
|
|
120
123
|
private tryDispatch;
|
|
121
124
|
private dispatchMention;
|
|
122
125
|
private swapSession;
|
|
126
|
+
private onModelLoading;
|
|
123
127
|
private handleEvent;
|
|
124
128
|
private applyUsage;
|
|
125
129
|
private onTurnComplete;
|
|
@@ -102,6 +102,7 @@ class ChatApp {
|
|
|
102
102
|
unsubscribe;
|
|
103
103
|
unsubscribeTheme;
|
|
104
104
|
unsubscribeSubAgents;
|
|
105
|
+
unsubscribeModelLoading;
|
|
105
106
|
runUnsubs = new Set();
|
|
106
107
|
activeRuns = new Set();
|
|
107
108
|
mentionAc;
|
|
@@ -197,6 +198,7 @@ class ChatApp {
|
|
|
197
198
|
this.tui.setToastForeground(this.theme().colors.text);
|
|
198
199
|
this.tui.requestRender(true);
|
|
199
200
|
});
|
|
201
|
+
this.unsubscribeModelLoading = this.host.subscribeModelLoading?.((model, loading) => this.onModelLoading(model, loading));
|
|
200
202
|
this.bindSession();
|
|
201
203
|
if (this.feature('subAgents')) {
|
|
202
204
|
this.unsubscribeSubAgents = this.host.subAgents.subscribe((run) => this.onSubAgentRun(run));
|
|
@@ -222,6 +224,7 @@ class ChatApp {
|
|
|
222
224
|
this.unsubscribeTheme?.();
|
|
223
225
|
this.unsubscribeSubAgents?.();
|
|
224
226
|
this.unsubscribeApproval?.();
|
|
227
|
+
this.unsubscribeModelLoading?.();
|
|
225
228
|
this.clearRuns();
|
|
226
229
|
this.stopSpinner();
|
|
227
230
|
this.clearError();
|
|
@@ -434,6 +437,21 @@ class ChatApp {
|
|
|
434
437
|
this.setStatus('ready');
|
|
435
438
|
this.tui.requestRender(true);
|
|
436
439
|
}
|
|
440
|
+
onModelLoading(model, loading) {
|
|
441
|
+
const name = model.split('/').pop() ?? model;
|
|
442
|
+
if (loading) {
|
|
443
|
+
this.status.busy = true;
|
|
444
|
+
this.setStatus(`loading ${name}…`);
|
|
445
|
+
this.startSpinner();
|
|
446
|
+
}
|
|
447
|
+
else if (!this.running) {
|
|
448
|
+
// Don't clobber an in-flight turn's status when the load finishes.
|
|
449
|
+
this.status.busy = false;
|
|
450
|
+
this.stopSpinner();
|
|
451
|
+
this.setStatus('ready');
|
|
452
|
+
}
|
|
453
|
+
this.tui.requestRender();
|
|
454
|
+
}
|
|
437
455
|
handleEvent(event) {
|
|
438
456
|
this.updateSpeaker();
|
|
439
457
|
this.transcript.applyEvent(event);
|