u-foo 2.3.16 → 2.3.18

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.18",
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 || "");
@@ -1,3 +1,6 @@
1
+ const os = require("os");
2
+ const { version: packageVersion } = require("../../package.json");
3
+
1
4
  function createAgentViewController(options = {}) {
2
5
  const {
3
6
  screen,
@@ -17,6 +20,7 @@ function createAgentViewController(options = {}) {
17
20
  setAgentListWindowStart = () => {},
18
21
  getAgentLabel = (id) => id,
19
22
  getAgentStates = () => ({}),
23
+ getProjectRoot = () => process.cwd(),
20
24
  setDashboardView = () => {},
21
25
  setScreenGrabKeys = (value) => {
22
26
  if (screen) screen.grabKeys = Boolean(value);
@@ -77,14 +81,64 @@ function createAgentViewController(options = {}) {
77
81
  return Math.max(min, Math.min(max, normalized));
78
82
  }
79
83
 
84
+ function charDisplayWidth(char = "") {
85
+ if (!char) return 0;
86
+ const code = char.codePointAt(0) || 0;
87
+ if (code === 0) return 0;
88
+ if (code < 32 || (code >= 0x7f && code < 0xa0)) return 0;
89
+ if ((code >= 0x0300 && code <= 0x036f) ||
90
+ (code >= 0x1ab0 && code <= 0x1aff) ||
91
+ (code >= 0x1dc0 && code <= 0x1dff) ||
92
+ (code >= 0x20d0 && code <= 0x20ff) ||
93
+ (code >= 0xfe20 && code <= 0xfe2f)) {
94
+ return 0;
95
+ }
96
+ if ((code >= 0x1100 && code <= 0x115f) ||
97
+ code === 0x2329 ||
98
+ code === 0x232a ||
99
+ (code >= 0x2e80 && code <= 0xa4cf) ||
100
+ (code >= 0xac00 && code <= 0xd7a3) ||
101
+ (code >= 0xf900 && code <= 0xfaff) ||
102
+ (code >= 0xfe10 && code <= 0xfe19) ||
103
+ (code >= 0xfe30 && code <= 0xfe6f) ||
104
+ (code >= 0xff00 && code <= 0xff60) ||
105
+ (code >= 0xffe0 && code <= 0xffe6) ||
106
+ (code >= 0x1f300 && code <= 0x1faff)) {
107
+ return 2;
108
+ }
109
+ return 1;
110
+ }
111
+
112
+ function displayWidth(text = "") {
113
+ return Array.from(stripAnsi(String(text || ""))).reduce((sum, char) => sum + charDisplayWidth(char), 0);
114
+ }
115
+
116
+ function padToWidth(text = "", width = 1) {
117
+ const cells = displayWidth(text);
118
+ return String(text || "") + " ".repeat(Math.max(0, width - cells));
119
+ }
120
+
121
+ function truncateToWidth(text = "", width = 1) {
122
+ const target = Math.max(1, width);
123
+ let out = "";
124
+ let cells = 0;
125
+ for (const char of Array.from(stripAnsi(String(text || "")))) {
126
+ const charWidth = charDisplayWidth(char);
127
+ if (cells + charWidth > target) break;
128
+ out += char;
129
+ cells += charWidth;
130
+ }
131
+ return padToWidth(out, target);
132
+ }
133
+
80
134
  function fitText(text = "", width = 1) {
81
135
  const normalizedWidth = Math.max(1, width);
82
136
  const clean = stripAnsi(String(text || "")).replace(/\r/g, "");
83
- if (clean.length <= normalizedWidth) {
84
- return clean + " ".repeat(normalizedWidth - clean.length);
137
+ if (displayWidth(clean) <= normalizedWidth) {
138
+ return padToWidth(clean, normalizedWidth);
85
139
  }
86
- if (normalizedWidth <= 1) return clean.slice(0, normalizedWidth);
87
- return clean.slice(0, normalizedWidth - 1) + "…";
140
+ if (normalizedWidth <= 1) return truncateToWidth(clean, normalizedWidth);
141
+ return `${truncateToWidth(clean, normalizedWidth - 1).trimEnd()}…`;
88
142
  }
89
143
 
90
144
  function horizontalLine(width = 80) {
@@ -95,17 +149,47 @@ function createAgentViewController(options = {}) {
95
149
  return fitText(text, Math.max(1, width));
96
150
  }
97
151
 
152
+ function sliceDisplayCells(text = "", startCell = 0, maxCells = 1) {
153
+ const targetStart = Math.max(0, startCell);
154
+ const targetWidth = Math.max(1, maxCells);
155
+ let out = "";
156
+ let cells = 0;
157
+ let started = false;
158
+ for (const char of Array.from(String(text || ""))) {
159
+ const charWidth = charDisplayWidth(char);
160
+ const nextCells = cells + charWidth;
161
+ if (!started) {
162
+ if (nextCells <= targetStart) {
163
+ cells = nextCells;
164
+ continue;
165
+ }
166
+ started = true;
167
+ }
168
+ if (displayWidth(out) + charWidth > targetWidth) break;
169
+ out += char;
170
+ cells = nextCells;
171
+ }
172
+ return out;
173
+ }
174
+
98
175
  function wrapTextLine(text = "", width = 80) {
99
176
  const inner = Math.max(1, width);
100
177
  const clean = stripAnsi(String(text || ""));
101
178
  if (!clean) return [""];
102
179
  const lines = [];
103
- let rest = clean;
104
- while (rest.length > inner) {
105
- lines.push(rest.slice(0, inner));
106
- rest = rest.slice(inner);
180
+ let current = "";
181
+ let cells = 0;
182
+ for (const char of Array.from(clean)) {
183
+ const charWidth = charDisplayWidth(char);
184
+ if (cells > 0 && cells + charWidth > inner) {
185
+ lines.push(current);
186
+ current = "";
187
+ cells = 0;
188
+ }
189
+ current += char;
190
+ cells += charWidth;
107
191
  }
108
- lines.push(rest);
192
+ lines.push(current);
109
193
  return lines;
110
194
  }
111
195
 
@@ -122,14 +206,93 @@ function createAgentViewController(options = {}) {
122
206
  processStdout.write(`\x1b[${row};1H\x1b[2K${content}`);
123
207
  }
124
208
 
209
+ function forceScreenRepaint() {
210
+ if (typeof screen.realloc === "function") {
211
+ screen.realloc();
212
+ return;
213
+ }
214
+ if (typeof screen.alloc === "function") {
215
+ screen.alloc(true);
216
+ }
217
+ }
218
+
219
+ function compactProjectPath(projectRoot = "") {
220
+ const raw = String(projectRoot || process.cwd() || "").trim();
221
+ const home = os.homedir();
222
+ if (home && (raw === home || raw.startsWith(`${home}/`))) {
223
+ return `~${raw.slice(home.length)}`;
224
+ }
225
+ return raw || ".";
226
+ }
227
+
228
+ function borderedLines(lines = [], innerWidth = 56) {
229
+ const contentWidth = Math.max(1, innerWidth);
230
+ const out = [`╭${"─".repeat(contentWidth + 2)}╮`];
231
+ for (const line of lines) {
232
+ out.push(`│ ${fitText(line, contentWidth)} │`);
233
+ }
234
+ out.push(`╰${"─".repeat(contentWidth + 2)}╯`);
235
+ return out;
236
+ }
237
+
238
+ function normalizeAgentKind(agentId = "") {
239
+ const text = String(agentId || "").trim().toLowerCase();
240
+ if (text.startsWith("codex:") || text === "codex") return "codex";
241
+ if (text.startsWith("claude:") || text.startsWith("claude-code:") || text === "claude" || text === "claude-code") {
242
+ return "claude";
243
+ }
244
+ return "internal";
245
+ }
246
+
247
+ function buildClaudeStartupLines(agentLabel = "", width = 80) {
248
+ const label = String(agentLabel || "").trim();
249
+ const projectPath = compactProjectPath(getProjectRoot());
250
+ const product = "Claude Code";
251
+ const detail = label ? `${label} · managed headless` : "managed headless";
252
+ const lines = [
253
+ ` ▐▛███▜▌${product} v${packageVersion}`,
254
+ `▝▜█████▛▘${detail}`,
255
+ ` ▘▘▝▝${projectPath}`,
256
+ "",
257
+ ];
258
+ if (width < 44) return lines;
259
+ return lines.map((line) => padToWidth(line, Math.min(58, Math.max(1, width))));
260
+ }
261
+
262
+ function buildCodexStartupLines(agentLabel = "", width = 80) {
263
+ const label = String(agentLabel || "").trim();
264
+ const projectPath = compactProjectPath(getProjectRoot());
265
+ if (width < 36) {
266
+ return [
267
+ `>_ OpenAI Codex`,
268
+ label ? `model: ${label}` : "model: managed headless",
269
+ `directory: ${projectPath}`,
270
+ "",
271
+ ];
272
+ }
273
+ const innerWidth = Math.min(56, Math.max(24, width - 4));
274
+ return [
275
+ ...borderedLines([
276
+ `>_ OpenAI Codex (ufoo v${packageVersion})`,
277
+ "",
278
+ `model: ${label ? `${label} · managed headless` : "managed headless"}`,
279
+ `directory: ${projectPath}`,
280
+ ], innerWidth),
281
+ "",
282
+ ];
283
+ }
284
+
285
+ function buildInternalStartupLines(agentId = "", agentLabel = "", width = 80) {
286
+ const kind = normalizeAgentKind(agentId);
287
+ if (kind === "codex") return buildCodexStartupLines(agentLabel || agentId, width);
288
+ return buildClaudeStartupLines(agentLabel || agentId, width);
289
+ }
290
+
125
291
  function resetBusView(agentId) {
126
292
  busInputValue = "";
127
293
  busInputCursor = 0;
128
294
  const label = getAgentLabel(agentId);
129
- busLogLines = [
130
- `ufoo internal · ${label}`,
131
- "",
132
- ];
295
+ busLogLines = buildInternalStartupLines(agentId, label, getCols());
133
296
  }
134
297
 
135
298
  function appendBusLog(text = "") {
@@ -148,15 +311,18 @@ function createAgentViewController(options = {}) {
148
311
  }
149
312
 
150
313
  function getBusInputViewport(width) {
151
- const inner = Math.max(1, width - 4);
314
+ const inner = Math.max(1, width - 2);
152
315
  const value = String(busInputValue || "").replace(/\n/g, "⏎");
153
- let start = 0;
154
- if (busInputCursor >= inner) {
155
- start = busInputCursor - inner + 1;
316
+ const beforeCursor = String(busInputValue || "").slice(0, busInputCursor).replace(/\n/g, "⏎");
317
+ const cursorCells = displayWidth(beforeCursor);
318
+ let startCell = 0;
319
+ if (cursorCells >= inner) {
320
+ startCell = cursorCells - inner + 1;
156
321
  }
322
+ const text = sliceDisplayCells(value, startCell, inner);
157
323
  return {
158
- text: value.slice(start, start + inner),
159
- cursorCol: Math.max(0, busInputCursor - start),
324
+ text,
325
+ cursorCol: Math.max(0, cursorCells - startCell),
160
326
  };
161
327
  }
162
328
 
@@ -284,7 +450,7 @@ function createAgentViewController(options = {}) {
284
450
  viewingAgent = null;
285
451
 
286
452
  processStdout.write(`\x1b[1;${rows}r`);
287
- processStdout.write("\x1b[2J\x1b[H");
453
+ processStdout.write("\x1b[?25h");
288
454
 
289
455
  if (detachedChildren) {
290
456
  for (const child of detachedChildren) screen.append(child);
@@ -296,9 +462,7 @@ function createAgentViewController(options = {}) {
296
462
  setDashboardView("agents");
297
463
  setSelectedAgentIndex(-1);
298
464
  setScreenGrabKeys(false);
299
- if (typeof screen.alloc === "function") {
300
- screen.alloc();
301
- }
465
+ forceScreenRepaint();
302
466
  clearTargetAgent();
303
467
  renderDashboard();
304
468
  focusInput();
@@ -333,10 +497,66 @@ function createAgentViewController(options = {}) {
333
497
  renderBusView();
334
498
  }
335
499
 
500
+ function inputBoundaries(text = "") {
501
+ const source = String(text || "");
502
+ if (!source) return [0];
503
+ try {
504
+ if (typeof Intl !== "undefined" && typeof Intl.Segmenter === "function") {
505
+ const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
506
+ const boundaries = [0];
507
+ for (const part of segmenter.segment(source)) {
508
+ boundaries.push(part.index + part.segment.length);
509
+ }
510
+ return Array.from(new Set(boundaries)).sort((a, b) => a - b);
511
+ }
512
+ } catch {
513
+ // Fall through to code point boundaries.
514
+ }
515
+ const boundaries = [0];
516
+ let offset = 0;
517
+ for (const char of Array.from(source)) {
518
+ offset += char.length;
519
+ boundaries.push(offset);
520
+ }
521
+ return boundaries;
522
+ }
523
+
524
+ function clampInputCursor(pos = busInputCursor) {
525
+ const boundaries = inputBoundaries(busInputValue);
526
+ const target = clamp(pos, 0, busInputValue.length);
527
+ let best = 0;
528
+ for (const boundary of boundaries) {
529
+ if (boundary <= target) best = boundary;
530
+ else break;
531
+ }
532
+ return best;
533
+ }
534
+
535
+ function previousInputBoundary(pos = busInputCursor) {
536
+ const boundaries = inputBoundaries(busInputValue);
537
+ const target = clamp(pos, 0, busInputValue.length);
538
+ let prev = 0;
539
+ for (const boundary of boundaries) {
540
+ if (boundary < target) prev = boundary;
541
+ else break;
542
+ }
543
+ return prev;
544
+ }
545
+
546
+ function nextInputBoundary(pos = busInputCursor) {
547
+ const boundaries = inputBoundaries(busInputValue);
548
+ const target = clamp(pos, 0, busInputValue.length);
549
+ for (const boundary of boundaries) {
550
+ if (boundary > target) return boundary;
551
+ }
552
+ return busInputValue.length;
553
+ }
554
+
336
555
  function deleteBusInputBeforeCursor() {
337
556
  if (busInputCursor <= 0) return;
338
- busInputValue = busInputValue.slice(0, busInputCursor - 1) + busInputValue.slice(busInputCursor);
339
- busInputCursor -= 1;
557
+ const previous = previousInputBoundary();
558
+ busInputValue = busInputValue.slice(0, previous) + busInputValue.slice(busInputCursor);
559
+ busInputCursor = previous;
340
560
  renderBusView();
341
561
  }
342
562
 
@@ -392,12 +612,12 @@ function createAgentViewController(options = {}) {
392
612
  return true;
393
613
  }
394
614
  if (keyName === "left") {
395
- busInputCursor = Math.max(0, busInputCursor - 1);
615
+ busInputCursor = previousInputBoundary();
396
616
  renderBusView();
397
617
  return true;
398
618
  }
399
619
  if (keyName === "right") {
400
- busInputCursor = Math.min(busInputValue.length, busInputCursor + 1);
620
+ busInputCursor = nextInputBoundary();
401
621
  renderBusView();
402
622
  return true;
403
623
  }
@@ -417,7 +637,9 @@ function createAgentViewController(options = {}) {
417
637
  }
418
638
  if (keyName === "delete") {
419
639
  if (busInputCursor < busInputValue.length) {
420
- busInputValue = busInputValue.slice(0, busInputCursor) + busInputValue.slice(busInputCursor + 1);
640
+ const next = nextInputBoundary();
641
+ busInputValue = busInputValue.slice(0, busInputCursor) + busInputValue.slice(next);
642
+ busInputCursor = clampInputCursor();
421
643
  renderBusView();
422
644
  }
423
645
  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
 
@@ -1538,6 +1543,7 @@ async function runChat(projectRoot, options = {}) {
1538
1543
  }
1539
1544
  return states;
1540
1545
  },
1546
+ getProjectRoot: () => activeProjectRoot,
1541
1547
  setDashboardView: (value) => {
1542
1548
  dashboardView = value;
1543
1549
  },
@@ -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
  }