u-foo 2.3.13 → 2.3.15
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 +1 -1
- package/src/chat/agentViewController.js +267 -3
- package/src/chat/index.js +13 -0
- package/src/chat/inputListenerController.js +196 -54
- package/src/chat/layout.js +18 -2
- package/src/code/tui.js +405 -50
package/package.json
CHANGED
|
@@ -32,6 +32,7 @@ function createAgentViewController(options = {}) {
|
|
|
32
32
|
connectAgentInput = () => {},
|
|
33
33
|
disconnectAgentInput = () => {},
|
|
34
34
|
sendRaw = () => {},
|
|
35
|
+
sendBusMessage = () => {},
|
|
35
36
|
sendResize = () => {},
|
|
36
37
|
requestScreenSnapshot = () => {},
|
|
37
38
|
} = options;
|
|
@@ -47,6 +48,9 @@ function createAgentViewController(options = {}) {
|
|
|
47
48
|
let agentBarVisible = false;
|
|
48
49
|
let detachedChildren = null;
|
|
49
50
|
let agentInputSuppressUntil = 0;
|
|
51
|
+
let busInputValue = "";
|
|
52
|
+
let busInputCursor = 0;
|
|
53
|
+
let busLogLines = [];
|
|
50
54
|
const originalRender = screen.render.bind(screen);
|
|
51
55
|
let renderFrozen = false;
|
|
52
56
|
|
|
@@ -63,6 +67,139 @@ function createAgentViewController(options = {}) {
|
|
|
63
67
|
return processStdout.columns || 80;
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
function stripAnsi(text = "") {
|
|
71
|
+
return String(text || "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "")
|
|
72
|
+
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function clamp(value, min, max) {
|
|
76
|
+
const normalized = Number.isFinite(value) ? Math.floor(value) : min;
|
|
77
|
+
return Math.max(min, Math.min(max, normalized));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function fitText(text = "", width = 1) {
|
|
81
|
+
const normalizedWidth = Math.max(1, width);
|
|
82
|
+
const clean = stripAnsi(String(text || "")).replace(/\r/g, "");
|
|
83
|
+
if (clean.length <= normalizedWidth) {
|
|
84
|
+
return clean + " ".repeat(normalizedWidth - clean.length);
|
|
85
|
+
}
|
|
86
|
+
if (normalizedWidth <= 1) return clean.slice(0, normalizedWidth);
|
|
87
|
+
return clean.slice(0, normalizedWidth - 1) + "…";
|
|
88
|
+
}
|
|
89
|
+
|
|
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))}┐`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function boxBottom(width = 80) {
|
|
98
|
+
return `└${"─".repeat(Math.max(1, width - 2))}┘`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function boxMiddle(text = "", width = 80) {
|
|
102
|
+
const inner = Math.max(1, width - 2);
|
|
103
|
+
return `│${fitText(text, inner)}│`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function wrapTextLine(text = "", width = 80) {
|
|
107
|
+
const inner = Math.max(1, width);
|
|
108
|
+
const clean = stripAnsi(String(text || ""));
|
|
109
|
+
if (!clean) return [""];
|
|
110
|
+
const lines = [];
|
|
111
|
+
let rest = clean;
|
|
112
|
+
while (rest.length > inner) {
|
|
113
|
+
lines.push(rest.slice(0, inner));
|
|
114
|
+
rest = rest.slice(inner);
|
|
115
|
+
}
|
|
116
|
+
lines.push(rest);
|
|
117
|
+
return lines;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getWrappedBusLogLines(width = 80) {
|
|
121
|
+
const inner = Math.max(1, width - 2);
|
|
122
|
+
const wrapped = [];
|
|
123
|
+
for (const line of busLogLines) {
|
|
124
|
+
wrapped.push(...wrapTextLine(line, inner));
|
|
125
|
+
}
|
|
126
|
+
return wrapped;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function writeAt(row, content = "") {
|
|
130
|
+
processStdout.write(`\x1b[${row};1H\x1b[2K${content}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function resetBusView(agentId) {
|
|
134
|
+
busInputValue = "";
|
|
135
|
+
busInputCursor = 0;
|
|
136
|
+
const label = getAgentLabel(agentId);
|
|
137
|
+
busLogLines = [
|
|
138
|
+
`ufoo internal · ${label}`,
|
|
139
|
+
"Enter 发送 · Esc 返回 · ↓ agent bar",
|
|
140
|
+
"",
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function appendBusLog(text = "") {
|
|
145
|
+
const clean = stripAnsi(String(text || "")).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
146
|
+
if (busLogLines.length === 0) busLogLines.push("");
|
|
147
|
+
for (const char of clean) {
|
|
148
|
+
if (char === "\n") {
|
|
149
|
+
busLogLines.push("");
|
|
150
|
+
} else {
|
|
151
|
+
busLogLines[busLogLines.length - 1] += char;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (busLogLines.length > 1000) {
|
|
155
|
+
busLogLines = busLogLines.slice(-1000);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getBusInputViewport(width) {
|
|
160
|
+
const inner = Math.max(1, width - 4);
|
|
161
|
+
const value = String(busInputValue || "").replace(/\n/g, "⏎");
|
|
162
|
+
let start = 0;
|
|
163
|
+
if (busInputCursor >= inner) {
|
|
164
|
+
start = busInputCursor - inner + 1;
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
text: value.slice(start, start + inner),
|
|
168
|
+
cursorCol: Math.max(0, busInputCursor - start),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function renderBusView() {
|
|
173
|
+
if (currentView !== "agent" || !agentViewUsesBus) return;
|
|
174
|
+
const rows = getRows();
|
|
175
|
+
const cols = getCols();
|
|
176
|
+
const width = Math.max(20, cols);
|
|
177
|
+
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;
|
|
182
|
+
const logContentHeight = Math.max(1, logContentBottom - logContentTop + 1);
|
|
183
|
+
const label = getAgentLabel(viewingAgent);
|
|
184
|
+
|
|
185
|
+
processStdout.write("\x1b[?25l");
|
|
186
|
+
writeAt(logTop, boxTop(`ufoo internal · ${label}`, width));
|
|
187
|
+
const visibleLines = getWrappedBusLogLines(width).slice(-logContentHeight);
|
|
188
|
+
for (let i = 0; i < logContentHeight; i += 1) {
|
|
189
|
+
writeAt(logContentTop + i, boxMiddle(visibleLines[i] || "", width));
|
|
190
|
+
}
|
|
191
|
+
writeAt(logBottom, boxBottom(width));
|
|
192
|
+
|
|
193
|
+
writeAt(inputTop, boxTop("message", width));
|
|
194
|
+
const viewport = getBusInputViewport(width);
|
|
195
|
+
writeAt(inputTop + 1, boxMiddle(`> ${viewport.text}`, width));
|
|
196
|
+
writeAt(inputTop + 2, boxBottom(width));
|
|
197
|
+
|
|
198
|
+
renderAgentDashboard();
|
|
199
|
+
const cursorCol = clamp(4 + viewport.cursorCol, 1, width);
|
|
200
|
+
processStdout.write(`\x1b[${inputTop + 1};${cursorCol}H\x1b[?25h`);
|
|
201
|
+
}
|
|
202
|
+
|
|
66
203
|
function renderAgentDashboard() {
|
|
67
204
|
if (!agentBarVisible && getFocusMode() !== "dashboard") return;
|
|
68
205
|
const rows = getRows();
|
|
@@ -127,8 +264,8 @@ function createAgentViewController(options = {}) {
|
|
|
127
264
|
agentInputSuppressUntil = now() + 300;
|
|
128
265
|
agentViewUsesBus = Boolean(options.useBus);
|
|
129
266
|
if (agentViewUsesBus) {
|
|
130
|
-
|
|
131
|
-
|
|
267
|
+
resetBusView(agentId);
|
|
268
|
+
renderBusView();
|
|
132
269
|
} else {
|
|
133
270
|
const sockPath = getInjectSockPath(agentId);
|
|
134
271
|
connectAgentOutput(sockPath);
|
|
@@ -153,6 +290,9 @@ function createAgentViewController(options = {}) {
|
|
|
153
290
|
agentViewUsesBus = false;
|
|
154
291
|
agentOutputSuppressed = false;
|
|
155
292
|
agentBarVisible = false;
|
|
293
|
+
busInputValue = "";
|
|
294
|
+
busInputCursor = 0;
|
|
295
|
+
busLogLines = [];
|
|
156
296
|
|
|
157
297
|
currentView = "main";
|
|
158
298
|
viewingAgent = null;
|
|
@@ -199,6 +339,117 @@ function createAgentViewController(options = {}) {
|
|
|
199
339
|
agentOutputSuppressed = true;
|
|
200
340
|
}
|
|
201
341
|
|
|
342
|
+
function insertBusInput(text = "") {
|
|
343
|
+
const value = String(text || "");
|
|
344
|
+
if (!value) return;
|
|
345
|
+
busInputValue = busInputValue.slice(0, busInputCursor) + value + busInputValue.slice(busInputCursor);
|
|
346
|
+
busInputCursor += value.length;
|
|
347
|
+
renderBusView();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function deleteBusInputBeforeCursor() {
|
|
351
|
+
if (busInputCursor <= 0) return;
|
|
352
|
+
busInputValue = busInputValue.slice(0, busInputCursor - 1) + busInputValue.slice(busInputCursor);
|
|
353
|
+
busInputCursor -= 1;
|
|
354
|
+
renderBusView();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function clearBusInput() {
|
|
358
|
+
busInputValue = "";
|
|
359
|
+
busInputCursor = 0;
|
|
360
|
+
renderBusView();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function submitBusInput() {
|
|
364
|
+
const text = String(busInputValue || "").trim();
|
|
365
|
+
if (!text) {
|
|
366
|
+
renderBusView();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
appendBusLog(`> ${text}\n`);
|
|
370
|
+
busInputValue = "";
|
|
371
|
+
busInputCursor = 0;
|
|
372
|
+
sendBusMessage(viewingAgent, text);
|
|
373
|
+
renderBusView();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function handleBusAgentKey(ch, key = {}) {
|
|
377
|
+
if (currentView !== "agent" || !agentViewUsesBus) return false;
|
|
378
|
+
const keyName = key && key.name;
|
|
379
|
+
|
|
380
|
+
if (keyName === "down") return false;
|
|
381
|
+
|
|
382
|
+
if (keyName === "escape") {
|
|
383
|
+
exitAgentView();
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
if (keyName === "return" || keyName === "enter") {
|
|
387
|
+
if (key && (key.shift || key.meta)) {
|
|
388
|
+
insertBusInput("\n");
|
|
389
|
+
} else {
|
|
390
|
+
submitBusInput();
|
|
391
|
+
}
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
if (key && key.ctrl && keyName === "u") {
|
|
395
|
+
clearBusInput();
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
if (key && key.ctrl && keyName === "a") {
|
|
399
|
+
busInputCursor = 0;
|
|
400
|
+
renderBusView();
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
if (key && key.ctrl && keyName === "e") {
|
|
404
|
+
busInputCursor = busInputValue.length;
|
|
405
|
+
renderBusView();
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
if (keyName === "left") {
|
|
409
|
+
busInputCursor = Math.max(0, busInputCursor - 1);
|
|
410
|
+
renderBusView();
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
if (keyName === "right") {
|
|
414
|
+
busInputCursor = Math.min(busInputValue.length, busInputCursor + 1);
|
|
415
|
+
renderBusView();
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
if (keyName === "home") {
|
|
419
|
+
busInputCursor = 0;
|
|
420
|
+
renderBusView();
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
if (keyName === "end") {
|
|
424
|
+
busInputCursor = busInputValue.length;
|
|
425
|
+
renderBusView();
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
if (keyName === "backspace") {
|
|
429
|
+
deleteBusInputBeforeCursor();
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
if (keyName === "delete") {
|
|
433
|
+
if (busInputCursor < busInputValue.length) {
|
|
434
|
+
busInputValue = busInputValue.slice(0, busInputCursor) + busInputValue.slice(busInputCursor + 1);
|
|
435
|
+
renderBusView();
|
|
436
|
+
}
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
if (ch && ch.length > 1 && (!keyName || keyName.length !== 1)) {
|
|
440
|
+
insertBusInput(ch.replace(/\r\n/g, "\n").replace(/\r/g, "\n"));
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
const insertChar = (ch && ch.length === 1)
|
|
444
|
+
? ch
|
|
445
|
+
: (keyName && keyName.length === 1 ? keyName : "");
|
|
446
|
+
if (insertChar && !/^[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]$/.test(insertChar)) {
|
|
447
|
+
insertBusInput(insertChar);
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
|
|
202
453
|
function sendRawToAgent(data) {
|
|
203
454
|
sendRaw(data);
|
|
204
455
|
}
|
|
@@ -219,6 +470,11 @@ function createAgentViewController(options = {}) {
|
|
|
219
470
|
const cleaned = text
|
|
220
471
|
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "")
|
|
221
472
|
.replace(/\x1b\[(?:[?>=]?[0-9]*c|[?]?6n|5n)/g, "");
|
|
473
|
+
if (agentViewUsesBus) {
|
|
474
|
+
appendBusLog(cleaned);
|
|
475
|
+
renderBusView();
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
222
478
|
if (cleaned) processStdout.write(cleaned);
|
|
223
479
|
if (agentBarVisible) {
|
|
224
480
|
const rows = getRows();
|
|
@@ -244,7 +500,11 @@ function createAgentViewController(options = {}) {
|
|
|
244
500
|
const cols = getCols();
|
|
245
501
|
processStdout.write(`\x1b[1;${rows - 1}r`);
|
|
246
502
|
sendResize(cols, Math.max(1, rows - 1));
|
|
247
|
-
|
|
503
|
+
if (agentViewUsesBus) {
|
|
504
|
+
renderBusView();
|
|
505
|
+
} else {
|
|
506
|
+
renderAgentDashboard();
|
|
507
|
+
}
|
|
248
508
|
return true;
|
|
249
509
|
}
|
|
250
510
|
|
|
@@ -270,6 +530,9 @@ function createAgentViewController(options = {}) {
|
|
|
270
530
|
|
|
271
531
|
function setAgentOutputSuppressed(value) {
|
|
272
532
|
agentOutputSuppressed = Boolean(value);
|
|
533
|
+
if (!agentOutputSuppressed && agentViewUsesBus) {
|
|
534
|
+
renderBusView();
|
|
535
|
+
}
|
|
273
536
|
}
|
|
274
537
|
|
|
275
538
|
function isAgentBarVisible() {
|
|
@@ -295,6 +558,7 @@ function createAgentViewController(options = {}) {
|
|
|
295
558
|
writeToAgentTerm,
|
|
296
559
|
placeAgentCursor,
|
|
297
560
|
handleResizeInAgentView,
|
|
561
|
+
handleBusAgentKey,
|
|
298
562
|
};
|
|
299
563
|
}
|
|
300
564
|
|
package/src/chat/index.js
CHANGED
|
@@ -1565,6 +1565,16 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1565
1565
|
sendRaw: (data) => {
|
|
1566
1566
|
sendRawWithCapabilities(data);
|
|
1567
1567
|
},
|
|
1568
|
+
sendBusMessage: (target, message) => {
|
|
1569
|
+
if (!target || !message) return;
|
|
1570
|
+
send({
|
|
1571
|
+
type: IPC_REQUEST_TYPES.BUS_SEND,
|
|
1572
|
+
target,
|
|
1573
|
+
message,
|
|
1574
|
+
injection_mode: "immediate",
|
|
1575
|
+
source: "chat-internal-agent-view",
|
|
1576
|
+
});
|
|
1577
|
+
},
|
|
1568
1578
|
sendResize: (cols, rows) => {
|
|
1569
1579
|
sendResizeWithCapabilities(cols, rows);
|
|
1570
1580
|
},
|
|
@@ -2026,6 +2036,9 @@ async function runChat(projectRoot, options = {}) {
|
|
|
2026
2036
|
if (key && key.ctrl && key.name === "c") {
|
|
2027
2037
|
return; // handled by screen.key(["C-c"])
|
|
2028
2038
|
}
|
|
2039
|
+
if (agentViewController && agentViewController.handleBusAgentKey(ch, key)) {
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2029
2042
|
// Down arrow: enter agents bar (same pattern as normal chat dashboard)
|
|
2030
2043
|
if (key && key.name === "down") {
|
|
2031
2044
|
enterAgentDashboardMode();
|
|
@@ -59,6 +59,82 @@ function createInputListenerController(options = {}) {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
function clampCursorPos(pos = 0, value = "") {
|
|
63
|
+
const text = String(value || "");
|
|
64
|
+
const normalized = Number.isFinite(pos) ? Math.floor(pos) : 0;
|
|
65
|
+
return Math.max(0, Math.min(text.length, normalized));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function refreshAfterEdit(textarea) {
|
|
69
|
+
resizeInput();
|
|
70
|
+
ensureInputCursorVisible();
|
|
71
|
+
updateCursor(textarea);
|
|
72
|
+
updateDraftFromInput();
|
|
73
|
+
|
|
74
|
+
if (textarea && shouldShowCompletion(textarea.value)) {
|
|
75
|
+
completionController.show(textarea.value);
|
|
76
|
+
} else {
|
|
77
|
+
completionController.hide();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
render(textarea);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function replaceInputRange(textarea, start, end, replacement = "") {
|
|
84
|
+
if (!textarea) return;
|
|
85
|
+
const value = String(textarea.value || "");
|
|
86
|
+
const safeStart = clampCursorPos(start, value);
|
|
87
|
+
const safeEnd = clampCursorPos(end, value);
|
|
88
|
+
const from = Math.min(safeStart, safeEnd);
|
|
89
|
+
const to = Math.max(safeStart, safeEnd);
|
|
90
|
+
const insert = String(replacement || "");
|
|
91
|
+
textarea.value = value.slice(0, from) + insert + value.slice(to);
|
|
92
|
+
setCursorPos(from + insert.length);
|
|
93
|
+
resetPreferredCol();
|
|
94
|
+
refreshAfterEdit(textarea);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function deleteWordBefore(textarea) {
|
|
98
|
+
const value = String((textarea && textarea.value) || "");
|
|
99
|
+
const cursorPos = clampCursorPos(getCursorPos(), value);
|
|
100
|
+
if (cursorPos <= 0) return;
|
|
101
|
+
const before = value.slice(0, cursorPos);
|
|
102
|
+
const match = before.match(/\s*\S+\s*$/);
|
|
103
|
+
const start = match ? cursorPos - match[0].length : Math.max(0, cursorPos - 1);
|
|
104
|
+
replaceInputRange(textarea, start, cursorPos, "");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function moveCursorByWord(textarea, direction = "forward") {
|
|
108
|
+
const value = String((textarea && textarea.value) || "");
|
|
109
|
+
const cursorPos = clampCursorPos(getCursorPos(), value);
|
|
110
|
+
if (direction === "backward") {
|
|
111
|
+
const before = value.slice(0, cursorPos);
|
|
112
|
+
const trimmedEnd = before.search(/\S\s*$/) >= 0 ? before.replace(/\s+$/, "") : before;
|
|
113
|
+
const match = trimmedEnd.match(/\S+$/);
|
|
114
|
+
return match ? trimmedEnd.length - match[0].length : 0;
|
|
115
|
+
}
|
|
116
|
+
const after = value.slice(cursorPos);
|
|
117
|
+
const match = after.match(/^\s*\S+/);
|
|
118
|
+
return match ? Math.min(value.length, cursorPos + match[0].length) : value.length;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function moveCursorToVisualBoundary(textarea, boundary = "start") {
|
|
122
|
+
const width = getWrapWidth();
|
|
123
|
+
const value = String((textarea && textarea.value) || "");
|
|
124
|
+
if (width <= 0) return boundary === "end" ? value.length : 0;
|
|
125
|
+
const cursorPos = clampCursorPos(getCursorPos(), value);
|
|
126
|
+
const { row } = getCursorRowCol(value, cursorPos, width);
|
|
127
|
+
const targetCol = boundary === "end" ? width : 0;
|
|
128
|
+
return getCursorPosForRowCol(value, row, targetCol, width);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function setCursorAndRender(textarea, nextPos) {
|
|
132
|
+
setCursorPos(clampCursorPos(nextPos, (textarea && textarea.value) || ""));
|
|
133
|
+
ensureInputCursorVisible();
|
|
134
|
+
updateCursor(textarea);
|
|
135
|
+
render(textarea);
|
|
136
|
+
}
|
|
137
|
+
|
|
62
138
|
function handleKey(ch, key = {}, textarea) {
|
|
63
139
|
const keyName = key && key.name;
|
|
64
140
|
|
|
@@ -136,8 +212,12 @@ function createInputListenerController(options = {}) {
|
|
|
136
212
|
}
|
|
137
213
|
|
|
138
214
|
if (keyName === "return" || keyName === "enter") {
|
|
139
|
-
|
|
215
|
+
const value = String((textarea && textarea.value) || "");
|
|
216
|
+
const cursorPos = clampCursorPos(getCursorPos(), value);
|
|
217
|
+
if (key && (key.shift || key.meta)) {
|
|
140
218
|
insertTextAtCursor("\n");
|
|
219
|
+
} else if (cursorPos > 0 && value[cursorPos - 1] === "\\") {
|
|
220
|
+
replaceInputRange(textarea, cursorPos - 1, cursorPos, "\n");
|
|
141
221
|
} else {
|
|
142
222
|
resetPreferredCol();
|
|
143
223
|
if (textarea && typeof textarea._done === "function") {
|
|
@@ -147,6 +227,87 @@ function createInputListenerController(options = {}) {
|
|
|
147
227
|
return;
|
|
148
228
|
}
|
|
149
229
|
|
|
230
|
+
if (key && key.ctrl) {
|
|
231
|
+
if (keyName === "a") {
|
|
232
|
+
setCursorAndRender(textarea, moveCursorToVisualBoundary(textarea, "start"));
|
|
233
|
+
resetPreferredCol();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (keyName === "e") {
|
|
237
|
+
setCursorAndRender(textarea, moveCursorToVisualBoundary(textarea, "end"));
|
|
238
|
+
resetPreferredCol();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (keyName === "b") {
|
|
242
|
+
const cursorPos = getCursorPos();
|
|
243
|
+
if (cursorPos > 0) setCursorAndRender(textarea, cursorPos - 1);
|
|
244
|
+
resetPreferredCol();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (keyName === "f") {
|
|
248
|
+
const cursorPos = getCursorPos();
|
|
249
|
+
const value = String((textarea && textarea.value) || "");
|
|
250
|
+
if (cursorPos < value.length) setCursorAndRender(textarea, cursorPos + 1);
|
|
251
|
+
resetPreferredCol();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (keyName === "d") {
|
|
255
|
+
const cursorPos = getCursorPos();
|
|
256
|
+
const value = String((textarea && textarea.value) || "");
|
|
257
|
+
if (cursorPos < value.length) replaceInputRange(textarea, cursorPos, cursorPos + 1, "");
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (keyName === "h") {
|
|
261
|
+
const cursorPos = getCursorPos();
|
|
262
|
+
if (cursorPos > 0) replaceInputRange(textarea, cursorPos - 1, cursorPos, "");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (keyName === "k") {
|
|
266
|
+
const cursorPos = getCursorPos();
|
|
267
|
+
const value = String((textarea && textarea.value) || "");
|
|
268
|
+
const target = moveCursorToVisualBoundary(textarea, "end");
|
|
269
|
+
if (target === cursorPos && value[cursorPos] === "\n") {
|
|
270
|
+
replaceInputRange(textarea, cursorPos, cursorPos + 1, "");
|
|
271
|
+
} else {
|
|
272
|
+
replaceInputRange(textarea, cursorPos, target, "");
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (keyName === "u") {
|
|
277
|
+
const cursorPos = getCursorPos();
|
|
278
|
+
const value = String((textarea && textarea.value) || "");
|
|
279
|
+
const target = moveCursorToVisualBoundary(textarea, "start");
|
|
280
|
+
if (target === cursorPos && value[cursorPos - 1] === "\n") {
|
|
281
|
+
replaceInputRange(textarea, cursorPos - 1, cursorPos, "");
|
|
282
|
+
} else {
|
|
283
|
+
replaceInputRange(textarea, target, cursorPos, "");
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (keyName === "w") {
|
|
288
|
+
deleteWordBefore(textarea);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (key && key.meta) {
|
|
294
|
+
if (keyName === "b") {
|
|
295
|
+
setCursorAndRender(textarea, moveCursorByWord(textarea, "backward"));
|
|
296
|
+
resetPreferredCol();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (keyName === "f") {
|
|
300
|
+
setCursorAndRender(textarea, moveCursorByWord(textarea, "forward"));
|
|
301
|
+
resetPreferredCol();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (keyName === "d") {
|
|
305
|
+
const cursorPos = getCursorPos();
|
|
306
|
+
replaceInputRange(textarea, cursorPos, moveCursorByWord(textarea, "forward"), "");
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
150
311
|
if (keyName === "left") {
|
|
151
312
|
const cursorPos = getCursorPos();
|
|
152
313
|
if (cursorPos > 0) setCursorPos(cursorPos - 1);
|
|
@@ -170,20 +331,14 @@ function createInputListenerController(options = {}) {
|
|
|
170
331
|
}
|
|
171
332
|
|
|
172
333
|
if (keyName === "home") {
|
|
173
|
-
|
|
334
|
+
setCursorAndRender(textarea, moveCursorToVisualBoundary(textarea, "start"));
|
|
174
335
|
resetPreferredCol();
|
|
175
|
-
ensureInputCursorVisible();
|
|
176
|
-
updateCursor(textarea);
|
|
177
|
-
render(textarea);
|
|
178
336
|
return;
|
|
179
337
|
}
|
|
180
338
|
|
|
181
339
|
if (keyName === "end") {
|
|
182
|
-
|
|
340
|
+
setCursorAndRender(textarea, moveCursorToVisualBoundary(textarea, "end"));
|
|
183
341
|
resetPreferredCol();
|
|
184
|
-
ensureInputCursorVisible();
|
|
185
|
-
updateCursor(textarea);
|
|
186
|
-
render(textarea);
|
|
187
342
|
return;
|
|
188
343
|
}
|
|
189
344
|
|
|
@@ -192,17 +347,6 @@ function createInputListenerController(options = {}) {
|
|
|
192
347
|
completionController.jumpToLast();
|
|
193
348
|
return;
|
|
194
349
|
}
|
|
195
|
-
if (historyUp()) {
|
|
196
|
-
completionController.hide();
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (keyName === "down") {
|
|
202
|
-
if (historyDown()) {
|
|
203
|
-
completionController.hide();
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
350
|
}
|
|
207
351
|
|
|
208
352
|
if (keyName === "up" || keyName === "down") {
|
|
@@ -220,11 +364,34 @@ function createInputListenerController(options = {}) {
|
|
|
220
364
|
|
|
221
365
|
const cursorPos = getCursorPos();
|
|
222
366
|
const value = (textarea && textarea.value) || "";
|
|
367
|
+
if (!value) {
|
|
368
|
+
if (keyName === "up") {
|
|
369
|
+
if (historyUp()) completionController.hide();
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (historyDown()) {
|
|
373
|
+
completionController.hide();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
enterDashboardMode();
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
223
379
|
const { row, col } = getCursorRowCol(value, cursorPos, innerWidth);
|
|
224
380
|
if (getPreferredCol() === null) setPreferredCol(col);
|
|
225
381
|
const totalRows = countLines(value, innerWidth);
|
|
226
382
|
|
|
383
|
+
if (keyName === "up" && row <= 0) {
|
|
384
|
+
if (historyUp()) {
|
|
385
|
+
completionController.hide();
|
|
386
|
+
}
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
227
390
|
if (keyName === "down" && row >= totalRows - 1) {
|
|
391
|
+
if (historyDown()) {
|
|
392
|
+
completionController.hide();
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
228
395
|
enterDashboardMode();
|
|
229
396
|
return;
|
|
230
397
|
}
|
|
@@ -261,21 +428,11 @@ function createInputListenerController(options = {}) {
|
|
|
261
428
|
if (keyName === "backspace") {
|
|
262
429
|
const cursorPos = getCursorPos();
|
|
263
430
|
if (cursorPos > 0 && textarea) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
resetPreferredCol();
|
|
267
|
-
resizeInput();
|
|
268
|
-
ensureInputCursorVisible();
|
|
269
|
-
updateCursor(textarea);
|
|
270
|
-
updateDraftFromInput();
|
|
271
|
-
|
|
272
|
-
if (shouldShowCompletion(textarea.value)) {
|
|
273
|
-
completionController.show(textarea.value);
|
|
431
|
+
if (key && (key.ctrl || key.meta)) {
|
|
432
|
+
deleteWordBefore(textarea);
|
|
274
433
|
} else {
|
|
275
|
-
|
|
434
|
+
replaceInputRange(textarea, cursorPos - 1, cursorPos, "");
|
|
276
435
|
}
|
|
277
|
-
|
|
278
|
-
render(textarea);
|
|
279
436
|
}
|
|
280
437
|
return;
|
|
281
438
|
}
|
|
@@ -283,13 +440,11 @@ function createInputListenerController(options = {}) {
|
|
|
283
440
|
if (keyName === "delete") {
|
|
284
441
|
const cursorPos = getCursorPos();
|
|
285
442
|
if (textarea && cursorPos < textarea.value.length) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
render(textarea);
|
|
292
|
-
updateDraftFromInput();
|
|
443
|
+
if (key && key.meta) {
|
|
444
|
+
replaceInputRange(textarea, cursorPos, moveCursorToVisualBoundary(textarea, "end"), "");
|
|
445
|
+
} else {
|
|
446
|
+
replaceInputRange(textarea, cursorPos, cursorPos + 1, "");
|
|
447
|
+
}
|
|
293
448
|
}
|
|
294
449
|
return;
|
|
295
450
|
}
|
|
@@ -300,21 +455,8 @@ function createInputListenerController(options = {}) {
|
|
|
300
455
|
|
|
301
456
|
if (insertChar && !/^[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]$/.test(insertChar) && textarea) {
|
|
302
457
|
const cursorPos = getCursorPos();
|
|
303
|
-
textarea.value = textarea.value.slice(0, cursorPos) + insertChar + textarea.value.slice(cursorPos);
|
|
304
|
-
setCursorPos(cursorPos + 1);
|
|
305
458
|
normalizeCommandPrefix();
|
|
306
|
-
|
|
307
|
-
resizeInput();
|
|
308
|
-
updateCursor(textarea);
|
|
309
|
-
updateDraftFromInput();
|
|
310
|
-
|
|
311
|
-
if (shouldShowCompletion(textarea.value)) {
|
|
312
|
-
completionController.show(textarea.value);
|
|
313
|
-
} else if (completionController.isActive()) {
|
|
314
|
-
completionController.hide();
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
render(textarea);
|
|
459
|
+
replaceInputRange(textarea, cursorPos, cursorPos, insertChar);
|
|
318
460
|
}
|
|
319
461
|
}
|
|
320
462
|
|
package/src/chat/layout.js
CHANGED
|
@@ -4,6 +4,8 @@ function createChatLayout(options = {}) {
|
|
|
4
4
|
currentInputHeight = 4,
|
|
5
5
|
dashboardHeight = 1,
|
|
6
6
|
version = "unknown",
|
|
7
|
+
logBorder = false,
|
|
8
|
+
logScrollbar = false,
|
|
7
9
|
} = options;
|
|
8
10
|
const normalizedDashboardHeight = Number.isFinite(dashboardHeight) && dashboardHeight > 0
|
|
9
11
|
? Math.floor(dashboardHeight)
|
|
@@ -32,24 +34,38 @@ function createChatLayout(options = {}) {
|
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
// Log area
|
|
37
|
+
// Log area
|
|
36
38
|
const logBox = blessed.log({
|
|
37
39
|
parent: screen,
|
|
38
40
|
top: 0,
|
|
39
41
|
left: 0,
|
|
40
42
|
width: "100%",
|
|
41
43
|
height: `100%-${reservedBottomLines}`, // Will be adjusted dynamically
|
|
44
|
+
border: logBorder ? { type: "line" } : null,
|
|
42
45
|
tags: true,
|
|
43
46
|
scrollable: true,
|
|
44
47
|
alwaysScroll: true,
|
|
45
48
|
scrollback: 10000,
|
|
46
|
-
scrollbar:
|
|
49
|
+
scrollbar: logScrollbar
|
|
50
|
+
? {
|
|
51
|
+
ch: " ",
|
|
52
|
+
track: { bg: "black" },
|
|
53
|
+
style: { bg: "gray" },
|
|
54
|
+
}
|
|
55
|
+
: null,
|
|
47
56
|
keys: true,
|
|
48
57
|
vi: true,
|
|
49
58
|
// Mouse handled globally (toggleable) to keep copy working
|
|
50
59
|
mouse: false,
|
|
51
60
|
// Ensure proper wrapping and width calculation
|
|
52
61
|
wrap: true,
|
|
62
|
+
padding: logBorder ? { left: 1, right: 1 } : undefined,
|
|
63
|
+
style: logBorder
|
|
64
|
+
? {
|
|
65
|
+
border: { fg: "gray" },
|
|
66
|
+
scrollbar: { bg: "gray" },
|
|
67
|
+
}
|
|
68
|
+
: undefined,
|
|
53
69
|
});
|
|
54
70
|
|
|
55
71
|
// Status line just above input
|
package/src/code/tui.js
CHANGED
|
@@ -293,6 +293,111 @@ function moveCursorHorizontally(cursorPos = 0, inputValue = "", direction = "rig
|
|
|
293
293
|
return Math.min(max, pos + 1);
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
function clampCursorPos(cursorPos = 0, inputValue = "") {
|
|
297
|
+
const text = String(inputValue || "");
|
|
298
|
+
const pos = Number.isFinite(cursorPos) ? Math.floor(cursorPos) : 0;
|
|
299
|
+
return Math.max(0, Math.min(text.length, pos));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function findLogicalLineStart(inputValue = "", cursorPos = 0) {
|
|
303
|
+
const text = String(inputValue || "");
|
|
304
|
+
const pos = clampCursorPos(cursorPos, text);
|
|
305
|
+
const prevNewline = text.lastIndexOf("\n", Math.max(0, pos - 1));
|
|
306
|
+
return prevNewline === -1 ? 0 : prevNewline + 1;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function findLogicalLineEnd(inputValue = "", cursorPos = 0) {
|
|
310
|
+
const text = String(inputValue || "");
|
|
311
|
+
const pos = clampCursorPos(cursorPos, text);
|
|
312
|
+
const nextNewline = text.indexOf("\n", pos);
|
|
313
|
+
return nextNewline === -1 ? text.length : nextNewline;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function moveCursorToVisualLineBoundary({
|
|
317
|
+
cursorPos = 0,
|
|
318
|
+
inputValue = "",
|
|
319
|
+
width = 80,
|
|
320
|
+
boundary = "start",
|
|
321
|
+
strWidth,
|
|
322
|
+
} = {}) {
|
|
323
|
+
const inputMath = require("../chat/inputMath");
|
|
324
|
+
const text = String(inputValue || "");
|
|
325
|
+
const normalizedWidth = Number.isFinite(width) ? Math.max(1, Math.floor(width)) : 1;
|
|
326
|
+
const pos = clampCursorPos(cursorPos, text);
|
|
327
|
+
const { row } = inputMath.getCursorRowCol(text, pos, normalizedWidth, strWidth);
|
|
328
|
+
if (boundary === "end") {
|
|
329
|
+
return inputMath.getCursorPosForRowCol(text, row, normalizedWidth, normalizedWidth, strWidth);
|
|
330
|
+
}
|
|
331
|
+
return inputMath.getCursorPosForRowCol(text, row, 0, normalizedWidth, strWidth);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function moveCursorVertically({
|
|
335
|
+
cursorPos = 0,
|
|
336
|
+
inputValue = "",
|
|
337
|
+
width = 80,
|
|
338
|
+
direction = "down",
|
|
339
|
+
preferredCol = null,
|
|
340
|
+
strWidth,
|
|
341
|
+
} = {}) {
|
|
342
|
+
const inputMath = require("../chat/inputMath");
|
|
343
|
+
const text = String(inputValue || "");
|
|
344
|
+
const normalizedWidth = Number.isFinite(width) ? Math.max(1, Math.floor(width)) : 1;
|
|
345
|
+
const pos = clampCursorPos(cursorPos, text);
|
|
346
|
+
const { row, col } = inputMath.getCursorRowCol(text, pos, normalizedWidth, strWidth);
|
|
347
|
+
const totalRows = inputMath.countLines(text, normalizedWidth, strWidth);
|
|
348
|
+
const targetCol = Number.isFinite(preferredCol) ? preferredCol : col;
|
|
349
|
+
|
|
350
|
+
if (direction === "up") {
|
|
351
|
+
if (row <= 0) {
|
|
352
|
+
return { moved: false, nextCursorPos: pos, preferredCol: targetCol, boundary: "top" };
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
moved: true,
|
|
356
|
+
nextCursorPos: inputMath.getCursorPosForRowCol(text, row - 1, targetCol, normalizedWidth, strWidth),
|
|
357
|
+
preferredCol: targetCol,
|
|
358
|
+
boundary: "",
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (row >= totalRows - 1) {
|
|
363
|
+
return { moved: false, nextCursorPos: pos, preferredCol: targetCol, boundary: "bottom" };
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
moved: true,
|
|
367
|
+
nextCursorPos: inputMath.getCursorPosForRowCol(text, row + 1, targetCol, normalizedWidth, strWidth),
|
|
368
|
+
preferredCol: targetCol,
|
|
369
|
+
boundary: "",
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function deleteWordBeforeCursor(inputValue = "", cursorPos = 0) {
|
|
374
|
+
const text = String(inputValue || "");
|
|
375
|
+
const pos = clampCursorPos(cursorPos, text);
|
|
376
|
+
if (pos <= 0) return { value: text, cursorPos: pos };
|
|
377
|
+
const before = text.slice(0, pos);
|
|
378
|
+
const after = text.slice(pos);
|
|
379
|
+
const match = before.match(/\s*\S+\s*$/);
|
|
380
|
+
const start = match ? pos - match[0].length : Math.max(0, pos - 1);
|
|
381
|
+
return {
|
|
382
|
+
value: before.slice(0, start) + after,
|
|
383
|
+
cursorPos: start,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function moveCursorByWord(inputValue = "", cursorPos = 0, direction = "forward") {
|
|
388
|
+
const text = String(inputValue || "");
|
|
389
|
+
const pos = clampCursorPos(cursorPos, text);
|
|
390
|
+
if (direction === "backward") {
|
|
391
|
+
const before = text.slice(0, pos);
|
|
392
|
+
const trimmedEnd = before.search(/\S\s*$/) >= 0 ? before.replace(/\s+$/, "") : before;
|
|
393
|
+
const match = trimmedEnd.match(/\S+$/);
|
|
394
|
+
return match ? trimmedEnd.length - match[0].length : 0;
|
|
395
|
+
}
|
|
396
|
+
const after = text.slice(pos);
|
|
397
|
+
const match = after.match(/^\s*\S+/);
|
|
398
|
+
return match ? Math.min(text.length, pos + match[0].length) : text.length;
|
|
399
|
+
}
|
|
400
|
+
|
|
296
401
|
function resolveHistoryDownTransition({
|
|
297
402
|
inputHistory = [],
|
|
298
403
|
historyIndex = 0,
|
|
@@ -499,6 +604,11 @@ function runUcodeTui({
|
|
|
499
604
|
let lastMergedToolGroup = null;
|
|
500
605
|
let toolMergeId = 0;
|
|
501
606
|
let cursorPos = 0;
|
|
607
|
+
let preferredCol = null;
|
|
608
|
+
let currentInputHeight = 4;
|
|
609
|
+
const MIN_INPUT_CONTENT_HEIGHT = 1;
|
|
610
|
+
const MAX_INPUT_CONTENT_HEIGHT = 8;
|
|
611
|
+
const DASHBOARD_HEIGHT = 1;
|
|
502
612
|
let autoBusTimer = null;
|
|
503
613
|
let autoBusQueued = false;
|
|
504
614
|
let autoBusError = "";
|
|
@@ -510,12 +620,15 @@ function runUcodeTui({
|
|
|
510
620
|
statusLine,
|
|
511
621
|
completionPanel,
|
|
512
622
|
dashboard,
|
|
623
|
+
inputTopLine,
|
|
513
624
|
promptBox,
|
|
514
625
|
input,
|
|
515
626
|
} = createChatLayout({
|
|
516
627
|
blessed,
|
|
517
628
|
currentInputHeight: 4,
|
|
518
629
|
version: UCODE_VERSION,
|
|
630
|
+
logBorder: true,
|
|
631
|
+
logScrollbar: true,
|
|
519
632
|
});
|
|
520
633
|
|
|
521
634
|
if (completionPanel && typeof completionPanel.hide === "function") {
|
|
@@ -555,6 +668,7 @@ function runUcodeTui({
|
|
|
555
668
|
promptBox.width = Math.max(2, plain.length + 1);
|
|
556
669
|
input.left = promptBox.width;
|
|
557
670
|
input.width = `100%-${promptBox.width}`;
|
|
671
|
+
resizeInput();
|
|
558
672
|
};
|
|
559
673
|
|
|
560
674
|
// --- Cursor position helpers (mirrors chat inputListenerController) ---
|
|
@@ -565,6 +679,10 @@ function runUcodeTui({
|
|
|
565
679
|
|
|
566
680
|
const getWrapWidth = () => inputMath.getWrapWidth(input, getInnerWidth());
|
|
567
681
|
|
|
682
|
+
const resetPreferredCol = () => {
|
|
683
|
+
preferredCol = null;
|
|
684
|
+
};
|
|
685
|
+
|
|
568
686
|
const ensureInputCursorVisible = () => {
|
|
569
687
|
const innerWidth = getWrapWidth();
|
|
570
688
|
if (innerWidth <= 0) return;
|
|
@@ -583,6 +701,105 @@ function runUcodeTui({
|
|
|
583
701
|
}
|
|
584
702
|
};
|
|
585
703
|
|
|
704
|
+
const resizeInput = () => {
|
|
705
|
+
const innerWidth = getWrapWidth();
|
|
706
|
+
if (innerWidth <= 0) return;
|
|
707
|
+
const totalRows = inputMath.countLines(input.value || "", innerWidth, (v) => input.strWidth(v));
|
|
708
|
+
const contentHeight = Math.min(
|
|
709
|
+
MAX_INPUT_CONTENT_HEIGHT,
|
|
710
|
+
Math.max(MIN_INPUT_CONTENT_HEIGHT, totalRows)
|
|
711
|
+
);
|
|
712
|
+
const targetHeight = contentHeight + DASHBOARD_HEIGHT + 2;
|
|
713
|
+
if (targetHeight !== currentInputHeight) {
|
|
714
|
+
currentInputHeight = targetHeight;
|
|
715
|
+
input.height = contentHeight;
|
|
716
|
+
promptBox.height = contentHeight;
|
|
717
|
+
if (inputTopLine) inputTopLine.bottom = currentInputHeight - 1;
|
|
718
|
+
}
|
|
719
|
+
statusLine.bottom = currentInputHeight;
|
|
720
|
+
logBox.height = Math.max(1, screen.height - currentInputHeight - 1);
|
|
721
|
+
ensureInputCursorVisible();
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
const renderInput = () => {
|
|
725
|
+
resizeInput();
|
|
726
|
+
ensureInputCursorVisible();
|
|
727
|
+
input._updateCursor();
|
|
728
|
+
screen.render();
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
const setCursor = (nextPos) => {
|
|
732
|
+
cursorPos = clampCursorPos(nextPos, input.value || "");
|
|
733
|
+
ensureInputCursorVisible();
|
|
734
|
+
input._updateCursor();
|
|
735
|
+
screen.render();
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
const setInputValue = (value) => {
|
|
739
|
+
input.setValue(value || "");
|
|
740
|
+
cursorPos = (value || "").length;
|
|
741
|
+
resetPreferredCol();
|
|
742
|
+
renderInput();
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
const replaceInputRange = (start, end, replacement = "") => {
|
|
746
|
+
const value = input.value || "";
|
|
747
|
+
const safeStart = clampCursorPos(start, value);
|
|
748
|
+
const safeEnd = clampCursorPos(end, value);
|
|
749
|
+
const from = Math.min(safeStart, safeEnd);
|
|
750
|
+
const to = Math.max(safeStart, safeEnd);
|
|
751
|
+
input.value = value.slice(0, from) + String(replacement || "") + value.slice(to);
|
|
752
|
+
cursorPos = from + String(replacement || "").length;
|
|
753
|
+
resetPreferredCol();
|
|
754
|
+
renderInput();
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
const insertTextAtCursor = (text = "") => {
|
|
758
|
+
const normalized = inputMath.normalizePaste(text);
|
|
759
|
+
if (!normalized) return;
|
|
760
|
+
replaceInputRange(cursorPos, cursorPos, normalized);
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
const deleteBeforeCursor = () => {
|
|
764
|
+
if (cursorPos <= 0) return;
|
|
765
|
+
replaceInputRange(cursorPos - 1, cursorPos, "");
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
const deleteAtCursor = () => {
|
|
769
|
+
const value = input.value || "";
|
|
770
|
+
if (cursorPos >= value.length) return;
|
|
771
|
+
replaceInputRange(cursorPos, cursorPos + 1, "");
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
const deleteToBoundary = (boundary) => {
|
|
775
|
+
const value = input.value || "";
|
|
776
|
+
const innerWidth = getWrapWidth();
|
|
777
|
+
const target = boundary === "end"
|
|
778
|
+
? moveCursorToVisualLineBoundary({
|
|
779
|
+
cursorPos,
|
|
780
|
+
inputValue: value,
|
|
781
|
+
width: innerWidth,
|
|
782
|
+
boundary: "end",
|
|
783
|
+
strWidth: (v) => input.strWidth(v),
|
|
784
|
+
})
|
|
785
|
+
: moveCursorToVisualLineBoundary({
|
|
786
|
+
cursorPos,
|
|
787
|
+
inputValue: value,
|
|
788
|
+
width: innerWidth,
|
|
789
|
+
boundary: "start",
|
|
790
|
+
strWidth: (v) => input.strWidth(v),
|
|
791
|
+
});
|
|
792
|
+
if (target === cursorPos && boundary === "end" && value[cursorPos] === "\n") {
|
|
793
|
+
replaceInputRange(cursorPos, cursorPos + 1, "");
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
if (target === cursorPos && boundary === "start" && value[cursorPos - 1] === "\n") {
|
|
797
|
+
replaceInputRange(cursorPos - 1, cursorPos, "");
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
replaceInputRange(Math.min(cursorPos, target), Math.max(cursorPos, target), "");
|
|
801
|
+
};
|
|
802
|
+
|
|
586
803
|
// Override _updateCursor to use our tracked cursorPos
|
|
587
804
|
input._updateCursor = function () {
|
|
588
805
|
if (this.screen.focused !== this) return;
|
|
@@ -603,8 +820,8 @@ function runUcodeTui({
|
|
|
603
820
|
};
|
|
604
821
|
|
|
605
822
|
// Override _listener to support cursor-aware editing
|
|
606
|
-
const origDone = input._done ? input._done.bind(input) : null;
|
|
607
823
|
let lastKeyRef = null;
|
|
824
|
+
let skipSubmitKeyRef = null;
|
|
608
825
|
input._listener = function (ch, key) {
|
|
609
826
|
const keyName = key && key.name;
|
|
610
827
|
|
|
@@ -614,69 +831,161 @@ function runUcodeTui({
|
|
|
614
831
|
if (key && key === lastKeyRef) return;
|
|
615
832
|
lastKeyRef = key || null;
|
|
616
833
|
|
|
617
|
-
|
|
618
|
-
|
|
834
|
+
if (keyName === "escape") return;
|
|
835
|
+
|
|
836
|
+
if (keyName === "return" || keyName === "enter") {
|
|
837
|
+
const value = this.value || "";
|
|
838
|
+
if (key && (key.shift || key.meta)) {
|
|
839
|
+
insertTextAtCursor("\n");
|
|
840
|
+
skipSubmitKeyRef = key || true;
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (cursorPos > 0 && value[cursorPos - 1] === "\\") {
|
|
844
|
+
replaceInputRange(cursorPos - 1, cursorPos, "\n");
|
|
845
|
+
skipSubmitKeyRef = key || true;
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
619
850
|
|
|
620
851
|
// Arrow keys handled by input.key() handlers below
|
|
621
852
|
if (keyName === "left" || keyName === "right" || keyName === "up" || keyName === "down") return;
|
|
622
853
|
|
|
854
|
+
if (key && key.ctrl) {
|
|
855
|
+
if (keyName === "a") {
|
|
856
|
+
setCursor(moveCursorToVisualLineBoundary({
|
|
857
|
+
cursorPos,
|
|
858
|
+
inputValue: this.value || "",
|
|
859
|
+
width: getWrapWidth(),
|
|
860
|
+
boundary: "start",
|
|
861
|
+
strWidth: (v) => this.strWidth(v),
|
|
862
|
+
}));
|
|
863
|
+
resetPreferredCol();
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (keyName === "e") {
|
|
867
|
+
setCursor(moveCursorToVisualLineBoundary({
|
|
868
|
+
cursorPos,
|
|
869
|
+
inputValue: this.value || "",
|
|
870
|
+
width: getWrapWidth(),
|
|
871
|
+
boundary: "end",
|
|
872
|
+
strWidth: (v) => this.strWidth(v),
|
|
873
|
+
}));
|
|
874
|
+
resetPreferredCol();
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
if (keyName === "b") {
|
|
878
|
+
setCursor(moveCursorHorizontally(cursorPos, this.value || "", "left"));
|
|
879
|
+
resetPreferredCol();
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
if (keyName === "f") {
|
|
883
|
+
setCursor(moveCursorHorizontally(cursorPos, this.value || "", "right"));
|
|
884
|
+
resetPreferredCol();
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (keyName === "d") {
|
|
888
|
+
deleteAtCursor();
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
if (keyName === "h") {
|
|
892
|
+
deleteBeforeCursor();
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
if (keyName === "k") {
|
|
896
|
+
deleteToBoundary("end");
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
if (keyName === "u") {
|
|
900
|
+
deleteToBoundary("start");
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (keyName === "w") {
|
|
904
|
+
const next = deleteWordBeforeCursor(this.value || "", cursorPos);
|
|
905
|
+
this.value = next.value;
|
|
906
|
+
cursorPos = next.cursorPos;
|
|
907
|
+
resetPreferredCol();
|
|
908
|
+
renderInput();
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if (key && key.meta) {
|
|
914
|
+
if (keyName === "b") {
|
|
915
|
+
setCursor(moveCursorByWord(this.value || "", cursorPos, "backward"));
|
|
916
|
+
resetPreferredCol();
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (keyName === "f") {
|
|
920
|
+
setCursor(moveCursorByWord(this.value || "", cursorPos, "forward"));
|
|
921
|
+
resetPreferredCol();
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
if (keyName === "d") {
|
|
925
|
+
const end = moveCursorByWord(this.value || "", cursorPos, "forward");
|
|
926
|
+
replaceInputRange(cursorPos, end, "");
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
623
931
|
if (keyName === "backspace") {
|
|
624
|
-
if (
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
932
|
+
if (key && (key.meta || key.ctrl)) {
|
|
933
|
+
const next = deleteWordBeforeCursor(this.value || "", cursorPos);
|
|
934
|
+
this.value = next.value;
|
|
935
|
+
cursorPos = next.cursorPos;
|
|
936
|
+
resetPreferredCol();
|
|
937
|
+
renderInput();
|
|
938
|
+
} else {
|
|
939
|
+
deleteBeforeCursor();
|
|
630
940
|
}
|
|
631
941
|
return;
|
|
632
942
|
}
|
|
633
943
|
|
|
634
944
|
if (keyName === "delete") {
|
|
635
|
-
if (
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
this.screen.render();
|
|
945
|
+
if (key && key.meta) {
|
|
946
|
+
deleteToBoundary("end");
|
|
947
|
+
} else {
|
|
948
|
+
deleteAtCursor();
|
|
640
949
|
}
|
|
641
950
|
return;
|
|
642
951
|
}
|
|
643
952
|
|
|
644
953
|
if (keyName === "home") {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
954
|
+
setCursor(moveCursorToVisualLineBoundary({
|
|
955
|
+
cursorPos,
|
|
956
|
+
inputValue: this.value || "",
|
|
957
|
+
width: getWrapWidth(),
|
|
958
|
+
boundary: "start",
|
|
959
|
+
strWidth: (v) => this.strWidth(v),
|
|
960
|
+
}));
|
|
961
|
+
resetPreferredCol();
|
|
649
962
|
return;
|
|
650
963
|
}
|
|
651
964
|
|
|
652
965
|
if (keyName === "end") {
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
966
|
+
setCursor(moveCursorToVisualLineBoundary({
|
|
967
|
+
cursorPos,
|
|
968
|
+
inputValue: this.value || "",
|
|
969
|
+
width: getWrapWidth(),
|
|
970
|
+
boundary: "end",
|
|
971
|
+
strWidth: (v) => this.strWidth(v),
|
|
972
|
+
}));
|
|
973
|
+
resetPreferredCol();
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
if (ch && ch.length > 1 && (!keyName || keyName.length !== 1)) {
|
|
978
|
+
insertTextAtCursor(ch);
|
|
657
979
|
return;
|
|
658
980
|
}
|
|
659
981
|
|
|
660
982
|
// Normal character insertion at cursor position
|
|
661
983
|
const insertChar = (ch && ch.length === 1) ? ch : (keyName && keyName.length === 1 ? keyName : null);
|
|
662
984
|
if (insertChar && !/^[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]$/.test(insertChar)) {
|
|
663
|
-
|
|
664
|
-
cursorPos += 1;
|
|
665
|
-
ensureInputCursorVisible();
|
|
666
|
-
this._updateCursor();
|
|
667
|
-
this.screen.render();
|
|
985
|
+
insertTextAtCursor(insertChar);
|
|
668
986
|
}
|
|
669
987
|
};
|
|
670
988
|
|
|
671
|
-
// Helper to set input value and reset cursor to end
|
|
672
|
-
const setInputValue = (value) => {
|
|
673
|
-
input.setValue(value || "");
|
|
674
|
-
cursorPos = (value || "").length;
|
|
675
|
-
ensureInputCursorVisible();
|
|
676
|
-
input._updateCursor();
|
|
677
|
-
screen.render();
|
|
678
|
-
};
|
|
679
|
-
|
|
680
989
|
const renderDashboard = () => {
|
|
681
990
|
let hint = "No target agents";
|
|
682
991
|
if (activeAgents.length > 0) {
|
|
@@ -922,7 +1231,7 @@ function runUcodeTui({
|
|
|
922
1231
|
statusInterval = null;
|
|
923
1232
|
}
|
|
924
1233
|
if (!message) {
|
|
925
|
-
statusLine.setContent(escapeBlessed(`UCODE · Ready${getBackgroundSuffix()}`));
|
|
1234
|
+
statusLine.setContent(escapeBlessed(`UCODE · Ready · Enter send · Shift/Alt+Enter newline · PgUp/PgDn log · Ctrl+O tools${getBackgroundSuffix()}`));
|
|
926
1235
|
screen.render();
|
|
927
1236
|
return;
|
|
928
1237
|
}
|
|
@@ -1321,6 +1630,8 @@ function runUcodeTui({
|
|
|
1321
1630
|
const trimmed = raw.trim();
|
|
1322
1631
|
input.setValue("");
|
|
1323
1632
|
cursorPos = 0;
|
|
1633
|
+
resetPreferredCol();
|
|
1634
|
+
resizeInput();
|
|
1324
1635
|
screen.render();
|
|
1325
1636
|
agentSelectionMode = false;
|
|
1326
1637
|
|
|
@@ -1345,7 +1656,11 @@ function runUcodeTui({
|
|
|
1345
1656
|
});
|
|
1346
1657
|
};
|
|
1347
1658
|
|
|
1348
|
-
input.key(["enter"], () => {
|
|
1659
|
+
input.key(["enter"], (ch, key) => {
|
|
1660
|
+
if (skipSubmitKeyRef && (!key || skipSubmitKeyRef === key || skipSubmitKeyRef === true)) {
|
|
1661
|
+
skipSubmitKeyRef = null;
|
|
1662
|
+
return false;
|
|
1663
|
+
}
|
|
1349
1664
|
submitInput(input.getValue());
|
|
1350
1665
|
return false;
|
|
1351
1666
|
});
|
|
@@ -1355,7 +1670,6 @@ function runUcodeTui({
|
|
|
1355
1670
|
agentSelectionMode,
|
|
1356
1671
|
inputValue: currentValue,
|
|
1357
1672
|
})) {
|
|
1358
|
-
const previousTarget = targetAgent;
|
|
1359
1673
|
targetAgent = null;
|
|
1360
1674
|
selectedAgentIndex = -1;
|
|
1361
1675
|
agentSelectionMode = false;
|
|
@@ -1365,12 +1679,43 @@ function runUcodeTui({
|
|
|
1365
1679
|
input.focus();
|
|
1366
1680
|
return false;
|
|
1367
1681
|
}
|
|
1368
|
-
if (
|
|
1682
|
+
if (currentValue) {
|
|
1683
|
+
const move = moveCursorVertically({
|
|
1684
|
+
cursorPos,
|
|
1685
|
+
inputValue: currentValue,
|
|
1686
|
+
width: getWrapWidth(),
|
|
1687
|
+
direction: "up",
|
|
1688
|
+
preferredCol,
|
|
1689
|
+
strWidth: (v) => input.strWidth(v),
|
|
1690
|
+
});
|
|
1691
|
+
preferredCol = move.preferredCol;
|
|
1692
|
+
if (move.moved) {
|
|
1693
|
+
setCursor(move.nextCursorPos);
|
|
1694
|
+
return false;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
if (inputHistory.length === 0) return false;
|
|
1369
1698
|
historyIndex = Math.max(0, historyIndex - 1);
|
|
1370
1699
|
setInputValue(inputHistory[historyIndex] || "");
|
|
1700
|
+
return false;
|
|
1371
1701
|
});
|
|
1372
1702
|
input.key(["down"], () => {
|
|
1373
1703
|
const currentValue = input.getValue();
|
|
1704
|
+
if (currentValue) {
|
|
1705
|
+
const move = moveCursorVertically({
|
|
1706
|
+
cursorPos,
|
|
1707
|
+
inputValue: currentValue,
|
|
1708
|
+
width: getWrapWidth(),
|
|
1709
|
+
direction: "down",
|
|
1710
|
+
preferredCol,
|
|
1711
|
+
strWidth: (v) => input.strWidth(v),
|
|
1712
|
+
});
|
|
1713
|
+
preferredCol = move.preferredCol;
|
|
1714
|
+
if (move.moved) {
|
|
1715
|
+
setCursor(move.nextCursorPos);
|
|
1716
|
+
return false;
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1374
1719
|
const historyTransition = resolveHistoryDownTransition({
|
|
1375
1720
|
inputHistory,
|
|
1376
1721
|
historyIndex,
|
|
@@ -1428,10 +1773,8 @@ function runUcodeTui({
|
|
|
1428
1773
|
}
|
|
1429
1774
|
const next = moveCursorHorizontally(cursorPos, currentValue, "left");
|
|
1430
1775
|
if (next !== cursorPos) {
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
input._updateCursor();
|
|
1434
|
-
screen.render();
|
|
1776
|
+
setCursor(next);
|
|
1777
|
+
resetPreferredCol();
|
|
1435
1778
|
}
|
|
1436
1779
|
return false;
|
|
1437
1780
|
});
|
|
@@ -1450,10 +1793,8 @@ function runUcodeTui({
|
|
|
1450
1793
|
}
|
|
1451
1794
|
const next = moveCursorHorizontally(cursorPos, currentValue, "right");
|
|
1452
1795
|
if (next !== cursorPos) {
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
input._updateCursor();
|
|
1456
|
-
screen.render();
|
|
1796
|
+
setCursor(next);
|
|
1797
|
+
resetPreferredCol();
|
|
1457
1798
|
}
|
|
1458
1799
|
return false;
|
|
1459
1800
|
});
|
|
@@ -1496,6 +1837,14 @@ function runUcodeTui({
|
|
|
1496
1837
|
}
|
|
1497
1838
|
screen.render();
|
|
1498
1839
|
});
|
|
1840
|
+
screen.key(["pageup"], () => {
|
|
1841
|
+
logBox.scroll(-Math.max(1, Math.floor((logBox.height || 10) / 2)));
|
|
1842
|
+
screen.render();
|
|
1843
|
+
});
|
|
1844
|
+
screen.key(["pagedown"], () => {
|
|
1845
|
+
logBox.scroll(Math.max(1, Math.floor((logBox.height || 10) / 2)));
|
|
1846
|
+
screen.render();
|
|
1847
|
+
});
|
|
1499
1848
|
input.key(["escape"], () => {
|
|
1500
1849
|
if (pendingTask && pendingTask.abortController && !pendingTask.abortController.signal.aborted) {
|
|
1501
1850
|
try {
|
|
@@ -1510,11 +1859,10 @@ function runUcodeTui({
|
|
|
1510
1859
|
});
|
|
1511
1860
|
return false;
|
|
1512
1861
|
}
|
|
1513
|
-
const previousTarget = targetAgent;
|
|
1514
1862
|
targetAgent = null;
|
|
1515
1863
|
selectedAgentIndex = -1;
|
|
1516
1864
|
agentSelectionMode = false;
|
|
1517
|
-
|
|
1865
|
+
setInputValue("");
|
|
1518
1866
|
setPrompt();
|
|
1519
1867
|
renderDashboard();
|
|
1520
1868
|
// Target selection cleared - removed redundant log
|
|
@@ -1573,6 +1921,13 @@ module.exports = {
|
|
|
1573
1921
|
cycleAgentSelectionIndex,
|
|
1574
1922
|
shouldClearAgentSelectionOnUp,
|
|
1575
1923
|
moveCursorHorizontally,
|
|
1924
|
+
clampCursorPos,
|
|
1925
|
+
findLogicalLineStart,
|
|
1926
|
+
findLogicalLineEnd,
|
|
1927
|
+
moveCursorToVisualLineBoundary,
|
|
1928
|
+
moveCursorVertically,
|
|
1929
|
+
deleteWordBeforeCursor,
|
|
1930
|
+
moveCursorByWord,
|
|
1576
1931
|
resolveHistoryDownTransition,
|
|
1577
1932
|
filterSelectableAgents,
|
|
1578
1933
|
stripLeakedEscapeTags,
|