u-foo 2.3.30 → 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.
Files changed (38) hide show
  1. package/package.json +5 -1
  2. package/scripts/chat-app-smoke.js +30 -0
  3. package/scripts/ink-demo.js +23 -0
  4. package/scripts/ink-smoke.js +30 -0
  5. package/scripts/ucode-app-smoke.js +36 -0
  6. package/src/chat/commandExecutor.js +6 -2
  7. package/src/chat/daemonMessageRouter.js +9 -1
  8. package/src/chat/daemonTransport.js +2 -1
  9. package/src/chat/dashboardKeyController.js +0 -40
  10. package/src/chat/dashboardView.js +0 -20
  11. package/src/chat/index.js +9 -1
  12. package/src/chat/inputSubmitHandler.js +34 -0
  13. package/src/chat/projectCloseController.js +1 -1
  14. package/src/chat/shellCommand.js +42 -0
  15. package/src/chat/transport.js +16 -3
  16. package/src/cli.js +4 -3
  17. package/src/code/agent.js +4 -0
  18. package/src/code/nativeRunner.js +74 -0
  19. package/src/code/taskDecomposer.js +5 -4
  20. package/src/code/tui.js +73 -561
  21. package/src/daemon/index.js +169 -27
  22. package/src/daemon/ipcServer.js +23 -1
  23. package/src/daemon/promptRequest.js +6 -1
  24. package/src/daemon/run.js +11 -4
  25. package/src/projects/runtimes.js +1 -1
  26. package/src/ufoo/agentRegistryDiagnostics.js +43 -0
  27. package/src/ui/MIGRATION.md +382 -0
  28. package/src/ui/components/ChatApp.js +2950 -0
  29. package/src/ui/components/DashboardBar.js +417 -0
  30. package/src/ui/components/InkDemo.js +96 -0
  31. package/src/ui/components/MultilineInput.js +387 -0
  32. package/src/ui/components/UcodeApp.js +813 -0
  33. package/src/ui/components/agentMirror.js +725 -0
  34. package/src/ui/components/chatReducer.js +337 -0
  35. package/src/ui/format/index.js +997 -0
  36. package/src/ui/index.js +9 -0
  37. package/src/ui/runInk.js +57 -0
  38. package/src/utils/nodeExecutable.js +26 -0
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 };
117
+ function runUcodeTui(props = {}) {
118
+ if (String(process.env.UFOO_TUI || "").trim().toLowerCase() === "blessed") {
119
+ return runUcodeBlessedTui(props);
337
120
  }
338
- const enter = selectedAgentIndex >= 0 && selectedAgentIndex < total ? selectedAgentIndex : 0;
339
- return { action: "enter", index: enter };
121
+ const { runUcodeInkTui } = require("../ui/components/UcodeApp");
122
+ return runUcodeInkTui(props);
340
123
  }
341
124
 
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;
539
- }
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
- });
630
- }
631
-
632
- function runUcodeTui({
125
+ function runUcodeBlessedTui({
633
126
  stdin = process.stdin,
634
127
  stdout = process.stdout,
635
128
  runSingleCommand = () => ({ kind: "empty" }),
@@ -1615,29 +1108,44 @@ function runUcodeTui({
1615
1108
  }
1616
1109
 
1617
1110
  if (result.kind === "nl") {
1618
- const statusMessages = [
1619
- "Thinking...",
1620
- "Processing your request...",
1621
- "Analyzing...",
1622
- "Working on it...",
1623
- ];
1624
- const randomStatus = statusMessages[Math.floor(Math.random() * statusMessages.length)];
1625
1111
  const abortController = new AbortController();
1626
1112
  const escapeStripper = createEscapeTagStripper();
1627
1113
  pendingTask = {
1628
1114
  abortController,
1629
1115
  startedAt: Date.now(),
1630
1116
  };
1631
- updateStatus(randomStatus, "thinking", {
1632
- showTimer: true,
1633
- startedAt: pendingTask.startedAt,
1634
- });
1117
+ const TOOL_LABELS = {
1118
+ read: "Reading file",
1119
+ write: "Writing file",
1120
+ edit: "Editing file",
1121
+ bash: "Running command",
1122
+ };
1123
+ const setNlStatus = (msg) => {
1124
+ updateStatus(msg, "thinking", {
1125
+ showTimer: true,
1126
+ startedAt: pendingTask.startedAt,
1127
+ });
1128
+ };
1129
+ setNlStatus("Waiting for model...");
1635
1130
  let streamState = null;
1636
1131
  let renderedToolLogCount = 0;
1637
1132
  let nlResult = null;
1638
1133
  try {
1639
1134
  nlResult = await runNaturalLanguageTask(result.task, state, {
1640
1135
  signal: abortController.signal,
1136
+ onPhase: (event) => {
1137
+ if (!event || typeof event !== "object") return;
1138
+ if (event.type === "request_start") {
1139
+ setNlStatus("Waiting for model...");
1140
+ } else if (event.type === "thinking_delta") {
1141
+ setNlStatus("Thinking...");
1142
+ } else if (event.type === "text_delta") {
1143
+ setNlStatus("Generating response...");
1144
+ } else if (event.type === "tool_request") {
1145
+ const label = TOOL_LABELS[String(event.name || "").toLowerCase()] || `Calling ${event.name}`;
1146
+ setNlStatus(`${label}...`);
1147
+ }
1148
+ },
1641
1149
  onDelta: (delta) => {
1642
1150
  const text = escapeStripper.write(String(delta || ""));
1643
1151
  if (!text) return;
@@ -1648,6 +1156,10 @@ function runUcodeTui({
1648
1156
  },
1649
1157
  onToolLog: (entry) => {
1650
1158
  renderedToolLogCount += 1;
1159
+ if (entry && entry.tool && entry.phase === "start") {
1160
+ const label = TOOL_LABELS[String(entry.tool || "").toLowerCase()] || `Calling ${entry.tool}`;
1161
+ setNlStatus(`${label}...`);
1162
+ }
1651
1163
  logToolHint(entry);
1652
1164
  },
1653
1165
  });