u-foo 2.3.12 → 2.3.14

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.
@@ -1,7 +1,5 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
- const { runCliAgent } = require("./cliRunner");
4
- const { normalizeCliOutput } = require("./normalizeOutput");
5
3
  const { buildStatus } = require("../daemon/status");
6
4
  const { getUfooPaths } = require("../ufoo/paths");
7
5
  const { normalizeGateRouterResult } = require("../controller/gateRouter");
@@ -633,7 +631,6 @@ async function runUfooAgent({
633
631
  loopRuntime = null,
634
632
  controllerMode = null,
635
633
  }) {
636
- const state = loadSessionState(projectRoot);
637
634
  const mode = String(routingMode || (routingContext && routingContext.mode) || "").trim().toLowerCase();
638
635
  const resolvedControllerMode = String(
639
636
  controllerMode
@@ -660,7 +657,6 @@ async function runUfooAgent({
660
657
  let res;
661
658
 
662
659
  const useDirectProvider = shouldUseDirectProvider(provider);
663
- let usedDirectProvider = false;
664
660
 
665
661
  if (useDirectProvider) {
666
662
  res = await runNativeRouterCall({
@@ -671,47 +667,22 @@ async function runUfooAgent({
671
667
  model,
672
668
  });
673
669
  if (!res.ok) {
670
+ // eslint-disable-next-line no-console
671
+ console.error(`[ufoo-agent] native provider failed: ${res.error || "unknown error"}`);
674
672
  return { ok: false, error: res.error };
675
673
  } else {
676
- usedDirectProvider = true;
677
674
  res = { ok: true, output: res.output, sessionId: "", provider: res.provider, model: res.model };
678
675
  }
679
676
  }
680
677
 
681
678
  if (!useDirectProvider) {
682
- res = await runCliAgent({
683
- provider,
684
- model,
685
- prompt: fullPrompt,
686
- systemPrompt,
687
- sessionId: state.data?.sessionId,
688
- disableSession: provider === "claude-cli",
689
- cwd: projectRoot,
690
- });
691
-
692
- if (!res.ok) {
693
- const msg = (res.error || "").toLowerCase();
694
- if (msg.includes("session id") || msg.includes("session-id") || msg.includes("already in use")) {
695
- res = await runCliAgent({
696
- provider,
697
- model,
698
- prompt: fullPrompt,
699
- systemPrompt,
700
- sessionId: undefined,
701
- disableSession: provider === "claude-cli",
702
- cwd: projectRoot,
703
- });
704
- }
705
- }
706
-
707
- if (!res.ok) {
708
- return { ok: false, error: res.error };
709
- }
679
+ const error = `unsupported ufoo-agent provider "${provider || ""}"; cliRunner fallback has been removed`;
680
+ // eslint-disable-next-line no-console
681
+ console.error(`[ufoo-agent] ${error}`);
682
+ return { ok: false, error };
710
683
  }
711
684
 
712
- const rawText = usedDirectProvider
713
- ? String(res.output || "").trim()
714
- : normalizeCliOutput(res.output);
685
+ const rawText = String(res.output || "").trim();
715
686
  const text = stripMarkdownFence(rawText);
716
687
  let payload = null;
717
688
  try {
@@ -442,7 +442,9 @@ function createDaemonMessageRouter(options = {}) {
442
442
  }
443
443
 
444
444
  function handleErrorMessage(msg) {
445
- resolveStatusLine(`{gray-fg}✗{/gray-fg} Error: ${msg.error}`);
445
+ const error = String(msg.error || "unknown error");
446
+ resolveStatusLine(`{gray-fg}✗{/gray-fg} Error: ${error}`);
447
+ logMessage("error", `{white-fg}✗{/white-fg} ${escapeBlessed(error)}`);
446
448
  renderScreen();
447
449
  return false;
448
450
  }
@@ -1,4 +1,4 @@
1
- const DEFAULT_MODE_OPTIONS = ["auto", "host", "terminal", "tmux", "internal"];
1
+ const DEFAULT_MODE_OPTIONS = ["auto", "host", "terminal", "tmux", "internal-pty", "internal"];
2
2
 
3
3
  function createDashboardKeyController(options = {}) {
4
4
  const {
@@ -1,6 +1,6 @@
1
1
  const { clampAgentWindowWithSelection } = require("./agentDirectory");
2
2
 
3
- const DEFAULT_MODE_OPTIONS = ["auto", "host", "terminal", "tmux", "internal"];
3
+ const DEFAULT_MODE_OPTIONS = ["auto", "host", "terminal", "tmux", "internal-pty", "internal"];
4
4
 
5
5
  function providerLabel(value) {
6
6
  if (value === "claude-cli") return "claude";
package/src/chat/index.js CHANGED
@@ -65,7 +65,7 @@ const {
65
65
  pruneTransientAgentStates,
66
66
  } = require("./transientAgentState");
67
67
 
68
- const MODE_OPTIONS = ["auto", "host", "terminal", "tmux", "internal"];
68
+ const MODE_OPTIONS = ["auto", "host", "terminal", "tmux", "internal-pty", "internal"];
69
69
 
70
70
  async function runChat(projectRoot, options = {}) {
71
71
  const globalMode = options && options.globalMode === true;
@@ -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