u-foo 2.3.31 → 2.3.32

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/src/code/tui.js CHANGED
@@ -1,57 +1,40 @@
1
- const chalk = require("chalk");
2
- const pkg = require("../../package.json");
3
-
4
- const UCODE_BANNER_LINES = [
5
- "█ █ █▀▀ █▀█ █▀▄ █▀▀",
6
- "█ █ █ █ █ █ █ █▀ ",
7
- "▀▀▀ ▀▀▀ ▀▀▀ ▀▀ ▀▀▀",
8
- ];
9
-
10
- const UCODE_VERSION = String((pkg && pkg.version) || "dev");
11
-
12
- // Status indicators
13
- const STATUS_INDICATORS = {
14
- thinking: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
15
- typing: ["◐", "◓", "◑", "◒"],
16
- waiting: ["∙", "∙∙", "∙∙∙", "∙∙", "∙"],
17
- };
18
-
19
- const ANSI_PATTERN = /\x1B\[[0-9;?]*[ -/]*[@-~]/g;
20
-
21
- function charDisplayWidth(char = "") {
22
- if (!char) return 0;
23
- const code = char.codePointAt(0) || 0;
24
- if (code === 0) return 0;
25
- if (code < 32 || (code >= 0x7f && code < 0xa0)) return 0;
26
- if ((code >= 0x0300 && code <= 0x036f) ||
27
- (code >= 0x1ab0 && code <= 0x1aff) ||
28
- (code >= 0x1dc0 && code <= 0x1dff) ||
29
- (code >= 0x20d0 && code <= 0x20ff) ||
30
- (code >= 0xfe20 && code <= 0xfe2f)) {
31
- return 0;
32
- }
33
- if ((code >= 0x1100 && code <= 0x115f) ||
34
- code === 0x2329 ||
35
- code === 0x232a ||
36
- (code >= 0x2e80 && code <= 0xa4cf) ||
37
- (code >= 0xac00 && code <= 0xd7a3) ||
38
- (code >= 0xf900 && code <= 0xfaff) ||
39
- (code >= 0xfe10 && code <= 0xfe19) ||
40
- (code >= 0xfe30 && code <= 0xfe6f) ||
41
- (code >= 0xff00 && code <= 0xff60) ||
42
- (code >= 0xffe0 && code <= 0xffe6) ||
43
- (code >= 0x1f300 && code <= 0x1faff)) {
44
- return 2;
45
- }
46
- return 1;
47
- }
1
+ const fmt = require("../ui/format");
48
2
 
49
- function displayCellWidth(text = "") {
50
- return Array.from(String(text || "").replace(ANSI_PATTERN, "")).reduce(
51
- (sum, char) => sum + charDisplayWidth(char),
52
- 0
53
- );
54
- }
3
+ const {
4
+ STATUS_INDICATORS,
5
+ StreamBuffer,
6
+ UCODE_BANNER_LINES,
7
+ UCODE_VERSION,
8
+ buildMergedToolExpandedLines,
9
+ buildMergedToolSummaryText,
10
+ buildUcodeBannerLines,
11
+ clampCursorPos,
12
+ createEscapeTagStripper,
13
+ cycleAgentSelectionIndex,
14
+ deleteWordBeforeCursor,
15
+ displayCellWidth,
16
+ filterSelectableAgents,
17
+ findLogicalLineEnd,
18
+ findLogicalLineStart,
19
+ formatHighlightedUserInput,
20
+ formatPendingElapsed,
21
+ loadActiveAgents,
22
+ moveCursorByWord,
23
+ moveCursorHorizontally,
24
+ moveCursorToVisualLineBoundary,
25
+ moveCursorVertically,
26
+ normalizeBashToolCommand,
27
+ normalizeModelLabel,
28
+ normalizeToolMergeEntry,
29
+ parseActiveAgentsFromBusStatus,
30
+ renderLogLinesWithMarkdown,
31
+ resolveAgentSelectionOnDown,
32
+ resolveHistoryDownTransition,
33
+ shouldClearAgentSelectionOnUp,
34
+ shouldEnterAgentSelection,
35
+ shouldUseUcodeTui,
36
+ stripLeakedEscapeTags,
37
+ } = fmt;
55
38
 
56
39
  function safeRead(getter, fallback = undefined) {
57
40
  try {
@@ -75,109 +58,6 @@ function resolveLogContentWidth({ logBox = null, screen = null, fallback = 80 }
75
58
  return Math.max(1, fallback);
76
59
  }
77
60
 
78
- function formatHighlightedUserInput(text = "", {
79
- width = 80,
80
- escapeText = (value) => String(value || ""),
81
- } = {}) {
82
- const plain = String(text || "").trim();
83
- if (!plain) return "";
84
- const targetWidth = Math.max(1, Math.floor(Number(width) || 80) - 1);
85
- const prefix = " → ";
86
- const suffix = " ";
87
- const contentWidth = displayCellWidth(`${prefix}${plain}${suffix}`);
88
- const pad = " ".repeat(Math.max(0, targetWidth - contentWidth));
89
- return `{cyan-bg}{white-fg}${prefix}${escapeText(plain)}${suffix}${pad}{/white-fg}{/cyan-bg}`;
90
- }
91
-
92
- // Stream buffer for smooth output
93
- class StreamBuffer {
94
- constructor(writer, options = {}) {
95
- this.writer = writer;
96
- this.buffer = "";
97
- this.delay = options.delay || 8; // ms between chunks
98
- this.chunkSize = options.chunkSize || 3; // chars per chunk
99
- this.isStreaming = false;
100
- this.streamPromise = null;
101
- }
102
-
103
- async write(text) {
104
- this.buffer += text;
105
- if (!this.isStreaming) {
106
- this.isStreaming = true;
107
- this.streamPromise = this.flush();
108
- }
109
- return this.streamPromise;
110
- }
111
-
112
- async flush() {
113
- while (this.buffer.length > 0) {
114
- const chunk = this.buffer.slice(0, this.chunkSize);
115
- this.buffer = this.buffer.slice(this.chunkSize);
116
- this.writer(chunk);
117
- if (this.buffer.length > 0) {
118
- await new Promise(resolve => setTimeout(resolve, this.delay));
119
- }
120
- }
121
- this.isStreaming = false;
122
- }
123
-
124
- async finish() {
125
- if (this.isStreaming) {
126
- await this.streamPromise;
127
- }
128
- if (this.buffer.length > 0) {
129
- this.writer(this.buffer);
130
- this.buffer = "";
131
- }
132
- }
133
- }
134
-
135
- function normalizeModelLabel(model = "") {
136
- const text = String(model || "").trim();
137
- if (text) return text;
138
- return "default";
139
- }
140
-
141
- function buildUcodeBannerLines({ model = "", engine = "ufoo-core", nickname = "", agentId = "", workspaceRoot = "", sessionId = "", width = 0 } = {}) {
142
- const modelLabel = normalizeModelLabel(model);
143
- void width;
144
- void engine; // Not using engine anymore
145
- void nickname;
146
- void agentId;
147
-
148
- // Get current working directory with ~ for home
149
- const path = require("path");
150
- const os = require("os");
151
- const currentDir = workspaceRoot || process.cwd();
152
- const homeDir = os.homedir();
153
-
154
- // Replace home directory with ~
155
- let shortPath = currentDir;
156
- if (currentDir.startsWith(homeDir)) {
157
- shortPath = currentDir.replace(homeDir, "~");
158
- }
159
-
160
- const logoLines = UCODE_BANNER_LINES.map((line) => chalk.cyan(line));
161
- const infoLines = [];
162
- infoLines.push(`${chalk.dim("Version:")} ${chalk.cyan.bold(UCODE_VERSION)}`);
163
- infoLines.push(`${chalk.dim("Model:")} ${chalk.yellow(modelLabel)}`);
164
- infoLines.push(`${chalk.dim("Dictionary:")} ${chalk.gray(shortPath)}`);
165
- const normalizedSessionId = String(sessionId || "").trim();
166
- if (normalizedSessionId) {
167
- infoLines.push(`${chalk.dim("Session:")} ${chalk.gray(normalizedSessionId)}`);
168
- }
169
- const logoPadding = " ".repeat(
170
- UCODE_BANNER_LINES.reduce((max, line) => Math.max(max, String(line || "").length), 0)
171
- );
172
- const rows = Math.max(logoLines.length, infoLines.length);
173
-
174
- return Array.from({ length: rows }, (_, index) => {
175
- const logoLine = logoLines[index] || logoPadding;
176
- const info = infoLines[index] || "";
177
- return ` ${logoLine} ${info}`;
178
- });
179
- }
180
-
181
61
  function escapeBlessedLiteral(text) {
182
62
  const raw = String(text == null ? "" : text);
183
63
  const safe = raw.replace(/\{\/escape\}/g, "{open}/escape{close}");
@@ -195,7 +75,7 @@ function buildUcodeBannerBlessedLines({
195
75
  } = {}) {
196
76
  const modelLabel = normalizeModelLabel(model);
197
77
  void width;
198
- void engine; // Not using engine anymore
78
+ void engine;
199
79
  void nickname;
200
80
  void agentId;
201
81
 
@@ -234,402 +114,15 @@ function buildUcodeBannerBlessedLines({
234
114
  });
235
115
  }
236
116
 
237
- function shouldUseUcodeTui({ stdin, stdout, jsonOutput, forceTui = false, disableTui = false } = {}) {
238
- if (disableTui) return false;
239
- if (jsonOutput) return false;
240
- if (forceTui) return true;
241
- return Boolean(stdin && stdin.isTTY && stdout && stdout.isTTY);
242
- }
243
-
244
- // Helper function to load agents from bus
245
- function parseActiveAgentsFromBusStatus(busStatus = "") {
246
- const lines = String(busStatus || "").replace(ANSI_PATTERN, "").split(/\r?\n/);
247
- const agents = [];
248
- let inOnlineSection = false;
249
-
250
- for (const line of lines) {
251
- const trimmed = String(line || "").trim();
252
- if (!trimmed) continue;
253
-
254
- if (/^Online agents:\s*$/i.test(trimmed)) {
255
- inOnlineSection = true;
256
- continue;
257
- }
258
- if (!inOnlineSection) continue;
259
-
260
- if (/^\(none\)$/i.test(trimmed)) {
261
- continue;
262
- }
263
-
264
- // Next heading means we have left the online agents section
265
- if (/^[A-Za-z][A-Za-z ]+:\s*$/.test(trimmed)) {
266
- break;
267
- }
268
-
269
- const rawId = trimmed.replace(/\s+\([^)]+\)\s*$/, "");
270
- if (!rawId) continue;
271
- const [type, ...idParts] = rawId.split(":");
272
- const id = idParts.join(":");
273
- if (!type) continue;
274
-
275
- agents.push({
276
- type,
277
- id,
278
- status: "active",
279
- fullId: rawId,
280
- nickname: (trimmed.match(/\(([^)]+)\)\s*$/) || [])[1] || "",
281
- });
282
- }
283
-
284
- // Fallback for legacy output: "type:id (active|idle)"
285
- if (agents.length === 0) {
286
- for (const line of lines) {
287
- const trimmed = String(line || "").trim();
288
- const match = trimmed.match(/^([a-z-]+):([a-f0-9]+)\s+\((active|idle)\)$/);
289
- if (!match) continue;
290
- agents.push({
291
- type: match[1],
292
- id: match[2],
293
- status: match[3],
294
- fullId: `${match[1]}:${match[2]}`,
295
- nickname: "",
296
- });
297
- }
298
- }
299
-
300
- return agents;
301
- }
302
-
303
- function loadActiveAgents(workspaceRoot) {
304
- try {
305
- const { execSync } = require("child_process");
306
- const busStatus = execSync("ufoo bus status", {
307
- cwd: workspaceRoot,
308
- encoding: "utf8",
309
- });
310
- return parseActiveAgentsFromBusStatus(busStatus);
311
- } catch {
312
- return [];
313
- }
314
- }
315
-
316
- function renderLogLinesWithMarkdown(text = "", state = {}, escapeFn = (value) => String(value || "")) {
317
- const { renderMarkdownLines } = require("../shared/markdownRenderer");
318
- return renderMarkdownLines(text, state, escapeFn);
319
- }
320
-
321
- function shouldEnterAgentSelection(inputValue = "") {
322
- const text = String(inputValue || "");
323
- const trimmed = text.trim();
324
- return !trimmed;
325
- }
326
-
327
- function resolveAgentSelectionOnDown({
328
- agentSelectionMode = false,
329
- selectedAgentIndex = -1,
330
- totalAgents = 0,
331
- } = {}) {
332
- const total = Number.isFinite(totalAgents) ? Math.max(0, Math.floor(totalAgents)) : 0;
333
- if (total <= 0) return { action: "none", index: -1 };
334
- if (agentSelectionMode) {
335
- const keep = selectedAgentIndex >= 0 && selectedAgentIndex < total ? selectedAgentIndex : 0;
336
- return { action: "hold", index: keep };
337
- }
338
- const enter = selectedAgentIndex >= 0 && selectedAgentIndex < total ? selectedAgentIndex : 0;
339
- return { action: "enter", index: enter };
340
- }
341
-
342
- function cycleAgentSelectionIndex(selectedAgentIndex = -1, totalAgents = 0, direction = "right") {
343
- const total = Number.isFinite(totalAgents) ? Math.max(0, Math.floor(totalAgents)) : 0;
344
- if (total <= 0) return -1;
345
- const current = selectedAgentIndex >= 0 && selectedAgentIndex < total ? selectedAgentIndex : 0;
346
- if (direction === "left") {
347
- return (current - 1 + total) % total;
348
- }
349
- return (current + 1) % total;
350
- }
351
-
352
- function shouldClearAgentSelectionOnUp({
353
- agentSelectionMode = false,
354
- inputValue = "",
355
- } = {}) {
356
- return Boolean(agentSelectionMode && shouldEnterAgentSelection(inputValue));
357
- }
358
-
359
- function moveCursorHorizontally(cursorPos = 0, inputValue = "", direction = "right") {
360
- const text = String(inputValue || "");
361
- const max = text.length;
362
- const pos = Number.isFinite(cursorPos) ? Math.max(0, Math.floor(cursorPos)) : 0;
363
- if (direction === "left") return Math.max(0, pos - 1);
364
- return Math.min(max, pos + 1);
365
- }
366
-
367
- function clampCursorPos(cursorPos = 0, inputValue = "") {
368
- const text = String(inputValue || "");
369
- const pos = Number.isFinite(cursorPos) ? Math.floor(cursorPos) : 0;
370
- return Math.max(0, Math.min(text.length, pos));
371
- }
372
-
373
- function findLogicalLineStart(inputValue = "", cursorPos = 0) {
374
- const text = String(inputValue || "");
375
- const pos = clampCursorPos(cursorPos, text);
376
- const prevNewline = text.lastIndexOf("\n", Math.max(0, pos - 1));
377
- return prevNewline === -1 ? 0 : prevNewline + 1;
378
- }
379
-
380
- function findLogicalLineEnd(inputValue = "", cursorPos = 0) {
381
- const text = String(inputValue || "");
382
- const pos = clampCursorPos(cursorPos, text);
383
- const nextNewline = text.indexOf("\n", pos);
384
- return nextNewline === -1 ? text.length : nextNewline;
385
- }
386
-
387
- function moveCursorToVisualLineBoundary({
388
- cursorPos = 0,
389
- inputValue = "",
390
- width = 80,
391
- boundary = "start",
392
- strWidth,
393
- } = {}) {
394
- const inputMath = require("../chat/inputMath");
395
- const text = String(inputValue || "");
396
- const normalizedWidth = Number.isFinite(width) ? Math.max(1, Math.floor(width)) : 1;
397
- const pos = clampCursorPos(cursorPos, text);
398
- const { row } = inputMath.getCursorRowCol(text, pos, normalizedWidth, strWidth);
399
- if (boundary === "end") {
400
- return inputMath.getCursorPosForRowCol(text, row, normalizedWidth, normalizedWidth, strWidth);
401
- }
402
- return inputMath.getCursorPosForRowCol(text, row, 0, normalizedWidth, strWidth);
403
- }
404
-
405
- function moveCursorVertically({
406
- cursorPos = 0,
407
- inputValue = "",
408
- width = 80,
409
- direction = "down",
410
- preferredCol = null,
411
- strWidth,
412
- } = {}) {
413
- const inputMath = require("../chat/inputMath");
414
- const text = String(inputValue || "");
415
- const normalizedWidth = Number.isFinite(width) ? Math.max(1, Math.floor(width)) : 1;
416
- const pos = clampCursorPos(cursorPos, text);
417
- const { row, col } = inputMath.getCursorRowCol(text, pos, normalizedWidth, strWidth);
418
- const totalRows = inputMath.countLines(text, normalizedWidth, strWidth);
419
- const targetCol = Number.isFinite(preferredCol) ? preferredCol : col;
420
-
421
- if (direction === "up") {
422
- if (row <= 0) {
423
- return { moved: false, nextCursorPos: pos, preferredCol: targetCol, boundary: "top" };
424
- }
425
- return {
426
- moved: true,
427
- nextCursorPos: inputMath.getCursorPosForRowCol(text, row - 1, targetCol, normalizedWidth, strWidth),
428
- preferredCol: targetCol,
429
- boundary: "",
430
- };
431
- }
432
-
433
- if (row >= totalRows - 1) {
434
- return { moved: false, nextCursorPos: pos, preferredCol: targetCol, boundary: "bottom" };
435
- }
436
- return {
437
- moved: true,
438
- nextCursorPos: inputMath.getCursorPosForRowCol(text, row + 1, targetCol, normalizedWidth, strWidth),
439
- preferredCol: targetCol,
440
- boundary: "",
441
- };
442
- }
443
-
444
- function deleteWordBeforeCursor(inputValue = "", cursorPos = 0) {
445
- const text = String(inputValue || "");
446
- const pos = clampCursorPos(cursorPos, text);
447
- if (pos <= 0) return { value: text, cursorPos: pos };
448
- const before = text.slice(0, pos);
449
- const after = text.slice(pos);
450
- const match = before.match(/\s*\S+\s*$/);
451
- const start = match ? pos - match[0].length : Math.max(0, pos - 1);
452
- return {
453
- value: before.slice(0, start) + after,
454
- cursorPos: start,
455
- };
456
- }
457
-
458
- function moveCursorByWord(inputValue = "", cursorPos = 0, direction = "forward") {
459
- const text = String(inputValue || "");
460
- const pos = clampCursorPos(cursorPos, text);
461
- if (direction === "backward") {
462
- const before = text.slice(0, pos);
463
- const trimmedEnd = before.search(/\S\s*$/) >= 0 ? before.replace(/\s+$/, "") : before;
464
- const match = trimmedEnd.match(/\S+$/);
465
- return match ? trimmedEnd.length - match[0].length : 0;
466
- }
467
- const after = text.slice(pos);
468
- const match = after.match(/^\s*\S+/);
469
- return match ? Math.min(text.length, pos + match[0].length) : text.length;
470
- }
471
-
472
- function resolveHistoryDownTransition({
473
- inputHistory = [],
474
- historyIndex = 0,
475
- currentValue = "",
476
- } = {}) {
477
- const history = Array.isArray(inputHistory) ? inputHistory : [];
478
- if (history.length <= 0) {
479
- return {
480
- moved: false,
481
- nextHistoryIndex: Number.isFinite(historyIndex) ? Math.max(0, Math.floor(historyIndex)) : 0,
482
- nextValue: String(currentValue || ""),
483
- };
484
- }
485
- const currentIndex = Number.isFinite(historyIndex) ? Math.max(0, Math.floor(historyIndex)) : 0;
486
- const nextHistoryIndex = Math.min(history.length, currentIndex + 1);
487
- const nextValue = nextHistoryIndex >= history.length ? "" : String(history[nextHistoryIndex] || "");
488
- const moved = nextHistoryIndex !== currentIndex || nextValue !== String(currentValue || "");
489
- return {
490
- moved,
491
- nextHistoryIndex,
492
- nextValue,
493
- };
494
- }
495
-
496
- function filterSelectableAgents(agents = [], selfSubscriberId = "") {
497
- const selfId = String(selfSubscriberId || "").trim();
498
- const list = Array.isArray(agents) ? agents : [];
499
- if (!selfId) {
500
- return list.filter((agent) => {
501
- const fullId = String(agent && agent.fullId ? agent.fullId : "").trim();
502
- const type = String(agent && agent.type ? agent.type : "").trim();
503
- if (fullId === "ufoo-agent") return false;
504
- if (type === "ufoo-agent") return false;
505
- return true;
506
- });
507
- }
508
- return list.filter((agent) => {
509
- const fullId = String(agent && agent.fullId ? agent.fullId : "").trim();
510
- const type = String(agent && agent.type ? agent.type : "").trim();
511
- if (!fullId) return true;
512
- if (fullId === "ufoo-agent") return false;
513
- if (type === "ufoo-agent") return false;
514
- return fullId !== selfId;
515
- });
516
- }
517
-
518
- function stripLeakedEscapeTags(text = "") {
519
- const source = String(text == null ? "" : text);
520
- const withoutClosedTags = source.replace(/\{[^{}\n]*escape[^{}\n]*\}/gi, "");
521
- const withoutDanglingEscape = withoutClosedTags.replace(/\{\s*\/?\s*escape[\s\S]*$/gi, "");
522
- return withoutDanglingEscape.replace(/\{\s*\/?\s*e?s?c?a?p?e?[^{}\n]*$/gi, "");
523
- }
524
-
525
- function findTrailingEscapeTagPrefix(text = "") {
526
- const raw = String(text == null ? "" : text);
527
- if (!raw) return "";
528
- const windowSize = 40;
529
- const tail = raw.slice(Math.max(0, raw.length - windowSize));
530
- const braceIndex = tail.lastIndexOf("{");
531
- if (braceIndex < 0) return "";
532
- const suffix = tail.slice(braceIndex);
533
- if (suffix.includes("}")) return "";
534
-
535
- const compact = suffix.toLowerCase().replace(/\s+/g, "");
536
- if (!compact.startsWith("{")) return "";
537
- if (/^\{\/?e?s?c?a?p?e?[^}]*$/.test(compact)) {
538
- return suffix;
117
+ function runUcodeTui(props = {}) {
118
+ if (String(process.env.UFOO_TUI || "").trim().toLowerCase() === "blessed") {
119
+ return runUcodeBlessedTui(props);
539
120
  }
540
- return "";
541
- }
542
-
543
- function createEscapeTagStripper() {
544
- let carry = "";
545
-
546
- return {
547
- write(chunk = "") {
548
- const incoming = String(chunk == null ? "" : chunk);
549
- if (!incoming && !carry) return "";
550
- const combined = `${carry}${incoming}`;
551
- const trailing = findTrailingEscapeTagPrefix(combined);
552
- const safeText = trailing
553
- ? combined.slice(0, combined.length - trailing.length)
554
- : combined;
555
- carry = trailing;
556
- return stripLeakedEscapeTags(safeText);
557
- },
558
- flush() {
559
- if (!carry) return "";
560
- // carry only stores trailing prefixes of escape tags; do not emit it
561
- // to avoid leaking partial markers like "{/escape" at stream end.
562
- const rest = "";
563
- carry = "";
564
- return rest;
565
- },
566
- };
567
- }
568
-
569
- function formatPendingElapsed(ms = 0) {
570
- const totalSeconds = Math.max(0, Math.floor(Number(ms) / 1000));
571
- return `${totalSeconds} s`;
572
- }
573
-
574
- function normalizeBashToolCommand(args = {}, payload = {}) {
575
- const argObj = args && typeof args === "object" ? args : {};
576
- const resObj = payload && typeof payload === "object" ? payload : {};
577
- const command = String(argObj.command || argObj.cmd || "").trim();
578
- const code = Number.isFinite(resObj.code) ? `exit ${resObj.code}` : "";
579
- return [command, code].filter(Boolean).join(" · ");
580
- }
581
-
582
- function normalizeToolMergeEntry(entry = {}) {
583
- const source = entry && typeof entry === "object" ? entry : {};
584
- const tool = String(source.tool || "").trim().toLowerCase() || "tool";
585
- const detail = String(source.detail || "").trim();
586
- const isError = Boolean(source.isError);
587
- const errorText = String(source.errorText || "").trim();
588
- const summary = [tool, detail].filter(Boolean).join(" · ") || tool;
589
- return {
590
- tool,
591
- detail,
592
- isError,
593
- errorText,
594
- summary,
595
- };
596
- }
597
-
598
- function buildMergedToolSummaryText(entries = []) {
599
- const list = Array.isArray(entries)
600
- ? entries.map((item) => normalizeToolMergeEntry(item))
601
- : [];
602
- const count = list.length;
603
- if (count <= 0) return "Ran tool";
604
- const first = list[0];
605
- if (count === 1) return `Ran ${first.summary}`;
606
- const errorCount = list.filter((item) => item.isError).length;
607
- const errorSuffix = errorCount > 0 ? ` · ${errorCount} error${errorCount === 1 ? "" : "s"}` : "";
608
- return `Ran ${first.summary} · … +${count - 1} calls${errorSuffix}`;
609
- }
610
-
611
- function buildMergedToolExpandedLines(entries = []) {
612
- const list = Array.isArray(entries)
613
- ? entries.map((item) => normalizeToolMergeEntry(item))
614
- : [];
615
- const maxLength = 120; // Max length for expanded lines
616
- return list.map((item, index) => {
617
- const base = item.summary;
618
- let line;
619
- if (!item.isError) {
620
- line = base;
621
- } else {
622
- line = item.errorText ? `${base} · error: ${item.errorText}` : `${base} · error`;
623
- }
624
- // Truncate long lines
625
- if (line.length > maxLength) {
626
- return line.slice(0, maxLength - 3) + "...";
627
- }
628
- return line;
629
- });
121
+ const { runUcodeInkTui } = require("../ui/components/UcodeApp");
122
+ return runUcodeInkTui(props);
630
123
  }
631
124
 
632
- function runUcodeTui({
125
+ function runUcodeBlessedTui({
633
126
  stdin = process.stdin,
634
127
  stdout = process.stdout,
635
128
  runSingleCommand = () => ({ kind: "empty" }),