u-foo 1.7.5 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) 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 +31 -5
  15. package/src/chat/index.js +152 -36
  16. package/src/chat/inputListenerController.js +14 -0
  17. package/src/chat/inputSubmitHandler.js +9 -5
  18. package/src/chat/transientAgentState.js +64 -0
  19. package/src/cli/groupCoreCommands.js +21 -12
  20. package/src/cli.js +23 -1
  21. package/src/daemon/groupOrchestrator.js +581 -97
  22. package/src/daemon/index.js +418 -3
  23. package/src/daemon/ops.js +25 -7
  24. package/src/daemon/promptLoop.js +16 -0
  25. package/src/daemon/promptRequest.js +126 -2
  26. package/src/daemon/reporting.js +18 -0
  27. package/src/daemon/soloBootstrap.js +435 -0
  28. package/src/daemon/status.js +5 -1
  29. package/src/globalMode.js +33 -0
  30. package/src/group/bootstrap.js +157 -0
  31. package/src/group/promptProfiles.js +646 -0
  32. package/src/group/templateValidation.js +99 -0
  33. package/src/group/validateTemplate.js +36 -5
  34. package/src/init/index.js +13 -7
  35. package/src/report/store.js +6 -0
  36. package/src/shared/eventContract.js +1 -0
  37. package/templates/groups/{dev-basic.json → build-lane.json} +38 -34
  38. package/templates/groups/product-discovery.json +79 -0
  39. package/templates/groups/ui-polish.json +87 -0
  40. package/templates/groups/verify-ship.json +79 -0
  41. 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
  }
@@ -36,6 +36,7 @@ function buildSummaryLine(options = {}) {
36
36
  launchMode = "terminal",
37
37
  agentProvider = "codex-cli",
38
38
  cronTasks = [],
39
+ pendingReports = 0,
39
40
  } = options;
40
41
  const agents = activeAgents.length > 0
41
42
  ? activeAgents.slice(0, 3)
@@ -46,6 +47,7 @@ function buildSummaryLine(options = {}) {
46
47
  return `{gray-fg}Agents:{/gray-fg} {cyan-fg}${agents}{/cyan-fg}`
47
48
  + ` {gray-fg}Mode:{/gray-fg} {cyan-fg}${launchMode}{/cyan-fg}`
48
49
  + ` {gray-fg}Agent:{/gray-fg} {cyan-fg}${providerLabel(agentProvider)}{/cyan-fg}`
50
+ + ` {gray-fg}Reports:{/gray-fg} {cyan-fg}${Number.isFinite(pendingReports) ? pendingReports : 0}{/cyan-fg}`
49
51
  + ` {gray-fg}Cron:{/gray-fg} {cyan-fg}${Array.isArray(cronTasks) ? cronTasks.length : 0}{/cyan-fg}`;
50
52
  }
51
53
 
@@ -57,6 +59,8 @@ function buildProjectRailLine(options = {}) {
57
59
  maxProjectWindow = 5,
58
60
  activeProjectRoot = "",
59
61
  projectsFocused = false,
62
+ globalScope = "",
63
+ dashboardHint = "",
60
64
  } = options;
61
65
  const rows = Array.isArray(projects) ? projects : [];
62
66
  let windowStart = projectListWindowStart;
