u-foo 3.0.10 → 3.0.12

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 (70) hide show
  1. package/README.md +25 -9
  2. package/README.zh-CN.md +22 -9
  3. package/SKILLS/ubus/SKILL.md +15 -9
  4. package/SKILLS/ufoo/SKILL.md +20 -6
  5. package/dist/tui/darwin-arm64/ufoo-tui +0 -0
  6. package/dist/tui/darwin-x64/ufoo-tui +0 -0
  7. package/dist/tui/linux-arm64/ufoo-tui +0 -0
  8. package/dist/tui/linux-x64/ufoo-tui +0 -0
  9. package/package.json +11 -3
  10. package/scripts/pack-tui.js +112 -0
  11. package/scripts/postinstall.js +11 -0
  12. package/src/agents/activity/activityReconcile.js +106 -0
  13. package/src/agents/activity/activityStatePublisher.js +31 -2
  14. package/src/agents/activity/index.js +1 -0
  15. package/src/agents/launch/launcher.js +19 -0
  16. package/src/agents/launch/ptyRunner.js +20 -1
  17. package/src/agents/prompts/groupBootstrap.js +4 -0
  18. package/src/agents/prompts/native/ufoo.js +2 -0
  19. package/src/app/chat/ChatController.js +433 -0
  20. package/src/app/chat/agentDirectory.js +63 -0
  21. package/src/app/chat/agentEnter.js +70 -0
  22. package/src/app/chat/agentIdentity.js +50 -0
  23. package/src/app/chat/bootstrap.js +66 -0
  24. package/src/app/chat/commandExecutor.js +108 -0
  25. package/src/app/chat/commands.js +31 -0
  26. package/src/app/chat/dashboardView.js +6 -2
  27. package/src/app/chat/historyStore.js +181 -0
  28. package/src/app/chat/index.js +14 -2
  29. package/src/app/chat/inputSubmitHandler.js +21 -7
  30. package/src/app/chat/ipcBuilders.js +52 -0
  31. package/src/app/chat/multiWindow/paneManager.js +10 -1
  32. package/src/app/chat/multiWindow/renderer.js +1 -1
  33. package/src/app/chat/multiWindow/vtFrame.js +93 -0
  34. package/src/app/chat/streamState.js +182 -0
  35. package/src/app/cli/features/doctor.js +22 -0
  36. package/src/code/UCODE_PROMPT.md +2 -0
  37. package/src/code/UcodeController.js +156 -0
  38. package/src/code/context/planGraphService.js +4 -0
  39. package/src/code/repl.js +4 -3
  40. package/src/code/runtime/taskLoop.js +46 -50
  41. package/src/code/tui.js +13 -2
  42. package/src/code/ucodeSlashDispatch.js +241 -0
  43. package/src/coordination/bus/activate.js +3 -0
  44. package/src/runtime/contracts/schemas/ufoo-ui-v1/envelope.json +28 -0
  45. package/src/runtime/contracts/uiProtocol.js +190 -0
  46. package/src/ui/{ink/chatLogModel.js → chatLogModel.js} +2 -2
  47. package/src/ui/dashboardBridge.js +81 -0
  48. package/src/ui/format/index.js +2 -2
  49. package/src/ui/index.js +8 -4
  50. package/src/ui/multiPaneBusMirror.js +137 -0
  51. package/src/ui/multiWindowHandoff.js +232 -0
  52. package/src/ui/ptyHandoff.js +23 -0
  53. package/src/ui/rustChatHost.js +1520 -0
  54. package/src/ui/rustMultiSession.js +497 -0
  55. package/src/ui/rustUcodeHost.js +999 -0
  56. package/src/ui/scrollbackReplay.js +82 -0
  57. package/src/ui/settingsBridge.js +49 -0
  58. package/src/ui/toolMergeBridge.js +66 -0
  59. package/src/ui/tuiLauncher.js +105 -0
  60. package/src/ui/ucodeStatusLine.js +74 -0
  61. package/src/ui/uiHostServer.js +339 -0
  62. package/src/ui/MIGRATION.md +0 -334
  63. package/src/ui/ink/ChatApp.js +0 -4163
  64. package/src/ui/ink/DashboardBar.js +0 -691
  65. package/src/ui/ink/InkDemo.js +0 -96
  66. package/src/ui/ink/MultilineInput.js +0 -662
  67. package/src/ui/ink/UcodeApp.js +0 -1675
  68. package/src/ui/ink/agentMirror.js +0 -730
  69. package/src/ui/ink/chatReducer.js +0 -473
  70. package/src/ui/runInk.js +0 -66
