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/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 +192 -48
- 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,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
|
|
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
|
-
function
|
|
91
|
-
|
|
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
|
|
98
|
-
return
|
|
144
|
+
function plainLine(text = "", width = 80) {
|
|
145
|
+
return fitText(text, Math.max(1, width));
|
|
99
146
|
}
|
|
100
147
|
|
|
101
|
-
function
|
|
102
|
-
const
|
|
103
|
-
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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(
|
|
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
|
|
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 -
|
|
248
|
+
const inner = Math.max(1, width - 2);
|
|
161
249
|
const value = String(busInputValue || "").replace(/\n/g, "⏎");
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
168
|
-
cursorCol: Math.max(0,
|
|
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
|
|
179
|
-
const
|
|
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,
|
|
276
|
+
writeAt(logContentTop + i, plainLine(visibleLines[i] || "", width));
|
|
190
277
|
}
|
|
191
|
-
writeAt(logBottom, boxBottom(width));
|
|
192
278
|
|
|
193
|
-
writeAt(inputTop,
|
|
279
|
+
writeAt(inputTop, horizontalLine(width));
|
|
194
280
|
const viewport = getBusInputViewport(width);
|
|
195
|
-
writeAt(inputTop + 1,
|
|
196
|
-
writeAt(inputTop + 2,
|
|
281
|
+
writeAt(inputTop + 1, plainLine(`> ${viewport.text}`, width));
|
|
282
|
+
writeAt(inputTop + 2, horizontalLine(width));
|
|
197
283
|
|
|
198
284
|
renderAgentDashboard();
|
|
199
|
-
const cursorCol = clamp(
|
|
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
|
-
|
|
353
|
-
|
|
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 =
|
|
551
|
+
busInputCursor = previousInputBoundary();
|
|
410
552
|
renderBusView();
|
|
411
553
|
return true;
|
|
412
554
|
}
|
|
413
555
|
if (keyName === "right") {
|
|
414
|
-
busInputCursor =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|