u-foo 2.3.30 → 2.3.32
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/package.json +5 -1
- package/scripts/chat-app-smoke.js +30 -0
- package/scripts/ink-demo.js +23 -0
- package/scripts/ink-smoke.js +30 -0
- package/scripts/ucode-app-smoke.js +36 -0
- package/src/chat/commandExecutor.js +6 -2
- package/src/chat/daemonMessageRouter.js +9 -1
- package/src/chat/daemonTransport.js +2 -1
- package/src/chat/dashboardKeyController.js +0 -40
- package/src/chat/dashboardView.js +0 -20
- package/src/chat/index.js +9 -1
- package/src/chat/inputSubmitHandler.js +34 -0
- package/src/chat/projectCloseController.js +1 -1
- package/src/chat/shellCommand.js +42 -0
- package/src/chat/transport.js +16 -3
- package/src/cli.js +4 -3
- package/src/code/agent.js +4 -0
- package/src/code/nativeRunner.js +74 -0
- package/src/code/taskDecomposer.js +5 -4
- package/src/code/tui.js +73 -561
- package/src/daemon/index.js +169 -27
- package/src/daemon/ipcServer.js +23 -1
- package/src/daemon/promptRequest.js +6 -1
- package/src/daemon/run.js +11 -4
- package/src/projects/runtimes.js +1 -1
- package/src/ufoo/agentRegistryDiagnostics.js +43 -0
- package/src/ui/MIGRATION.md +382 -0
- package/src/ui/components/ChatApp.js +2950 -0
- package/src/ui/components/DashboardBar.js +417 -0
- package/src/ui/components/InkDemo.js +96 -0
- package/src/ui/components/MultilineInput.js +387 -0
- package/src/ui/components/UcodeApp.js +813 -0
- package/src/ui/components/agentMirror.js +725 -0
- package/src/ui/components/chatReducer.js +337 -0
- package/src/ui/format/index.js +997 -0
- package/src/ui/index.js +9 -0
- package/src/ui/runInk.js +57 -0
- package/src/utils/nodeExecutable.js +26 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "u-foo",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.32",
|
|
4
4
|
"description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://ufoo.dev",
|
|
@@ -42,6 +42,8 @@
|
|
|
42
42
|
"test": "jest",
|
|
43
43
|
"test:watch": "jest --watch",
|
|
44
44
|
"test:coverage": "jest --coverage",
|
|
45
|
+
"ink:demo": "node scripts/ink-demo.js",
|
|
46
|
+
"ink:smoke": "node scripts/ink-smoke.js",
|
|
45
47
|
"bench:global-switch": "node scripts/global-chat-switch-benchmark.js"
|
|
46
48
|
},
|
|
47
49
|
"dependencies": {
|
|
@@ -51,7 +53,9 @@
|
|
|
51
53
|
"chalk": "^4.1.2",
|
|
52
54
|
"commander": "^13.1.0",
|
|
53
55
|
"gray-matter": "^4.0.3",
|
|
56
|
+
"ink": "^5.2.1",
|
|
54
57
|
"node-pty": "^1.1.0",
|
|
58
|
+
"react": "^18.3.1",
|
|
55
59
|
"ws": "^8.19.0",
|
|
56
60
|
"xterm-addon-serialize": "^0.11.0",
|
|
57
61
|
"xterm-headless": "^5.3.0"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Headless mount test for the ChatApp shell. Boots the ink TUI with stub
|
|
5
|
+
* props (no daemon, no real bootstrap) and checks the component tree
|
|
6
|
+
* renders without throwing. Used for CI parity with ucode-app-smoke.js.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { runInk } = require("../src/ui/runInk");
|
|
10
|
+
const { createChatApp } = require("../src/ui/components/ChatApp");
|
|
11
|
+
|
|
12
|
+
(async () => {
|
|
13
|
+
const props = {
|
|
14
|
+
activeProjectRoot: process.cwd(),
|
|
15
|
+
globalMode: false,
|
|
16
|
+
globalScope: "project",
|
|
17
|
+
};
|
|
18
|
+
const handle = await runInk((React, ink) => {
|
|
19
|
+
const ChatApp = createChatApp({ React, ink, props, interactive: false });
|
|
20
|
+
return React.createElement(ChatApp);
|
|
21
|
+
}, { stdout: process.stdout, stderr: process.stderr });
|
|
22
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
23
|
+
handle.unmount();
|
|
24
|
+
await handle.waitUntilExit().catch(() => undefined);
|
|
25
|
+
process.stdout.write("\nchat-app-smoke: ok\n");
|
|
26
|
+
process.exit(0);
|
|
27
|
+
})().catch((err) => {
|
|
28
|
+
process.stderr.write(`chat-app-smoke: failed: ${err && err.stack || err}\n`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Interactive ink demo. Run from a real TTY:
|
|
6
|
+
* npm run ink:demo
|
|
7
|
+
* or
|
|
8
|
+
* node scripts/ink-demo.js
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { runInk } = require("../src/ui/runInk");
|
|
12
|
+
const { createInkDemo } = require("../src/ui/components/InkDemo");
|
|
13
|
+
|
|
14
|
+
(async () => {
|
|
15
|
+
const handle = await runInk((React, ink) => {
|
|
16
|
+
const InkDemo = createInkDemo({ React, ink, interactive: true });
|
|
17
|
+
return React.createElement(InkDemo);
|
|
18
|
+
});
|
|
19
|
+
await handle.waitUntilExit();
|
|
20
|
+
})().catch((err) => {
|
|
21
|
+
process.stderr.write(`ink-demo: ${err && err.stack || err}\n`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Headless smoke test for the ink demo: mounts the component, lets it tick
|
|
5
|
+
* once, then unmounts. Exits with non-zero on any error. Used to confirm
|
|
6
|
+
* the CJS->ESM bridge and component tree work without occupying the TTY.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { runInk } = require("../src/ui/runInk");
|
|
10
|
+
const { createInkDemo } = require("../src/ui/components/InkDemo");
|
|
11
|
+
|
|
12
|
+
(async () => {
|
|
13
|
+
const handle = await runInk((React, ink) => {
|
|
14
|
+
const InkDemo = createInkDemo({ React, ink, interactive: false });
|
|
15
|
+
return React.createElement(InkDemo);
|
|
16
|
+
}, {
|
|
17
|
+
stdout: process.stdout,
|
|
18
|
+
stderr: process.stderr,
|
|
19
|
+
stdin: process.stdin,
|
|
20
|
+
exitOnCtrlC: false,
|
|
21
|
+
});
|
|
22
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
23
|
+
handle.unmount();
|
|
24
|
+
await handle.waitUntilExit().catch(() => undefined);
|
|
25
|
+
process.stdout.write("\nink-smoke: ok\n");
|
|
26
|
+
process.exit(0);
|
|
27
|
+
})().catch((err) => {
|
|
28
|
+
process.stderr.write(`ink-smoke: failed: ${err && err.stack || err}\n`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Headless mount test for the UcodeApp shell. Boots the ink TUI with stub
|
|
5
|
+
* runner props, lets it render once, then unmounts. Used to confirm the ink
|
|
6
|
+
* code path stays compilable as P1 evolves.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { runInk } = require("../src/ui/runInk");
|
|
10
|
+
const { createUcodeApp } = require("../src/ui/components/UcodeApp");
|
|
11
|
+
|
|
12
|
+
(async () => {
|
|
13
|
+
const props = {
|
|
14
|
+
stdin: process.stdin,
|
|
15
|
+
stdout: process.stdout,
|
|
16
|
+
runSingleCommand: () => ({ kind: "empty" }),
|
|
17
|
+
runNaturalLanguageTask: async () => ({ ok: true, summary: "ok" }),
|
|
18
|
+
runUbusCommand: async () => ({ ok: false, error: "ubus unsupported", summary: "" }),
|
|
19
|
+
formatNlResult: () => "ok",
|
|
20
|
+
workspaceRoot: process.cwd(),
|
|
21
|
+
state: { model: "test-model", sessionId: "smoke", engine: "ufoo-core" },
|
|
22
|
+
autoBus: { enabled: false, getPendingCount: () => 0, subscriberId: "" },
|
|
23
|
+
};
|
|
24
|
+
const handle = await runInk((React, ink) => {
|
|
25
|
+
const UcodeApp = createUcodeApp({ React, ink, props, interactive: false });
|
|
26
|
+
return React.createElement(UcodeApp);
|
|
27
|
+
}, { stdout: process.stdout, stderr: process.stderr });
|
|
28
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
29
|
+
handle.unmount();
|
|
30
|
+
await handle.waitUntilExit().catch(() => undefined);
|
|
31
|
+
process.stdout.write("\nucode-app-smoke: ok\n");
|
|
32
|
+
process.exit(0);
|
|
33
|
+
})().catch((err) => {
|
|
34
|
+
process.stderr.write(`ucode-app-smoke: failed: ${err && err.stack || err}\n`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
});
|
|
@@ -237,7 +237,7 @@ function createCommandExecutor(options = {}) {
|
|
|
237
237
|
|
|
238
238
|
if (subcommand === "stop") {
|
|
239
239
|
statusMsg("{gray-fg}⚙{/gray-fg} Stopping daemon...");
|
|
240
|
-
stopDaemon(targetRoot);
|
|
240
|
+
stopDaemon(targetRoot, { source: "chat-command:/daemon stop" });
|
|
241
241
|
await sleep(1000);
|
|
242
242
|
if (!isDaemonRunning(targetRoot)) {
|
|
243
243
|
statusMsg("{gray-fg}✓{/gray-fg} Daemon stopped");
|
|
@@ -249,8 +249,12 @@ function createCommandExecutor(options = {}) {
|
|
|
249
249
|
|
|
250
250
|
if (subcommand === "restart") {
|
|
251
251
|
statusMsg("{gray-fg}⚙{/gray-fg} Restarting daemon...");
|
|
252
|
-
stopDaemon(targetRoot);
|
|
252
|
+
stopDaemon(targetRoot, { source: "chat-command:/daemon restart" });
|
|
253
253
|
await sleep(500);
|
|
254
|
+
if (isDaemonRunning(targetRoot)) {
|
|
255
|
+
statusMsg("{gray-fg}✗{/gray-fg} Failed to stop daemon");
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
254
258
|
startDaemon(targetRoot);
|
|
255
259
|
await sleep(1000);
|
|
256
260
|
if (isDaemonRunning(targetRoot)) {
|
|
@@ -415,7 +415,15 @@ function createDaemonMessageRouter(options = {}) {
|
|
|
415
415
|
const delta = typeof streamPayload.delta === "string"
|
|
416
416
|
? decodeEscapedNewlines(streamPayload.delta)
|
|
417
417
|
: "";
|
|
418
|
-
if (delta)
|
|
418
|
+
if (delta || streamPayload.done) {
|
|
419
|
+
writeToAgentTerm(delta, {
|
|
420
|
+
data,
|
|
421
|
+
publisher,
|
|
422
|
+
streamPayload,
|
|
423
|
+
done: Boolean(streamPayload.done),
|
|
424
|
+
reason: streamPayload.reason || "",
|
|
425
|
+
});
|
|
426
|
+
}
|
|
419
427
|
} else if (displayMessage) {
|
|
420
428
|
writeToAgentTerm(`${displayMessage}\r\n`);
|
|
421
429
|
}
|
|
@@ -29,6 +29,7 @@ function createDaemonTransport(options = {}) {
|
|
|
29
29
|
|
|
30
30
|
async function connectClientForTarget(override = {}) {
|
|
31
31
|
const target = resolveTarget(override);
|
|
32
|
+
const autoStart = override.autoStart !== false;
|
|
32
33
|
let client = await connectWithRetry(
|
|
33
34
|
target.sockPath,
|
|
34
35
|
primaryRetries,
|
|
@@ -39,7 +40,7 @@ function createDaemonTransport(options = {}) {
|
|
|
39
40
|
// Retry once with a fresh daemon start and longer wait.
|
|
40
41
|
// Check if a restart is already in progress via the explicit restart flow.
|
|
41
42
|
const isExplicitRestartInProgress = restartLocks.get(target.projectRoot);
|
|
42
|
-
if (!isExplicitRestartInProgress && !isRunning(target.projectRoot)) {
|
|
43
|
+
if (autoStart && !isExplicitRestartInProgress && !isRunning(target.projectRoot)) {
|
|
43
44
|
startDaemon(target.projectRoot);
|
|
44
45
|
await new Promise((resolve) => setTimeout(resolve, restartDelayMs));
|
|
45
46
|
}
|
|
@@ -18,7 +18,6 @@ function createDashboardKeyController(options = {}) {
|
|
|
18
18
|
exitDashboardMode = () => {},
|
|
19
19
|
setLaunchMode = () => {},
|
|
20
20
|
setAgentProvider = () => {},
|
|
21
|
-
setAutoResume = () => {},
|
|
22
21
|
clampAgentWindow = () => {},
|
|
23
22
|
clampAgentWindowWithSelection = () => {},
|
|
24
23
|
requestProjectSwitch = () => {},
|
|
@@ -314,44 +313,6 @@ function createDashboardKeyController(options = {}) {
|
|
|
314
313
|
return true;
|
|
315
314
|
}
|
|
316
315
|
|
|
317
|
-
function handleResumeKey(key) {
|
|
318
|
-
if (key.name === "left") {
|
|
319
|
-
state.selectedResumeIndex = state.selectedResumeIndex <= 0
|
|
320
|
-
? state.resumeOptions.length - 1
|
|
321
|
-
: state.selectedResumeIndex - 1;
|
|
322
|
-
renderDashboardAndScreen();
|
|
323
|
-
return true;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
if (key.name === "right") {
|
|
327
|
-
state.selectedResumeIndex = state.selectedResumeIndex >= state.resumeOptions.length - 1
|
|
328
|
-
? 0
|
|
329
|
-
: state.selectedResumeIndex + 1;
|
|
330
|
-
renderDashboardAndScreen();
|
|
331
|
-
return true;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (key.name === "up") {
|
|
335
|
-
state.dashboardView = "provider";
|
|
336
|
-
renderDashboardAndScreen();
|
|
337
|
-
return true;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (key.name === "enter" || key.name === "return") {
|
|
341
|
-
const selected = state.resumeOptions[state.selectedResumeIndex];
|
|
342
|
-
if (selected) setAutoResume(selected.value);
|
|
343
|
-
exitDashboardMode(false);
|
|
344
|
-
return true;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
if (key.name === "escape") {
|
|
348
|
-
exitDashboardMode(false);
|
|
349
|
-
return true;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return true;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
316
|
function handleProjectsKey(key) {
|
|
356
317
|
const projects = Array.isArray(state.projects) ? state.projects : [];
|
|
357
318
|
if (projects.length === 0) {
|
|
@@ -557,7 +518,6 @@ function createDashboardKeyController(options = {}) {
|
|
|
557
518
|
|
|
558
519
|
if (state.dashboardView === "mode") return handleModeKey(key);
|
|
559
520
|
if (state.dashboardView === "provider") return handleProviderKey(key);
|
|
560
|
-
if (state.dashboardView === "resume") return handleResumeKey(key);
|
|
561
521
|
if (state.dashboardView === "cron") return handleCronKey(key);
|
|
562
522
|
|
|
563
523
|
return handleAgentsKey(key);
|
|
@@ -174,11 +174,9 @@ function buildDashboardDetailLine(options = {}) {
|
|
|
174
174
|
getAgentState = () => "",
|
|
175
175
|
selectedModeIndex = 0,
|
|
176
176
|
selectedProviderIndex = 0,
|
|
177
|
-
selectedResumeIndex = 0,
|
|
178
177
|
selectedCronIndex = -1,
|
|
179
178
|
cronTasks = [],
|
|
180
179
|
providerOptions = [],
|
|
181
|
-
resumeOptions = [],
|
|
182
180
|
dashHints = {},
|
|
183
181
|
modeOptions = DEFAULT_MODE_OPTIONS,
|
|
184
182
|
} = options;
|
|
@@ -210,18 +208,6 @@ function buildDashboardDetailLine(options = {}) {
|
|
|
210
208
|
return { content, windowStart };
|
|
211
209
|
}
|
|
212
210
|
|
|
213
|
-
if (dashboardView === "resume") {
|
|
214
|
-
const resumeParts = resumeOptions.map((opt, i) => {
|
|
215
|
-
if (i === selectedResumeIndex) {
|
|
216
|
-
return `{inverse}${opt.label}{/inverse}`;
|
|
217
|
-
}
|
|
218
|
-
return `{cyan-fg}${opt.label}{/cyan-fg}`;
|
|
219
|
-
});
|
|
220
|
-
content += `{gray-fg}Resume:{/gray-fg} ${resumeParts.join(" ")}`;
|
|
221
|
-
content += ` {gray-fg}│ ${dashHints.resume || ""}{/gray-fg}`;
|
|
222
|
-
return { content, windowStart };
|
|
223
|
-
}
|
|
224
|
-
|
|
225
211
|
if (dashboardView === "cron") {
|
|
226
212
|
const items = Array.isArray(cronTasks) ? cronTasks : [];
|
|
227
213
|
const summary = items.length > 0
|
|
@@ -295,13 +281,11 @@ function computeDashboardContent(options = {}) {
|
|
|
295
281
|
agentProvider = "codex-cli",
|
|
296
282
|
selectedModeIndex = 0,
|
|
297
283
|
selectedProviderIndex = 0,
|
|
298
|
-
selectedResumeIndex = 0,
|
|
299
284
|
selectedCronIndex = -1,
|
|
300
285
|
cronTasks = [],
|
|
301
286
|
loopSummary = null,
|
|
302
287
|
pendingReports = 0,
|
|
303
288
|
providerOptions = [],
|
|
304
|
-
resumeOptions = [],
|
|
305
289
|
dashHints = {},
|
|
306
290
|
modeOptions = DEFAULT_MODE_OPTIONS,
|
|
307
291
|
} = options;
|
|
@@ -357,11 +341,9 @@ function computeDashboardContent(options = {}) {
|
|
|
357
341
|
getAgentState,
|
|
358
342
|
selectedModeIndex,
|
|
359
343
|
selectedProviderIndex,
|
|
360
|
-
selectedResumeIndex,
|
|
361
344
|
selectedCronIndex,
|
|
362
345
|
cronTasks,
|
|
363
346
|
providerOptions,
|
|
364
|
-
resumeOptions,
|
|
365
347
|
dashHints,
|
|
366
348
|
modeOptions,
|
|
367
349
|
});
|
|
@@ -383,11 +365,9 @@ function computeDashboardContent(options = {}) {
|
|
|
383
365
|
getAgentState,
|
|
384
366
|
selectedModeIndex,
|
|
385
367
|
selectedProviderIndex,
|
|
386
|
-
selectedResumeIndex,
|
|
387
368
|
selectedCronIndex,
|
|
388
369
|
cronTasks,
|
|
389
370
|
providerOptions,
|
|
390
|
-
resumeOptions,
|
|
391
371
|
dashHints,
|
|
392
372
|
modeOptions,
|
|
393
373
|
});
|
package/src/chat/index.js
CHANGED
|
@@ -69,7 +69,7 @@ const {
|
|
|
69
69
|
|
|
70
70
|
const MODE_OPTIONS = ["auto", "host", "terminal", "tmux", "internal-pty", "internal"];
|
|
71
71
|
|
|
72
|
-
async function
|
|
72
|
+
async function runChatBlessed(projectRoot, options = {}) {
|
|
73
73
|
const globalMode = options && options.globalMode === true;
|
|
74
74
|
const DASHBOARD_HEIGHT = globalMode ? 2 : 1;
|
|
75
75
|
let activeProjectRoot = projectRoot;
|
|
@@ -2211,4 +2211,12 @@ async function runChat(projectRoot, options = {}) {
|
|
|
2211
2211
|
screen.render();
|
|
2212
2212
|
}
|
|
2213
2213
|
|
|
2214
|
+
async function runChat(projectRoot, options = {}) {
|
|
2215
|
+
if (String(process.env.UFOO_TUI || "").trim().toLowerCase() === "blessed") {
|
|
2216
|
+
return runChatBlessed(projectRoot, options);
|
|
2217
|
+
}
|
|
2218
|
+
const { runChatInk } = require("../ui/components/ChatApp");
|
|
2219
|
+
return runChatInk(projectRoot, options);
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2214
2222
|
module.exports = { runChat };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { IPC_REQUEST_TYPES } = require("../shared/eventContract");
|
|
2
2
|
const { decodeEscapedNewlines } = require("./text");
|
|
3
3
|
const { shouldEchoCommandInChat } = require("./commands");
|
|
4
|
+
const { parseShellCommand, runShellCommand: defaultRunShellCommand } = require("./shellCommand");
|
|
4
5
|
|
|
5
6
|
function createInputSubmitHandler(options = {}) {
|
|
6
7
|
const {
|
|
@@ -24,6 +25,8 @@ function createInputSubmitHandler(options = {}) {
|
|
|
24
25
|
commitInputHistory = () => {},
|
|
25
26
|
focusInput = () => {},
|
|
26
27
|
renderScreen = () => {}, // Add renderScreen callback
|
|
28
|
+
runShellCommand = defaultRunShellCommand,
|
|
29
|
+
getShellCwd = () => process.cwd(),
|
|
27
30
|
} = options;
|
|
28
31
|
|
|
29
32
|
if (!state || typeof state !== "object") {
|
|
@@ -90,6 +93,37 @@ function createInputSubmitHandler(options = {}) {
|
|
|
90
93
|
|
|
91
94
|
commitInputHistory(text);
|
|
92
95
|
|
|
96
|
+
const shellCommand = parseShellCommand(text);
|
|
97
|
+
if (shellCommand) {
|
|
98
|
+
logMessage("user", `{gray-fg}!{/gray-fg} ${escapeBlessed(shellCommand)}`);
|
|
99
|
+
queueStatusLine(`Running: ${escapeBlessed(shellCommand)}`);
|
|
100
|
+
renderScreen();
|
|
101
|
+
try {
|
|
102
|
+
const result = await runShellCommand(shellCommand, { cwd: getShellCwd() });
|
|
103
|
+
const stdout = String(result && result.stdout ? result.stdout : "").trimEnd();
|
|
104
|
+
const stderr = String(result && result.stderr ? result.stderr : "").trimEnd();
|
|
105
|
+
if (stdout) {
|
|
106
|
+
stdout.split(/\r?\n/).forEach((line) => logMessage("system", escapeBlessed(line)));
|
|
107
|
+
}
|
|
108
|
+
if (stderr) {
|
|
109
|
+
stderr.split(/\r?\n/).forEach((line) => logMessage(result && result.ok ? "system" : "error", escapeBlessed(line)));
|
|
110
|
+
}
|
|
111
|
+
if (!stdout && !stderr) {
|
|
112
|
+
logMessage("system", "{gray-fg}(no output){/gray-fg}");
|
|
113
|
+
}
|
|
114
|
+
if (result && result.ok) {
|
|
115
|
+
queueStatusLine(`Done: ${escapeBlessed(shellCommand)}`);
|
|
116
|
+
} else {
|
|
117
|
+
const suffix = result && result.signal ? ` signal ${result.signal}` : ` exit ${result && result.code != null ? result.code : 1}`;
|
|
118
|
+
logMessage("error", `{white-fg}✗{/white-fg} Command failed:${escapeBlessed(suffix)}`);
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
logMessage("error", `{white-fg}✗{/white-fg} Command error: ${escapeBlessed(err && err.message ? err.message : err)}`);
|
|
122
|
+
}
|
|
123
|
+
focusInput();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
93
127
|
if (state.targetAgent) {
|
|
94
128
|
const label = getAgentLabel(state.targetAgent);
|
|
95
129
|
logMessage(
|
|
@@ -86,7 +86,7 @@ function createProjectCloseController(options = {}) {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
const wasRunning = Boolean(isRunning(projectRoot));
|
|
89
|
-
stopDaemon(projectRoot);
|
|
89
|
+
stopDaemon(projectRoot, { source: `project-close:${projectRoot}` });
|
|
90
90
|
|
|
91
91
|
refreshProjects();
|
|
92
92
|
renderDashboard();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { exec } = require("child_process");
|
|
4
|
+
|
|
5
|
+
function parseShellCommand(text) {
|
|
6
|
+
const value = String(text || "").trim();
|
|
7
|
+
if (!value.startsWith("!")) return null;
|
|
8
|
+
const command = value.slice(1).trim();
|
|
9
|
+
if (!command) return null;
|
|
10
|
+
return command;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function runShellCommand(command, options = {}) {
|
|
14
|
+
const cmd = String(command || "").trim();
|
|
15
|
+
if (!cmd) return Promise.resolve({ ok: false, code: null, stdout: "", stderr: "empty command" });
|
|
16
|
+
const cwd = options.cwd || process.cwd();
|
|
17
|
+
const timeout = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 120000;
|
|
18
|
+
const maxBuffer = Number.isFinite(options.maxBuffer) ? options.maxBuffer : 1024 * 1024;
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
exec(cmd, {
|
|
21
|
+
cwd,
|
|
22
|
+
env: process.env,
|
|
23
|
+
timeout,
|
|
24
|
+
maxBuffer,
|
|
25
|
+
shell: process.env.SHELL || "/bin/sh",
|
|
26
|
+
}, (error, stdout, stderr) => {
|
|
27
|
+
resolve({
|
|
28
|
+
ok: !error,
|
|
29
|
+
code: error && Number.isFinite(error.code) ? error.code : 0,
|
|
30
|
+
signal: error && error.signal ? error.signal : null,
|
|
31
|
+
stdout: String(stdout || ""),
|
|
32
|
+
stderr: String(stderr || ""),
|
|
33
|
+
error: error && error.message ? error.message : "",
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
parseShellCommand,
|
|
41
|
+
runShellCommand,
|
|
42
|
+
};
|
package/src/chat/transport.js
CHANGED
|
@@ -2,6 +2,7 @@ const net = require("net");
|
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
const { spawn, spawnSync } = require("child_process");
|
|
5
|
+
const { resolveNodeExecutable } = require("../utils/nodeExecutable");
|
|
5
6
|
|
|
6
7
|
function connectSocket(sockPath, options = {}) {
|
|
7
8
|
const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0
|
|
@@ -57,21 +58,33 @@ function startDaemon(projectRoot, options = {}) {
|
|
|
57
58
|
const env = options.forceResume
|
|
58
59
|
? { ...process.env, UFOO_FORCE_RESUME: "1" }
|
|
59
60
|
: process.env;
|
|
60
|
-
const child = spawn(
|
|
61
|
+
const child = spawn(resolveNodeExecutable(), [daemonBin, "daemon", "--start"], {
|
|
61
62
|
detached: true,
|
|
62
63
|
stdio: "ignore",
|
|
63
64
|
cwd: projectRoot,
|
|
64
65
|
env,
|
|
65
66
|
});
|
|
67
|
+
child.on("error", (err) => {
|
|
68
|
+
if (typeof options.onError === "function") {
|
|
69
|
+
options.onError(err);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
66
72
|
child.unref();
|
|
73
|
+
return child;
|
|
67
74
|
}
|
|
68
75
|
|
|
69
|
-
function stopDaemon(projectRoot) {
|
|
76
|
+
function stopDaemon(projectRoot, options = {}) {
|
|
70
77
|
const daemonBin = resolveProjectFile(projectRoot, path.join("bin", "ufoo.js"), path.join("bin", "ufoo.js"));
|
|
71
|
-
|
|
78
|
+
const source = String(
|
|
79
|
+
options.source
|
|
80
|
+
|| `chat-transport pid=${process.pid} cwd=${process.cwd()} argv=${process.argv.join(" ")}`
|
|
81
|
+
);
|
|
82
|
+
const result = spawnSync(resolveNodeExecutable(), [daemonBin, "daemon", "--stop"], {
|
|
72
83
|
stdio: "ignore",
|
|
73
84
|
cwd: projectRoot,
|
|
85
|
+
env: { ...process.env, UFOO_DAEMON_STOP_SOURCE: source },
|
|
74
86
|
});
|
|
87
|
+
return Boolean(result && !result.error && result.status === 0);
|
|
75
88
|
}
|
|
76
89
|
|
|
77
90
|
async function connectWithRetry(sockPath, retries, delayMs, options = {}) {
|
package/src/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ const { resolveSoloAgentType } = require("./solo/commands");
|
|
|
14
14
|
const { listProjectRuntimes, getCurrentProjectRuntime } = require("./projects/registry");
|
|
15
15
|
const { canonicalProjectRoot, buildProjectId } = require("./projects/projectId");
|
|
16
16
|
const { getUfooPaths } = require("./ufoo/paths");
|
|
17
|
+
const { resolveNodeExecutable } = require("./utils/nodeExecutable");
|
|
17
18
|
|
|
18
19
|
function getPackageRoot() {
|
|
19
20
|
return path.resolve(__dirname, "..");
|
|
@@ -60,7 +61,7 @@ async function connectWithRetry(sockPath, retries, delayMs) {
|
|
|
60
61
|
async function ensureDaemonRunning(projectRoot) {
|
|
61
62
|
if (isRunning(projectRoot)) return;
|
|
62
63
|
const repoRoot = getPackageRoot();
|
|
63
|
-
run(
|
|
64
|
+
run(resolveNodeExecutable(), [path.join(repoRoot, "bin", "ufoo.js"), "daemon", "start"]);
|
|
64
65
|
const sock = socketPath(projectRoot);
|
|
65
66
|
for (let i = 0; i < 30; i += 1) {
|
|
66
67
|
if (fs.existsSync(sock)) {
|
|
@@ -1733,7 +1734,7 @@ async function runCli(argv) {
|
|
|
1733
1734
|
return;
|
|
1734
1735
|
}
|
|
1735
1736
|
if (cmd === "daemon") {
|
|
1736
|
-
run(
|
|
1737
|
+
run(resolveNodeExecutable(), [path.join(repoRoot, "bin", "ufoo.js"), "daemon", ...rest]);
|
|
1737
1738
|
return;
|
|
1738
1739
|
}
|
|
1739
1740
|
if (cmd === "chat") {
|
|
@@ -1741,7 +1742,7 @@ async function runCli(argv) {
|
|
|
1741
1742
|
if (rest.includes("-g") || rest.includes("--global")) {
|
|
1742
1743
|
chatArgs.push("-g");
|
|
1743
1744
|
}
|
|
1744
|
-
run(
|
|
1745
|
+
run(resolveNodeExecutable(), [path.join(repoRoot, "bin", "ufoo.js"), ...chatArgs]);
|
|
1745
1746
|
return;
|
|
1746
1747
|
}
|
|
1747
1748
|
if (cmd === "project") {
|
package/src/code/agent.js
CHANGED
|
@@ -491,6 +491,8 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
|
|
|
491
491
|
const runNativeAgentImpl = typeof options.runNativeAgentImpl === "function"
|
|
492
492
|
? options.runNativeAgentImpl
|
|
493
493
|
: runNativeAgentTask;
|
|
494
|
+
const onPhase = typeof options.onPhase === "function" ? options.onPhase : null;
|
|
495
|
+
const onThinkingDelta = typeof options.onThinkingDelta === "function" ? options.onThinkingDelta : null;
|
|
494
496
|
const invokeNative = (sessionIdValue = "", timeoutOverrideMs = timeoutMs) => runNativeAgentImpl({
|
|
495
497
|
workspaceRoot,
|
|
496
498
|
provider,
|
|
@@ -501,6 +503,8 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
|
|
|
501
503
|
sessionId: String(sessionIdValue || ""),
|
|
502
504
|
timeoutMs: timeoutOverrideMs,
|
|
503
505
|
onStreamDelta: onStream,
|
|
506
|
+
onThinkingDelta,
|
|
507
|
+
onPhase,
|
|
504
508
|
onToolEvent: (event) => {
|
|
505
509
|
pushToolLog(event);
|
|
506
510
|
},
|