@@ -7,6 +7,7 @@ const { PTY_SOCKET_MESSAGE_TYPES, PTY_SOCKET_SUBSCRIBE_MODES } = require("../../
7
7
  const { getUfooPaths } = require("../../coordination/state/paths");
8
8
  const { ActivityDetector } = require("../activity/activityDetector");
9
9
  const { createActivityStatePublisher } = require("../activity/activityStatePublisher");
10
+ const { reconcileDetectorOwnedActivity } = require("../activity/activityReconcile");
10
11
  const {
11
12
  parseStreamEnvelope,
12
13
  shouldAutoReplyFromPtyToPublisher,
@@ -734,7 +735,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
734
735
  activityPublisher.publish(snap.state, {
735
736
  since: snap.since,
736
737
  detail: snap.detail,
737
- });
738
+ }, { reconcile: true, force: true });
738
739
  }
739
740
 
740
741
  activityDetector.onChange((newState, oldState) => {
@@ -766,6 +767,23 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
766
767
  // instead of waiting for the next 30s heartbeat tick.
767
768
  writeActivityState();
768
769
 
770
+ // Heal daemon inject-stamp desync (disk working, detector idle/ready).
771
+ const activityReconcileTimer = setInterval(() => {
772
+ try {
773
+ reconcileDetectorOwnedActivity({
774
+ detector: activityDetector,
775
+ publisher: activityPublisher,
776
+ agentsFile: agentsFilePath,
777
+ subscriber,
778
+ });
779
+ } catch {
780
+ // reconcile must never break the runner
781
+ }
782
+ }, 2000);
783
+ if (typeof activityReconcileTimer.unref === "function") {
784
+ activityReconcileTimer.unref();
785
+ }
786
+
769
787
  function attachPty(proc) {
770
788
  proc.onData((data) => {
771
789
  const raw = String(data || "");
@@ -932,6 +950,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
932
950
 
933
951
  const stop = () => {
934
952
  running = false;
953
+ clearInterval(activityReconcileTimer);
935
954
  cleanupInjectServer(injectServer);
936
955
  if (process.stdin && outerInputHandler) {
937
956
  if (typeof process.stdin.off === "function") {
@@ -40,6 +40,10 @@ const SHARED_UFOO_PROTOCOL = [
40
40
  "",
41
41
  "`ufoo bus send <target> \"<message>\"`",
42
42
  "",
43
+ "After sending, do not poll `ufoo bus check`, invoke `/ubus`, sleep, or wait",
44
+ "for a reply. Continue the current task; any follow-up message will be",
45
+ "automatically injected into your prompt/session.",
46
+ "",
43
47
  "On received bus work: execute it, then `ufoo bus ack \"$UFOO_SUBSCRIBER_ID\"`.",
44
48
  "Reply only if you have a concrete result, answer, or follow-up the sender",
45
49
  "needs. Default is ack-only; silence is a valid response.",
@@ -9,11 +9,13 @@ Participate in multi-agent coordination through the ufoo bus/context system:
9
9
  - Use shared memory for durable project facts. Read existing memory before writing new memory; do not use it for transient task state.
10
10
  - Support launch/close/resume/inject flows managed by ufoo daemon.
11
11
  - Prefer canonical ufoo commands (\`ufoo ctx\`, \`ufoo bus\`, \`ufoo memory\`, \`ufoo report\`) for coordination and status sync.
12
+ - After sending a bus message, do not poll \`ufoo bus check\`, invoke \`/ubus\`, sleep, or wait for a reply. Continue the current task; any follow-up message will be automatically injected into your prompt/session.
12
13
 
13
14
  Execution protocol:
14
15
  - On session start, check context quickly:
15
16
  \`ufoo ctx decisions -l\`
16
17
  \`ufoo ctx decisions -n 1\`
18
+ - If \`ubus\` is explicitly requested, execute its pending-message flow immediately; this does not change the no-polling rule after you send a message.
17
19
  - After handling work that arrived from chat (\`[manual]<to:...>\`) or bus (\`[ufoo]<from:...>\`), report lifecycle:
18
20
  \`ufoo report start|progress|done|error "<short summary>"\`
19
21
  Do not emulate report failures with \`ufoo bus send ufoo-agent ...\`; if \`ufoo report\` fails, continue without a fallback bus report.
@@ -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
+ };