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