u-foo 3.0.24 → 3.0.26

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.
@@ -154,17 +154,18 @@ function buildUcodeBannerLines({
154
154
  }
155
155
  shortPath = path.normalize(shortPath);
156
156
 
157
- const logoLines = UCODE_BANNER_LINES.map((line) => chalk.cyan(line));
157
+ // The logo is literal content. The renderer receives the completed banner
158
+ // row as a dedicated raw entry, so the logo never goes through log styling.
159
+ const logoLines = UCODE_BANNER_LINES;
158
160
  const infoLines = [];
159
- infoLines.push(`${chalk.dim("Version:")} ${chalk.cyan.bold(UCODE_VERSION)}`);
160
- infoLines.push(`${chalk.dim("Model:")} ${chalk.yellow(modelLabel)}`);
161
+ infoLines.push(`Model: ${modelLabel}`);
161
162
  if (planMode) {
162
- infoLines.push(`${chalk.dim("Mode:")} ${chalk.magenta.bold("PLAN")}`);
163
+ infoLines.push("Mode: PLAN");
163
164
  }
164
- infoLines.push(`${chalk.dim("Dictionary:")} ${chalk.gray(shortPath)}`);
165
+ infoLines.push(`Dictionary: ${shortPath}`);
165
166
  const normalizedSessionId = String(sessionId || "").trim();
166
167
  if (normalizedSessionId) {
167
- infoLines.push(`${chalk.dim("Session:")} ${chalk.gray(normalizedSessionId)}`);
168
+ infoLines.push(`Session: ${normalizedSessionId}`);
168
169
  }
169
170
  const logoPadding = " ".repeat(
170
171
  UCODE_BANNER_LINES.reduce((max, line) => Math.max(max, String(line || "").length), 0)
@@ -174,7 +175,7 @@ function buildUcodeBannerLines({
174
175
  return Array.from({ length: rows }, (_, index) => {
175
176
  const logoLine = logoLines[index] || logoPadding;
176
177
  const info = infoLines[index] || "";
177
- return ` ${logoLine} ${info}`;
178
+ return `${logoLine} ${info}`;
178
179
  });
179
180
  }
180
181
 
@@ -323,6 +324,66 @@ function toolMessagePreview(message = {}) {
323
324
  return raw;
324
325
  }
325
326
 
327
+ function parseToolCallArguments(call = {}) {
328
+ const raw = call && call.function && call.function.arguments != null
329
+ ? call.function.arguments
330
+ : (call && call.arguments != null ? call.arguments : call && call.input);
331
+ if (raw && typeof raw === "object") return raw;
332
+ if (typeof raw !== "string" || !raw.trim()) return {};
333
+ try {
334
+ const parsed = JSON.parse(raw);
335
+ return parsed && typeof parsed === "object" ? parsed : {};
336
+ } catch {
337
+ return {};
338
+ }
339
+ }
340
+
341
+ function formatToolDisplayName(tool = "") {
342
+ const words = String(tool || "tool")
343
+ .trim()
344
+ .split(/[_\-\s]+/)
345
+ .filter(Boolean);
346
+ return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ") || "Tool";
347
+ }
348
+
349
+ function formatUcodeToolLine(tool = "", args = {}, payload = {}) {
350
+ const name = String(tool || "tool").trim().toLowerCase() || "tool";
351
+ const detail = normalizeToolLogDetail(name, args, payload);
352
+ return `• ${formatToolDisplayName(name)}${detail ? ` ${detail}` : ""}`;
353
+ }
354
+
355
+ function collapseConsecutiveUcodeToolEntries(entries = []) {
356
+ const source = Array.isArray(entries) ? entries : [];
357
+ const collapsed = [];
358
+ for (let index = 0; index < source.length;) {
359
+ const entry = source[index];
360
+ if (!entry || entry.kind !== "tool") {
361
+ collapsed.push(entry);
362
+ index += 1;
363
+ continue;
364
+ }
365
+
366
+ let end = index + 1;
367
+ while (end < source.length && source[end] && source[end].kind === "tool") end += 1;
368
+ const group = source.slice(index, end);
369
+ if (group.length === 1) {
370
+ collapsed.push(entry);
371
+ } else {
372
+ const detail = group
373
+ .map((item) => String(item.text || "").replace(/^•\s*/, ""))
374
+ .join("\n");
375
+ collapsed.push({
376
+ ...entry,
377
+ text: `${String(entry.text || "• Tool")} · +${group.length - 1} calls (Ctrl+O expand)`,
378
+ detail,
379
+ expanded: false,
380
+ });
381
+ }
382
+ index = end;
383
+ }
384
+ return collapsed;
385
+ }
386
+
326
387
  /**
327
388
  * Collapse attached-image prompt prefixes / base64 blobs for TUI log display.
328
389
  */
@@ -355,9 +416,6 @@ function buildUcodeSessionLogEntries(messages = [], options = {}) {
355
416
  : { inCodeBlock: false };
356
417
  const idPrefix = String(options.idPrefix || "h");
357
418
  let seq = Number.isFinite(options.startSeq) ? Math.max(0, Math.floor(options.startSeq)) : 0;
358
- const maxToolPreviewLines = Number.isFinite(options.maxToolPreviewLines)
359
- ? Math.max(1, Math.floor(options.maxToolPreviewLines))
360
- : 4;
361
419
  const entries = [];
362
420
 
363
421
  const pushLines = (text, kind) => {
@@ -373,14 +431,12 @@ function buildUcodeSessionLogEntries(messages = [], options = {}) {
373
431
  } else {
374
432
  lines = source.split(/\r?\n/);
375
433
  }
376
- for (const line of lines) {
377
- entries.push({
378
- id: `${idPrefix}-${seq}`,
379
- text: String(line || ""),
380
- kind,
381
- });
382
- seq += 1;
383
- }
434
+ entries.push({
435
+ id: `${idPrefix}-${seq}`,
436
+ text: lines.map((line) => String(line || "")).join("\n"),
437
+ kind,
438
+ });
439
+ seq += 1;
384
440
  };
385
441
 
386
442
  for (const message of list) {
@@ -390,37 +446,35 @@ function buildUcodeSessionLogEntries(messages = [], options = {}) {
390
446
  const text = redactUserMessageForLog(messageContentText(message));
391
447
  if (!text.trim()) continue;
392
448
  const lines = text.split(/\r?\n/);
393
- lines.forEach((line, index) => {
394
- pushLines(index === 0 ? `› ${line}` : line, "user");
395
- });
449
+ pushLines(lines.map((line, index) => (index === 0 ? `› ${line}` : line)).join("\n"), "user");
396
450
  continue;
397
451
  }
398
452
  if (role === "assistant") {
399
453
  const text = messageContentText(message);
400
454
  if (text) pushLines(text, "assistant");
401
455
  const calls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
402
- if (calls.length > 0) {
403
- const names = calls
404
- .map((call) => {
405
- if (!call || typeof call !== "object") return "";
406
- if (call.function && call.function.name) return String(call.function.name);
407
- return String(call.name || "");
408
- })
409
- .map((name) => name.trim())
410
- .filter(Boolean);
411
- if (names.length > 0) pushLines(`⚙ ${names.join(" · ")}`, "system");
456
+ for (const call of calls) {
457
+ if (!call || typeof call !== "object") continue;
458
+ const name = String(
459
+ (call.function && call.function.name) || call.name || "tool"
460
+ ).trim();
461
+ pushLines(formatUcodeToolLine(name, parseToolCallArguments(call)), "tool");
412
462
  }
413
463
  continue;
414
464
  }
415
465
  if (role === "tool") {
416
- const preview = toolMessagePreview(message);
417
- if (!preview.trim()) continue;
418
- const clipped = preview.split(/\r?\n/).slice(0, maxToolPreviewLines).join("\n");
419
- pushLines(clipped, "toolDetail");
466
+ // The corresponding assistant tool_call already carries the command or
467
+ // path. Tool-result artifacts are internal storage references and do
468
+ // not belong in the restored transcript presentation.
469
+ continue;
420
470
  }
421
471
  }
422
472
 
423
- return { entries, nextSeq: seq, markdownState };
473
+ return {
474
+ entries: collapseConsecutiveUcodeToolEntries(entries),
475
+ nextSeq: seq,
476
+ markdownState,
477
+ };
424
478
  }
425
479
 
426
480
  function shouldEnterAgentSelection(inputValue = "") {
@@ -826,6 +880,19 @@ function buildToolMergeRowText(entries = []) {
826
880
  return `· ${summary}`;
827
881
  }
828
882
 
883
+ function buildUcodeToolRowText(entries = []) {
884
+ const list = Array.isArray(entries)
885
+ ? entries.map((item) => normalizeToolMergeEntry(item))
886
+ : [];
887
+ if (list.length === 0) return "• Tool";
888
+ const first = list[0];
889
+ const firstLine = `• ${formatToolDisplayName(first.tool)}${first.detail ? ` ${first.detail}` : ""}`;
890
+ if (list.length === 1) return firstLine;
891
+ const errorCount = list.filter((item) => item.isError).length;
892
+ const errorSuffix = errorCount > 0 ? ` · ${errorCount} error${errorCount === 1 ? "" : "s"}` : "";
893
+ return `${firstLine} · +${list.length - 1} calls${errorSuffix}`;
894
+ }
895
+
829
896
  /**
830
897
  * Lay out the global-mode project rail inside a single line. Like
831
898
  * planAgentsFooter, but with two differences:
@@ -1316,6 +1383,8 @@ module.exports = {
1316
1383
  buildMergedToolExpandedLines,
1317
1384
  buildMergedToolSummaryText,
1318
1385
  buildToolMergeRowText,
1386
+ buildUcodeToolRowText,
1387
+ collapseConsecutiveUcodeToolEntries,
1319
1388
  buildCompletions,
1320
1389
  buildUcodeBannerLines,
1321
1390
  buildUcodeSessionLogEntries,
@@ -140,7 +140,7 @@ function createBlessedAdapters(escapeFn = (value) => String(value || "")) {
140
140
  return `{bold}${value}{/bold}`;
141
141
  },
142
142
  quoteMarker: () => "{gray-fg}│{/gray-fg}",
143
- bulletMarker: () => "{gray-fg}{/gray-fg}",
143
+ bulletMarker: () => "{gray-fg}-{/gray-fg}",
144
144
  orderedMarker: (value) => `{gray-fg}${escape(value)}.{/gray-fg}`,
145
145
  rule: () => "{gray-fg}────────────────────────{/gray-fg}",
146
146
  fenceOpen: (language) => (
@@ -181,7 +181,7 @@ function createAnsiAdapters() {
181
181
  return paint.bold(text);
182
182
  },
183
183
  quoteMarker: () => paint.gray("│"),
184
- bulletMarker: () => paint.gray(""),
184
+ bulletMarker: () => paint.gray("-"),
185
185
  orderedMarker: (value) => paint.gray(`${value}.`),
186
186
  rule: () => paint.gray("────────────────────────"),
187
187
  fenceOpen: (language) => (
@@ -39,6 +39,7 @@ const {
39
39
  resolveDaemonEndpoint,
40
40
  routeDaemonRequest,
41
41
  } = require("../runtime/daemon/endpoint");
42
+ const PACKAGE_VERSION = require("../../package.json").version;
42
43
 
43
44
  function stripTags(value) {
44
45
  return String(value || "").replace(/\{[^}]+\}/g, "");
@@ -979,6 +980,7 @@ async function runChatRust(projectRoot, options = {}) {
979
980
  const projects = env.globalMode ? loadGlobalProjectRows(activeProjectRoot) : [];
980
981
  return {
981
982
  status: "ready",
983
+ package_version: PACKAGE_VERSION,
982
984
  footer: agents.footer || "",
983
985
  entries: historyToEntries(history),
984
986
  input_history: Array.isArray(inputHistory) ? inputHistory.filter(Boolean) : [],
@@ -17,6 +17,7 @@ const {
17
17
  } = require("../code/UcodeController");
18
18
  const { createEnvelope, encodeMessage } = require("../runtime/contracts/uiProtocol");
19
19
  const fmt = require("./format");
20
+ const PACKAGE_VERSION = require("../../package.json").version;
20
21
 
21
22
  function stripTags(value) {
22
23
  return String(value || "").replace(/\{[^}]+\}/g, "");
@@ -162,6 +163,46 @@ function normalizeToolLogEntry(entry = {}) {
162
163
  return fmt.normalizeToolMergeEntry({ tool, detail, isError, errorText });
163
164
  }
164
165
 
166
+ function splitUcodeBannerRow(line, index) {
167
+ const row = String(line || "");
168
+ const logo = String((fmt.UCODE_BANNER_LINES || [])[index] || "");
169
+ if (logo && row.startsWith(logo)) {
170
+ return { logo, metadata: row.slice(logo.length).trimStart() };
171
+ }
172
+ return { logo: "", metadata: row.trimStart() };
173
+ }
174
+
175
+ /**
176
+ * Keep a model's reasoning in one mutable scrollback entry. The TUI owns the
177
+ * collapsed/expanded presentation; the host only sends an initial row plus
178
+ * append-only deltas for that row.
179
+ */
180
+ function createThinkingLogPublisher(publish, thinkingStatus, streamId) {
181
+ const id = `${String(streamId || "stream")}-thinking`;
182
+ let started = false;
183
+
184
+ function onThinkingDelta(delta) {
185
+ const text = String(delta || "");
186
+ if (!text) return;
187
+ if (!started) {
188
+ started = true;
189
+ publish("thinking.start", { id });
190
+ }
191
+ publish("thinking.delta", { id, text });
192
+ if (thinkingStatus && typeof thinkingStatus.onThinkingDelta === "function") {
193
+ thinkingStatus.onThinkingDelta(text);
194
+ }
195
+ }
196
+
197
+ function stop() {
198
+ if (thinkingStatus && typeof thinkingStatus.reset === "function") {
199
+ thinkingStatus.reset();
200
+ }
201
+ }
202
+
203
+ return { id, onThinkingDelta, stop };
204
+ }
205
+
165
206
  async function runUcodeRust(props = {}) {
166
207
  const plan = resolveTuiLaunchPlan({
167
208
  mode: props.tuiMode || process.env.UFOO_TUI || "rust",
@@ -321,25 +362,22 @@ async function runUcodeRust(props = {}) {
321
362
  } else {
322
363
  lines = raw.split(/\r?\n/);
323
364
  }
324
- let last = null;
325
- for (const line of lines) {
326
- entrySeq += 1;
327
- last = {
328
- id: `u-${entrySeq}`,
329
- kind,
330
- text: stripTags(line),
331
- // Keep ANSI in a parallel field if strip removes styling; prefer raw
332
- // ANSI line for Rust when it contains escape sequences.
333
- ansi: line,
334
- speaker: "",
335
- };
336
- // Prefer ANSI when present so Rust can paint spans; otherwise plain.
337
- if (/\x1b\[/.test(line)) {
338
- last.text = line;
339
- }
340
- publish("transcript.append", last);
365
+ const block = lines.map((line) => String(line || "")).join("\n");
366
+ entrySeq += 1;
367
+ const entry = {
368
+ id: `u-${entrySeq}`,
369
+ kind,
370
+ text: stripTags(block),
371
+ // Keep ANSI in a parallel field if strip removes styling; prefer the
372
+ // rendered block for Rust when it contains escape sequences.
373
+ ansi: block,
374
+ speaker: "",
375
+ };
376
+ if (/\x1b\[/.test(block)) {
377
+ entry.text = block;
341
378
  }
342
- return last;
379
+ publish("transcript.append", entry);
380
+ return entry;
343
381
  }
344
382
 
345
383
  function replaceTranscript(entries = []) {
@@ -351,6 +389,8 @@ async function runUcodeRust(props = {}) {
351
389
  kind: String((entry && entry.kind) || "system"),
352
390
  text,
353
391
  speaker: String((entry && entry.speaker) || ""),
392
+ detail: String((entry && entry.detail) || ""),
393
+ expanded: Boolean(entry && entry.expanded),
354
394
  };
355
395
  });
356
396
  entrySeq = Math.max(entrySeq, normalized.length);
@@ -461,13 +501,25 @@ async function runUcodeRust(props = {}) {
461
501
  );
462
502
  return {
463
503
  status: "ready",
504
+ package_version: PACKAGE_VERSION,
464
505
  footer: agentsSnap.footer,
465
- entries: banner.concat([""]).map((line, idx) => ({
466
- id: `b-${idx}`,
467
- kind: "system",
468
- text: stripTags(line),
506
+ entries: banner.map((line, idx) => {
507
+ const { logo, metadata } = splitUcodeBannerRow(line, idx);
508
+ return {
509
+ id: `b-${idx}`,
510
+ kind: "banner",
511
+ // Logo and metadata are separate raw renderer segments. Neither
512
+ // ever passes through the ordinary log formatter.
513
+ text: logo,
514
+ detail: metadata,
515
+ speaker: "",
516
+ };
517
+ }).concat([{
518
+ id: `b-${banner.length}`,
519
+ kind: "spacer",
520
+ text: "",
469
521
  speaker: "",
470
- })),
522
+ }]),
471
523
  input_history: [],
472
524
  agents: agentsSnap.agents,
473
525
  attachment_count: pendingAttachments.length,
@@ -509,17 +561,28 @@ async function runUcodeRust(props = {}) {
509
561
  tools.beginScope();
510
562
  thinking.reset();
511
563
  publish("stream.start", { id: streamId });
564
+ const thinkingLog = createThinkingLogPublisher(publish, thinking, streamId);
565
+ let responseStarted = false;
566
+ const beginResponse = () => {
567
+ if (responseStarted) return;
568
+ responseStarted = true;
569
+ thinkingLog.stop();
570
+ publish("status.set", { text: "Generating response…", busy: true });
571
+ };
512
572
  try {
513
573
  const result = await submit(answerText, props.state, {
514
574
  signal: abort.signal,
515
575
  onDelta: (delta) => {
576
+ beginResponse();
516
577
  publish("stream.delta", { id: streamId, text: String(delta || "") });
517
578
  },
518
- onThinkingDelta: thinking.onThinkingDelta,
579
+ onThinkingDelta: (delta) => {
580
+ if (!responseStarted) thinkingLog.onThinkingDelta(delta);
581
+ },
519
582
  onToolLog: ingestToolLog,
520
583
  });
521
584
  tools.flush();
522
- thinking.reset();
585
+ thinkingLog.stop();
523
586
  publish("stream.done", { id: streamId });
524
587
  if (!result || result.ok === false) {
525
588
  appendLog(`Error: ${(result && result.error) || "resume failed"}`, "error");
@@ -535,7 +598,7 @@ async function runUcodeRust(props = {}) {
535
598
  return { ok: true, waiting: false };
536
599
  } catch (err) {
537
600
  tools.flush();
538
- thinking.reset();
601
+ thinkingLog.stop();
539
602
  publish("stream.done", { id: streamId });
540
603
  if (abort.signal.aborted) {
541
604
  appendLog("Task cancelled.", "system");
@@ -557,20 +620,31 @@ async function runUcodeRust(props = {}) {
557
620
  tools.beginScope();
558
621
  thinking.reset();
559
622
  publish("stream.start", { id: streamId });
623
+ const thinkingLog = createThinkingLogPublisher(publish, thinking, streamId);
624
+ let responseStarted = false;
625
+ const beginResponse = () => {
626
+ if (responseStarted) return;
627
+ responseStarted = true;
628
+ thinkingLog.stop();
629
+ publish("status.set", { text: "Generating response…", busy: true });
630
+ };
560
631
  try {
561
632
  const nlResult = await props.runNaturalLanguageTask(text, props.state, {
562
633
  signal: abort.signal,
563
634
  onDelta: (delta) => {
635
+ beginResponse();
564
636
  publish("stream.delta", { id: streamId, text: String(delta || "") });
565
637
  },
566
- onThinkingDelta: thinking.onThinkingDelta,
638
+ onThinkingDelta: (delta) => {
639
+ if (!responseStarted) thinkingLog.onThinkingDelta(delta);
640
+ },
567
641
  onToolLog: ingestToolLog,
568
642
  onPhase: (event) => {
569
643
  if (!event || typeof event !== "object") return;
570
644
  if (event.type === "request_start") {
571
645
  publish("status.set", { text: "Waiting for model…", busy: true });
572
646
  } else if (event.type === "text_delta") {
573
- publish("status.set", { text: "Generating response…", busy: true });
647
+ beginResponse();
574
648
  } else if (event.type === "tool_request") {
575
649
  const tool = String(event.name || "tool").trim() || "tool";
576
650
  const label = (fmt.TOOL_LABELS && fmt.TOOL_LABELS[tool.toLowerCase()])
@@ -580,7 +654,7 @@ async function runUcodeRust(props = {}) {
580
654
  },
581
655
  });
582
656
  tools.flush();
583
- thinking.reset();
657
+ thinkingLog.stop();
584
658
  publish("stream.done", { id: streamId });
585
659
  if (nlResult && nlResult.summary) appendLog(nlResult.summary, "system");
586
660
  if (typeof props.formatNlResult === "function" && nlResult) {
@@ -596,7 +670,7 @@ async function runUcodeRust(props = {}) {
596
670
  return { ok: true, waiting: false, result: nlResult };
597
671
  } catch (err) {
598
672
  tools.flush();
599
- thinking.reset();
673
+ thinkingLog.stop();
600
674
  publish("stream.done", { id: streamId });
601
675
  if (abort.signal.aborted) {
602
676
  appendLog("Task cancelled.", "system");
@@ -994,6 +1068,8 @@ module.exports = {
994
1068
  runUcodeRust,
995
1069
  buildPlanSetPayload,
996
1070
  normalizeToolLogEntry,
1071
+ splitUcodeBannerRow,
1072
+ createThinkingLogPublisher,
997
1073
  buildUcodeAgentsSnapshot,
998
1074
  buildUcodeCompletionItems,
999
1075
  };
@@ -23,13 +23,11 @@ function createToolMergePublisher(publish) {
23
23
  merge = null;
24
24
  return;
25
25
  }
26
- const summary = fmt.buildMergedToolSummaryText(merge.entries);
26
+ const summary = fmt.buildUcodeToolRowText(merge.entries);
27
27
  const detail = fmt.buildMergedToolExpandedLines(merge.entries).join("\n");
28
- const row = typeof fmt.buildToolMergeRowText === "function"
29
- ? fmt.buildToolMergeRowText(merge.entries)
30
- : (merge.entries.length >= 2
31
- ? `· ${summary} (Ctrl+O expand)`
32
- : summary);
28
+ const row = merge.entries.length >= 2
29
+ ? `${summary} (Ctrl+O expand)`
30
+ : summary;
33
31
  publish("tool.group", {
34
32
  id: `tool-merge-${merge.id}`,
35
33
  summary: row || summary,
@@ -47,7 +45,7 @@ function createToolMergePublisher(publish) {
47
45
  if (merge) {
48
46
  publish("tool.start", {
49
47
  id: `tool-merge-${merge.id}`,
50
- summary: fmt.buildMergedToolSummaryText(merge.entries),
48
+ summary: fmt.buildUcodeToolRowText(merge.entries),
51
49
  });
52
50
  }
53
51
  }