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,241 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Shared ucode slash-command dispatcher for Rust TUI hosts.
5
+ * (NL foreground streaming stays host-specific.)
6
+ */
7
+
8
+ const { applyUcodeModelCommand } = require("./modelCommand");
9
+ const { applyUcodePlanCommand, formatPlanModeStatus } = require("./context/planMode");
10
+ const { summarizeSessionUsage, formatSessionUsageStatus } = require("./usageStore");
11
+ const fmt = require("../ui/format");
12
+
13
+ /**
14
+ * @param {object} result - parse result from runSingleCommand
15
+ * @param {object} ports
16
+ * @param {object} ports.state
17
+ * @param {string} ports.workspaceRoot
18
+ * @param {(text: string, kind?: string) => void} ports.appendLog
19
+ * @param {() => void} [ports.persist]
20
+ * @param {() => void} [ports.publishPlan]
21
+ * @param {(meter: object) => void} [ports.publishUsage]
22
+ * @param {() => void} [ports.onExit]
23
+ * @param {(sessionId: string) => object} [ports.resumeSession]
24
+ * @param {(entries: object[], notice: string) => void} [ports.replaceTranscript]
25
+ * @param {(tool: string, args: object, payload: object) => void} [ports.onTool]
26
+ * @param {(task: string) => Promise<void>|void} [ports.onBackground]
27
+ * @param {(task: string) => Promise<object>|void} [ports.onNaturalLanguage]
28
+ * @param {(state: object, opts: object) => Promise<object>} [ports.runUbus]
29
+ * @param {(msg: string) => void} [ports.setBusyStatus]
30
+ * @param {() => void} [ports.clearBusyStatus]
31
+ * @param {string[]} [ports.bannerLines]
32
+ * @returns {Promise<{ handled: boolean, waiting?: boolean }>}
33
+ */
34
+ async function dispatchUcodeSlashCommand(result, ports = {}) {
35
+ if (!result || typeof result !== "object") {
36
+ return { handled: false };
37
+ }
38
+
39
+ const appendLog = typeof ports.appendLog === "function" ? ports.appendLog : () => {};
40
+ const persist = typeof ports.persist === "function" ? ports.persist : () => {};
41
+ const publishPlan = typeof ports.publishPlan === "function" ? ports.publishPlan : () => {};
42
+ const workspaceRoot = String(
43
+ ports.workspaceRoot
44
+ || (ports.state && ports.state.workspaceRoot)
45
+ || process.cwd()
46
+ );
47
+ const state = ports.state || {};
48
+
49
+ switch (result.kind) {
50
+ case "empty":
51
+ case "probe":
52
+ return { handled: true };
53
+
54
+ case "exit":
55
+ if (typeof ports.onExit === "function") ports.onExit();
56
+ return { handled: true, exit: true };
57
+
58
+ case "help":
59
+ case "error":
60
+ case "skills":
61
+ case "legacy_ufoo_marker":
62
+ if (result.output) {
63
+ appendLog(result.output, result.kind === "error" ? "error" : "system");
64
+ }
65
+ if (result.error) {
66
+ appendLog(`Error: ${result.error}`, "error");
67
+ }
68
+ return { handled: true };
69
+
70
+ case "status": {
71
+ try {
72
+ const usageSummary = summarizeSessionUsage({
73
+ workspaceRoot,
74
+ sessionId: state.sessionId || "",
75
+ });
76
+ appendLog(formatSessionUsageStatus(usageSummary), "system");
77
+ if (state.executionState) {
78
+ const planLines = formatPlanModeStatus(state.executionState)
79
+ .split("\n")
80
+ .slice(0, 6)
81
+ .join("\n");
82
+ if (planLines.trim()) appendLog(planLines, "system");
83
+ }
84
+ } catch (err) {
85
+ appendLog(`Error: ${err && err.message ? err.message : "status failed"}`, "error");
86
+ }
87
+ return { handled: true };
88
+ }
89
+
90
+ case "model": {
91
+ const applied = await applyUcodeModelCommand(state, result, { workspaceRoot });
92
+ appendLog(applied.output || "", applied.ok ? "system" : "error");
93
+ if (applied.ok && result.action === "set") {
94
+ try {
95
+ const { buildContextMeter } = require("./contextWindow");
96
+ const prev = state.contextMeter || {};
97
+ const nextMeter = buildContextMeter({
98
+ usedTokens: prev.usedTokens || 0,
99
+ model: state.model || "",
100
+ });
101
+ state.contextMeter = nextMeter;
102
+ if (typeof ports.publishUsage === "function") ports.publishUsage(nextMeter);
103
+ } catch {
104
+ // ignore
105
+ }
106
+ persist();
107
+ }
108
+ return { handled: true };
109
+ }
110
+
111
+ case "plan": {
112
+ const applied = applyUcodePlanCommand(state, result);
113
+ appendLog(applied.output || "", applied.ok ? "system" : "error");
114
+ if (applied.refreshPlanUi || applied.ok) publishPlan();
115
+ if (applied.ok) persist();
116
+ return { handled: true };
117
+ }
118
+
119
+ case "ubus": {
120
+ if (typeof ports.runUbus !== "function") {
121
+ appendLog("Error: ubus unavailable", "error");
122
+ return { handled: true };
123
+ }
124
+ if (typeof ports.setBusyStatus === "function") {
125
+ ports.setBusyStatus("Checking bus messages...");
126
+ }
127
+ try {
128
+ const { extractAgentNickname } = require("./agent");
129
+ const ubusResult = await ports.runUbus(state, {
130
+ workspaceRoot,
131
+ onMessageReceived: (msg) => {
132
+ const nickname = extractAgentNickname(msg && msg.from) || (msg && msg.from) || "bus";
133
+ appendLog(`${nickname}: ${(msg && msg.task) || ""}`, "bus");
134
+ },
135
+ });
136
+ if (!ubusResult || !ubusResult.ok) {
137
+ appendLog(`Error: ${(ubusResult && ubusResult.error) || "ubus failed"}`, "error");
138
+ return { handled: true };
139
+ }
140
+ const exchanges = Array.isArray(ubusResult.messageExchanges)
141
+ ? ubusResult.messageExchanges
142
+ : [];
143
+ if (exchanges.length > 0) {
144
+ for (const exchange of exchanges) {
145
+ const nickname = extractAgentNickname(exchange && exchange.from)
146
+ || (exchange && exchange.from)
147
+ || "bus";
148
+ appendLog(`@${nickname} ${(exchange && exchange.reply) || ""}`, "bus");
149
+ }
150
+ } else if (Number(ubusResult.handled) === 0) {
151
+ appendLog("ubus: no pending messages.", "system");
152
+ }
153
+ persist();
154
+ } finally {
155
+ if (typeof ports.clearBusyStatus === "function") ports.clearBusyStatus();
156
+ }
157
+ return { handled: true };
158
+ }
159
+
160
+ case "resume": {
161
+ if (typeof ports.resumeSession !== "function") {
162
+ appendLog("Error: resume unsupported", "error");
163
+ return { handled: true };
164
+ }
165
+ const resumed = ports.resumeSession(result.sessionId);
166
+ if (!resumed || !resumed.ok) {
167
+ appendLog(`Error: ${(resumed && resumed.error) || "resume failed"}`, "error");
168
+ return { handled: true };
169
+ }
170
+ const markdownState = { inCodeBlock: false };
171
+ const history = fmt.buildUcodeSessionLogEntries(
172
+ Array.isArray(state.nlMessages) ? state.nlMessages : [],
173
+ { markdownState, idPrefix: "h", startSeq: 0 },
174
+ );
175
+ const banner = Array.isArray(ports.bannerLines) ? ports.bannerLines : [];
176
+ const bannerEntries = banner.concat([""]).map((line, idx) => ({
177
+ id: `b-${idx}`,
178
+ kind: "system",
179
+ text: String(line || ""),
180
+ speaker: "",
181
+ }));
182
+ const notice = {
183
+ id: `h-resume-${Date.now().toString(36)}`,
184
+ kind: "system",
185
+ text: `Resumed session ${resumed.sessionId} (${resumed.restoredMessages} messages).`,
186
+ speaker: "",
187
+ };
188
+ const entries = bannerEntries.concat(history.entries || []).concat([notice]);
189
+ if (typeof ports.replaceTranscript === "function") {
190
+ ports.replaceTranscript(entries, notice.text);
191
+ } else {
192
+ for (const entry of entries) {
193
+ appendLog(entry.text, entry.kind || "system");
194
+ }
195
+ }
196
+ if (state.contextMeter && typeof ports.publishUsage === "function") {
197
+ ports.publishUsage(state.contextMeter);
198
+ }
199
+ publishPlan();
200
+ return { handled: true };
201
+ }
202
+
203
+ case "tool": {
204
+ if (typeof ports.onTool === "function") {
205
+ const payload = result.result && typeof result.result === "object" ? result.result : {};
206
+ ports.onTool(result.tool, result.args || {}, payload);
207
+ } else if (result.output) {
208
+ appendLog(result.output, "system");
209
+ }
210
+ return { handled: true };
211
+ }
212
+
213
+ case "nl_bg": {
214
+ if (typeof ports.onBackground === "function") {
215
+ await ports.onBackground(result.task);
216
+ } else {
217
+ appendLog("Error: background tasks unavailable", "error");
218
+ }
219
+ return { handled: true };
220
+ }
221
+
222
+ case "nl": {
223
+ if (typeof ports.onNaturalLanguage === "function") {
224
+ const nl = await ports.onNaturalLanguage(result.task);
225
+ return { handled: true, waiting: Boolean(nl && nl.waiting) };
226
+ }
227
+ return { handled: false };
228
+ }
229
+
230
+ default:
231
+ if (result.output) {
232
+ appendLog(result.output, result.ok === false ? "error" : "system");
233
+ return { handled: true };
234
+ }
235
+ return { handled: false };
236
+ }
237
+ }
238
+
239
+ module.exports = {
240
+ dispatchUcodeSlashCommand,
241
+ };
@@ -179,6 +179,9 @@ end tell`;
179
179
  if (adapter.capabilities.supportsInternalQueueLoop) {
180
180
  throw new Error("Internal mode agents cannot be activated (no terminal window)");
181
181
  }
182
+ if (info.launch_mode === "host") {
183
+ throw new Error("Host did not expose activate; add \"activate\" to host capabilities/commands");
184
+ }
182
185
  throw new Error("Cannot activate: missing tty or tmux_pane for agent");
183
186
  }
184
187
 
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ufoo.dev/schemas/ufoo-ui-v1/envelope.json",
4
+ "title": "ufoo-ui/1 envelope",
5
+ "type": "object",
6
+ "required": ["protocol", "kind", "payload"],
7
+ "additionalProperties": true,
8
+ "properties": {
9
+ "protocol": { "const": "ufoo-ui/1" },
10
+ "kind": {
11
+ "type": "string",
12
+ "enum": ["hello", "welcome", "command", "event", "snapshot", "result", "error"]
13
+ },
14
+ "name": { "type": "string" },
15
+ "request_id": { "type": "string" },
16
+ "seq": { "type": "integer", "minimum": 1 },
17
+ "scope": {
18
+ "type": "object",
19
+ "additionalProperties": true,
20
+ "properties": {
21
+ "surface": { "type": "string" },
22
+ "project_id": { "type": "string" },
23
+ "view_id": { "type": "string" }
24
+ }
25
+ },
26
+ "payload": { "type": "object" }
27
+ }
28
+ }
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * ufoo-ui/1 — Node host ↔ Rust TUI control protocol.
5
+ *
6
+ * Daemon IPC stays separate. This protocol never shares the TTY stdout used
7
+ * for drawing; it rides a dedicated Unix socket (or extra fd).
8
+ */
9
+
10
+ const PROTOCOL = "ufoo-ui/1";
11
+
12
+ // Capability advertised by clients that can render Rust-native /multi
13
+ // and internal "side" activate (same multi.* wire; multi.set.kind =
14
+ // "multi" | "side"). "side" is not a user-facing mode name.
15
+ const MULTI_FRAMES_CAPABILITY = "multi-frames-v1";
16
+
17
+ const KINDS = Object.freeze([
18
+ "hello",
19
+ "welcome",
20
+ "command",
21
+ "event",
22
+ "snapshot",
23
+ "result",
24
+ "error",
25
+ ]);
26
+
27
+ const COMMAND_NAMES = Object.freeze([
28
+ "input.submit",
29
+ "input.paste",
30
+ "task.cancel",
31
+ "agent.select",
32
+ "agent.open",
33
+ "agent.close",
34
+ "agent.view.submit",
35
+ "agent.view.exit",
36
+ "project.switch",
37
+ "project.close",
38
+ "project.return_controller",
39
+ "cron.stop",
40
+ "completion.request",
41
+ "command.execute",
42
+ "interaction.respond",
43
+ "settings.set",
44
+ "ui.suspend.request",
45
+ "ui.resync.request",
46
+ "app.exit",
47
+ "multi.exit",
48
+ "multi.focus",
49
+ "multi.viewport",
50
+ "multi.raw",
51
+ ]);
52
+
53
+ const EVENT_NAMES = Object.freeze([
54
+ "app.snapshot",
55
+ "transcript.reset",
56
+ "transcript.append",
57
+ "transcript.patch",
58
+ "stream.start",
59
+ "stream.delta",
60
+ "stream.done",
61
+ "status.set",
62
+ "agents.snapshot",
63
+ "agents.patch",
64
+ "projects.snapshot",
65
+ "cron.snapshot",
66
+ "loop.set",
67
+ "completions.set",
68
+ "interaction.request",
69
+ "interaction.clear",
70
+ "plan.set",
71
+ "tool.start",
72
+ "tool.result",
73
+ "tool.group",
74
+ "settings.snapshot",
75
+ "prompt.apply_paste",
76
+ "prompt.set_prefix",
77
+ "attachments.set",
78
+ "usage.set",
79
+ "agent.view.open",
80
+ "agent.view.append",
81
+ "agent.view.status",
82
+ "agent.view.close",
83
+ "ui.suspend.prepare",
84
+ "ui.resume",
85
+ "connection.set",
86
+ "error",
87
+ "multi.set",
88
+ "multi.pane.frame",
89
+ ]);
90
+
91
+ function createEnvelope({
92
+ kind,
93
+ name = "",
94
+ requestId = null,
95
+ seq = null,
96
+ scope = null,
97
+ payload = {},
98
+ } = {}) {
99
+ const envelope = {
100
+ protocol: PROTOCOL,
101
+ kind: String(kind || "").trim(),
102
+ name: String(name || "").trim(),
103
+ payload: payload && typeof payload === "object" ? payload : {},
104
+ };
105
+ if (requestId != null && requestId !== "") {
106
+ envelope.request_id = String(requestId);
107
+ }
108
+ if (Number.isFinite(seq)) {
109
+ envelope.seq = Math.floor(seq);
110
+ }
111
+ if (scope && typeof scope === "object") {
112
+ envelope.scope = {
113
+ surface: String(scope.surface || ""),
114
+ project_id: String(scope.project_id || scope.projectId || ""),
115
+ view_id: String(scope.view_id || scope.viewId || "main"),
116
+ };
117
+ }
118
+ return envelope;
119
+ }
120
+
121
+ function validateEnvelope(raw) {
122
+ const errors = [];
123
+ if (!raw || typeof raw !== "object") {
124
+ return { ok: false, errors: ["envelope must be an object"] };
125
+ }
126
+ if (raw.protocol !== PROTOCOL) {
127
+ errors.push(`unsupported protocol "${raw.protocol}"`);
128
+ }
129
+ if (!KINDS.includes(String(raw.kind || ""))) {
130
+ errors.push(`invalid kind "${raw.kind}"`);
131
+ }
132
+ if (raw.kind === "command" && raw.name && !COMMAND_NAMES.includes(String(raw.name))) {
133
+ // Additive: unknown commands are soft-fail for forward compat.
134
+ }
135
+ if (raw.payload != null && typeof raw.payload !== "object") {
136
+ errors.push("payload must be an object");
137
+ }
138
+ return { ok: errors.length === 0, errors };
139
+ }
140
+
141
+ function encodeMessage(envelope) {
142
+ const check = validateEnvelope(envelope);
143
+ if (!check.ok) {
144
+ const err = new Error(`ufoo-ui encode failed: ${check.errors.join("; ")}`);
145
+ err.code = "UFOO_UI_ENCODE";
146
+ err.errors = check.errors;
147
+ throw err;
148
+ }
149
+ return `${JSON.stringify(envelope)}\n`;
150
+ }
151
+
152
+ function decodeMessage(line) {
153
+ const text = String(line || "").trim();
154
+ if (!text) return { ok: false, errors: ["empty line"] };
155
+ let parsed;
156
+ try {
157
+ parsed = JSON.parse(text);
158
+ } catch (err) {
159
+ return { ok: false, errors: [`invalid json: ${err.message}`] };
160
+ }
161
+ const check = validateEnvelope(parsed);
162
+ if (!check.ok) return check;
163
+ return { ok: true, envelope: parsed, errors: [] };
164
+ }
165
+
166
+ function createSeqCounter(start = 0) {
167
+ let seq = Math.max(0, Math.floor(Number(start) || 0));
168
+ return {
169
+ next() {
170
+ seq += 1;
171
+ return seq;
172
+ },
173
+ peek() {
174
+ return seq;
175
+ },
176
+ };
177
+ }
178
+
179
+ module.exports = {
180
+ PROTOCOL,
181
+ MULTI_FRAMES_CAPABILITY,
182
+ KINDS,
183
+ COMMAND_NAMES,
184
+ EVENT_NAMES,
185
+ createEnvelope,
186
+ validateEnvelope,
187
+ encodeMessage,
188
+ decodeMessage,
189
+ createSeqCounter,
190
+ };
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
 
3
- const { renderMarkdownLinesAnsi } = require("../format/markdownRenderer");
3
+ const { renderMarkdownLinesAnsi } = require("./format/markdownRenderer");
4
4
 
5
5
  // Match ucode: markdown only for conversational prose. System/user rows stay
6
- // plain so app-generated prefixes (›, ✓, Error:) keep Ink color control.
6
+ // plain so app-generated prefixes (›, ✓, Error:) keep host color control.
7
7
  const MARKDOWN_BODY_KINDS = new Set(["assistant", "agent", "report"]);
8
8
 
9
9
  function stripBlessedTags(text = "") {
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Shared dashboard snapshot helpers for Rust chat host.
5
+ */
6
+
7
+ const { formatLoopSummary } = require("../app/chat/dashboardView");
8
+
9
+ function loadGlobalProjectRows(activeProjectRoot = "") {
10
+ const path = require("path");
11
+ const {
12
+ listProjectRuntimes,
13
+ filterVisibleProjectRuntimes,
14
+ isGlobalControllerProjectRoot,
15
+ markProjectStopped,
16
+ canonicalProjectRoot,
17
+ } = require("../runtime/projects");
18
+
19
+ function resolveRoot(row = {}) {
20
+ const raw = String((row && (row.root || row.project_root || row.projectRoot)) || "").trim();
21
+ if (!raw) return "";
22
+ try {
23
+ return canonicalProjectRoot(raw);
24
+ } catch {
25
+ return path.resolve(raw);
26
+ }
27
+ }
28
+
29
+ let rows = listProjectRuntimes({ validate: true, cleanupTmp: true }) || [];
30
+ for (const row of rows) {
31
+ const status = String((row && row.status) || "").trim().toLowerCase();
32
+ const root = resolveRoot(row);
33
+ if (status === "stale" && root && !isGlobalControllerProjectRoot(root)) {
34
+ try {
35
+ markProjectStopped(root);
36
+ } catch {
37
+ // ignore
38
+ }
39
+ }
40
+ }
41
+ rows = filterVisibleProjectRuntimes(rows);
42
+ rows = rows.filter((row) => !isGlobalControllerProjectRoot(resolveRoot(row)));
43
+ return rows.map((row) => ({
44
+ id: row.project_id || row.project_root || "",
45
+ label: row.project_name || (row.project_root ? path.basename(row.project_root) : ""),
46
+ root: row.project_root || "",
47
+ status: row.status || "",
48
+ active: resolveRoot(row) === String(activeProjectRoot || ""),
49
+ }));
50
+ }
51
+
52
+ function cronTasksFromStatus(data = {}) {
53
+ const cron = data && data.cron;
54
+ const tasks = cron && Array.isArray(cron.tasks) ? cron.tasks : [];
55
+ return tasks.map((task, index) => ({
56
+ id: String((task && (task.id || task.task_id)) || `cron-${index}`),
57
+ label: String((task && (task.label || task.name || task.summary || task.id)) || `cron-${index}`),
58
+ summary: String((task && (task.summary || task.schedule || "")) || ""),
59
+ }));
60
+ }
61
+
62
+ function buildDashboardPublishPayload(controller, data = {}) {
63
+ const agents = controller.getAgentsSnapshot();
64
+ const cron = cronTasksFromStatus(data);
65
+ const loop = (data && data.loop) || agents.loop || null;
66
+ const loopText = formatLoopSummary(loop);
67
+ return {
68
+ agents: agents.agents,
69
+ footer: agents.footer,
70
+ cron,
71
+ loop,
72
+ loop_summary: loopText,
73
+ };
74
+ }
75
+
76
+ module.exports = {
77
+ loadGlobalProjectRows,
78
+ cronTasksFromStatus,
79
+ buildDashboardPublishPayload,
80
+ formatLoopSummary,
81
+ };
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
 
3
3
  /**
4
- * Pure formatting + input-math helpers for the ink-based TUIs under
5
- * src/ui/ink/. No terminal widget import is allowed in this module.
4
+ * Pure formatting helpers shared by hosts and tests.
5
+ * No terminal widget import is allowed in this module.
6
6
  */
7
7
 
8
8
  const chalk = require("chalk");
package/src/ui/index.js CHANGED
@@ -1,9 +1,13 @@
1
1
  "use strict";
2
2
 
3
- const { runInk } = require("./runInk");
4
- const { createInkDemo } = require("./ink/InkDemo");
3
+ /**
4
+ * UI helpers. Terminal rendering is Rust ufoo-tui only.
5
+ */
6
+
7
+ const { PROTOCOL, resolveTuiLaunchPlan, resolveUfooTuiBinary } = require("./tuiLauncher");
5
8
 
6
9
  module.exports = {
7
- runInk,
8
- createInkDemo,
10
+ PROTOCOL,
11
+ resolveTuiLaunchPlan,
12
+ resolveUfooTuiBinary,
9
13
  };