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.
Files changed (65) hide show
  1. package/README.md +25 -9
  2. package/README.zh-CN.md +22 -9
  3. package/dist/tui/darwin-arm64/ufoo-tui +0 -0
  4. package/dist/tui/darwin-x64/ufoo-tui +0 -0
  5. package/dist/tui/linux-arm64/ufoo-tui +0 -0
  6. package/dist/tui/linux-x64/ufoo-tui +0 -0
  7. package/package.json +12 -4
  8. package/scripts/pack-tui.js +112 -0
  9. package/scripts/postinstall.js +11 -0
  10. package/src/agents/activity/activityReconcile.js +106 -0
  11. package/src/agents/activity/activityStatePublisher.js +31 -2
  12. package/src/agents/activity/index.js +1 -0
  13. package/src/agents/launch/launcher.js +19 -0
  14. package/src/agents/launch/ptyRunner.js +20 -1
  15. package/src/app/chat/ChatController.js +433 -0
  16. package/src/app/chat/agentDirectory.js +63 -0
  17. package/src/app/chat/agentEnter.js +70 -0
  18. package/src/app/chat/agentIdentity.js +50 -0
  19. package/src/app/chat/bootstrap.js +66 -0
  20. package/src/app/chat/commandExecutor.js +108 -0
  21. package/src/app/chat/commands.js +38 -1
  22. package/src/app/chat/dashboardView.js +6 -2
  23. package/src/app/chat/historyStore.js +181 -0
  24. package/src/app/chat/index.js +14 -2
  25. package/src/app/chat/inputSubmitHandler.js +21 -7
  26. package/src/app/chat/ipcBuilders.js +52 -0
  27. package/src/app/chat/multiWindow/paneManager.js +10 -1
  28. package/src/app/chat/multiWindow/renderer.js +1 -1
  29. package/src/app/chat/multiWindow/vtFrame.js +93 -0
  30. package/src/app/chat/streamState.js +182 -0
  31. package/src/app/cli/features/doctor.js +22 -0
  32. package/src/code/UcodeController.js +156 -0
  33. package/src/code/context/planGraphService.js +4 -0
  34. package/src/code/repl.js +4 -3
  35. package/src/code/runtime/taskLoop.js +46 -50
  36. package/src/code/tui.js +13 -2
  37. package/src/code/ucodeSlashDispatch.js +241 -0
  38. package/src/coordination/bus/activate.js +3 -0
  39. package/src/runtime/contracts/schemas/ufoo-ui-v1/envelope.json +28 -0
  40. package/src/runtime/contracts/uiProtocol.js +190 -0
  41. package/src/ui/{ink/chatLogModel.js → chatLogModel.js} +2 -2
  42. package/src/ui/dashboardBridge.js +81 -0
  43. package/src/ui/format/index.js +2 -2
  44. package/src/ui/index.js +8 -4
  45. package/src/ui/multiPaneBusMirror.js +137 -0
  46. package/src/ui/multiWindowHandoff.js +232 -0
  47. package/src/ui/ptyHandoff.js +23 -0
  48. package/src/ui/rustChatHost.js +1520 -0
  49. package/src/ui/rustMultiSession.js +497 -0
  50. package/src/ui/rustUcodeHost.js +999 -0
  51. package/src/ui/scrollbackReplay.js +82 -0
  52. package/src/ui/settingsBridge.js +49 -0
  53. package/src/ui/toolMergeBridge.js +66 -0
  54. package/src/ui/tuiLauncher.js +105 -0
  55. package/src/ui/ucodeStatusLine.js +74 -0
  56. package/src/ui/uiHostServer.js +339 -0
  57. package/src/ui/MIGRATION.md +0 -334
  58. package/src/ui/ink/ChatApp.js +0 -4152
  59. package/src/ui/ink/DashboardBar.js +0 -691
  60. package/src/ui/ink/InkDemo.js +0 -96
  61. package/src/ui/ink/MultilineInput.js +0 -662
  62. package/src/ui/ink/UcodeApp.js +0 -1675
  63. package/src/ui/ink/agentMirror.js +0 -730
  64. package/src/ui/ink/chatReducer.js +0 -473
  65. package/src/ui/runInk.js +0 -66
@@ -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");
@@ -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 = {