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/README.md +310 -293
- package/README.zh-CN.md +288 -289
- package/package.json +1 -1
- package/src/agent/internalRunner.js +11 -1
- package/src/chat/agentViewController.js +182 -24
- package/src/chat/index.js +6 -1
- package/src/chat/inputMath.js +24 -10
package/package.json
CHANGED
|
@@ -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 =
|
|
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
|
|
84
|
-
return clean
|
|
133
|
+
if (displayWidth(clean) <= normalizedWidth) {
|
|
134
|
+
return padToWidth(clean, normalizedWidth);
|
|
85
135
|
}
|
|
86
|
-
if (normalizedWidth <= 1) return clean
|
|
87
|
-
return clean
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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(
|
|
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 -
|
|
248
|
+
const inner = Math.max(1, width - 2);
|
|
152
249
|
const value = String(busInputValue || "").replace(/\n/g, "⏎");
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
159
|
-
cursorCol: Math.max(0,
|
|
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
|
-
|
|
339
|
-
|
|
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 =
|
|
551
|
+
busInputCursor = previousInputBoundary();
|
|
396
552
|
renderBusView();
|
|
397
553
|
return true;
|
|
398
554
|
}
|
|
399
555
|
if (keyName === "right") {
|
|
400
|
-
busInputCursor =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/src/chat/inputMath.js
CHANGED
|
@@ -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
|
-
|
|
68
|
-
if (typeof
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
75
|
-
if (typeof
|
|
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
|
-
|
|
81
|
-
|
|
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
|
}
|