u-foo 2.3.22 → 2.3.24
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 +1 -1
- package/src/bus/inject.js +172 -4
- package/src/chat/agentViewController.js +41 -1
- package/src/code/tui.js +80 -7
package/package.json
CHANGED
package/src/bus/inject.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { spawn } = require("child_process");
|
|
1
|
+
const { spawn, spawnSync } = require("child_process");
|
|
2
2
|
const fs = require("fs");
|
|
3
3
|
const net = require("net");
|
|
4
4
|
const path = require("path");
|
|
@@ -12,12 +12,46 @@ const logInject = (message) => {
|
|
|
12
12
|
}
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
function escapeAppleScriptString(value) {
|
|
16
|
+
return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function appleScriptStringLiteral(value) {
|
|
20
|
+
const lines = String(value ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
21
|
+
if (lines.length === 0) return '""';
|
|
22
|
+
return lines.map((line) => `"${escapeAppleScriptString(line)}"`).join(" & linefeed & ");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function runAppleScript(lines = []) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const proc = spawn("osascript", lines.flatMap((line) => ["-e", line]));
|
|
28
|
+
let stderr = "";
|
|
29
|
+
let stdout = "";
|
|
30
|
+
|
|
31
|
+
proc.stdout.on("data", (data) => {
|
|
32
|
+
stdout += data.toString("utf8");
|
|
33
|
+
});
|
|
34
|
+
proc.stderr.on("data", (data) => {
|
|
35
|
+
stderr += data.toString("utf8");
|
|
36
|
+
});
|
|
37
|
+
proc.on("close", (code) => {
|
|
38
|
+
if (code === 0) {
|
|
39
|
+
resolve(stdout.trim());
|
|
40
|
+
} else {
|
|
41
|
+
reject(new Error(stderr.trim() || "AppleScript failed"));
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
proc.on("error", reject);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
15
48
|
/**
|
|
16
49
|
* 命令注入器 - 将命令注入到终端
|
|
17
50
|
*
|
|
18
51
|
* 支持的方式:
|
|
19
52
|
* 1. PTY socket(直接写入,无需macOS权限)
|
|
20
53
|
* 2. tmux send-keys(无需权限)
|
|
54
|
+
* 3. Terminal.app/iTerm2 tty lookup(macOS terminal mode fallback)
|
|
21
55
|
*/
|
|
22
56
|
class Injector {
|
|
23
57
|
constructor(busDir, agentsFile) {
|
|
@@ -175,6 +209,128 @@ class Injector {
|
|
|
175
209
|
textProc.on("error", reject);
|
|
176
210
|
}
|
|
177
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Use Terminal.app's tty metadata to locate the target tab and paste input.
|
|
214
|
+
*/
|
|
215
|
+
async injectTerminal(tty, command) {
|
|
216
|
+
const ttyLiteral = appleScriptStringLiteral(tty);
|
|
217
|
+
const commandLiteral = appleScriptStringLiteral(command);
|
|
218
|
+
const lines = [
|
|
219
|
+
'tell application "Terminal"',
|
|
220
|
+
" set targetWindow to missing value",
|
|
221
|
+
" set targetTab to missing value",
|
|
222
|
+
" repeat with w in windows",
|
|
223
|
+
" repeat with t in tabs of w",
|
|
224
|
+
" try",
|
|
225
|
+
` if tty of t is ${ttyLiteral} then`,
|
|
226
|
+
" set targetWindow to w",
|
|
227
|
+
" set targetTab to t",
|
|
228
|
+
" exit repeat",
|
|
229
|
+
" end if",
|
|
230
|
+
" end try",
|
|
231
|
+
" end repeat",
|
|
232
|
+
" if targetTab is not missing value then exit repeat",
|
|
233
|
+
" end repeat",
|
|
234
|
+
" if targetTab is missing value then",
|
|
235
|
+
` error "No Terminal tab found with tty: " & ${ttyLiteral}`,
|
|
236
|
+
" end if",
|
|
237
|
+
" activate",
|
|
238
|
+
" set selected tab of targetWindow to targetTab",
|
|
239
|
+
" set index of targetWindow to 1",
|
|
240
|
+
"end tell",
|
|
241
|
+
"set oldClipboard to the clipboard",
|
|
242
|
+
"try",
|
|
243
|
+
` set the clipboard to ${commandLiteral}`,
|
|
244
|
+
" delay 0.1",
|
|
245
|
+
' tell application "System Events"',
|
|
246
|
+
' tell process "Terminal"',
|
|
247
|
+
" key code 53",
|
|
248
|
+
" delay 0.1",
|
|
249
|
+
' keystroke "v" using command down',
|
|
250
|
+
" delay 0.2",
|
|
251
|
+
" keystroke return",
|
|
252
|
+
" end tell",
|
|
253
|
+
" end tell",
|
|
254
|
+
"on error errMsg number errNo",
|
|
255
|
+
" set the clipboard to oldClipboard",
|
|
256
|
+
" error errMsg number errNo",
|
|
257
|
+
"end try",
|
|
258
|
+
"delay 0.2",
|
|
259
|
+
"set the clipboard to oldClipboard",
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
return runAppleScript(lines);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Check if iTerm2 is available before trying its direct write API.
|
|
267
|
+
*/
|
|
268
|
+
isItermRunning() {
|
|
269
|
+
try {
|
|
270
|
+
const res = spawnSync("osascript", ["-e", 'application "iTerm2" is running'], {
|
|
271
|
+
encoding: "utf8",
|
|
272
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
273
|
+
});
|
|
274
|
+
return res.status === 0 && String(res.stdout || "").trim() === "true";
|
|
275
|
+
} catch {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Use iTerm2's tty metadata to locate the target session and write input.
|
|
282
|
+
*/
|
|
283
|
+
async injectIterm(tty, command) {
|
|
284
|
+
const ttyLiteral = appleScriptStringLiteral(tty);
|
|
285
|
+
const commandLiteral = appleScriptStringLiteral(command);
|
|
286
|
+
const lines = [
|
|
287
|
+
'tell application "iTerm2"',
|
|
288
|
+
" activate",
|
|
289
|
+
" repeat with w in windows",
|
|
290
|
+
" repeat with t in tabs of w",
|
|
291
|
+
" repeat with s in sessions of t",
|
|
292
|
+
" try",
|
|
293
|
+
` if tty of s is ${ttyLiteral} then`,
|
|
294
|
+
" select s",
|
|
295
|
+
` write text ${commandLiteral} to s`,
|
|
296
|
+
" return",
|
|
297
|
+
" end if",
|
|
298
|
+
" end try",
|
|
299
|
+
" end repeat",
|
|
300
|
+
" end repeat",
|
|
301
|
+
" end repeat",
|
|
302
|
+
` error "No iTerm2 session found with tty: " & ${ttyLiteral}`,
|
|
303
|
+
"end tell",
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
return runAppleScript(lines);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async injectMacTerminal(tty, command, meta = {}) {
|
|
310
|
+
if (process.platform !== "darwin") {
|
|
311
|
+
throw new Error("Terminal.app injection is only supported on macOS");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const terminalApp = String(meta.terminal_app || meta.terminalApp || "").trim().toLowerCase();
|
|
315
|
+
const shouldTryIterm = terminalApp === "iterm2" || (!terminalApp && this.isItermRunning());
|
|
316
|
+
|
|
317
|
+
if (shouldTryIterm) {
|
|
318
|
+
try {
|
|
319
|
+
logInject(`[inject] Using iTerm2 AppleScript for tty: ${tty}`);
|
|
320
|
+
await this.injectIterm(tty, command);
|
|
321
|
+
return;
|
|
322
|
+
} catch (err) {
|
|
323
|
+
if (terminalApp === "iterm2") {
|
|
324
|
+
throw err;
|
|
325
|
+
}
|
|
326
|
+
logInject(`[inject] iTerm2 failed: ${err.message}, trying Terminal.app`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
logInject(`[inject] Using Terminal.app AppleScript for tty: ${tty}`);
|
|
331
|
+
await this.injectTerminal(tty, command);
|
|
332
|
+
}
|
|
333
|
+
|
|
178
334
|
/**
|
|
179
335
|
* 获取订阅者的 inject socket 路径
|
|
180
336
|
*/
|
|
@@ -252,6 +408,7 @@ class Injector {
|
|
|
252
408
|
* 优先级:
|
|
253
409
|
* 1. PTY socket(直接写入,无需macOS权限)
|
|
254
410
|
* 2. tmux send-keys(无需权限)
|
|
411
|
+
* 3. Terminal.app/iTerm2 tty lookup(terminal mode fallback)
|
|
255
412
|
*/
|
|
256
413
|
async inject(subscriber, commandOverride = "") {
|
|
257
414
|
if (String(subscriber || "").startsWith("ufoo-code:")) {
|
|
@@ -300,8 +457,10 @@ class Injector {
|
|
|
300
457
|
}
|
|
301
458
|
}
|
|
302
459
|
|
|
303
|
-
// 读取 tty(tmux 需要)
|
|
304
|
-
const
|
|
460
|
+
// 读取 tty(tmux/Terminal.app fallback 需要)
|
|
461
|
+
const recordedTty = this.readTty(subscriber);
|
|
462
|
+
const metaTty = isValidTty(meta.tty) ? meta.tty : "";
|
|
463
|
+
const tty = recordedTty || metaTty;
|
|
305
464
|
|
|
306
465
|
// 2. 尝试 tmux(无需权限)
|
|
307
466
|
// Launch mode may be temporarily missing/stale (e.g. rejoin from non-interactive context).
|
|
@@ -329,8 +488,17 @@ class Injector {
|
|
|
329
488
|
}
|
|
330
489
|
}
|
|
331
490
|
|
|
491
|
+
// 3. Plain Terminal.app/iTerm2 fallback: locate the window/tab by tty and paste input.
|
|
492
|
+
const allowMacTerminalFallback = (launchMode === "terminal" || (!launchMode && tty && isValidTty(tty)))
|
|
493
|
+
&& tty
|
|
494
|
+
&& isValidTty(tty);
|
|
495
|
+
if (allowMacTerminalFallback) {
|
|
496
|
+
await this.injectMacTerminal(tty, command, meta);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
332
500
|
// 没有可用的注入方式
|
|
333
|
-
throw new Error(`No inject method available for ${subscriber}. PTY socket or
|
|
501
|
+
throw new Error(`No inject method available for ${subscriber}. PTY socket, tmux, or Terminal.app injection required.`);
|
|
334
502
|
}
|
|
335
503
|
}
|
|
336
504
|
|
|
@@ -61,6 +61,7 @@ function createAgentViewController(options = {}) {
|
|
|
61
61
|
let busLogLines = [];
|
|
62
62
|
let busStartupAgentId = "";
|
|
63
63
|
let busStartupLineCount = 0;
|
|
64
|
+
let busAgentReplyActive = false;
|
|
64
65
|
const originalRender = screen.render.bind(screen);
|
|
65
66
|
let renderFrozen = false;
|
|
66
67
|
|
|
@@ -361,6 +362,7 @@ function createAgentViewController(options = {}) {
|
|
|
361
362
|
function resetBusView(agentId) {
|
|
362
363
|
busInputValue = "";
|
|
363
364
|
busInputCursor = 0;
|
|
365
|
+
busAgentReplyActive = false;
|
|
364
366
|
busStartupAgentId = agentId || "";
|
|
365
367
|
const label = getAgentLabel(agentId);
|
|
366
368
|
const startupLines = staticStartupLines(agentId, label, getCols());
|
|
@@ -390,6 +392,42 @@ function createAgentViewController(options = {}) {
|
|
|
390
392
|
if (busLogLines.length > 1000) {
|
|
391
393
|
busLogLines = busLogLines.slice(-1000);
|
|
392
394
|
}
|
|
395
|
+
if (clean.endsWith("\n")) {
|
|
396
|
+
busAgentReplyActive = false;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function ensureBusLinePrefix(prefix = "") {
|
|
401
|
+
if (busLogLines.length === 0) {
|
|
402
|
+
busLogLines.push(prefix);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (busLogLines[busLogLines.length - 1] === "") {
|
|
406
|
+
busLogLines[busLogLines.length - 1] = prefix;
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
busLogLines.push(prefix);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function appendBusAgentReply(text = "") {
|
|
413
|
+
const clean = stripAnsi(String(text || "")).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
414
|
+
if (!clean) return;
|
|
415
|
+
for (const char of clean) {
|
|
416
|
+
if (char === "\n") {
|
|
417
|
+
busLogLines.push("");
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (!busAgentReplyActive) {
|
|
421
|
+
ensureBusLinePrefix("• ");
|
|
422
|
+
busAgentReplyActive = true;
|
|
423
|
+
} else if (busLogLines.length === 0 || busLogLines[busLogLines.length - 1] === "") {
|
|
424
|
+
ensureBusLinePrefix(" ");
|
|
425
|
+
}
|
|
426
|
+
busLogLines[busLogLines.length - 1] += char;
|
|
427
|
+
}
|
|
428
|
+
if (busLogLines.length > 1000) {
|
|
429
|
+
busLogLines = busLogLines.slice(-1000);
|
|
430
|
+
}
|
|
393
431
|
}
|
|
394
432
|
|
|
395
433
|
function getBusInputViewport(width) {
|
|
@@ -533,6 +571,7 @@ function createAgentViewController(options = {}) {
|
|
|
533
571
|
busLogLines = [];
|
|
534
572
|
busStartupAgentId = "";
|
|
535
573
|
busStartupLineCount = 0;
|
|
574
|
+
busAgentReplyActive = false;
|
|
536
575
|
|
|
537
576
|
currentView = "main";
|
|
538
577
|
viewingAgent = null;
|
|
@@ -661,6 +700,7 @@ function createAgentViewController(options = {}) {
|
|
|
661
700
|
return;
|
|
662
701
|
}
|
|
663
702
|
appendBusLog(`> ${text}\n`);
|
|
703
|
+
busAgentReplyActive = false;
|
|
664
704
|
busInputValue = "";
|
|
665
705
|
busInputCursor = 0;
|
|
666
706
|
sendBusMessage(viewingAgent, text);
|
|
@@ -767,7 +807,7 @@ function createAgentViewController(options = {}) {
|
|
|
767
807
|
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "")
|
|
768
808
|
.replace(/\x1b\[(?:[?>=]?[0-9]*c|[?]?6n|5n)/g, "");
|
|
769
809
|
if (agentViewUsesBus) {
|
|
770
|
-
|
|
810
|
+
appendBusAgentReply(cleaned);
|
|
771
811
|
renderBusView();
|
|
772
812
|
return;
|
|
773
813
|
}
|
package/src/code/tui.js
CHANGED
|
@@ -18,6 +18,77 @@ const STATUS_INDICATORS = {
|
|
|
18
18
|
|
|
19
19
|
const ANSI_PATTERN = /\x1B\[[0-9;?]*[ -/]*[@-~]/g;
|
|
20
20
|
|
|
21
|
+
function charDisplayWidth(char = "") {
|
|
22
|
+
if (!char) return 0;
|
|
23
|
+
const code = char.codePointAt(0) || 0;
|
|
24
|
+
if (code === 0) return 0;
|
|
25
|
+
if (code < 32 || (code >= 0x7f && code < 0xa0)) return 0;
|
|
26
|
+
if ((code >= 0x0300 && code <= 0x036f) ||
|
|
27
|
+
(code >= 0x1ab0 && code <= 0x1aff) ||
|
|
28
|
+
(code >= 0x1dc0 && code <= 0x1dff) ||
|
|
29
|
+
(code >= 0x20d0 && code <= 0x20ff) ||
|
|
30
|
+
(code >= 0xfe20 && code <= 0xfe2f)) {
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
if ((code >= 0x1100 && code <= 0x115f) ||
|
|
34
|
+
code === 0x2329 ||
|
|
35
|
+
code === 0x232a ||
|
|
36
|
+
(code >= 0x2e80 && code <= 0xa4cf) ||
|
|
37
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
38
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
39
|
+
(code >= 0xfe10 && code <= 0xfe19) ||
|
|
40
|
+
(code >= 0xfe30 && code <= 0xfe6f) ||
|
|
41
|
+
(code >= 0xff00 && code <= 0xff60) ||
|
|
42
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
43
|
+
(code >= 0x1f300 && code <= 0x1faff)) {
|
|
44
|
+
return 2;
|
|
45
|
+
}
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function displayCellWidth(text = "") {
|
|
50
|
+
return Array.from(String(text || "").replace(ANSI_PATTERN, "")).reduce(
|
|
51
|
+
(sum, char) => sum + charDisplayWidth(char),
|
|
52
|
+
0
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function safeRead(getter, fallback = undefined) {
|
|
57
|
+
try {
|
|
58
|
+
return getter();
|
|
59
|
+
} catch {
|
|
60
|
+
return fallback;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resolveLogContentWidth({ logBox = null, screen = null, fallback = 80 } = {}) {
|
|
65
|
+
const coords = safeRead(() => logBox && typeof logBox._getCoords === "function" ? logBox._getCoords() : null, null);
|
|
66
|
+
if (coords && Number.isFinite(coords.xl) && Number.isFinite(coords.xi)) {
|
|
67
|
+
return Math.max(1, coords.xl - coords.xi);
|
|
68
|
+
}
|
|
69
|
+
const width = safeRead(() => logBox && logBox.width, null);
|
|
70
|
+
if (typeof width === "number") return Math.max(1, width);
|
|
71
|
+
const screenWidth = safeRead(() => screen && screen.width, null);
|
|
72
|
+
if (typeof screenWidth === "number") return Math.max(1, screenWidth);
|
|
73
|
+
const screenCols = safeRead(() => screen && screen.cols, null);
|
|
74
|
+
if (typeof screenCols === "number") return Math.max(1, screenCols);
|
|
75
|
+
return Math.max(1, fallback);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatHighlightedUserInput(text = "", {
|
|
79
|
+
width = 80,
|
|
80
|
+
escapeText = (value) => String(value || ""),
|
|
81
|
+
} = {}) {
|
|
82
|
+
const plain = String(text || "").trim();
|
|
83
|
+
if (!plain) return "";
|
|
84
|
+
const targetWidth = Math.max(1, Math.floor(Number(width) || 80) - 1);
|
|
85
|
+
const prefix = " → ";
|
|
86
|
+
const suffix = " ";
|
|
87
|
+
const contentWidth = displayCellWidth(`${prefix}${plain}${suffix}`);
|
|
88
|
+
const pad = " ".repeat(Math.max(0, targetWidth - contentWidth));
|
|
89
|
+
return `{cyan-bg}{white-fg}${prefix}${escapeText(plain)}${suffix}${pad}{/white-fg}{/cyan-bg}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
21
92
|
// Stream buffer for smooth output
|
|
22
93
|
class StreamBuffer {
|
|
23
94
|
constructor(writer, options = {}) {
|
|
@@ -1027,13 +1098,12 @@ function runUcodeTui({
|
|
|
1027
1098
|
|
|
1028
1099
|
const logUserInput = (text = "") => {
|
|
1029
1100
|
activeToolMerge = null;
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
logBox.log(`{cyan-bg}{white-fg}${content}${pad}{/white-fg}{/cyan-bg}`);
|
|
1101
|
+
const line = formatHighlightedUserInput(text, {
|
|
1102
|
+
width: resolveLogContentWidth({ logBox, screen, fallback: (stdout && stdout.columns) || 80 }),
|
|
1103
|
+
escapeText: escapeBlessed,
|
|
1104
|
+
});
|
|
1105
|
+
if (!line) return;
|
|
1106
|
+
logBox.log(line);
|
|
1037
1107
|
logBox.log(""); // Add line break after user input
|
|
1038
1108
|
screen.render();
|
|
1039
1109
|
};
|
|
@@ -1911,6 +1981,9 @@ module.exports = {
|
|
|
1911
1981
|
UCODE_BANNER_LINES,
|
|
1912
1982
|
UCODE_VERSION,
|
|
1913
1983
|
StreamBuffer,
|
|
1984
|
+
displayCellWidth,
|
|
1985
|
+
resolveLogContentWidth,
|
|
1986
|
+
formatHighlightedUserInput,
|
|
1914
1987
|
buildUcodeBannerLines,
|
|
1915
1988
|
buildUcodeBannerBlessedLines,
|
|
1916
1989
|
parseActiveAgentsFromBusStatus,
|