u-foo 1.4.1 → 1.5.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/bin/ufoo.js +15 -7
- package/package.json +3 -2
- package/scripts/global-chat-switch-benchmark.js +406 -0
- package/src/chat/chatLogController.js +28 -5
- package/src/chat/commandExecutor.js +127 -3
- package/src/chat/commands.js +8 -0
- package/src/chat/daemonConnection.js +36 -1
- package/src/chat/daemonCoordinator.js +36 -0
- package/src/chat/daemonTransport.js +36 -5
- package/src/chat/dashboardKeyController.js +80 -1
- package/src/chat/dashboardView.js +289 -93
- package/src/chat/index.js +537 -37
- package/src/chat/inputHistoryController.js +33 -3
- package/src/chat/inputListenerController.js +22 -12
- package/src/chat/layout.js +12 -7
- package/src/chat/streamTracker.js +6 -0
- package/src/cli.js +167 -4
- package/src/daemon/index.js +42 -2
- package/src/daemon/ops.js +199 -23
- package/src/projects/projectId.js +29 -0
- package/src/projects/registry.js +279 -0
package/src/chat/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const path = require("path");
|
|
2
|
+
const os = require("os");
|
|
3
|
+
const crypto = require("crypto");
|
|
2
4
|
const blessed = require("blessed");
|
|
3
5
|
const { execSync } = require("child_process");
|
|
4
6
|
const fs = require("fs");
|
|
@@ -41,8 +43,19 @@ const { createDaemonCoordinator } = require("./daemonCoordinator");
|
|
|
41
43
|
const { IPC_REQUEST_TYPES } = require("../shared/eventContract");
|
|
42
44
|
const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
|
|
43
45
|
const { createDaemonTransport } = require("./daemonTransport");
|
|
46
|
+
const { listProjectRuntimes } = require("../projects/registry");
|
|
47
|
+
const { canonicalProjectRoot, buildProjectId } = require("../projects/projectId");
|
|
48
|
+
|
|
49
|
+
async function runChat(projectRoot, options = {}) {
|
|
50
|
+
const globalMode = options && options.globalMode === true;
|
|
51
|
+
const DASHBOARD_HEIGHT = globalMode ? 2 : 1;
|
|
52
|
+
let activeProjectRoot = projectRoot;
|
|
53
|
+
try {
|
|
54
|
+
activeProjectRoot = canonicalProjectRoot(projectRoot);
|
|
55
|
+
} catch {
|
|
56
|
+
activeProjectRoot = path.resolve(projectRoot || process.cwd());
|
|
57
|
+
}
|
|
44
58
|
|
|
45
|
-
async function runChat(projectRoot) {
|
|
46
59
|
if (!fs.existsSync(getUfooPaths(projectRoot).ufooDir)) {
|
|
47
60
|
const repoRoot = path.join(__dirname, "..", "..");
|
|
48
61
|
const init = new UfooInit(repoRoot);
|
|
@@ -51,7 +64,6 @@ async function runChat(projectRoot) {
|
|
|
51
64
|
|
|
52
65
|
// Ensure subscriber ID exists for chat (persistent across restarts)
|
|
53
66
|
if (!process.env.UFOO_SUBSCRIBER_ID) {
|
|
54
|
-
const crypto = require("crypto");
|
|
55
67
|
const sessionFile = path.join(getUfooPaths(projectRoot).ufooDir, "chat", "session-id.txt");
|
|
56
68
|
const sessionDir = path.dirname(sessionFile);
|
|
57
69
|
fs.mkdirSync(sessionDir, { recursive: true });
|
|
@@ -88,10 +100,12 @@ async function runChat(projectRoot) {
|
|
|
88
100
|
let autoResume = config.autoResume !== false;
|
|
89
101
|
let cronTasks = [];
|
|
90
102
|
|
|
91
|
-
// Dynamic input height settings
|
|
92
|
-
// Layout:
|
|
93
|
-
const
|
|
94
|
-
const
|
|
103
|
+
// Dynamic input height settings.
|
|
104
|
+
// Layout: dashboard(N) + inputBottom(1) + content + inputTop(1) + status(1)
|
|
105
|
+
const MIN_INPUT_CONTENT_HEIGHT = 1;
|
|
106
|
+
const MAX_INPUT_CONTENT_HEIGHT = 6;
|
|
107
|
+
const MIN_INPUT_HEIGHT = MIN_INPUT_CONTENT_HEIGHT + DASHBOARD_HEIGHT + 2;
|
|
108
|
+
const MAX_INPUT_HEIGHT = MAX_INPUT_CONTENT_HEIGHT + DASHBOARD_HEIGHT + 2;
|
|
95
109
|
let currentInputHeight = MIN_INPUT_HEIGHT;
|
|
96
110
|
const pkg = require("../../package.json");
|
|
97
111
|
const {
|
|
@@ -108,18 +122,136 @@ async function runChat(projectRoot) {
|
|
|
108
122
|
} = createChatLayout({
|
|
109
123
|
blessed,
|
|
110
124
|
currentInputHeight,
|
|
125
|
+
dashboardHeight: DASHBOARD_HEIGHT,
|
|
111
126
|
version: pkg.version,
|
|
112
127
|
});
|
|
113
128
|
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
const
|
|
129
|
+
const globalChatRoot = path.join(os.homedir(), ".ufoo", "chat");
|
|
130
|
+
const globalDraftsFile = path.join(globalChatRoot, "global-drafts.json");
|
|
131
|
+
const GLOBAL_DRAFT_PERSIST_DEBOUNCE_MS = 150;
|
|
132
|
+
let globalDraftsLoaded = false;
|
|
133
|
+
let globalDraftPersistTimer = null;
|
|
134
|
+
const globalDraftMap = new Map();
|
|
135
|
+
|
|
136
|
+
function safeCanonicalProjectRoot(targetRoot) {
|
|
137
|
+
try {
|
|
138
|
+
return canonicalProjectRoot(targetRoot);
|
|
139
|
+
} catch {
|
|
140
|
+
return path.resolve(targetRoot || process.cwd());
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function resolveHistoryContext(targetProjectRoot) {
|
|
145
|
+
const canonicalRoot = safeCanonicalProjectRoot(targetProjectRoot);
|
|
146
|
+
if (!globalMode) {
|
|
147
|
+
const localHistoryDir = path.join(getUfooPaths(canonicalRoot).ufooDir, "chat");
|
|
148
|
+
return {
|
|
149
|
+
projectRoot: canonicalRoot,
|
|
150
|
+
historyDir: localHistoryDir,
|
|
151
|
+
historyFile: path.join(localHistoryDir, "history.jsonl"),
|
|
152
|
+
inputHistoryDir: localHistoryDir,
|
|
153
|
+
inputHistoryFile: path.join(localHistoryDir, "input-history.jsonl"),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
let projectId = "";
|
|
157
|
+
try {
|
|
158
|
+
projectId = buildProjectId(canonicalRoot);
|
|
159
|
+
} catch {
|
|
160
|
+
projectId = crypto.createHash("sha256").update(canonicalRoot).digest("hex").slice(0, 16);
|
|
161
|
+
}
|
|
162
|
+
const globalHistoryDir = path.join(globalChatRoot, "global-history");
|
|
163
|
+
const globalInputHistoryDir = path.join(globalChatRoot, "global-input-history");
|
|
164
|
+
return {
|
|
165
|
+
projectRoot: canonicalRoot,
|
|
166
|
+
projectId,
|
|
167
|
+
historyDir: globalHistoryDir,
|
|
168
|
+
historyFile: path.join(globalHistoryDir, `${projectId}.jsonl`),
|
|
169
|
+
inputHistoryDir: globalInputHistoryDir,
|
|
170
|
+
inputHistoryFile: path.join(globalInputHistoryDir, `${projectId}.jsonl`),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function loadGlobalDraftsOnce() {
|
|
175
|
+
if (!globalMode || globalDraftsLoaded) return;
|
|
176
|
+
globalDraftsLoaded = true;
|
|
177
|
+
try {
|
|
178
|
+
const raw = fs.readFileSync(globalDraftsFile, "utf8");
|
|
179
|
+
const parsed = JSON.parse(String(raw || "{}"));
|
|
180
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
|
|
181
|
+
Object.entries(parsed).forEach(([projectRootKey, draft]) => {
|
|
182
|
+
if (typeof draft !== "string") return;
|
|
183
|
+
const canonicalKey = safeCanonicalProjectRoot(projectRootKey);
|
|
184
|
+
if (!canonicalKey) return;
|
|
185
|
+
globalDraftMap.set(canonicalKey, draft);
|
|
186
|
+
});
|
|
187
|
+
} catch {
|
|
188
|
+
// Ignore missing/invalid drafts file.
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function writeGlobalDraftsToDisk() {
|
|
193
|
+
if (!globalMode) return;
|
|
194
|
+
const out = {};
|
|
195
|
+
for (const [projectRootKey, draft] of globalDraftMap.entries()) {
|
|
196
|
+
if (!projectRootKey) continue;
|
|
197
|
+
if (typeof draft !== "string" || draft.length === 0) continue;
|
|
198
|
+
out[projectRootKey] = draft;
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
fs.mkdirSync(path.dirname(globalDraftsFile), { recursive: true });
|
|
202
|
+
fs.writeFileSync(globalDraftsFile, `${JSON.stringify(out, null, 2)}\n`, "utf8");
|
|
203
|
+
} catch {
|
|
204
|
+
// Ignore draft persistence failures.
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function persistGlobalDrafts(options = {}) {
|
|
209
|
+
if (!globalMode) return;
|
|
210
|
+
const immediate = Boolean(options.immediate);
|
|
211
|
+
if (immediate) {
|
|
212
|
+
if (globalDraftPersistTimer) {
|
|
213
|
+
clearTimeout(globalDraftPersistTimer);
|
|
214
|
+
globalDraftPersistTimer = null;
|
|
215
|
+
}
|
|
216
|
+
writeGlobalDraftsToDisk();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (globalDraftPersistTimer) {
|
|
220
|
+
clearTimeout(globalDraftPersistTimer);
|
|
221
|
+
}
|
|
222
|
+
globalDraftPersistTimer = setTimeout(() => {
|
|
223
|
+
globalDraftPersistTimer = null;
|
|
224
|
+
writeGlobalDraftsToDisk();
|
|
225
|
+
}, GLOBAL_DRAFT_PERSIST_DEBOUNCE_MS);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function getProjectDraft(targetProjectRoot) {
|
|
229
|
+
if (!globalMode) return "";
|
|
230
|
+
loadGlobalDraftsOnce();
|
|
231
|
+
const canonicalRoot = safeCanonicalProjectRoot(targetProjectRoot);
|
|
232
|
+
return globalDraftMap.get(canonicalRoot) || "";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function setProjectDraft(targetProjectRoot, draft, options = {}) {
|
|
236
|
+
if (!globalMode) return;
|
|
237
|
+
loadGlobalDraftsOnce();
|
|
238
|
+
const canonicalRoot = safeCanonicalProjectRoot(targetProjectRoot);
|
|
239
|
+
const text = String(draft || "");
|
|
240
|
+
if (!text) {
|
|
241
|
+
globalDraftMap.delete(canonicalRoot);
|
|
242
|
+
} else {
|
|
243
|
+
globalDraftMap.set(canonicalRoot, text);
|
|
244
|
+
}
|
|
245
|
+
persistGlobalDrafts(options);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let currentHistoryContext = resolveHistoryContext(activeProjectRoot);
|
|
117
249
|
|
|
118
|
-
|
|
250
|
+
let chatLogController = createChatLogController({
|
|
119
251
|
logBox,
|
|
120
252
|
fsModule: fs,
|
|
121
|
-
historyDir,
|
|
122
|
-
historyFile,
|
|
253
|
+
historyDir: currentHistoryContext.historyDir,
|
|
254
|
+
historyFile: currentHistoryContext.historyFile,
|
|
123
255
|
});
|
|
124
256
|
|
|
125
257
|
const streamTracker = createStreamTracker({
|
|
@@ -293,12 +425,67 @@ async function runChat(projectRoot) {
|
|
|
293
425
|
}
|
|
294
426
|
|
|
295
427
|
inputHistoryController = createInputHistoryController({
|
|
296
|
-
inputHistoryFile,
|
|
297
|
-
historyDir,
|
|
428
|
+
inputHistoryFile: currentHistoryContext.inputHistoryFile,
|
|
429
|
+
historyDir: currentHistoryContext.inputHistoryDir,
|
|
298
430
|
setInputValue,
|
|
299
431
|
getInputValue: () => input.value || "",
|
|
300
432
|
});
|
|
301
433
|
|
|
434
|
+
function captureCurrentProjectDraft() {
|
|
435
|
+
if (!inputHistoryController || typeof inputHistoryController.getDraftForPersistence !== "function") {
|
|
436
|
+
return input.value || "";
|
|
437
|
+
}
|
|
438
|
+
return inputHistoryController.getDraftForPersistence();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function seedGlobalHistoryFromProject(nextContext) {
|
|
442
|
+
if (!globalMode || !nextContext || !nextContext.projectRoot) return;
|
|
443
|
+
const projectUfooDir = getUfooPaths(nextContext.projectRoot).ufooDir;
|
|
444
|
+
const projectChatDir = path.join(projectUfooDir, "chat");
|
|
445
|
+
const projectHistoryFile = path.join(projectChatDir, "history.jsonl");
|
|
446
|
+
const projectInputHistoryFile = path.join(projectChatDir, "input-history.jsonl");
|
|
447
|
+
try {
|
|
448
|
+
if (!fs.existsSync(nextContext.historyFile) && fs.existsSync(projectHistoryFile)) {
|
|
449
|
+
fs.mkdirSync(path.dirname(nextContext.historyFile), { recursive: true });
|
|
450
|
+
fs.copyFileSync(projectHistoryFile, nextContext.historyFile);
|
|
451
|
+
}
|
|
452
|
+
} catch {
|
|
453
|
+
// best-effort seed only
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
if (!fs.existsSync(nextContext.inputHistoryFile) && fs.existsSync(projectInputHistoryFile)) {
|
|
457
|
+
fs.mkdirSync(path.dirname(nextContext.inputHistoryFile), { recursive: true });
|
|
458
|
+
fs.copyFileSync(projectInputHistoryFile, nextContext.inputHistoryFile);
|
|
459
|
+
}
|
|
460
|
+
} catch {
|
|
461
|
+
// best-effort seed only
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function applyProjectHistoryContext(nextProjectRoot) {
|
|
466
|
+
streamTracker.discardAll();
|
|
467
|
+
const nextContext = resolveHistoryContext(nextProjectRoot);
|
|
468
|
+
seedGlobalHistoryFromProject(nextContext);
|
|
469
|
+
currentHistoryContext = nextContext;
|
|
470
|
+
chatLogController.setHistoryTarget({
|
|
471
|
+
historyDir: nextContext.historyDir,
|
|
472
|
+
historyFile: nextContext.historyFile,
|
|
473
|
+
});
|
|
474
|
+
chatLogController.resetViewState();
|
|
475
|
+
|
|
476
|
+
inputHistoryController.setHistoryTarget({
|
|
477
|
+
inputHistoryFile: nextContext.inputHistoryFile,
|
|
478
|
+
historyDir: nextContext.inputHistoryDir,
|
|
479
|
+
});
|
|
480
|
+
inputHistoryController.loadInputHistory();
|
|
481
|
+
const nextDraft = getProjectDraft(nextContext.projectRoot);
|
|
482
|
+
inputHistoryController.restoreDraft(nextDraft);
|
|
483
|
+
|
|
484
|
+
clearLog();
|
|
485
|
+
loadHistory();
|
|
486
|
+
pending = null;
|
|
487
|
+
}
|
|
488
|
+
|
|
302
489
|
function historyUp() {
|
|
303
490
|
if (!inputHistoryController) return false;
|
|
304
491
|
return inputHistoryController.historyUp();
|
|
@@ -310,6 +497,9 @@ async function runChat(projectRoot) {
|
|
|
310
497
|
}
|
|
311
498
|
|
|
312
499
|
function exitHandler() {
|
|
500
|
+
if (globalMode) {
|
|
501
|
+
setProjectDraft(activeProjectRoot, captureCurrentProjectDraft(), { immediate: true });
|
|
502
|
+
}
|
|
313
503
|
if (daemonCoordinator) {
|
|
314
504
|
daemonCoordinator.markExit();
|
|
315
505
|
}
|
|
@@ -396,8 +586,8 @@ async function runChat(projectRoot) {
|
|
|
396
586
|
if (innerWidth <= 0) return;
|
|
397
587
|
|
|
398
588
|
const numLines = countLines(input.value, innerWidth);
|
|
399
|
-
const contentHeight = Math.min(
|
|
400
|
-
const targetHeight = contentHeight +
|
|
589
|
+
const contentHeight = Math.min(MAX_INPUT_CONTENT_HEIGHT, Math.max(MIN_INPUT_CONTENT_HEIGHT, numLines));
|
|
590
|
+
const targetHeight = contentHeight + DASHBOARD_HEIGHT + 2;
|
|
401
591
|
|
|
402
592
|
if (targetHeight !== currentInputHeight) {
|
|
403
593
|
currentInputHeight = targetHeight;
|
|
@@ -408,7 +598,7 @@ async function runChat(projectRoot) {
|
|
|
408
598
|
statusLine.bottom = currentInputHeight;
|
|
409
599
|
// Reposition completion panel if active
|
|
410
600
|
if (completionController.isActive()) completionController.reflow();
|
|
411
|
-
// dashboard and inputBottomLine stay fixed at bottom
|
|
601
|
+
// dashboard and inputBottomLine stay fixed at the bottom region.
|
|
412
602
|
logBox.height = Math.max(1, screen.height - currentInputHeight - 1);
|
|
413
603
|
ensureInputCursorVisible();
|
|
414
604
|
}
|
|
@@ -450,7 +640,7 @@ async function runChat(projectRoot) {
|
|
|
450
640
|
currentInputHeight = MIN_INPUT_HEIGHT;
|
|
451
641
|
if (inputHistoryController) inputHistoryController.setIndexToEnd();
|
|
452
642
|
completionController.hide();
|
|
453
|
-
const contentHeight =
|
|
643
|
+
const contentHeight = MIN_INPUT_CONTENT_HEIGHT;
|
|
454
644
|
input.height = contentHeight;
|
|
455
645
|
promptBox.height = contentHeight;
|
|
456
646
|
inputTopLine.bottom = currentInputHeight - 1;
|
|
@@ -467,10 +657,14 @@ async function runChat(projectRoot) {
|
|
|
467
657
|
let activeAgentMetaMap = new Map(); // Store full meta including launch_mode
|
|
468
658
|
let agentListWindowStart = 0;
|
|
469
659
|
const MAX_AGENT_WINDOW = 4;
|
|
660
|
+
let projectRuntimes = [];
|
|
661
|
+
let projectListWindowStart = 0;
|
|
662
|
+
const MAX_PROJECT_WINDOW = 5;
|
|
663
|
+
let selectedProjectIndex = -1;
|
|
470
664
|
let selectedAgentIndex = -1; // -1 = not in dashboard selection mode
|
|
471
665
|
let targetAgent = null; // Selected agent for direct messaging
|
|
472
666
|
let focusMode = "input"; // "input" or "dashboard"
|
|
473
|
-
let dashboardView = "agents"; // "agents" | "mode" | "provider" | "assistant" | "cron"
|
|
667
|
+
let dashboardView = "agents"; // "projects" | "agents" | "mode" | "provider" | "assistant" | "cron"
|
|
474
668
|
let reportPendingTotal = 0;
|
|
475
669
|
let selectedModeIndex = launchMode === "internal" ? 2 : (launchMode === "tmux" ? 1 : 0);
|
|
476
670
|
const providerOptions = [
|
|
@@ -496,12 +690,16 @@ async function runChat(projectRoot) {
|
|
|
496
690
|
let selectedResumeIndex = autoResume ? 0 : 1;
|
|
497
691
|
const DASH_HINTS = {
|
|
498
692
|
agents: "←/→ select · Enter · ↓ mode · ↑ back",
|
|
693
|
+
agentsGlobal: "←/→ select · Enter · ↓ mode · ↑ projects",
|
|
499
694
|
agentsEmpty: "↓ mode · ↑ back",
|
|
500
695
|
mode: "←/→ select · Enter · ↓ provider · ↑ back",
|
|
501
696
|
provider: "←/→ select · Enter · ↓ assistant · ↑ back",
|
|
502
697
|
assistant: "←/→ select · Enter · ↓ cron · ↑ back",
|
|
503
698
|
cron: "Ctrl+X close · ↑ back",
|
|
504
699
|
resume: "",
|
|
700
|
+
projects: "Use /project switch <index|path>",
|
|
701
|
+
projectsFocus: "←/→ switch · ↓ second row · Enter confirm · ↑ back",
|
|
702
|
+
projectsEmpty: "Run ufoo chat or ufoo daemon start in project directories",
|
|
505
703
|
};
|
|
506
704
|
const AGENT_BAR_HINTS = {
|
|
507
705
|
normal: "↓ agents",
|
|
@@ -668,7 +866,7 @@ async function runChat(projectRoot) {
|
|
|
668
866
|
labelMap: activeAgentLabelMap,
|
|
669
867
|
lookupNickname: (nickname) => {
|
|
670
868
|
try {
|
|
671
|
-
const busPath = getUfooPaths(
|
|
869
|
+
const busPath = getUfooPaths(activeProjectRoot).agentsFile;
|
|
672
870
|
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
673
871
|
for (const [id, meta] of Object.entries(bus.agents || {})) {
|
|
674
872
|
if (meta && meta.nickname === nickname) return id;
|
|
@@ -687,7 +885,7 @@ async function runChat(projectRoot) {
|
|
|
687
885
|
labelMap: activeAgentLabelMap,
|
|
688
886
|
lookupNicknameById: (id) => {
|
|
689
887
|
try {
|
|
690
|
-
const busPath = getUfooPaths(
|
|
888
|
+
const busPath = getUfooPaths(activeProjectRoot).agentsFile;
|
|
691
889
|
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
692
890
|
const meta = bus.agents && bus.agents[id];
|
|
693
891
|
if (meta && meta.nickname) return meta.nickname;
|
|
@@ -712,6 +910,60 @@ async function runChat(projectRoot) {
|
|
|
712
910
|
clampAgentWindowWithSelection(selectedAgentIndex);
|
|
713
911
|
}
|
|
714
912
|
|
|
913
|
+
function resolveRuntimeProjectRoot(row = {}) {
|
|
914
|
+
const raw = row && row.project_root ? String(row.project_root) : "";
|
|
915
|
+
if (!raw) return "";
|
|
916
|
+
try {
|
|
917
|
+
return canonicalProjectRoot(raw);
|
|
918
|
+
} catch {
|
|
919
|
+
return path.resolve(raw);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function refreshProjectRuntimes() {
|
|
924
|
+
let rows = [];
|
|
925
|
+
try {
|
|
926
|
+
rows = listProjectRuntimes({ validate: true, cleanupTmp: true });
|
|
927
|
+
} catch {
|
|
928
|
+
rows = [];
|
|
929
|
+
}
|
|
930
|
+
const normalizedActive = String(activeProjectRoot || "");
|
|
931
|
+
if (
|
|
932
|
+
normalizedActive
|
|
933
|
+
&& !rows.some((row) => resolveRuntimeProjectRoot(row) === normalizedActive)
|
|
934
|
+
) {
|
|
935
|
+
rows.unshift({
|
|
936
|
+
project_root: normalizedActive,
|
|
937
|
+
project_name: path.basename(normalizedActive) || normalizedActive,
|
|
938
|
+
status: "untracked",
|
|
939
|
+
last_seen: null,
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
projectRuntimes = rows;
|
|
943
|
+
|
|
944
|
+
if (projectRuntimes.length === 0) {
|
|
945
|
+
selectedProjectIndex = -1;
|
|
946
|
+
projectListWindowStart = 0;
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
const activeIndex = projectRuntimes.findIndex(
|
|
950
|
+
(row) => resolveRuntimeProjectRoot(row) === normalizedActive
|
|
951
|
+
);
|
|
952
|
+
if (selectedProjectIndex < 0 || selectedProjectIndex >= projectRuntimes.length) {
|
|
953
|
+
selectedProjectIndex = activeIndex >= 0 ? activeIndex : 0;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function syncSelectedProjectToActive() {
|
|
958
|
+
if (!Array.isArray(projectRuntimes) || projectRuntimes.length === 0) return;
|
|
959
|
+
const activeIndex = projectRuntimes.findIndex(
|
|
960
|
+
(row) => resolveRuntimeProjectRoot(row) === String(activeProjectRoot || "")
|
|
961
|
+
);
|
|
962
|
+
if (activeIndex >= 0) {
|
|
963
|
+
selectedProjectIndex = activeIndex;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
715
967
|
function send(req) {
|
|
716
968
|
if (!daemonCoordinator) return;
|
|
717
969
|
daemonCoordinator.send(req);
|
|
@@ -870,9 +1122,15 @@ async function runChat(projectRoot) {
|
|
|
870
1122
|
|
|
871
1123
|
function renderDashboard() {
|
|
872
1124
|
const computed = computeDashboardContent({
|
|
1125
|
+
globalMode,
|
|
873
1126
|
focusMode,
|
|
874
1127
|
dashboardView,
|
|
875
1128
|
activeAgents,
|
|
1129
|
+
projects: projectRuntimes,
|
|
1130
|
+
selectedProjectIndex,
|
|
1131
|
+
projectListWindowStart,
|
|
1132
|
+
maxProjectWindow: MAX_PROJECT_WINDOW,
|
|
1133
|
+
activeProjectRoot,
|
|
876
1134
|
selectedAgentIndex,
|
|
877
1135
|
agentListWindowStart,
|
|
878
1136
|
maxAgentWindow: MAX_AGENT_WINDOW,
|
|
@@ -892,12 +1150,23 @@ async function runChat(projectRoot) {
|
|
|
892
1150
|
pendingReports: reportPendingTotal,
|
|
893
1151
|
dashHints: DASH_HINTS,
|
|
894
1152
|
});
|
|
895
|
-
|
|
896
|
-
|
|
1153
|
+
if (globalMode && (focusMode !== "dashboard" || dashboardView === "projects")) {
|
|
1154
|
+
projectListWindowStart = computed.windowStart;
|
|
1155
|
+
} else {
|
|
1156
|
+
agentListWindowStart = computed.windowStart;
|
|
1157
|
+
}
|
|
1158
|
+
let dashboardContent = computed.content;
|
|
1159
|
+
if (globalMode && !String(dashboardContent || "").includes("\n")) {
|
|
1160
|
+
dashboardContent = `${dashboardContent}\n `;
|
|
1161
|
+
}
|
|
1162
|
+
dashboard.setContent(dashboardContent);
|
|
897
1163
|
}
|
|
898
1164
|
|
|
899
1165
|
function updateDashboard(status) {
|
|
900
1166
|
activeAgents = status.active || [];
|
|
1167
|
+
if (globalMode) {
|
|
1168
|
+
refreshProjectRuntimes();
|
|
1169
|
+
}
|
|
901
1170
|
reportPendingTotal = Number.isFinite(status?.reports?.pending_total)
|
|
902
1171
|
? status.reports.pending_total
|
|
903
1172
|
: 0;
|
|
@@ -906,7 +1175,7 @@ async function runChat(projectRoot) {
|
|
|
906
1175
|
let fallbackMap = null;
|
|
907
1176
|
if (metaList.length === 0 && activeAgents.length > 0) {
|
|
908
1177
|
try {
|
|
909
|
-
const busPath = getUfooPaths(
|
|
1178
|
+
const busPath = getUfooPaths(activeProjectRoot).agentsFile;
|
|
910
1179
|
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
911
1180
|
fallbackMap = new Map();
|
|
912
1181
|
for (const [id, meta] of Object.entries(bus.agents || {})) {
|
|
@@ -957,10 +1226,15 @@ async function runChat(projectRoot) {
|
|
|
957
1226
|
|
|
958
1227
|
function enterDashboardMode() {
|
|
959
1228
|
focusMode = "dashboard";
|
|
960
|
-
dashboardView = "agents";
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1229
|
+
dashboardView = globalMode ? "projects" : "agents";
|
|
1230
|
+
if (globalMode) {
|
|
1231
|
+
refreshProjectRuntimes();
|
|
1232
|
+
syncSelectedProjectToActive();
|
|
1233
|
+
} else {
|
|
1234
|
+
selectedAgentIndex = activeAgents.length > 0 ? 0 : -1;
|
|
1235
|
+
agentListWindowStart = 0;
|
|
1236
|
+
clampAgentWindow();
|
|
1237
|
+
}
|
|
964
1238
|
selectedModeIndex = launchMode === "internal" ? 2 : (launchMode === "tmux" ? 1 : 0);
|
|
965
1239
|
selectedProviderIndex = Math.max(0, providerOptions.findIndex((opt) => opt.value === agentProvider));
|
|
966
1240
|
selectedAssistantIndex = Math.max(
|
|
@@ -968,8 +1242,8 @@ async function runChat(projectRoot) {
|
|
|
968
1242
|
assistantOptions.findIndex((opt) => opt.value === assistantEngine)
|
|
969
1243
|
);
|
|
970
1244
|
selectedResumeIndex = autoResume ? 0 : 1;
|
|
971
|
-
// Immediately set @target when first agent is selected
|
|
972
|
-
if (selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
|
|
1245
|
+
// Immediately set @target when first agent is selected.
|
|
1246
|
+
if (!globalMode && selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
|
|
973
1247
|
targetAgent = activeAgents[selectedAgentIndex];
|
|
974
1248
|
updatePromptBox();
|
|
975
1249
|
}
|
|
@@ -985,6 +1259,9 @@ async function runChat(projectRoot) {
|
|
|
985
1259
|
currentView: { get: () => getCurrentView() },
|
|
986
1260
|
focusMode: { get: () => focusMode, set: (value) => { focusMode = value; } },
|
|
987
1261
|
dashboardView: { get: () => dashboardView, set: (value) => { dashboardView = value; } },
|
|
1262
|
+
selectedProjectIndex: { get: () => selectedProjectIndex, set: (value) => { selectedProjectIndex = value; } },
|
|
1263
|
+
projects: { get: () => projectRuntimes },
|
|
1264
|
+
activeProjectRoot: { get: () => activeProjectRoot },
|
|
988
1265
|
selectedAgentIndex: { get: () => selectedAgentIndex, set: (value) => { selectedAgentIndex = value; } },
|
|
989
1266
|
activeAgents: { get: () => activeAgents },
|
|
990
1267
|
viewingAgent: { get: () => getViewingAgent() },
|
|
@@ -1009,7 +1286,7 @@ async function runChat(projectRoot) {
|
|
|
1009
1286
|
|
|
1010
1287
|
function activateAgent(agentId) {
|
|
1011
1288
|
if (!agentId) return;
|
|
1012
|
-
const activator = new AgentActivator(
|
|
1289
|
+
const activator = new AgentActivator(activeProjectRoot);
|
|
1013
1290
|
activator.activate(agentId).catch(() => {});
|
|
1014
1291
|
}
|
|
1015
1292
|
|
|
@@ -1022,6 +1299,7 @@ async function runChat(projectRoot) {
|
|
|
1022
1299
|
|
|
1023
1300
|
const dashboardController = createDashboardKeyController({
|
|
1024
1301
|
state: dashboardState,
|
|
1302
|
+
globalMode,
|
|
1025
1303
|
existsSync: fs.existsSync,
|
|
1026
1304
|
getInjectSockPath,
|
|
1027
1305
|
getAgentAdapter,
|
|
@@ -1041,6 +1319,7 @@ async function runChat(projectRoot) {
|
|
|
1041
1319
|
setAutoResume,
|
|
1042
1320
|
clampAgentWindow,
|
|
1043
1321
|
clampAgentWindowWithSelection,
|
|
1322
|
+
requestProjectSwitch: requestProjectSwitchByIndex,
|
|
1044
1323
|
renderDashboard,
|
|
1045
1324
|
renderAgentDashboard,
|
|
1046
1325
|
renderScreen: () => screen.render(),
|
|
@@ -1059,8 +1338,9 @@ async function runChat(projectRoot) {
|
|
|
1059
1338
|
updatePromptBox();
|
|
1060
1339
|
}
|
|
1061
1340
|
focusMode = "input";
|
|
1062
|
-
dashboardView = "agents";
|
|
1341
|
+
dashboardView = globalMode ? "projects" : "agents";
|
|
1063
1342
|
selectedAgentIndex = -1;
|
|
1343
|
+
// Keep selectedProjectIndex across focus transitions so global rail preserves context.
|
|
1064
1344
|
screen.grabKeys = false;
|
|
1065
1345
|
renderDashboard();
|
|
1066
1346
|
focusInput();
|
|
@@ -1075,7 +1355,7 @@ async function runChat(projectRoot) {
|
|
|
1075
1355
|
|
|
1076
1356
|
function getInjectSockPath(agentId) {
|
|
1077
1357
|
const safeName = subscriberToSafeName(agentId);
|
|
1078
|
-
return path.join(getUfooPaths(
|
|
1358
|
+
return path.join(getUfooPaths(activeProjectRoot).busQueuesDir, safeName, "inject.sock");
|
|
1079
1359
|
}
|
|
1080
1360
|
|
|
1081
1361
|
agentViewController = createAgentViewController({
|
|
@@ -1180,8 +1460,8 @@ async function runChat(projectRoot) {
|
|
|
1180
1460
|
const connected = await daemonCoordinator.connect();
|
|
1181
1461
|
if (!connected) {
|
|
1182
1462
|
// Check if daemon failed to start
|
|
1183
|
-
if (!isRunning(
|
|
1184
|
-
const logFile = getUfooPaths(
|
|
1463
|
+
if (!isRunning(activeProjectRoot)) {
|
|
1464
|
+
const logFile = getUfooPaths(activeProjectRoot).ufooDaemonLog;
|
|
1185
1465
|
// eslint-disable-next-line no-console
|
|
1186
1466
|
console.error("Failed to start ufoo daemon. Check logs at:", logFile);
|
|
1187
1467
|
throw new Error("Daemon failed to start. Check the daemon log for details.");
|
|
@@ -1189,6 +1469,214 @@ async function runChat(projectRoot) {
|
|
|
1189
1469
|
throw new Error("Failed to connect to ufoo daemon (timeout). The daemon may still be starting.");
|
|
1190
1470
|
}
|
|
1191
1471
|
|
|
1472
|
+
function resolveProjectSwitchTarget(rawTarget) {
|
|
1473
|
+
const target = String(rawTarget || "").trim();
|
|
1474
|
+
if (!target) {
|
|
1475
|
+
throw new Error("missing target");
|
|
1476
|
+
}
|
|
1477
|
+
if (/^\d+$/.test(target)) {
|
|
1478
|
+
const index = Number.parseInt(target, 10);
|
|
1479
|
+
if (!Number.isFinite(index) || index <= 0) {
|
|
1480
|
+
throw new Error("invalid project index");
|
|
1481
|
+
}
|
|
1482
|
+
const rows = listProjectRuntimes({ validate: true, cleanupTmp: true });
|
|
1483
|
+
const item = rows[index - 1];
|
|
1484
|
+
if (!item || !item.project_root) {
|
|
1485
|
+
throw new Error("project index out of range");
|
|
1486
|
+
}
|
|
1487
|
+
return {
|
|
1488
|
+
projectRoot: canonicalProjectRoot(item.project_root),
|
|
1489
|
+
source: `index ${index}`,
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
return {
|
|
1493
|
+
projectRoot: canonicalProjectRoot(target),
|
|
1494
|
+
source: target,
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
async function switchProjectConnection(targetInput) {
|
|
1499
|
+
let targetInfo;
|
|
1500
|
+
try {
|
|
1501
|
+
targetInfo = resolveProjectSwitchTarget(targetInput);
|
|
1502
|
+
} catch (err) {
|
|
1503
|
+
return {
|
|
1504
|
+
ok: false,
|
|
1505
|
+
error: err && err.message ? err.message : "invalid project target",
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
const nextProjectRoot = targetInfo.projectRoot;
|
|
1509
|
+
if (!nextProjectRoot) {
|
|
1510
|
+
return { ok: false, error: "invalid project target" };
|
|
1511
|
+
}
|
|
1512
|
+
if (nextProjectRoot === activeProjectRoot) {
|
|
1513
|
+
return { ok: true, project_root: activeProjectRoot, unchanged: true };
|
|
1514
|
+
}
|
|
1515
|
+
const outgoingDraftSnapshot = captureCurrentProjectDraft();
|
|
1516
|
+
|
|
1517
|
+
try {
|
|
1518
|
+
const nextPaths = getUfooPaths(nextProjectRoot);
|
|
1519
|
+
if (!fs.existsSync(nextPaths.ufooDir)) {
|
|
1520
|
+
const repoRoot = path.join(__dirname, "..", "..");
|
|
1521
|
+
const init = new UfooInit(repoRoot);
|
|
1522
|
+
await init.init({ modules: "context,bus", project: nextProjectRoot });
|
|
1523
|
+
}
|
|
1524
|
+
if (!isRunning(nextProjectRoot)) {
|
|
1525
|
+
startDaemon(nextProjectRoot);
|
|
1526
|
+
}
|
|
1527
|
+
const result = await daemonCoordinator.switchProject({
|
|
1528
|
+
projectRoot: nextProjectRoot,
|
|
1529
|
+
sockPath: socketPath(nextProjectRoot),
|
|
1530
|
+
});
|
|
1531
|
+
if (!result || result.ok !== true) {
|
|
1532
|
+
return {
|
|
1533
|
+
ok: false,
|
|
1534
|
+
error: (result && result.error) || "switch failed",
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
const previousProjectRoot = activeProjectRoot;
|
|
1538
|
+
if (previousProjectRoot && previousProjectRoot !== nextProjectRoot) {
|
|
1539
|
+
setProjectDraft(previousProjectRoot, outgoingDraftSnapshot);
|
|
1540
|
+
}
|
|
1541
|
+
activeProjectRoot = nextProjectRoot;
|
|
1542
|
+
applyProjectHistoryContext(nextProjectRoot);
|
|
1543
|
+
if (globalMode) {
|
|
1544
|
+
refreshProjectRuntimes();
|
|
1545
|
+
syncSelectedProjectToActive();
|
|
1546
|
+
renderDashboard();
|
|
1547
|
+
screen.render();
|
|
1548
|
+
}
|
|
1549
|
+
return {
|
|
1550
|
+
ok: true,
|
|
1551
|
+
project_root: activeProjectRoot,
|
|
1552
|
+
};
|
|
1553
|
+
} catch (err) {
|
|
1554
|
+
return {
|
|
1555
|
+
ok: false,
|
|
1556
|
+
error: err && err.message ? err.message : "switch failed",
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
let projectSwitching = false;
|
|
1562
|
+
let pendingProjectSwitchRoot = null;
|
|
1563
|
+
let projectSwitchDebounceTimer = null;
|
|
1564
|
+
let projectSwitchFlushPromise = null;
|
|
1565
|
+
const PROJECT_SWITCH_DEBOUNCE_MS = 200;
|
|
1566
|
+
|
|
1567
|
+
function cancelProjectSwitchDebounce() {
|
|
1568
|
+
if (!projectSwitchDebounceTimer) return;
|
|
1569
|
+
clearTimeout(projectSwitchDebounceTimer);
|
|
1570
|
+
projectSwitchDebounceTimer = null;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
function scheduleProjectSwitchFlush(delayMs = PROJECT_SWITCH_DEBOUNCE_MS) {
|
|
1574
|
+
cancelProjectSwitchDebounce();
|
|
1575
|
+
projectSwitchDebounceTimer = setTimeout(() => {
|
|
1576
|
+
projectSwitchDebounceTimer = null;
|
|
1577
|
+
flushPendingProjectSwitch().catch((err) => {
|
|
1578
|
+
const message = err && err.message ? err.message : String(err || "switch failed");
|
|
1579
|
+
logMessage("error", `{white-fg}✗{/white-fg} Switch failed: ${escapeBlessed(message)}`);
|
|
1580
|
+
});
|
|
1581
|
+
}, Math.max(0, Number.isFinite(delayMs) ? delayMs : PROJECT_SWITCH_DEBOUNCE_MS));
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
async function flushPendingProjectSwitch() {
|
|
1585
|
+
if (projectSwitchFlushPromise) {
|
|
1586
|
+
return projectSwitchFlushPromise;
|
|
1587
|
+
}
|
|
1588
|
+
projectSwitchFlushPromise = (async () => {
|
|
1589
|
+
projectSwitching = true;
|
|
1590
|
+
let lastResult = { ok: true, project_root: activeProjectRoot, unchanged: true };
|
|
1591
|
+
try {
|
|
1592
|
+
while (pendingProjectSwitchRoot) {
|
|
1593
|
+
const nextProjectRoot = pendingProjectSwitchRoot;
|
|
1594
|
+
pendingProjectSwitchRoot = null;
|
|
1595
|
+
if (!nextProjectRoot || nextProjectRoot === activeProjectRoot) continue;
|
|
1596
|
+
const result = await switchProjectConnection(nextProjectRoot);
|
|
1597
|
+
lastResult = result || { ok: false, error: "switch failed" };
|
|
1598
|
+
if (!result || result.ok !== true) {
|
|
1599
|
+
const reason = (result && result.error) || "switch failed";
|
|
1600
|
+
logMessage("error", `{white-fg}✗{/white-fg} Switch failed: ${escapeBlessed(reason)}`);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
return lastResult;
|
|
1604
|
+
} finally {
|
|
1605
|
+
projectSwitching = false;
|
|
1606
|
+
if (globalMode) {
|
|
1607
|
+
refreshProjectRuntimes();
|
|
1608
|
+
syncSelectedProjectToActive();
|
|
1609
|
+
renderDashboard();
|
|
1610
|
+
screen.render();
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
})();
|
|
1614
|
+
try {
|
|
1615
|
+
return await projectSwitchFlushPromise;
|
|
1616
|
+
} finally {
|
|
1617
|
+
projectSwitchFlushPromise = null;
|
|
1618
|
+
if (pendingProjectSwitchRoot && !projectSwitchDebounceTimer) {
|
|
1619
|
+
scheduleProjectSwitchFlush(0);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
function requestProjectSwitchByIndex(index) {
|
|
1625
|
+
if (!globalMode) return;
|
|
1626
|
+
const numericIndex = Number(index);
|
|
1627
|
+
const nextIndex = Number.isFinite(numericIndex) ? Math.trunc(numericIndex) : Number.NaN;
|
|
1628
|
+
if (!Number.isFinite(nextIndex) || nextIndex < 0 || nextIndex >= projectRuntimes.length) {
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
selectedProjectIndex = nextIndex;
|
|
1632
|
+
const selected = projectRuntimes[nextIndex] || {};
|
|
1633
|
+
const nextProjectRoot = resolveRuntimeProjectRoot(selected);
|
|
1634
|
+
renderDashboard();
|
|
1635
|
+
screen.render();
|
|
1636
|
+
if (!nextProjectRoot) return;
|
|
1637
|
+
pendingProjectSwitchRoot = nextProjectRoot;
|
|
1638
|
+
scheduleProjectSwitchFlush();
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
async function requestProjectSwitchByTarget(targetInput) {
|
|
1642
|
+
let targetInfo;
|
|
1643
|
+
try {
|
|
1644
|
+
targetInfo = resolveProjectSwitchTarget(targetInput);
|
|
1645
|
+
} catch (err) {
|
|
1646
|
+
return {
|
|
1647
|
+
ok: false,
|
|
1648
|
+
error: err && err.message ? err.message : "invalid project target",
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
const nextProjectRoot = targetInfo && targetInfo.projectRoot ? targetInfo.projectRoot : "";
|
|
1652
|
+
if (!nextProjectRoot) {
|
|
1653
|
+
return { ok: false, error: "invalid project target" };
|
|
1654
|
+
}
|
|
1655
|
+
if (nextProjectRoot === activeProjectRoot) {
|
|
1656
|
+
return { ok: true, project_root: activeProjectRoot, unchanged: true };
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
pendingProjectSwitchRoot = nextProjectRoot;
|
|
1660
|
+
cancelProjectSwitchDebounce();
|
|
1661
|
+
|
|
1662
|
+
let attempts = 0;
|
|
1663
|
+
while (attempts < 4) {
|
|
1664
|
+
attempts += 1;
|
|
1665
|
+
const result = await flushPendingProjectSwitch();
|
|
1666
|
+
if (activeProjectRoot === nextProjectRoot) {
|
|
1667
|
+
return { ok: true, project_root: activeProjectRoot };
|
|
1668
|
+
}
|
|
1669
|
+
if (!pendingProjectSwitchRoot) {
|
|
1670
|
+
if (result && result.ok !== true) return result;
|
|
1671
|
+
return { ok: false, error: "switch failed" };
|
|
1672
|
+
}
|
|
1673
|
+
if (pendingProjectSwitchRoot !== nextProjectRoot) {
|
|
1674
|
+
pendingProjectSwitchRoot = nextProjectRoot;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
return { ok: false, error: "switch did not complete" };
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1192
1680
|
const commandExecutor = createCommandExecutor({
|
|
1193
1681
|
projectRoot,
|
|
1194
1682
|
parseCommand,
|
|
@@ -1211,9 +1699,15 @@ async function runChat(projectRoot) {
|
|
|
1211
1699
|
});
|
|
1212
1700
|
},
|
|
1213
1701
|
activateAgent: async (target) => {
|
|
1214
|
-
const activator = new AgentActivator(
|
|
1702
|
+
const activator = new AgentActivator(activeProjectRoot);
|
|
1215
1703
|
await activator.activate(target);
|
|
1216
1704
|
},
|
|
1705
|
+
listProjects: () => listProjectRuntimes({ validate: true, cleanupTmp: true }),
|
|
1706
|
+
getCurrentProject: () => ({
|
|
1707
|
+
project_root: activeProjectRoot,
|
|
1708
|
+
project_name: path.basename(activeProjectRoot),
|
|
1709
|
+
}),
|
|
1710
|
+
switchProject: async ({ target } = {}) => requestProjectSwitchByTarget(target),
|
|
1217
1711
|
});
|
|
1218
1712
|
|
|
1219
1713
|
async function executeCommand(text) {
|
|
@@ -1246,7 +1740,7 @@ async function runChat(projectRoot) {
|
|
|
1246
1740
|
},
|
|
1247
1741
|
enterAgentView,
|
|
1248
1742
|
activateAgent: async (agentId) => {
|
|
1249
|
-
const activator = new AgentActivator(
|
|
1743
|
+
const activator = new AgentActivator(activeProjectRoot);
|
|
1250
1744
|
await activator.activate(agentId);
|
|
1251
1745
|
},
|
|
1252
1746
|
getInjectSockPath,
|
|
@@ -1347,6 +1841,12 @@ async function runChat(projectRoot) {
|
|
|
1347
1841
|
}
|
|
1348
1842
|
loadHistory();
|
|
1349
1843
|
loadInputHistory();
|
|
1844
|
+
if (globalMode) {
|
|
1845
|
+
inputHistoryController.restoreDraft(getProjectDraft(activeProjectRoot));
|
|
1846
|
+
}
|
|
1847
|
+
if (globalMode) {
|
|
1848
|
+
refreshProjectRuntimes();
|
|
1849
|
+
}
|
|
1350
1850
|
renderDashboard();
|
|
1351
1851
|
resizeInput();
|
|
1352
1852
|
requestStatus();
|