u-foo 1.7.4 → 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.
- package/README.md +9 -1
- package/README.zh-CN.md +9 -1
- package/bin/ufoo.js +4 -2
- package/package.json +1 -1
- package/src/agent/cliRunner.js +3 -2
- package/src/agent/ucodeBootstrap.js +5 -3
- package/src/agent/ufooAgent.js +185 -6
- package/src/assistant/constants.js +1 -1
- package/src/assistant/engine.js +1 -6
- package/src/chat/commandExecutor.js +116 -19
- package/src/chat/commands.js +8 -1
- package/src/chat/completionController.js +40 -0
- package/src/chat/cronScheduler.js +37 -6
- package/src/chat/daemonMessageRouter.js +23 -3
- package/src/chat/dashboardKeyController.js +48 -59
- package/src/chat/dashboardView.js +31 -39
- package/src/chat/index.js +154 -77
- package/src/chat/inputListenerController.js +14 -0
- package/src/chat/inputSubmitHandler.js +9 -5
- package/src/chat/settingsController.js +0 -28
- package/src/chat/transientAgentState.js +64 -0
- package/src/cli/groupCoreCommands.js +21 -12
- package/src/cli.js +23 -1
- package/src/daemon/cronOps.js +48 -11
- package/src/daemon/groupOrchestrator.js +581 -97
- package/src/daemon/index.js +420 -5
- package/src/daemon/ops.js +25 -7
- package/src/daemon/promptLoop.js +16 -0
- package/src/daemon/promptRequest.js +126 -2
- package/src/daemon/reporting.js +18 -0
- package/src/daemon/soloBootstrap.js +435 -0
- package/src/daemon/status.js +7 -1
- package/src/globalMode.js +33 -0
- package/src/group/bootstrap.js +157 -0
- package/src/group/promptProfiles.js +646 -0
- package/src/group/templateValidation.js +99 -0
- package/src/group/validateTemplate.js +36 -5
- package/src/init/index.js +13 -7
- package/src/report/store.js +6 -0
- package/src/shared/eventContract.js +1 -0
- package/templates/groups/{dev-basic.json → build-lane.json} +38 -34
- package/templates/groups/product-discovery.json +79 -0
- package/templates/groups/ui-polish.json +87 -0
- package/templates/groups/verify-ship.json +79 -0
- package/templates/groups/research-quick.json +0 -49
|
@@ -6,6 +6,8 @@ const { runGroupCoreCommand } = require("../cli/groupCoreCommands");
|
|
|
6
6
|
const { loadConfig: loadProjectConfig, saveConfig: saveProjectConfig, loadGlobalUcodeConfig, saveGlobalUcodeConfig } = require("../config");
|
|
7
7
|
const { resolveTransport } = require("../code/nativeRunner");
|
|
8
8
|
const { parseIntervalMs, formatIntervalMs } = require("./cronScheduler");
|
|
9
|
+
const { isGlobalControllerProjectRoot, resolveGlobalControllerUfooDir } = require("../globalMode");
|
|
10
|
+
const { loadPromptProfileRegistry } = require("../group/promptProfiles");
|
|
9
11
|
|
|
10
12
|
function defaultCreateDoctor(projectRoot) {
|
|
11
13
|
const UfooDoctor = require("../doctor");
|
|
@@ -92,6 +94,7 @@ function createCommandExecutor(options = {}) {
|
|
|
92
94
|
stopCronTask = () => false,
|
|
93
95
|
runGroupCore = runGroupCoreCommand,
|
|
94
96
|
requestCron = null,
|
|
97
|
+
globalMode = false,
|
|
95
98
|
listProjects = () => [],
|
|
96
99
|
getCurrentProject = () => ({ projectRoot }),
|
|
97
100
|
switchProject = async () => ({ ok: false, error: "project switching unavailable" }),
|
|
@@ -404,7 +407,7 @@ function createCommandExecutor(options = {}) {
|
|
|
404
407
|
if (args.length === 0) {
|
|
405
408
|
logMessage(
|
|
406
409
|
"error",
|
|
407
|
-
"{white-fg}✗{/white-fg} Usage: /launch <claude|codex|ucode> [nickname=<name>] [count=<n>] [scope=inplace|window]"
|
|
410
|
+
"{white-fg}✗{/white-fg} Usage: /launch <claude|codex|ucode> [nickname=<name>] [profile=<id>] [count=<n>] [scope=inplace|window]"
|
|
408
411
|
);
|
|
409
412
|
return;
|
|
410
413
|
}
|
|
@@ -452,6 +455,7 @@ function createCommandExecutor(options = {}) {
|
|
|
452
455
|
}
|
|
453
456
|
|
|
454
457
|
const nickname = parsedOptions.nickname || "";
|
|
458
|
+
const promptProfile = parsedOptions.profile || parsedOptions.prompt_profile || "";
|
|
455
459
|
const count = parseInt(parsedOptions.count || "1", 10);
|
|
456
460
|
const scopeRaw = parsedOptions.scope || parsedOptions.launch_scope || parsedOptions.window || "";
|
|
457
461
|
let launchScope = normalizeLaunchScopeOption(scopeRaw, "inplace");
|
|
@@ -473,6 +477,10 @@ function createCommandExecutor(options = {}) {
|
|
|
473
477
|
logMessage("error", "{white-fg}✗{/white-fg} nickname requires count=1");
|
|
474
478
|
return;
|
|
475
479
|
}
|
|
480
|
+
if (promptProfile && count > 1) {
|
|
481
|
+
logMessage("error", "{white-fg}✗{/white-fg} profile requires count=1");
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
476
484
|
|
|
477
485
|
try {
|
|
478
486
|
const request = {
|
|
@@ -480,6 +488,7 @@ function createCommandExecutor(options = {}) {
|
|
|
480
488
|
agent: normalizedAgent,
|
|
481
489
|
count: Number.isFinite(count) ? count : 1,
|
|
482
490
|
nickname,
|
|
491
|
+
prompt_profile: promptProfile,
|
|
483
492
|
launch_scope: launchScope,
|
|
484
493
|
...collectHostLaunchRequestContext(),
|
|
485
494
|
};
|
|
@@ -494,6 +503,52 @@ function createCommandExecutor(options = {}) {
|
|
|
494
503
|
}
|
|
495
504
|
}
|
|
496
505
|
|
|
506
|
+
async function handleRoleCommand(args = []) {
|
|
507
|
+
const action = String(args[0] || "").trim().toLowerCase();
|
|
508
|
+
if (action === "list" || action === "ls") {
|
|
509
|
+
try {
|
|
510
|
+
const registry = loadPromptProfileRegistry(projectRoot);
|
|
511
|
+
const profiles = registry.profiles || [];
|
|
512
|
+
if (profiles.length === 0) {
|
|
513
|
+
logMessage("system", "{white-fg}⚙{/white-fg} No prompt profiles found.");
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
logMessage("system", `{white-fg}⚙{/white-fg} Available prompt profiles (${profiles.length}):`);
|
|
517
|
+
for (const p of profiles) {
|
|
518
|
+
const aliases = p.aliases && p.aliases.length > 0 ? ` {gray-fg}(${p.aliases.join(", ")}){/gray-fg}` : "";
|
|
519
|
+
const source = p.source ? ` {cyan-fg}[${p.source}]{/cyan-fg}` : "";
|
|
520
|
+
const summary = p.summary ? ` ${p.summary}` : "";
|
|
521
|
+
logMessage("system", ` {bold}${escapeBlessed(p.id)}{/bold}${aliases}${source}`);
|
|
522
|
+
if (summary) {
|
|
523
|
+
logMessage("system", ` ${escapeBlessed(summary)}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
} catch (err) {
|
|
527
|
+
logMessage("error", `{white-fg}✗{/white-fg} Failed to list profiles: ${escapeBlessed(err.message)}`);
|
|
528
|
+
}
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const target = String(args[0] || "").trim();
|
|
533
|
+
const profile = String(args[1] || "").trim();
|
|
534
|
+
if (!target || !profile) {
|
|
535
|
+
logMessage("error", "{white-fg}✗{/white-fg} Usage: /role <agent-id|nickname> <prompt-profile>");
|
|
536
|
+
logMessage("error", " /role list");
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
try {
|
|
541
|
+
send({
|
|
542
|
+
type: IPC_REQUEST_TYPES.ASSIGN_ROLE,
|
|
543
|
+
target,
|
|
544
|
+
prompt_profile: profile,
|
|
545
|
+
});
|
|
546
|
+
schedule(requestStatus, 1000);
|
|
547
|
+
} catch (err) {
|
|
548
|
+
logMessage("error", `{white-fg}✗{/white-fg} Role assignment failed: ${escapeBlessed(err.message)}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
497
552
|
async function handleResumeCommand(args = []) {
|
|
498
553
|
const action = String(args[0] || "").toLowerCase();
|
|
499
554
|
if (action === "list" || action === "ls") {
|
|
@@ -517,7 +572,11 @@ function createCommandExecutor(options = {}) {
|
|
|
517
572
|
|
|
518
573
|
if (subcommand === "list") {
|
|
519
574
|
const rowsRaw = await Promise.resolve(listProjects());
|
|
520
|
-
const rows = Array.isArray(rowsRaw) ? rowsRaw : []
|
|
575
|
+
const rows = (Array.isArray(rowsRaw) ? rowsRaw : []).filter((row) => {
|
|
576
|
+
if (!globalMode) return true;
|
|
577
|
+
const root = row && row.project_root ? String(row.project_root) : "";
|
|
578
|
+
return !isGlobalControllerProjectRoot(root);
|
|
579
|
+
});
|
|
521
580
|
const current = await Promise.resolve(getCurrentProject());
|
|
522
581
|
const currentRoot = current && current.project_root ? String(current.project_root) : "";
|
|
523
582
|
if (rows.length === 0) {
|
|
@@ -545,12 +604,19 @@ function createCommandExecutor(options = {}) {
|
|
|
545
604
|
logMessage("error", "{white-fg}✗{/white-fg} Current project unavailable");
|
|
546
605
|
return;
|
|
547
606
|
}
|
|
607
|
+
if (globalMode && isGlobalControllerProjectRoot(current.project_root)) {
|
|
608
|
+
logMessage(
|
|
609
|
+
"system",
|
|
610
|
+
`{cyan-fg}Current:{/cyan-fg} global controller (${escapeBlessed(resolveGlobalControllerUfooDir())})`
|
|
611
|
+
);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
548
614
|
logMessage("system", `{cyan-fg}Current:{/cyan-fg} ${escapeBlessed(current.project_root)}`);
|
|
549
615
|
return;
|
|
550
616
|
}
|
|
551
617
|
|
|
552
618
|
if (subcommand === "switch") {
|
|
553
|
-
const target =
|
|
619
|
+
const target = args.slice(1).join(" ").trim();
|
|
554
620
|
if (!target) {
|
|
555
621
|
logMessage("error", "{white-fg}✗{/white-fg} Usage: /project switch <index|path>");
|
|
556
622
|
return;
|
|
@@ -570,6 +636,27 @@ function createCommandExecutor(options = {}) {
|
|
|
570
636
|
logMessage("error", "{white-fg}✗{/white-fg} Unknown project command. Use: list, current, switch");
|
|
571
637
|
}
|
|
572
638
|
|
|
639
|
+
async function handleOpenCommand(args = []) {
|
|
640
|
+
if (!globalMode) {
|
|
641
|
+
logMessage("error", "{white-fg}✗{/white-fg} /open is only available in global mode");
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const target = args.join(" ").trim();
|
|
645
|
+
if (!target) {
|
|
646
|
+
logMessage("error", "{white-fg}✗{/white-fg} Usage: /open <path>");
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
logMessage("system", `{white-fg}⚙{/white-fg} Opening project: ${escapeBlessed(target)}`);
|
|
650
|
+
const result = await Promise.resolve(switchProject({ target }));
|
|
651
|
+
if (!result || result.ok !== true) {
|
|
652
|
+
const reason = result && result.error ? String(result.error) : "open failed";
|
|
653
|
+
logMessage("error", `{white-fg}✗{/white-fg} Open failed: ${escapeBlessed(reason)}`);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const nextRoot = result.project_root || result.projectRoot || "";
|
|
657
|
+
logMessage("system", `{white-fg}✓{/white-fg} Opened project: ${escapeBlessed(nextRoot)}`);
|
|
658
|
+
}
|
|
659
|
+
|
|
573
660
|
function parseKeyValueArgs(args = []) {
|
|
574
661
|
const parsed = {};
|
|
575
662
|
for (const raw of args) {
|
|
@@ -688,6 +775,9 @@ function createCommandExecutor(options = {}) {
|
|
|
688
775
|
const targetsRaw = String(
|
|
689
776
|
kv.target || kv.targets || kv.agent || kv.agents || ""
|
|
690
777
|
).trim();
|
|
778
|
+
const title = String(
|
|
779
|
+
kv.title || kv.name || kv.label || ""
|
|
780
|
+
).trim();
|
|
691
781
|
const prompt = String(
|
|
692
782
|
kv.prompt || kv.message || kv.msg || nonKvParts.join(" ") || ""
|
|
693
783
|
).trim();
|
|
@@ -695,7 +785,7 @@ function createCommandExecutor(options = {}) {
|
|
|
695
785
|
if ((!intervalRaw && !atRaw) || !targetsRaw || !prompt) {
|
|
696
786
|
logMessage(
|
|
697
787
|
"error",
|
|
698
|
-
"{white-fg}✗{/white-fg} Usage: /cron start every=<10s|5m> or at=\"YYYY-MM-DD HH:mm\" target=<agent1,agent2> prompt=\"...\""
|
|
788
|
+
"{white-fg}✗{/white-fg} Usage: /cron start every=<10s|5m> or at=\"YYYY-MM-DD HH:mm\" target=<agent1,agent2> [title=\"...\"] prompt=\"...\""
|
|
699
789
|
);
|
|
700
790
|
return;
|
|
701
791
|
}
|
|
@@ -728,21 +818,18 @@ function createCommandExecutor(options = {}) {
|
|
|
728
818
|
}
|
|
729
819
|
|
|
730
820
|
if (typeof requestCron === "function") {
|
|
821
|
+
const request = {
|
|
822
|
+
operation: "start",
|
|
823
|
+
targets,
|
|
824
|
+
prompt,
|
|
825
|
+
};
|
|
826
|
+
if (title) request.title = title;
|
|
731
827
|
if (atMs > 0) {
|
|
732
|
-
|
|
733
|
-
operation: "start",
|
|
734
|
-
once_at_ms: atMs,
|
|
735
|
-
targets,
|
|
736
|
-
prompt,
|
|
737
|
-
});
|
|
828
|
+
request.once_at_ms = atMs;
|
|
738
829
|
} else {
|
|
739
|
-
|
|
740
|
-
operation: "start",
|
|
741
|
-
interval_ms: intervalMs,
|
|
742
|
-
targets,
|
|
743
|
-
prompt,
|
|
744
|
-
});
|
|
830
|
+
request.interval_ms = intervalMs;
|
|
745
831
|
}
|
|
832
|
+
requestCron(request);
|
|
746
833
|
schedule(requestStatus, 200);
|
|
747
834
|
return;
|
|
748
835
|
}
|
|
@@ -752,11 +839,13 @@ function createCommandExecutor(options = {}) {
|
|
|
752
839
|
return;
|
|
753
840
|
}
|
|
754
841
|
|
|
755
|
-
const
|
|
842
|
+
const taskPayload = {
|
|
756
843
|
intervalMs,
|
|
757
844
|
targets,
|
|
758
845
|
prompt,
|
|
759
|
-
}
|
|
846
|
+
};
|
|
847
|
+
if (title) taskPayload.title = title;
|
|
848
|
+
const task = createCronTask(taskPayload);
|
|
760
849
|
if (!task) {
|
|
761
850
|
logMessage("error", "{white-fg}✗{/white-fg} Failed to create cron task");
|
|
762
851
|
return;
|
|
@@ -764,7 +853,7 @@ function createCommandExecutor(options = {}) {
|
|
|
764
853
|
|
|
765
854
|
logMessage(
|
|
766
855
|
"system",
|
|
767
|
-
`{white-fg}✓{/white-fg} Cron started ${task.id}: ${atMs > 0 ? `at ${formatCronAt(atMs)}` : `every ${formatIntervalMs(intervalMs)}`} -> ${targets.join(", ")}`
|
|
856
|
+
`{white-fg}✓{/white-fg} Cron started ${task.id}: ${task.label || `${atMs > 0 ? `at ${formatCronAt(atMs)}` : `every ${formatIntervalMs(intervalMs)}`} -> ${targets.join(", ")}`}`
|
|
768
857
|
);
|
|
769
858
|
}
|
|
770
859
|
|
|
@@ -1118,12 +1207,18 @@ function createCommandExecutor(options = {}) {
|
|
|
1118
1207
|
case "launch":
|
|
1119
1208
|
await handleLaunchCommand(args);
|
|
1120
1209
|
return true;
|
|
1210
|
+
case "open":
|
|
1211
|
+
await handleOpenCommand(args);
|
|
1212
|
+
return true;
|
|
1121
1213
|
case "resume":
|
|
1122
1214
|
await handleResumeCommand(args);
|
|
1123
1215
|
return true;
|
|
1124
1216
|
case "project":
|
|
1125
1217
|
await handleProjectCommand(args);
|
|
1126
1218
|
return true;
|
|
1219
|
+
case "role":
|
|
1220
|
+
await handleRoleCommand(args);
|
|
1221
|
+
return true;
|
|
1127
1222
|
case "cron":
|
|
1128
1223
|
await handleCronCommand(args);
|
|
1129
1224
|
return true;
|
|
@@ -1152,8 +1247,10 @@ function createCommandExecutor(options = {}) {
|
|
|
1152
1247
|
handleCtxCommand,
|
|
1153
1248
|
handleSkillsCommand,
|
|
1154
1249
|
handleLaunchCommand,
|
|
1250
|
+
handleOpenCommand,
|
|
1155
1251
|
handleResumeCommand,
|
|
1156
1252
|
handleProjectCommand,
|
|
1253
|
+
handleRoleCommand,
|
|
1157
1254
|
handleCronCommand,
|
|
1158
1255
|
handleGroupCommand,
|
|
1159
1256
|
handleSettingsCommand,
|
package/src/chat/commands.js
CHANGED
|
@@ -30,7 +30,7 @@ const COMMAND_TREE = {
|
|
|
30
30
|
"/cron": {
|
|
31
31
|
desc: "Cron scheduler operations",
|
|
32
32
|
children: {
|
|
33
|
-
start: { desc: "Create cron task" },
|
|
33
|
+
start: { desc: "Create cron task (optional title)" },
|
|
34
34
|
list: { desc: "List cron tasks" },
|
|
35
35
|
stop: { desc: "Stop cron task by id or all" },
|
|
36
36
|
},
|
|
@@ -47,6 +47,7 @@ const COMMAND_TREE = {
|
|
|
47
47
|
},
|
|
48
48
|
},
|
|
49
49
|
"/init": { desc: "Initialize modules" },
|
|
50
|
+
"/open": { desc: "Open project path in global mode" },
|
|
50
51
|
"/launch": {
|
|
51
52
|
desc: "Launch new agent",
|
|
52
53
|
children: {
|
|
@@ -63,6 +64,12 @@ const COMMAND_TREE = {
|
|
|
63
64
|
switch: { desc: "Switch daemon connection to project index/path" },
|
|
64
65
|
},
|
|
65
66
|
},
|
|
67
|
+
"/role": {
|
|
68
|
+
desc: "Assign preset role to an existing agent",
|
|
69
|
+
children: {
|
|
70
|
+
list: { desc: "List available prompt profiles" },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
66
73
|
"/resume": {
|
|
67
74
|
desc: "Resume agents (optional nickname) or list recoverable targets",
|
|
68
75
|
children: {
|
|
@@ -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
|
}
|
|
@@ -29,13 +29,39 @@ function sanitizeSummaryText(value = "") {
|
|
|
29
29
|
.trim();
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function truncateCronText(value = "", maxLength = 24) {
|
|
33
|
+
const text = String(value || "").trim();
|
|
34
|
+
if (!text) return "";
|
|
35
|
+
if (text.length <= maxLength) return text;
|
|
36
|
+
return `${text.slice(0, Math.max(1, maxLength - 3))}...`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveTaskTitle(task = {}) {
|
|
40
|
+
const explicitTitle = truncateCronText(
|
|
41
|
+
sanitizeSummaryText(task.title || "").replace(/:/g, "-"),
|
|
42
|
+
24
|
|
43
|
+
);
|
|
44
|
+
if (explicitTitle) return explicitTitle;
|
|
45
|
+
const fallbackTitle = truncateCronText(
|
|
46
|
+
sanitizeSummaryText(task.prompt || "").replace(/:/g, "-"),
|
|
47
|
+
24
|
|
48
|
+
);
|
|
49
|
+
return fallbackTitle || "(empty)";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildTaskLabel(task = {}) {
|
|
53
|
+
const targets = Array.isArray(task.targets) && task.targets.length > 0
|
|
54
|
+
? task.targets.join("+")
|
|
55
|
+
: "unknown";
|
|
56
|
+
const title = resolveTaskTitle(task);
|
|
57
|
+
const interval = formatIntervalMs(task.intervalMs || 0);
|
|
58
|
+
return `${targets}:${title}:${interval}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
32
61
|
function summarizeTask(task = {}) {
|
|
33
62
|
const id = String(task.id || "");
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const promptRaw = sanitizeSummaryText(task.prompt || "");
|
|
37
|
-
const prompt = promptRaw.length > 24 ? `${promptRaw.slice(0, 24)}...` : promptRaw;
|
|
38
|
-
return `${id}@${interval}->${targets}: ${prompt || "(empty)"}`;
|
|
63
|
+
const label = buildTaskLabel(task);
|
|
64
|
+
return id ? `${id} ${label}` : label;
|
|
39
65
|
}
|
|
40
66
|
|
|
41
67
|
function createCronScheduler(options = {}) {
|
|
@@ -58,7 +84,7 @@ function createCronScheduler(options = {}) {
|
|
|
58
84
|
}
|
|
59
85
|
}
|
|
60
86
|
|
|
61
|
-
function addTask({ intervalMs = 0, targets = [], prompt = "" } = {}) {
|
|
87
|
+
function addTask({ intervalMs = 0, targets = [], prompt = "", title = "" } = {}) {
|
|
62
88
|
const safeInterval = Number.parseInt(intervalMs, 10);
|
|
63
89
|
const safeTargets = Array.isArray(targets)
|
|
64
90
|
? targets.map((item) => String(item || "").trim()).filter(Boolean)
|
|
@@ -74,6 +100,7 @@ function createCronScheduler(options = {}) {
|
|
|
74
100
|
intervalMs: safeInterval,
|
|
75
101
|
targets: Array.from(new Set(safeTargets)),
|
|
76
102
|
prompt: safePrompt,
|
|
103
|
+
title: resolveTaskTitle({ title, prompt: safePrompt }),
|
|
77
104
|
createdAt: nowFn(),
|
|
78
105
|
lastRunAt: 0,
|
|
79
106
|
tickCount: 0,
|
|
@@ -100,6 +127,7 @@ function createCronScheduler(options = {}) {
|
|
|
100
127
|
notifyChange();
|
|
101
128
|
return {
|
|
102
129
|
...task,
|
|
130
|
+
label: buildTaskLabel(task),
|
|
103
131
|
summary: summarizeTask(task),
|
|
104
132
|
};
|
|
105
133
|
}
|
|
@@ -110,9 +138,11 @@ function createCronScheduler(options = {}) {
|
|
|
110
138
|
intervalMs: task.intervalMs,
|
|
111
139
|
targets: task.targets.slice(),
|
|
112
140
|
prompt: task.prompt,
|
|
141
|
+
title: task.title,
|
|
113
142
|
createdAt: task.createdAt,
|
|
114
143
|
lastRunAt: task.lastRunAt,
|
|
115
144
|
tickCount: task.tickCount,
|
|
145
|
+
label: buildTaskLabel(task),
|
|
116
146
|
summary: summarizeTask(task),
|
|
117
147
|
}));
|
|
118
148
|
}
|
|
@@ -155,6 +185,7 @@ function createCronScheduler(options = {}) {
|
|
|
155
185
|
module.exports = {
|
|
156
186
|
parseIntervalMs,
|
|
157
187
|
formatIntervalMs,
|
|
188
|
+
buildTaskLabel,
|
|
158
189
|
summarizeTask,
|
|
159
190
|
createCronScheduler,
|
|
160
191
|
};
|
|
@@ -254,12 +254,12 @@ function createDaemonMessageRouter(options = {}) {
|
|
|
254
254
|
if (task.mode === "once") {
|
|
255
255
|
logMessage(
|
|
256
256
|
"system",
|
|
257
|
-
`{white-fg}✓{/white-fg} Cron scheduled ${escapeBlessed(task.id)}
|
|
257
|
+
`{white-fg}✓{/white-fg} Cron scheduled ${escapeBlessed(task.id)}: ${escapeBlessed(task.label || task.onceAt || String(task.onceAtMs || ""))}`
|
|
258
258
|
);
|
|
259
259
|
} else {
|
|
260
260
|
logMessage(
|
|
261
261
|
"system",
|
|
262
|
-
`{white-fg}✓{/white-fg} Cron started ${escapeBlessed(task.id)}:
|
|
262
|
+
`{white-fg}✓{/white-fg} Cron started ${escapeBlessed(task.id)}: ${escapeBlessed(task.label || task.interval || String(task.intervalMs || ""))}`
|
|
263
263
|
);
|
|
264
264
|
}
|
|
265
265
|
} else if (operation === "stop") {
|
|
@@ -286,7 +286,14 @@ function createDaemonMessageRouter(options = {}) {
|
|
|
286
286
|
payload.disambiguate.candidates.length > 0
|
|
287
287
|
) {
|
|
288
288
|
const pending = getPending();
|
|
289
|
-
|
|
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
|