u-foo 3.0.10 → 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.
- package/README.md +25 -9
- package/README.zh-CN.md +22 -9
- package/dist/tui/darwin-arm64/ufoo-tui +0 -0
- package/dist/tui/darwin-x64/ufoo-tui +0 -0
- package/dist/tui/linux-arm64/ufoo-tui +0 -0
- package/dist/tui/linux-x64/ufoo-tui +0 -0
- package/package.json +11 -3
- package/scripts/pack-tui.js +112 -0
- package/scripts/postinstall.js +11 -0
- package/src/agents/activity/activityReconcile.js +106 -0
- package/src/agents/activity/activityStatePublisher.js +31 -2
- package/src/agents/activity/index.js +1 -0
- package/src/agents/launch/launcher.js +19 -0
- package/src/agents/launch/ptyRunner.js +20 -1
- package/src/app/chat/ChatController.js +433 -0
- package/src/app/chat/agentDirectory.js +63 -0
- package/src/app/chat/agentEnter.js +70 -0
- package/src/app/chat/agentIdentity.js +50 -0
- package/src/app/chat/bootstrap.js +66 -0
- package/src/app/chat/commandExecutor.js +108 -0
- package/src/app/chat/commands.js +31 -0
- package/src/app/chat/dashboardView.js +6 -2
- package/src/app/chat/historyStore.js +181 -0
- package/src/app/chat/index.js +14 -2
- package/src/app/chat/inputSubmitHandler.js +21 -7
- package/src/app/chat/ipcBuilders.js +52 -0
- package/src/app/chat/multiWindow/paneManager.js +10 -1
- package/src/app/chat/multiWindow/renderer.js +1 -1
- package/src/app/chat/multiWindow/vtFrame.js +93 -0
- package/src/app/chat/streamState.js +182 -0
- package/src/app/cli/features/doctor.js +22 -0
- package/src/code/UcodeController.js +156 -0
- package/src/code/context/planGraphService.js +4 -0
- package/src/code/repl.js +4 -3
- package/src/code/runtime/taskLoop.js +46 -50
- package/src/code/tui.js +13 -2
- package/src/code/ucodeSlashDispatch.js +241 -0
- package/src/coordination/bus/activate.js +3 -0
- package/src/runtime/contracts/schemas/ufoo-ui-v1/envelope.json +28 -0
- package/src/runtime/contracts/uiProtocol.js +190 -0
- package/src/ui/{ink/chatLogModel.js → chatLogModel.js} +2 -2
- package/src/ui/dashboardBridge.js +81 -0
- package/src/ui/format/index.js +2 -2
- package/src/ui/index.js +8 -4
- package/src/ui/multiPaneBusMirror.js +137 -0
- package/src/ui/multiWindowHandoff.js +232 -0
- package/src/ui/ptyHandoff.js +23 -0
- package/src/ui/rustChatHost.js +1520 -0
- package/src/ui/rustMultiSession.js +497 -0
- package/src/ui/rustUcodeHost.js +999 -0
- package/src/ui/scrollbackReplay.js +82 -0
- package/src/ui/settingsBridge.js +49 -0
- package/src/ui/toolMergeBridge.js +66 -0
- package/src/ui/tuiLauncher.js +105 -0
- package/src/ui/ucodeStatusLine.js +74 -0
- package/src/ui/uiHostServer.js +339 -0
- package/src/ui/MIGRATION.md +0 -334
- package/src/ui/ink/ChatApp.js +0 -4163
- package/src/ui/ink/DashboardBar.js +0 -691
- package/src/ui/ink/InkDemo.js +0 -96
- package/src/ui/ink/MultilineInput.js +0 -662
- package/src/ui/ink/UcodeApp.js +0 -1675
- package/src/ui/ink/agentMirror.js +0 -730
- package/src/ui/ink/chatReducer.js +0 -473
- 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
|
+
};
|