@@ -92,7 +96,7 @@ function buildProjectRailLine(options = {}) {
92
96
  const absoluteIndex = start + i;
93
97
  const name = String((row && row.project_name) || (row && row.project_root) || "-");
94
98
  const rowRoot = String((row && row.project_root) || "");
95
- const isActiveProject = Boolean(activeRoot && rowRoot === activeRoot);
99
+ const isActiveProject = globalScope !== "controller" && Boolean(activeRoot && rowRoot === activeRoot);
96
100
  const isSelected = absoluteIndex === safeSelectedIndex;
97
101
  if (projectsFocused && isSelected) {
98
102
  return `{inverse}${name}{/inverse}`;
@@ -105,9 +109,10 @@ function buildProjectRailLine(options = {}) {
105
109
 
106
110
  const leftMore = start > 0 ? "{gray-fg}<{/gray-fg} " : "";
107
111
  const rightMore = end < rows.length ? " {gray-fg}>{/gray-fg}" : "";
112
+ const hintPart = dashboardHint ? `{|}{gray-fg}${dashboardHint}{/gray-fg}` : "";
108
113
  return {
109
114
  hasProjects: true,
110
- line: ` {gray-fg}Projects:{/gray-fg} ${leftMore}${projectParts.join(" ")}${rightMore}`,
115
+ line: ` {gray-fg}Projects:{/gray-fg} ${leftMore}${projectParts.join(" ")}${rightMore}${hintPart}`,
111
116
  windowStart,
112
117
  };
113
118
  }
@@ -125,7 +130,9 @@ function buildDashboardDetailLine(options = {}) {
125
130
  selectedModeIndex = 0,
126
131
  selectedProviderIndex = 0,
127
132
  selectedResumeIndex = 0,
133
+ selectedCronIndex = -1,
128
134
  cronTasks = [],
135
+ pendingReports = 0,
129
136
  providerOptions = [],
130
137
  resumeOptions = [],
131
138
  dashHints = {},
@@ -174,9 +181,15 @@ function buildDashboardDetailLine(options = {}) {
174
181
  if (dashboardView === "cron") {
175
182
  const items = Array.isArray(cronTasks) ? cronTasks : [];
176
183
  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}`;
184
+ ? items.map((item, index) => {
185
+ const label = item.label || item.summary || item.id || "";
186
+ if (!label) return "";
187
+ return index === selectedCronIndex
188
+ ? `{inverse}${label}{/inverse}`
189
+ : `{cyan-fg}${label}{/cyan-fg}`;
190
+ }).filter(Boolean).join(", ")
191
+ : "{cyan-fg}none{/cyan-fg}";
192
+ content += `{gray-fg}Cron:{/gray-fg} ${summary}`;
180
193
  content += ` {gray-fg}│ ${dashHints.cron || ""}{/gray-fg}`;
181
194
  return { content, windowStart };
182
195
  }
@@ -220,6 +233,7 @@ function buildDashboardDetailLine(options = {}) {
220
233
  function computeDashboardContent(options = {}) {
221
234
  const {
222
235
  globalMode = false,
236
+ globalScope = "controller",
223
237
  focusMode = "input",
224
238
  dashboardView = "agents",
225
239
  activeAgents = [],
@@ -238,7 +252,9 @@ function computeDashboardContent(options = {}) {
238
252
  selectedModeIndex = 0,
239
253
  selectedProviderIndex = 0,
240
254
  selectedResumeIndex = 0,
255
+ selectedCronIndex = -1,
241
256
  cronTasks = [],
257
+ pendingReports = 0,
242
258
  providerOptions = [],
243
259
  resumeOptions = [],
244
260
  dashHints = {},
@@ -247,6 +263,10 @@ function computeDashboardContent(options = {}) {
247
263
 
248
264
  if (globalMode) {
249
265
  const projectsFocused = focusMode === "dashboard" && dashboardView === "projects";
266
+ let dashboardHint = "";
267
+ if (projectsFocused) {
268
+ dashboardHint = globalScope === "controller" ? "Enter\u2192project" : "Esc\u2192global";
269
+ }
250
270
  const rail = buildProjectRailLine({
251
271
  projects,
252
272
  selectedProjectIndex,
@@ -254,6 +274,8 @@ function computeDashboardContent(options = {}) {
254
274
  maxProjectWindow,
255
275
  activeProjectRoot,
256
276
  projectsFocused,
277
+ globalScope,
278
+ dashboardHint,
257
279
  });
258
280
  if (!rail.hasProjects) {
259
281
  const line2 = ` {gray-fg}${dashHints.projectsEmpty || "Run ufoo chat/daemon in projects to populate registry"}{/gray-fg}`;
@@ -271,6 +293,7 @@ function computeDashboardContent(options = {}) {
271
293
  launchMode,
272
294
  agentProvider,
273
295
  cronTasks,
296
+ pendingReports,
274
297
  });
275
298
  return {
276
299
  content: `${rail.line}\n ${line2}`,
@@ -290,6 +313,7 @@ function computeDashboardContent(options = {}) {
290
313
  selectedModeIndex,
291
314
  selectedProviderIndex,
292
315
  selectedResumeIndex,
316
+ selectedCronIndex,
293
317
  cronTasks,
294
318
  providerOptions,
295
319
  resumeOptions,
@@ -315,6 +339,7 @@ function computeDashboardContent(options = {}) {
315
339
  selectedModeIndex,
316
340
  selectedProviderIndex,
317
341
  selectedResumeIndex,
342
+ selectedCronIndex,
318
343
  cronTasks,
319
344
  providerOptions,
320
345
  resumeOptions,
@@ -331,6 +356,7 @@ function computeDashboardContent(options = {}) {
331
356
  launchMode,
332
357
  agentProvider,
333
358
  cronTasks,
359
+ pendingReports,
334
360
  });
335
361
 
336
362
  return { content, windowStart: agentListWindowStart };