u-foo 2.3.16 → 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.16",
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,14 +77,64 @@ 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
140
  function horizontalLine(width = 80) {
@@ -95,17 +145,47 @@ function createAgentViewController(options = {}) {
95
145
  return fitText(text, Math.max(1, width));
96
146
  }
97
147
 
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;
169
+ }
170
+
98
171
  function wrapTextLine(text = "", width = 80) {
99
172
  const inner = Math.max(1, width);
100
173
  const clean = stripAnsi(String(text || ""));
101
174
  if (!clean) return [""];
102
175
  const lines = [];
103
- let rest = clean;
104
- while (rest.length > inner) {
105
- lines.push(rest.slice(0, inner));
106
- 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;
107
187
  }
108
- lines.push(rest);
188
+ lines.push(current);
109
189
  return lines;
110
190
  }
111
191
 
@@ -122,14 +202,31 @@ function createAgentViewController(options = {}) {
122
202
  processStdout.write(`\x1b[${row};1H\x1b[2K${content}`);
123
203
  }
124
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
+
125
225
  function resetBusView(agentId) {
126
226
  busInputValue = "";
127
227
  busInputCursor = 0;
128
228
  const label = getAgentLabel(agentId);
129
- busLogLines = [
130
- `ufoo internal · ${label}`,
131
- "",
132
- ];
229
+ busLogLines = buildInternalStartupLines(label, getCols());
133
230
  }
134
231
 
135
232
  function appendBusLog(text = "") {
@@ -148,15 +245,18 @@ function createAgentViewController(options = {}) {
148
245
  }
149
246
 
150
247
  function getBusInputViewport(width) {
151
- const inner = Math.max(1, width - 4);
248
+ const inner = Math.max(1, width - 2);
152
249
  const value = String(busInputValue || "").replace(/\n/g, "⏎");
153
- let start = 0;
154
- if (busInputCursor >= inner) {
155
- 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;
156
255
  }
256
+ const text = sliceDisplayCells(value, startCell, inner);
157
257
  return {
158
- text: value.slice(start, start + inner),
159
- cursorCol: Math.max(0, busInputCursor - start),
258
+ text,
259
+ cursorCol: Math.max(0, cursorCells - startCell),
160
260
  };
161
261
  }
162
262
 
@@ -333,10 +433,66 @@ function createAgentViewController(options = {}) {
333
433
  renderBusView();
334
434
  }
335
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
+
336
491
  function deleteBusInputBeforeCursor() {
337
492
  if (busInputCursor <= 0) return;
338
- busInputValue = busInputValue.slice(0, busInputCursor - 1) + busInputValue.slice(busInputCursor);
339
- busInputCursor -= 1;
493
+ const previous = previousInputBoundary();
494
+ busInputValue = busInputValue.slice(0, previous) + busInputValue.slice(busInputCursor);
495
+ busInputCursor = previous;
340
496
  renderBusView();
341
497
  }
342
498
 
@@ -392,12 +548,12 @@ function createAgentViewController(options = {}) {
392
548
  return true;
393
549
  }
394
550
  if (keyName === "left") {
395
- busInputCursor = Math.max(0, busInputCursor - 1);
551
+ busInputCursor = previousInputBoundary();
396
552
  renderBusView();
397
553
  return true;
398
554
  }
399
555
  if (keyName === "right") {
400
- busInputCursor = Math.min(busInputValue.length, busInputCursor + 1);
556
+ busInputCursor = nextInputBoundary();
401
557
  renderBusView();
402
558
  return true;
403
559
  }
@@ -417,7 +573,9 @@ function createAgentViewController(options = {}) {
417
573
  }
418
574
  if (keyName === "delete") {
419
575
  if (busInputCursor < busInputValue.length) {
420
- 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();
421
579
  renderBusView();
422
580
  }
423
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
  }