u-foo 1.7.5 → 1.8.1

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.
Files changed (44) hide show
  1. package/README.md +9 -1
  2. package/README.zh-CN.md +9 -1
  3. package/bin/ufoo.js +4 -2
  4. package/package.json +1 -1
  5. package/src/agent/cliRunner.js +3 -2
  6. package/src/agent/ucodeBootstrap.js +5 -3
  7. package/src/agent/ufooAgent.js +184 -5
  8. package/src/assistant/constants.js +1 -1
  9. package/src/chat/commandExecutor.js +98 -3
  10. package/src/chat/commands.js +7 -0
  11. package/src/chat/completionController.js +40 -0
  12. package/src/chat/daemonMessageRouter.js +21 -1
  13. package/src/chat/dashboardKeyController.js +55 -3
  14. package/src/chat/dashboardView.js +26 -5
  15. package/src/chat/index.js +148 -41
  16. package/src/chat/inputListenerController.js +14 -0
  17. package/src/chat/inputMath.js +1 -1
  18. package/src/chat/inputSubmitHandler.js +9 -5
  19. package/src/chat/transientAgentState.js +64 -0
  20. package/src/cli/groupCoreCommands.js +21 -12
  21. package/src/cli.js +23 -1
  22. package/src/code/tui.js +1 -1
  23. package/src/daemon/cronOps.js +11 -4
  24. package/src/daemon/groupOrchestrator.js +581 -97
  25. package/src/daemon/index.js +418 -3
  26. package/src/daemon/ops.js +25 -7
  27. package/src/daemon/promptLoop.js +16 -0
  28. package/src/daemon/promptRequest.js +126 -2
  29. package/src/daemon/reporting.js +18 -0
  30. package/src/daemon/soloBootstrap.js +435 -0
  31. package/src/daemon/status.js +5 -1
  32. package/src/globalMode.js +33 -0
  33. package/src/group/bootstrap.js +157 -0
  34. package/src/group/promptProfiles.js +646 -0
  35. package/src/group/templateValidation.js +99 -0
  36. package/src/group/validateTemplate.js +36 -5
  37. package/src/init/index.js +13 -7
  38. package/src/report/store.js +6 -0
  39. package/src/shared/eventContract.js +1 -0
  40. package/templates/groups/{dev-basic.json → build-lane.json} +38 -34
  41. package/templates/groups/product-discovery.json +79 -0
  42. package/templates/groups/ui-polish.json +87 -0
  43. package/templates/groups/verify-ship.json +79 -0
  44. package/templates/groups/research-quick.json +0 -49
