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.
- package/README.md +25 -9
- package/README.zh-CN.md +22 -9
- package/SKILLS/ubus/SKILL.md +15 -9
- package/SKILLS/ufoo/SKILL.md +20 -6
- 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/agents/prompts/groupBootstrap.js +4 -0
- package/src/agents/prompts/native/ufoo.js +2 -0
- 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/UCODE_PROMPT.md +2 -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
|
@@ -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
|
+
};
|