u-foo 1.4.1 → 1.6.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.
Files changed (47) hide show
  1. package/README.md +21 -0
  2. package/README.zh-CN.md +21 -0
  3. package/bin/ufoo.js +15 -7
  4. package/modules/AGENTS.template.md +4 -102
  5. package/package.json +3 -2
  6. package/scripts/global-chat-switch-benchmark.js +406 -0
  7. package/src/agent/activityDetector.js +328 -0
  8. package/src/agent/activityStatePublisher.js +67 -0
  9. package/src/agent/activityStateWriter.js +40 -0
  10. package/src/agent/internalRunner.js +13 -0
  11. package/src/agent/launcher.js +47 -7
  12. package/src/agent/notifier.js +73 -4
  13. package/src/agent/ptyRunner.js +81 -34
  14. package/src/agent/ufooAgent.js +192 -6
  15. package/src/bus/message.js +1 -9
  16. package/src/bus/subscriber.js +2 -0
  17. package/src/bus/utils.js +10 -0
  18. package/src/chat/agentBar.js +21 -3
  19. package/src/chat/agentViewController.js +2 -0
  20. package/src/chat/chatLogController.js +28 -5
  21. package/src/chat/commandExecutor.js +127 -3
  22. package/src/chat/commands.js +8 -0
  23. package/src/chat/daemonConnection.js +77 -4
  24. package/src/chat/daemonCoordinator.js +36 -0
  25. package/src/chat/daemonMessageRouter.js +22 -0
  26. package/src/chat/daemonTransport.js +47 -5
  27. package/src/chat/daemonTransportDefaults.js +1 -0
  28. package/src/chat/dashboardKeyController.js +89 -1
  29. package/src/chat/dashboardView.js +312 -93
  30. package/src/chat/index.js +683 -41
  31. package/src/chat/inputHistoryController.js +33 -3
  32. package/src/chat/inputListenerController.js +22 -12
  33. package/src/chat/layout.js +12 -7
  34. package/src/chat/projectCloseController.js +119 -0
  35. package/src/chat/projectRuntimes.js +55 -0
  36. package/src/chat/statusLineController.js +52 -6
  37. package/src/chat/streamTracker.js +6 -0
  38. package/src/chat/transport.js +41 -5
  39. package/src/cli.js +167 -4
  40. package/src/daemon/index.js +54 -5
  41. package/src/daemon/ipcServer.js +6 -1
  42. package/src/daemon/ops.js +245 -35
  43. package/src/daemon/status.js +3 -1
  44. package/src/init/index.js +32 -3
  45. package/src/projects/projectId.js +29 -0
  46. package/src/projects/registry.js +279 -0
  47. package/src/ufoo/agentsStore.js +44 -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");
@@ -36,13 +38,30 @@ const { createChatLogController } = require("./chatLogController");
36
38
  const { createPasteController } = require("./pasteController");
37
39
  const { createAgentViewController } = require("./agentViewController");
38
40
  const { createSettingsController } = require("./settingsController");
41
+ const { createProjectCloseController } = require("./projectCloseController");
39
42
  const { createChatLayout } = require("./layout");
40
43
  const { createDaemonCoordinator } = require("./daemonCoordinator");
41
44
  const { IPC_REQUEST_TYPES } = require("../shared/eventContract");
42
45
  const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
43
46
  const { createDaemonTransport } = require("./daemonTransport");