@@ -11,6 +11,7 @@ function createCompletionController(options = {}) {
11
11
  completionPanel,
12
12
  promptBox,
13
13
  commandRegistry = [],
14
+ getGroupTemplateCandidates = () => [],
14
15
  getMentionCandidates = () => [],
15
16
  normalizeCommandPrefix = () => {},
16
17
  truncateText = (text) => String(text || ""),
@@ -141,8 +142,33 @@ function createCompletionController(options = {}) {
141
142
  const parts = trimmed.split(/\s+/);
142
143
  const mainCmd = parts[0];
143
144
  const isLaunch = mainCmd && mainCmd.toLowerCase() === "/launch";
145
+ const isGroup = mainCmd && mainCmd.toLowerCase() === "/group";
144
146
  const wantsSubcommands = (parts.length > 1 || (endsWithSpace && parts.length === 1));
145
147
 
148
+ if (isGroup) {
149
+ const groupSubcommand = String(parts[1] || "").trim().toLowerCase();
150
+ const wantsGroupRunArgs = groupSubcommand === "run" && (parts.length > 2 || endsWithSpace);
151
+ if (wantsGroupRunArgs) {
152
+ const aliasFilter = String(parts[2] || "").trim().toLowerCase();
153
+ return (Array.isArray(getGroupTemplateCandidates()) ? getGroupTemplateCandidates() : [])
154
+ .map((item) => {
155
+ const alias = String(item && item.alias ? item.alias : item && item.cmd ? item.cmd : "").trim();
156
+ if (!alias) return null;
157
+ const desc = String(item && item.desc ? item.desc : item && item.name ? item.name : "").trim();
158
+ const source = String(item && item.source ? item.source : "").trim();
159
+ const detail = [desc, source].filter(Boolean).join(" · ");
160
+ return {
161
+ cmd: alias,
162
+ desc: detail,
163
+ isArgumentSuggestion: true,
164
+ argumentPrefix: "/group run",
165
+ };
166
+ })
167
+ .filter((item) => item && (!aliasFilter || item.cmd.toLowerCase().startsWith(aliasFilter)))
168
+ .sort((a, b) => a.cmd.localeCompare(b.cmd, "en", { sensitivity: "base" }));
169
+ }
170
+ }
171
+
146
172
  if ((wantsSubcommands || isLaunch) && mainCmd && mainCmd.startsWith("/")) {
147
173
  const subFilter = parts[1] || "";
148
174
  const mainCmdObj = commandRegistry.find((item) =>
@@ -261,6 +287,13 @@ function createCompletionController(options = {}) {
261
287
  return { text: `${completedCore} `, isComplete };
262
288
  }
263
289
 
290
+ if (selected.isArgumentSuggestion) {
291
+ const prefix = String(selected.argumentPrefix || "").trim();
292
+ const completedCore = prefix ? `${prefix} ${selected.cmd}` : selected.cmd;
293
+ const isComplete = trimmed === completedCore || trimmed.startsWith(`${completedCore} `);
294
+ return { text: `${completedCore} `, isComplete };
295
+ }
296
+
264
297
  const completedCore = selected.cmd;
265
298
  const hasChildren = selected.subcommands && selected.subcommands.length > 0;
266
299
  const isComplete =
@@ -291,6 +324,9 @@ function createCompletionController(options = {}) {
291
324
  const parts = input.value.split(/\s+/);
292
325
  parts[parts.length - 1] = selected.cmd;
293
326
  input.value = `${parts.join(" ")} `;
327
+ } else if (selected.isArgumentSuggestion) {
328
+ const prefix = String(selected.argumentPrefix || "").trim();
329
+ input.value = prefix ? `${prefix} ${selected.cmd} ` : `${selected.cmd} `;
294
330
  } else {
295
331
  input.value = `${selected.cmd} `;
296
332
  }
@@ -304,6 +340,8 @@ function createCompletionController(options = {}) {
304
340
 
305
341
  if (!selected.isSubcommand && selected.subcommands && selected.subcommands.length > 0) {
306
342
  show(input.value);
343
+ } else if (selected.isSubcommand && selected.parentCmd === "/group" && selected.cmd === "run") {
344
+ show(input.value);
307
345
  } else {
308
346
  hide();
309
347
  }
@@ -346,6 +384,8 @@ function createCompletionController(options = {}) {
346
384
  applyPreview(nextPreview);
347
385
  if (!selected.isSubcommand && selected.subcommands && selected.subcommands.length > 0) {
348
386
  show(input.value);
387
+ } else if (selected.isSubcommand && selected.parentCmd === "/group" && selected.cmd === "run") {
388
+ show(input.value);
349
389
  } else {
350
390
  hide();
351
391
  }
@@ -286,7 +286,14 @@ function createDaemonMessageRouter(options = {}) {
286
286
  payload.disambiguate.candidates.length > 0
287
287
  ) {
288
288
  const pending = getPending();
289
- setPending({ disambiguate: payload.disambiguate, original: pending && pending.original });
289
+ const routedProjectRoot = payload.routed_project && payload.routed_project.project_root
290
+ ? payload.routed_project.project_root
291
+ : (pending && pending.project_root ? pending.project_root : "");
292
+ setPending({
293
+ disambiguate: payload.disambiguate,
294
+ original: pending && pending.original,
295
+ project_root: routedProjectRoot || undefined,
296
+ });
290
297
  const prompt = payload.disambiguate.prompt || "Choose target:";
291
298
  resolveStatusLine(`{gray-fg}?{/gray-fg} ${escapeBlessed(prompt)}`);
292
299
  logMessage("disambiguate", `{white-fg}?{/white-fg} ${escapeBlessed(prompt)}`);
@@ -323,6 +330,19 @@ function createDaemonMessageRouter(options = {}) {
323
330
  requestStatus();
324
331
  return true;
325
332
  }
333
+ if (data.event === "controller_report") {
334
+ const report = data.report && typeof data.report === "object" ? data.report : {};
335
+ const publisher = report.agent_id || data.publisher || "ufoo-agent";
336
+ const displayName = resolveAgentDisplayName(publisher);
337
+ const detail = report.summary || report.message || data.message || report.task_id || "report";
338
+ logMessage(
339
+ "system",
340
+ `{gray-fg}↥{/gray-fg} {cyan-fg}${escapeBlessed(displayName)}{/cyan-fg} {gray-fg}→ ufoo-agent{/gray-fg} ${escapeBlessed(detail)}`
341
+ );
342
+ requestStatus();
343
+ renderScreen();
344
+ return true;
345
+ }
326
346
  const prefix = data.event === "broadcast" ? "{gray-fg}⇢{/gray-fg}" : "{gray-fg}↔{/gray-fg}";
327
347
  const publisher = data.publisher && data.publisher !== "unknown"
328
348
  ? data.publisher
@@ -23,6 +23,9 @@ function createDashboardKeyController(options = {}) {
23
23
  clampAgentWindowWithSelection = () => {},
24
24
  requestProjectSwitch = () => {},
25
25
  requestCloseProject = () => {},
26
+ requestCron = () => {},
27
+ setGlobalScope = () => {},
28
+ getGlobalScope = () => "",
26
29
  renderDashboard = () => {},
27
30
  renderAgentDashboard = () => {},
28
31
  renderScreen = () => {},
@@ -232,6 +235,8 @@ function createDashboardKeyController(options = {}) {
232
235
 
233
236
  if (key.name === "down") {
234
237
  state.dashboardView = "cron";
238
+ const cronTasks = Array.isArray(state.cronTasks) ? state.cronTasks : [];
239
+ state.selectedCronIndex = cronTasks.length > 0 ? 0 : -1;
235
240
  renderDashboardAndScreen();
236
241
  return true;
237
242
  }
@@ -258,6 +263,25 @@ function createDashboardKeyController(options = {}) {
258
263
  }
259
264
 
260
265
  function handleCronKey(key) {
266
+ const cronTasks = Array.isArray(state.cronTasks) ? state.cronTasks : [];
267
+ const maxIndex = cronTasks.length - 1;
268
+
269
+ if (key.name === "left") {
270
+ if (maxIndex >= 0 && state.selectedCronIndex > 0) {
271
+ state.selectedCronIndex -= 1;
272
+ renderDashboardAndScreen();
273
+ }
274
+ return true;
275
+ }
276
+
277
+ if (key.name === "right") {
278
+ if (maxIndex >= 0 && state.selectedCronIndex < maxIndex) {
279
+ state.selectedCronIndex += 1;
280
+ renderDashboardAndScreen();
281
+ }
282
+ return true;
283
+ }
284
+
261
285
  if (key.name === "up") {
262
286
  state.dashboardView = "provider";
263
287
  renderDashboardAndScreen();
@@ -265,6 +289,14 @@ function createDashboardKeyController(options = {}) {
265
289
  }
266
290
 
267
291
  if (key.name === "x" && key.ctrl) {
292
+ if (maxIndex >= 0 && state.selectedCronIndex >= 0 && state.selectedCronIndex <= maxIndex) {
293
+ const task = cronTasks[state.selectedCronIndex];
294
+ const id = task && task.id ? String(task.id).trim() : "";
295
+ if (id) {
296
+ requestCron({ operation: "stop", id });
297
+ return true;
298
+ }
299
+ }
268
300
  exitDashboardMode(false);
269
301
  return true;
270
302
  }
@@ -353,7 +385,9 @@ function createDashboardKeyController(options = {}) {
353
385
  const next = current - 1;
354
386
  state.selectedProjectIndex = next;
355
387
  renderDashboardAndScreen();
356
- requestProjectSwitch(next);
388
+ if (getGlobalScope() === "project") {
389
+ requestProjectSwitch(next);
390
+ }
357
391
  }
358
392
  return true;
359
393
  }
@@ -364,17 +398,35 @@ function createDashboardKeyController(options = {}) {
364
398
  const next = current + 1;
365
399
  state.selectedProjectIndex = next;
366
400
  renderDashboardAndScreen();
367
- requestProjectSwitch(next);
401
+ if (getGlobalScope() === "project") {
402
+ requestProjectSwitch(next);
403
+ }
368
404
  }
369
405
  return true;
370
406
  }
371
407
 
372
- if (key.name === "up" || key.name === "escape") {
408
+ if (key.name === "up") {
409
+ exitDashboardMode(false);
410
+ return true;
411
+ }
412
+
413
+ if (key.name === "escape") {
414
+ if (getGlobalScope() === "project") {
415
+ setGlobalScope("controller");
416
+ }
373
417
  exitDashboardMode(false);
374
418
  return true;
375
419
  }
376
420
 
377
421
  if (key.name === "enter" || key.name === "return") {
422
+ const current = Number.isFinite(state.selectedProjectIndex) ? state.selectedProjectIndex : 0;
423
+ if (current >= 0 && current < projects.length) {
424
+ const selectedRow = projects[current];
425
+ const selectedProjectRoot = String((selectedRow && selectedRow.project_root) || "");
426
+ if (selectedProjectRoot) {
427
+ setGlobalScope("project", selectedProjectRoot);
428
+ }
429
+ }
378
430
  exitDashboardMode(false);
379
431
  return true;
380
432
  }
@@ -57,6 +57,8 @@ function buildProjectRailLine(options = {}) {
57
57
  maxProjectWindow = 5,
58
58
  activeProjectRoot = "",
59
59
  projectsFocused = false,
60
+ globalScope = "",
61
+ dashboardHint = "",
60
62
  } = options;
61
63
  const rows = Array.isArray(projects) ? projects : [];
62
64
  let windowStart = projectListWindowStart;
@@ -92,7 +94,7 @@ function buildProjectRailLine(options = {}) {
92
94
  const absoluteIndex = start + i;
93
95
  const name = String((row && row.project_name) || (row && row.project_root) || "-");
94
96
  const rowRoot = String((row && row.project_root) || "");
95
- const isActiveProject = Boolean(activeRoot && rowRoot === activeRoot);
97
+ const isActiveProject = globalScope !== "controller" && Boolean(activeRoot && rowRoot === activeRoot);
96
98
  const isSelected = absoluteIndex === safeSelectedIndex;
97
99
  if (projectsFocused && isSelected) {
98
100
  return `{inverse}${name}{/inverse}`;
@@ -105,9 +107,10 @@ function buildProjectRailLine(options = {}) {
105
107
 
106
108
  const leftMore = start > 0 ? "{gray-fg}<{/gray-fg} " : "";
107
109
  const rightMore = end < rows.length ? " {gray-fg}>{/gray-fg}" : "";
110
+ const hintPart = dashboardHint ? `{|}{gray-fg}${dashboardHint}{/gray-fg}` : "";
108
111
  return {
109
112
  hasProjects: true,
110
- line: ` {gray-fg}Projects:{/gray-fg} ${leftMore}${projectParts.join(" ")}${rightMore}`,
113
+ line: ` {gray-fg}Projects:{/gray-fg} ${leftMore}${projectParts.join(" ")}${rightMore}${hintPart}`,
111
114
  windowStart,
112
115
  };
113
116
  }
@@ -125,6 +128,7 @@ function buildDashboardDetailLine(options = {}) {
125
128
  selectedModeIndex = 0,
126
129
  selectedProviderIndex = 0,
127
130
  selectedResumeIndex = 0,
131
+ selectedCronIndex = -1,
128
132
  cronTasks = [],
129
133
  providerOptions = [],
130
134
  resumeOptions = [],
@@ -174,9 +178,15 @@ function buildDashboardDetailLine(options = {}) {
174
178
  if (dashboardView === "cron") {
175
179
  const items = Array.isArray(cronTasks) ? cronTasks : [];
176
180
  const summary = items.length > 0
177
- ? items.map((item) => item.label || item.summary || item.id || "").filter(Boolean).join(", ")
178
- : "none";
179
- content += `{gray-fg}Cron:{/gray-fg} {inverse}${summary}{/inverse}`;
181
+ ? items.map((item, index) => {
182
+ const label = item.label || item.summary || item.id || "";
183
+ if (!label) return "";
184
+ return index === selectedCronIndex
185
+ ? `{inverse}${label}{/inverse}`
186
+ : `{cyan-fg}${label}{/cyan-fg}`;
187
+ }).filter(Boolean).join(", ")
188
+ : "{cyan-fg}none{/cyan-fg}";
189
+ content += `{gray-fg}Cron:{/gray-fg} ${summary}`;
180
190
  content += ` {gray-fg}│ ${dashHints.cron || ""}{/gray-fg}`;
181
191
  return { content, windowStart };
182
192
  }
@@ -220,6 +230,7 @@ function buildDashboardDetailLine(options = {}) {
220
230
  function computeDashboardContent(options = {}) {
221
231
  const {
222
232
  globalMode = false,
233
+ globalScope = "controller",
223
234
  focusMode = "input",
224
235
  dashboardView = "agents",
225
236
  activeAgents = [],
@@ -238,7 +249,9 @@ function computeDashboardContent(options = {}) {
238
249
  selectedModeIndex = 0,
239
250
  selectedProviderIndex = 0,
240
251
  selectedResumeIndex = 0,
252
+ selectedCronIndex = -1,
241
253
  cronTasks = [],
254
+ pendingReports = 0,
242
255
  providerOptions = [],
243
256
  resumeOptions = [],
244
257
  dashHints = {},
@@ -247,6 +260,10 @@ function computeDashboardContent(options = {}) {
247
260
 
248
261
  if (globalMode) {
249
262
  const projectsFocused = focusMode === "dashboard" && dashboardView === "projects";
263
+ let dashboardHint = "";
264
+ if (projectsFocused) {
265
+ dashboardHint = globalScope === "controller" ? "Enter\u2192project" : "Esc\u2192global";
266
+ }
250
267
  const rail = buildProjectRailLine({
251
268
  projects,
252
269
  selectedProjectIndex,
@@ -254,6 +271,8 @@ function computeDashboardContent(options = {}) {
254
271
  maxProjectWindow,
255
272
  activeProjectRoot,
256
273
  projectsFocused,
274
+ globalScope,
275
+ dashboardHint,
257
276
  });
258
277
  if (!rail.hasProjects) {
259
278
  const line2 = ` {gray-fg}${dashHints.projectsEmpty || "Run ufoo chat/daemon in projects to populate registry"}{/gray-fg}`;
@@ -290,6 +309,7 @@ function computeDashboardContent(options = {}) {
290
309
  selectedModeIndex,
291
310
  selectedProviderIndex,
292
311
  selectedResumeIndex,
312
+ selectedCronIndex,
293
313
  cronTasks,
294
314
  providerOptions,
295
315
  resumeOptions,
@@ -315,6 +335,7 @@ function computeDashboardContent(options = {}) {
315
335
  selectedModeIndex,
316
336
  selectedProviderIndex,
317
337
  selectedResumeIndex,
338
+ selectedCronIndex,
318
339
  cronTasks,
319
340
  providerOptions,
320
341
  resumeOptions,