u-foo 2.3.30 → 2.3.32
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/package.json +5 -1
- package/scripts/chat-app-smoke.js +30 -0
- package/scripts/ink-demo.js +23 -0
- package/scripts/ink-smoke.js +30 -0
- package/scripts/ucode-app-smoke.js +36 -0
- package/src/chat/commandExecutor.js +6 -2
- package/src/chat/daemonMessageRouter.js +9 -1
- package/src/chat/daemonTransport.js +2 -1
- package/src/chat/dashboardKeyController.js +0 -40
- package/src/chat/dashboardView.js +0 -20
- package/src/chat/index.js +9 -1
- package/src/chat/inputSubmitHandler.js +34 -0
- package/src/chat/projectCloseController.js +1 -1
- package/src/chat/shellCommand.js +42 -0
- package/src/chat/transport.js +16 -3
- package/src/cli.js +4 -3
- package/src/code/agent.js +4 -0
- package/src/code/nativeRunner.js +74 -0
- package/src/code/taskDecomposer.js +5 -4
- package/src/code/tui.js +73 -561
- package/src/daemon/index.js +169 -27
- package/src/daemon/ipcServer.js +23 -1
- package/src/daemon/promptRequest.js +6 -1
- package/src/daemon/run.js +11 -4
- package/src/projects/runtimes.js +1 -1
- package/src/ufoo/agentRegistryDiagnostics.js +43 -0
- package/src/ui/MIGRATION.md +382 -0
- package/src/ui/components/ChatApp.js +2950 -0
- package/src/ui/components/DashboardBar.js +417 -0
- package/src/ui/components/InkDemo.js +96 -0
- package/src/ui/components/MultilineInput.js +387 -0
- package/src/ui/components/UcodeApp.js +813 -0
- package/src/ui/components/agentMirror.js +725 -0
- package/src/ui/components/chatReducer.js +337 -0
- package/src/ui/format/index.js +997 -0
- package/src/ui/index.js +9 -0
- package/src/ui/runInk.js +57 -0
- package/src/utils/nodeExecutable.js +26 -0
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Raw PTY passthrough for the internal-agent view.
|
|
5
|
+
*
|
|
6
|
+
* Ink can't usefully render arbitrary ANSI inside a Box (it would lose
|
|
7
|
+
* cursor positioning, scroll regions and curses-style redraws), so this
|
|
8
|
+
* helper takes over stdout/stdin while the user is "inside" an agent.
|
|
9
|
+
* The strategy mirrors the blessed implementation:
|
|
10
|
+
* - clear the screen, set a scroll region with a 1-line bottom bar
|
|
11
|
+
* - stream agentSockets data straight to process.stdout
|
|
12
|
+
* - forward raw stdin bytes to agentSockets.sendRaw
|
|
13
|
+
* - on Esc, run the cleanup callback and let ChatApp re-render
|
|
14
|
+
*
|
|
15
|
+
* Returns a stop() function that the caller invokes on exit.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const { createAgentSockets } = require("../../chat/agentSockets");
|
|
19
|
+
const { loadInternalAgentLogHistory } = require("../../chat/internalAgentLogHistory");
|
|
20
|
+
const { IPC_REQUEST_TYPES, IPC_RESPONSE_TYPES } = require("../../shared/eventContract");
|
|
21
|
+
const { getUfooPaths } = require("../../ufoo/paths");
|
|
22
|
+
const os = require("os");
|
|
23
|
+
const path = require("path");
|
|
24
|
+
const readline = require("readline");
|
|
25
|
+
|
|
26
|
+
function stripAnsi(text = "") {
|
|
27
|
+
return String(text || "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "")
|
|
28
|
+
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function decodeEscapedNewlines(text = "") {
|
|
32
|
+
return String(text || "").replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n").replace(/\\r/g, "\n");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function charDisplayWidth(char = "") {
|
|
36
|
+
if (!char) return 0;
|
|
37
|
+
const code = char.codePointAt(0) || 0;
|
|
38
|
+
if (code === 0) return 0;
|
|
39
|
+
if (code < 32 || (code >= 0x7f && code < 0xa0)) return 0;
|
|
40
|
+
if ((code >= 0x0300 && code <= 0x036f) ||
|
|
41
|
+
(code >= 0x1ab0 && code <= 0x1aff) ||
|
|
42
|
+
(code >= 0x1dc0 && code <= 0x1dff) ||
|
|
43
|
+
(code >= 0x20d0 && code <= 0x20ff) ||
|
|
44
|
+
(code >= 0xfe20 && code <= 0xfe2f)) {
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
if ((code >= 0x1100 && code <= 0x115f) ||
|
|
48
|
+
code === 0x2329 ||
|
|
49
|
+
code === 0x232a ||
|
|
50
|
+
(code >= 0x2e80 && code <= 0xa4cf) ||
|
|
51
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
52
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
53
|
+
(code >= 0xfe10 && code <= 0xfe19) ||
|
|
54
|
+
(code >= 0xfe30 && code <= 0xfe6f) ||
|
|
55
|
+
(code >= 0xff00 && code <= 0xff60) ||
|
|
56
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
57
|
+
(code >= 0x1f300 && code <= 0x1faff)) {
|
|
58
|
+
return 2;
|
|
59
|
+
}
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function displayWidth(text = "") {
|
|
64
|
+
return Array.from(stripAnsi(String(text || ""))).reduce((sum, char) => sum + charDisplayWidth(char), 0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function padToWidth(text = "", width = 1) {
|
|
68
|
+
const cells = displayWidth(text);
|
|
69
|
+
return String(text || "") + " ".repeat(Math.max(0, width - cells));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function truncateToWidth(text = "", width = 1) {
|
|
73
|
+
const target = Math.max(1, width);
|
|
74
|
+
let out = "";
|
|
75
|
+
let cells = 0;
|
|
76
|
+
for (const char of Array.from(stripAnsi(String(text || "")))) {
|
|
77
|
+
const charWidth = charDisplayWidth(char);
|
|
78
|
+
if (cells + charWidth > target) break;
|
|
79
|
+
out += char;
|
|
80
|
+
cells += charWidth;
|
|
81
|
+
}
|
|
82
|
+
return padToWidth(out, target);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function fitText(text = "", width = 1) {
|
|
86
|
+
const normalizedWidth = Math.max(1, width);
|
|
87
|
+
const clean = stripAnsi(String(text || "")).replace(/\r/g, "");
|
|
88
|
+
if (displayWidth(clean) <= normalizedWidth) {
|
|
89
|
+
return padToWidth(clean, normalizedWidth);
|
|
90
|
+
}
|
|
91
|
+
if (normalizedWidth <= 1) return truncateToWidth(clean, normalizedWidth);
|
|
92
|
+
return `${truncateToWidth(clean, normalizedWidth - 1).trimEnd()}…`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function horizontalLine(width = 80) {
|
|
96
|
+
return "─".repeat(Math.max(1, width));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function sliceDisplayCells(text = "", startCell = 0, maxCells = 1) {
|
|
100
|
+
const targetStart = Math.max(0, startCell);
|
|
101
|
+
const targetWidth = Math.max(1, maxCells);
|
|
102
|
+
let out = "";
|
|
103
|
+
let cells = 0;
|
|
104
|
+
let started = false;
|
|
105
|
+
for (const char of Array.from(String(text || ""))) {
|
|
106
|
+
const charWidth = charDisplayWidth(char);
|
|
107
|
+
const nextCells = cells + charWidth;
|
|
108
|
+
if (!started) {
|
|
109
|
+
if (nextCells <= targetStart) {
|
|
110
|
+
cells = nextCells;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
started = true;
|
|
114
|
+
}
|
|
115
|
+
if (displayWidth(out) + charWidth > targetWidth) break;
|
|
116
|
+
out += char;
|
|
117
|
+
cells = nextCells;
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function wrapTextLine(text = "", width = 80) {
|
|
123
|
+
const inner = Math.max(1, width);
|
|
124
|
+
const clean = stripAnsi(String(text || ""));
|
|
125
|
+
if (!clean) return [""];
|
|
126
|
+
const lines = [];
|
|
127
|
+
let current = "";
|
|
128
|
+
let cells = 0;
|
|
129
|
+
for (const char of Array.from(clean)) {
|
|
130
|
+
const charWidth = charDisplayWidth(char);
|
|
131
|
+
if (cells > 0 && cells + charWidth > inner) {
|
|
132
|
+
lines.push(current);
|
|
133
|
+
current = "";
|
|
134
|
+
cells = 0;
|
|
135
|
+
}
|
|
136
|
+
current += char;
|
|
137
|
+
cells += charWidth;
|
|
138
|
+
}
|
|
139
|
+
lines.push(current);
|
|
140
|
+
return lines;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function inputBoundaries(text = "") {
|
|
144
|
+
const source = String(text || "");
|
|
145
|
+
if (!source) return [0];
|
|
146
|
+
try {
|
|
147
|
+
if (typeof Intl !== "undefined" && typeof Intl.Segmenter === "function") {
|
|
148
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
149
|
+
const boundaries = [0];
|
|
150
|
+
for (const part of segmenter.segment(source)) {
|
|
151
|
+
boundaries.push(part.index + part.segment.length);
|
|
152
|
+
}
|
|
153
|
+
return Array.from(new Set(boundaries)).sort((a, b) => a - b);
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
// Fall through to code point boundaries.
|
|
157
|
+
}
|
|
158
|
+
const boundaries = [0];
|
|
159
|
+
let offset = 0;
|
|
160
|
+
for (const char of Array.from(source)) {
|
|
161
|
+
offset += char.length;
|
|
162
|
+
boundaries.push(offset);
|
|
163
|
+
}
|
|
164
|
+
return boundaries;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function compactProjectPath(projectRoot = "") {
|
|
168
|
+
const raw = String(projectRoot || process.cwd() || "").trim();
|
|
169
|
+
const home = os.homedir();
|
|
170
|
+
if (home && (raw === home || raw.startsWith(`${home}/`))) {
|
|
171
|
+
return `~${raw.slice(home.length)}`;
|
|
172
|
+
}
|
|
173
|
+
return raw || ".";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function parseBusDisplayMessage(raw = "") {
|
|
177
|
+
let displayMessage = String(raw || "");
|
|
178
|
+
let streamPayload = null;
|
|
179
|
+
try {
|
|
180
|
+
const parsed = JSON.parse(raw);
|
|
181
|
+
if (parsed && typeof parsed === "object" && parsed.reply) {
|
|
182
|
+
displayMessage = parsed.reply;
|
|
183
|
+
} else if (parsed && typeof parsed === "object" && parsed.stream) {
|
|
184
|
+
streamPayload = parsed;
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
187
|
+
// Not JSON.
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
displayMessage: decodeEscapedNewlines(displayMessage),
|
|
191
|
+
streamPayload,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function normalizeActivityState(value = "") {
|
|
196
|
+
const state = String(value || "").trim().toLowerCase();
|
|
197
|
+
if (state === "waiting") return "waiting_input";
|
|
198
|
+
if (state === "busy" || state === "processing") return "working";
|
|
199
|
+
return state;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function activityLabel(state = "") {
|
|
203
|
+
if (state === "waiting_input") return "waiting";
|
|
204
|
+
if (state === "idle" || state === "ready") return "ready";
|
|
205
|
+
return state || "ready";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function startAgentMirror({
|
|
209
|
+
agentId,
|
|
210
|
+
projectRoot,
|
|
211
|
+
onExit = () => {},
|
|
212
|
+
stdin = process.stdin,
|
|
213
|
+
stdout = process.stdout,
|
|
214
|
+
} = {}) {
|
|
215
|
+
if (!agentId) throw new Error("startAgentMirror requires agentId");
|
|
216
|
+
|
|
217
|
+
const cols = stdout.columns || 80;
|
|
218
|
+
const rows = stdout.rows || 24;
|
|
219
|
+
|
|
220
|
+
// Mirror the lookup that runChatBlessed uses:
|
|
221
|
+
// <bus-queues-dir>/<safeName>/inject.sock. We sanitise the agent id the
|
|
222
|
+
// same way so a daemon launched by either TUI is reachable from the
|
|
223
|
+
// other.
|
|
224
|
+
const safeName = String(agentId || "").replace(/[^A-Za-z0-9_-]/g, "_");
|
|
225
|
+
const sockPath = path.join(
|
|
226
|
+
getUfooPaths(projectRoot || process.cwd()).busQueuesDir,
|
|
227
|
+
safeName,
|
|
228
|
+
"inject.sock"
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const writeOut = (text) => stdout.write(text);
|
|
232
|
+
|
|
233
|
+
const sockets = createAgentSockets({
|
|
234
|
+
onTermWrite: (text) => writeOut(text),
|
|
235
|
+
onPlaceCursor: (cursor) => {
|
|
236
|
+
if (!cursor) return;
|
|
237
|
+
const row = Math.max(1, (cursor.row || 0) + 1);
|
|
238
|
+
const col = Math.max(1, (cursor.col || 0) + 1);
|
|
239
|
+
writeOut(`\x1b[${row};${col}H`);
|
|
240
|
+
},
|
|
241
|
+
isAgentView: () => true,
|
|
242
|
+
isBusMode: () => false,
|
|
243
|
+
getViewingAgent: () => agentId,
|
|
244
|
+
sendBusRaw: () => {},
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Clear screen + reserve a 1-line bar at the bottom for our exit hint.
|
|
248
|
+
writeOut("\x1b[2J\x1b[H");
|
|
249
|
+
writeOut(`\x1b[1;${Math.max(1, rows - 1)}r`);
|
|
250
|
+
writeOut(`\x1b[${rows};1H\x1b[7m esc \x1b[0m return to chat · attached to ${agentId}`);
|
|
251
|
+
writeOut("\x1b[H");
|
|
252
|
+
|
|
253
|
+
sockets.connectOutput(sockPath);
|
|
254
|
+
sockets.connectInput(sockPath);
|
|
255
|
+
sockets.sendResize(cols, Math.max(1, rows - 1));
|
|
256
|
+
|
|
257
|
+
let stopped = false;
|
|
258
|
+
const wasRaw = Boolean(stdin.isRaw);
|
|
259
|
+
if (typeof stdin.setRawMode === "function") stdin.setRawMode(true);
|
|
260
|
+
stdin.resume();
|
|
261
|
+
|
|
262
|
+
const onData = (chunk) => {
|
|
263
|
+
if (stopped) return;
|
|
264
|
+
// Esc on its own (single 0x1b byte, no follow-up) exits the mirror.
|
|
265
|
+
// We can't perfectly distinguish a bare Esc from the start of an
|
|
266
|
+
// arrow-key sequence; the convention here is "Esc + nothing within
|
|
267
|
+
// 50ms means leave". Anything else gets forwarded as-is.
|
|
268
|
+
if (chunk.length === 1 && chunk[0] === 0x1b) {
|
|
269
|
+
setTimeout(() => {
|
|
270
|
+
if (!stopped && pendingEsc === chunk) stop();
|
|
271
|
+
}, 50);
|
|
272
|
+
pendingEsc = chunk;
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
pendingEsc = null;
|
|
276
|
+
sockets.sendRaw(chunk);
|
|
277
|
+
};
|
|
278
|
+
let pendingEsc = null;
|
|
279
|
+
|
|
280
|
+
const onResize = () => {
|
|
281
|
+
if (stopped) return;
|
|
282
|
+
const cols2 = stdout.columns || 80;
|
|
283
|
+
const rows2 = stdout.rows || 24;
|
|
284
|
+
writeOut(`\x1b[1;${Math.max(1, rows2 - 1)}r`);
|
|
285
|
+
sockets.sendResize(cols2, Math.max(1, rows2 - 1));
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
stdin.on("data", onData);
|
|
289
|
+
stdout.on && stdout.on("resize", onResize);
|
|
290
|
+
|
|
291
|
+
function stop() {
|
|
292
|
+
if (stopped) return;
|
|
293
|
+
stopped = true;
|
|
294
|
+
stdin.off("data", onData);
|
|
295
|
+
if (stdout.off) stdout.off("resize", onResize);
|
|
296
|
+
sockets.disconnectOutput();
|
|
297
|
+
sockets.disconnectInput();
|
|
298
|
+
if (typeof stdin.setRawMode === "function") stdin.setRawMode(wasRaw);
|
|
299
|
+
// Reset scroll region + clear screen so the next ink mount has a
|
|
300
|
+
// clean canvas.
|
|
301
|
+
writeOut(`\x1b[1;${stdout.rows || 24}r`);
|
|
302
|
+
writeOut("\x1b[2J\x1b[H");
|
|
303
|
+
onExit();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return stop;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function startInternalAgentMirror({
|
|
310
|
+
agentId,
|
|
311
|
+
agentLabel = "",
|
|
312
|
+
agentAliases = [],
|
|
313
|
+
projectRoot,
|
|
314
|
+
daemonConnection = null,
|
|
315
|
+
setDaemonMessageHandler = () => {},
|
|
316
|
+
onExit = () => {},
|
|
317
|
+
stdin = process.stdin,
|
|
318
|
+
stdout = process.stdout,
|
|
319
|
+
} = {}) {
|
|
320
|
+
if (!agentId) throw new Error("startInternalAgentMirror requires agentId");
|
|
321
|
+
|
|
322
|
+
const label = String(agentLabel || agentId);
|
|
323
|
+
let inputValue = "";
|
|
324
|
+
let inputCursor = 0;
|
|
325
|
+
let logLines = [];
|
|
326
|
+
let replyActive = false;
|
|
327
|
+
let stopped = false;
|
|
328
|
+
let statusState = "ready";
|
|
329
|
+
let statusDetail = "";
|
|
330
|
+
|
|
331
|
+
const writeOut = (text) => stdout.write(text);
|
|
332
|
+
const cols = () => Math.max(20, stdout.columns || 80);
|
|
333
|
+
const rows = () => Math.max(8, stdout.rows || 24);
|
|
334
|
+
const aliases = new Set([String(agentId), label].concat(agentAliases || []).filter(Boolean).map(String));
|
|
335
|
+
|
|
336
|
+
function writeAt(row, content = "") {
|
|
337
|
+
writeOut(`\x1b[${row};1H\x1b[2K${content}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function buildStartupLines(width) {
|
|
341
|
+
return [
|
|
342
|
+
fitText(`ufoo internal agent · ${label}`, width),
|
|
343
|
+
fitText(`agent: ${agentId}`, width),
|
|
344
|
+
fitText(`directory: ${compactProjectPath(projectRoot)}`, width),
|
|
345
|
+
"",
|
|
346
|
+
];
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function resetLogLines() {
|
|
350
|
+
let history = [];
|
|
351
|
+
try {
|
|
352
|
+
history = loadInternalAgentLogHistory(projectRoot || process.cwd(), agentId, {
|
|
353
|
+
maxEvents: 400,
|
|
354
|
+
maxLines: 1000,
|
|
355
|
+
});
|
|
356
|
+
} catch {
|
|
357
|
+
history = [];
|
|
358
|
+
}
|
|
359
|
+
logLines = buildStartupLines(cols()).concat(history.length > 0 ? history : [""]);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function trimLogLines() {
|
|
363
|
+
if (logLines.length > 1000) logLines = logLines.slice(-1000);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function appendLog(text = "") {
|
|
367
|
+
const clean = stripAnsi(String(text || "")).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
368
|
+
if (logLines.length === 0) logLines.push("");
|
|
369
|
+
for (const char of clean) {
|
|
370
|
+
if (char === "\n") {
|
|
371
|
+
logLines.push("");
|
|
372
|
+
} else {
|
|
373
|
+
logLines[logLines.length - 1] += char;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
trimLogLines();
|
|
377
|
+
if (clean.endsWith("\n")) replyActive = false;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function ensureReplyPrefix(prefix = "* ") {
|
|
381
|
+
if (logLines.length === 0) {
|
|
382
|
+
logLines.push(prefix);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (logLines[logLines.length - 1] === "") {
|
|
386
|
+
logLines[logLines.length - 1] = prefix;
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
logLines.push(prefix);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function appendAgentReply(text = "") {
|
|
393
|
+
const clean = stripAnsi(String(text || "")).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
394
|
+
if (!clean) return;
|
|
395
|
+
for (const char of clean) {
|
|
396
|
+
if (char === "\n") {
|
|
397
|
+
logLines.push("");
|
|
398
|
+
replyActive = false;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (!replyActive) {
|
|
402
|
+
ensureReplyPrefix("* ");
|
|
403
|
+
replyActive = true;
|
|
404
|
+
} else if (logLines.length === 0 || logLines[logLines.length - 1] === "") {
|
|
405
|
+
ensureReplyPrefix(" ");
|
|
406
|
+
}
|
|
407
|
+
logLines[logLines.length - 1] += char;
|
|
408
|
+
}
|
|
409
|
+
trimLogLines();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function previousBoundary(pos = inputCursor) {
|
|
413
|
+
const boundaries = inputBoundaries(inputValue);
|
|
414
|
+
const target = Math.max(0, Math.min(inputValue.length, pos));
|
|
415
|
+
let prev = 0;
|
|
416
|
+
for (const boundary of boundaries) {
|
|
417
|
+
if (boundary < target) prev = boundary;
|
|
418
|
+
else break;
|
|
419
|
+
}
|
|
420
|
+
return prev;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function nextBoundary(pos = inputCursor) {
|
|
424
|
+
const boundaries = inputBoundaries(inputValue);
|
|
425
|
+
const target = Math.max(0, Math.min(inputValue.length, pos));
|
|
426
|
+
for (const boundary of boundaries) {
|
|
427
|
+
if (boundary > target) return boundary;
|
|
428
|
+
}
|
|
429
|
+
return inputValue.length;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function insertInput(text = "") {
|
|
433
|
+
const value = String(text || "");
|
|
434
|
+
if (!value) return;
|
|
435
|
+
inputValue = inputValue.slice(0, inputCursor) + value + inputValue.slice(inputCursor);
|
|
436
|
+
inputCursor += value.length;
|
|
437
|
+
render();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function getInputViewport(width) {
|
|
441
|
+
const inner = Math.max(1, width - 2);
|
|
442
|
+
const value = String(inputValue || "").replace(/\n/g, "⏎");
|
|
443
|
+
const beforeCursor = String(inputValue || "").slice(0, inputCursor).replace(/\n/g, "⏎");
|
|
444
|
+
const cursorCells = displayWidth(beforeCursor);
|
|
445
|
+
let startCell = 0;
|
|
446
|
+
if (cursorCells >= inner) startCell = cursorCells - inner + 1;
|
|
447
|
+
return {
|
|
448
|
+
text: sliceDisplayCells(value, startCell, inner),
|
|
449
|
+
cursorCol: Math.max(0, cursorCells - startCell),
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function buildStatusLine(width) {
|
|
454
|
+
const labelText = activityLabel(statusState);
|
|
455
|
+
const detail = statusDetail ? ` · ${statusDetail}` : "";
|
|
456
|
+
return fitText(`ufoo · ${labelText} · Enter send · Esc back${detail}`, width);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function getVisibleLogLines(width, height) {
|
|
460
|
+
const wrapped = [];
|
|
461
|
+
for (const line of logLines) wrapped.push(...wrapTextLine(line, width));
|
|
462
|
+
return wrapped.slice(-Math.max(1, height));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function render() {
|
|
466
|
+
if (stopped) return;
|
|
467
|
+
const width = cols();
|
|
468
|
+
const height = rows();
|
|
469
|
+
const inputTop = Math.max(4, height - 3);
|
|
470
|
+
const logRows = Math.max(1, inputTop - 2);
|
|
471
|
+
const visible = getVisibleLogLines(width, logRows);
|
|
472
|
+
|
|
473
|
+
writeOut("\x1b[?25l");
|
|
474
|
+
for (let i = 0; i < logRows; i += 1) {
|
|
475
|
+
writeAt(1 + i, fitText(visible[i] || "", width));
|
|
476
|
+
}
|
|
477
|
+
writeAt(inputTop - 1, buildStatusLine(width));
|
|
478
|
+
writeAt(inputTop, horizontalLine(width));
|
|
479
|
+
const viewport = getInputViewport(width);
|
|
480
|
+
writeAt(inputTop + 1, fitText(`> ${viewport.text}`, width));
|
|
481
|
+
writeAt(inputTop + 2, horizontalLine(width));
|
|
482
|
+
writeAt(height, fitText(`esc return to chat · internal bus · ${label}`, width));
|
|
483
|
+
writeOut(`\x1b[${inputTop + 1};${Math.max(1, Math.min(width, 3 + viewport.cursorCol))}H\x1b[?25h`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function isAlias(value) {
|
|
487
|
+
return aliases.has(String(value || ""));
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function updateStatusFromMeta(meta = {}) {
|
|
491
|
+
const nextState = normalizeActivityState(meta.activity_state || meta.state || "");
|
|
492
|
+
const nextDetail = String(meta.activity_detail || meta.detail || meta.status_text || "").trim();
|
|
493
|
+
if (!nextState && !nextDetail) return;
|
|
494
|
+
const normalized = nextState || statusState || "ready";
|
|
495
|
+
if (normalized !== statusState || nextDetail !== statusDetail) {
|
|
496
|
+
statusState = normalized;
|
|
497
|
+
statusDetail = nextDetail;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function handleStatusMessage(msg) {
|
|
502
|
+
const data = msg && msg.data && typeof msg.data === "object" ? msg.data : {};
|
|
503
|
+
const metaList = Array.isArray(data.active_meta) ? data.active_meta : [];
|
|
504
|
+
for (const meta of metaList) {
|
|
505
|
+
const metaId = meta && (meta.fullId || meta.subscriber_id || meta.id) ? String(meta.fullId || meta.subscriber_id || meta.id) : "";
|
|
506
|
+
if (isAlias(metaId) || isAlias(`${meta.type || ""}:${meta.id || ""}`)) {
|
|
507
|
+
updateStatusFromMeta(meta);
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
render();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function handleBusMessage(msg) {
|
|
515
|
+
const data = msg && msg.data && typeof msg.data === "object" ? msg.data : {};
|
|
516
|
+
if (data.event === "activity_state_changed") {
|
|
517
|
+
const actor = String(data.subscriber || data.publisher || "").trim();
|
|
518
|
+
if (isAlias(actor)) {
|
|
519
|
+
updateStatusFromMeta({
|
|
520
|
+
activity_state: data.state || data.activity_state,
|
|
521
|
+
activity_detail: data.detail || (data.data && data.data.detail) || data.message || "",
|
|
522
|
+
});
|
|
523
|
+
render();
|
|
524
|
+
}
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const publisher = String(data.publisher || (data.event === "broadcast" ? "broadcast" : "bus"));
|
|
529
|
+
const target = String(data.target || data.subscriber || "");
|
|
530
|
+
const fromAgent = isAlias(publisher);
|
|
531
|
+
const toAgent = isAlias(target);
|
|
532
|
+
if (!fromAgent && !toAgent) return;
|
|
533
|
+
|
|
534
|
+
const rawMessage = String(data.message || "");
|
|
535
|
+
const { displayMessage, streamPayload } = parseBusDisplayMessage(rawMessage);
|
|
536
|
+
if (data.silent && !streamPayload) return;
|
|
537
|
+
|
|
538
|
+
const ownPrompt = data.source === "chat-internal-agent-view" && toAgent && !fromAgent;
|
|
539
|
+
if (ownPrompt) return;
|
|
540
|
+
|
|
541
|
+
if (streamPayload) {
|
|
542
|
+
if (fromAgent) {
|
|
543
|
+
const delta = typeof streamPayload.delta === "string"
|
|
544
|
+
? decodeEscapedNewlines(streamPayload.delta)
|
|
545
|
+
: "";
|
|
546
|
+
if (delta) appendAgentReply(delta);
|
|
547
|
+
if (streamPayload.done) replyActive = false;
|
|
548
|
+
render();
|
|
549
|
+
}
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (!displayMessage) return;
|
|
554
|
+
if (fromAgent) {
|
|
555
|
+
appendAgentReply(`${displayMessage}\n`);
|
|
556
|
+
} else if (toAgent) {
|
|
557
|
+
appendLog(`> ${displayMessage}\n`);
|
|
558
|
+
}
|
|
559
|
+
render();
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function handleDaemonMessage(msg) {
|
|
563
|
+
if (!msg || typeof msg !== "object") return false;
|
|
564
|
+
if (msg.type === IPC_RESPONSE_TYPES.STATUS) {
|
|
565
|
+
handleStatusMessage(msg);
|
|
566
|
+
} else if (msg.type === IPC_RESPONSE_TYPES.BUS) {
|
|
567
|
+
handleBusMessage(msg);
|
|
568
|
+
} else if (msg.type === IPC_RESPONSE_TYPES.ERROR) {
|
|
569
|
+
appendLog(`[Error] ${msg.error || "unknown error"}\n`);
|
|
570
|
+
render();
|
|
571
|
+
} else if (msg.type === IPC_RESPONSE_TYPES.BUS_SEND_OK) {
|
|
572
|
+
statusState = "ready";
|
|
573
|
+
statusDetail = "";
|
|
574
|
+
render();
|
|
575
|
+
}
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function send(req) {
|
|
580
|
+
if (!daemonConnection || typeof daemonConnection.send !== "function") {
|
|
581
|
+
appendLog("[Error] daemon connection unavailable\n");
|
|
582
|
+
render();
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
daemonConnection.send(req);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function submitInput() {
|
|
589
|
+
const text = String(inputValue || "").trim();
|
|
590
|
+
if (!text) {
|
|
591
|
+
render();
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
appendLog(`> ${text}\n`);
|
|
595
|
+
replyActive = false;
|
|
596
|
+
inputValue = "";
|
|
597
|
+
inputCursor = 0;
|
|
598
|
+
statusState = "working";
|
|
599
|
+
statusDetail = "";
|
|
600
|
+
send({
|
|
601
|
+
type: IPC_REQUEST_TYPES.BUS_SEND,
|
|
602
|
+
target: agentId,
|
|
603
|
+
message: text,
|
|
604
|
+
injection_mode: "immediate",
|
|
605
|
+
source: "chat-internal-agent-view",
|
|
606
|
+
});
|
|
607
|
+
render();
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const onKeypress = (str, key = {}) => {
|
|
611
|
+
if (stopped) return;
|
|
612
|
+
const name = key && key.name;
|
|
613
|
+
if (name === "escape" || (key && key.ctrl && name === "c")) {
|
|
614
|
+
stop();
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (name === "return" || name === "enter") {
|
|
618
|
+
if (key && (key.shift || key.meta)) insertInput("\n");
|
|
619
|
+
else submitInput();
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
if (key && key.ctrl && name === "u") {
|
|
623
|
+
inputValue = "";
|
|
624
|
+
inputCursor = 0;
|
|
625
|
+
render();
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (key && key.ctrl && name === "a") {
|
|
629
|
+
inputCursor = 0;
|
|
630
|
+
render();
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (key && key.ctrl && name === "e") {
|
|
634
|
+
inputCursor = inputValue.length;
|
|
635
|
+
render();
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (name === "left") {
|
|
639
|
+
inputCursor = previousBoundary();
|
|
640
|
+
render();
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (name === "right") {
|
|
644
|
+
inputCursor = nextBoundary();
|
|
645
|
+
render();
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (name === "home") {
|
|
649
|
+
inputCursor = 0;
|
|
650
|
+
render();
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (name === "end") {
|
|
654
|
+
inputCursor = inputValue.length;
|
|
655
|
+
render();
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (name === "backspace") {
|
|
659
|
+
if (inputCursor > 0) {
|
|
660
|
+
const prev = previousBoundary();
|
|
661
|
+
inputValue = inputValue.slice(0, prev) + inputValue.slice(inputCursor);
|
|
662
|
+
inputCursor = prev;
|
|
663
|
+
render();
|
|
664
|
+
}
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
if (name === "delete") {
|
|
668
|
+
if (inputCursor < inputValue.length) {
|
|
669
|
+
const next = nextBoundary();
|
|
670
|
+
inputValue = inputValue.slice(0, inputCursor) + inputValue.slice(next);
|
|
671
|
+
render();
|
|
672
|
+
}
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
if (str && !/^[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]+$/.test(str)) {
|
|
676
|
+
insertInput(str.replace(/\r\n/g, "\n").replace(/\r/g, "\n"));
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
const onResize = () => render();
|
|
681
|
+
const wasRaw = Boolean(stdin.isRaw);
|
|
682
|
+
resetLogLines();
|
|
683
|
+
|
|
684
|
+
writeOut("\x1b[2J\x1b[H");
|
|
685
|
+
writeOut(`\x1b[1;${Math.max(1, rows() - 1)}r`);
|
|
686
|
+
setDaemonMessageHandler(handleDaemonMessage);
|
|
687
|
+
try {
|
|
688
|
+
if (daemonConnection && typeof daemonConnection.connect === "function") {
|
|
689
|
+
Promise.resolve(daemonConnection.connect()).catch((err) => {
|
|
690
|
+
appendLog(`[Error] ${err && err.message ? err.message : err}\n`);
|
|
691
|
+
render();
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
} catch (err) {
|
|
695
|
+
appendLog(`[Error] ${err && err.message ? err.message : err}\n`);
|
|
696
|
+
}
|
|
697
|
+
send({ type: IPC_REQUEST_TYPES.BUS_WATCH, agent_id: agentId, enabled: true });
|
|
698
|
+
send({ type: IPC_REQUEST_TYPES.STATUS });
|
|
699
|
+
render();
|
|
700
|
+
|
|
701
|
+
if (typeof stdin.setRawMode === "function") stdin.setRawMode(true);
|
|
702
|
+
stdin.resume();
|
|
703
|
+
readline.emitKeypressEvents(stdin);
|
|
704
|
+
stdin.on("keypress", onKeypress);
|
|
705
|
+
stdout.on && stdout.on("resize", onResize);
|
|
706
|
+
|
|
707
|
+
function stop() {
|
|
708
|
+
if (stopped) return;
|
|
709
|
+
stopped = true;
|
|
710
|
+
send({ type: IPC_REQUEST_TYPES.BUS_WATCH, agent_id: agentId, enabled: false });
|
|
711
|
+
setDaemonMessageHandler(() => {});
|
|
712
|
+
if (stdin.off) stdin.off("keypress", onKeypress);
|
|
713
|
+
else if (stdin.removeListener) stdin.removeListener("keypress", onKeypress);
|
|
714
|
+
if (stdout.off) stdout.off("resize", onResize);
|
|
715
|
+
else if (stdout.removeListener) stdout.removeListener("resize", onResize);
|
|
716
|
+
if (typeof stdin.setRawMode === "function") stdin.setRawMode(wasRaw);
|
|
717
|
+
writeOut(`\x1b[1;${stdout.rows || 24}r`);
|
|
718
|
+
writeOut("\x1b[2J\x1b[H\x1b[?25h");
|
|
719
|
+
onExit();
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return stop;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
module.exports = { startAgentMirror, startInternalAgentMirror };
|