u-foo 3.0.9 → 3.0.11

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 (65) hide show
  1. package/README.md +25 -9
  2. package/README.zh-CN.md +22 -9
  3. package/dist/tui/darwin-arm64/ufoo-tui +0 -0
  4. package/dist/tui/darwin-x64/ufoo-tui +0 -0
  5. package/dist/tui/linux-arm64/ufoo-tui +0 -0
  6. package/dist/tui/linux-x64/ufoo-tui +0 -0
  7. package/package.json +12 -4
  8. package/scripts/pack-tui.js +112 -0
  9. package/scripts/postinstall.js +11 -0
  10. package/src/agents/activity/activityReconcile.js +106 -0
  11. package/src/agents/activity/activityStatePublisher.js +31 -2
  12. package/src/agents/activity/index.js +1 -0
  13. package/src/agents/launch/launcher.js +19 -0
  14. package/src/agents/launch/ptyRunner.js +20 -1
  15. package/src/app/chat/ChatController.js +433 -0
  16. package/src/app/chat/agentDirectory.js +63 -0
  17. package/src/app/chat/agentEnter.js +70 -0
  18. package/src/app/chat/agentIdentity.js +50 -0
  19. package/src/app/chat/bootstrap.js +66 -0
  20. package/src/app/chat/commandExecutor.js +108 -0
  21. package/src/app/chat/commands.js +38 -1
  22. package/src/app/chat/dashboardView.js +6 -2
  23. package/src/app/chat/historyStore.js +181 -0
  24. package/src/app/chat/index.js +14 -2
  25. package/src/app/chat/inputSubmitHandler.js +21 -7
  26. package/src/app/chat/ipcBuilders.js +52 -0
  27. package/src/app/chat/multiWindow/paneManager.js +10 -1
  28. package/src/app/chat/multiWindow/renderer.js +1 -1
  29. package/src/app/chat/multiWindow/vtFrame.js +93 -0
  30. package/src/app/chat/streamState.js +182 -0
  31. package/src/app/cli/features/doctor.js +22 -0
  32. package/src/code/UcodeController.js +156 -0
  33. package/src/code/context/planGraphService.js +4 -0
  34. package/src/code/repl.js +4 -3
  35. package/src/code/runtime/taskLoop.js +46 -50
  36. package/src/code/tui.js +13 -2
  37. package/src/code/ucodeSlashDispatch.js +241 -0
  38. package/src/coordination/bus/activate.js +3 -0
  39. package/src/runtime/contracts/schemas/ufoo-ui-v1/envelope.json +28 -0
  40. package/src/runtime/contracts/uiProtocol.js +190 -0
  41. package/src/ui/{ink/chatLogModel.js → chatLogModel.js} +2 -2
  42. package/src/ui/dashboardBridge.js +81 -0
  43. package/src/ui/format/index.js +2 -2
  44. package/src/ui/index.js +8 -4
  45. package/src/ui/multiPaneBusMirror.js +137 -0
  46. package/src/ui/multiWindowHandoff.js +232 -0
  47. package/src/ui/ptyHandoff.js +23 -0
  48. package/src/ui/rustChatHost.js +1520 -0
  49. package/src/ui/rustMultiSession.js +497 -0
  50. package/src/ui/rustUcodeHost.js +999 -0
  51. package/src/ui/scrollbackReplay.js +82 -0
  52. package/src/ui/settingsBridge.js +49 -0
  53. package/src/ui/toolMergeBridge.js +66 -0
  54. package/src/ui/tuiLauncher.js +105 -0
  55. package/src/ui/ucodeStatusLine.js +74 -0
  56. package/src/ui/uiHostServer.js +339 -0
  57. package/src/ui/MIGRATION.md +0 -334
  58. package/src/ui/ink/ChatApp.js +0 -4152
  59. package/src/ui/ink/DashboardBar.js +0 -691
  60. package/src/ui/ink/InkDemo.js +0 -96
  61. package/src/ui/ink/MultilineInput.js +0 -662
  62. package/src/ui/ink/UcodeApp.js +0 -1675
  63. package/src/ui/ink/agentMirror.js +0 -730
  64. package/src/ui/ink/chatReducer.js +0 -473
  65. package/src/ui/runInk.js +0 -66
