u-foo 2.3.15 → 2.3.17

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.15",
3
+ "version": "2.3.17",
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",
@@ -184,6 +184,16 @@ function createBusSender(projectRoot, subscriber) {
184
184
  return { enqueue, flush };
185
185
  }
186
186
 
187
+ function isChatUiSource(source = "") {
188
+ const value = String(source || "").trim();
189
+ return value === "chat-direct" || value === "chat-internal-agent-view";
190
+ }
191
+
192
+ function shouldStreamReplyToPublisher(projectRoot, publisher, evt = {}) {
193
+ if (isChatUiSource(evt && evt.data ? evt.data.source : "")) return true;
194
+ return shouldForwardStreamToPublisher(projectRoot, publisher);
195
+ }
196
+
187
197
  function drainQueue(queueFile) {
188
198
  if (!fs.existsSync(queueFile)) return [];
189
199
  const processingFile = `${queueFile}.processing.${process.pid}.${Date.now()}`;
@@ -304,7 +314,7 @@ async function handleEvent(
304
314
  .filter(Boolean)
305
315
  .join("\n\n");
306
316
  const publisher = evt.publisher || "unknown";
307
- const streamToPublisher = shouldForwardStreamToPublisher(projectRoot, publisher);
317
+ const streamToPublisher = shouldStreamReplyToPublisher(projectRoot, publisher, evt);
308
318
 
309
319
  const emitStreamDelta = (delta) => {
310
320
  const text = String(delta || "");
@@ -77,30 +77,95 @@ function createAgentViewController(options = {}) {
77
77
  return Math.max(min, Math.min(max, normalized));
78
78
  }
79
79
 
80
+ function charDisplayWidth(char = "") {
81
+ if (!char) return 0;
82
+ const code = char.codePointAt(0) || 0;
83
+ if (code === 0) return 0;
84
+ if (code < 32 || (code >= 0x7f && code < 0xa0)) return 0;
85
+ if ((code >= 0x0300 && code <= 0x036f) ||
86
+ (code >= 0x1ab0 && code <= 0x1aff) ||
87
+ (code >= 0x1dc0 && code <= 0x1dff) ||
88
+ (code >= 0x20d0 && code <= 0x20ff) ||
89
+ (code >= 0xfe20 && code <= 0xfe2f)) {
90
+ return 0;
91
+ }
92
+ if ((code >= 0x1100 && code <= 0x115f) ||
93
+ code === 0x2329 ||
94
+ code === 0x232a ||
95
+ (code >= 0x2e80 && code <= 0xa4cf) ||
96
+ (code >= 0xac00 && code <= 0xd7a3) ||
97
+ (code >= 0xf900 && code <= 0xfaff) ||
98
+ (code >= 0xfe10 && code <= 0xfe19) ||
99
+ (code >= 0xfe30 && code <= 0xfe6f) ||
100
+ (code >= 0xff00 && code <= 0xff60) ||
101
+ (code >= 0xffe0 && code <= 0xffe6) ||
102
+ (code >= 0x1f300 && code <= 0x1faff)) {
103
+ return 2;
104
+ }
105
+ return 1;
106
+ }
107
+
108
+ function displayWidth(text = "") {
109
+ return Array.from(stripAnsi(String(text || ""))).reduce((sum, char) => sum + charDisplayWidth(char), 0);
110
+ }
111
+
112
+ function padToWidth(text = "", width = 1) {
113
+ const cells = displayWidth(text);
114
+ return String(text || "") + " ".repeat(Math.max(0, width - cells));
115
+ }
116
+
117
+ function truncateToWidth(text = "", width = 1) {
118
+ const target = Math.max(1, width);
119
+ let out = "";
120
+ let cells = 0;
121
+ for (const char of Array.from(stripAnsi(String(text || "")))) {
122
+ const charWidth = charDisplayWidth(char);
123
+ if (cells + charWidth > target) break;
124
+ out += char;
125
+ cells += charWidth;
126
+ }
127
+ return padToWidth(out, target);
128
+ }
129
+
80
130
  function fitText(text = "", width = 1) {
81
131
  const normalizedWidth = Math.max(1, width);
82
132
  const clean = stripAnsi(String(text || "")).replace(/\r/g, "");
83
- if (clean.length <= normalizedWidth) {
84
- return clean + " ".repeat(normalizedWidth - clean.length);
133
+ if (displayWidth(clean) <= normalizedWidth) {
134
+ return padToWidth(clean, normalizedWidth);
85
135
  }
86
- if (normalizedWidth <= 1) return clean.slice(0, normalizedWidth);
87
- return clean.slice(0, normalizedWidth - 1) + "…";
136
+ if (normalizedWidth <= 1) return truncateToWidth(clean, normalizedWidth);
137
+ return `${truncateToWidth(clean, normalizedWidth - 1).trimEnd()}…`;
88
138
  }
89
139
 
90
- function boxTop(title = "", width = 80) {
91
- const inner = Math.max(1, width - 2);
92
- const label = title ? ` ${title} ` : "";
93
- const safe = stripAnsi(label).slice(0, inner);
94
- return `┌${safe}${"─".repeat(Math.max(0, inner - safe.length))}┐`;
140
+ function horizontalLine(width = 80) {
141
+ return "─".repeat(Math.max(1, width));
95
142
  }
96
143
 
97
- function boxBottom(width = 80) {
98
- return `└${"─".repeat(Math.max(1, width - 2))}┘`;
144
+ function plainLine(text = "", width = 80) {
145
+ return fitText(text, Math.max(1, width));
99
146
  }
100
147
 
101
- function boxMiddle(text = "", width = 80) {
102
- const inner = Math.max(1, width - 2);
103
- return `│${fitText(text, inner)}│`;
148
+ function sliceDisplayCells(text = "", startCell = 0, maxCells = 1) {
149
+ const targetStart = Math.max(0, startCell);
150
+ const targetWidth = Math.max(1, maxCells);
151
+ let out = "";
152
+ let cells = 0;
153
+ let started = false;
154
+ for (const char of Array.from(String(text || ""))) {
155
+ const charWidth = charDisplayWidth(char);
156
+ const nextCells = cells + charWidth;
157
+ if (!started) {
158
+ if (nextCells <= targetStart) {
159
+ cells = nextCells;
160
+ continue;
161
+ }
162
+ started = true;
163
+ }
164
+ if (displayWidth(out) + charWidth > targetWidth) break;
165
+ out += char;
166
+ cells = nextCells;
167
+ }
168
+ return out;
104
169
  }
105
170
 
106
171
  function wrapTextLine(text = "", width = 80) {
@@ -108,17 +173,24 @@ function createAgentViewController(options = {}) {
108
173
  const clean = stripAnsi(String(text || ""));
109
174
  if (!clean) return [""];
110
175
  const lines = [];
111
- let rest = clean;
112
- while (rest.length > inner) {
113
- lines.push(rest.slice(0, inner));
114
- rest = rest.slice(inner);
176
+ let current = "";
177
+ let cells = 0;
178
+ for (const char of Array.from(clean)) {
179
+ const charWidth = charDisplayWidth(char);
180
+ if (cells > 0 && cells + charWidth > inner) {
181
+ lines.push(current);
182
+ current = "";
183
+ cells = 0;
184
+ }
185
+ current += char;
186
+ cells += charWidth;
115
187
  }
116
- lines.push(rest);
188
+ lines.push(current);
117
189
  return lines;
118
190
  }
119
191
 
120
192
  function getWrappedBusLogLines(width = 80) {
121
- const inner = Math.max(1, width - 2);
193
+ const inner = Math.max(1, width);
122
194
  const wrapped = [];
123
195
  for (const line of busLogLines) {
124
196
  wrapped.push(...wrapTextLine(line, inner));
@@ -130,15 +202,31 @@ function createAgentViewController(options = {}) {
130
202
  processStdout.write(`\x1b[${row};1H\x1b[2K${content}`);
131
203
  }
132
204
 
205
+ function buildInternalStartupLines(agentLabel = "", width = 80) {
206
+ const label = String(agentLabel || "").trim();
207
+ if (width < 48) {
208
+ return [
209
+ `Welcome to ufoo internal`,
210
+ label ? `agent ${label}` : "agent internal",
211
+ "",
212
+ ];
213
+ }
214
+ return [
215
+ "Welcome to ufoo internal",
216
+ "································",
217
+ " ░░░░░░ ██╗ ██╗",
218
+ " ░░░ ░░░░░░░░░░ ██║ ██║",
219
+ " ░░░░░░░░░░░░░░░░ ╚██████╔╝",
220
+ label ? `agent ${label}` : "agent internal",
221
+ "",
222
+ ];
223
+ }
224
+
133
225
  function resetBusView(agentId) {
134
226
  busInputValue = "";
135
227
  busInputCursor = 0;
136
228
  const label = getAgentLabel(agentId);
137
- busLogLines = [
138
- `ufoo internal · ${label}`,
139
- "Enter 发送 · Esc 返回 · ↓ agent bar",
140
- "",
141
- ];
229
+ busLogLines = buildInternalStartupLines(label, getCols());
142
230
  }
143
231
 
144
232
  function appendBusLog(text = "") {
@@ -157,15 +245,18 @@ function createAgentViewController(options = {}) {
157
245
  }
158
246
 
159
247
  function getBusInputViewport(width) {
160
- const inner = Math.max(1, width - 4);
248
+ const inner = Math.max(1, width - 2);
161
249
  const value = String(busInputValue || "").replace(/\n/g, "⏎");
162
- let start = 0;
163
- if (busInputCursor >= inner) {
164
- start = busInputCursor - inner + 1;
250
+ const beforeCursor = String(busInputValue || "").slice(0, busInputCursor).replace(/\n/g, "⏎");
251
+ const cursorCells = displayWidth(beforeCursor);
252
+ let startCell = 0;
253
+ if (cursorCells >= inner) {
254
+ startCell = cursorCells - inner + 1;
165
255
  }
256
+ const text = sliceDisplayCells(value, startCell, inner);
166
257
  return {
167
- text: value.slice(start, start + inner),
168
- cursorCol: Math.max(0, busInputCursor - start),
258
+ text,
259
+ cursorCol: Math.max(0, cursorCells - startCell),
169
260
  };
170
261
  }
171
262
 
@@ -175,28 +266,23 @@ function createAgentViewController(options = {}) {
175
266
  const cols = getCols();
176
267
  const width = Math.max(20, cols);
177
268
  const inputTop = Math.max(4, rows - 3);
178
- const logTop = 1;
179
- const logBottom = Math.max(logTop + 1, inputTop - 1);
180
- const logContentTop = logTop + 1;
181
- const logContentBottom = logBottom - 1;
269
+ const logContentTop = 1;
270
+ const logContentBottom = Math.max(logContentTop, inputTop - 1);
182
271
  const logContentHeight = Math.max(1, logContentBottom - logContentTop + 1);
183
- const label = getAgentLabel(viewingAgent);
184
272
 
185
273
  processStdout.write("\x1b[?25l");
186
- writeAt(logTop, boxTop(`ufoo internal · ${label}`, width));
187
274
  const visibleLines = getWrappedBusLogLines(width).slice(-logContentHeight);
188
275
  for (let i = 0; i < logContentHeight; i += 1) {
189
- writeAt(logContentTop + i, boxMiddle(visibleLines[i] || "", width));
276
+ writeAt(logContentTop + i, plainLine(visibleLines[i] || "", width));
190
277
  }
191
- writeAt(logBottom, boxBottom(width));
192
278
 
193
- writeAt(inputTop, boxTop("message", width));
279
+ writeAt(inputTop, horizontalLine(width));
194
280
  const viewport = getBusInputViewport(width);
195
- writeAt(inputTop + 1, boxMiddle(`> ${viewport.text}`, width));
196
- writeAt(inputTop + 2, boxBottom(width));
281
+ writeAt(inputTop + 1, plainLine(`> ${viewport.text}`, width));
282
+ writeAt(inputTop + 2, horizontalLine(width));
197
283
 
198
284
  renderAgentDashboard();
199
- const cursorCol = clamp(4 + viewport.cursorCol, 1, width);
285
+ const cursorCol = clamp(3 + viewport.cursorCol, 1, width);
200
286
  processStdout.write(`\x1b[${inputTop + 1};${cursorCol}H\x1b[?25h`);
201
287
  }
202
288
 
@@ -347,10 +433,66 @@ function createAgentViewController(options = {}) {
347
433
  renderBusView();
348
434
  }
349
435
 
436
+ function inputBoundaries(text = "") {
437
+ const source = String(text || "");
438
+ if (!source) return [0];
439
+ try {
440
+ if (typeof Intl !== "undefined" && typeof Intl.Segmenter === "function") {
441
+ const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
442
+ const boundaries = [0];
443
+ for (const part of segmenter.segment(source)) {
444
+ boundaries.push(part.index + part.segment.length);
445
+ }
446
+ return Array.from(new Set(boundaries)).sort((a, b) => a - b);
447
+ }
448
+ } catch {
449
+ // Fall through to code point boundaries.
450
+ }
451
+ const boundaries = [0];
452
+ let offset = 0;
453
+ for (const char of Array.from(source)) {
454
+ offset += char.length;
455
+ boundaries.push(offset);
456
+ }
457
+ return boundaries;
458
+ }
459
+
460
+ function clampInputCursor(pos = busInputCursor) {
461
+ const boundaries = inputBoundaries(busInputValue);
462
+ const target = clamp(pos, 0, busInputValue.length);
463
+ let best = 0;
464
+ for (const boundary of boundaries) {
465
+ if (boundary <= target) best = boundary;
466
+ else break;
467
+ }
468
+ return best;
469
+ }
470
+
471
+ function previousInputBoundary(pos = busInputCursor) {
472
+ const boundaries = inputBoundaries(busInputValue);
473
+ const target = clamp(pos, 0, busInputValue.length);
474
+ let prev = 0;
475
+ for (const boundary of boundaries) {
476
+ if (boundary < target) prev = boundary;
477
+ else break;
478
+ }
479
+ return prev;
480
+ }
481
+
482
+ function nextInputBoundary(pos = busInputCursor) {
483
+ const boundaries = inputBoundaries(busInputValue);
484
+ const target = clamp(pos, 0, busInputValue.length);
485
+ for (const boundary of boundaries) {
486
+ if (boundary > target) return boundary;
487
+ }
488
+ return busInputValue.length;
489
+ }
490
+
350
491
  function deleteBusInputBeforeCursor() {
351
492
  if (busInputCursor <= 0) return;
352
- busInputValue = busInputValue.slice(0, busInputCursor - 1) + busInputValue.slice(busInputCursor);
353
- busInputCursor -= 1;
493
+ const previous = previousInputBoundary();
494
+ busInputValue = busInputValue.slice(0, previous) + busInputValue.slice(busInputCursor);
495
+ busInputCursor = previous;
354
496
  renderBusView();
355
497
  }
356
498
 
@@ -406,12 +548,12 @@ function createAgentViewController(options = {}) {
406
548
  return true;
407
549
  }
408
550
  if (keyName === "left") {
409
- busInputCursor = Math.max(0, busInputCursor - 1);
551
+ busInputCursor = previousInputBoundary();
410
552
  renderBusView();
411
553
  return true;
412
554
  }
413
555
  if (keyName === "right") {
414
- busInputCursor = Math.min(busInputValue.length, busInputCursor + 1);
556
+ busInputCursor = nextInputBoundary();
415
557
  renderBusView();
416
558
  return true;
417
559
  }
@@ -431,7 +573,9 @@ function createAgentViewController(options = {}) {
431
573
  }
432
574
  if (keyName === "delete") {
433
575
  if (busInputCursor < busInputValue.length) {
434
- busInputValue = busInputValue.slice(0, busInputCursor) + busInputValue.slice(busInputCursor + 1);
576
+ const next = nextInputBoundary();
577
+ busInputValue = busInputValue.slice(0, busInputCursor) + busInputValue.slice(next);
578
+ busInputCursor = clampInputCursor();
435
579
  renderBusView();
436
580
  }
437
581
  return true;
package/src/chat/index.js CHANGED
@@ -355,7 +355,12 @@ async function runChat(projectRoot, options = {}) {
355
355
  let preferredCol = null;
356
356
 
357
357
  function getInnerWidth() {
358
- const promptWidth = promptBox && typeof promptBox.width === "number" ? promptBox.width : 2;
358
+ let promptWidth = 2;
359
+ try {
360
+ if (promptBox && typeof promptBox.width === "number") promptWidth = promptBox.width;
361
+ } catch {
362
+ promptWidth = 2;
363
+ }
359
364
  return inputMath.getInnerWidth({ input, screen, promptWidth });
360
365
  }
361
366
 
@@ -50,10 +50,18 @@ function safeStrWidth(strWidth, value) {
50
50
  return Array.from(expanded).length;
51
51
  }
52
52
 
53
+ function safeRead(getter, fallback = undefined) {
54
+ try {
55
+ return getter();
56
+ } catch {
57
+ return fallback;
58
+ }
59
+ }
60
+
53
61
  function getInnerWidth({ input, screen, promptWidth = 2 }) {
54
62
  const targetInput = input && typeof input === "object" ? input : {};
55
63
  const targetScreen = screen && typeof screen === "object" ? screen : {};
56
- let lpos = targetInput.lpos || null;
64
+ let lpos = safeRead(() => targetInput.lpos, null) || null;
57
65
  if (!lpos && typeof targetInput._getCoords === "function") {
58
66
  try {
59
67
  lpos = targetInput._getCoords();
@@ -64,21 +72,27 @@ function getInnerWidth({ input, screen, promptWidth = 2 }) {
64
72
  if (lpos && Number.isFinite(lpos.xl) && Number.isFinite(lpos.xi)) {
65
73
  return Math.max(1, lpos.xl - lpos.xi);
66
74
  }
67
- if (typeof targetInput.width === "number") return Math.max(1, targetInput.width);
68
- if (typeof targetInput.width === "string") {
69
- const match = targetInput.width.match(/^100%-([0-9]+)$/);
70
- if (match && typeof targetScreen.width === "number") {
71
- return Math.max(1, targetScreen.width - parseInt(match[1], 10));
75
+ const inputWidth = safeRead(() => targetInput.width);
76
+ if (typeof inputWidth === "number") return Math.max(1, inputWidth);
77
+ if (typeof inputWidth === "string") {
78
+ const match = inputWidth.match(/^100%-([0-9]+)$/);
79
+ const screenWidth = safeRead(() => targetScreen.width);
80
+ if (match && typeof screenWidth === "number") {
81
+ return Math.max(1, screenWidth - parseInt(match[1], 10));
72
82
  }
73
83
  }
74
- if (typeof targetScreen.width === "number") return Math.max(1, targetScreen.width - promptWidth);
75
- if (typeof targetScreen.cols === "number") return Math.max(1, targetScreen.cols - promptWidth);
84
+ const screenWidth = safeRead(() => targetScreen.width);
85
+ if (typeof screenWidth === "number") return Math.max(1, screenWidth - promptWidth);
86
+ const screenCols = safeRead(() => targetScreen.cols);
87
+ if (typeof screenCols === "number") return Math.max(1, screenCols - promptWidth);
76
88
  return 1;
77
89
  }
78
90
 
79
91
  function getWrapWidth(input, fallbackWidth) {
80
- if (input && input._clines && typeof input._clines.width === "number") {
81
- return Math.max(1, input._clines.width);
92
+ const clines = safeRead(() => input && input._clines, null);
93
+ const clinesWidth = safeRead(() => clines && clines.width);
94
+ if (typeof clinesWidth === "number") {
95
+ return Math.max(1, clinesWidth);
82
96
  }
83
97
  return Math.max(1, fallbackWidth || 1);
84
98
  }