47
+ const { listProjectRuntimes } = require("../projects/registry");
48
+ const { canonicalProjectRoot, buildProjectId } = require("../projects/projectId");
49
+ const {
50
+ sortProjectRuntimes,
51
+ parseTimestampMs,
52
+ filterVisibleProjectRuntimes,
53
+ } = require("./projectRuntimes");
54
+
55
+ async function runChat(projectRoot, options = {}) {
56
+ const globalMode = options && options.globalMode === true;
57
+ const DASHBOARD_HEIGHT = globalMode ? 2 : 1;
58
+ let activeProjectRoot = projectRoot;
59
+ try {
60
+ activeProjectRoot = canonicalProjectRoot(projectRoot);
61
+ } catch {
62
+ activeProjectRoot = path.resolve(projectRoot || process.cwd());
63
+ }
44
64
 
45
- async function runChat(projectRoot) {
46
65
  if (!fs.existsSync(getUfooPaths(projectRoot).ufooDir)) {
47
66
  const repoRoot = path.join(__dirname, "..", "..");
48
67
  const init = new UfooInit(repoRoot);
@@ -51,7 +70,6 @@ async function runChat(projectRoot) {
51
70
 
52
71
  // Ensure subscriber ID exists for chat (persistent across restarts)
53
72
  if (!process.env.UFOO_SUBSCRIBER_ID) {
54
- const crypto = require("crypto");
55
73
  const sessionFile = path.join(getUfooPaths(projectRoot).ufooDir, "chat", "session-id.txt");
56
74
  const sessionDir = path.dirname(sessionFile);
57
75
  fs.mkdirSync(sessionDir, { recursive: true });
@@ -88,10 +106,12 @@ async function runChat(projectRoot) {
88
106
  let autoResume = config.autoResume !== false;
89
107
  let cronTasks = [];
90
108
 
91
- // Dynamic input height settings
92
- // Layout: topLine(1) + content + bottomLine(1) + dashboard(1)
93
- const MIN_INPUT_HEIGHT = 4; // 1 content + 3
94
- const MAX_INPUT_HEIGHT = 9; // 6 content + 3
109
+ // Dynamic input height settings.
110
+ // Layout: dashboard(N) + inputBottom(1) + content + inputTop(1) + status(1)
111
+ const MIN_INPUT_CONTENT_HEIGHT = 1;
112
+ const MAX_INPUT_CONTENT_HEIGHT = 6;
113
+ const MIN_INPUT_HEIGHT = MIN_INPUT_CONTENT_HEIGHT + DASHBOARD_HEIGHT + 2;
114
+ const MAX_INPUT_HEIGHT = MAX_INPUT_CONTENT_HEIGHT + DASHBOARD_HEIGHT + 2;
95
115
  let currentInputHeight = MIN_INPUT_HEIGHT;
96
116
  const pkg = require("../../package.json");
97
117
  const {
@@ -108,18 +128,136 @@ async function runChat(projectRoot) {
108
128
  } = createChatLayout({
109
129
  blessed,
110
130
  currentInputHeight,
131
+ dashboardHeight: DASHBOARD_HEIGHT,
111
132
  version: pkg.version,
112
133
  });
113
134
 
114
- const historyDir = path.join(getUfooPaths(projectRoot).ufooDir, "chat");
115
- const historyFile = path.join(historyDir, "history.jsonl");
116
- const inputHistoryFile = path.join(historyDir, "input-history.jsonl");
135
+ const globalChatRoot = path.join(os.homedir(), ".ufoo", "chat");
136
+ const globalDraftsFile = path.join(globalChatRoot, "global-drafts.json");
137
+ const GLOBAL_DRAFT_PERSIST_DEBOUNCE_MS = 150;
138
+ let globalDraftsLoaded = false;
139
+ let globalDraftPersistTimer = null;
140
+ const globalDraftMap = new Map();
141
+
142
+ function safeCanonicalProjectRoot(targetRoot) {
143
+ try {
144
+ return canonicalProjectRoot(targetRoot);
145
+ } catch {
146
+ return path.resolve(targetRoot || process.cwd());
147
+ }
148
+ }
149
+
150
+ function resolveHistoryContext(targetProjectRoot) {
151
+ const canonicalRoot = safeCanonicalProjectRoot(targetProjectRoot);
152
+ if (!globalMode) {
153
+ const localHistoryDir = path.join(getUfooPaths(canonicalRoot).ufooDir, "chat");
154
+ return {
155
+ projectRoot: canonicalRoot,
156
+ historyDir: localHistoryDir,
157
+ historyFile: path.join(localHistoryDir, "history.jsonl"),
158
+ inputHistoryDir: localHistoryDir,
159
+ inputHistoryFile: path.join(localHistoryDir, "input-history.jsonl"),
160
+ };
161
+ }
162
+ let projectId = "";
163
+ try {
164
+ projectId = buildProjectId(canonicalRoot);
165
+ } catch {
166
+ projectId = crypto.createHash("sha256").update(canonicalRoot).digest("hex").slice(0, 16);
167
+ }
168
+ const globalHistoryDir = path.join(globalChatRoot, "global-history");
169
+ const globalInputHistoryDir = path.join(globalChatRoot, "global-input-history");
170
+ return {
171
+ projectRoot: canonicalRoot,
172
+ projectId,
173
+ historyDir: globalHistoryDir,
174
+ historyFile: path.join(globalHistoryDir, `${projectId}.jsonl`),
175
+ inputHistoryDir: globalInputHistoryDir,
176
+ inputHistoryFile: path.join(globalInputHistoryDir, `${projectId}.jsonl`),
177
+ };
178
+ }
179
+
180
+ function loadGlobalDraftsOnce() {
181
+ if (!globalMode || globalDraftsLoaded) return;
182
+ globalDraftsLoaded = true;
183
+ try {
184
+ const raw = fs.readFileSync(globalDraftsFile, "utf8");
185
+ const parsed = JSON.parse(String(raw || "{}"));
186
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
187
+ Object.entries(parsed).forEach(([projectRootKey, draft]) => {
188
+ if (typeof draft !== "string") return;
189
+ const canonicalKey = safeCanonicalProjectRoot(projectRootKey);
190
+ if (!canonicalKey) return;
191
+ globalDraftMap.set(canonicalKey, draft);
192
+ });
193
+ } catch {
194
+ // Ignore missing/invalid drafts file.
195
+ }
196
+ }
197
+
198
+ function writeGlobalDraftsToDisk() {
199
+ if (!globalMode) return;
200
+ const out = {};
201
+ for (const [projectRootKey, draft] of globalDraftMap.entries()) {
202
+ if (!projectRootKey) continue;
203
+ if (typeof draft !== "string" || draft.length === 0) continue;
204
+ out[projectRootKey] = draft;
205
+ }
206
+ try {
207
+ fs.mkdirSync(path.dirname(globalDraftsFile), { recursive: true });
208
+ fs.writeFileSync(globalDraftsFile, `${JSON.stringify(out, null, 2)}\n`, "utf8");
209
+ } catch {
210
+ // Ignore draft persistence failures.
211
+ }
212
+ }
213
+
214
+ function persistGlobalDrafts(options = {}) {
215
+ if (!globalMode) return;
216
+ const immediate = Boolean(options.immediate);
217
+ if (immediate) {
218
+ if (globalDraftPersistTimer) {
219
+ clearTimeout(globalDraftPersistTimer);
220
+ globalDraftPersistTimer = null;
221
+ }
222
+ writeGlobalDraftsToDisk();
223
+ return;
224
+ }
225
+ if (globalDraftPersistTimer) {
226
+ clearTimeout(globalDraftPersistTimer);
227
+ }
228
+ globalDraftPersistTimer = setTimeout(() => {
229
+ globalDraftPersistTimer = null;
230
+ writeGlobalDraftsToDisk();
231
+ }, GLOBAL_DRAFT_PERSIST_DEBOUNCE_MS);
232
+ }
233
+
234
+ function getProjectDraft(targetProjectRoot) {
235
+ if (!globalMode) return "";
236
+ loadGlobalDraftsOnce();
237
+ const canonicalRoot = safeCanonicalProjectRoot(targetProjectRoot);
238
+ return globalDraftMap.get(canonicalRoot) || "";
239
+ }
240
+
241
+ function setProjectDraft(targetProjectRoot, draft, options = {}) {
242
+ if (!globalMode) return;
243
+ loadGlobalDraftsOnce();
244
+ const canonicalRoot = safeCanonicalProjectRoot(targetProjectRoot);
245
+ const text = String(draft || "");
246
+ if (!text) {
247
+ globalDraftMap.delete(canonicalRoot);
248
+ } else {
249
+ globalDraftMap.set(canonicalRoot, text);
250
+ }
251
+ persistGlobalDrafts(options);
252
+ }
253
+
254
+ let currentHistoryContext = resolveHistoryContext(activeProjectRoot);
117
255
 
118
- const chatLogController = createChatLogController({
256
+ let chatLogController = createChatLogController({
119
257
  logBox,
120
258
  fsModule: fs,
121
- historyDir,
122
- historyFile,
259
+ historyDir: currentHistoryContext.historyDir,
260
+ historyFile: currentHistoryContext.historyFile,
123
261
  });
124
262
 
125
263
  const streamTracker = createStreamTracker({
@@ -293,12 +431,67 @@ async function runChat(projectRoot) {
293
431
  }
294
432
 
295
433
  inputHistoryController = createInputHistoryController({
296
- inputHistoryFile,
297
- historyDir,
434
+ inputHistoryFile: currentHistoryContext.inputHistoryFile,
435
+ historyDir: currentHistoryContext.inputHistoryDir,
298
436
  setInputValue,
299
437
  getInputValue: () => input.value || "",
300
438
  });
301
439
 
440
+ function captureCurrentProjectDraft() {
441
+ if (!inputHistoryController || typeof inputHistoryController.getDraftForPersistence !== "function") {
442
+ return input.value || "";
443
+ }
444
+ return inputHistoryController.getDraftForPersistence();
445
+ }
446
+
447
+ function seedGlobalHistoryFromProject(nextContext) {
448
+ if (!globalMode || !nextContext || !nextContext.projectRoot) return;
449
+ const projectUfooDir = getUfooPaths(nextContext.projectRoot).ufooDir;
450
+ const projectChatDir = path.join(projectUfooDir, "chat");
451
+ const projectHistoryFile = path.join(projectChatDir, "history.jsonl");
452
+ const projectInputHistoryFile = path.join(projectChatDir, "input-history.jsonl");
453
+ try {
454
+ if (!fs.existsSync(nextContext.historyFile) && fs.existsSync(projectHistoryFile)) {
455
+ fs.mkdirSync(path.dirname(nextContext.historyFile), { recursive: true });
456
+ fs.copyFileSync(projectHistoryFile, nextContext.historyFile);
457
+ }
458
+ } catch {
459
+ // best-effort seed only
460
+ }
461
+ try {
462
+ if (!fs.existsSync(nextContext.inputHistoryFile) && fs.existsSync(projectInputHistoryFile)) {
463
+ fs.mkdirSync(path.dirname(nextContext.inputHistoryFile), { recursive: true });
464
+ fs.copyFileSync(projectInputHistoryFile, nextContext.inputHistoryFile);
465
+ }
466
+ } catch {
467
+ // best-effort seed only
468
+ }
469
+ }
470
+
471
+ function applyProjectHistoryContext(nextProjectRoot) {
472
+ streamTracker.discardAll();
473
+ const nextContext = resolveHistoryContext(nextProjectRoot);
474
+ seedGlobalHistoryFromProject(nextContext);
475
+ currentHistoryContext = nextContext;
476
+ chatLogController.setHistoryTarget({
477
+ historyDir: nextContext.historyDir,
478
+ historyFile: nextContext.historyFile,
479
+ });
480
+ chatLogController.resetViewState();
481
+
482
+ inputHistoryController.setHistoryTarget({
483
+ inputHistoryFile: nextContext.inputHistoryFile,
484
+ historyDir: nextContext.inputHistoryDir,
485
+ });
486
+ inputHistoryController.loadInputHistory();
487
+ const nextDraft = getProjectDraft(nextContext.projectRoot);
488
+ inputHistoryController.restoreDraft(nextDraft);
489
+
490
+ clearLog();
491
+ loadHistory();
492
+ pending = null;
493
+ }
494
+
302
495
  function historyUp() {
303
496
  if (!inputHistoryController) return false;
304
497
  return inputHistoryController.historyUp();
@@ -310,6 +503,9 @@ async function runChat(projectRoot) {
310
503
  }
311
504
 
312
505
  function exitHandler() {
506
+ if (globalMode) {
507
+ setProjectDraft(activeProjectRoot, captureCurrentProjectDraft(), { immediate: true });
508
+ }
313
509
  if (daemonCoordinator) {
314
510
  daemonCoordinator.markExit();
315
511
  }
@@ -396,8 +592,8 @@ async function runChat(projectRoot) {
396
592
  if (innerWidth <= 0) return;
397
593
 
398
594
  const numLines = countLines(input.value, innerWidth);
399
- const contentHeight = Math.min(MAX_INPUT_HEIGHT - 3, Math.max(1, numLines));
400
- const targetHeight = contentHeight + 3; // +1 topLine +1 bottomLine +1 dashboard
595
+ const contentHeight = Math.min(MAX_INPUT_CONTENT_HEIGHT, Math.max(MIN_INPUT_CONTENT_HEIGHT, numLines));
596
+ const targetHeight = contentHeight + DASHBOARD_HEIGHT + 2;
401
597
 
402
598
  if (targetHeight !== currentInputHeight) {
403
599
  currentInputHeight = targetHeight;
@@ -408,7 +604,7 @@ async function runChat(projectRoot) {
408
604
  statusLine.bottom = currentInputHeight;
409
605
  // Reposition completion panel if active
410
606
  if (completionController.isActive()) completionController.reflow();
411
- // dashboard and inputBottomLine stay fixed at bottom 0 and 1
607
+ // dashboard and inputBottomLine stay fixed at the bottom region.
412
608
  logBox.height = Math.max(1, screen.height - currentInputHeight - 1);
413
609
  ensureInputCursorVisible();
414
610
  }
@@ -450,7 +646,7 @@ async function runChat(projectRoot) {
450
646
  currentInputHeight = MIN_INPUT_HEIGHT;
451
647
  if (inputHistoryController) inputHistoryController.setIndexToEnd();
452
648
  completionController.hide();
453
- const contentHeight = 1; // MIN content height
649
+ const contentHeight = MIN_INPUT_CONTENT_HEIGHT;
454
650
  input.height = contentHeight;
455
651
  promptBox.height = contentHeight;
456
652
  inputTopLine.bottom = currentInputHeight - 1;
@@ -465,12 +661,17 @@ async function runChat(projectRoot) {
465
661
  let activeAgents = [];
466
662
  let activeAgentLabelMap = new Map();
467
663
  let activeAgentMetaMap = new Map(); // Store full meta including launch_mode
664
+ const transientAgentStateMap = new Map();
468
665
  let agentListWindowStart = 0;
469
666
  const MAX_AGENT_WINDOW = 4;
667
+ let projectRuntimes = [];
668
+ let projectListWindowStart = 0;
669
+ const MAX_PROJECT_WINDOW = 5;
670
+ let selectedProjectIndex = -1;
470
671
  let selectedAgentIndex = -1; // -1 = not in dashboard selection mode
471
672
  let targetAgent = null; // Selected agent for direct messaging
472
673
  let focusMode = "input"; // "input" or "dashboard"
473
- let dashboardView = "agents"; // "agents" | "mode" | "provider" | "assistant" | "cron"
674
+ let dashboardView = "agents"; // "projects" | "agents" | "mode" | "provider" | "assistant" | "cron"
474
675
  let reportPendingTotal = 0;
475
676
  let selectedModeIndex = launchMode === "internal" ? 2 : (launchMode === "tmux" ? 1 : 0);
476
677
  const providerOptions = [
@@ -496,12 +697,16 @@ async function runChat(projectRoot) {
496
697
  let selectedResumeIndex = autoResume ? 0 : 1;
497
698
  const DASH_HINTS = {
498
699
  agents: "←/→ select · Enter · ↓ mode · ↑ back",
700
+ agentsGlobal: "←/→ select · Enter · ↓ mode · ↑ projects",
499
701
  agentsEmpty: "↓ mode · ↑ back",
500
702
  mode: "←/→ select · Enter · ↓ provider · ↑ back",
501
703
  provider: "←/→ select · Enter · ↓ assistant · ↑ back",
502
704
  assistant: "←/→ select · Enter · ↓ cron · ↑ back",
503
705
  cron: "Ctrl+X close · ↑ back",
504
706
  resume: "",
707
+ projects: "Use /project switch <index|path>",
708
+ projectsFocus: "←/→ switch · Ctrl+X close · ↓ second row · Enter confirm · ↑ back",
709
+ projectsEmpty: "Run ufoo chat or ufoo daemon start in project directories",
505
710
  };
506
711
  const AGENT_BAR_HINTS = {
507
712
  normal: "↓ agents",
@@ -668,7 +873,7 @@ async function runChat(projectRoot) {
668
873
  labelMap: activeAgentLabelMap,
669
874
  lookupNickname: (nickname) => {
670
875
  try {
671
- const busPath = getUfooPaths(projectRoot).agentsFile;
876
+ const busPath = getUfooPaths(activeProjectRoot).agentsFile;
672
877
  const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
673
878
  for (const [id, meta] of Object.entries(bus.agents || {})) {
674
879
  if (meta && meta.nickname === nickname) return id;
@@ -687,7 +892,7 @@ async function runChat(projectRoot) {
687
892
  labelMap: activeAgentLabelMap,
688
893
  lookupNicknameById: (id) => {
689
894
  try {
690
- const busPath = getUfooPaths(projectRoot).agentsFile;
895
+ const busPath = getUfooPaths(activeProjectRoot).agentsFile;
691
896
  const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
692
897
  const meta = bus.agents && bus.agents[id];
693
898
  if (meta && meta.nickname) return meta.nickname;
@@ -712,6 +917,81 @@ async function runChat(projectRoot) {
712
917
  clampAgentWindowWithSelection(selectedAgentIndex);
713
918
  }
714
919
 
920
+ function resolveRuntimeProjectRoot(row = {}) {
921
+ const raw = row && row.project_root ? String(row.project_root) : "";
922
+ if (!raw) return "";
923
+ try {
924
+ return canonicalProjectRoot(raw);
925
+ } catch {
926
+ return path.resolve(raw);
927
+ }
928
+ }
929
+
930
+ function refreshProjectRuntimes() {
931
+ let rows = [];
932
+ try {
933
+ rows = listProjectRuntimes({ validate: true, cleanupTmp: true });
934
+ } catch {
935
+ rows = [];
936
+ }
937
+ rows = filterVisibleProjectRuntimes(rows);
938
+ const normalizedActive = String(activeProjectRoot || "");
939
+ if (
940
+ normalizedActive
941
+ && !rows.some((row) => resolveRuntimeProjectRoot(row) === normalizedActive)
942
+ ) {
943
+ rows.unshift({
944
+ project_root: normalizedActive,
945
+ project_name: path.basename(normalizedActive) || normalizedActive,
946
+ status: "untracked",
947
+ last_seen: null,
948
+ });
949
+ }
950
+ projectRuntimes = sortProjectRuntimes({
951
+ rows,
952
+ activeProjectRoot: normalizedActive,
953
+ resolveProjectRoot: resolveRuntimeProjectRoot,
954
+ getInteractionMs: (row) => {
955
+ const rowRoot = resolveRuntimeProjectRoot(row);
956
+ if (!rowRoot) return 0;
957
+ try {
958
+ const historyContext = resolveHistoryContext(rowRoot);
959
+ if (historyContext && historyContext.historyFile && fs.existsSync(historyContext.historyFile)) {
960
+ const stat = fs.statSync(historyContext.historyFile);
961
+ if (Number.isFinite(stat.mtimeMs) && stat.mtimeMs > 0) {
962
+ return stat.mtimeMs;
963
+ }
964
+ }
965
+ } catch {
966
+ // fall through
967
+ }
968
+ return parseTimestampMs(row && row.last_seen);
969
+ },
970
+ });
971
+
972
+ if (projectRuntimes.length === 0) {
973
+ selectedProjectIndex = -1;
974
+ projectListWindowStart = 0;
975
+ return;
976
+ }
977
+ const activeIndex = projectRuntimes.findIndex(
978
+ (row) => resolveRuntimeProjectRoot(row) === normalizedActive
979
+ );
980
+ if (selectedProjectIndex < 0 || selectedProjectIndex >= projectRuntimes.length) {
981
+ selectedProjectIndex = activeIndex >= 0 ? activeIndex : 0;
982
+ }
983
+ }
984
+
985
+ function syncSelectedProjectToActive() {
986
+ if (!Array.isArray(projectRuntimes) || projectRuntimes.length === 0) return;
987
+ const activeIndex = projectRuntimes.findIndex(
988
+ (row) => resolveRuntimeProjectRoot(row) === String(activeProjectRoot || "")
989
+ );
990
+ if (activeIndex >= 0) {
991
+ selectedProjectIndex = activeIndex;
992
+ }
993
+ }
994
+
715
995
  function send(req) {
716
996
  if (!daemonCoordinator) return;
717
997
  daemonCoordinator.send(req);
@@ -788,8 +1068,6 @@ async function runChat(projectRoot) {
788
1068
  logMessage("error", "{white-fg}✗{/white-fg} No agent selected");
789
1069
  return;
790
1070
  }
791
- const label = getAgentLabel(agentId);
792
- logMessage("status", `{white-fg}⚙{/white-fg} Closing ${label}...`);
793
1071
  send({ type: IPC_REQUEST_TYPES.CLOSE_AGENT, agent_id: agentId });
794
1072
  }
795
1073
 
@@ -870,13 +1148,31 @@ async function runChat(projectRoot) {
870
1148
 
871
1149
  function renderDashboard() {
872
1150
  const computed = computeDashboardContent({
1151
+ globalMode,
873
1152
  focusMode,
874
1153
  dashboardView,
875
1154
  activeAgents,
1155
+ projects: projectRuntimes,
1156
+ selectedProjectIndex,
1157
+ projectListWindowStart,
1158
+ maxProjectWindow: MAX_PROJECT_WINDOW,
1159
+ activeProjectRoot,
876
1160
  selectedAgentIndex,
877
1161
  agentListWindowStart,
878
1162
  maxAgentWindow: MAX_AGENT_WINDOW,
879
1163
  getAgentLabel,
1164
+ getAgentState: (agentId) => {
1165
+ let metaState = "";
1166
+ if (activeAgentMetaMap) {
1167
+ const meta = activeAgentMetaMap.get(agentId);
1168
+ metaState = meta && typeof meta.activity_state === "string"
1169
+ ? String(meta.activity_state).trim()
1170
+ : "";
1171
+ }
1172
+ if (metaState) return metaState;
1173
+ const transientState = transientAgentStateMap.get(agentId);
1174
+ return typeof transientState === "string" ? transientState : "";
1175
+ },
880
1176
  launchMode,
881
1177
  agentProvider,
882
1178
  assistantEngine,
@@ -892,12 +1188,51 @@ async function runChat(projectRoot) {
892
1188
  pendingReports: reportPendingTotal,
893
1189
  dashHints: DASH_HINTS,
894
1190
  });
895
- agentListWindowStart = computed.windowStart;
896
- dashboard.setContent(computed.content);
1191
+ if (globalMode && (focusMode !== "dashboard" || dashboardView === "projects")) {
1192
+ projectListWindowStart = computed.windowStart;
1193
+ } else {
1194
+ agentListWindowStart = computed.windowStart;
1195
+ }
1196
+ let dashboardContent = computed.content;
1197
+ if (globalMode && !String(dashboardContent || "").includes("\n")) {
1198
+ dashboardContent = `${dashboardContent}\n `;
1199
+ }
1200
+ dashboard.setContent(dashboardContent);
1201
+ }
1202
+
1203
+ function readDiskMetaForActiveAgents(activeList = []) {
1204
+ const map = new Map();
1205
+ const ids = Array.isArray(activeList) ? activeList : [];
1206
+ if (ids.length === 0) return map;
1207
+ try {
1208
+ const busPath = getUfooPaths(activeProjectRoot).agentsFile;
1209
+ if (!fs.existsSync(busPath)) return map;
1210
+ const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
1211
+ const agents = bus && bus.agents && typeof bus.agents === "object" ? bus.agents : {};
1212
+ for (const id of ids) {
1213
+ const meta = agents[id];
1214
+ if (!meta || typeof meta !== "object") continue;
1215
+ map.set(id, meta);
1216
+ }
1217
+ } catch {
1218
+ // ignore disk fallback errors
1219
+ }
1220
+ return map;
897
1221
  }
898
1222
 
899
1223
  function updateDashboard(status) {
900
1224
  activeAgents = status.active || [];
1225
+ if (transientAgentStateMap.size > 0) {
1226
+ const activeSet = new Set(activeAgents);
1227
+ for (const id of transientAgentStateMap.keys()) {
1228
+ if (!activeSet.has(id)) {
1229
+ transientAgentStateMap.delete(id);
1230
+ }
1231
+ }
1232
+ }
1233
+ if (globalMode) {
1234
+ refreshProjectRuntimes();
1235
+ }
901
1236
  reportPendingTotal = Number.isFinite(status?.reports?.pending_total)
902
1237
  ? status.reports.pending_total
903
1238
  : 0;
@@ -906,7 +1241,7 @@ async function runChat(projectRoot) {
906
1241
  let fallbackMap = null;
907
1242
  if (metaList.length === 0 && activeAgents.length > 0) {
908
1243
  try {
909
- const busPath = getUfooPaths(projectRoot).agentsFile;
1244
+ const busPath = getUfooPaths(activeProjectRoot).agentsFile;
910
1245
  const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
911
1246
  fallbackMap = new Map();
912
1247
  for (const [id, meta] of Object.entries(bus.agents || {})) {
@@ -918,7 +1253,35 @@ async function runChat(projectRoot) {
918
1253
  }
919
1254
  const maps = agentDirectory.buildAgentMaps(activeAgents, metaList, fallbackMap);
920
1255
  activeAgentLabelMap = maps.labelMap;
921
- activeAgentMetaMap = maps.metaMap;
1256
+ const diskMetaMap = readDiskMetaForActiveAgents(activeAgents);
1257
+ if (diskMetaMap.size > 0) {
1258
+ const mergedMetaMap = new Map(maps.metaMap);
1259
+ for (const id of activeAgents) {
1260
+ const currentMeta = mergedMetaMap.get(id);
1261
+ const diskMeta = diskMetaMap.get(id);
1262
+ if (!currentMeta && diskMeta) {
1263
+ mergedMetaMap.set(id, { id, ...diskMeta });
1264
+ continue;
1265
+ }
1266
+ if (!currentMeta || !diskMeta) continue;
1267
+ const currentState = typeof currentMeta.activity_state === "string"
1268
+ ? String(currentMeta.activity_state).trim()
1269
+ : "";
1270
+ const diskState = typeof diskMeta.activity_state === "string"
1271
+ ? String(diskMeta.activity_state).trim()
1272
+ : "";
1273
+ if (!currentState && diskState) {
1274
+ mergedMetaMap.set(id, {
1275
+ ...currentMeta,
1276
+ activity_state: diskState,
1277
+ activity_since: currentMeta.activity_since || diskMeta.activity_since || "",
1278
+ });
1279
+ }
1280
+ }
1281
+ activeAgentMetaMap = mergedMetaMap;
1282
+ } else {
1283
+ activeAgentMetaMap = maps.metaMap;
1284
+ }
922
1285
  clampAgentWindow();
923
1286
  // If viewing agent went offline, exit view
924
1287
  const currentView = getCurrentView();
@@ -957,10 +1320,15 @@ async function runChat(projectRoot) {
957
1320
 
958
1321
  function enterDashboardMode() {
959
1322
  focusMode = "dashboard";
960
- dashboardView = "agents";
961
- selectedAgentIndex = activeAgents.length > 0 ? 0 : -1;
962
- agentListWindowStart = 0;
963
- clampAgentWindow();
1323
+ dashboardView = globalMode ? "projects" : "agents";
1324
+ if (globalMode) {
1325
+ refreshProjectRuntimes();
1326
+ syncSelectedProjectToActive();
1327
+ } else {
1328
+ selectedAgentIndex = activeAgents.length > 0 ? 0 : -1;
1329
+ agentListWindowStart = 0;
1330
+ clampAgentWindow();
1331
+ }
964
1332
  selectedModeIndex = launchMode === "internal" ? 2 : (launchMode === "tmux" ? 1 : 0);
965
1333
  selectedProviderIndex = Math.max(0, providerOptions.findIndex((opt) => opt.value === agentProvider));
966
1334
  selectedAssistantIndex = Math.max(
@@ -968,8 +1336,8 @@ async function runChat(projectRoot) {
968
1336
  assistantOptions.findIndex((opt) => opt.value === assistantEngine)
969
1337
  );
970
1338
  selectedResumeIndex = autoResume ? 0 : 1;
971
- // Immediately set @target when first agent is selected
972
- if (selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
1339
+ // Immediately set @target when first agent is selected.
1340
+ if (!globalMode && selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
973
1341
  targetAgent = activeAgents[selectedAgentIndex];
974
1342
  updatePromptBox();
975
1343
  }
@@ -985,6 +1353,9 @@ async function runChat(projectRoot) {
985
1353
  currentView: { get: () => getCurrentView() },
986
1354
  focusMode: { get: () => focusMode, set: (value) => { focusMode = value; } },
987
1355
  dashboardView: { get: () => dashboardView, set: (value) => { dashboardView = value; } },
1356
+ selectedProjectIndex: { get: () => selectedProjectIndex, set: (value) => { selectedProjectIndex = value; } },
1357
+ projects: { get: () => projectRuntimes },
1358
+ activeProjectRoot: { get: () => activeProjectRoot },
988
1359
  selectedAgentIndex: { get: () => selectedAgentIndex, set: (value) => { selectedAgentIndex = value; } },
989
1360
  activeAgents: { get: () => activeAgents },
990
1361
  viewingAgent: { get: () => getViewingAgent() },
@@ -1009,7 +1380,7 @@ async function runChat(projectRoot) {
1009
1380
 
1010
1381
  function activateAgent(agentId) {
1011
1382
  if (!agentId) return;
1012
- const activator = new AgentActivator(projectRoot);
1383
+ const activator = new AgentActivator(activeProjectRoot);
1013
1384
  activator.activate(agentId).catch(() => {});
1014
1385
  }
1015
1386
 
@@ -1022,6 +1393,7 @@ async function runChat(projectRoot) {
1022
1393
 
1023
1394
  const dashboardController = createDashboardKeyController({
1024
1395
  state: dashboardState,
1396
+ globalMode,
1025
1397
  existsSync: fs.existsSync,
1026
1398
  getInjectSockPath,
1027
1399
  getAgentAdapter,
@@ -1041,6 +1413,8 @@ async function runChat(projectRoot) {
1041
1413
  setAutoResume,
1042
1414
  clampAgentWindow,
1043
1415
  clampAgentWindowWithSelection,
1416
+ requestProjectSwitch: requestProjectSwitchByIndex,
1417
+ requestCloseProject: requestCloseProjectByIndex,
1044
1418
  renderDashboard,
1045
1419
  renderAgentDashboard,
1046
1420
  renderScreen: () => screen.render(),
@@ -1059,8 +1433,9 @@ async function runChat(projectRoot) {
1059
1433
  updatePromptBox();
1060
1434
  }
1061
1435
  focusMode = "input";
1062
- dashboardView = "agents";
1436
+ dashboardView = globalMode ? "projects" : "agents";
1063
1437
  selectedAgentIndex = -1;
1438
+ // Keep selectedProjectIndex across focus transitions so global rail preserves context.
1064
1439
  screen.grabKeys = false;
1065
1440
  renderDashboard();
1066
1441
  focusInput();
@@ -1075,7 +1450,7 @@ async function runChat(projectRoot) {
1075
1450
 
1076
1451
  function getInjectSockPath(agentId) {
1077
1452
  const safeName = subscriberToSafeName(agentId);
1078
- return path.join(getUfooPaths(projectRoot).busQueuesDir, safeName, "inject.sock");
1453
+ return path.join(getUfooPaths(activeProjectRoot).busQueuesDir, safeName, "inject.sock");
1079
1454
  }
1080
1455
 
1081
1456
  agentViewController = createAgentViewController({
@@ -1099,6 +1474,15 @@ async function runChat(projectRoot) {
1099
1474
  agentListWindowStart = value;
1100
1475
  },
1101
1476
  getAgentLabel,
1477
+ getAgentStates: () => {
1478
+ const states = {};
1479
+ if (activeAgentMetaMap) {
1480
+ for (const [id, meta] of activeAgentMetaMap) {
1481
+ if (meta && meta.activity_state) states[id] = meta.activity_state;
1482
+ }
1483
+ }
1484
+ return states;
1485
+ },
1102
1486
  setDashboardView: (value) => {
1103
1487
  dashboardView = value;
1104
1488
  },
@@ -1164,6 +1548,21 @@ async function runChat(projectRoot) {
1164
1548
  appendStreamDelta,
1165
1549
  finalizeStream,
1166
1550
  hasStream: (publisher) => streamTracker.hasStream(publisher),
1551
+ setTransientAgentState: (agentId, state) => {
1552
+ if (!agentId || !state) return;
1553
+ transientAgentStateMap.set(agentId, state);
1554
+ },
1555
+ clearTransientAgentState: (agentId) => {
1556
+ if (!agentId) return;
1557
+ transientAgentStateMap.delete(agentId);
1558
+ },
1559
+ refreshDashboard: () => {
1560
+ if (getCurrentView() === "agent") {
1561
+ renderAgentDashboard();
1562
+ return;
1563
+ }
1564
+ renderDashboard();
1565
+ },
1167
1566
  });
1168
1567
 
1169
1568
  daemonCoordinator = createDaemonCoordinator({
@@ -1180,8 +1579,8 @@ async function runChat(projectRoot) {
1180
1579
  const connected = await daemonCoordinator.connect();
1181
1580
  if (!connected) {
1182
1581
  // Check if daemon failed to start
1183
- if (!isRunning(projectRoot)) {
1184
- const logFile = getUfooPaths(projectRoot).ufooDaemonLog;
1582
+ if (!isRunning(activeProjectRoot)) {
1583
+ const logFile = getUfooPaths(activeProjectRoot).ufooDaemonLog;
1185
1584
  // eslint-disable-next-line no-console
1186
1585
  console.error("Failed to start ufoo daemon. Check logs at:", logFile);
1187
1586
  throw new Error("Daemon failed to start. Check the daemon log for details.");
@@ -1189,6 +1588,237 @@ async function runChat(projectRoot) {
1189
1588
  throw new Error("Failed to connect to ufoo daemon (timeout). The daemon may still be starting.");
1190
1589
  }
1191
1590
 
1591
+ function resolveProjectSwitchTarget(rawTarget) {
1592
+ const target = String(rawTarget || "").trim();
1593
+ if (!target) {
1594
+ throw new Error("missing target");
1595
+ }
1596
+ if (/^\d+$/.test(target)) {
1597
+ const index = Number.parseInt(target, 10);
1598
+ if (!Number.isFinite(index) || index <= 0) {
1599
+ throw new Error("invalid project index");
1600
+ }
1601
+ const rows = listProjectRuntimes({ validate: true, cleanupTmp: true });
1602
+ const item = rows[index - 1];
1603
+ if (!item || !item.project_root) {
1604
+ throw new Error("project index out of range");
1605
+ }
1606
+ return {
1607
+ projectRoot: canonicalProjectRoot(item.project_root),
1608
+ source: `index ${index}`,
1609
+ };
1610
+ }
1611
+ return {
1612
+ projectRoot: canonicalProjectRoot(target),
1613
+ source: target,
1614
+ };
1615
+ }
1616
+
1617
+ async function switchProjectConnection(targetInput) {
1618
+ let targetInfo;
1619
+ try {
1620
+ targetInfo = resolveProjectSwitchTarget(targetInput);
1621
+ } catch (err) {
1622
+ return {
1623
+ ok: false,
1624
+ error: err && err.message ? err.message : "invalid project target",
1625
+ };
1626
+ }
1627
+ const nextProjectRoot = targetInfo.projectRoot;
1628
+ if (!nextProjectRoot) {
1629
+ return { ok: false, error: "invalid project target" };
1630
+ }
1631
+ if (nextProjectRoot === activeProjectRoot) {
1632
+ return { ok: true, project_root: activeProjectRoot, unchanged: true };
1633
+ }
1634
+ const outgoingDraftSnapshot = captureCurrentProjectDraft();
1635
+
1636
+ try {
1637
+ const nextPaths = getUfooPaths(nextProjectRoot);
1638
+ if (!fs.existsSync(nextPaths.ufooDir)) {
1639
+ const repoRoot = path.join(__dirname, "..", "..");
1640
+ const init = new UfooInit(repoRoot);
1641
+ await init.init({ modules: "context,bus", project: nextProjectRoot });
1642
+ }
1643
+ if (!isRunning(nextProjectRoot)) {
1644
+ startDaemon(nextProjectRoot);
1645
+ }
1646
+ const result = await daemonCoordinator.switchProject({
1647
+ projectRoot: nextProjectRoot,
1648
+ sockPath: socketPath(nextProjectRoot),
1649
+ });
1650
+ if (!result || result.ok !== true) {
1651
+ return {
1652
+ ok: false,
1653
+ error: (result && result.error) || "switch failed",
1654
+ };
1655
+ }
1656
+ const previousProjectRoot = activeProjectRoot;
1657
+ if (previousProjectRoot && previousProjectRoot !== nextProjectRoot) {
1658
+ setProjectDraft(previousProjectRoot, outgoingDraftSnapshot);
1659
+ }
1660
+ activeProjectRoot = nextProjectRoot;
1661
+ applyProjectHistoryContext(nextProjectRoot);
1662
+ if (globalMode) {
1663
+ refreshProjectRuntimes();
1664
+ syncSelectedProjectToActive();
1665
+ renderDashboard();
1666
+ screen.render();
1667
+ }
1668
+ return {
1669
+ ok: true,
1670
+ project_root: activeProjectRoot,
1671
+ };
1672
+ } catch (err) {
1673
+ return {
1674
+ ok: false,
1675
+ error: err && err.message ? err.message : "switch failed",
1676
+ };
1677
+ }
1678
+ }
1679
+
1680
+ let projectSwitching = false;
1681
+ let pendingProjectSwitchRoot = null;
1682
+ let projectSwitchDebounceTimer = null;
1683
+ let projectSwitchFlushPromise = null;
1684
+ const PROJECT_SWITCH_DEBOUNCE_MS = 200;
1685
+
1686
+ function cancelProjectSwitchDebounce() {
1687
+ if (!projectSwitchDebounceTimer) return;
1688
+ clearTimeout(projectSwitchDebounceTimer);
1689
+ projectSwitchDebounceTimer = null;
1690
+ }
1691
+
1692
+ function scheduleProjectSwitchFlush(delayMs = PROJECT_SWITCH_DEBOUNCE_MS) {
1693
+ cancelProjectSwitchDebounce();
1694
+ projectSwitchDebounceTimer = setTimeout(() => {
1695
+ projectSwitchDebounceTimer = null;
1696
+ flushPendingProjectSwitch().catch((err) => {
1697
+ const message = err && err.message ? err.message : String(err || "switch failed");
1698
+ logMessage("error", `{white-fg}✗{/white-fg} Switch failed: ${escapeBlessed(message)}`);
1699
+ });
1700
+ }, Math.max(0, Number.isFinite(delayMs) ? delayMs : PROJECT_SWITCH_DEBOUNCE_MS));
1701
+ }
1702
+
1703
+ async function flushPendingProjectSwitch() {
1704
+ if (projectSwitchFlushPromise) {
1705
+ return projectSwitchFlushPromise;
1706
+ }
1707
+ projectSwitchFlushPromise = (async () => {
1708
+ projectSwitching = true;
1709
+ let lastResult = { ok: true, project_root: activeProjectRoot, unchanged: true };
1710
+ try {
1711
+ while (pendingProjectSwitchRoot) {
1712
+ const nextProjectRoot = pendingProjectSwitchRoot;
1713
+ pendingProjectSwitchRoot = null;
1714
+ if (!nextProjectRoot || nextProjectRoot === activeProjectRoot) continue;
1715
+ const result = await switchProjectConnection(nextProjectRoot);
1716
+ lastResult = result || { ok: false, error: "switch failed" };
1717
+ if (!result || result.ok !== true) {
1718
+ const reason = (result && result.error) || "switch failed";
1719
+ logMessage("error", `{white-fg}✗{/white-fg} Switch failed: ${escapeBlessed(reason)}`);
1720
+ }
1721
+ }
1722
+ return lastResult;
1723
+ } finally {
1724
+ projectSwitching = false;
1725
+ if (globalMode) {
1726
+ refreshProjectRuntimes();
1727
+ syncSelectedProjectToActive();
1728
+ renderDashboard();
1729
+ screen.render();
1730
+ }
1731
+ }
1732
+ })();
1733
+ try {
1734
+ return await projectSwitchFlushPromise;
1735
+ } finally {
1736
+ projectSwitchFlushPromise = null;
1737
+ if (pendingProjectSwitchRoot && !projectSwitchDebounceTimer) {
1738
+ scheduleProjectSwitchFlush(0);
1739
+ }
1740
+ }
1741
+ }
1742
+
1743
+ function requestProjectSwitchByIndex(index) {
1744
+ if (!globalMode) return;
1745
+ const numericIndex = Number(index);
1746
+ const nextIndex = Number.isFinite(numericIndex) ? Math.trunc(numericIndex) : Number.NaN;
1747
+ if (!Number.isFinite(nextIndex) || nextIndex < 0 || nextIndex >= projectRuntimes.length) {
1748
+ return;
1749
+ }
1750
+ selectedProjectIndex = nextIndex;
1751
+ const selected = projectRuntimes[nextIndex] || {};
1752
+ const nextProjectRoot = resolveRuntimeProjectRoot(selected);
1753
+ renderDashboard();
1754
+ screen.render();
1755
+ if (!nextProjectRoot) return;
1756
+ pendingProjectSwitchRoot = nextProjectRoot;
1757
+ scheduleProjectSwitchFlush();
1758
+ }
1759
+
1760
+ async function requestProjectSwitchByTarget(targetInput) {
1761
+ let targetInfo;
1762
+ try {
1763
+ targetInfo = resolveProjectSwitchTarget(targetInput);
1764
+ } catch (err) {
1765
+ return {
1766
+ ok: false,
1767
+ error: err && err.message ? err.message : "invalid project target",
1768
+ };
1769
+ }
1770
+ const nextProjectRoot = targetInfo && targetInfo.projectRoot ? targetInfo.projectRoot : "";
1771
+ if (!nextProjectRoot) {
1772
+ return { ok: false, error: "invalid project target" };
1773
+ }
1774
+ if (nextProjectRoot === activeProjectRoot) {
1775
+ return { ok: true, project_root: activeProjectRoot, unchanged: true };
1776
+ }
1777
+
1778
+ pendingProjectSwitchRoot = nextProjectRoot;
1779
+ cancelProjectSwitchDebounce();
1780
+
1781
+ let attempts = 0;
1782
+ while (attempts < 4) {
1783
+ attempts += 1;
1784
+ const result = await flushPendingProjectSwitch();
1785
+ if (activeProjectRoot === nextProjectRoot) {
1786
+ return { ok: true, project_root: activeProjectRoot };
1787
+ }
1788
+ if (!pendingProjectSwitchRoot) {
1789
+ if (result && result.ok !== true) return result;
1790
+ return { ok: false, error: "switch failed" };
1791
+ }
1792
+ if (pendingProjectSwitchRoot !== nextProjectRoot) {
1793
+ pendingProjectSwitchRoot = nextProjectRoot;
1794
+ }
1795
+ }
1796
+ return { ok: false, error: "switch did not complete" };
1797
+ }
1798
+
1799
+ const projectCloseController = createProjectCloseController({
1800
+ getProjects: () => projectRuntimes,
1801
+ getActiveProjectRoot: () => activeProjectRoot,
1802
+ resolveProjectRoot: resolveRuntimeProjectRoot,
1803
+ isRunning,
1804
+ stopDaemon,
1805
+ switchProject: (targetProjectRoot) => requestProjectSwitchByTarget(targetProjectRoot),
1806
+ refreshProjects: () => {
1807
+ if (!globalMode) return;
1808
+ refreshProjectRuntimes();
1809
+ syncSelectedProjectToActive();
1810
+ },
1811
+ renderDashboard,
1812
+ renderScreen: () => screen.render(),
1813
+ logMessage,
1814
+ escapeBlessed,
1815
+ });
1816
+
1817
+ function requestCloseProjectByIndex(index) {
1818
+ if (!globalMode) return;
1819
+ void projectCloseController.requestCloseProject(index);
1820
+ }
1821
+
1192
1822
  const commandExecutor = createCommandExecutor({
1193
1823
  projectRoot,
1194
1824
  parseCommand,
@@ -1211,9 +1841,15 @@ async function runChat(projectRoot) {
1211
1841
  });
1212
1842
  },
1213
1843
  activateAgent: async (target) => {
1214
- const activator = new AgentActivator(projectRoot);
1844
+ const activator = new AgentActivator(activeProjectRoot);
1215
1845
  await activator.activate(target);
1216
1846
  },
1847
+ listProjects: () => listProjectRuntimes({ validate: true, cleanupTmp: true }),
1848
+ getCurrentProject: () => ({
1849
+ project_root: activeProjectRoot,
1850
+ project_name: path.basename(activeProjectRoot),
1851
+ }),
1852
+ switchProject: async ({ target } = {}) => requestProjectSwitchByTarget(target),
1217
1853
  });
1218
1854
 
1219
1855
  async function executeCommand(text) {
@@ -1246,7 +1882,7 @@ async function runChat(projectRoot) {
1246
1882
  },
1247
1883
  enterAgentView,
1248
1884
  activateAgent: async (agentId) => {
1249
- const activator = new AgentActivator(projectRoot);
1885
+ const activator = new AgentActivator(activeProjectRoot);
1250
1886
  await activator.activate(agentId);
1251
1887
  },
1252
1888
  getInjectSockPath,
@@ -1347,6 +1983,12 @@ async function runChat(projectRoot) {
1347
1983
  }
1348
1984
  loadHistory();
1349
1985
  loadInputHistory();
1986
+ if (globalMode) {
1987
+ inputHistoryController.restoreDraft(getProjectDraft(activeProjectRoot));
1988
+ }
1989
+ if (globalMode) {
1990
+ refreshProjectRuntimes();
1991
+ }
1350
1992
  renderDashboard();
1351
1993
  resizeInput();
1352
1994
  requestStatus();
@@ -1356,7 +1998,7 @@ async function runChat(projectRoot) {
1356
1998
  if (daemonCoordinator && daemonCoordinator.isConnected()) {
1357
1999
  requestStatus();
1358
2000
  }
1359
- }, 30000);
2001
+ }, 5000);
1360
2002
  screen.on("resize", () => {
1361
2003
  if (handleResizeInAgentView()) {
1362
2004
  return;