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.
- 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 +12 -4
- 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 +38 -1
- 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 -4152
- 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,999 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Phase 4 Rust ucode host: Node owns runner/session; ufoo-tui owns TTY.
|
|
5
|
+
* Opt-in via UFOO_TUI=rust or UFOO_TUI_UCODE_RUST=1.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const os = require("os");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const { spawn } = require("child_process");
|
|
11
|
+
const { createUiHostServer, createAuthToken } = require("./uiHostServer");
|
|
12
|
+
const { resolveTuiLaunchPlan } = require("./tuiLauncher");
|
|
13
|
+
const { createToolMergePublisher } = require("./toolMergeBridge");
|
|
14
|
+
const {
|
|
15
|
+
createUcodeController,
|
|
16
|
+
createThinkingStatusPublisher,
|
|
17
|
+
} = require("../code/UcodeController");
|
|
18
|
+
const { createEnvelope, encodeMessage } = require("../runtime/contracts/uiProtocol");
|
|
19
|
+
const fmt = require("./format");
|
|
20
|
+
|
|
21
|
+
function stripTags(value) {
|
|
22
|
+
return String(value || "").replace(/\{[^}]+\}/g, "");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildUcodeAgentsSnapshot(workspaceRoot, selfSubscriberId = "") {
|
|
26
|
+
const list = fmt.filterSelectableAgents(
|
|
27
|
+
fmt.loadActiveAgents(workspaceRoot),
|
|
28
|
+
selfSubscriberId
|
|
29
|
+
);
|
|
30
|
+
const agents = list.map((agent) => {
|
|
31
|
+
const id = String((agent && (agent.fullId || agent.id)) || "").trim();
|
|
32
|
+
const label = String(
|
|
33
|
+
(agent && (agent.nickname || agent.fullId || agent.id)) || id
|
|
34
|
+
).trim() || id;
|
|
35
|
+
return {
|
|
36
|
+
id,
|
|
37
|
+
label,
|
|
38
|
+
activity_state: String((agent && agent.status) || ""),
|
|
39
|
+
};
|
|
40
|
+
}).filter((agent) => agent.id);
|
|
41
|
+
const labels = agents.map((agent) => agent.label).filter(Boolean);
|
|
42
|
+
const footer = labels.length === 0
|
|
43
|
+
? "Agents: none"
|
|
44
|
+
: labels.length <= 4
|
|
45
|
+
? `Agents: ${labels.join(" · ")}`
|
|
46
|
+
: `Agents: ${labels.slice(0, 3).join(" · ")} +${labels.length - 3}`;
|
|
47
|
+
return { agents, footer };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build ucode completion rows for the Rust TUI host.
|
|
52
|
+
* Passes live agents for `@` and optional remote `/model` catalog.
|
|
53
|
+
*/
|
|
54
|
+
async function buildUcodeCompletionItems({
|
|
55
|
+
text = "",
|
|
56
|
+
workspaceRoot = process.cwd(),
|
|
57
|
+
state = {},
|
|
58
|
+
selfSubscriberId = "",
|
|
59
|
+
remoteModels = [],
|
|
60
|
+
limit = 20,
|
|
61
|
+
} = {}) {
|
|
62
|
+
const { UCODE_COMMAND_REGISTRY, UCODE_COMMAND_TREE } = require("../code/commands");
|
|
63
|
+
const { listSessionSummaries } = require("../code/sessionStore");
|
|
64
|
+
const {
|
|
65
|
+
suggestUcodeModels,
|
|
66
|
+
suggestUcodeThinkingLevels,
|
|
67
|
+
} = require("../code/modelCommand");
|
|
68
|
+
let resumeSessions = [];
|
|
69
|
+
try {
|
|
70
|
+
resumeSessions = listSessionSummaries(workspaceRoot, { limit: 40 });
|
|
71
|
+
} catch {
|
|
72
|
+
resumeSessions = [];
|
|
73
|
+
}
|
|
74
|
+
const agentsSnap = buildUcodeAgentsSnapshot(workspaceRoot, selfSubscriberId);
|
|
75
|
+
const agents = agentsSnap.agents.map((agent) => agent.id);
|
|
76
|
+
const agentLabels = agentsSnap.agents.map((agent) => agent.label || agent.id);
|
|
77
|
+
return fmt.buildCompletions({
|
|
78
|
+
text: String(text || ""),
|
|
79
|
+
agents,
|
|
80
|
+
agentLabels,
|
|
81
|
+
commands: UCODE_COMMAND_REGISTRY,
|
|
82
|
+
commandTree: UCODE_COMMAND_TREE,
|
|
83
|
+
argumentLists: {
|
|
84
|
+
"/resume": resumeSessions,
|
|
85
|
+
"/model": suggestUcodeModels(state || {}, {
|
|
86
|
+
models: Array.isArray(remoteModels) ? remoteModels : [],
|
|
87
|
+
}),
|
|
88
|
+
"/model/thinking": suggestUcodeThinkingLevels(state || {}),
|
|
89
|
+
},
|
|
90
|
+
limit,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildPlanSetPayload(executionState, options = {}) {
|
|
95
|
+
try {
|
|
96
|
+
const { buildPlanUiProjection } = require("../code/context/planProjection");
|
|
97
|
+
const projection = buildPlanUiProjection(executionState, {
|
|
98
|
+
cols: Number(options.cols) > 0 ? Number(options.cols) : 80,
|
|
99
|
+
activityMessage: String(options.activityMessage || ""),
|
|
100
|
+
});
|
|
101
|
+
if (!projection || !projection.visible) {
|
|
102
|
+
return {
|
|
103
|
+
summary: "",
|
|
104
|
+
lines: [],
|
|
105
|
+
hash: projection && projection.hash || "",
|
|
106
|
+
visible: false,
|
|
107
|
+
idle_hint: String((projection && projection.idleHint) || ""),
|
|
108
|
+
status_line: "",
|
|
109
|
+
band_mode: String((projection && projection.bandMode) || ""),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
let lines = Array.isArray(projection.bandLines) ? projection.bandLines.slice() : [];
|
|
113
|
+
const md = String(projection.roadmapMarkdown || "").trim();
|
|
114
|
+
if (md) {
|
|
115
|
+
try {
|
|
116
|
+
const rendered = fmt.renderLogLinesWithMarkdownAnsi(md, { inCodeBlock: false });
|
|
117
|
+
if (Array.isArray(rendered) && rendered.length > 0) {
|
|
118
|
+
lines = rendered.map((line) => String(line || ""));
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
// keep bandLines
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const summary = String(
|
|
125
|
+
projection.statusLine
|
|
126
|
+
|| projection.activityStatusLine
|
|
127
|
+
|| lines[0]
|
|
128
|
+
|| ""
|
|
129
|
+
).trim();
|
|
130
|
+
return {
|
|
131
|
+
summary,
|
|
132
|
+
text: summary,
|
|
133
|
+
lines,
|
|
134
|
+
hash: projection.hash || "",
|
|
135
|
+
visible: true,
|
|
136
|
+
idle_hint: String(projection.idleHint || ""),
|
|
137
|
+
status_line: String(projection.statusLine || projection.activityStatusLine || ""),
|
|
138
|
+
activity_status_line: String(projection.activityStatusLine || ""),
|
|
139
|
+
band_mode: String(projection.bandMode || ""),
|
|
140
|
+
};
|
|
141
|
+
} catch {
|
|
142
|
+
return {
|
|
143
|
+
summary: "",
|
|
144
|
+
lines: [],
|
|
145
|
+
hash: "",
|
|
146
|
+
visible: false,
|
|
147
|
+
idle_hint: "",
|
|
148
|
+
status_line: "",
|
|
149
|
+
band_mode: "",
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function normalizeToolLogEntry(entry = {}) {
|
|
155
|
+
const tool = String(entry.tool || "").trim().toLowerCase();
|
|
156
|
+
if (!tool) return null;
|
|
157
|
+
const resObj = entry.result && typeof entry.result === "object" ? entry.result : {};
|
|
158
|
+
const phase = String(entry.phase || "").trim().toLowerCase();
|
|
159
|
+
const isError = phase === "error" || resObj.ok === false;
|
|
160
|
+
const detail = fmt.normalizeToolLogDetail(tool, entry.args, resObj);
|
|
161
|
+
const errorText = String(entry.error || resObj.error || "").trim();
|
|
162
|
+
return fmt.normalizeToolMergeEntry({ tool, detail, isError, errorText });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function runUcodeRust(props = {}) {
|
|
166
|
+
const plan = resolveTuiLaunchPlan({
|
|
167
|
+
mode: props.tuiMode || process.env.UFOO_TUI || "rust",
|
|
168
|
+
requireRust: true,
|
|
169
|
+
surface: "ucode",
|
|
170
|
+
});
|
|
171
|
+
if (plan.mode !== "rust" || !plan.binary) {
|
|
172
|
+
const err = new Error(`Rust TUI unavailable (${plan.reason || "unknown"})`);
|
|
173
|
+
err.code = "UFOO_TUI_UNAVAILABLE";
|
|
174
|
+
err.plan = plan;
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const workspaceRoot = props.workspaceRoot || process.cwd();
|
|
179
|
+
const uiSocketPath = path.join(
|
|
180
|
+
os.tmpdir(),
|
|
181
|
+
`ufoo-ui-ucode-${process.pid}-${Date.now()}.sock`
|
|
182
|
+
);
|
|
183
|
+
const uiAuthToken = createAuthToken();
|
|
184
|
+
|
|
185
|
+
let hostRef = null;
|
|
186
|
+
let entrySeq = 0;
|
|
187
|
+
let autoBusTimer = null;
|
|
188
|
+
let agentsTimer = null;
|
|
189
|
+
let autoBusQueued = false;
|
|
190
|
+
let autoBusError = "";
|
|
191
|
+
let closing = false;
|
|
192
|
+
let pendingAttachments = [];
|
|
193
|
+
let selectedAgentId = "";
|
|
194
|
+
let selectedAgentLabel = "";
|
|
195
|
+
let markdownState = { inCodeBlock: false };
|
|
196
|
+
let remoteModels = [];
|
|
197
|
+
let remoteModelsCacheKey = "";
|
|
198
|
+
let remoteModelsInflight = null;
|
|
199
|
+
const backgroundTasks = new Map();
|
|
200
|
+
let backgroundSeq = 0;
|
|
201
|
+
let requestExit = false;
|
|
202
|
+
const MARKDOWN_LOG_KINDS = new Set(["assistant", "error"]);
|
|
203
|
+
const controller = createUcodeController({ projectRoot: workspaceRoot });
|
|
204
|
+
|
|
205
|
+
function publish(name, payload) {
|
|
206
|
+
if (!hostRef) return;
|
|
207
|
+
hostRef.broadcast(hostRef.createEvent(name, payload, {
|
|
208
|
+
surface: "ucode",
|
|
209
|
+
project_id: workspaceRoot,
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function publishAgents() {
|
|
214
|
+
const selfSubscriberId = String(
|
|
215
|
+
(props.autoBus && props.autoBus.subscriberId)
|
|
216
|
+
|| process.env.UFOO_SUBSCRIBER_ID
|
|
217
|
+
|| ""
|
|
218
|
+
).trim();
|
|
219
|
+
const snapshot = buildUcodeAgentsSnapshot(workspaceRoot, selfSubscriberId);
|
|
220
|
+
publish("agents.snapshot", snapshot);
|
|
221
|
+
return snapshot;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function remoteModelsKey() {
|
|
225
|
+
const provider = String((props.state && props.state.provider) || "").trim();
|
|
226
|
+
const model = String((props.state && props.state.model) || "").trim();
|
|
227
|
+
return `${workspaceRoot}|${provider}|${model}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function ensureRemoteModels() {
|
|
231
|
+
const key = remoteModelsKey();
|
|
232
|
+
if (remoteModelsCacheKey === key && remoteModels.length > 0) {
|
|
233
|
+
return remoteModels;
|
|
234
|
+
}
|
|
235
|
+
if (remoteModelsInflight && remoteModelsCacheKey === key) {
|
|
236
|
+
return remoteModelsInflight;
|
|
237
|
+
}
|
|
238
|
+
remoteModelsCacheKey = key;
|
|
239
|
+
const { listUcodeModels } = require("../code/modelCommand");
|
|
240
|
+
remoteModelsInflight = (async () => {
|
|
241
|
+
try {
|
|
242
|
+
const listed = await listUcodeModels(props.state || {}, { workspaceRoot });
|
|
243
|
+
if (listed && listed.ok) {
|
|
244
|
+
remoteModels = Array.isArray(listed.models) ? listed.models : [];
|
|
245
|
+
}
|
|
246
|
+
} catch {
|
|
247
|
+
// keep prior cache on network/provider errors
|
|
248
|
+
} finally {
|
|
249
|
+
remoteModelsInflight = null;
|
|
250
|
+
}
|
|
251
|
+
return remoteModels;
|
|
252
|
+
})();
|
|
253
|
+
return remoteModelsInflight;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const tools = createToolMergePublisher((name, payload) => publish(name, payload));
|
|
257
|
+
const thinking = createThinkingStatusPublisher(publish);
|
|
258
|
+
|
|
259
|
+
function getBackgroundSuffix() {
|
|
260
|
+
let running = 0;
|
|
261
|
+
let done = 0;
|
|
262
|
+
let failed = 0;
|
|
263
|
+
for (const task of backgroundTasks.values()) {
|
|
264
|
+
if (!task || typeof task !== "object") continue;
|
|
265
|
+
if (task.status === "running") running += 1;
|
|
266
|
+
else if (task.status === "done") done += 1;
|
|
267
|
+
else if (task.status === "failed") failed += 1;
|
|
268
|
+
}
|
|
269
|
+
const parts = [];
|
|
270
|
+
if (running) parts.push(`${running} running`);
|
|
271
|
+
if (done) parts.push(`${done} done`);
|
|
272
|
+
if (failed) parts.push(`${failed} failed`);
|
|
273
|
+
return parts.length ? ` · BG ${parts.join("/")}` : "";
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function publishStatus(text = "ready", busy = false) {
|
|
277
|
+
const planPayload = buildPlanSetPayload(props.state && props.state.executionState, {});
|
|
278
|
+
const idleHint = !busy && text === "ready"
|
|
279
|
+
? String(planPayload.idle_hint || "").trim()
|
|
280
|
+
: "";
|
|
281
|
+
const base = idleHint ? `ready · ${idleHint}` : text;
|
|
282
|
+
publish("status.set", {
|
|
283
|
+
text: `${base}${getBackgroundSuffix()}`,
|
|
284
|
+
busy: Boolean(busy),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function publishPlan(activityMessage = "") {
|
|
289
|
+
const payload = buildPlanSetPayload(props.state && props.state.executionState, {
|
|
290
|
+
activityMessage,
|
|
291
|
+
});
|
|
292
|
+
publish("plan.set", payload);
|
|
293
|
+
return payload;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function ingestToolLog(entry) {
|
|
297
|
+
if (!entry || typeof entry !== "object") return;
|
|
298
|
+
const normalized = normalizeToolLogEntry(entry);
|
|
299
|
+
if (normalized) tools.pushTool(normalized);
|
|
300
|
+
if (
|
|
301
|
+
entry.tool === "plan_graph"
|
|
302
|
+
|| entry.phase === "end"
|
|
303
|
+
|| entry.phase === "result"
|
|
304
|
+
) {
|
|
305
|
+
publishPlan();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function appendLog(text, kind = "assistant") {
|
|
310
|
+
const raw = String(text == null ? "" : text);
|
|
311
|
+
let lines = [raw];
|
|
312
|
+
if (MARKDOWN_LOG_KINDS.has(kind)) {
|
|
313
|
+
try {
|
|
314
|
+
const rendered = fmt.renderLogLinesWithMarkdownAnsi(raw, markdownState);
|
|
315
|
+
if (Array.isArray(rendered) && rendered.length > 0) {
|
|
316
|
+
lines = rendered.map((line) => String(line || ""));
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
lines = raw.split(/\r?\n/);
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
lines = raw.split(/\r?\n/);
|
|
323
|
+
}
|
|
324
|
+
let last = null;
|
|
325
|
+
for (const line of lines) {
|
|
326
|
+
entrySeq += 1;
|
|
327
|
+
last = {
|
|
328
|
+
id: `u-${entrySeq}`,
|
|
329
|
+
kind,
|
|
330
|
+
text: stripTags(line),
|
|
331
|
+
// Keep ANSI in a parallel field if strip removes styling; prefer raw
|
|
332
|
+
// ANSI line for Rust when it contains escape sequences.
|
|
333
|
+
ansi: line,
|
|
334
|
+
speaker: "",
|
|
335
|
+
};
|
|
336
|
+
// Prefer ANSI when present so Rust can paint spans; otherwise plain.
|
|
337
|
+
if (/\x1b\[/.test(line)) {
|
|
338
|
+
last.text = line;
|
|
339
|
+
}
|
|
340
|
+
publish("transcript.append", last);
|
|
341
|
+
}
|
|
342
|
+
return last;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function replaceTranscript(entries = []) {
|
|
346
|
+
markdownState = { inCodeBlock: false };
|
|
347
|
+
const normalized = (Array.isArray(entries) ? entries : []).map((entry, idx) => {
|
|
348
|
+
const text = String((entry && entry.text) || "");
|
|
349
|
+
return {
|
|
350
|
+
id: String((entry && entry.id) || `r-${idx}`),
|
|
351
|
+
kind: String((entry && entry.kind) || "system"),
|
|
352
|
+
text,
|
|
353
|
+
speaker: String((entry && entry.speaker) || ""),
|
|
354
|
+
};
|
|
355
|
+
});
|
|
356
|
+
entrySeq = Math.max(entrySeq, normalized.length);
|
|
357
|
+
publish("transcript.reset", { entries: normalized });
|
|
358
|
+
tools.reset && tools.reset();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function publishUsageFromResult(result) {
|
|
362
|
+
const meter = (result && result.contextMeter)
|
|
363
|
+
|| (props.state && props.state.contextMeter)
|
|
364
|
+
|| null;
|
|
365
|
+
const label = String((meter && meter.label) || "").trim();
|
|
366
|
+
if (label) {
|
|
367
|
+
if (props.state) props.state.contextMeter = meter;
|
|
368
|
+
publish("usage.set", { text: label, label });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function clearAttachments() {
|
|
373
|
+
pendingAttachments = [];
|
|
374
|
+
publish("attachments.set", { count: 0, labels: [] });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function handlePasteCommand(payload = {}) {
|
|
378
|
+
const {
|
|
379
|
+
handleImagePaste,
|
|
380
|
+
formatImageLogLabel,
|
|
381
|
+
} = require("../code/imageIngest");
|
|
382
|
+
const outcome = handleImagePaste(String(payload.text || ""), {
|
|
383
|
+
workspaceRoot,
|
|
384
|
+
sessionId: (props.state && props.state.sessionId) || "",
|
|
385
|
+
tryClipboard: true,
|
|
386
|
+
});
|
|
387
|
+
if (Array.isArray(outcome.attachments) && outcome.attachments.length > 0) {
|
|
388
|
+
for (const item of outcome.attachments) {
|
|
389
|
+
if (!item || typeof item !== "object") continue;
|
|
390
|
+
const relPath = String(item.relPath || "").trim();
|
|
391
|
+
if (relPath && pendingAttachments.some((existing) => existing && existing.relPath === relPath)) {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
pendingAttachments.push(item);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (Array.isArray(outcome.errors) && outcome.errors.length > 0 && pendingAttachments.length === 0) {
|
|
398
|
+
appendLog(`Image paste: ${outcome.errors[0]}`, "system");
|
|
399
|
+
}
|
|
400
|
+
const labels = pendingAttachments.map((item) => formatImageLogLabel(item) || "image");
|
|
401
|
+
publish("attachments.set", {
|
|
402
|
+
count: pendingAttachments.length,
|
|
403
|
+
labels,
|
|
404
|
+
});
|
|
405
|
+
publish("prompt.apply_paste", { text: String(outcome.text || "") });
|
|
406
|
+
return {
|
|
407
|
+
ok: true,
|
|
408
|
+
text: outcome.text || "",
|
|
409
|
+
count: pendingAttachments.length,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function composeSubmitText(rawText = "") {
|
|
414
|
+
const {
|
|
415
|
+
buildAttachedImagesPromptPrefix,
|
|
416
|
+
formatUserLogWithAttachments,
|
|
417
|
+
} = require("../code/imageIngest");
|
|
418
|
+
const attachments = pendingAttachments.slice();
|
|
419
|
+
const trimmed = String(rawText || "").trim();
|
|
420
|
+
const modelText = `${buildAttachedImagesPromptPrefix(attachments)}${trimmed}`.trim();
|
|
421
|
+
const logText = formatUserLogWithAttachments(trimmed, attachments) || trimmed;
|
|
422
|
+
return { modelText, logText, attachments };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function persistIfNeeded() {
|
|
426
|
+
if (typeof props.persistSessionState !== "function" || !props.state) return;
|
|
427
|
+
try {
|
|
428
|
+
const persisted = props.persistSessionState(props.state);
|
|
429
|
+
if (persisted && persisted.ok === false) {
|
|
430
|
+
appendLog(
|
|
431
|
+
`Error: failed to persist session ${props.state.sessionId || ""}: ${persisted.error || "unknown"}`,
|
|
432
|
+
"error"
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
} catch (err) {
|
|
436
|
+
appendLog(`Error: persist failed: ${err && err.message ? err.message : err}`, "error");
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const banner = fmt.buildUcodeBannerLines({
|
|
441
|
+
model: (props.state && props.state.model) || process.env.UFOO_UCODE_MODEL || "",
|
|
442
|
+
engine: (props.state && props.state.engine) || "ufoo-core",
|
|
443
|
+
workspaceRoot,
|
|
444
|
+
sessionId: (props.state && props.state.sessionId) || "",
|
|
445
|
+
planMode: Boolean(
|
|
446
|
+
props.state
|
|
447
|
+
&& props.state.executionState
|
|
448
|
+
&& props.state.executionState.planMode
|
|
449
|
+
),
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
function buildSnapshot() {
|
|
453
|
+
const meter = props.state && props.state.contextMeter;
|
|
454
|
+
const agentsSnap = buildUcodeAgentsSnapshot(
|
|
455
|
+
workspaceRoot,
|
|
456
|
+
String(
|
|
457
|
+
(props.autoBus && props.autoBus.subscriberId)
|
|
458
|
+
|| process.env.UFOO_SUBSCRIBER_ID
|
|
459
|
+
|| ""
|
|
460
|
+
).trim()
|
|
461
|
+
);
|
|
462
|
+
return {
|
|
463
|
+
status: "ready",
|
|
464
|
+
footer: agentsSnap.footer,
|
|
465
|
+
entries: banner.concat([""]).map((line, idx) => ({
|
|
466
|
+
id: `b-${idx}`,
|
|
467
|
+
kind: "system",
|
|
468
|
+
text: stripTags(line),
|
|
469
|
+
speaker: "",
|
|
470
|
+
})),
|
|
471
|
+
input_history: [],
|
|
472
|
+
agents: agentsSnap.agents,
|
|
473
|
+
attachment_count: pendingAttachments.length,
|
|
474
|
+
usage: String((meter && meter.label) || ""),
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function publishPendingInteraction() {
|
|
479
|
+
try {
|
|
480
|
+
const {
|
|
481
|
+
hasPendingUserInteraction,
|
|
482
|
+
getPendingUserInteraction,
|
|
483
|
+
formatInteractionPromptLines,
|
|
484
|
+
} = require("../code/context/userInteraction");
|
|
485
|
+
if (!props.state || !hasPendingUserInteraction(props.state.executionState)) {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
const pending = getPendingUserInteraction(props.state.executionState);
|
|
489
|
+
const lines = formatInteractionPromptLines(pending, { cols: 80 });
|
|
490
|
+
publish("interaction.request", {
|
|
491
|
+
id: String((pending && pending.id) || "ask"),
|
|
492
|
+
kind: String((pending && pending.kind) || "ask_user"),
|
|
493
|
+
prompt: String((pending && pending.prompt) || "Input required"),
|
|
494
|
+
lines: Array.isArray(lines) ? lines.map((line) => stripTags(line)) : [],
|
|
495
|
+
});
|
|
496
|
+
publish("status.set", { text: "waiting for reply…", busy: false });
|
|
497
|
+
return true;
|
|
498
|
+
} catch {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function resumeInteraction(answerText) {
|
|
504
|
+
return controller.runExclusive(async (abort) => {
|
|
505
|
+
const submit = typeof props.submitUserInteractionAnswer === "function"
|
|
506
|
+
? props.submitUserInteractionAnswer
|
|
507
|
+
: require("../code/protocol").submitUserInteractionAnswer;
|
|
508
|
+
const streamId = `resume-${Date.now()}`;
|
|
509
|
+
tools.beginScope();
|
|
510
|
+
thinking.reset();
|
|
511
|
+
publish("stream.start", { id: streamId });
|
|
512
|
+
try {
|
|
513
|
+
const result = await submit(answerText, props.state, {
|
|
514
|
+
signal: abort.signal,
|
|
515
|
+
onDelta: (delta) => {
|
|
516
|
+
publish("stream.delta", { id: streamId, text: String(delta || "") });
|
|
517
|
+
},
|
|
518
|
+
onThinkingDelta: thinking.onThinkingDelta,
|
|
519
|
+
onToolLog: ingestToolLog,
|
|
520
|
+
});
|
|
521
|
+
tools.flush();
|
|
522
|
+
thinking.reset();
|
|
523
|
+
publish("stream.done", { id: streamId });
|
|
524
|
+
if (!result || result.ok === false) {
|
|
525
|
+
appendLog(`Error: ${(result && result.error) || "resume failed"}`, "error");
|
|
526
|
+
} else if (result.shouldEchoSummary) {
|
|
527
|
+
appendLog(result.echoSummaryText || result.summary || "", "assistant");
|
|
528
|
+
}
|
|
529
|
+
persistIfNeeded();
|
|
530
|
+
publishPlan();
|
|
531
|
+
publishUsageFromResult(result);
|
|
532
|
+
if (publishPendingInteraction()) {
|
|
533
|
+
return { ok: true, waiting: true };
|
|
534
|
+
}
|
|
535
|
+
return { ok: true, waiting: false };
|
|
536
|
+
} catch (err) {
|
|
537
|
+
tools.flush();
|
|
538
|
+
thinking.reset();
|
|
539
|
+
publish("stream.done", { id: streamId });
|
|
540
|
+
if (abort.signal.aborted) {
|
|
541
|
+
appendLog("Task cancelled.", "system");
|
|
542
|
+
return { ok: false, cancelled: true };
|
|
543
|
+
}
|
|
544
|
+
appendLog(`Error: ${err && err.message ? err.message : err}`, "error");
|
|
545
|
+
return { ok: false };
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async function runNaturalLanguage(text) {
|
|
551
|
+
return controller.runExclusive(async (abort) => {
|
|
552
|
+
if (typeof props.runNaturalLanguageTask !== "function") {
|
|
553
|
+
appendLog("runNaturalLanguageTask unavailable", "error");
|
|
554
|
+
return { ok: false };
|
|
555
|
+
}
|
|
556
|
+
const streamId = `nl-${Date.now()}`;
|
|
557
|
+
tools.beginScope();
|
|
558
|
+
thinking.reset();
|
|
559
|
+
publish("stream.start", { id: streamId });
|
|
560
|
+
try {
|
|
561
|
+
const nlResult = await props.runNaturalLanguageTask(text, props.state, {
|
|
562
|
+
signal: abort.signal,
|
|
563
|
+
onDelta: (delta) => {
|
|
564
|
+
publish("stream.delta", { id: streamId, text: String(delta || "") });
|
|
565
|
+
},
|
|
566
|
+
onThinkingDelta: thinking.onThinkingDelta,
|
|
567
|
+
onToolLog: ingestToolLog,
|
|
568
|
+
onPhase: (event) => {
|
|
569
|
+
if (!event || typeof event !== "object") return;
|
|
570
|
+
if (event.type === "request_start") {
|
|
571
|
+
publish("status.set", { text: "Waiting for model…", busy: true });
|
|
572
|
+
} else if (event.type === "text_delta") {
|
|
573
|
+
publish("status.set", { text: "Generating response…", busy: true });
|
|
574
|
+
} else if (event.type === "tool_request") {
|
|
575
|
+
const tool = String(event.name || "tool").trim() || "tool";
|
|
576
|
+
const label = (fmt.TOOL_LABELS && fmt.TOOL_LABELS[tool.toLowerCase()])
|
|
577
|
+
|| `Calling ${tool}`;
|
|
578
|
+
publish("status.set", { text: `${label}…`, busy: true });
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
tools.flush();
|
|
583
|
+
thinking.reset();
|
|
584
|
+
publish("stream.done", { id: streamId });
|
|
585
|
+
if (nlResult && nlResult.summary) appendLog(nlResult.summary, "system");
|
|
586
|
+
if (typeof props.formatNlResult === "function" && nlResult) {
|
|
587
|
+
const formatted = props.formatNlResult(nlResult);
|
|
588
|
+
if (formatted) appendLog(formatted, "assistant");
|
|
589
|
+
}
|
|
590
|
+
persistIfNeeded();
|
|
591
|
+
publishPlan();
|
|
592
|
+
publishUsageFromResult(nlResult);
|
|
593
|
+
if (publishPendingInteraction()) {
|
|
594
|
+
return { ok: true, waiting: true };
|
|
595
|
+
}
|
|
596
|
+
return { ok: true, waiting: false, result: nlResult };
|
|
597
|
+
} catch (err) {
|
|
598
|
+
tools.flush();
|
|
599
|
+
thinking.reset();
|
|
600
|
+
publish("stream.done", { id: streamId });
|
|
601
|
+
if (abort.signal.aborted) {
|
|
602
|
+
appendLog("Task cancelled.", "system");
|
|
603
|
+
return { ok: false, cancelled: true };
|
|
604
|
+
}
|
|
605
|
+
appendLog(`Error: ${err && err.message ? err.message : err}`, "error");
|
|
606
|
+
return { ok: false };
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async function runAutoBusOnce() {
|
|
612
|
+
const autoBus = props.autoBus || {};
|
|
613
|
+
if (!autoBus.enabled || closing) return;
|
|
614
|
+
const getPendingCount = typeof autoBus.getPendingCount === "function"
|
|
615
|
+
? autoBus.getPendingCount
|
|
616
|
+
: () => 0;
|
|
617
|
+
if (Number(getPendingCount()) <= 0) {
|
|
618
|
+
autoBusError = "";
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (typeof props.runUbusCommand !== "function") return;
|
|
622
|
+
|
|
623
|
+
await controller.runExclusive(async (abort) => {
|
|
624
|
+
publish("status.set", { text: "Processing bus messages…", busy: true });
|
|
625
|
+
try {
|
|
626
|
+
const { extractAgentNickname } = require("../code/agent");
|
|
627
|
+
const ubusResult = await props.runUbusCommand(props.state, {
|
|
628
|
+
workspaceRoot,
|
|
629
|
+
subscriberId: autoBus.subscriberId,
|
|
630
|
+
signal: abort.signal,
|
|
631
|
+
onMessageReceived: (msg) => {
|
|
632
|
+
const nickname = extractAgentNickname(msg && msg.from) || (msg && msg.from) || "bus";
|
|
633
|
+
appendLog(`${nickname}: ${(msg && msg.task) || ""}`, "bus");
|
|
634
|
+
publish("status.set", { text: "Working on bus task…", busy: true });
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
if (!ubusResult || !ubusResult.ok) {
|
|
638
|
+
const nextError = String((ubusResult && ubusResult.error) || "ubus failed");
|
|
639
|
+
if (nextError !== autoBusError) {
|
|
640
|
+
autoBusError = nextError;
|
|
641
|
+
appendLog(`Error: ${nextError}`, "error");
|
|
642
|
+
}
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
autoBusError = "";
|
|
646
|
+
const exchanges = Array.isArray(ubusResult.messageExchanges)
|
|
647
|
+
? ubusResult.messageExchanges
|
|
648
|
+
: [];
|
|
649
|
+
for (const exchange of exchanges) {
|
|
650
|
+
const nickname = extractAgentNickname(exchange && exchange.from)
|
|
651
|
+
|| (exchange && exchange.from)
|
|
652
|
+
|| "bus";
|
|
653
|
+
appendLog(`@${nickname} ${(exchange && exchange.reply) || ""}`, "bus");
|
|
654
|
+
}
|
|
655
|
+
if (Number(ubusResult.handled) > 0) persistIfNeeded();
|
|
656
|
+
publishPlan();
|
|
657
|
+
} finally {
|
|
658
|
+
if (!publishPendingInteraction()) {
|
|
659
|
+
publish("status.set", { text: "ready", busy: false });
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function scheduleAutoBus() {
|
|
666
|
+
const autoBus = props.autoBus || {};
|
|
667
|
+
if (!autoBus.enabled || closing || autoBusQueued || controller.isBusy()) return;
|
|
668
|
+
const getPendingCount = typeof autoBus.getPendingCount === "function"
|
|
669
|
+
? autoBus.getPendingCount
|
|
670
|
+
: () => 0;
|
|
671
|
+
if (Number(getPendingCount()) <= 0) return;
|
|
672
|
+
autoBusQueued = true;
|
|
673
|
+
Promise.resolve()
|
|
674
|
+
.then(() => runAutoBusOnce())
|
|
675
|
+
.catch((err) => {
|
|
676
|
+
appendLog(`Error: ${err && err.message ? err.message : "ubus failed"}`, "error");
|
|
677
|
+
})
|
|
678
|
+
.finally(() => {
|
|
679
|
+
autoBusQueued = false;
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const host = createUiHostServer({
|
|
684
|
+
socketPath: uiSocketPath,
|
|
685
|
+
authToken: uiAuthToken,
|
|
686
|
+
capabilities: ["ucode", "scrollback", "prompt", "interaction", "auto-bus"],
|
|
687
|
+
onClientReady(socket) {
|
|
688
|
+
const snap = createEnvelope({
|
|
689
|
+
kind: "snapshot",
|
|
690
|
+
name: "app.snapshot",
|
|
691
|
+
seq: host.nextSeq(),
|
|
692
|
+
scope: { surface: "ucode", project_id: workspaceRoot },
|
|
693
|
+
payload: buildSnapshot(),
|
|
694
|
+
});
|
|
695
|
+
socket.write(encodeMessage(snap));
|
|
696
|
+
publishPendingInteraction();
|
|
697
|
+
publishPlan();
|
|
698
|
+
},
|
|
699
|
+
async onCommand(cmd) {
|
|
700
|
+
const name = String(cmd.name || "");
|
|
701
|
+
const payload = cmd.payload && typeof cmd.payload === "object" ? cmd.payload : {};
|
|
702
|
+
|
|
703
|
+
if (name === "app.exit") {
|
|
704
|
+
closing = true;
|
|
705
|
+
controller.cancelTask();
|
|
706
|
+
return { ok: true };
|
|
707
|
+
}
|
|
708
|
+
if (name === "task.cancel") {
|
|
709
|
+
controller.cancelTask();
|
|
710
|
+
publish("status.set", { text: "cancelling…", busy: true });
|
|
711
|
+
appendLog("⚙ Cancellation requested. Stopping the current task...", "system");
|
|
712
|
+
return { ok: true };
|
|
713
|
+
}
|
|
714
|
+
if (name === "completion.request") {
|
|
715
|
+
const text = String(payload.text || "");
|
|
716
|
+
if (/^\s*\/model(\s|$)/i.test(text)) {
|
|
717
|
+
await ensureRemoteModels();
|
|
718
|
+
}
|
|
719
|
+
const selfSubscriberId = String(
|
|
720
|
+
(props.autoBus && props.autoBus.subscriberId)
|
|
721
|
+
|| process.env.UFOO_SUBSCRIBER_ID
|
|
722
|
+
|| ""
|
|
723
|
+
).trim();
|
|
724
|
+
const items = await buildUcodeCompletionItems({
|
|
725
|
+
text,
|
|
726
|
+
workspaceRoot,
|
|
727
|
+
state: props.state || {},
|
|
728
|
+
selfSubscriberId,
|
|
729
|
+
remoteModels,
|
|
730
|
+
limit: 20,
|
|
731
|
+
});
|
|
732
|
+
publish("completions.set", { items });
|
|
733
|
+
return { ok: true, count: items.length };
|
|
734
|
+
}
|
|
735
|
+
if (name === "ui.resync.request") {
|
|
736
|
+
publish("app.snapshot", buildSnapshot());
|
|
737
|
+
publishAgents();
|
|
738
|
+
publishPlan();
|
|
739
|
+
return { ok: true };
|
|
740
|
+
}
|
|
741
|
+
if (name === "agent.select") {
|
|
742
|
+
selectedAgentId = String(payload.agent_id || payload.agentId || "").trim();
|
|
743
|
+
selectedAgentLabel = String(payload.label || selectedAgentId).trim();
|
|
744
|
+
publish("prompt.set_prefix", {
|
|
745
|
+
prefix: selectedAgentId
|
|
746
|
+
? `›@${selectedAgentLabel || selectedAgentId} `
|
|
747
|
+
: "› ",
|
|
748
|
+
});
|
|
749
|
+
publish("status.set", {
|
|
750
|
+
text: selectedAgentId
|
|
751
|
+
? `target @${selectedAgentLabel || selectedAgentId}`
|
|
752
|
+
: "ready",
|
|
753
|
+
});
|
|
754
|
+
return { ok: true, agent_id: selectedAgentId };
|
|
755
|
+
}
|
|
756
|
+
if (name === "input.paste") {
|
|
757
|
+
return handlePasteCommand(payload);
|
|
758
|
+
}
|
|
759
|
+
if (name === "interaction.respond") {
|
|
760
|
+
if (payload.cancelled) {
|
|
761
|
+
appendLog("interaction cancelled", "system");
|
|
762
|
+
publish("interaction.clear", {});
|
|
763
|
+
publish("status.set", { text: "ready", busy: false });
|
|
764
|
+
return { ok: true, cancelled: true };
|
|
765
|
+
}
|
|
766
|
+
publish("interaction.clear", {});
|
|
767
|
+
publish("status.set", { text: "applying reply…", busy: true });
|
|
768
|
+
const result = await resumeInteraction(String(payload.text || ""));
|
|
769
|
+
if (!result.waiting) {
|
|
770
|
+
publish("status.set", { text: "ready", busy: false });
|
|
771
|
+
}
|
|
772
|
+
return { ok: true, ...result };
|
|
773
|
+
}
|
|
774
|
+
if (name === "input.submit") {
|
|
775
|
+
const rawText = String(payload.text || "");
|
|
776
|
+
const { modelText, logText, attachments } = composeSubmitText(rawText);
|
|
777
|
+
if (!modelText && attachments.length === 0) return { ok: true, empty: true };
|
|
778
|
+
clearAttachments();
|
|
779
|
+
tools.beginScope();
|
|
780
|
+
|
|
781
|
+
try {
|
|
782
|
+
const { hasPendingUserInteraction } = require("../code/context/userInteraction");
|
|
783
|
+
if (props.state && hasPendingUserInteraction(props.state.executionState)) {
|
|
784
|
+
appendLog(`› ${logText || rawText}`, "user");
|
|
785
|
+
publishStatus("applying reply…", true);
|
|
786
|
+
const result = await resumeInteraction(modelText || rawText);
|
|
787
|
+
if (!result.waiting) publishStatus("ready", false);
|
|
788
|
+
return { ok: true, routed: "interaction" };
|
|
789
|
+
}
|
|
790
|
+
} catch {
|
|
791
|
+
// fall through
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
appendLog(`› ${logText || rawText}`, "user");
|
|
795
|
+
publishStatus("working…", true);
|
|
796
|
+
|
|
797
|
+
try {
|
|
798
|
+
const trimmed = String(rawText || "").trim();
|
|
799
|
+
if (trimmed.startsWith("/") && attachments.length === 0) {
|
|
800
|
+
if (typeof props.runSingleCommand !== "function") {
|
|
801
|
+
appendLog("slash commands unavailable", "error");
|
|
802
|
+
} else {
|
|
803
|
+
const { dispatchUcodeSlashCommand } = require("../code/ucodeSlashDispatch");
|
|
804
|
+
let parsed;
|
|
805
|
+
try {
|
|
806
|
+
parsed = props.runSingleCommand(trimmed, workspaceRoot);
|
|
807
|
+
} catch (err) {
|
|
808
|
+
appendLog(`Error: ${err && err.message ? err.message : "command parse failed"}`, "error");
|
|
809
|
+
parsed = null;
|
|
810
|
+
}
|
|
811
|
+
if (parsed) {
|
|
812
|
+
const dispatched = await dispatchUcodeSlashCommand(parsed, {
|
|
813
|
+
state: props.state,
|
|
814
|
+
workspaceRoot,
|
|
815
|
+
appendLog,
|
|
816
|
+
persist: persistIfNeeded,
|
|
817
|
+
publishPlan,
|
|
818
|
+
publishUsage: (meter) => {
|
|
819
|
+
const label = String((meter && meter.label) || "").trim();
|
|
820
|
+
if (label) publish("usage.set", { text: label, label });
|
|
821
|
+
},
|
|
822
|
+
onExit: () => {
|
|
823
|
+
requestExit = true;
|
|
824
|
+
closing = true;
|
|
825
|
+
publish("app.exit", { reason: "slash_exit" });
|
|
826
|
+
},
|
|
827
|
+
bannerLines: banner,
|
|
828
|
+
resumeSession: (sessionId) => {
|
|
829
|
+
if (typeof props.resumeSessionState !== "function") {
|
|
830
|
+
return { ok: false, error: "resume unsupported" };
|
|
831
|
+
}
|
|
832
|
+
return props.resumeSessionState(props.state, sessionId, workspaceRoot);
|
|
833
|
+
},
|
|
834
|
+
replaceTranscript: (entries) => {
|
|
835
|
+
tools.flush();
|
|
836
|
+
replaceTranscript(entries);
|
|
837
|
+
},
|
|
838
|
+
onTool: (tool, args, payload) => {
|
|
839
|
+
ingestToolLog({
|
|
840
|
+
tool,
|
|
841
|
+
args,
|
|
842
|
+
phase: payload && payload.ok === false ? "error" : "end",
|
|
843
|
+
error: (payload && payload.error) || "",
|
|
844
|
+
result: payload,
|
|
845
|
+
});
|
|
846
|
+
tools.flush();
|
|
847
|
+
},
|
|
848
|
+
onBackground: async (task) => {
|
|
849
|
+
backgroundSeq += 1;
|
|
850
|
+
const jobId = `bg-${Date.now().toString(36)}-${backgroundSeq.toString(36)}`;
|
|
851
|
+
const taskRecord = {
|
|
852
|
+
id: jobId,
|
|
853
|
+
task,
|
|
854
|
+
status: "running",
|
|
855
|
+
startedAt: Date.now(),
|
|
856
|
+
summary: "",
|
|
857
|
+
};
|
|
858
|
+
backgroundTasks.set(jobId, taskRecord);
|
|
859
|
+
publishStatus("ready", false);
|
|
860
|
+
appendLog(`[${jobId}] started in background.`, "system");
|
|
861
|
+
const bgState = {
|
|
862
|
+
workspaceRoot: props.state && props.state.workspaceRoot,
|
|
863
|
+
provider: props.state && props.state.provider,
|
|
864
|
+
model: props.state && props.state.model,
|
|
865
|
+
engine: props.state && props.state.engine,
|
|
866
|
+
context: props.state && props.state.context,
|
|
867
|
+
nlMessages: Array.isArray(props.state && props.state.nlMessages)
|
|
868
|
+
? props.state.nlMessages.slice()
|
|
869
|
+
: [],
|
|
870
|
+
sessionId: "",
|
|
871
|
+
timeoutMs: props.state && props.state.timeoutMs,
|
|
872
|
+
jsonOutput: false,
|
|
873
|
+
};
|
|
874
|
+
Promise.resolve()
|
|
875
|
+
.then(() => {
|
|
876
|
+
if (typeof props.runNaturalLanguageTask !== "function") {
|
|
877
|
+
throw new Error("runNaturalLanguageTask unavailable");
|
|
878
|
+
}
|
|
879
|
+
return props.runNaturalLanguageTask(task, bgState);
|
|
880
|
+
})
|
|
881
|
+
.then((nlResult) => {
|
|
882
|
+
taskRecord.status = nlResult && nlResult.ok ? "done" : "failed";
|
|
883
|
+
taskRecord.finishedAt = Date.now();
|
|
884
|
+
const formatted = typeof props.formatNlResult === "function"
|
|
885
|
+
? props.formatNlResult(nlResult, false)
|
|
886
|
+
: "";
|
|
887
|
+
taskRecord.summary = String(formatted || "").trim();
|
|
888
|
+
appendLog(
|
|
889
|
+
`[${jobId}] ${taskRecord.status}: ${taskRecord.summary || "no summary"}`,
|
|
890
|
+
"system"
|
|
891
|
+
);
|
|
892
|
+
})
|
|
893
|
+
.catch((err) => {
|
|
894
|
+
taskRecord.status = "failed";
|
|
895
|
+
taskRecord.finishedAt = Date.now();
|
|
896
|
+
taskRecord.summary = err && err.message
|
|
897
|
+
? String(err.message)
|
|
898
|
+
: "background task failed";
|
|
899
|
+
appendLog(`[${jobId}] failed: ${taskRecord.summary}`, "error");
|
|
900
|
+
})
|
|
901
|
+
.finally(() => {
|
|
902
|
+
publishStatus("ready", false);
|
|
903
|
+
});
|
|
904
|
+
},
|
|
905
|
+
onNaturalLanguage: async (task) => runNaturalLanguage(task),
|
|
906
|
+
runUbus: typeof props.runUbusCommand === "function"
|
|
907
|
+
? (state, opts) => props.runUbusCommand(state, opts)
|
|
908
|
+
: null,
|
|
909
|
+
setBusyStatus: (msg) => publishStatus(msg || "working…", true),
|
|
910
|
+
clearBusyStatus: () => publishStatus("ready", false),
|
|
911
|
+
});
|
|
912
|
+
if (dispatched && dispatched.waiting) {
|
|
913
|
+
return { ok: true, routed: "ucode", waiting: true };
|
|
914
|
+
}
|
|
915
|
+
if (requestExit) {
|
|
916
|
+
return { ok: true, routed: "exit" };
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
} else {
|
|
921
|
+
const result = await runNaturalLanguage(modelText);
|
|
922
|
+
if (result && result.waiting) {
|
|
923
|
+
return { ok: true, routed: "ucode", waiting: true };
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
} finally {
|
|
927
|
+
try {
|
|
928
|
+
const { hasPendingUserInteraction } = require("../code/context/userInteraction");
|
|
929
|
+
if (!(props.state && hasPendingUserInteraction(props.state.executionState))) {
|
|
930
|
+
publishStatus("ready", false);
|
|
931
|
+
}
|
|
932
|
+
} catch {
|
|
933
|
+
publishStatus("ready", false);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return { ok: true, routed: "ucode" };
|
|
937
|
+
}
|
|
938
|
+
return { ok: false, error: `unsupported command ${name}` };
|
|
939
|
+
},
|
|
940
|
+
});
|
|
941
|
+
hostRef = host;
|
|
942
|
+
await host.listen();
|
|
943
|
+
controller.start();
|
|
944
|
+
publishAgents();
|
|
945
|
+
ensureRemoteModels().catch(() => {});
|
|
946
|
+
agentsTimer = setInterval(publishAgents, 3000);
|
|
947
|
+
if (typeof agentsTimer.unref === "function") agentsTimer.unref();
|
|
948
|
+
|
|
949
|
+
if (props.autoBus && props.autoBus.enabled) {
|
|
950
|
+
autoBusTimer = setInterval(scheduleAutoBus, 1500);
|
|
951
|
+
if (typeof autoBusTimer.unref === "function") autoBusTimer.unref();
|
|
952
|
+
scheduleAutoBus();
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const child = spawn(plan.binary, [
|
|
956
|
+
"--surface", "ucode",
|
|
957
|
+
"--ui-socket", uiSocketPath,
|
|
958
|
+
], {
|
|
959
|
+
stdio: "inherit",
|
|
960
|
+
env: {
|
|
961
|
+
...process.env,
|
|
962
|
+
UFOO_UI_PROTOCOL: plan.protocol,
|
|
963
|
+
UFOO_UI_TOKEN: uiAuthToken,
|
|
964
|
+
},
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
const exitCode = await new Promise((resolve) => {
|
|
968
|
+
child.on("error", (err) => {
|
|
969
|
+
// eslint-disable-next-line no-console
|
|
970
|
+
console.error("ufoo-tui spawn failed");
|
|
971
|
+
resolve(1);
|
|
972
|
+
});
|
|
973
|
+
child.on("close", (code, signal) => {
|
|
974
|
+
resolve(signal ? 1 : (code == null ? 0 : code));
|
|
975
|
+
});
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
closing = true;
|
|
979
|
+
if (autoBusTimer) {
|
|
980
|
+
clearInterval(autoBusTimer);
|
|
981
|
+
autoBusTimer = null;
|
|
982
|
+
}
|
|
983
|
+
if (agentsTimer) {
|
|
984
|
+
clearInterval(agentsTimer);
|
|
985
|
+
agentsTimer = null;
|
|
986
|
+
}
|
|
987
|
+
thinking.reset();
|
|
988
|
+
controller.stop();
|
|
989
|
+
await host.close();
|
|
990
|
+
return exitCode;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
module.exports = {
|
|
994
|
+
runUcodeRust,
|
|
995
|
+
buildPlanSetPayload,
|
|
996
|
+
normalizeToolLogEntry,
|
|
997
|
+
buildUcodeAgentsSnapshot,
|
|
998
|
+
buildUcodeCompletionItems,
|
|
999
|
+
};
|