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
package/src/chat/index.js
CHANGED
|
@@ -9,7 +9,6 @@ const {
|
|
|
9
9
|
saveConfig,
|
|
10
10
|
normalizeLaunchMode,
|
|
11
11
|
normalizeAgentProvider,
|
|
12
|
-
normalizeAssistantEngine,
|
|
13
12
|
} = require("../config");
|
|
14
13
|
const { socketPath, isRunning } = require("../daemon");
|
|
15
14
|
const UfooInit = require("../init");
|
|
@@ -46,11 +45,19 @@ const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
|
|
|
46
45
|
const { createDaemonTransport } = require("./daemonTransport");
|
|
47
46
|
const { listProjectRuntimes, resolveRuntimeDir } = require("../projects/registry");
|
|
48
47
|
const { canonicalProjectRoot, buildProjectId } = require("../projects/projectId");
|
|
48
|
+
const { loadTemplateRegistry } = require("../group/templates");
|
|
49
49
|
const {
|
|
50
50
|
sortProjectRuntimes,
|
|
51
51
|
parseTimestampMs,
|
|
52
52
|
filterVisibleProjectRuntimes,
|
|
53
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");
|
|
54
61
|
|
|
55
62
|
const MODE_OPTIONS = ["auto", "host", "terminal", "tmux", "internal"];
|
|
56
63
|
|
|
@@ -64,10 +71,25 @@ async function runChat(projectRoot, options = {}) {
|
|
|
64
71
|
activeProjectRoot = path.resolve(projectRoot || process.cwd());
|
|
65
72
|
}
|
|
66
73
|
|
|
67
|
-
|
|
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)) {
|
|
68
86
|
const repoRoot = path.join(__dirname, "..", "..");
|
|
69
87
|
const init = new UfooInit(repoRoot);
|
|
70
|
-
await init.init({
|
|
88
|
+
await init.init({
|
|
89
|
+
modules: "context,bus",
|
|
90
|
+
project: projectRoot,
|
|
91
|
+
controllerMode: globalMode,
|
|
92
|
+
});
|
|
71
93
|
}
|
|
72
94
|
|
|
73
95
|
// Ensure subscriber ID exists for chat (persistent across restarts)
|
|
@@ -104,7 +126,6 @@ async function runChat(projectRoot, options = {}) {
|
|
|
104
126
|
const config = loadConfig(projectRoot);
|
|
105
127
|
let launchMode = config.launchMode;
|
|
106
128
|
let agentProvider = config.agentProvider;
|
|
107
|
-
let assistantEngine = normalizeAssistantEngine(config.assistantEngine);
|
|
108
129
|
let autoResume = config.autoResume !== false;
|
|
109
130
|
let cronTasks = [];
|
|
110
131
|
|
|
@@ -530,6 +551,14 @@ async function runChat(projectRoot, options = {}) {
|
|
|
530
551
|
completionPanel,
|
|
531
552
|
promptBox,
|
|
532
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
|
+
},
|
|
533
562
|
getMentionCandidates: () => activeAgents.map((id) => ({
|
|
534
563
|
id,
|
|
535
564
|
label: getAgentLabel(id),
|
|
@@ -560,6 +589,14 @@ async function runChat(projectRoot, options = {}) {
|
|
|
560
589
|
getSelectedAgentIndex: () => selectedAgentIndex,
|
|
561
590
|
getActiveAgents: () => activeAgents,
|
|
562
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
|
+
},
|
|
563
600
|
requestCloseAgent,
|
|
564
601
|
logMessage,
|
|
565
602
|
isSuppressKeypress: () => pasteController.isSuppressKeypress(),
|
|
@@ -675,7 +712,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
675
712
|
let selectedAgentIndex = -1; // -1 = not in dashboard selection mode
|
|
676
713
|
let targetAgent = null; // Selected agent for direct messaging
|
|
677
714
|
let focusMode = "input"; // "input" or "dashboard"
|
|
678
|
-
let dashboardView = "agents"; // "projects" | "agents" | "mode" | "provider" | "
|
|
715
|
+
let dashboardView = "agents"; // "projects" | "agents" | "mode" | "provider" | "cron"
|
|
679
716
|
let reportPendingTotal = 0;
|
|
680
717
|
let selectedModeIndex = Math.max(0, MODE_OPTIONS.indexOf(launchMode));
|
|
681
718
|
const providerOptions = [
|
|
@@ -684,31 +721,21 @@ async function runChat(projectRoot, options = {}) {
|
|
|
684
721
|
{ label: "ucode", value: "ucode" },
|
|
685
722
|
];
|
|
686
723
|
let selectedProviderIndex = Math.max(0, providerOptions.findIndex((opt) => opt.value === agentProvider));
|
|
687
|
-
const assistantOptions = [
|
|
688
|
-
{ label: "auto", value: "auto" },
|
|
689
|
-
{ label: "codex", value: "codex" },
|
|
690
|
-
{ label: "claude", value: "claude" },
|
|
691
|
-
{ label: "ucode", value: "ufoo" },
|
|
692
|
-
];
|
|
693
|
-
let selectedAssistantIndex = Math.max(
|
|
694
|
-
0,
|
|
695
|
-
assistantOptions.findIndex((opt) => opt.value === assistantEngine)
|
|
696
|
-
);
|
|
697
724
|
const resumeOptions = [
|
|
698
725
|
{ label: "Resume previous session", value: true },
|
|
699
726
|
{ label: "Start new session", value: false },
|
|
700
727
|
];
|
|
701
728
|
let selectedResumeIndex = autoResume ? 0 : 1;
|
|
729
|
+
let selectedCronIndex = -1;
|
|
702
730
|
const DASH_HINTS = {
|
|
703
731
|
agents: "←/→ select · Enter · ↓ mode · ↑ back",
|
|
704
732
|
agentsGlobal: "←/→ select · Enter · ↓ mode · ↑ projects",
|
|
705
733
|
agentsEmpty: "↓ mode · ↑ back",
|
|
706
734
|
mode: "←/→ select · Enter · ↓ provider · ↑ back",
|
|
707
|
-
provider: "←/→ select · Enter · ↓
|
|
708
|
-
|
|
709
|
-
cron: "Ctrl+X close · ↑ back",
|
|
735
|
+
provider: "←/→ select · Enter · ↓ cron · ↑ back",
|
|
736
|
+
cron: "←/→ switch · Ctrl+X stop · ↑ back",
|
|
710
737
|
resume: "",
|
|
711
|
-
projects: "Use /project switch <index|path>",
|
|
738
|
+
projects: "Use /open <path> or /project switch <index|path>",
|
|
712
739
|
projectsFocus: "←/→ switch · Ctrl+X close · ↓ second row · Enter confirm · ↑ back",
|
|
713
740
|
projectsEmpty: "Run ufoo chat or ufoo daemon start in project directories",
|
|
714
741
|
};
|
|
@@ -939,9 +966,13 @@ async function runChat(projectRoot, options = {}) {
|
|
|
939
966
|
rows = [];
|
|
940
967
|
}
|
|
941
968
|
rows = filterVisibleProjectRuntimes(rows);
|
|
969
|
+
if (globalMode) {
|
|
970
|
+
rows = rows.filter((row) => !isGlobalControllerProjectRoot(resolveRuntimeProjectRoot(row)));
|
|
971
|
+
}
|
|
942
972
|
const normalizedActive = String(activeProjectRoot || "");
|
|
943
973
|
if (
|
|
944
974
|
normalizedActive
|
|
975
|
+
&& !(globalMode && isGlobalControllerProjectRoot(normalizedActive))
|
|
945
976
|
&& !rows.some((row) => resolveRuntimeProjectRoot(row) === normalizedActive)
|
|
946
977
|
) {
|
|
947
978
|
rows.unshift({
|
|
@@ -1002,18 +1033,24 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1002
1033
|
}
|
|
1003
1034
|
|
|
1004
1035
|
function updatePromptBox() {
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
} else {
|
|
1012
|
-
promptBox.setContent(">");
|
|
1013
|
-
promptBox.width = 2;
|
|
1014
|
-
input.left = 2;
|
|
1015
|
-
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, "");
|
|
1016
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
|
+
|
|
1017
1054
|
if (!input.parent || !promptBox.parent) return;
|
|
1018
1055
|
resizeInput();
|
|
1019
1056
|
if (typeof input._updateCursor === "function") {
|
|
@@ -1081,12 +1118,6 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1081
1118
|
}
|
|
1082
1119
|
}
|
|
1083
1120
|
|
|
1084
|
-
function setAssistantEngine(value) {
|
|
1085
|
-
if (settingsController) {
|
|
1086
|
-
settingsController.setAssistantEngine(value);
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
1121
|
function setAutoResume(value) {
|
|
1091
1122
|
if (settingsController) {
|
|
1092
1123
|
settingsController.setAutoResume(value);
|
|
@@ -1103,7 +1134,6 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1103
1134
|
saveConfig,
|
|
1104
1135
|
normalizeLaunchMode,
|
|
1105
1136
|
normalizeAgentProvider,
|
|
1106
|
-
normalizeAssistantEngine,
|
|
1107
1137
|
fsModule: fs,
|
|
1108
1138
|
getUfooPaths,
|
|
1109
1139
|
logMessage,
|
|
@@ -1124,14 +1154,6 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1124
1154
|
setSelectedProviderIndex: (value) => {
|
|
1125
1155
|
selectedProviderIndex = value;
|
|
1126
1156
|
},
|
|
1127
|
-
getAssistantEngine: () => assistantEngine,
|
|
1128
|
-
setAssistantEngineState: (value) => {
|
|
1129
|
-
assistantEngine = value;
|
|
1130
|
-
},
|
|
1131
|
-
setSelectedAssistantIndex: (value) => {
|
|
1132
|
-
selectedAssistantIndex = value;
|
|
1133
|
-
},
|
|
1134
|
-
assistantOptions,
|
|
1135
1157
|
providerOptions,
|
|
1136
1158
|
modeOptions: MODE_OPTIONS,
|
|
1137
1159
|
getAutoResume: () => autoResume,
|
|
@@ -1154,6 +1176,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1154
1176
|
function renderDashboard() {
|
|
1155
1177
|
const computed = computeDashboardContent({
|
|
1156
1178
|
globalMode,
|
|
1179
|
+
globalScope,
|
|
1157
1180
|
focusMode,
|
|
1158
1181
|
dashboardView,
|
|
1159
1182
|
activeAgents,
|
|
@@ -1175,20 +1198,19 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1175
1198
|
: "";
|
|
1176
1199
|
}
|
|
1177
1200
|
if (metaState) return metaState;
|
|
1178
|
-
|
|
1179
|
-
|
|
1201
|
+
return getTransientAgentState(transientAgentStateMap, agentId, {
|
|
1202
|
+
ttlMs: DEFAULT_TRANSIENT_AGENT_STATE_TTL_MS,
|
|
1203
|
+
});
|
|
1180
1204
|
},
|
|
1181
1205
|
launchMode,
|
|
1182
1206
|
agentProvider,
|
|
1183
|
-
assistantEngine,
|
|
1184
1207
|
autoResume,
|
|
1185
1208
|
selectedModeIndex,
|
|
1186
1209
|
selectedProviderIndex,
|
|
1187
|
-
selectedAssistantIndex,
|
|
1188
1210
|
selectedResumeIndex,
|
|
1211
|
+
selectedCronIndex,
|
|
1189
1212
|
cronTasks,
|
|
1190
1213
|
providerOptions,
|
|
1191
|
-
assistantOptions,
|
|
1192
1214
|
resumeOptions,
|
|
1193
1215
|
pendingReports: reportPendingTotal,
|
|
1194
1216
|
dashHints: DASH_HINTS,
|
|
@@ -1228,20 +1250,19 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1228
1250
|
|
|
1229
1251
|
function updateDashboard(status) {
|
|
1230
1252
|
activeAgents = status.active || [];
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
if (!activeSet.has(id)) {
|
|
1235
|
-
transientAgentStateMap.delete(id);
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1253
|
+
pruneTransientAgentStates(transientAgentStateMap, activeAgents, {
|
|
1254
|
+
ttlMs: DEFAULT_TRANSIENT_AGENT_STATE_TTL_MS,
|
|
1255
|
+
});
|
|
1239
1256
|
if (globalMode) {
|
|
1240
1257
|
refreshProjectRuntimes();
|
|
1241
1258
|
}
|
|
1242
|
-
|
|
1259
|
+
const publicPending = Number.isFinite(status?.reports?.pending_total)
|
|
1243
1260
|
? status.reports.pending_total
|
|
1244
1261
|
: 0;
|
|
1262
|
+
const controllerPending = Number.isFinite(status?.controller?.pending_total)
|
|
1263
|
+
? status.controller.pending_total
|
|
1264
|
+
: 0;
|
|
1265
|
+
reportPendingTotal = publicPending + controllerPending;
|
|
1245
1266
|
cronTasks = Array.isArray(status?.cron?.tasks) ? status.cron.tasks : [];
|
|
1246
1267
|
const metaList = Array.isArray(status.active_meta) ? status.active_meta : [];
|
|
1247
1268
|
let fallbackMap = null;
|
|
@@ -1317,6 +1338,12 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1317
1338
|
selectedAgentIndex = 0;
|
|
1318
1339
|
}
|
|
1319
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
|
+
}
|
|
1320
1347
|
}
|
|
1321
1348
|
}
|
|
1322
1349
|
syncTargetFromSelection();
|
|
@@ -1329,7 +1356,14 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1329
1356
|
dashboardView = globalMode ? "projects" : "agents";
|
|
1330
1357
|
if (globalMode) {
|
|
1331
1358
|
refreshProjectRuntimes();
|
|
1332
|
-
|
|
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
|
+
}
|
|
1333
1367
|
} else {
|
|
1334
1368
|
selectedAgentIndex = activeAgents.length > 0 ? 0 : -1;
|
|
1335
1369
|
agentListWindowStart = 0;
|
|
@@ -1337,11 +1371,8 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1337
1371
|
}
|
|
1338
1372
|
selectedModeIndex = Math.max(0, MODE_OPTIONS.indexOf(launchMode));
|
|
1339
1373
|
selectedProviderIndex = Math.max(0, providerOptions.findIndex((opt) => opt.value === agentProvider));
|
|
1340
|
-
selectedAssistantIndex = Math.max(
|
|
1341
|
-
0,
|
|
1342
|
-
assistantOptions.findIndex((opt) => opt.value === assistantEngine)
|
|
1343
|
-
);
|
|
1344
1374
|
selectedResumeIndex = autoResume ? 0 : 1;
|
|
1375
|
+
selectedCronIndex = cronTasks.length > 0 ? 0 : -1;
|
|
1345
1376
|
// Immediately set @target when first agent is selected.
|
|
1346
1377
|
if (!globalMode && selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
|
|
1347
1378
|
targetAgent = activeAgents[selectedAgentIndex];
|
|
@@ -1368,15 +1399,13 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1368
1399
|
activeAgentMetaMap: { get: () => activeAgentMetaMap },
|
|
1369
1400
|
selectedModeIndex: { get: () => selectedModeIndex, set: (value) => { selectedModeIndex = value; } },
|
|
1370
1401
|
selectedProviderIndex: { get: () => selectedProviderIndex, set: (value) => { selectedProviderIndex = value; } },
|
|
1371
|
-
selectedAssistantIndex: { get: () => selectedAssistantIndex, set: (value) => { selectedAssistantIndex = value; } },
|
|
1372
1402
|
selectedResumeIndex: { get: () => selectedResumeIndex, set: (value) => { selectedResumeIndex = value; } },
|
|
1403
|
+
selectedCronIndex: { get: () => selectedCronIndex, set: (value) => { selectedCronIndex = value; } },
|
|
1373
1404
|
launchMode: { get: () => launchMode },
|
|
1374
1405
|
agentProvider: { get: () => agentProvider },
|
|
1375
|
-
assistantEngine: { get: () => assistantEngine },
|
|
1376
1406
|
autoResume: { get: () => autoResume },
|
|
1377
1407
|
cronTasks: { get: () => cronTasks },
|
|
1378
1408
|
providerOptions: { get: () => providerOptions },
|
|
1379
|
-
assistantOptions: { get: () => assistantOptions },
|
|
1380
1409
|
resumeOptions: { get: () => resumeOptions },
|
|
1381
1410
|
agentOutputSuppressed: {
|
|
1382
1411
|
get: () => getAgentOutputSuppressed(),
|
|
@@ -1415,12 +1444,24 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1415
1444
|
exitDashboardMode,
|
|
1416
1445
|
setLaunchMode,
|
|
1417
1446
|
setAgentProvider,
|
|
1418
|
-
setAssistantEngine,
|
|
1419
1447
|
setAutoResume,
|
|
1420
1448
|
clampAgentWindow,
|
|
1421
1449
|
clampAgentWindowWithSelection,
|
|
1422
1450
|
requestProjectSwitch: requestProjectSwitchByIndex,
|
|
1423
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,
|
|
1424
1465
|
renderDashboard,
|
|
1425
1466
|
renderAgentDashboard,
|
|
1426
1467
|
renderScreen: () => screen.render(),
|
|
@@ -1557,7 +1598,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1557
1598
|
hasStream: (publisher) => streamTracker.hasStream(publisher),
|
|
1558
1599
|
setTransientAgentState: (agentId, state) => {
|
|
1559
1600
|
if (!agentId || !state) return;
|
|
1560
|
-
transientAgentStateMap
|
|
1601
|
+
setTransientAgentStateValue(transientAgentStateMap, agentId, state);
|
|
1561
1602
|
},
|
|
1562
1603
|
clearTransientAgentState: (agentId) => {
|
|
1563
1604
|
if (!agentId) return;
|
|
@@ -1669,6 +1710,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1669
1710
|
if (globalMode) {
|
|
1670
1711
|
refreshProjectRuntimes();
|
|
1671
1712
|
syncSelectedProjectToActive();
|
|
1713
|
+
updatePromptBox();
|
|
1672
1714
|
renderDashboard();
|
|
1673
1715
|
screen.render();
|
|
1674
1716
|
}
|
|
@@ -1684,6 +1726,44 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1684
1726
|
}
|
|
1685
1727
|
}
|
|
1686
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
|
+
|
|
1687
1767
|
let projectSwitching = false;
|
|
1688
1768
|
let pendingProjectSwitchRoot = null;
|
|
1689
1769
|
let projectSwitchDebounceTimer = null;
|
|
@@ -1854,9 +1934,12 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1854
1934
|
listProjects: () => listProjectRuntimes({ validate: true, cleanupTmp: true }),
|
|
1855
1935
|
getCurrentProject: () => ({
|
|
1856
1936
|
project_root: activeProjectRoot,
|
|
1857
|
-
project_name:
|
|
1937
|
+
project_name: globalMode && isGlobalControllerProjectRoot(activeProjectRoot)
|
|
1938
|
+
? "global-controller"
|
|
1939
|
+
: path.basename(activeProjectRoot),
|
|
1858
1940
|
}),
|
|
1859
1941
|
switchProject: async ({ target } = {}) => requestProjectSwitchByTarget(target),
|
|
1942
|
+
globalMode,
|
|
1860
1943
|
});
|
|
1861
1944
|
|
|
1862
1945
|
async function executeCommand(text) {
|
|
@@ -1972,13 +2055,6 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1972
2055
|
focusInput();
|
|
1973
2056
|
});
|
|
1974
2057
|
|
|
1975
|
-
// Escape in input mode only clears @target, never exits
|
|
1976
|
-
input.key(["escape"], () => {
|
|
1977
|
-
if (targetAgent) {
|
|
1978
|
-
clearTargetAgent();
|
|
1979
|
-
}
|
|
1980
|
-
});
|
|
1981
|
-
|
|
1982
2058
|
focusInput();
|
|
1983
2059
|
if (screen.program && typeof screen.program.decset === "function") {
|
|
1984
2060
|
screen.program.decset(2004);
|
|
@@ -1996,6 +2072,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1996
2072
|
if (globalMode) {
|
|
1997
2073
|
refreshProjectRuntimes();
|
|
1998
2074
|
}
|
|
2075
|
+
updatePromptBox();
|
|
1999
2076
|
renderDashboard();
|
|
2000
2077
|
resizeInput();
|
|
2001
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 {
|
|
@@ -6,7 +6,6 @@ function createSettingsController(options = {}) {
|
|
|
6
6
|
saveConfig = () => {},
|
|
7
7
|
normalizeLaunchMode = (value) => value,
|
|
8
8
|
normalizeAgentProvider = (value) => value,
|
|
9
|
-
normalizeAssistantEngine = (value) => value,
|
|
10
9
|
fsModule,
|
|
11
10
|
getUfooPaths = () => ({ agentDir: "" }),
|
|
12
11
|
logMessage = () => {},
|
|
@@ -19,10 +18,6 @@ function createSettingsController(options = {}) {
|
|
|
19
18
|
getAgentProvider = () => "codex-cli",
|
|
20
19
|
setAgentProviderState = () => {},
|
|
21
20
|
setSelectedProviderIndex = () => {},
|
|
22
|
-
getAssistantEngine = () => "auto",
|
|
23
|
-
setAssistantEngineState = () => {},
|
|
24
|
-
setSelectedAssistantIndex = () => {},
|
|
25
|
-
assistantOptions = [],
|
|
26
21
|
providerOptions = [],
|
|
27
22
|
modeOptions = [],
|
|
28
23
|
getAutoResume = () => true,
|
|
@@ -41,14 +36,6 @@ function createSettingsController(options = {}) {
|
|
|
41
36
|
return value === "claude-cli" ? "claude" : "codex";
|
|
42
37
|
}
|
|
43
38
|
|
|
44
|
-
function assistantLabel(value) {
|
|
45
|
-
const normalized = normalizeAssistantEngine(value);
|
|
46
|
-
if (normalized === "codex") return "codex";
|
|
47
|
-
if (normalized === "claude") return "claude";
|
|
48
|
-
if (normalized === "ufoo") return "ufoo";
|
|
49
|
-
return "auto";
|
|
50
|
-
}
|
|
51
|
-
|
|
52
39
|
function clearUfooAgentIdentity() {
|
|
53
40
|
const agentDir = getUfooPaths(projectRoot).agentDir;
|
|
54
41
|
const stateFile = path.join(agentDir, "ufoo-agent.json");
|
|
@@ -106,26 +93,11 @@ function createSettingsController(options = {}) {
|
|
|
106
93
|
return true;
|
|
107
94
|
}
|
|
108
95
|
|
|
109
|
-
function setAssistantEngine(engine) {
|
|
110
|
-
const next = normalizeAssistantEngine(engine);
|
|
111
|
-
if (next === getAssistantEngine()) return false;
|
|
112
|
-
setAssistantEngineState(next);
|
|
113
|
-
const idx = assistantOptions.findIndex((opt) => opt && opt.value === next);
|
|
114
|
-
setSelectedAssistantIndex(idx >= 0 ? idx : 0);
|
|
115
|
-
saveConfig(projectRoot, { assistantEngine: next });
|
|
116
|
-
logMessage("status", `{white-fg}⚙{/white-fg} assistant-engine: ${assistantLabel(next)}`);
|
|
117
|
-
renderDashboard();
|
|
118
|
-
renderScreen();
|
|
119
|
-
return true;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
96
|
return {
|
|
123
97
|
providerLabel,
|
|
124
|
-
assistantLabel,
|
|
125
98
|
clearUfooAgentIdentity,
|
|
126
99
|
setLaunchMode,
|
|
127
100
|
setAgentProvider,
|
|
128
|
-
setAssistantEngine,
|
|
129
101
|
setAutoResume,
|
|
130
102
|
};
|
|
131
103
|
}
|
|
@@ -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
|
+
};
|