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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "2.3.13",
3
+ "version": "2.3.15",
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",
@@ -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
- const label = getAgentLabel(agentId);
131
- processStdout.write(`ufoo internal · ${label}\r\n\r\n> `);
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
- renderAgentDashboard();
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
- if (key && key.shift) {
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
- setCursorPos(0);
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
- setCursorPos((textarea && textarea.value ? textarea.value.length : 0));
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
- textarea.value = textarea.value.slice(0, cursorPos - 1) + textarea.value.slice(cursorPos);
265
- setCursorPos(cursorPos - 1);
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
- completionController.hide();
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
- textarea.value = textarea.value.slice(0, cursorPos) + textarea.value.slice(cursorPos + 1);
287
- resetPreferredCol();
288
- resizeInput();
289
- ensureInputCursorVisible();
290
- updateCursor(textarea);
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
- resetPreferredCol();
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
 
@@ -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 (no border for cleaner look)
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: null,
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
- // Let enter/return/escape pass through to blessed key handlers
618
- if (keyName === "return" || keyName === "enter" || keyName === "escape") return;
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 (cursorPos > 0 && this.value) {
625
- this.value = this.value.slice(0, cursorPos - 1) + this.value.slice(cursorPos);
626
- cursorPos -= 1;
627
- ensureInputCursorVisible();
628
- this._updateCursor();
629
- this.screen.render();
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 (this.value && cursorPos < this.value.length) {
636
- this.value = this.value.slice(0, cursorPos) + this.value.slice(cursorPos + 1);
637
- ensureInputCursorVisible();
638
- this._updateCursor();
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
- cursorPos = 0;
646
- ensureInputCursorVisible();
647
- this._updateCursor();
648
- this.screen.render();
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
- cursorPos = (this.value || "").length;
654
- ensureInputCursorVisible();
655
- this._updateCursor();
656
- this.screen.render();
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
- this.value = (this.value || "").slice(0, cursorPos) + insertChar + (this.value || "").slice(cursorPos);
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 (inputHistory.length === 0) return;
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
- cursorPos = next;
1432
- ensureInputCursorVisible();
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
- cursorPos = next;
1454
- ensureInputCursorVisible();
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
- input.setValue("");
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,