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.
- package/README.md +25 -9
- package/README.zh-CN.md +22 -9
- 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 +12 -4
- 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/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 +38 -1
- 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/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 -4152
- 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,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("
|
|
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
|
|
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
|
+
};
|
package/src/ui/format/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Pure formatting
|
|
5
|
-
*
|
|
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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
8
|
-
|
|
10
|
+
PROTOCOL,
|
|
11
|
+
resolveTuiLaunchPlan,
|
|
12
|
+
resolveUfooTuiBinary,
|
|
9
13
|
};
|