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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convert a virtual-terminal screen into ANSI-styled text lines suitable for
|
|
5
|
+
* shipping over `multi.pane.frame`. Rust decodes these with `ansi-to-tui`.
|
|
6
|
+
*
|
|
7
|
+
* The colour/attribute-to-ANSI logic mirrors renderer.js so the rendered
|
|
8
|
+
* frame matches the Ink multi-window presentation.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const MAX_LINE_BYTES = 4096;
|
|
12
|
+
|
|
13
|
+
function attrToAnsi(attr) {
|
|
14
|
+
const parts = [];
|
|
15
|
+
if (attr.bold) parts.push("1");
|
|
16
|
+
if (attr.dim) parts.push("2");
|
|
17
|
+
if (attr.italic) parts.push("3");
|
|
18
|
+
if (attr.underline) parts.push("4");
|
|
19
|
+
if (attr.inverse) parts.push("7");
|
|
20
|
+
if (attr.fgRgb) {
|
|
21
|
+
parts.push(`38;2;${attr.fgRgb[0]};${attr.fgRgb[1]};${attr.fgRgb[2]}`);
|
|
22
|
+
} else if (attr.fg !== 7) {
|
|
23
|
+
if (attr.fg < 8) parts.push(String(30 + attr.fg));
|
|
24
|
+
else if (attr.fg < 16) parts.push(String(90 + attr.fg - 8));
|
|
25
|
+
else parts.push(`38;5;${attr.fg}`);
|
|
26
|
+
}
|
|
27
|
+
if (attr.bgRgb) {
|
|
28
|
+
parts.push(`48;2;${attr.bgRgb[0]};${attr.bgRgb[1]};${attr.bgRgb[2]}`);
|
|
29
|
+
} else if (attr.bg > 0) {
|
|
30
|
+
if (attr.bg < 8) parts.push(String(40 + attr.bg));
|
|
31
|
+
else if (attr.bg < 16) parts.push(String(100 + attr.bg - 8));
|
|
32
|
+
else parts.push(`48;5;${attr.bg}`);
|
|
33
|
+
}
|
|
34
|
+
return parts.length > 0 ? `\x1b[${parts.join(";")}m` : "";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function truncateLineBytes(line, maxBytes) {
|
|
38
|
+
if (!line) return "";
|
|
39
|
+
if (Buffer.byteLength(line, "utf8") <= maxBytes) return line;
|
|
40
|
+
const buf = Buffer.from(line, "utf8");
|
|
41
|
+
return buf.slice(0, maxBytes).toString("utf8");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function vtScreenToAnsiLines(vt, options = {}) {
|
|
45
|
+
if (!vt || typeof vt.getScreen !== "function") return [];
|
|
46
|
+
const { buffer, rows, cols, cursorRow, cursorCol } = vt.getScreen();
|
|
47
|
+
const maxRows = Number.isFinite(options.maxRows) && options.maxRows > 0
|
|
48
|
+
? Math.floor(options.maxRows)
|
|
49
|
+
: rows;
|
|
50
|
+
const maxCols = Number.isFinite(options.maxCols) && options.maxCols > 0
|
|
51
|
+
? Math.floor(options.maxCols)
|
|
52
|
+
: cols;
|
|
53
|
+
const cursorInverse = Boolean(options.cursorInverse);
|
|
54
|
+
const reset = "\x1b[0m";
|
|
55
|
+
const limit = Math.min(rows, maxRows);
|
|
56
|
+
const out = [];
|
|
57
|
+
for (let r = 0; r < limit; r++) {
|
|
58
|
+
const row = buffer[r] || [];
|
|
59
|
+
let line = "";
|
|
60
|
+
let lastAttr = "";
|
|
61
|
+
let col = 0;
|
|
62
|
+
let c = 0;
|
|
63
|
+
while (col < maxCols && c < cols) {
|
|
64
|
+
const cell = row[c];
|
|
65
|
+
if (!cell) {
|
|
66
|
+
line += " ";
|
|
67
|
+
col += 1;
|
|
68
|
+
c += 1;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (cell.wideContinuation) {
|
|
72
|
+
c += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const isCursor = cursorInverse && r === cursorRow && c === cursorCol;
|
|
76
|
+
const attr = isCursor ? { ...cell.attr, inverse: !cell.attr.inverse } : cell.attr;
|
|
77
|
+
const ansi = attrToAnsi(attr);
|
|
78
|
+
if (ansi !== lastAttr) {
|
|
79
|
+
if (lastAttr) line += reset;
|
|
80
|
+
line += ansi;
|
|
81
|
+
lastAttr = ansi;
|
|
82
|
+
}
|
|
83
|
+
line += cell.char || " ";
|
|
84
|
+
col += 1;
|
|
85
|
+
c += 1;
|
|
86
|
+
}
|
|
87
|
+
if (lastAttr) line += reset;
|
|
88
|
+
out.push(truncateLineBytes(line, MAX_LINE_BYTES));
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { vtScreenToAnsiLines, MAX_LINE_BYTES };
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Burst-coalescing sender + chat stream state (Phase 0B extraction from ChatApp).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { stripBlessedTags } = require("../../ui/chatLogModel");
|
|
8
|
+
|
|
9
|
+
const STREAM_FLUSH_INTERVAL_MS = 80;
|
|
10
|
+
|
|
11
|
+
function createThrottledSender(send, windowMs = 500) {
|
|
12
|
+
let lastSentAt = 0;
|
|
13
|
+
let timer = null;
|
|
14
|
+
const fire = () => {
|
|
15
|
+
timer = null;
|
|
16
|
+
lastSentAt = Date.now();
|
|
17
|
+
send();
|
|
18
|
+
};
|
|
19
|
+
return () => {
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
const elapsed = now - lastSentAt;
|
|
22
|
+
if (elapsed >= windowMs) {
|
|
23
|
+
if (timer) {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
timer = null;
|
|
26
|
+
}
|
|
27
|
+
lastSentAt = now;
|
|
28
|
+
send();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (!timer) {
|
|
32
|
+
timer = setTimeout(fire, windowMs - elapsed);
|
|
33
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createChatStreamState({
|
|
39
|
+
dispatch,
|
|
40
|
+
appendHistory,
|
|
41
|
+
displayNameForPublisher = (value) => value,
|
|
42
|
+
flushIntervalMs = STREAM_FLUSH_INTERVAL_MS,
|
|
43
|
+
} = {}) {
|
|
44
|
+
const streams = new Map();
|
|
45
|
+
const pendingDeliveries = new Map();
|
|
46
|
+
const pendingDeltas = new Map();
|
|
47
|
+
let flushTimer = null;
|
|
48
|
+
|
|
49
|
+
function flushDeltas() {
|
|
50
|
+
if (flushTimer) {
|
|
51
|
+
clearTimeout(flushTimer);
|
|
52
|
+
flushTimer = null;
|
|
53
|
+
}
|
|
54
|
+
if (pendingDeltas.size === 0) return;
|
|
55
|
+
for (const batch of pendingDeltas.values()) {
|
|
56
|
+
dispatch({
|
|
57
|
+
type: "stream/delta",
|
|
58
|
+
publisher: batch.publisher,
|
|
59
|
+
delta: batch.parts.join(""),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
pendingDeltas.clear();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function scheduleFlush() {
|
|
66
|
+
if (flushTimer) return;
|
|
67
|
+
flushTimer = setTimeout(flushDeltas, flushIntervalMs);
|
|
68
|
+
if (typeof flushTimer.unref === "function") flushTimer.unref();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function deliveryKey(agentId, agentLabel) {
|
|
72
|
+
return String(agentId || agentLabel || "").trim();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function markPendingDelivery(agentId, agentLabel) {
|
|
76
|
+
const key = deliveryKey(agentId, agentLabel);
|
|
77
|
+
if (!key) return;
|
|
78
|
+
const existing = pendingDeliveries.get(key) || { count: 0, keys: new Set() };
|
|
79
|
+
existing.count += 1;
|
|
80
|
+
for (const candidate of [agentId, agentLabel]) {
|
|
81
|
+
const value = String(candidate || "").trim();
|
|
82
|
+
if (value) {
|
|
83
|
+
pendingDeliveries.set(value, existing);
|
|
84
|
+
existing.keys.add(value);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getPendingState(publisher, displayName) {
|
|
90
|
+
for (const candidate of [publisher, displayName]) {
|
|
91
|
+
const key = String(candidate || "").trim();
|
|
92
|
+
if (key && pendingDeliveries.has(key)) {
|
|
93
|
+
return { key, state: pendingDeliveries.get(key) };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function consumePendingDelivery(publisher, displayName) {
|
|
100
|
+
const hit = getPendingState(publisher, displayName);
|
|
101
|
+
if (!hit) return false;
|
|
102
|
+
hit.state.count -= 1;
|
|
103
|
+
if (hit.state.count <= 0) {
|
|
104
|
+
for (const key of hit.state.keys || []) pendingDeliveries.delete(key);
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function beginStream(publisher, prefix, continuationPrefix, meta) {
|
|
110
|
+
const key = String(publisher || "bus");
|
|
111
|
+
let state = streams.get(key);
|
|
112
|
+
if (state) return state;
|
|
113
|
+
const displayName = stripBlessedTags(prefix || displayNameForPublisher(key) || key)
|
|
114
|
+
.replace(/\s*·\s*$/, "")
|
|
115
|
+
.trim() || displayNameForPublisher(key) || key;
|
|
116
|
+
state = {
|
|
117
|
+
publisher: key,
|
|
118
|
+
displayName,
|
|
119
|
+
prefix,
|
|
120
|
+
continuationPrefix,
|
|
121
|
+
parts: [],
|
|
122
|
+
meta: meta || {},
|
|
123
|
+
};
|
|
124
|
+
streams.set(key, state);
|
|
125
|
+
dispatch({ type: "stream/begin", publisher: displayName });
|
|
126
|
+
return state;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function appendStreamDelta(state, delta) {
|
|
130
|
+
if (!state || !delta) return;
|
|
131
|
+
const text = String(delta || "");
|
|
132
|
+
state.parts.push(text);
|
|
133
|
+
let batch = pendingDeltas.get(state.publisher);
|
|
134
|
+
if (!batch) {
|
|
135
|
+
batch = { publisher: state.displayName || state.publisher, parts: [] };
|
|
136
|
+
pendingDeltas.set(state.publisher, batch);
|
|
137
|
+
}
|
|
138
|
+
batch.parts.push(text);
|
|
139
|
+
scheduleFlush();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function finalizeStream(publisher, meta, reason = "") {
|
|
143
|
+
const key = String(publisher || "bus");
|
|
144
|
+
const state = streams.get(key);
|
|
145
|
+
if (!state) return;
|
|
146
|
+
flushDeltas();
|
|
147
|
+
dispatch({ type: "stream/end" });
|
|
148
|
+
if (typeof appendHistory === "function") {
|
|
149
|
+
const full = state.parts.join("");
|
|
150
|
+
const text = state.displayName
|
|
151
|
+
? `${state.displayName}: ${full}`
|
|
152
|
+
: full;
|
|
153
|
+
appendHistory("bus", text, { ...(meta || state.meta || {}), stream_done: true, stream_reason: reason });
|
|
154
|
+
}
|
|
155
|
+
streams.delete(key);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function hasStream(publisher) {
|
|
159
|
+
return streams.has(String(publisher || "bus"));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
markPendingDelivery,
|
|
164
|
+
getPendingState,
|
|
165
|
+
consumePendingDelivery,
|
|
166
|
+
beginStream,
|
|
167
|
+
appendStreamDelta,
|
|
168
|
+
finalizeStream,
|
|
169
|
+
hasStream,
|
|
170
|
+
flushDeltas,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** @deprecated Prefer createChatStreamState. */
|
|
175
|
+
const createInkStreamState = createChatStreamState;
|
|
176
|
+
|
|
177
|
+
module.exports = {
|
|
178
|
+
STREAM_FLUSH_INTERVAL_MS,
|
|
179
|
+
createThrottledSender,
|
|
180
|
+
createChatStreamState,
|
|
181
|
+
createInkStreamState,
|
|
182
|
+
};
|
|
@@ -13,6 +13,27 @@ class RepoDoctor {
|
|
|
13
13
|
this.failed = true;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
reportTui() {
|
|
17
|
+
try {
|
|
18
|
+
const { resolveTuiLaunchPlan, resolveUfooTuiBinary } = require("../../../ui/tuiLauncher");
|
|
19
|
+
const binary = resolveUfooTuiBinary();
|
|
20
|
+
const plan = resolveTuiLaunchPlan({ mode: process.env.UFOO_TUI || "auto" });
|
|
21
|
+
console.log("TUI:");
|
|
22
|
+
console.log(`- UFOO_TUI=${process.env.UFOO_TUI || "auto"} → ${plan.mode} (${plan.reason})`);
|
|
23
|
+
if (binary) {
|
|
24
|
+
console.log(`- binary: ${binary}${plan.version ? ` (${plan.version})` : ""}`);
|
|
25
|
+
} else {
|
|
26
|
+
console.log("- binary: missing (required; Ink TUI removed)");
|
|
27
|
+
}
|
|
28
|
+
console.log("- force: UFOO_TUI=rust | UFOO_TUI_BIN=/path/to/ufoo-tui");
|
|
29
|
+
if (plan.mode === "error") {
|
|
30
|
+
console.log(`- note: chat/ucode will fail until ufoo-tui is built (${plan.reason})`);
|
|
31
|
+
}
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.log(`TUI: unavailable (${err && err.message ? err.message : err})`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
16
37
|
run() {
|
|
17
38
|
const skillsDir = path.join(this.repoRoot, "SKILLS");
|
|
18
39
|
const contextSkill = path.join(skillsDir, "uctx", "SKILL.md");
|
|
@@ -30,6 +51,7 @@ class RepoDoctor {
|
|
|
30
51
|
console.log("Skills:");
|
|
31
52
|
if (fs.existsSync(contextSkill)) console.log(`- uctx: ${contextSkill}`);
|
|
32
53
|
if (fs.existsSync(busSkill)) console.log(`- ubus: ${busSkill}`);
|
|
54
|
+
this.reportTui();
|
|
33
55
|
|
|
34
56
|
if (this.failed) {
|
|
35
57
|
console.log("Status: FAILED");
|
package/src/code/UCODE_PROMPT.md
CHANGED
|
@@ -17,11 +17,13 @@ ufoo integration requirements:
|
|
|
17
17
|
- Use shared memory for durable project facts. Read existing memory before writing new memory; do not use it for transient task state.
|
|
18
18
|
- Support launch/close/resume/inject flows managed by ufoo daemon.
|
|
19
19
|
- Prefer canonical ufoo commands (`ufoo ctx`, `ufoo bus`, `ufoo memory`, `ufoo report`) for coordination and status sync.
|
|
20
|
+
- 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.
|
|
20
21
|
|
|
21
22
|
Execution protocol:
|
|
22
23
|
- On session start, check context quickly:
|
|
23
24
|
`ufoo ctx decisions -l`
|
|
24
25
|
`ufoo ctx decisions -n 1`
|
|
26
|
+
- If `ubus` is explicitly requested, execute its pending-message flow immediately; this does not change the no-polling rule after you send a message.
|
|
25
27
|
- After handling work that arrived from chat (`[manual]<to:...>`) or bus (`[ufoo]<from:...>`), report lifecycle:
|
|
26
28
|
`ufoo report start|progress|done|error "<short summary>"`
|
|
27
29
|
Do not emulate report failures with `ufoo bus send ufoo-agent ...`; if `ufoo report` fails, continue without a fallback bus report.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Headless UcodeController — task serial queue, cancel, view ports.
|
|
5
|
+
*
|
|
6
|
+
* Runner/tools/session stay in src/code/*; Ink/Rust hosts attach via ports.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function createUcodeController({
|
|
10
|
+
projectRoot = process.cwd(),
|
|
11
|
+
ports = {},
|
|
12
|
+
} = {}) {
|
|
13
|
+
const view = {
|
|
14
|
+
dispatch: typeof ports.dispatch === "function" ? ports.dispatch : () => {},
|
|
15
|
+
setStatus: typeof ports.setStatus === "function" ? ports.setStatus : () => {},
|
|
16
|
+
appendLog: typeof ports.appendLog === "function" ? ports.appendLog : () => {},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let started = false;
|
|
20
|
+
let abortController = null;
|
|
21
|
+
let chain = Promise.resolve();
|
|
22
|
+
let queueDepth = 0;
|
|
23
|
+
|
|
24
|
+
function start() {
|
|
25
|
+
started = true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function stop() {
|
|
29
|
+
if (abortController) {
|
|
30
|
+
try {
|
|
31
|
+
abortController.abort();
|
|
32
|
+
} catch {
|
|
33
|
+
// ignore
|
|
34
|
+
}
|
|
35
|
+
abortController = null;
|
|
36
|
+
}
|
|
37
|
+
queueDepth = 0;
|
|
38
|
+
started = false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function beginTask() {
|
|
42
|
+
abortController = new AbortController();
|
|
43
|
+
queueDepth = Math.max(1, queueDepth);
|
|
44
|
+
return abortController;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function endTask() {
|
|
48
|
+
if (queueDepth > 0) queueDepth -= 1;
|
|
49
|
+
if (queueDepth === 0) abortController = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function cancelTask() {
|
|
53
|
+
if (abortController) abortController.abort();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isBusy() {
|
|
57
|
+
return queueDepth > 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Serialize async work so NL / auto-bus / resume never overlap.
|
|
62
|
+
* AbortController is allocated synchronously so cancel works before the slot starts.
|
|
63
|
+
*/
|
|
64
|
+
function runExclusive(fn) {
|
|
65
|
+
const abort = new AbortController();
|
|
66
|
+
queueDepth += 1;
|
|
67
|
+
abortController = abort;
|
|
68
|
+
const run = chain.then(async () => {
|
|
69
|
+
if (!started) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
abortController = abort;
|
|
73
|
+
try {
|
|
74
|
+
if (abort.signal.aborted) {
|
|
75
|
+
const err = new Error("aborted");
|
|
76
|
+
err.name = "AbortError";
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
return await fn(abort);
|
|
80
|
+
} finally {
|
|
81
|
+
queueDepth = Math.max(0, queueDepth - 1);
|
|
82
|
+
if (abortController === abort) abortController = null;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
chain = run.catch(() => {});
|
|
86
|
+
return run;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
projectRoot,
|
|
91
|
+
view,
|
|
92
|
+
isStarted: () => started,
|
|
93
|
+
isBusy,
|
|
94
|
+
start,
|
|
95
|
+
stop,
|
|
96
|
+
beginTask,
|
|
97
|
+
endTask,
|
|
98
|
+
cancelTask,
|
|
99
|
+
runExclusive,
|
|
100
|
+
getAbortSignal: () => (abortController ? abortController.signal : null),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Throttle thinking_delta → status.set so fast streams don't flood the UI.
|
|
106
|
+
*/
|
|
107
|
+
function createThinkingStatusPublisher(publish, options = {}) {
|
|
108
|
+
const intervalMs = Number(options.intervalMs) > 0 ? Number(options.intervalMs) : 120;
|
|
109
|
+
let tail = "";
|
|
110
|
+
let timer = null;
|
|
111
|
+
let lastFlush = 0;
|
|
112
|
+
|
|
113
|
+
function collapse(text) {
|
|
114
|
+
const raw = String(text || "").replace(/\s+/g, " ").trim();
|
|
115
|
+
if (!raw) return "Thinking…";
|
|
116
|
+
return raw.length > 72 ? `${raw.slice(-72)}` : raw;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function flush() {
|
|
120
|
+
timer = null;
|
|
121
|
+
lastFlush = Date.now();
|
|
122
|
+
publish("status.set", {
|
|
123
|
+
text: collapse(tail),
|
|
124
|
+
busy: true,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function onThinkingDelta(chunk) {
|
|
129
|
+
tail += String(chunk || "");
|
|
130
|
+
const elapsed = Date.now() - lastFlush;
|
|
131
|
+
if (elapsed >= intervalMs) {
|
|
132
|
+
flush();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (!timer) {
|
|
136
|
+
timer = setTimeout(flush, Math.max(16, intervalMs - elapsed));
|
|
137
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function reset() {
|
|
142
|
+
if (timer) {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
timer = null;
|
|
145
|
+
}
|
|
146
|
+
tail = "";
|
|
147
|
+
lastFlush = 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { onThinkingDelta, reset, flush };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = {
|
|
154
|
+
createUcodeController,
|
|
155
|
+
createThinkingStatusPublisher,
|
|
156
|
+
};
|
|
@@ -121,6 +121,10 @@ function finalizeTerminalPrimaryPlan(executionState = null, planGraph = null, ad
|
|
|
121
121
|
const pg = planGraph && typeof planGraph === "object" ? planGraph : null;
|
|
122
122
|
if (!pg || !pg.graphId) return null;
|
|
123
123
|
if (!advance || String(advance.yieldReason || "") !== "graph_terminal") return null;
|
|
124
|
+
// TaskLoop children may briefly sit in planGraph during processTaskRun;
|
|
125
|
+
// never archive them as the Agent Loop primary (they stay in graphs[]).
|
|
126
|
+
const ownerKind = String((pg.owner && pg.owner.kind) || "agent_loop").trim();
|
|
127
|
+
if (ownerKind === "task_loop") return null;
|
|
124
128
|
|
|
125
129
|
const completionSummary = buildPlanCompletionSummary(pg, advance);
|
|
126
130
|
const archived = archivePlanGraph(executionState, pg, { reason: "graph_terminal" });
|
package/src/code/repl.js
CHANGED
|
@@ -350,10 +350,11 @@ async function runUcodeCoreAgent({
|
|
|
350
350
|
runNaturalLanguageTask,
|
|
351
351
|
runUbusCommand,
|
|
352
352
|
formatNlResult,
|
|
353
|
+
submitUserInteractionAnswer,
|
|
353
354
|
workspaceRoot,
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
355
|
+
state,
|
|
356
|
+
resumeSessionState,
|
|
357
|
+
persistSessionState,
|
|
357
358
|
autoBus: {
|
|
358
359
|
enabled: shouldAutoConsumeBus(process.env.UFOO_SUBSCRIBER_ID || ""),
|
|
359
360
|
getPendingCount: () => getPendingBusCount(state.workspaceRoot || workspaceRoot, process.env.UFOO_SUBSCRIBER_ID || ""),
|
|
@@ -263,10 +263,10 @@ function processTaskRun(executionState = null, taskRunId = "", options = {}) {
|
|
|
263
263
|
live.lastFocusText = renderTaskFocusText(focus);
|
|
264
264
|
putTaskRun(executionState, live);
|
|
265
265
|
|
|
266
|
-
// Advance child graph tools if runTool provided
|
|
266
|
+
// Advance child graph tools if runTool provided. Target via graphId so
|
|
267
|
+
// runPlanGraphCommand keeps the Agent Loop primary intact and does not
|
|
268
|
+
// treat the TaskLoop child as a terminal primary plan.
|
|
267
269
|
if (typeof options.runTool === "function") {
|
|
268
|
-
const previousActive = executionState.planGraph;
|
|
269
|
-
executionState.planGraph = child;
|
|
270
270
|
const wrappedRunTool = (toolInput = {}) => {
|
|
271
271
|
const current = getTaskRun(executionState, taskRunId);
|
|
272
272
|
if (!current || current.cancelRequested || current.status === "cancelling") {
|
|
@@ -296,62 +296,58 @@ function processTaskRun(executionState = null, taskRunId = "", options = {}) {
|
|
|
296
296
|
});
|
|
297
297
|
return options.runTool(toolInput);
|
|
298
298
|
};
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
299
|
+
const advanced = runPlanGraphCommand({
|
|
300
|
+
operation: "patch",
|
|
301
|
+
graphId: live.childGraphId,
|
|
302
|
+
operations: [],
|
|
303
|
+
commandId: `advance_${live.id}_${Date.now()}`,
|
|
304
|
+
}, {
|
|
305
|
+
executionState,
|
|
306
|
+
runTool: wrappedRunTool,
|
|
307
|
+
autoAdvance: true,
|
|
308
|
+
knownTools: options.knownTools,
|
|
309
|
+
});
|
|
310
|
+
const after = getGraph(executionState, live.childGraphId);
|
|
311
|
+
if (after) {
|
|
312
|
+
after.owner = child.owner || after.owner;
|
|
313
|
+
after.parentGraphId = child.parentGraphId || after.parentGraphId;
|
|
314
|
+
after.parentNodeId = child.parentNodeId || after.parentNodeId;
|
|
315
|
+
setGraph(executionState, after);
|
|
316
|
+
}
|
|
317
|
+
casTaskRunStatus(executionState, live.id, {
|
|
318
|
+
expectedStatus: "running",
|
|
319
|
+
nextStatus: "running",
|
|
320
|
+
phase: after && after.waitingFor ? "waiting_model" : "executing_tools",
|
|
321
|
+
});
|
|
322
|
+
syncParentNodeFromRun(executionState, getTaskRun(executionState, taskRunId));
|
|
323
|
+
|
|
324
|
+
if (after && after.waitingFor) {
|
|
325
|
+
routeGraphYield(executionState, {
|
|
326
|
+
graph: after,
|
|
327
|
+
reason: after.lastYieldReason || "llm_required",
|
|
328
|
+
waitingFor: after.waitingFor,
|
|
309
329
|
});
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
executionState.planGraph.parentNodeId = child.parentNodeId;
|
|
315
|
-
setGraph(executionState, executionState.planGraph);
|
|
316
|
-
}
|
|
317
|
-
const after = getGraph(executionState, live.childGraphId) || executionState.planGraph;
|
|
318
|
-
casTaskRunStatus(executionState, live.id, {
|
|
319
|
-
expectedStatus: "running",
|
|
320
|
-
nextStatus: "running",
|
|
321
|
-
phase: after && after.waitingFor ? "waiting_model" : "executing_tools",
|
|
330
|
+
enqueueTaskEvent(executionState, live.id, {
|
|
331
|
+
kind: "model_turn",
|
|
332
|
+
waitingFor: after.waitingFor,
|
|
333
|
+
focusText: live.lastFocusText,
|
|
322
334
|
});
|
|
323
|
-
syncParentNodeFromRun(executionState, getTaskRun(executionState, taskRunId));
|
|
324
|
-
|
|
325
|
-
if (after && after.waitingFor) {
|
|
326
|
-
routeGraphYield(executionState, {
|
|
327
|
-
graph: after,
|
|
328
|
-
reason: after.lastYieldReason || "llm_required",
|
|
329
|
-
waitingFor: after.waitingFor,
|
|
330
|
-
});
|
|
331
|
-
enqueueTaskEvent(executionState, live.id, {
|
|
332
|
-
kind: "model_turn",
|
|
333
|
-
waitingFor: after.waitingFor,
|
|
334
|
-
focusText: live.lastFocusText,
|
|
335
|
-
});
|
|
336
|
-
return {
|
|
337
|
-
ok: true,
|
|
338
|
-
status: "running",
|
|
339
|
-
yieldReason: "llm_required",
|
|
340
|
-
focusText: live.lastFocusText,
|
|
341
|
-
waitingFor: after.waitingFor,
|
|
342
|
-
advance: advanced.modelPayload || advanced,
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
335
|
return {
|
|
346
336
|
ok: true,
|
|
347
337
|
status: "running",
|
|
348
|
-
yieldReason: "
|
|
338
|
+
yieldReason: "llm_required",
|
|
349
339
|
focusText: live.lastFocusText,
|
|
340
|
+
waitingFor: after.waitingFor,
|
|
350
341
|
advance: advanced.modelPayload || advanced,
|
|
351
342
|
};
|
|
352
|
-
} finally {
|
|
353
|
-
executionState.planGraph = previousActive;
|
|
354
343
|
}
|
|
344
|
+
return {
|
|
345
|
+
ok: true,
|
|
346
|
+
status: "running",
|
|
347
|
+
yieldReason: "awaiting_complete_task",
|
|
348
|
+
focusText: live.lastFocusText,
|
|
349
|
+
advance: advanced.modelPayload || advanced,
|
|
350
|
+
};
|
|
355
351
|
}
|
|
356
352
|
|
|
357
353
|
enqueueTaskEvent(executionState, live.id, {
|
package/src/code/tui.js
CHANGED
|
@@ -38,8 +38,19 @@ const {
|
|
|
38
38
|
} = fmt;
|
|
39
39
|
|
|
40
40
|
function runUcodeTui(props = {}) {
|
|
41
|
-
const {
|
|
42
|
-
|
|
41
|
+
const { resolveTuiLaunchPlan } = require("../ui/tuiLauncher");
|
|
42
|
+
const plan = resolveTuiLaunchPlan({
|
|
43
|
+
mode: props.tuiMode || process.env.UFOO_TUI,
|
|
44
|
+
surface: "ucode",
|
|
45
|
+
});
|
|
46
|
+
if (plan.mode !== "rust") {
|
|
47
|
+
const err = new Error(`Rust TUI unavailable (${plan.reason})`);
|
|
48
|
+
err.code = "UFOO_TUI_UNAVAILABLE";
|
|
49
|
+
err.plan = plan;
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
const { runUcodeRust } = require("../ui/rustUcodeHost");
|
|
53
|
+
return runUcodeRust({ ...props, tuiMode: "rust" });
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
module.exports = {
|