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.
- 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 +184 -5
- package/src/assistant/constants.js +1 -1
- package/src/chat/commandExecutor.js +98 -3
- package/src/chat/commands.js +7 -0
- package/src/chat/completionController.js +40 -0
- package/src/chat/daemonMessageRouter.js +21 -1
- package/src/chat/dashboardKeyController.js +55 -3
- package/src/chat/dashboardView.js +31 -5
- package/src/chat/index.js +152 -36
- package/src/chat/inputListenerController.js +14 -0
- package/src/chat/inputSubmitHandler.js +9 -5
- package/src/chat/transientAgentState.js +64 -0
- package/src/cli/groupCoreCommands.js +21 -12
- package/src/cli.js +23 -1
- package/src/daemon/groupOrchestrator.js +581 -97
- package/src/daemon/index.js +418 -3
- 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 +5 -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
package/src/chat/index.js
CHANGED
|
@@ -45,11 +45,19 @@ const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
|
|
|
45
45
|
const { createDaemonTransport } = require("./daemonTransport");
|
|
46
46
|
const { listProjectRuntimes, resolveRuntimeDir } = require("../projects/registry");
|
|
47
47
|
const { canonicalProjectRoot, buildProjectId } = require("../projects/projectId");
|
|
48
|
+
const { loadTemplateRegistry } = require("../group/templates");
|
|
48
49
|
const {
|
|
49
50
|
sortProjectRuntimes,
|
|
50
51
|
parseTimestampMs,
|
|
51
52
|
filterVisibleProjectRuntimes,
|
|
52
53
|
} = require("./projectRuntimes");
|
|
54
|
+
const { isGlobalControllerProjectRoot, resolveGlobalControllerProjectRoot } = require("../globalMode");
|
|
55
|
+
const {
|
|
56
|
+
DEFAULT_TRANSIENT_AGENT_STATE_TTL_MS,
|
|
57
|
+
setTransientAgentState: setTransientAgentStateValue,
|
|
58
|
+
getTransientAgentState,
|
|
59
|
+
pruneTransientAgentStates,
|
|
60
|
+
} = require("./transientAgentState");
|
|
53
61
|
|
|
54
62
|
const MODE_OPTIONS = ["auto", "host", "terminal", "tmux", "internal"];
|
|
55
63
|
|
|
@@ -63,10 +71,25 @@ async function runChat(projectRoot, options = {}) {
|
|
|
63
71
|
activeProjectRoot = path.resolve(projectRoot || process.cwd());
|
|
64
72
|
}
|
|
65
73
|
|
|
66
|
-
|
|
74
|
+
let globalScope = globalMode ? "controller" : "project";
|
|
75
|
+
|
|
76
|
+
const runtimePaths = getUfooPaths(projectRoot);
|
|
77
|
+
const contextIndexFile = path.join(runtimePaths.ufooDir, "context", "decisions.jsonl");
|
|
78
|
+
const needsGlobalControllerBootstrap = globalMode && (
|
|
79
|
+
!fs.existsSync(runtimePaths.ufooDir)
|
|
80
|
+
|| !fs.existsSync(runtimePaths.busDir)
|
|
81
|
+
|| !fs.existsSync(runtimePaths.agentDir)
|
|
82
|
+
|| !fs.existsSync(contextIndexFile)
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (needsGlobalControllerBootstrap || !fs.existsSync(runtimePaths.ufooDir)) {
|
|
67
86
|
const repoRoot = path.join(__dirname, "..", "..");
|
|
68
87
|
const init = new UfooInit(repoRoot);
|
|
69
|
-
await init.init({
|
|
88
|
+
await init.init({
|
|
89
|
+
modules: "context,bus",
|
|
90
|
+
project: projectRoot,
|
|
91
|
+
controllerMode: globalMode,
|
|
92
|
+
});
|
|
70
93
|
}
|
|
71
94
|
|
|
72
95
|
// Ensure subscriber ID exists for chat (persistent across restarts)
|
|
@@ -528,6 +551,14 @@ async function runChat(projectRoot, options = {}) {
|
|
|
528
551
|
completionPanel,
|
|
529
552
|
promptBox,
|
|
530
553
|
commandRegistry: COMMAND_REGISTRY,
|
|
554
|
+
getGroupTemplateCandidates: () => {
|
|
555
|
+
const registry = loadTemplateRegistry(activeProjectRoot);
|
|
556
|
+
return registry.templates.map((item) => ({
|
|
557
|
+
alias: item.alias,
|
|
558
|
+
name: item.templateName || item.templateId || "",
|
|
559
|
+
source: item.source || "",
|
|
560
|
+
}));
|
|
561
|
+
},
|
|
531
562
|
getMentionCandidates: () => activeAgents.map((id) => ({
|
|
532
563
|
id,
|
|
533
564
|
label: getAgentLabel(id),
|
|
@@ -558,6 +589,14 @@ async function runChat(projectRoot, options = {}) {
|
|
|
558
589
|
getSelectedAgentIndex: () => selectedAgentIndex,
|
|
559
590
|
getActiveAgents: () => activeAgents,
|
|
560
591
|
getTargetAgent: () => targetAgent,
|
|
592
|
+
getGlobalScope: () => globalScope,
|
|
593
|
+
clearTargetAgent,
|
|
594
|
+
exitProjectScope: () => {
|
|
595
|
+
setGlobalScope("controller").catch((err) => {
|
|
596
|
+
const message = err && err.message ? err.message : String(err || "scope switch failed");
|
|
597
|
+
logMessage("error", `{white-fg}✗{/white-fg} Scope switch failed: ${escapeBlessed(message)}`);
|
|
598
|
+
});
|
|
599
|
+
},
|
|
561
600
|
requestCloseAgent,
|
|
562
601
|
logMessage,
|
|
563
602
|
isSuppressKeypress: () => pasteController.isSuppressKeypress(),
|
|
@@ -687,15 +726,16 @@ async function runChat(projectRoot, options = {}) {
|
|
|
687
726
|
{ label: "Start new session", value: false },
|
|
688
727
|
];
|
|
689
728
|
let selectedResumeIndex = autoResume ? 0 : 1;
|
|
729
|
+
let selectedCronIndex = -1;
|
|
690
730
|
const DASH_HINTS = {
|
|
691
731
|
agents: "←/→ select · Enter · ↓ mode · ↑ back",
|
|
692
732
|
agentsGlobal: "←/→ select · Enter · ↓ mode · ↑ projects",
|
|
693
733
|
agentsEmpty: "↓ mode · ↑ back",
|
|
694
734
|
mode: "←/→ select · Enter · ↓ provider · ↑ back",
|
|
695
735
|
provider: "←/→ select · Enter · ↓ cron · ↑ back",
|
|
696
|
-
cron: "Ctrl+X
|
|
736
|
+
cron: "←/→ switch · Ctrl+X stop · ↑ back",
|
|
697
737
|
resume: "",
|
|
698
|
-
projects: "Use /project switch <index|path>",
|
|
738
|
+
projects: "Use /open <path> or /project switch <index|path>",
|
|
699
739
|
projectsFocus: "←/→ switch · Ctrl+X close · ↓ second row · Enter confirm · ↑ back",
|
|
700
740
|
projectsEmpty: "Run ufoo chat or ufoo daemon start in project directories",
|
|
701
741
|
};
|
|
@@ -926,9 +966,13 @@ async function runChat(projectRoot, options = {}) {
|
|
|
926
966
|
rows = [];
|
|
927
967
|
}
|
|
928
968
|
rows = filterVisibleProjectRuntimes(rows);
|
|
969
|
+
if (globalMode) {
|
|
970
|
+
rows = rows.filter((row) => !isGlobalControllerProjectRoot(resolveRuntimeProjectRoot(row)));
|
|
971
|
+
}
|
|
929
972
|
const normalizedActive = String(activeProjectRoot || "");
|
|
930
973
|
if (
|
|
931
974
|
normalizedActive
|
|
975
|
+
&& !(globalMode && isGlobalControllerProjectRoot(normalizedActive))
|
|
932
976
|
&& !rows.some((row) => resolveRuntimeProjectRoot(row) === normalizedActive)
|
|
933
977
|
) {
|
|
934
978
|
rows.unshift({
|
|
@@ -989,18 +1033,24 @@ async function runChat(projectRoot, options = {}) {
|
|
|
989
1033
|
}
|
|
990
1034
|
|
|
991
1035
|
function updatePromptBox() {
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
} else {
|
|
999
|
-
promptBox.setContent(">");
|
|
1000
|
-
promptBox.width = 2;
|
|
1001
|
-
input.left = 2;
|
|
1002
|
-
input.width = "100%-2";
|
|
1036
|
+
// Determine scope prefix (only in global mode)
|
|
1037
|
+
let prefix = "";
|
|
1038
|
+
if (globalMode && globalScope === "controller") {
|
|
1039
|
+
prefix = "g";
|
|
1040
|
+
} else if (globalMode && globalScope === "project") {
|
|
1041
|
+
prefix = truncateText(path.basename(activeProjectRoot), 10, "");
|
|
1003
1042
|
}
|
|
1043
|
+
|
|
1044
|
+
// Build content: [prefix]>[>@agent]
|
|
1045
|
+
const content = targetAgent
|
|
1046
|
+
? `${prefix}>@${getAgentLabel(targetAgent)}`
|
|
1047
|
+
: `${prefix}>`;
|
|
1048
|
+
|
|
1049
|
+
promptBox.setContent(content);
|
|
1050
|
+
promptBox.width = content.length + 1; // content + spacer
|
|
1051
|
+
input.left = promptBox.width;
|
|
1052
|
+
input.width = `100%-${promptBox.width}`;
|
|
1053
|
+
|
|
1004
1054
|
if (!input.parent || !promptBox.parent) return;
|
|
1005
1055
|
resizeInput();
|
|
1006
1056
|
if (typeof input._updateCursor === "function") {
|
|
@@ -1126,6 +1176,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1126
1176
|
function renderDashboard() {
|
|
1127
1177
|
const computed = computeDashboardContent({
|
|
1128
1178
|
globalMode,
|
|
1179
|
+
globalScope,
|
|
1129
1180
|
focusMode,
|
|
1130
1181
|
dashboardView,
|
|
1131
1182
|
activeAgents,
|
|
@@ -1147,8 +1198,9 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1147
1198
|
: "";
|
|
1148
1199
|
}
|
|
1149
1200
|
if (metaState) return metaState;
|
|
1150
|
-
|
|
1151
|
-
|
|
1201
|
+
return getTransientAgentState(transientAgentStateMap, agentId, {
|
|
1202
|
+
ttlMs: DEFAULT_TRANSIENT_AGENT_STATE_TTL_MS,
|
|
1203
|
+
});
|
|
1152
1204
|
},
|
|
1153
1205
|
launchMode,
|
|
1154
1206
|
agentProvider,
|
|
@@ -1156,6 +1208,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1156
1208
|
selectedModeIndex,
|
|
1157
1209
|
selectedProviderIndex,
|
|
1158
1210
|
selectedResumeIndex,
|
|
1211
|
+
selectedCronIndex,
|
|
1159
1212
|
cronTasks,
|
|
1160
1213
|
providerOptions,
|
|
1161
1214
|
resumeOptions,
|
|
@@ -1197,20 +1250,19 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1197
1250
|
|
|
1198
1251
|
function updateDashboard(status) {
|
|
1199
1252
|
activeAgents = status.active || [];
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
if (!activeSet.has(id)) {
|
|
1204
|
-
transientAgentStateMap.delete(id);
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1253
|
+
pruneTransientAgentStates(transientAgentStateMap, activeAgents, {
|
|
1254
|
+
ttlMs: DEFAULT_TRANSIENT_AGENT_STATE_TTL_MS,
|
|
1255
|
+
});
|
|
1208
1256
|
if (globalMode) {
|
|
1209
1257
|
refreshProjectRuntimes();
|
|
1210
1258
|
}
|
|
1211
|
-
|
|
1259
|
+
const publicPending = Number.isFinite(status?.reports?.pending_total)
|
|
1212
1260
|
? status.reports.pending_total
|
|
1213
1261
|
: 0;
|
|
1262
|
+
const controllerPending = Number.isFinite(status?.controller?.pending_total)
|
|
1263
|
+
? status.controller.pending_total
|
|
1264
|
+
: 0;
|
|
1265
|
+
reportPendingTotal = publicPending + controllerPending;
|
|
1214
1266
|
cronTasks = Array.isArray(status?.cron?.tasks) ? status.cron.tasks : [];
|
|
1215
1267
|
const metaList = Array.isArray(status.active_meta) ? status.active_meta : [];
|
|
1216
1268
|
let fallbackMap = null;
|
|
@@ -1286,6 +1338,12 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1286
1338
|
selectedAgentIndex = 0;
|
|
1287
1339
|
}
|
|
1288
1340
|
clampAgentWindow();
|
|
1341
|
+
} else if (dashboardView === "cron") {
|
|
1342
|
+
if (cronTasks.length === 0) {
|
|
1343
|
+
selectedCronIndex = -1;
|
|
1344
|
+
} else if (selectedCronIndex < 0 || selectedCronIndex >= cronTasks.length) {
|
|
1345
|
+
selectedCronIndex = Math.max(0, Math.min(selectedCronIndex, cronTasks.length - 1));
|
|
1346
|
+
}
|
|
1289
1347
|
}
|
|
1290
1348
|
}
|
|
1291
1349
|
syncTargetFromSelection();
|
|
@@ -1298,7 +1356,14 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1298
1356
|
dashboardView = globalMode ? "projects" : "agents";
|
|
1299
1357
|
if (globalMode) {
|
|
1300
1358
|
refreshProjectRuntimes();
|
|
1301
|
-
|
|
1359
|
+
if (globalScope === "project") {
|
|
1360
|
+
syncSelectedProjectToActive();
|
|
1361
|
+
} else {
|
|
1362
|
+
// Controller scope: no active project in list, init to 0 for navigation
|
|
1363
|
+
if (projectRuntimes.length > 0 && (selectedProjectIndex < 0 || selectedProjectIndex >= projectRuntimes.length)) {
|
|
1364
|
+
selectedProjectIndex = 0;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1302
1367
|
} else {
|
|
1303
1368
|
selectedAgentIndex = activeAgents.length > 0 ? 0 : -1;
|
|
1304
1369
|
agentListWindowStart = 0;
|
|
@@ -1307,6 +1372,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1307
1372
|
selectedModeIndex = Math.max(0, MODE_OPTIONS.indexOf(launchMode));
|
|
1308
1373
|
selectedProviderIndex = Math.max(0, providerOptions.findIndex((opt) => opt.value === agentProvider));
|
|
1309
1374
|
selectedResumeIndex = autoResume ? 0 : 1;
|
|
1375
|
+
selectedCronIndex = cronTasks.length > 0 ? 0 : -1;
|
|
1310
1376
|
// Immediately set @target when first agent is selected.
|
|
1311
1377
|
if (!globalMode && selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
|
|
1312
1378
|
targetAgent = activeAgents[selectedAgentIndex];
|
|
@@ -1334,6 +1400,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1334
1400
|
selectedModeIndex: { get: () => selectedModeIndex, set: (value) => { selectedModeIndex = value; } },
|
|
1335
1401
|
selectedProviderIndex: { get: () => selectedProviderIndex, set: (value) => { selectedProviderIndex = value; } },
|
|
1336
1402
|
selectedResumeIndex: { get: () => selectedResumeIndex, set: (value) => { selectedResumeIndex = value; } },
|
|
1403
|
+
selectedCronIndex: { get: () => selectedCronIndex, set: (value) => { selectedCronIndex = value; } },
|
|
1337
1404
|
launchMode: { get: () => launchMode },
|
|
1338
1405
|
agentProvider: { get: () => agentProvider },
|
|
1339
1406
|
autoResume: { get: () => autoResume },
|
|
@@ -1382,6 +1449,19 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1382
1449
|
clampAgentWindowWithSelection,
|
|
1383
1450
|
requestProjectSwitch: requestProjectSwitchByIndex,
|
|
1384
1451
|
requestCloseProject: requestCloseProjectByIndex,
|
|
1452
|
+
requestCron: (payload = {}) => {
|
|
1453
|
+
send({
|
|
1454
|
+
type: IPC_REQUEST_TYPES.CRON,
|
|
1455
|
+
...payload,
|
|
1456
|
+
});
|
|
1457
|
+
},
|
|
1458
|
+
setGlobalScope: (scope, targetProjectRoot) => {
|
|
1459
|
+
setGlobalScope(scope, targetProjectRoot).catch((err) => {
|
|
1460
|
+
const message = err && err.message ? err.message : String(err || "scope switch failed");
|
|
1461
|
+
logMessage("error", `{white-fg}✗{/white-fg} Scope switch failed: ${escapeBlessed(message)}`);
|
|
1462
|
+
});
|
|
1463
|
+
},
|
|
1464
|
+
getGlobalScope: () => globalScope,
|
|
1385
1465
|
renderDashboard,
|
|
1386
1466
|
renderAgentDashboard,
|
|
1387
1467
|
renderScreen: () => screen.render(),
|
|
@@ -1518,7 +1598,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1518
1598
|
hasStream: (publisher) => streamTracker.hasStream(publisher),
|
|
1519
1599
|
setTransientAgentState: (agentId, state) => {
|
|
1520
1600
|
if (!agentId || !state) return;
|
|
1521
|
-
transientAgentStateMap
|
|
1601
|
+
setTransientAgentStateValue(transientAgentStateMap, agentId, state);
|
|
1522
1602
|
},
|
|
1523
1603
|
clearTransientAgentState: (agentId) => {
|
|
1524
1604
|
if (!agentId) return;
|
|
@@ -1630,6 +1710,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1630
1710
|
if (globalMode) {
|
|
1631
1711
|
refreshProjectRuntimes();
|
|
1632
1712
|
syncSelectedProjectToActive();
|
|
1713
|
+
updatePromptBox();
|
|
1633
1714
|
renderDashboard();
|
|
1634
1715
|
screen.render();
|
|
1635
1716
|
}
|
|
@@ -1645,6 +1726,44 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1645
1726
|
}
|
|
1646
1727
|
}
|
|
1647
1728
|
|
|
1729
|
+
async function setGlobalScope(scope, targetProjectRoot) {
|
|
1730
|
+
if (!globalMode) return;
|
|
1731
|
+
|
|
1732
|
+
if (scope === "controller") {
|
|
1733
|
+
if (globalScope === "controller") return;
|
|
1734
|
+
const controllerRoot = resolveGlobalControllerProjectRoot();
|
|
1735
|
+
if (activeProjectRoot !== controllerRoot) {
|
|
1736
|
+
const result = await requestProjectSwitchByTarget(controllerRoot);
|
|
1737
|
+
if (!result || !result.ok) {
|
|
1738
|
+
const reason = (result && result.error) || "switch to controller failed";
|
|
1739
|
+
logMessage("error", `{white-fg}✗{/white-fg} Scope switch failed: ${escapeBlessed(reason)}`);
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
globalScope = "controller";
|
|
1744
|
+
targetAgent = null;
|
|
1745
|
+
updatePromptBox();
|
|
1746
|
+
if (projectRuntimes.length > 0 && (selectedProjectIndex < 0 || selectedProjectIndex >= projectRuntimes.length)) {
|
|
1747
|
+
selectedProjectIndex = 0;
|
|
1748
|
+
}
|
|
1749
|
+
renderDashboard();
|
|
1750
|
+
screen.render();
|
|
1751
|
+
} else if (scope === "project") {
|
|
1752
|
+
if (!targetProjectRoot) return;
|
|
1753
|
+
targetAgent = null;
|
|
1754
|
+
const result = await requestProjectSwitchByTarget(targetProjectRoot);
|
|
1755
|
+
if (!result || !result.ok) {
|
|
1756
|
+
const reason = (result && result.error) || "switch to project failed";
|
|
1757
|
+
logMessage("error", `{white-fg}✗{/white-fg} Scope switch failed: ${escapeBlessed(reason)}`);
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
globalScope = "project";
|
|
1761
|
+
updatePromptBox();
|
|
1762
|
+
renderDashboard();
|
|
1763
|
+
screen.render();
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1648
1767
|
let projectSwitching = false;
|
|
1649
1768
|
let pendingProjectSwitchRoot = null;
|
|
1650
1769
|
let projectSwitchDebounceTimer = null;
|
|
@@ -1815,9 +1934,12 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1815
1934
|
listProjects: () => listProjectRuntimes({ validate: true, cleanupTmp: true }),
|
|
1816
1935
|
getCurrentProject: () => ({
|
|
1817
1936
|
project_root: activeProjectRoot,
|
|
1818
|
-
project_name:
|
|
1937
|
+
project_name: globalMode && isGlobalControllerProjectRoot(activeProjectRoot)
|
|
1938
|
+
? "global-controller"
|
|
1939
|
+
: path.basename(activeProjectRoot),
|
|
1819
1940
|
}),
|
|
1820
1941
|
switchProject: async ({ target } = {}) => requestProjectSwitchByTarget(target),
|
|
1942
|
+
globalMode,
|
|
1821
1943
|
});
|
|
1822
1944
|
|
|
1823
1945
|
async function executeCommand(text) {
|
|
@@ -1933,13 +2055,6 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1933
2055
|
focusInput();
|
|
1934
2056
|
});
|
|
1935
2057
|
|
|
1936
|
-
// Escape in input mode only clears @target, never exits
|
|
1937
|
-
input.key(["escape"], () => {
|
|
1938
|
-
if (targetAgent) {
|
|
1939
|
-
clearTargetAgent();
|
|
1940
|
-
}
|
|
1941
|
-
});
|
|
1942
|
-
|
|
1943
2058
|
focusInput();
|
|
1944
2059
|
if (screen.program && typeof screen.program.decset === "function") {
|
|
1945
2060
|
screen.program.decset(2004);
|
|
@@ -1957,6 +2072,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1957
2072
|
if (globalMode) {
|
|
1958
2073
|
refreshProjectRuntimes();
|
|
1959
2074
|
}
|
|
2075
|
+
updatePromptBox();
|
|
1960
2076
|
renderDashboard();
|
|
1961
2077
|
resizeInput();
|
|
1962
2078
|
requestStatus();
|
|
@@ -7,6 +7,9 @@ function createInputListenerController(options = {}) {
|
|
|
7
7
|
getSelectedAgentIndex = () => -1,
|
|
8
8
|
getActiveAgents = () => [],
|
|
9
9
|
getTargetAgent = () => null,
|
|
10
|
+
getGlobalScope = () => "",
|
|
11
|
+
clearTargetAgent = () => {},
|
|
12
|
+
exitProjectScope = () => {},
|
|
10
13
|
requestCloseAgent = () => {},
|
|
11
14
|
logMessage = () => {},
|
|
12
15
|
isSuppressKeypress = () => false,
|
|
@@ -238,6 +241,17 @@ function createInputListenerController(options = {}) {
|
|
|
238
241
|
}
|
|
239
242
|
|
|
240
243
|
if (keyName === "escape") {
|
|
244
|
+
// Layer 1: clear @target agent
|
|
245
|
+
if (getTargetAgent && getTargetAgent()) {
|
|
246
|
+
clearTargetAgent();
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
// Layer 2: exit project scope → global scope
|
|
250
|
+
if (getGlobalScope && getGlobalScope() === "project") {
|
|
251
|
+
exitProjectScope();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
// Layer 3: existing behavior (cancel input)
|
|
241
255
|
if (textarea && typeof textarea._done === "function") {
|
|
242
256
|
textarea._done(null, null);
|
|
243
257
|
}
|
|
@@ -154,14 +154,18 @@ function createInputSubmitHandler(options = {}) {
|
|
|
154
154
|
const choice = state.pending.disambiguate.candidates[idx - 1];
|
|
155
155
|
if (choice) {
|
|
156
156
|
queueStatusLine(`ufoo-agent processing (assigning ${choice.agent_id})`);
|
|
157
|
+
const requestMeta = {
|
|
158
|
+
source: "chat-dialog",
|
|
159
|
+
dispatch_default_injection_mode: "immediate",
|
|
160
|
+
allow_relevance_queue: true,
|
|
161
|
+
};
|
|
162
|
+
if (state.pending.project_root) {
|
|
163
|
+
requestMeta.force_project_root = state.pending.project_root;
|
|
164
|
+
}
|
|
157
165
|
send({
|
|
158
166
|
type: IPC_REQUEST_TYPES.PROMPT,
|
|
159
167
|
text: `Use agent ${choice.agent_id} to handle: ${state.pending.original || "the request"}`,
|
|
160
|
-
request_meta:
|
|
161
|
-
source: "chat-dialog",
|
|
162
|
-
dispatch_default_injection_mode: "immediate",
|
|
163
|
-
allow_relevance_queue: true,
|
|
164
|
-
},
|
|
168
|
+
request_meta: requestMeta,
|
|
165
169
|
});
|
|
166
170
|
state.pending = null;
|
|
167
171
|
} else {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_TRANSIENT_AGENT_STATE_TTL_MS = 8000;
|
|
4
|
+
|
|
5
|
+
function normalizeNow(now) {
|
|
6
|
+
return Number.isFinite(now) ? now : Date.now();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function setTransientAgentState(store, agentId, state, now = Date.now()) {
|
|
10
|
+
if (!(store instanceof Map)) return;
|
|
11
|
+
const id = String(agentId || "").trim();
|
|
12
|
+
const nextState = String(state || "").trim();
|
|
13
|
+
if (!id || !nextState) return;
|
|
14
|
+
store.set(id, {
|
|
15
|
+
state: nextState,
|
|
16
|
+
updatedAt: normalizeNow(now),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getTransientAgentState(store, agentId, options = {}) {
|
|
21
|
+
if (!(store instanceof Map)) return "";
|
|
22
|
+
const id = String(agentId || "").trim();
|
|
23
|
+
if (!id) return "";
|
|
24
|
+
const entry = store.get(id);
|
|
25
|
+
if (!entry) return "";
|
|
26
|
+
|
|
27
|
+
const ttlMs = Number.isFinite(options.ttlMs)
|
|
28
|
+
? Math.max(0, Math.trunc(options.ttlMs))
|
|
29
|
+
: DEFAULT_TRANSIENT_AGENT_STATE_TTL_MS;
|
|
30
|
+
const now = normalizeNow(options.now);
|
|
31
|
+
const state = typeof entry === "string" ? entry : String(entry.state || "").trim();
|
|
32
|
+
const updatedAt = typeof entry === "object" && Number.isFinite(entry.updatedAt)
|
|
33
|
+
? entry.updatedAt
|
|
34
|
+
: now;
|
|
35
|
+
|
|
36
|
+
if (!state) {
|
|
37
|
+
store.delete(id);
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
if (ttlMs > 0 && now - updatedAt > ttlMs) {
|
|
41
|
+
store.delete(id);
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
return state;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function pruneTransientAgentStates(store, activeAgentIds = [], options = {}) {
|
|
48
|
+
if (!(store instanceof Map)) return;
|
|
49
|
+
const activeSet = new Set(Array.isArray(activeAgentIds) ? activeAgentIds : []);
|
|
50
|
+
for (const id of Array.from(store.keys())) {
|
|
51
|
+
if (!activeSet.has(id)) {
|
|
52
|
+
store.delete(id);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
getTransientAgentState(store, id, options);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
DEFAULT_TRANSIENT_AGENT_STATE_TTL_MS,
|
|
61
|
+
setTransientAgentState,
|
|
62
|
+
getTransientAgentState,
|
|
63
|
+
pruneTransientAgentStates,
|
|
64
|
+
};
|
|
@@ -6,7 +6,7 @@ const {
|
|
|
6
6
|
resolveTemplateReference,
|
|
7
7
|
createTemplateFromBuiltin,
|
|
8
8
|
} = require("../group/templates");
|
|
9
|
-
const {
|
|
9
|
+
const { validateTemplateTarget } = require("../group/templateValidation");
|
|
10
10
|
|
|
11
11
|
function parseTemplateNewArgs(args = []) {
|
|
12
12
|
const alias = String(args[0] || "").trim();
|
|
@@ -97,15 +97,13 @@ function printList({ templates, errors }, { write, json, cwd }) {
|
|
|
97
97
|
function formatResolveErrors(errors = []) {
|
|
98
98
|
if (!Array.isArray(errors) || errors.length === 0) return "";
|
|
99
99
|
return errors
|
|
100
|
-
.map((item) => `${item.filePath}: ${item.error}`)
|
|
100
|
+
.map((item) => `${item.filePath}: ${item.error || item.message || "unknown error"}`)
|
|
101
101
|
.join("; ");
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
function throwResolveFailure(target, resolved = {}) {
|
|
105
105
|
const details = formatResolveErrors(resolved.errors || []);
|
|
106
|
-
if (details) {
|
|
107
|
-
throw new Error(`Failed to load template "${target}": ${details}`);
|
|
108
|
-
}
|
|
106
|
+
if (details) throw new Error(`Failed to load template "${target}": ${details}`);
|
|
109
107
|
throw new Error(`Template not found: ${target}`);
|
|
110
108
|
}
|
|
111
109
|
|
|
@@ -120,6 +118,7 @@ function printValidation(result, target, entry, { write, json }) {
|
|
|
120
118
|
filePath: entry.filePath,
|
|
121
119
|
ok: result.ok,
|
|
122
120
|
errors: result.errors,
|
|
121
|
+
prompt_profiles: result.promptProfiles || [],
|
|
123
122
|
},
|
|
124
123
|
null,
|
|
125
124
|
2
|
|
@@ -130,6 +129,15 @@ function printValidation(result, target, entry, { write, json }) {
|
|
|
130
129
|
|
|
131
130
|
if (result.ok) {
|
|
132
131
|
write(`✓ Template "${entry.alias}" is valid (${entry.source})`);
|
|
132
|
+
if (Array.isArray(result.promptProfiles) && result.promptProfiles.length > 0) {
|
|
133
|
+
for (const profile of result.promptProfiles) {
|
|
134
|
+
write(
|
|
135
|
+
` - ${profile.nickname || profile.agent_id || "agent"}: `
|
|
136
|
+
+ `${profile.requested_profile} -> ${profile.resolved_profile} `
|
|
137
|
+
+ `[${profile.profile_source}]`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
133
141
|
return;
|
|
134
142
|
}
|
|
135
143
|
|
|
@@ -144,6 +152,7 @@ async function runGroupCoreCommand(subcmd, cmdArgs = [], options = {}) {
|
|
|
144
152
|
const write = typeof options.write === "function" ? options.write : console.log;
|
|
145
153
|
const json = Boolean(options.json);
|
|
146
154
|
const templatesOptions = options.templatesOptions || {};
|
|
155
|
+
const promptProfilesOptions = options.promptProfilesOptions || {};
|
|
147
156
|
|
|
148
157
|
const args = Array.isArray(cmdArgs) ? cmdArgs.filter((item) => item !== undefined) : [];
|
|
149
158
|
const normalizedSubcmd = String(subcmd || "").trim().toLowerCase();
|
|
@@ -205,18 +214,18 @@ async function runGroupCoreCommand(subcmd, cmdArgs = [], options = {}) {
|
|
|
205
214
|
if (action === "validate") {
|
|
206
215
|
const target = String(args[1] || "").trim();
|
|
207
216
|
if (!target) throw new Error("group template validate requires <alias|path>");
|
|
208
|
-
const
|
|
217
|
+
const result = validateTemplateTarget(cwd, target, {
|
|
209
218
|
allowPath: true,
|
|
210
219
|
cwd,
|
|
211
|
-
|
|
220
|
+
templatesOptions,
|
|
221
|
+
promptProfilesOptions,
|
|
212
222
|
});
|
|
213
|
-
if (!
|
|
214
|
-
throwResolveFailure(target,
|
|
223
|
+
if (!result.entry) {
|
|
224
|
+
throwResolveFailure(target, { errors: result.errors || [] });
|
|
215
225
|
}
|
|
216
|
-
|
|
217
|
-
printValidation(result, target, resolved.entry, { write, json });
|
|
226
|
+
printValidation(result, target, result.entry, { write, json });
|
|
218
227
|
if (!result.ok) {
|
|
219
|
-
throw new Error(`Template validation failed: ${
|
|
228
|
+
throw new Error(`Template validation failed: ${result.entry.alias}`);
|
|
220
229
|
}
|
|
221
230
|
return;
|
|
222
231
|
}
|
package/src/cli.js
CHANGED
|
@@ -411,7 +411,8 @@ async function runCli(argv) {
|
|
|
411
411
|
.description("Launch an agent (ucode, uclaude, ucodex)")
|
|
412
412
|
.argument("<agent>", "Agent type: ucode|uclaude|ucodex|claude|codex")
|
|
413
413
|
.argument("[nickname]", "Optional nickname for the agent")
|
|
414
|
-
.
|
|
414
|
+
.option("--profile <id>", "Prompt profile to assign after launch")
|
|
415
|
+
.action(async (agent, nickname, opts) => {
|
|
415
416
|
try {
|
|
416
417
|
const projectRoot = process.cwd();
|
|
417
418
|
await ensureDaemonRunning(projectRoot);
|
|
@@ -436,6 +437,7 @@ async function runCli(argv) {
|
|
|
436
437
|
type: "launch_agent",
|
|
437
438
|
agent: normalizedAgent,
|
|
438
439
|
nickname: nickname || "",
|
|
440
|
+
prompt_profile: opts.profile || "",
|
|
439
441
|
count: 1,
|
|
440
442
|
...collectHostLaunchRequestContext(),
|
|
441
443
|
});
|
|
@@ -446,6 +448,26 @@ async function runCli(argv) {
|
|
|
446
448
|
process.exitCode = 1;
|
|
447
449
|
}
|
|
448
450
|
});
|
|
451
|
+
program
|
|
452
|
+
.command("role")
|
|
453
|
+
.description("Assign a preset role to an existing agent")
|
|
454
|
+
.argument("<target>", "Agent subscriber id or nickname")
|
|
455
|
+
.argument("<profile>", "Prompt profile id or alias")
|
|
456
|
+
.action(async (target, profile) => {
|
|
457
|
+
try {
|
|
458
|
+
const projectRoot = process.cwd();
|
|
459
|
+
await ensureDaemonRunning(projectRoot);
|
|
460
|
+
const resp = await sendDaemonRequest(projectRoot, {
|
|
461
|
+
type: "assign_role",
|
|
462
|
+
target,
|
|
463
|
+
prompt_profile: profile,
|
|
464
|
+
});
|
|
465
|
+
console.log(resp?.data?.reply || `Assigned role ${profile}`);
|
|
466
|
+
} catch (err) {
|
|
467
|
+
console.error(err.message || String(err));
|
|
468
|
+
process.exitCode = 1;
|
|
469
|
+
}
|
|
470
|
+
});
|
|
449
471
|
program
|
|
450
472
|
.command("resume")
|
|
451
473
|
.description("Resume agent sessions (optional nickname)")
|