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.
- 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 +26 -5
- package/src/chat/index.js +148 -41
- package/src/chat/inputListenerController.js +14 -0
- package/src/chat/inputMath.js +1 -1
- 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/code/tui.js +1 -1
- package/src/daemon/cronOps.js +11 -4
- 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)
|
|
@@ -352,7 +375,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
352
375
|
}
|
|
353
376
|
|
|
354
377
|
function ensureInputCursorVisible() {
|
|
355
|
-
const innerWidth =
|
|
378
|
+
const innerWidth = getWrapWidth();
|
|
356
379
|
if (innerWidth <= 0) return;
|
|
357
380
|
const totalRows = countLines(input.value, innerWidth);
|
|
358
381
|
const visibleRows = Math.max(1, input.height || 1);
|
|
@@ -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(),
|
|
@@ -674,7 +713,6 @@ async function runChat(projectRoot, options = {}) {
|
|
|
674
713
|
let targetAgent = null; // Selected agent for direct messaging
|
|
675
714
|
let focusMode = "input"; // "input" or "dashboard"
|
|
676
715
|
let dashboardView = "agents"; // "projects" | "agents" | "mode" | "provider" | "cron"
|
|
677
|
-
let reportPendingTotal = 0;
|
|
678
716
|
let selectedModeIndex = Math.max(0, MODE_OPTIONS.indexOf(launchMode));
|
|
679
717
|
const providerOptions = [
|
|
680
718
|
{ label: "codex", value: "codex-cli" },
|
|
@@ -687,15 +725,16 @@ async function runChat(projectRoot, options = {}) {
|
|
|
687
725
|
{ label: "Start new session", value: false },
|
|
688
726
|
];
|
|
689
727
|
let selectedResumeIndex = autoResume ? 0 : 1;
|
|
728
|
+
let selectedCronIndex = -1;
|
|
690
729
|
const DASH_HINTS = {
|
|
691
730
|
agents: "←/→ select · Enter · ↓ mode · ↑ back",
|
|
692
731
|
agentsGlobal: "←/→ select · Enter · ↓ mode · ↑ projects",
|
|
693
732
|
agentsEmpty: "↓ mode · ↑ back",
|
|
694
733
|
mode: "←/→ select · Enter · ↓ provider · ↑ back",
|
|
695
734
|
provider: "←/→ select · Enter · ↓ cron · ↑ back",
|
|
696
|
-
cron: "Ctrl+X
|
|
735
|
+
cron: "←/→ switch · Ctrl+X stop · ↑ back",
|
|
697
736
|
resume: "",
|
|
698
|
-
projects: "Use /project switch <index|path>",
|
|
737
|
+
projects: "Use /open <path> or /project switch <index|path>",
|
|
699
738
|
projectsFocus: "←/→ switch · Ctrl+X close · ↓ second row · Enter confirm · ↑ back",
|
|
700
739
|
projectsEmpty: "Run ufoo chat or ufoo daemon start in project directories",
|
|
701
740
|
};
|
|
@@ -926,9 +965,13 @@ async function runChat(projectRoot, options = {}) {
|
|
|
926
965
|
rows = [];
|
|
927
966
|
}
|
|
928
967
|
rows = filterVisibleProjectRuntimes(rows);
|
|
968
|
+
if (globalMode) {
|
|
969
|
+
rows = rows.filter((row) => !isGlobalControllerProjectRoot(resolveRuntimeProjectRoot(row)));
|
|
970
|
+
}
|
|
929
971
|
const normalizedActive = String(activeProjectRoot || "");
|
|
930
972
|
if (
|
|
931
973
|
normalizedActive
|
|
974
|
+
&& !(globalMode && isGlobalControllerProjectRoot(normalizedActive))
|
|
932
975
|
&& !rows.some((row) => resolveRuntimeProjectRoot(row) === normalizedActive)
|
|
933
976
|
) {
|
|
934
977
|
rows.unshift({
|
|
@@ -989,18 +1032,24 @@ async function runChat(projectRoot, options = {}) {
|
|
|
989
1032
|
}
|
|
990
1033
|
|
|
991
1034
|
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";
|
|
1035
|
+
// Determine scope prefix (only in global mode)
|
|
1036
|
+
let prefix = "";
|
|
1037
|
+
if (globalMode && globalScope === "controller") {
|
|
1038
|
+
prefix = "g";
|
|
1039
|
+
} else if (globalMode && globalScope === "project") {
|
|
1040
|
+
prefix = truncateText(path.basename(activeProjectRoot), 10, "");
|
|
1003
1041
|
}
|
|
1042
|
+
|
|
1043
|
+
// Build content: [prefix]>[>@agent]
|
|
1044
|
+
const content = targetAgent
|
|
1045
|
+
? `${prefix}>@${getAgentLabel(targetAgent)}`
|
|
1046
|
+
: `${prefix}>`;
|
|
1047
|
+
|
|
1048
|
+
promptBox.setContent(content);
|
|
1049
|
+
promptBox.width = content.length + 1; // content + spacer
|
|
1050
|
+
input.left = promptBox.width;
|
|
1051
|
+
input.width = `100%-${promptBox.width}`;
|
|
1052
|
+
|
|
1004
1053
|
if (!input.parent || !promptBox.parent) return;
|
|
1005
1054
|
resizeInput();
|
|
1006
1055
|
if (typeof input._updateCursor === "function") {
|
|
@@ -1126,6 +1175,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1126
1175
|
function renderDashboard() {
|
|
1127
1176
|
const computed = computeDashboardContent({
|
|
1128
1177
|
globalMode,
|
|
1178
|
+
globalScope,
|
|
1129
1179
|
focusMode,
|
|
1130
1180
|
dashboardView,
|
|
1131
1181
|
activeAgents,
|
|
@@ -1147,8 +1197,9 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1147
1197
|
: "";
|
|
1148
1198
|
}
|
|
1149
1199
|
if (metaState) return metaState;
|
|
1150
|
-
|
|
1151
|
-
|
|
1200
|
+
return getTransientAgentState(transientAgentStateMap, agentId, {
|
|
1201
|
+
ttlMs: DEFAULT_TRANSIENT_AGENT_STATE_TTL_MS,
|
|
1202
|
+
});
|
|
1152
1203
|
},
|
|
1153
1204
|
launchMode,
|
|
1154
1205
|
agentProvider,
|
|
@@ -1156,10 +1207,10 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1156
1207
|
selectedModeIndex,
|
|
1157
1208
|
selectedProviderIndex,
|
|
1158
1209
|
selectedResumeIndex,
|
|
1210
|
+
selectedCronIndex,
|
|
1159
1211
|
cronTasks,
|
|
1160
1212
|
providerOptions,
|
|
1161
1213
|
resumeOptions,
|
|
1162
|
-
pendingReports: reportPendingTotal,
|
|
1163
1214
|
dashHints: DASH_HINTS,
|
|
1164
1215
|
modeOptions: MODE_OPTIONS,
|
|
1165
1216
|
});
|
|
@@ -1197,20 +1248,12 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1197
1248
|
|
|
1198
1249
|
function updateDashboard(status) {
|
|
1199
1250
|
activeAgents = status.active || [];
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
if (!activeSet.has(id)) {
|
|
1204
|
-
transientAgentStateMap.delete(id);
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1251
|
+
pruneTransientAgentStates(transientAgentStateMap, activeAgents, {
|
|
1252
|
+
ttlMs: DEFAULT_TRANSIENT_AGENT_STATE_TTL_MS,
|
|
1253
|
+
});
|
|
1208
1254
|
if (globalMode) {
|
|
1209
1255
|
refreshProjectRuntimes();
|
|
1210
1256
|
}
|
|
1211
|
-
reportPendingTotal = Number.isFinite(status?.reports?.pending_total)
|
|
1212
|
-
? status.reports.pending_total
|
|
1213
|
-
: 0;
|
|
1214
1257
|
cronTasks = Array.isArray(status?.cron?.tasks) ? status.cron.tasks : [];
|
|
1215
1258
|
const metaList = Array.isArray(status.active_meta) ? status.active_meta : [];
|
|
1216
1259
|
let fallbackMap = null;
|
|
@@ -1286,6 +1329,12 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1286
1329
|
selectedAgentIndex = 0;
|
|
1287
1330
|
}
|
|
1288
1331
|
clampAgentWindow();
|
|
1332
|
+
} else if (dashboardView === "cron") {
|
|
1333
|
+
if (cronTasks.length === 0) {
|
|
1334
|
+
selectedCronIndex = -1;
|
|
1335
|
+
} else if (selectedCronIndex < 0 || selectedCronIndex >= cronTasks.length) {
|
|
1336
|
+
selectedCronIndex = Math.max(0, Math.min(selectedCronIndex, cronTasks.length - 1));
|
|
1337
|
+
}
|
|
1289
1338
|
}
|
|
1290
1339
|
}
|
|
1291
1340
|
syncTargetFromSelection();
|
|
@@ -1298,7 +1347,14 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1298
1347
|
dashboardView = globalMode ? "projects" : "agents";
|
|
1299
1348
|
if (globalMode) {
|
|
1300
1349
|
refreshProjectRuntimes();
|
|
1301
|
-
|
|
1350
|
+
if (globalScope === "project") {
|
|
1351
|
+
syncSelectedProjectToActive();
|
|
1352
|
+
} else {
|
|
1353
|
+
// Controller scope: no active project in list, init to 0 for navigation
|
|
1354
|
+
if (projectRuntimes.length > 0 && (selectedProjectIndex < 0 || selectedProjectIndex >= projectRuntimes.length)) {
|
|
1355
|
+
selectedProjectIndex = 0;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1302
1358
|
} else {
|
|
1303
1359
|
selectedAgentIndex = activeAgents.length > 0 ? 0 : -1;
|
|
1304
1360
|
agentListWindowStart = 0;
|
|
@@ -1307,6 +1363,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1307
1363
|
selectedModeIndex = Math.max(0, MODE_OPTIONS.indexOf(launchMode));
|
|
1308
1364
|
selectedProviderIndex = Math.max(0, providerOptions.findIndex((opt) => opt.value === agentProvider));
|
|
1309
1365
|
selectedResumeIndex = autoResume ? 0 : 1;
|
|
1366
|
+
selectedCronIndex = cronTasks.length > 0 ? 0 : -1;
|
|
1310
1367
|
// Immediately set @target when first agent is selected.
|
|
1311
1368
|
if (!globalMode && selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
|
|
1312
1369
|
targetAgent = activeAgents[selectedAgentIndex];
|
|
@@ -1334,6 +1391,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1334
1391
|
selectedModeIndex: { get: () => selectedModeIndex, set: (value) => { selectedModeIndex = value; } },
|
|
1335
1392
|
selectedProviderIndex: { get: () => selectedProviderIndex, set: (value) => { selectedProviderIndex = value; } },
|
|
1336
1393
|
selectedResumeIndex: { get: () => selectedResumeIndex, set: (value) => { selectedResumeIndex = value; } },
|
|
1394
|
+
selectedCronIndex: { get: () => selectedCronIndex, set: (value) => { selectedCronIndex = value; } },
|
|
1337
1395
|
launchMode: { get: () => launchMode },
|
|
1338
1396
|
agentProvider: { get: () => agentProvider },
|
|
1339
1397
|
autoResume: { get: () => autoResume },
|
|
@@ -1382,6 +1440,19 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1382
1440
|
clampAgentWindowWithSelection,
|
|
1383
1441
|
requestProjectSwitch: requestProjectSwitchByIndex,
|
|
1384
1442
|
requestCloseProject: requestCloseProjectByIndex,
|
|
1443
|
+
requestCron: (payload = {}) => {
|
|
1444
|
+
send({
|
|
1445
|
+
type: IPC_REQUEST_TYPES.CRON,
|
|
1446
|
+
...payload,
|
|
1447
|
+
});
|
|
1448
|
+
},
|
|
1449
|
+
setGlobalScope: (scope, targetProjectRoot) => {
|
|
1450
|
+
setGlobalScope(scope, targetProjectRoot).catch((err) => {
|
|
1451
|
+
const message = err && err.message ? err.message : String(err || "scope switch failed");
|
|
1452
|
+
logMessage("error", `{white-fg}✗{/white-fg} Scope switch failed: ${escapeBlessed(message)}`);
|
|
1453
|
+
});
|
|
1454
|
+
},
|
|
1455
|
+
getGlobalScope: () => globalScope,
|
|
1385
1456
|
renderDashboard,
|
|
1386
1457
|
renderAgentDashboard,
|
|
1387
1458
|
renderScreen: () => screen.render(),
|
|
@@ -1518,7 +1589,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1518
1589
|
hasStream: (publisher) => streamTracker.hasStream(publisher),
|
|
1519
1590
|
setTransientAgentState: (agentId, state) => {
|
|
1520
1591
|
if (!agentId || !state) return;
|
|
1521
|
-
transientAgentStateMap
|
|
1592
|
+
setTransientAgentStateValue(transientAgentStateMap, agentId, state);
|
|
1522
1593
|
},
|
|
1523
1594
|
clearTransientAgentState: (agentId) => {
|
|
1524
1595
|
if (!agentId) return;
|
|
@@ -1630,6 +1701,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1630
1701
|
if (globalMode) {
|
|
1631
1702
|
refreshProjectRuntimes();
|
|
1632
1703
|
syncSelectedProjectToActive();
|
|
1704
|
+
updatePromptBox();
|
|
1633
1705
|
renderDashboard();
|
|
1634
1706
|
screen.render();
|
|
1635
1707
|
}
|
|
@@ -1645,6 +1717,44 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1645
1717
|
}
|
|
1646
1718
|
}
|
|
1647
1719
|
|
|
1720
|
+
async function setGlobalScope(scope, targetProjectRoot) {
|
|
1721
|
+
if (!globalMode) return;
|
|
1722
|
+
|
|
1723
|
+
if (scope === "controller") {
|
|
1724
|
+
if (globalScope === "controller") return;
|
|
1725
|
+
const controllerRoot = resolveGlobalControllerProjectRoot();
|
|
1726
|
+
if (activeProjectRoot !== controllerRoot) {
|
|
1727
|
+
const result = await requestProjectSwitchByTarget(controllerRoot);
|
|
1728
|
+
if (!result || !result.ok) {
|
|
1729
|
+
const reason = (result && result.error) || "switch to controller failed";
|
|
1730
|
+
logMessage("error", `{white-fg}✗{/white-fg} Scope switch failed: ${escapeBlessed(reason)}`);
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
globalScope = "controller";
|
|
1735
|
+
targetAgent = null;
|
|
1736
|
+
updatePromptBox();
|
|
1737
|
+
if (projectRuntimes.length > 0 && (selectedProjectIndex < 0 || selectedProjectIndex >= projectRuntimes.length)) {
|
|
1738
|
+
selectedProjectIndex = 0;
|
|
1739
|
+
}
|
|
1740
|
+
renderDashboard();
|
|
1741
|
+
screen.render();
|
|
1742
|
+
} else if (scope === "project") {
|
|
1743
|
+
if (!targetProjectRoot) return;
|
|
1744
|
+
targetAgent = null;
|
|
1745
|
+
const result = await requestProjectSwitchByTarget(targetProjectRoot);
|
|
1746
|
+
if (!result || !result.ok) {
|
|
1747
|
+
const reason = (result && result.error) || "switch to project failed";
|
|
1748
|
+
logMessage("error", `{white-fg}✗{/white-fg} Scope switch failed: ${escapeBlessed(reason)}`);
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
globalScope = "project";
|
|
1752
|
+
updatePromptBox();
|
|
1753
|
+
renderDashboard();
|
|
1754
|
+
screen.render();
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1648
1758
|
let projectSwitching = false;
|
|
1649
1759
|
let pendingProjectSwitchRoot = null;
|
|
1650
1760
|
let projectSwitchDebounceTimer = null;
|
|
@@ -1815,9 +1925,12 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1815
1925
|
listProjects: () => listProjectRuntimes({ validate: true, cleanupTmp: true }),
|
|
1816
1926
|
getCurrentProject: () => ({
|
|
1817
1927
|
project_root: activeProjectRoot,
|
|
1818
|
-
project_name:
|
|
1928
|
+
project_name: globalMode && isGlobalControllerProjectRoot(activeProjectRoot)
|
|
1929
|
+
? "global-controller"
|
|
1930
|
+
: path.basename(activeProjectRoot),
|
|
1819
1931
|
}),
|
|
1820
1932
|
switchProject: async ({ target } = {}) => requestProjectSwitchByTarget(target),
|
|
1933
|
+
globalMode,
|
|
1821
1934
|
});
|
|
1822
1935
|
|
|
1823
1936
|
async function executeCommand(text) {
|
|
@@ -1933,13 +2046,6 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1933
2046
|
focusInput();
|
|
1934
2047
|
});
|
|
1935
2048
|
|
|
1936
|
-
// Escape in input mode only clears @target, never exits
|
|
1937
|
-
input.key(["escape"], () => {
|
|
1938
|
-
if (targetAgent) {
|
|
1939
|
-
clearTargetAgent();
|
|
1940
|
-
}
|
|
1941
|
-
});
|
|
1942
|
-
|
|
1943
2049
|
focusInput();
|
|
1944
2050
|
if (screen.program && typeof screen.program.decset === "function") {
|
|
1945
2051
|
screen.program.decset(2004);
|
|
@@ -1957,6 +2063,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1957
2063
|
if (globalMode) {
|
|
1958
2064
|
refreshProjectRuntimes();
|
|
1959
2065
|
}
|
|
2066
|
+
updatePromptBox();
|
|
1960
2067
|
renderDashboard();
|
|
1961
2068
|
resizeInput();
|
|
1962
2069
|
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
|
}
|
package/src/chat/inputMath.js
CHANGED
|
@@ -6,7 +6,7 @@ function safeStrWidth(strWidth, value) {
|
|
|
6
6
|
function getInnerWidth({ input, screen, promptWidth = 2 }) {
|
|
7
7
|
const lpos = input.lpos || input._getCoords();
|
|
8
8
|
if (lpos && Number.isFinite(lpos.xl) && Number.isFinite(lpos.xi)) {
|
|
9
|
-
return Math.max(1, lpos.xl - lpos.xi
|
|
9
|
+
return Math.max(1, lpos.xl - lpos.xi);
|
|
10
10
|
}
|
|
11
11
|
if (typeof input.width === "number") return Math.max(1, input.width);
|
|
12
12
|
if (typeof input.width === "string") {
|
|
@@ -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)")
|
package/src/code/tui.js
CHANGED
|
@@ -566,7 +566,7 @@ function runUcodeTui({
|
|
|
566
566
|
const getWrapWidth = () => inputMath.getWrapWidth(input, getInnerWidth());
|
|
567
567
|
|
|
568
568
|
const ensureInputCursorVisible = () => {
|
|
569
|
-
const innerWidth =
|
|
569
|
+
const innerWidth = getWrapWidth();
|
|
570
570
|
if (innerWidth <= 0) return;
|
|
571
571
|
const totalRows = inputMath.countLines(input.value || "", innerWidth, (v) => input.strWidth(v));
|
|
572
572
|
const visibleRows = Math.max(1, input.height || 1);
|
package/src/daemon/cronOps.js
CHANGED
|
@@ -255,6 +255,13 @@ function createDaemonCronController(options = {}) {
|
|
|
255
255
|
task.timer = null;
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
+
function detachTimer(timer) {
|
|
259
|
+
if (timer && typeof timer.unref === "function") {
|
|
260
|
+
timer.unref();
|
|
261
|
+
}
|
|
262
|
+
return timer;
|
|
263
|
+
}
|
|
264
|
+
|
|
258
265
|
function runTask(task) {
|
|
259
266
|
task.lastRunAt = nowFn();
|
|
260
267
|
task.tickCount += 1;
|
|
@@ -292,17 +299,17 @@ function createDaemonCronController(options = {}) {
|
|
|
292
299
|
function attachTaskTimer(task) {
|
|
293
300
|
if (task.onceAtMs > 0) {
|
|
294
301
|
const delay = Math.max(0, task.onceAtMs - nowFn());
|
|
295
|
-
task.timer = setTimeoutFn(() => {
|
|
302
|
+
task.timer = detachTimer(setTimeoutFn(() => {
|
|
296
303
|
runTask(task);
|
|
297
304
|
stopTask(task.id);
|
|
298
|
-
}, delay);
|
|
305
|
+
}, delay));
|
|
299
306
|
return;
|
|
300
307
|
}
|
|
301
308
|
|
|
302
|
-
task.timer = setIntervalFn(() => {
|
|
309
|
+
task.timer = detachTimer(setIntervalFn(() => {
|
|
303
310
|
runTask(task);
|
|
304
311
|
persistState();
|
|
305
|
-
}, task.intervalMs);
|
|
312
|
+
}, task.intervalMs));
|
|
306
313
|
}
|
|
307
314
|
|
|
308
315
|
function addTask({ intervalMs = 0, onceAtMs = 0, targets = [], prompt = "", title = "" } = {}) {
|