@@ -0,0 +1,433 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Headless ChatController — composition root for Ink + Rust chat adapters.
5
+ *
6
+ * Owns stream state, STATUS→agents snapshot, slash/submit wiring ports.
7
+ * View adapters attach via `ports` (dispatch / publish / log / setStatus).
8
+ */
9
+
10
+ const { createThrottledSender, createChatStreamState } = require("./streamState");
11
+ const {
12
+ loadChatHistory,
13
+ loadInputHistory,
14
+ appendChatHistory,
15
+ appendInputHistory,
16
+ chatHistoryOptionsForScope,
17
+ chatHistoryFilePath,
18
+ } = require("./historyStore");
19
+ const { bootstrapEnvironment, ensureSubscriberId } = require("./bootstrap");
20
+ const {
21
+ buildAgentMaps,
22
+ resolveAgentId,
23
+ normalizeStatusToAgentsSnapshot,
24
+ toInkAgentsDispatchList,
25
+ } = require("./agentDirectory");
26
+ const { buildPromptIpcRequest, buildDirectBusSendRequest } = require("./ipcBuilders");
27
+ const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
28
+
29
+ function createChatController({
30
+ projectRoot = process.cwd(),
31
+ globalMode = false,
32
+ ports = {},
33
+ } = {}) {
34
+ const view = {
35
+ dispatch: typeof ports.dispatch === "function" ? ports.dispatch : () => {},
36
+ logMessage: typeof ports.logMessage === "function" ? ports.logMessage : () => {},
37
+ setStatus: typeof ports.setStatus === "function" ? ports.setStatus : () => {},
38
+ publish: typeof ports.publish === "function" ? ports.publish : () => {},
39
+ getState: typeof ports.getState === "function" ? ports.getState : () => ({}),
40
+ mapAgentsForDispatch: typeof ports.mapAgentsForDispatch === "function"
41
+ ? ports.mapAgentsForDispatch
42
+ : (snapshot) => snapshot.agents,
43
+ appendHistory: typeof ports.appendHistory === "function"
44
+ ? ports.appendHistory
45
+ : (type, text, meta) => appendChatHistory(projectRoot, type, text, meta, { globalMode }),
46
+ displayNameForPublisher: typeof ports.displayNameForPublisher === "function"
47
+ ? ports.displayNameForPublisher
48
+ : (value) => value,
49
+ send: typeof ports.send === "function" ? ports.send : null,
50
+ };
51
+
52
+ let started = false;
53
+ let statusTimer = null;
54
+ let streamState = null;
55
+ let requestDaemonStatus = null;
56
+ let commandExecutor = null;
57
+ let submitHandler = null;
58
+ let daemonSend = typeof view.send === "function" ? view.send : () => {};
59
+
60
+ const session = {
61
+ agents: [],
62
+ metaMap: new Map(),
63
+ labelMap: new Map(),
64
+ footer: "",
65
+ cron: null,
66
+ loop: null,
67
+ targetAgent: null,
68
+ pending: null,
69
+ };
70
+
71
+ function setSend(fn) {
72
+ daemonSend = typeof fn === "function" ? fn : () => {};
73
+ }
74
+
75
+ function applyStatus(data = {}) {
76
+ const snapshot = normalizeStatusToAgentsSnapshot(data);
77
+ session.agents = snapshot.agents.map((row) => row.id || row.fullId);
78
+ session.metaMap = snapshot.metaMap;
79
+ session.labelMap = snapshot.labelMap;
80
+ session.footer = snapshot.footer;
81
+ session.cron = snapshot.cron;
82
+ session.loop = snapshot.loop;
83
+ view.publish("agents.snapshot", {
84
+ agents: snapshot.agents.map((row) => ({
85
+ id: row.id,
86
+ label: row.label,
87
+ activity_state: row.activity_state || "",
88
+ activity_detail: row.activity_detail || "",
89
+ })),
90
+ footer: snapshot.footer,
91
+ cron: snapshot.cron,
92
+ loop: snapshot.loop,
93
+ });
94
+ view.dispatch({ type: "agents/set", list: view.mapAgentsForDispatch(snapshot) });
95
+ return snapshot;
96
+ }
97
+
98
+ function patchAgentActivity(agentId, patch = {}) {
99
+ if (!agentId) return;
100
+ const existing = session.metaMap.get(agentId) || { id: agentId };
101
+ const next = { ...existing, ...patch };
102
+ session.metaMap.set(agentId, next);
103
+ const rebuilt = normalizeStatusToAgentsSnapshot({
104
+ active: session.agents,
105
+ active_meta: session.agents.map((id) => session.metaMap.get(id) || { id }),
106
+ });
107
+ session.footer = rebuilt.footer;
108
+ view.publish("agents.patch", {
109
+ agents: rebuilt.agents.map((row) => ({
110
+ id: row.id,
111
+ label: row.label,
112
+ activity_state: row.activity_state || "",
113
+ activity_detail: row.activity_detail || "",
114
+ })),
115
+ footer: rebuilt.footer,
116
+ });
117
+ }
118
+
119
+ function getAgentLabel(id) {
120
+ return session.labelMap.get(id) || (session.metaMap.get(id) || {}).nickname || id;
121
+ }
122
+
123
+ function ensureCommandExecutor() {
124
+ if (commandExecutor) return commandExecutor;
125
+ const { createCommandExecutor } = require("./commandExecutor");
126
+ const { parseCommand } = require("./commands");
127
+ const { startDaemon, stopDaemon } = require("./transport");
128
+ const AgentActivator = require("../../coordination/bus/activate");
129
+ const fs = require("fs");
130
+
131
+ commandExecutor = createCommandExecutor({
132
+ projectRoot,
133
+ getActiveProjectRoot: typeof ports.getActiveProjectRoot === "function"
134
+ ? ports.getActiveProjectRoot
135
+ : () => projectRoot,
136
+ parseCommand,
137
+ escapeBlessed: (value) => String(value == null ? "" : value),
138
+ logMessage: view.logMessage,
139
+ resolveStatusLine: (text) => view.setStatus(text),
140
+ renderScreen: () => {},
141
+ clearLog: typeof ports.clearLog === "function"
142
+ ? ports.clearLog
143
+ : () => {
144
+ try {
145
+ const root = typeof ports.getActiveProjectRoot === "function"
146
+ ? ports.getActiveProjectRoot()
147
+ : projectRoot;
148
+ const opts = typeof ports.getHistoryOptions === "function"
149
+ ? ports.getHistoryOptions()
150
+ : { globalMode };
151
+ const file = chatHistoryFilePath(root, opts);
152
+ if (file && fs.existsSync(file)) fs.writeFileSync(file, "");
153
+ } catch {
154
+ // ignore
155
+ }
156
+ view.publish("transcript.reset", {});
157
+ view.dispatch({ type: "log/clear" });
158
+ },
159
+ getActiveAgents: () => session.agents.slice(),
160
+ getActiveAgentMetaMap: () => session.metaMap,
161
+ getAgentLabel,
162
+ isDaemonRunning: typeof ports.isDaemonRunning === "function"
163
+ ? ports.isDaemonRunning
164
+ : () => true,
165
+ startDaemon: (root, options = {}) => startDaemon(root || projectRoot, options),
166
+ stopDaemon: (root, options = {}) => stopDaemon(root || projectRoot, options),
167
+ restartDaemon: typeof ports.restartDaemon === "function"
168
+ ? ports.restartDaemon
169
+ : async () => {},
170
+ send: (req) => daemonSend(req),
171
+ requestStatus: () => {
172
+ if (typeof requestDaemonStatus === "function") requestDaemonStatus();
173
+ else daemonSend({ type: IPC_REQUEST_TYPES.STATUS });
174
+ },
175
+ requestCron: (payload = {}) => daemonSend({ type: IPC_REQUEST_TYPES.CRON, ...payload }),
176
+ activateAgent: async (target) => {
177
+ const activator = new AgentActivator(projectRoot);
178
+ await activator.activate(target);
179
+ },
180
+ globalMode: Boolean(globalMode),
181
+ listProjects: typeof ports.listProjects === "function"
182
+ ? ports.listProjects
183
+ : () => [],
184
+ getCurrentProject: typeof ports.getCurrentProject === "function"
185
+ ? ports.getCurrentProject
186
+ : () => ({ project_root: projectRoot }),
187
+ switchProject: typeof ports.switchProject === "function"
188
+ ? ports.switchProject
189
+ : async () => ({ ok: false, error: "project switching unavailable" }),
190
+ toggleMultiWindow: typeof ports.toggleMultiWindow === "function"
191
+ ? ports.toggleMultiWindow
192
+ : () => {
193
+ view.logMessage("system", "Multi-window unavailable in this host.");
194
+ },
195
+ applyChatSettings: typeof ports.applyChatSettings === "function"
196
+ ? ports.applyChatSettings
197
+ : null,
198
+ });
199
+ return commandExecutor;
200
+ }
201
+
202
+ function ensureSubmitHandler() {
203
+ if (submitHandler) return submitHandler;
204
+ const { createInputSubmitHandler } = require("./inputSubmitHandler");
205
+ const { parseAtTarget } = require("./commands");
206
+ const submitState = {};
207
+ Object.defineProperties(submitState, {
208
+ targetAgent: {
209
+ get: () => session.targetAgent,
210
+ set: (next) => {
211
+ session.targetAgent = next || null;
212
+ },
213
+ },
214
+ pending: {
215
+ get: () => session.pending,
216
+ set: (next) => {
217
+ session.pending = next || null;
218
+ },
219
+ },
220
+ activeAgentMetaMap: {
221
+ get: () => session.metaMap,
222
+ },
223
+ });
224
+ submitHandler = createInputSubmitHandler({
225
+ state: submitState,
226
+ parseAtTarget,
227
+ resolveAgentId: (label) => resolveAgentId({
228
+ label,
229
+ activeAgents: session.agents,
230
+ labelMap: session.labelMap,
231
+ lookupNickname: (nickname) => {
232
+ for (const [id, meta] of session.metaMap.entries()) {
233
+ if (!meta) continue;
234
+ if (
235
+ meta.nickname === nickname
236
+ || meta.scoped_nickname === nickname
237
+ || meta.display_nickname === nickname
238
+ ) {
239
+ return id;
240
+ }
241
+ }
242
+ return null;
243
+ },
244
+ }),
245
+ executeCommand: async (text) => ensureCommandExecutor().executeCommand(text),
246
+ queueStatusLine: (text) => view.setStatus(text),
247
+ send: (req) => daemonSend(req),
248
+ logMessage: view.logMessage,
249
+ getAgentLabel,
250
+ escapeBlessed: (value) => String(value == null ? "" : value),
251
+ markPendingDelivery: (agentId) => {
252
+ if (streamState) streamState.markPendingDelivery(agentId, getAgentLabel(agentId));
253
+ },
254
+ clearTargetAgent: () => {
255
+ session.targetAgent = null;
256
+ view.publish("prompt.set_prefix", { prefix: "› " });
257
+ },
258
+ setTargetAgent: (agentId) => {
259
+ session.targetAgent = agentId || null;
260
+ if (!agentId) {
261
+ view.publish("prompt.set_prefix", { prefix: "› " });
262
+ return;
263
+ }
264
+ const label = getAgentLabel(agentId);
265
+ view.publish("prompt.set_prefix", { prefix: `›@${label} ` });
266
+ },
267
+ enterAgentView: (agentId, options = {}) => {
268
+ if (typeof ports.enterAgentView === "function") {
269
+ return ports.enterAgentView(agentId, options);
270
+ }
271
+ view.logMessage("system", "Agent enter unavailable in this host.");
272
+ },
273
+ getAgentAdapter: (agentId) => {
274
+ if (typeof ports.getAgentAdapter === "function") {
275
+ return ports.getAgentAdapter(agentId);
276
+ }
277
+ try {
278
+ const { createTerminalAdapterRouter } = require("../../runtime/terminal/adapterRouter");
279
+ const meta = session.metaMap.get(agentId) || {};
280
+ const launchMode = String(meta.launch_mode || meta.launchMode || "").trim();
281
+ return createTerminalAdapterRouter().getAdapter({ launchMode, agentId, meta });
282
+ } catch {
283
+ return null;
284
+ }
285
+ },
286
+ activateAgent: async (agentId) => {
287
+ if (typeof ports.activateAgent === "function") {
288
+ return ports.activateAgent(agentId);
289
+ }
290
+ const AgentActivator = require("../../coordination/bus/activate");
291
+ const root = typeof ports.getActiveProjectRoot === "function"
292
+ ? ports.getActiveProjectRoot()
293
+ : projectRoot;
294
+ const activator = new AgentActivator(root || projectRoot);
295
+ await activator.activate(agentId);
296
+ },
297
+ focusMultiPane: typeof ports.focusMultiPane === "function"
298
+ ? ports.focusMultiPane
299
+ : null,
300
+ commitInputHistory: (value) => {
301
+ const root = typeof ports.getActiveProjectRoot === "function"
302
+ ? ports.getActiveProjectRoot()
303
+ : projectRoot;
304
+ const opts = typeof ports.getHistoryOptions === "function"
305
+ ? ports.getHistoryOptions()
306
+ : { globalMode };
307
+ appendInputHistory(root || projectRoot, value, opts);
308
+ },
309
+ focusInput: () => {},
310
+ renderScreen: () => {},
311
+ getShellCwd: () => {
312
+ if (typeof ports.getActiveProjectRoot === "function") {
313
+ return ports.getActiveProjectRoot() || projectRoot;
314
+ }
315
+ return projectRoot;
316
+ },
317
+ });
318
+ return submitHandler;
319
+ }
320
+
321
+ async function submitInput(text) {
322
+ const handler = ensureSubmitHandler();
323
+ await handler.handleSubmit(text);
324
+ }
325
+
326
+ function start(options = {}) {
327
+ if (started) return;
328
+ started = true;
329
+ if (typeof options.send === "function") setSend(options.send);
330
+ streamState = createChatStreamState({
331
+ dispatch: view.dispatch,
332
+ appendHistory: view.appendHistory,
333
+ displayNameForPublisher: (publisher) => {
334
+ if (session.labelMap.has(publisher)) return session.labelMap.get(publisher);
335
+ return view.displayNameForPublisher(publisher);
336
+ },
337
+ });
338
+ const sendStatus = typeof options.sendStatus === "function"
339
+ ? options.sendStatus
340
+ : () => daemonSend({ type: IPC_REQUEST_TYPES.STATUS });
341
+ requestDaemonStatus = createThrottledSender(sendStatus, 500);
342
+ const intervalMs = Number(options.statusIntervalMs) > 0 ? Number(options.statusIntervalMs) : 3000;
343
+ statusTimer = setInterval(() => {
344
+ try {
345
+ requestDaemonStatus();
346
+ } catch {
347
+ // ignore poll errors
348
+ }
349
+ }, intervalMs);
350
+ if (typeof statusTimer.unref === "function") statusTimer.unref();
351
+ try {
352
+ requestDaemonStatus();
353
+ } catch {
354
+ // ignore
355
+ }
356
+ }
357
+
358
+ function stop() {
359
+ if (statusTimer) {
360
+ clearInterval(statusTimer);
361
+ statusTimer = null;
362
+ }
363
+ if (streamState && typeof streamState.flushDeltas === "function") {
364
+ streamState.flushDeltas();
365
+ }
366
+ started = false;
367
+ }
368
+
369
+ return {
370
+ projectRoot,
371
+ globalMode,
372
+ session,
373
+ bootstrapEnvironment: (root = projectRoot, options = {}) => bootstrapEnvironment(root, {
374
+ globalMode,
375
+ ...options,
376
+ }),
377
+ ensureSubscriberId: (root = projectRoot) => ensureSubscriberId(root),
378
+ loadChatHistory: (cap = 200, options = {}) => loadChatHistory(projectRoot, cap, {
379
+ globalMode,
380
+ ...options,
381
+ }),
382
+ loadInputHistory: (cap = 200, options = {}) => loadInputHistory(projectRoot, cap, {
383
+ globalMode,
384
+ ...options,
385
+ }),
386
+ appendChatHistory: (type, text, meta = {}, options = {}) => appendChatHistory(
387
+ projectRoot,
388
+ type,
389
+ text,
390
+ meta,
391
+ { globalMode, ...options },
392
+ ),
393
+ appendInputHistory: (value, options = {}) => appendInputHistory(projectRoot, value, {
394
+ globalMode,
395
+ ...options,
396
+ }),
397
+ chatHistoryOptionsForScope,
398
+ getStreamState: () => streamState,
399
+ getAgentsFooter: () => session.footer,
400
+ getAgentsSnapshot: () => ({
401
+ agents: session.agents.map((id) => {
402
+ const meta = session.metaMap.get(id) || {};
403
+ return {
404
+ id,
405
+ label: getAgentLabel(id),
406
+ activity_state: meta.activity_state || "",
407
+ activity_detail: meta.activity_detail || "",
408
+ };
409
+ }),
410
+ footer: session.footer,
411
+ cron: session.cron,
412
+ loop: session.loop,
413
+ }),
414
+ applyStatus,
415
+ patchAgentActivity,
416
+ setSend,
417
+ submitInput,
418
+ ensureCommandExecutor,
419
+ buildPromptIpcRequest,
420
+ buildDirectBusSendRequest,
421
+ buildAgentMaps,
422
+ requestDaemonStatus: () => {
423
+ if (typeof requestDaemonStatus === "function") requestDaemonStatus();
424
+ },
425
+ start,
426
+ stop,
427
+ };
428
+ }
429
+
430
+ module.exports = {
431
+ createChatController,
432
+ toInkAgentsDispatchList,
433
+ };
@@ -79,10 +79,73 @@ function clampAgentWindowWithSelection({
79
79
  return nextStart;
80
80
  }
81
81
 
82
+ /**
83
+ * Normalize daemon STATUS payload into agents list + footer string.
84
+ * Shared by Ink dashboard and Rust `agents.snapshot`.
85
+ */
86
+ function normalizeStatusToAgentsSnapshot(data = {}) {
87
+ const activeIds = Array.isArray(data.active) ? data.active : [];
88
+ const metaList = Array.isArray(data.active_meta) ? data.active_meta : [];
89
+ const { labelMap, metaMap } = buildAgentMaps(activeIds, metaList);
90
+ const agents = activeIds.map((id) => {
91
+ const meta = metaMap.get(id) || {};
92
+ const colon = String(id).indexOf(":");
93
+ const fallbackType = colon > 0 ? String(id).slice(0, colon) : String(id);
94
+ const fallbackId = colon > 0 ? String(id).slice(colon + 1) : "";
95
+ const label = labelMap.get(id) || id;
96
+ const activity = String(meta.activity_state || "").trim();
97
+ return {
98
+ ...meta,
99
+ id,
100
+ fullId: id,
101
+ label,
102
+ type: meta.type || fallbackType,
103
+ agentId: meta.id || fallbackId,
104
+ nickname: label,
105
+ activity_state: activity,
106
+ activity_detail: String(meta.activity_detail || ""),
107
+ launch_mode: meta.launch_mode || meta.launchMode || "",
108
+ };
109
+ });
110
+ const footer = agents.length === 0
111
+ ? "no agents"
112
+ : agents.map((agent) => {
113
+ const mark = agent.activity_state === "working" ? "*"
114
+ : agent.activity_state === "waiting_input" ? "?"
115
+ : agent.activity_state === "blocked" ? "!"
116
+ : "";
117
+ return mark ? `${mark}${agent.label}` : agent.label;
118
+ }).join(" · ");
119
+ return {
120
+ agents,
121
+ footer,
122
+ labelMap,
123
+ metaMap,
124
+ cron: data.cron || null,
125
+ loop: data.loop || null,
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Ink chatReducer expects `id` as the short agent id and `fullId` as the key.
131
+ * Shared so ChatController and ChatApp stay aligned.
132
+ */
133
+ function toInkAgentsDispatchList(snapshot = {}) {
134
+ const agents = Array.isArray(snapshot.agents) ? snapshot.agents : [];
135
+ return agents.map((row) => ({
136
+ ...row,
137
+ fullId: row.fullId || row.id,
138
+ id: row.agentId || row.id,
139
+ nickname: row.label || row.nickname,
140
+ }));
141
+ }
142
+
82
143
  module.exports = {
83
144
  buildAgentMaps,
84
145
  getAgentLabel,
85
146
  resolveAgentId,
86
147
  resolveAgentDisplayName,
87
148
  clampAgentWindowWithSelection,
149
+ normalizeStatusToAgentsSnapshot,
150
+ toInkAgentsDispatchList,
88
151
  };
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Agent enter / dashboard navigation helpers (Phase 0B extraction).
5
+ */
6
+
7
+ const path = require("path");
8
+
9
+ function resolveInjectSockPathForAgent(projectRoot, agentId) {
10
+ const { getUfooPaths } = require("../../coordination/state/paths");
11
+ const { subscriberToSafeName } = require("../../coordination/bus/utils");
12
+ const safeName = subscriberToSafeName(agentId);
13
+ return path.join(getUfooPaths(projectRoot || process.cwd()).busQueuesDir, safeName, "inject.sock");
14
+ }
15
+
16
+ function resolveAgentEnterRequest({
17
+ agentId,
18
+ projectRoot = "",
19
+ activeAgentMeta = new Map(),
20
+ settings = {},
21
+ } = {}) {
22
+ const id = String(agentId || "").trim();
23
+ if (!id) return null;
24
+
25
+ const metaMap = activeAgentMeta instanceof Map ? activeAgentMeta : new Map();
26
+ const meta = metaMap.get(id) || {};
27
+ const configuredLaunchMode = settings && settings.launchMode && settings.launchMode !== "auto"
28
+ ? settings.launchMode
29
+ : "";
30
+ const launchMode = String(meta.launch_mode || meta.launchMode || configuredLaunchMode || "").trim();
31
+ const { createTerminalAdapterRouter } = require("../../runtime/terminal/adapterRouter");
32
+ const adapter = createTerminalAdapterRouter().getAdapter({ launchMode, agentId: id, meta });
33
+ const caps = adapter && adapter.capabilities ? adapter.capabilities : {};
34
+
35
+ return {
36
+ agentId: id,
37
+ projectRoot: String(projectRoot || ""),
38
+ launchMode,
39
+ useBus: Boolean(caps.supportsInternalQueueLoop && !caps.supportsSocketProtocol),
40
+ supportsSocket: Boolean(caps.supportsSocketProtocol),
41
+ supportsInternalQueue: Boolean(caps.supportsInternalQueueLoop),
42
+ supportsActivate: Boolean(caps.supportsActivate),
43
+ };
44
+ }
45
+
46
+ function resolveDashboardAgentEnterAction(enterRequest = {}) {
47
+ if (!enterRequest || typeof enterRequest !== "object") return "none";
48
+ if (enterRequest.useBus) return "internal";
49
+ if (enterRequest.supportsActivate) return "activate";
50
+ // No PTY mirror fallback — host must expose activate (or use terminal/tmux/internal).
51
+ return "none";
52
+ }
53
+
54
+ function buildEmptyProjectsDownActions(state = {}, displayAgents = []) {
55
+ if (!state.emptyProjectsDownArmed) {
56
+ return [{ type: "projects/armEmptyDown" }];
57
+ }
58
+ const actions = [{ type: "view/set", view: "agents" }];
59
+ if (displayAgents.length > 0 && state.selectedAgentIndex < 0) {
60
+ actions.push({ type: "agents/select", index: 0 });
61
+ }
62
+ return actions;
63
+ }
64
+
65
+ module.exports = {
66
+ resolveInjectSockPathForAgent,
67
+ resolveAgentEnterRequest,
68
+ resolveDashboardAgentEnterAction,
69
+ buildEmptyProjectsDownActions,
70
+ };
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Agent label / id resolution for chat (Phase 0B extraction).
5
+ */
6
+
7
+ function getAgentLabelFor(meta, agentId) {
8
+ if (meta && meta.display_nickname) return meta.display_nickname;
9
+ if (meta && meta.nickname) return meta.nickname;
10
+ if (!agentId) return "";
11
+ const colon = agentId.indexOf(":");
12
+ if (colon < 0) return agentId;
13
+ const head = agentId.slice(0, colon);
14
+ const tail = agentId.slice(colon + 1).slice(0, 6);
15
+ return tail ? `${head}:${tail}` : head;
16
+ }
17
+
18
+ function buildActiveAgentLabelMap(activeAgents = [], activeAgentMeta = new Map()) {
19
+ const out = new Map();
20
+ const metaMap = activeAgentMeta instanceof Map ? activeAgentMeta : new Map();
21
+ for (const id of Array.isArray(activeAgents) ? activeAgents : []) {
22
+ out.set(id, getAgentLabelFor(metaMap.get(id), id));
23
+ }
24
+ return out;
25
+ }
26
+
27
+ function resolveActiveAgentId(label, activeAgents = [], activeAgentMeta = new Map()) {
28
+ const { resolveAgentId } = require("./agentDirectory");
29
+ const metaMap = activeAgentMeta instanceof Map ? activeAgentMeta : new Map();
30
+ return resolveAgentId({
31
+ label,
32
+ activeAgents: Array.isArray(activeAgents) ? activeAgents : [],
33
+ labelMap: buildActiveAgentLabelMap(activeAgents, metaMap),
34
+ lookupNickname: (nickname) => {
35
+ for (const [id, meta] of metaMap.entries()) {
36
+ if (!meta) continue;
37
+ if (meta.nickname === nickname || meta.scoped_nickname === nickname || meta.display_nickname === nickname) {
38
+ return id;
39
+ }
40
+ }
41
+ return null;
42
+ },
43
+ });
44
+ }
45
+
46
+ module.exports = {
47
+ getAgentLabelFor,
48
+ buildActiveAgentLabelMap,
49
+ resolveActiveAgentId,
50
+ };
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Chat bootstrap helpers (Phase 0B extraction from ChatApp).
5
+ * Headless — safe for ChatController and unit tests without Ink.
6
+ */
7
+
8
+ const path = require("path");
9
+ const fs = require("fs");
10
+ const crypto = require("crypto");
11
+
12
+ function bootstrapEnvironment(projectRoot, options = {}) {
13
+ const { canonicalProjectRoot } = require("../../runtime/projects");
14
+ const { getUfooPaths } = require("../../coordination/state/paths");
15
+ const UfooInit = require("../../app/cli/features/init");
16
+ const { isRunning } = require("../../runtime/daemon");
17
+ const { startDaemon } = require("../../app/chat/transport");
18
+
19
+ const globalMode = options && options.globalMode === true;
20
+ let activeProjectRoot = projectRoot;
21
+ try {
22
+ activeProjectRoot = canonicalProjectRoot(projectRoot);
23
+ } catch {
24
+ activeProjectRoot = path.resolve(projectRoot || process.cwd());
25
+ }
26
+
27
+ const runtimePaths = getUfooPaths(projectRoot);
28
+ const contextIndexFile = path.join(runtimePaths.ufooDir, "context", "decisions.jsonl");
29
+ const needsBootstrap = globalMode && (
30
+ !fs.existsSync(runtimePaths.ufooDir)
31
+ || !fs.existsSync(runtimePaths.busDir)
32
+ || !fs.existsSync(runtimePaths.agentDir)
33
+ || !fs.existsSync(contextIndexFile)
34
+ );
35
+
36
+ return {
37
+ activeProjectRoot,
38
+ globalMode,
39
+ runtimePaths,
40
+ needsBootstrap,
41
+ UfooInit,
42
+ isRunning,
43
+ startDaemon,
44
+ };
45
+ }
46
+
47
+ async function ensureSubscriberId(projectRoot) {
48
+ if (process.env.UFOO_SUBSCRIBER_ID) return;
49
+ const { getUfooPaths } = require("../../coordination/state/paths");
50
+ const sessionFile = path.join(getUfooPaths(projectRoot).ufooDir, "chat", "session-id.txt");
51
+ const sessionDir = path.dirname(sessionFile);
52
+ fs.mkdirSync(sessionDir, { recursive: true });
53
+ let sessionId;
54
+ if (fs.existsSync(sessionFile)) {
55
+ sessionId = fs.readFileSync(sessionFile, "utf8").trim();
56
+ } else {
57
+ sessionId = crypto.randomBytes(4).toString("hex");
58
+ fs.writeFileSync(sessionFile, sessionId, "utf8");
59
+ }
60
+ process.env.UFOO_SUBSCRIBER_ID = `claude-code:${sessionId}`;
61
+ }
62
+
63
+ module.exports = {
64
+ bootstrapEnvironment,
65
+ ensureSubscriberId,
66
+ };