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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "2.3.22",
3
+ "version": "2.3.24",
4
4
  "description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://ufoo.dev",
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 tty = this.readTty(subscriber);
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 tmux required.`);
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
- appendBusLog(cleaned);
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 plain = String(text || "").trim();
1031
- if (!plain) return;
1032
- const content = ` → ${escapeBlessed(plain)} `;
1033
- const visibleLen = plain.length + 4; // " → " + text + " "
1034
- const boxWidth = logBox.width || 80;
1035
- const pad = boxWidth > visibleLen ? " ".repeat(boxWidth - visibleLen) : "";
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,