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.
Files changed (70) hide show
  1. package/README.md +25 -9
  2. package/README.zh-CN.md +22 -9
  3. package/SKILLS/ubus/SKILL.md +15 -9
  4. package/SKILLS/ufoo/SKILL.md +20 -6
  5. package/dist/tui/darwin-arm64/ufoo-tui +0 -0
  6. package/dist/tui/darwin-x64/ufoo-tui +0 -0
  7. package/dist/tui/linux-arm64/ufoo-tui +0 -0
  8. package/dist/tui/linux-x64/ufoo-tui +0 -0
  9. package/package.json +11 -3
  10. package/scripts/pack-tui.js +112 -0
  11. package/scripts/postinstall.js +11 -0
  12. package/src/agents/activity/activityReconcile.js +106 -0
  13. package/src/agents/activity/activityStatePublisher.js +31 -2
  14. package/src/agents/activity/index.js +1 -0
  15. package/src/agents/launch/launcher.js +19 -0
  16. package/src/agents/launch/ptyRunner.js +20 -1
  17. package/src/agents/prompts/groupBootstrap.js +4 -0
  18. package/src/agents/prompts/native/ufoo.js +2 -0
  19. package/src/app/chat/ChatController.js +433 -0
  20. package/src/app/chat/agentDirectory.js +63 -0
  21. package/src/app/chat/agentEnter.js +70 -0
  22. package/src/app/chat/agentIdentity.js +50 -0
  23. package/src/app/chat/bootstrap.js +66 -0
  24. package/src/app/chat/commandExecutor.js +108 -0
  25. package/src/app/chat/commands.js +31 -0
  26. package/src/app/chat/dashboardView.js +6 -2
  27. package/src/app/chat/historyStore.js +181 -0
  28. package/src/app/chat/index.js +14 -2
  29. package/src/app/chat/inputSubmitHandler.js +21 -7
  30. package/src/app/chat/ipcBuilders.js +52 -0
  31. package/src/app/chat/multiWindow/paneManager.js +10 -1
  32. package/src/app/chat/multiWindow/renderer.js +1 -1
  33. package/src/app/chat/multiWindow/vtFrame.js +93 -0
  34. package/src/app/chat/streamState.js +182 -0
  35. package/src/app/cli/features/doctor.js +22 -0
  36. package/src/code/UCODE_PROMPT.md +2 -0
  37. package/src/code/UcodeController.js +156 -0
  38. package/src/code/context/planGraphService.js +4 -0
  39. package/src/code/repl.js +4 -3
  40. package/src/code/runtime/taskLoop.js +46 -50
  41. package/src/code/tui.js +13 -2
  42. package/src/code/ucodeSlashDispatch.js +241 -0
  43. package/src/coordination/bus/activate.js +3 -0
  44. package/src/runtime/contracts/schemas/ufoo-ui-v1/envelope.json +28 -0
  45. package/src/runtime/contracts/uiProtocol.js +190 -0
  46. package/src/ui/{ink/chatLogModel.js → chatLogModel.js} +2 -2
  47. package/src/ui/dashboardBridge.js +81 -0
  48. package/src/ui/format/index.js +2 -2
  49. package/src/ui/index.js +8 -4
  50. package/src/ui/multiPaneBusMirror.js +137 -0
  51. package/src/ui/multiWindowHandoff.js +232 -0
  52. package/src/ui/ptyHandoff.js +23 -0
  53. package/src/ui/rustChatHost.js +1520 -0
  54. package/src/ui/rustMultiSession.js +497 -0
  55. package/src/ui/rustUcodeHost.js +999 -0
  56. package/src/ui/scrollbackReplay.js +82 -0
  57. package/src/ui/settingsBridge.js +49 -0
  58. package/src/ui/toolMergeBridge.js +66 -0
  59. package/src/ui/tuiLauncher.js +105 -0
  60. package/src/ui/ucodeStatusLine.js +74 -0
  61. package/src/ui/uiHostServer.js +339 -0
  62. package/src/ui/MIGRATION.md +0 -334
  63. package/src/ui/ink/ChatApp.js +0 -4163
  64. package/src/ui/ink/DashboardBar.js +0 -691
  65. package/src/ui/ink/InkDemo.js +0 -96
  66. package/src/ui/ink/MultilineInput.js +0 -662
  67. package/src/ui/ink/UcodeApp.js +0 -1675
  68. package/src/ui/ink/agentMirror.js +0 -730
  69. package/src/ui/ink/chatReducer.js +0 -473
  70. 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");
@@ -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
- state,
355
- resumeSessionState,
356
- persistSessionState,
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
- try {
300
- const advanced = runPlanGraphCommand({
301
- operation: "patch",
302
- operations: [],
303
- commandId: `advance_${live.id}_${Date.now()}`,
304
- }, {
305
- executionState,
306
- runTool: wrappedRunTool,
307
- autoAdvance: true,
308
- knownTools: options.knownTools,
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
- // Persist child back
311
- if (executionState.planGraph) {
312
- executionState.planGraph.owner = child.owner;
313
- executionState.planGraph.parentGraphId = child.parentGraphId;
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: "awaiting_complete_task",
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 { runUcodeInkTui } = require("../ui/ink/UcodeApp");
42
- return runUcodeInkTui(props);
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 = {