@femtomc/mu-agent 26.2.107 → 26.2.109
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 +5 -22
- package/assets/mu-tui-logo.png +0 -0
- package/dist/extensions/index.d.ts +0 -1
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +0 -1
- package/dist/extensions/mu-operator.d.ts.map +1 -1
- package/dist/extensions/mu-operator.js +0 -2
- package/dist/extensions/mu-serve.d.ts.map +1 -1
- package/dist/extensions/mu-serve.js +0 -2
- package/dist/extensions/ui.d.ts.map +1 -1
- package/dist/extensions/ui.js +806 -34
- package/dist/operator.d.ts +7 -337
- package/dist/operator.d.ts.map +1 -1
- package/dist/operator.js +1 -62
- package/package.json +33 -33
- package/prompts/skills/core/SKILL.md +8 -5
- package/prompts/skills/core/memory/SKILL.md +2 -2
- package/prompts/skills/core/mu/SKILL.md +20 -7
- package/prompts/skills/core/programmable-ui/SKILL.md +299 -0
- package/prompts/skills/subagents/SKILL.md +92 -8
- package/prompts/skills/subagents/control-flow/SKILL.md +118 -13
- package/prompts/skills/subagents/execution/SKILL.md +144 -31
- package/prompts/skills/subagents/model-routing/SKILL.md +146 -13
- package/prompts/skills/subagents/planning/SKILL.md +239 -90
- package/prompts/skills/writing/SKILL.md +2 -2
- package/dist/extensions/hud.d.ts +0 -4
- package/dist/extensions/hud.d.ts.map +0 -1
- package/dist/extensions/hud.js +0 -483
- package/prompts/skills/subagents/hud/SKILL.md +0 -205
package/dist/extensions/ui.js
CHANGED
|
@@ -1,13 +1,34 @@
|
|
|
1
|
-
import { normalizeUiDocs, parseUiDoc, } from "@femtomc/mu-core";
|
|
1
|
+
import { normalizeUiDocs, parseUiDoc, resolveUiStatusProfileName, uiStatusProfileWarnings, } from "@femtomc/mu-core";
|
|
2
|
+
import { matchesKey } from "@mariozechner/pi-tui";
|
|
2
3
|
import { registerMuSubcommand } from "./mu-command-dispatcher.js";
|
|
3
4
|
const UI_DISPLAY_DOCS_MAX = 16;
|
|
4
5
|
const UI_WIDGET_COMPONENTS_MAX = 6;
|
|
5
6
|
const UI_WIDGET_ACTIONS_MAX = 4;
|
|
7
|
+
const UI_WIDGET_COMPONENT_DETAILS_MAX = 3;
|
|
8
|
+
const UI_PICKER_COMPONENTS_MAX = 8;
|
|
9
|
+
const UI_PICKER_LIST_ITEMS_MAX = 4;
|
|
10
|
+
const UI_PICKER_KEYVALUE_ROWS_MAX = 4;
|
|
6
11
|
const UI_SESSION_KEY_FALLBACK = "__mu_ui_active_session__";
|
|
12
|
+
const UI_PROMPT_PREVIEW_MAX = 160;
|
|
13
|
+
const UI_INTERACT_SHORTCUT = "ctrl+shift+u";
|
|
14
|
+
const UI_INTERACT_OVERLAY_OPTIONS = {
|
|
15
|
+
anchor: "top-left",
|
|
16
|
+
row: 0,
|
|
17
|
+
col: 0,
|
|
18
|
+
width: "100%",
|
|
19
|
+
maxHeight: "100%",
|
|
20
|
+
margin: 0,
|
|
21
|
+
};
|
|
7
22
|
const STATE_BY_SESSION = new Map();
|
|
8
23
|
const UI_STATE_TTL_MS = 30 * 60 * 1000; // keep session state for 30 minutes after last access
|
|
9
24
|
function createState() {
|
|
10
|
-
return {
|
|
25
|
+
return {
|
|
26
|
+
docsById: new Map(),
|
|
27
|
+
pendingPrompt: null,
|
|
28
|
+
promptedRevisionKeys: new Set(),
|
|
29
|
+
awaitingUiIds: new Set(),
|
|
30
|
+
interactionDepth: 0,
|
|
31
|
+
};
|
|
11
32
|
}
|
|
12
33
|
function pruneStaleStates(nowMs) {
|
|
13
34
|
for (const [key, entry] of STATE_BY_SESSION.entries()) {
|
|
@@ -45,6 +66,60 @@ function touchState(key) {
|
|
|
45
66
|
function activeDocs(state, maxDocs = UI_DISPLAY_DOCS_MAX) {
|
|
46
67
|
return normalizeUiDocs([...state.docsById.values()], { maxDocs });
|
|
47
68
|
}
|
|
69
|
+
function docRevisionKey(doc) {
|
|
70
|
+
return `${doc.ui_id}:${doc.revision.id}:${doc.revision.version}`;
|
|
71
|
+
}
|
|
72
|
+
function retainPromptedRevisionKeysForActiveDocs(state) {
|
|
73
|
+
const activeRevisionKeys = new Set();
|
|
74
|
+
for (const doc of state.docsById.values()) {
|
|
75
|
+
activeRevisionKeys.add(docRevisionKey(doc));
|
|
76
|
+
}
|
|
77
|
+
for (const key of [...state.promptedRevisionKeys]) {
|
|
78
|
+
if (!activeRevisionKeys.has(key)) {
|
|
79
|
+
state.promptedRevisionKeys.delete(key);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function retainAwaitingUiIdsForActiveDocs(state) {
|
|
84
|
+
for (const uiId of [...state.awaitingUiIds]) {
|
|
85
|
+
const doc = state.docsById.get(uiId);
|
|
86
|
+
if (!doc || runnableActions(doc).length === 0) {
|
|
87
|
+
state.awaitingUiIds.delete(uiId);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function armAutoPromptForUiDocs(state, changedUiIds) {
|
|
92
|
+
if (changedUiIds.length === 0) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const changedDocs = [];
|
|
96
|
+
for (const uiId of changedUiIds) {
|
|
97
|
+
const doc = state.docsById.get(uiId);
|
|
98
|
+
if (doc) {
|
|
99
|
+
changedDocs.push(doc);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const candidates = changedDocs.filter((doc) => {
|
|
103
|
+
if (runnableActions(doc).length === 0) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
return !state.promptedRevisionKeys.has(docRevisionKey(doc));
|
|
107
|
+
});
|
|
108
|
+
if (candidates.length === 0) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
candidates.sort((left, right) => {
|
|
112
|
+
if (left.updated_at_ms !== right.updated_at_ms) {
|
|
113
|
+
return right.updated_at_ms - left.updated_at_ms;
|
|
114
|
+
}
|
|
115
|
+
return left.ui_id.localeCompare(right.ui_id);
|
|
116
|
+
});
|
|
117
|
+
const doc = candidates[0];
|
|
118
|
+
const actions = runnableActions(doc);
|
|
119
|
+
const actionId = actions.length === 1 ? actions[0].id : undefined;
|
|
120
|
+
state.pendingPrompt = { uiId: doc.ui_id, actionId };
|
|
121
|
+
state.promptedRevisionKeys.add(docRevisionKey(doc));
|
|
122
|
+
}
|
|
48
123
|
function preferredDocForState(state, candidate) {
|
|
49
124
|
const existing = state.docsById.get(candidate.ui_id);
|
|
50
125
|
if (!existing) {
|
|
@@ -54,6 +129,26 @@ function preferredDocForState(state, candidate) {
|
|
|
54
129
|
const chosen = merged.find((doc) => doc.ui_id === candidate.ui_id);
|
|
55
130
|
return chosen ?? candidate;
|
|
56
131
|
}
|
|
132
|
+
function awaitingDocs(state, docs) {
|
|
133
|
+
return docs.filter((doc) => state.awaitingUiIds.has(doc.ui_id) && runnableActions(doc).length > 0);
|
|
134
|
+
}
|
|
135
|
+
function beginUiInteraction(ctx, state) {
|
|
136
|
+
state.interactionDepth += 1;
|
|
137
|
+
refreshUi(ctx);
|
|
138
|
+
}
|
|
139
|
+
function endUiInteraction(ctx, state) {
|
|
140
|
+
state.interactionDepth = Math.max(0, state.interactionDepth - 1);
|
|
141
|
+
refreshUi(ctx);
|
|
142
|
+
}
|
|
143
|
+
async function withUiInteraction(ctx, state, run) {
|
|
144
|
+
beginUiInteraction(ctx, state);
|
|
145
|
+
try {
|
|
146
|
+
return await run();
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
endUiInteraction(ctx, state);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
57
152
|
function short(text, max = 64) {
|
|
58
153
|
const normalized = text.replace(/\s+/g, " ").trim();
|
|
59
154
|
if (normalized.length <= max) {
|
|
@@ -64,27 +159,119 @@ function short(text, max = 64) {
|
|
|
64
159
|
}
|
|
65
160
|
return `${normalized.slice(0, max - 1)}…`;
|
|
66
161
|
}
|
|
67
|
-
function
|
|
162
|
+
function statusProfileId(doc) {
|
|
163
|
+
return resolveUiStatusProfileName(doc);
|
|
164
|
+
}
|
|
165
|
+
function statusProfileMetadata(doc) {
|
|
166
|
+
const profile = doc.metadata.profile;
|
|
167
|
+
if (!isPlainObject(profile)) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
return profile;
|
|
171
|
+
}
|
|
172
|
+
function statusProfileVariant(doc) {
|
|
173
|
+
const profile = statusProfileMetadata(doc);
|
|
174
|
+
const rawVariant = typeof profile?.variant === "string" ? profile.variant.trim().toLowerCase() : "";
|
|
175
|
+
return rawVariant.length > 0 ? rawVariant : "status";
|
|
176
|
+
}
|
|
177
|
+
function isStatusProfileStatusVariant(doc) {
|
|
178
|
+
return statusProfileId(doc) !== null && statusProfileVariant(doc) === "status";
|
|
179
|
+
}
|
|
180
|
+
function statusProfileSnapshot(doc, format) {
|
|
181
|
+
if (!isStatusProfileStatusVariant(doc)) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
const profile = statusProfileMetadata(doc);
|
|
185
|
+
if (!profile) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
const snapshot = profile.snapshot;
|
|
189
|
+
if (!isPlainObject(snapshot)) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const raw = snapshot[format];
|
|
193
|
+
if (typeof raw !== "string") {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const normalized = raw.trim();
|
|
197
|
+
return normalized.length > 0 ? normalized : null;
|
|
198
|
+
}
|
|
199
|
+
function compactSnapshotValue(doc) {
|
|
200
|
+
const profileCompact = statusProfileSnapshot(doc, "compact");
|
|
201
|
+
if (profileCompact) {
|
|
202
|
+
return short(profileCompact, 96);
|
|
203
|
+
}
|
|
204
|
+
if (doc.summary) {
|
|
205
|
+
return short(doc.summary, 64);
|
|
206
|
+
}
|
|
207
|
+
return `${short(doc.title, 32)} (${doc.revision.version})`;
|
|
208
|
+
}
|
|
209
|
+
function statusProfileDocCount(docs) {
|
|
210
|
+
return docs.reduce((count, doc) => count + (isStatusProfileStatusVariant(doc) ? 1 : 0), 0);
|
|
211
|
+
}
|
|
212
|
+
function statusProfileWarningCount(docs) {
|
|
213
|
+
return docs.reduce((count, doc) => count + uiStatusProfileWarnings(doc).length, 0);
|
|
214
|
+
}
|
|
215
|
+
function statusSummary(docs, awaitingCount = 0) {
|
|
68
216
|
const ids = docs.map((doc) => doc.ui_id).join(", ") || "(none)";
|
|
69
|
-
|
|
217
|
+
const parts = [`UI docs: ${docs.length}`, `ids: ${ids}`];
|
|
218
|
+
const statusProfiles = statusProfileDocCount(docs);
|
|
219
|
+
if (statusProfiles > 0) {
|
|
220
|
+
parts.push(`status_profiles: ${statusProfiles}`);
|
|
221
|
+
}
|
|
222
|
+
const warningCount = statusProfileWarningCount(docs);
|
|
223
|
+
if (warningCount > 0) {
|
|
224
|
+
parts.push(`profile_warnings: ${warningCount}`);
|
|
225
|
+
}
|
|
226
|
+
if (awaitingCount > 0) {
|
|
227
|
+
parts.push(`awaiting: ${awaitingCount}`);
|
|
228
|
+
}
|
|
229
|
+
return parts.join(" · ");
|
|
70
230
|
}
|
|
71
231
|
function snapshotText(docs, format) {
|
|
72
232
|
if (docs.length === 0) {
|
|
73
233
|
return "(no UI docs)";
|
|
74
234
|
}
|
|
75
235
|
if (format === "compact") {
|
|
76
|
-
return docs
|
|
77
|
-
.map((doc) => `${doc.ui_id}: ${short(doc.title, 32)} (${doc.revision.version})`)
|
|
78
|
-
.join(" | ");
|
|
236
|
+
return docs.map((doc) => `${doc.ui_id}: ${compactSnapshotValue(doc)}`).join(" | ");
|
|
79
237
|
}
|
|
80
238
|
const lines = [];
|
|
81
239
|
docs.slice(0, 8).forEach((doc, idx) => {
|
|
82
|
-
|
|
83
|
-
|
|
240
|
+
const profileId = statusProfileId(doc);
|
|
241
|
+
lines.push(`${idx + 1}. ${doc.title} [${doc.ui_id}]${profileId ? ` profile=${profileId}` : ""}`);
|
|
242
|
+
const profileMultiline = statusProfileSnapshot(doc, "multiline");
|
|
243
|
+
if (profileMultiline) {
|
|
244
|
+
const snapshotLines = profileMultiline
|
|
245
|
+
.split(/\r?\n/)
|
|
246
|
+
.map((line) => line.trim())
|
|
247
|
+
.filter((line) => line.length > 0);
|
|
248
|
+
if (snapshotLines.length > 0) {
|
|
249
|
+
for (const line of snapshotLines) {
|
|
250
|
+
lines.push(` ${short(line, 120)}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
lines.push(` snapshot: ${compactSnapshotValue(doc)}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
else if (isStatusProfileStatusVariant(doc)) {
|
|
258
|
+
lines.push(` snapshot: ${compactSnapshotValue(doc)}`);
|
|
259
|
+
}
|
|
260
|
+
else if (doc.summary) {
|
|
84
261
|
lines.push(` summary: ${short(doc.summary, 120)}`);
|
|
85
262
|
}
|
|
263
|
+
if (isStatusProfileStatusVariant(doc)) {
|
|
264
|
+
if (doc.actions.length > 0) {
|
|
265
|
+
lines.push(` actions omitted for status profile (${doc.actions.length})`);
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
86
269
|
if (doc.actions.length > 0) {
|
|
87
|
-
|
|
270
|
+
const labels = [...doc.actions]
|
|
271
|
+
.sort((left, right) => left.id.localeCompare(right.id))
|
|
272
|
+
.map((action) => action.label)
|
|
273
|
+
.join(", ");
|
|
274
|
+
lines.push(` actions: ${labels}`);
|
|
88
275
|
}
|
|
89
276
|
});
|
|
90
277
|
if (docs.length > 8) {
|
|
@@ -116,22 +303,397 @@ function parseDocListInput(value) {
|
|
|
116
303
|
}
|
|
117
304
|
return { ok: true, docs: normalizeUiDocs(docs, { maxDocs: UI_DISPLAY_DOCS_MAX }) };
|
|
118
305
|
}
|
|
306
|
+
function statusProfileWarningsExtraForDoc(doc) {
|
|
307
|
+
const warnings = uiStatusProfileWarnings(doc);
|
|
308
|
+
if (warnings.length === 0) {
|
|
309
|
+
return {};
|
|
310
|
+
}
|
|
311
|
+
return { profile_warnings: warnings };
|
|
312
|
+
}
|
|
313
|
+
function statusProfileWarningsExtraForDocs(docs) {
|
|
314
|
+
const byUiId = {};
|
|
315
|
+
for (const doc of docs) {
|
|
316
|
+
const warnings = uiStatusProfileWarnings(doc);
|
|
317
|
+
if (warnings.length > 0) {
|
|
318
|
+
byUiId[doc.ui_id] = warnings;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return Object.keys(byUiId).length > 0 ? { profile_warnings: byUiId } : {};
|
|
322
|
+
}
|
|
119
323
|
function parseSnapshotFormat(raw) {
|
|
120
324
|
const normalized = (raw ?? "compact").trim().toLowerCase();
|
|
121
325
|
return normalized === "multiline" ? "multiline" : "compact";
|
|
122
326
|
}
|
|
327
|
+
function isPlainObject(value) {
|
|
328
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
329
|
+
}
|
|
330
|
+
function actionCommandText(action) {
|
|
331
|
+
const raw = typeof action.metadata.command_text === "string" ? action.metadata.command_text.trim() : "";
|
|
332
|
+
return raw.length > 0 ? raw : null;
|
|
333
|
+
}
|
|
334
|
+
function extractTemplateKeys(text) {
|
|
335
|
+
const keys = [];
|
|
336
|
+
const seen = new Set();
|
|
337
|
+
const re = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
|
|
338
|
+
let match;
|
|
339
|
+
while ((match = re.exec(text)) !== null) {
|
|
340
|
+
const key = (match[1] ?? "").trim();
|
|
341
|
+
if (!key || seen.has(key)) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
seen.add(key);
|
|
345
|
+
keys.push(key);
|
|
346
|
+
}
|
|
347
|
+
return keys;
|
|
348
|
+
}
|
|
349
|
+
function escapeRegExp(value) {
|
|
350
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
351
|
+
}
|
|
352
|
+
function replaceTemplateValues(template, values) {
|
|
353
|
+
let out = template;
|
|
354
|
+
for (const [key, value] of Object.entries(values)) {
|
|
355
|
+
const keyRe = new RegExp(`\\{\\{\\s*${escapeRegExp(key)}\\s*\\}\\}`, "g");
|
|
356
|
+
out = out.replace(keyRe, value);
|
|
357
|
+
}
|
|
358
|
+
return out;
|
|
359
|
+
}
|
|
360
|
+
function primitiveTemplateDefault(value) {
|
|
361
|
+
switch (typeof value) {
|
|
362
|
+
case "string":
|
|
363
|
+
return value;
|
|
364
|
+
case "number":
|
|
365
|
+
case "boolean":
|
|
366
|
+
return String(value);
|
|
367
|
+
default:
|
|
368
|
+
return value === null ? "" : null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function valueAtTemplatePath(root, key) {
|
|
372
|
+
if (!key) {
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
const segments = key.split(".").filter((segment) => segment.length > 0);
|
|
376
|
+
if (segments.length === 0) {
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
let current = root;
|
|
380
|
+
for (const segment of segments) {
|
|
381
|
+
if (Array.isArray(current)) {
|
|
382
|
+
const index = Number.parseInt(segment, 10);
|
|
383
|
+
if (!Number.isInteger(index) || index < 0 || index >= current.length) {
|
|
384
|
+
return undefined;
|
|
385
|
+
}
|
|
386
|
+
current = current[index];
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (!isPlainObject(current)) {
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
current = current[segment];
|
|
393
|
+
}
|
|
394
|
+
return current;
|
|
395
|
+
}
|
|
396
|
+
function templateDefaultsForAction(action, keys) {
|
|
397
|
+
if (!isPlainObject(action.payload) || keys.length === 0) {
|
|
398
|
+
return {};
|
|
399
|
+
}
|
|
400
|
+
const payload = action.payload;
|
|
401
|
+
const out = {};
|
|
402
|
+
for (const key of keys) {
|
|
403
|
+
const fallback = primitiveTemplateDefault(valueAtTemplatePath(payload, key));
|
|
404
|
+
if (fallback !== null) {
|
|
405
|
+
out[key] = fallback;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
async function collectTemplateValues(opts) {
|
|
411
|
+
if (opts.templateKeys.length === 0) {
|
|
412
|
+
return {};
|
|
413
|
+
}
|
|
414
|
+
const defaults = templateDefaultsForAction(opts.action, opts.templateKeys);
|
|
415
|
+
const out = { ...defaults };
|
|
416
|
+
for (const key of opts.templateKeys) {
|
|
417
|
+
const defaultValue = defaults[key];
|
|
418
|
+
if (typeof defaultValue === "string" && defaultValue.trim().length > 0) {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
const placeholder = typeof defaultValue === "string" ? defaultValue : `value for ${key}`;
|
|
422
|
+
const entered = await opts.ctx.ui.input(`UI field: ${key}`, placeholder);
|
|
423
|
+
if (entered === undefined) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
out[key] = entered;
|
|
427
|
+
}
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
function composePromptFromAction(opts) {
|
|
431
|
+
const rendered = replaceTemplateValues(opts.commandText, opts.templateValues).trim();
|
|
432
|
+
const unresolvedKeys = extractTemplateKeys(rendered);
|
|
433
|
+
if (unresolvedKeys.length === 0) {
|
|
434
|
+
return rendered;
|
|
435
|
+
}
|
|
436
|
+
const unresolvedLines = unresolvedKeys.map((key) => `- ${key}: (missing)`);
|
|
437
|
+
return [rendered, "", "Missing template values:", ...unresolvedLines].join("\n");
|
|
438
|
+
}
|
|
439
|
+
function runnableActions(doc) {
|
|
440
|
+
if (isStatusProfileStatusVariant(doc)) {
|
|
441
|
+
return [];
|
|
442
|
+
}
|
|
443
|
+
return doc.actions.filter((action) => actionCommandText(action) !== null);
|
|
444
|
+
}
|
|
445
|
+
function boundedIndex(index, length) {
|
|
446
|
+
if (length <= 0) {
|
|
447
|
+
return 0;
|
|
448
|
+
}
|
|
449
|
+
if (index < 0) {
|
|
450
|
+
return 0;
|
|
451
|
+
}
|
|
452
|
+
if (index >= length) {
|
|
453
|
+
return length - 1;
|
|
454
|
+
}
|
|
455
|
+
return index;
|
|
456
|
+
}
|
|
457
|
+
function pickerComponentLines(component) {
|
|
458
|
+
switch (component.kind) {
|
|
459
|
+
case "text":
|
|
460
|
+
return [`text · ${component.text}`];
|
|
461
|
+
case "list": {
|
|
462
|
+
const lines = [`list${component.title ? ` · ${component.title}` : ""}`];
|
|
463
|
+
const visible = component.items.slice(0, UI_PICKER_LIST_ITEMS_MAX);
|
|
464
|
+
for (const item of visible) {
|
|
465
|
+
const detail = item.detail ? ` — ${item.detail}` : "";
|
|
466
|
+
lines.push(`• ${item.label}${detail}`);
|
|
467
|
+
}
|
|
468
|
+
if (component.items.length > visible.length) {
|
|
469
|
+
lines.push(`... (+${component.items.length - visible.length} more items)`);
|
|
470
|
+
}
|
|
471
|
+
return lines;
|
|
472
|
+
}
|
|
473
|
+
case "key_value": {
|
|
474
|
+
const lines = [`key_value${component.title ? ` · ${component.title}` : ""}`];
|
|
475
|
+
const visible = component.rows.slice(0, UI_PICKER_KEYVALUE_ROWS_MAX);
|
|
476
|
+
for (const row of visible) {
|
|
477
|
+
lines.push(`${row.key}: ${row.value}`);
|
|
478
|
+
}
|
|
479
|
+
if (component.rows.length > visible.length) {
|
|
480
|
+
lines.push(`... (+${component.rows.length - visible.length} more rows)`);
|
|
481
|
+
}
|
|
482
|
+
return lines;
|
|
483
|
+
}
|
|
484
|
+
case "divider":
|
|
485
|
+
return ["divider"];
|
|
486
|
+
default:
|
|
487
|
+
return ["component"];
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
class UiActionPickerComponent {
|
|
491
|
+
#entries;
|
|
492
|
+
#theme;
|
|
493
|
+
#done;
|
|
494
|
+
#mode = "doc";
|
|
495
|
+
#docIndex = 0;
|
|
496
|
+
#actionIndex = 0;
|
|
497
|
+
constructor(opts) {
|
|
498
|
+
this.#entries = opts.entries;
|
|
499
|
+
this.#theme = opts.theme;
|
|
500
|
+
this.#done = opts.done;
|
|
501
|
+
if (opts.initialUiId && opts.initialUiId.trim().length > 0) {
|
|
502
|
+
const initialDocIndex = this.#entries.findIndex((entry) => entry.doc.ui_id === opts.initialUiId);
|
|
503
|
+
if (initialDocIndex >= 0) {
|
|
504
|
+
this.#docIndex = initialDocIndex;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const actions = this.#currentActions();
|
|
508
|
+
if (opts.initialActionId && opts.initialActionId.trim().length > 0) {
|
|
509
|
+
const initialActionIndex = actions.findIndex((action) => action.id === opts.initialActionId);
|
|
510
|
+
if (initialActionIndex >= 0) {
|
|
511
|
+
this.#actionIndex = initialActionIndex;
|
|
512
|
+
this.#mode = "action";
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
#currentEntry() {
|
|
517
|
+
return this.#entries[this.#docIndex];
|
|
518
|
+
}
|
|
519
|
+
#currentActions() {
|
|
520
|
+
return this.#currentEntry().actions;
|
|
521
|
+
}
|
|
522
|
+
#currentAction() {
|
|
523
|
+
const actions = this.#currentActions();
|
|
524
|
+
return actions.length > 0 ? actions[boundedIndex(this.#actionIndex, actions.length)] : null;
|
|
525
|
+
}
|
|
526
|
+
#moveDoc(delta) {
|
|
527
|
+
this.#docIndex = boundedIndex(this.#docIndex + delta, this.#entries.length);
|
|
528
|
+
this.#actionIndex = boundedIndex(this.#actionIndex, this.#currentActions().length);
|
|
529
|
+
}
|
|
530
|
+
#moveAction(delta) {
|
|
531
|
+
const actions = this.#currentActions();
|
|
532
|
+
if (actions.length === 0) {
|
|
533
|
+
this.#actionIndex = 0;
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
this.#actionIndex = boundedIndex(this.#actionIndex + delta, actions.length);
|
|
537
|
+
}
|
|
538
|
+
#submit() {
|
|
539
|
+
const action = this.#currentAction();
|
|
540
|
+
if (!action) {
|
|
541
|
+
this.#done(null);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
this.#done({
|
|
545
|
+
doc: this.#currentEntry().doc,
|
|
546
|
+
action,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
handleInput(data) {
|
|
550
|
+
if (matchesKey(data, "escape")) {
|
|
551
|
+
this.#done(null);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (matchesKey(data, "tab") || matchesKey(data, "right")) {
|
|
555
|
+
this.#mode = "action";
|
|
556
|
+
this.#actionIndex = boundedIndex(this.#actionIndex, this.#currentActions().length);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (matchesKey(data, "shift+tab") || matchesKey(data, "left")) {
|
|
560
|
+
this.#mode = "doc";
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (matchesKey(data, "up")) {
|
|
564
|
+
if (this.#mode === "doc") {
|
|
565
|
+
this.#moveDoc(-1);
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
this.#moveAction(-1);
|
|
569
|
+
}
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (matchesKey(data, "down")) {
|
|
573
|
+
if (this.#mode === "doc") {
|
|
574
|
+
this.#moveDoc(1);
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
this.#moveAction(1);
|
|
578
|
+
}
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
if (matchesKey(data, "enter") || matchesKey(data, "return")) {
|
|
582
|
+
if (this.#mode === "doc") {
|
|
583
|
+
const actions = this.#currentActions();
|
|
584
|
+
if (actions.length <= 1) {
|
|
585
|
+
this.#submit();
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
this.#mode = "action";
|
|
589
|
+
}
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
this.#submit();
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
invalidate() {
|
|
596
|
+
// No cached state.
|
|
597
|
+
}
|
|
598
|
+
render(width) {
|
|
599
|
+
const maxWidth = Math.max(24, width - 2);
|
|
600
|
+
const lines = [];
|
|
601
|
+
lines.push(this.#theme.fg("accent", short("Programmable UI", maxWidth)));
|
|
602
|
+
lines.push(this.#theme.fg("dim", short("↑/↓ move · tab switch · enter select/submit · esc cancel", maxWidth)));
|
|
603
|
+
lines.push("");
|
|
604
|
+
lines.push(this.#theme.fg(this.#mode === "doc" ? "accent" : "dim", short(`Documents (${this.#entries.length})`, maxWidth)));
|
|
605
|
+
for (let idx = 0; idx < this.#entries.length; idx += 1) {
|
|
606
|
+
const entry = this.#entries[idx];
|
|
607
|
+
const active = idx === this.#docIndex;
|
|
608
|
+
const marker = active ? (this.#mode === "doc" ? "▶" : "▸") : " ";
|
|
609
|
+
const label = `${marker} ${entry.doc.ui_id} · ${entry.doc.title}`;
|
|
610
|
+
lines.push(this.#theme.fg(active ? "accent" : "muted", short(label, maxWidth)));
|
|
611
|
+
}
|
|
612
|
+
const selectedDoc = this.#currentEntry().doc;
|
|
613
|
+
if (selectedDoc.summary) {
|
|
614
|
+
lines.push("");
|
|
615
|
+
lines.push(this.#theme.fg("dim", short(`Summary: ${selectedDoc.summary}`, maxWidth)));
|
|
616
|
+
}
|
|
617
|
+
lines.push("");
|
|
618
|
+
lines.push(this.#theme.fg("dim", short(`Components (${selectedDoc.components.length})`, maxWidth)));
|
|
619
|
+
const visibleComponents = selectedDoc.components.slice(0, UI_PICKER_COMPONENTS_MAX);
|
|
620
|
+
for (const component of visibleComponents) {
|
|
621
|
+
const componentLines = pickerComponentLines(component);
|
|
622
|
+
for (let idx = 0; idx < componentLines.length; idx += 1) {
|
|
623
|
+
const line = componentLines[idx];
|
|
624
|
+
const prefix = idx === 0 ? " " : " ";
|
|
625
|
+
lines.push(this.#theme.fg("text", short(`${prefix}${line}`, maxWidth)));
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (selectedDoc.components.length > visibleComponents.length) {
|
|
629
|
+
lines.push(this.#theme.fg("muted", short(` ... (+${selectedDoc.components.length - visibleComponents.length} more components)`, maxWidth)));
|
|
630
|
+
}
|
|
631
|
+
const actions = this.#currentActions();
|
|
632
|
+
lines.push("");
|
|
633
|
+
lines.push(this.#theme.fg(this.#mode === "action" ? "accent" : "dim", short(`Actions (${actions.length})`, maxWidth)));
|
|
634
|
+
for (let idx = 0; idx < actions.length; idx += 1) {
|
|
635
|
+
const action = actions[idx];
|
|
636
|
+
const active = idx === this.#actionIndex;
|
|
637
|
+
const marker = active ? (this.#mode === "action" ? "▶" : "▸") : " ";
|
|
638
|
+
const label = `${marker} ${action.id} · ${action.label}`;
|
|
639
|
+
lines.push(this.#theme.fg(active ? "accent" : "text", short(label, maxWidth)));
|
|
640
|
+
}
|
|
641
|
+
const action = this.#currentAction();
|
|
642
|
+
if (action?.description) {
|
|
643
|
+
lines.push("");
|
|
644
|
+
lines.push(this.#theme.fg("dim", short(`Ask: ${action.description}`, maxWidth)));
|
|
645
|
+
}
|
|
646
|
+
if (action?.component_id) {
|
|
647
|
+
lines.push(this.#theme.fg("dim", short(`Targets component: ${action.component_id}`, maxWidth)));
|
|
648
|
+
}
|
|
649
|
+
const commandText = action ? actionCommandText(action) : null;
|
|
650
|
+
if (commandText) {
|
|
651
|
+
lines.push("");
|
|
652
|
+
lines.push(this.#theme.fg("dim", short(`Prompt template: ${commandText}`, maxWidth)));
|
|
653
|
+
}
|
|
654
|
+
return lines;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
async function pickUiActionInteractively(opts) {
|
|
658
|
+
const selected = await opts.ctx.ui.custom((_tui, theme, _keybindings, done) => new UiActionPickerComponent({
|
|
659
|
+
entries: opts.entries,
|
|
660
|
+
theme: theme,
|
|
661
|
+
done,
|
|
662
|
+
initialUiId: opts.uiId,
|
|
663
|
+
initialActionId: opts.actionId,
|
|
664
|
+
}), {
|
|
665
|
+
overlay: true,
|
|
666
|
+
overlayOptions: UI_INTERACT_OVERLAY_OPTIONS,
|
|
667
|
+
});
|
|
668
|
+
return selected ?? null;
|
|
669
|
+
}
|
|
123
670
|
function applyUiAction(params, state) {
|
|
671
|
+
retainPromptedRevisionKeysForActiveDocs(state);
|
|
672
|
+
retainAwaitingUiIdsForActiveDocs(state);
|
|
124
673
|
const docs = activeDocs(state);
|
|
674
|
+
const awaitingCount = awaitingDocs(state, docs).length;
|
|
125
675
|
switch (params.action) {
|
|
126
676
|
case "status":
|
|
127
|
-
return {
|
|
677
|
+
return {
|
|
678
|
+
ok: true,
|
|
679
|
+
action: "status",
|
|
680
|
+
message: statusSummary(docs, awaitingCount),
|
|
681
|
+
extra: {
|
|
682
|
+
status_profile_count: statusProfileDocCount(docs),
|
|
683
|
+
...statusProfileWarningsExtraForDocs(docs),
|
|
684
|
+
},
|
|
685
|
+
};
|
|
128
686
|
case "snapshot": {
|
|
129
687
|
const format = parseSnapshotFormat(params.snapshot_format);
|
|
130
688
|
return {
|
|
131
689
|
ok: true,
|
|
132
690
|
action: "snapshot",
|
|
133
691
|
message: snapshotText(docs, format),
|
|
134
|
-
extra: {
|
|
692
|
+
extra: {
|
|
693
|
+
snapshot_format: format,
|
|
694
|
+
status_profile_count: statusProfileDocCount(docs),
|
|
695
|
+
...statusProfileWarningsExtraForDocs(docs),
|
|
696
|
+
},
|
|
135
697
|
};
|
|
136
698
|
}
|
|
137
699
|
case "set":
|
|
@@ -142,11 +704,20 @@ function applyUiAction(params, state) {
|
|
|
142
704
|
}
|
|
143
705
|
const preferred = preferredDocForState(state, parsed.doc);
|
|
144
706
|
state.docsById.set(parsed.doc.ui_id, preferred);
|
|
707
|
+
if (runnableActions(preferred).length > 0) {
|
|
708
|
+
state.awaitingUiIds.add(parsed.doc.ui_id);
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
state.awaitingUiIds.delete(parsed.doc.ui_id);
|
|
712
|
+
}
|
|
713
|
+
retainPromptedRevisionKeysForActiveDocs(state);
|
|
714
|
+
retainAwaitingUiIdsForActiveDocs(state);
|
|
145
715
|
return {
|
|
146
716
|
ok: true,
|
|
147
717
|
action: params.action,
|
|
148
718
|
message: `UI doc set: ${parsed.doc.ui_id}`,
|
|
149
|
-
extra: { ui_id: parsed.doc.ui_id },
|
|
719
|
+
extra: { ui_id: parsed.doc.ui_id, ...statusProfileWarningsExtraForDoc(preferred) },
|
|
720
|
+
changedUiIds: [parsed.doc.ui_id],
|
|
150
721
|
};
|
|
151
722
|
}
|
|
152
723
|
case "replace": {
|
|
@@ -155,14 +726,24 @@ function applyUiAction(params, state) {
|
|
|
155
726
|
return { ok: false, action: "replace", message: parsed.error };
|
|
156
727
|
}
|
|
157
728
|
state.docsById.clear();
|
|
729
|
+
state.awaitingUiIds.clear();
|
|
158
730
|
for (const doc of parsed.docs) {
|
|
159
731
|
state.docsById.set(doc.ui_id, doc);
|
|
732
|
+
if (runnableActions(doc).length > 0) {
|
|
733
|
+
state.awaitingUiIds.add(doc.ui_id);
|
|
734
|
+
}
|
|
160
735
|
}
|
|
736
|
+
retainPromptedRevisionKeysForActiveDocs(state);
|
|
737
|
+
retainAwaitingUiIdsForActiveDocs(state);
|
|
161
738
|
return {
|
|
162
739
|
ok: true,
|
|
163
740
|
action: "replace",
|
|
164
741
|
message: `UI docs replaced (${parsed.docs.length}).`,
|
|
165
|
-
extra: {
|
|
742
|
+
extra: {
|
|
743
|
+
doc_count: parsed.docs.length,
|
|
744
|
+
...statusProfileWarningsExtraForDocs(parsed.docs),
|
|
745
|
+
},
|
|
746
|
+
changedUiIds: parsed.docs.map((doc) => doc.ui_id),
|
|
166
747
|
};
|
|
167
748
|
}
|
|
168
749
|
case "remove": {
|
|
@@ -173,10 +754,19 @@ function applyUiAction(params, state) {
|
|
|
173
754
|
if (!state.docsById.delete(uiId)) {
|
|
174
755
|
return { ok: false, action: "remove", message: `UI doc not found: ${uiId}` };
|
|
175
756
|
}
|
|
757
|
+
if (state.pendingPrompt?.uiId === uiId) {
|
|
758
|
+
state.pendingPrompt = null;
|
|
759
|
+
}
|
|
760
|
+
state.awaitingUiIds.delete(uiId);
|
|
761
|
+
retainPromptedRevisionKeysForActiveDocs(state);
|
|
762
|
+
retainAwaitingUiIdsForActiveDocs(state);
|
|
176
763
|
return { ok: true, action: "remove", message: `UI doc removed: ${uiId}` };
|
|
177
764
|
}
|
|
178
765
|
case "clear":
|
|
179
766
|
state.docsById.clear();
|
|
767
|
+
state.pendingPrompt = null;
|
|
768
|
+
state.promptedRevisionKeys.clear();
|
|
769
|
+
state.awaitingUiIds.clear();
|
|
180
770
|
return { ok: true, action: "clear", message: "UI docs cleared." };
|
|
181
771
|
}
|
|
182
772
|
}
|
|
@@ -195,49 +785,83 @@ function buildToolResult(opts) {
|
|
|
195
785
|
};
|
|
196
786
|
return result;
|
|
197
787
|
}
|
|
198
|
-
function renderDocPreview(theme, doc) {
|
|
788
|
+
function renderDocPreview(theme, doc, opts) {
|
|
199
789
|
const lines = [];
|
|
200
|
-
|
|
790
|
+
const headerParts = [theme.fg("accent", doc.title), theme.fg("muted", `[${doc.ui_id}]`)];
|
|
791
|
+
if (opts.awaitingResponse) {
|
|
792
|
+
headerParts.push(theme.fg("accent", "awaiting-response"));
|
|
793
|
+
}
|
|
794
|
+
lines.push(headerParts.join(" "));
|
|
201
795
|
if (doc.summary) {
|
|
202
796
|
lines.push(theme.fg("muted", short(doc.summary, 80)));
|
|
203
797
|
}
|
|
798
|
+
if (opts.awaitingCount > 0) {
|
|
799
|
+
lines.push(theme.fg("accent", `Awaiting user response for ${opts.awaitingCount} UI doc(s).`));
|
|
800
|
+
}
|
|
204
801
|
const components = doc.components.slice(0, UI_WIDGET_COMPONENTS_MAX);
|
|
205
802
|
if (components.length > 0) {
|
|
206
803
|
lines.push(theme.fg("dim", "Components:"));
|
|
207
804
|
for (const component of components) {
|
|
208
|
-
|
|
805
|
+
const previewLines = componentPreviewLines(component);
|
|
806
|
+
for (let idx = 0; idx < previewLines.length; idx += 1) {
|
|
807
|
+
const line = previewLines[idx];
|
|
808
|
+
lines.push(`${idx === 0 ? " " : " "}${line}`);
|
|
809
|
+
}
|
|
209
810
|
}
|
|
210
811
|
}
|
|
211
|
-
|
|
812
|
+
const interactiveActions = runnableActions(doc);
|
|
813
|
+
if (interactiveActions.length > 0) {
|
|
212
814
|
lines.push(theme.fg("muted", "Actions:"));
|
|
213
|
-
const visibleActions =
|
|
815
|
+
const visibleActions = interactiveActions.slice(0, UI_WIDGET_ACTIONS_MAX);
|
|
214
816
|
for (let idx = 0; idx < visibleActions.length; idx += 1) {
|
|
215
817
|
const action = visibleActions[idx];
|
|
216
818
|
lines.push(` ${idx + 1}. ${action.label}`);
|
|
217
819
|
}
|
|
218
|
-
if (
|
|
219
|
-
lines.push(` ... (+${
|
|
820
|
+
if (interactiveActions.length > visibleActions.length) {
|
|
821
|
+
lines.push(` ... (+${interactiveActions.length - visibleActions.length} more actions)`);
|
|
822
|
+
}
|
|
823
|
+
if (opts.awaitingResponse) {
|
|
824
|
+
lines.push(theme.fg("accent", "Awaiting your response. Select an action to continue."));
|
|
220
825
|
}
|
|
221
|
-
lines.push(theme.fg("dim",
|
|
826
|
+
lines.push(theme.fg("dim", `Press ${UI_INTERACT_SHORTCUT} to compose and submit a prompt from actions.`));
|
|
222
827
|
}
|
|
223
828
|
else {
|
|
224
829
|
lines.push(theme.fg("dim", "No interactive actions."));
|
|
225
830
|
}
|
|
226
831
|
return lines;
|
|
227
832
|
}
|
|
228
|
-
function
|
|
833
|
+
function componentPreviewLines(component) {
|
|
229
834
|
const { kind } = component;
|
|
230
835
|
switch (kind) {
|
|
231
836
|
case "text":
|
|
232
|
-
return `text · ${short(component.text, 80)}
|
|
233
|
-
case "list":
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
837
|
+
return [`text · ${short(component.text, 80)}`];
|
|
838
|
+
case "list": {
|
|
839
|
+
const lines = [`list · ${component.title ?? kind} · ${component.items.length} item(s)`];
|
|
840
|
+
const visible = component.items.slice(0, UI_WIDGET_COMPONENT_DETAILS_MAX);
|
|
841
|
+
for (const item of visible) {
|
|
842
|
+
const detail = item.detail ? ` — ${short(item.detail, 28)}` : "";
|
|
843
|
+
lines.push(`• ${short(item.label, 72)}${detail}`);
|
|
844
|
+
}
|
|
845
|
+
if (component.items.length > visible.length) {
|
|
846
|
+
lines.push(`... (+${component.items.length - visible.length} more items)`);
|
|
847
|
+
}
|
|
848
|
+
return lines;
|
|
849
|
+
}
|
|
850
|
+
case "key_value": {
|
|
851
|
+
const lines = [`key_value · ${component.title ?? kind} · ${component.rows.length} row(s)`];
|
|
852
|
+
const visible = component.rows.slice(0, UI_WIDGET_COMPONENT_DETAILS_MAX);
|
|
853
|
+
for (const row of visible) {
|
|
854
|
+
lines.push(`${short(row.key, 20)}: ${short(row.value, 52)}`);
|
|
855
|
+
}
|
|
856
|
+
if (component.rows.length > visible.length) {
|
|
857
|
+
lines.push(`... (+${component.rows.length - visible.length} more rows)`);
|
|
858
|
+
}
|
|
859
|
+
return lines;
|
|
860
|
+
}
|
|
237
861
|
case "divider":
|
|
238
|
-
return "divider";
|
|
862
|
+
return ["divider"];
|
|
239
863
|
}
|
|
240
|
-
return kind;
|
|
864
|
+
return [kind];
|
|
241
865
|
}
|
|
242
866
|
function refreshUi(ctx) {
|
|
243
867
|
const key = sessionKey(ctx);
|
|
@@ -245,27 +869,137 @@ function refreshUi(ctx) {
|
|
|
245
869
|
if (!ctx.hasUI) {
|
|
246
870
|
return;
|
|
247
871
|
}
|
|
872
|
+
retainAwaitingUiIdsForActiveDocs(state);
|
|
248
873
|
const docs = activeDocs(state);
|
|
249
874
|
if (docs.length === 0) {
|
|
250
875
|
ctx.ui.setStatus("mu-ui", undefined);
|
|
251
876
|
ctx.ui.setWidget("mu-ui", undefined);
|
|
252
877
|
return;
|
|
253
878
|
}
|
|
879
|
+
const awaiting = awaitingDocs(state, docs);
|
|
254
880
|
const labels = docs.map((doc) => doc.ui_id).join(", ");
|
|
881
|
+
const readiness = state.interactionDepth > 0
|
|
882
|
+
? ctx.ui.theme.fg("accent", "prompting")
|
|
883
|
+
: awaiting.length > 0
|
|
884
|
+
? ctx.ui.theme.fg("accent", `awaiting ${awaiting.length}`)
|
|
885
|
+
: ctx.ui.theme.fg("dim", "ready");
|
|
255
886
|
ctx.ui.setStatus("mu-ui", [
|
|
256
887
|
ctx.ui.theme.fg("dim", "ui"),
|
|
257
888
|
ctx.ui.theme.fg("muted", "·"),
|
|
258
889
|
ctx.ui.theme.fg("accent", `${docs.length}`),
|
|
259
890
|
ctx.ui.theme.fg("muted", "·"),
|
|
891
|
+
readiness,
|
|
892
|
+
ctx.ui.theme.fg("muted", "·"),
|
|
260
893
|
ctx.ui.theme.fg("text", labels),
|
|
261
894
|
].join(" "));
|
|
262
|
-
|
|
895
|
+
if (state.interactionDepth > 0) {
|
|
896
|
+
ctx.ui.setWidget("mu-ui", undefined);
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
const primaryDoc = awaiting[0] ?? docs[0];
|
|
900
|
+
ctx.ui.setWidget("mu-ui", renderDocPreview(ctx.ui.theme, primaryDoc, {
|
|
901
|
+
awaitingResponse: state.awaitingUiIds.has(primaryDoc.ui_id),
|
|
902
|
+
awaitingCount: awaiting.length,
|
|
903
|
+
}), { placement: "belowEditor" });
|
|
263
904
|
}
|
|
264
905
|
export function uiExtension(pi) {
|
|
906
|
+
const commandUsage = "/mu ui status|snapshot [compact|multiline]|interact [ui_id [action_id]]";
|
|
907
|
+
const usage = `Usage: ${commandUsage}`;
|
|
908
|
+
const runUiActionFromDoc = async (ctx, state, uiId, actionId) => withUiInteraction(ctx, state, async () => {
|
|
909
|
+
const docs = activeDocs(state, UI_DISPLAY_DOCS_MAX);
|
|
910
|
+
if (docs.length === 0) {
|
|
911
|
+
ctx.ui.notify("No UI docs are currently available.", "info");
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
const entries = docs
|
|
915
|
+
.map((doc) => ({ doc, actions: runnableActions(doc) }))
|
|
916
|
+
.filter((entry) => entry.actions.length > 0);
|
|
917
|
+
if (entries.length === 0) {
|
|
918
|
+
ctx.ui.notify("No runnable UI actions are currently available.", "error");
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
let selectedDoc = null;
|
|
922
|
+
let selectedAction = null;
|
|
923
|
+
const normalizedUiId = uiId?.trim() ?? "";
|
|
924
|
+
const normalizedActionId = actionId?.trim() ?? "";
|
|
925
|
+
if (normalizedUiId.length > 0 && normalizedActionId.length > 0) {
|
|
926
|
+
const entry = entries.find((candidate) => candidate.doc.ui_id === normalizedUiId) ?? null;
|
|
927
|
+
if (!entry) {
|
|
928
|
+
ctx.ui.notify(`UI doc not found: ${normalizedUiId}`, "error");
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
const action = entry.actions.find((candidate) => candidate.id === normalizedActionId) ?? null;
|
|
932
|
+
if (!action) {
|
|
933
|
+
ctx.ui.notify(`Action not found: ${normalizedActionId} in ${normalizedUiId}`, "error");
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
selectedDoc = entry.doc;
|
|
937
|
+
selectedAction = action;
|
|
938
|
+
}
|
|
939
|
+
else {
|
|
940
|
+
const picked = await pickUiActionInteractively({
|
|
941
|
+
ctx,
|
|
942
|
+
entries,
|
|
943
|
+
uiId: normalizedUiId.length > 0 ? normalizedUiId : undefined,
|
|
944
|
+
actionId: normalizedActionId.length > 0 ? normalizedActionId : undefined,
|
|
945
|
+
});
|
|
946
|
+
if (!picked) {
|
|
947
|
+
ctx.ui.notify("UI interaction cancelled.", "info");
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
selectedDoc = picked.doc;
|
|
951
|
+
selectedAction = picked.action;
|
|
952
|
+
}
|
|
953
|
+
if (!selectedDoc || !selectedAction) {
|
|
954
|
+
ctx.ui.notify("No UI action was selected.", "error");
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
const commandText = actionCommandText(selectedAction);
|
|
958
|
+
if (!commandText) {
|
|
959
|
+
ctx.ui.notify(`Action ${selectedAction.id} is missing metadata.command_text.`, "error");
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
const templateKeys = extractTemplateKeys(commandText);
|
|
963
|
+
const templateValues = await collectTemplateValues({
|
|
964
|
+
ctx,
|
|
965
|
+
action: selectedAction,
|
|
966
|
+
templateKeys,
|
|
967
|
+
});
|
|
968
|
+
if (!templateValues) {
|
|
969
|
+
ctx.ui.notify("UI interaction cancelled.", "info");
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
const composed = composePromptFromAction({
|
|
973
|
+
commandText,
|
|
974
|
+
templateValues,
|
|
975
|
+
});
|
|
976
|
+
const edited = await ctx.ui.editor(`Review prompt (${selectedDoc.ui_id}/${selectedAction.id})`, composed);
|
|
977
|
+
if (edited === undefined) {
|
|
978
|
+
ctx.ui.notify("UI submit cancelled.", "info");
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
const finalPrompt = edited.trim();
|
|
982
|
+
if (finalPrompt.length === 0) {
|
|
983
|
+
ctx.ui.notify("Cannot submit an empty prompt.", "error");
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
const confirmed = await ctx.ui.confirm("Submit UI prompt", short(finalPrompt, UI_PROMPT_PREVIEW_MAX));
|
|
987
|
+
if (!confirmed) {
|
|
988
|
+
ctx.ui.notify("UI submit cancelled.", "info");
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
pi.sendUserMessage(finalPrompt);
|
|
992
|
+
state.awaitingUiIds.delete(selectedDoc.ui_id);
|
|
993
|
+
if (state.pendingPrompt?.uiId === selectedDoc.ui_id) {
|
|
994
|
+
state.pendingPrompt = null;
|
|
995
|
+
}
|
|
996
|
+
retainAwaitingUiIdsForActiveDocs(state);
|
|
997
|
+
ctx.ui.notify(`Submitted prompt from ${selectedDoc.ui_id}/${selectedAction.id}.`, "info");
|
|
998
|
+
});
|
|
265
999
|
registerMuSubcommand(pi, {
|
|
266
1000
|
subcommand: "ui",
|
|
267
|
-
summary: "Inspect interactive UI docs",
|
|
268
|
-
usage:
|
|
1001
|
+
summary: "Inspect and manage interactive UI docs",
|
|
1002
|
+
usage: commandUsage,
|
|
269
1003
|
handler: async (args, ctx) => {
|
|
270
1004
|
const tokens = args
|
|
271
1005
|
.trim()
|
|
@@ -285,7 +1019,27 @@ export function uiExtension(pi) {
|
|
|
285
1019
|
ctx.ui.notify(result.message, result.ok ? "info" : "error");
|
|
286
1020
|
return;
|
|
287
1021
|
}
|
|
288
|
-
|
|
1022
|
+
if (subcommand === "interact") {
|
|
1023
|
+
if (tokens.length > 3) {
|
|
1024
|
+
ctx.ui.notify(usage, "info");
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
const uiId = tokens[1];
|
|
1028
|
+
const actionId = tokens[2];
|
|
1029
|
+
await runUiActionFromDoc(ctx, state, uiId, actionId);
|
|
1030
|
+
refreshUi(ctx);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
ctx.ui.notify(usage, "info");
|
|
1034
|
+
},
|
|
1035
|
+
});
|
|
1036
|
+
pi.registerShortcut(UI_INTERACT_SHORTCUT, {
|
|
1037
|
+
description: "Interact with programmable UI docs and submit prompt",
|
|
1038
|
+
handler: async (ctx) => {
|
|
1039
|
+
const key = sessionKey(ctx);
|
|
1040
|
+
const state = ensureState(key);
|
|
1041
|
+
await runUiActionFromDoc(ctx, state);
|
|
1042
|
+
refreshUi(ctx);
|
|
289
1043
|
},
|
|
290
1044
|
});
|
|
291
1045
|
pi.registerTool({
|
|
@@ -312,6 +1066,9 @@ export function uiExtension(pi) {
|
|
|
312
1066
|
const state = ensureState(key);
|
|
313
1067
|
const params = paramsRaw;
|
|
314
1068
|
const result = applyUiAction(params, state);
|
|
1069
|
+
if (ctx.hasUI && result.ok && result.changedUiIds && result.changedUiIds.length > 0) {
|
|
1070
|
+
armAutoPromptForUiDocs(state, result.changedUiIds);
|
|
1071
|
+
}
|
|
315
1072
|
refreshUi(ctx);
|
|
316
1073
|
return buildToolResult({ state, ...result });
|
|
317
1074
|
},
|
|
@@ -322,6 +1079,21 @@ export function uiExtension(pi) {
|
|
|
322
1079
|
pi.on("session_switch", (_event, ctx) => {
|
|
323
1080
|
refreshUi(ctx);
|
|
324
1081
|
});
|
|
1082
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
1083
|
+
if (!ctx.hasUI) {
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
const key = sessionKey(ctx);
|
|
1087
|
+
const state = ensureState(key);
|
|
1088
|
+
const pending = state.pendingPrompt;
|
|
1089
|
+
if (!pending) {
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
state.pendingPrompt = null;
|
|
1093
|
+
ctx.ui.notify(`Agent requested input via ${pending.uiId}. Submit now or press ${UI_INTERACT_SHORTCUT} later.`, "info");
|
|
1094
|
+
await runUiActionFromDoc(ctx, state, pending.uiId, pending.actionId);
|
|
1095
|
+
refreshUi(ctx);
|
|
1096
|
+
});
|
|
325
1097
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
326
1098
|
const key = sessionKey(ctx);
|
|
327
1099
|
touchState(key);
|