u-foo 2.4.8 → 2.4.9

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.4.8",
3
+ "version": "2.4.9",
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",
package/src/code/tui.js CHANGED
@@ -5,6 +5,7 @@ const {
5
5
  StreamBuffer,
6
6
  UCODE_BANNER_LINES,
7
7
  UCODE_VERSION,
8
+ appendToolMergeEntry,
8
9
  buildMergedToolExpandedLines,
9
10
  buildMergedToolSummaryText,
10
11
  buildUcodeBannerLines,
@@ -31,6 +32,7 @@ const {
31
32
  shouldClearAgentSelectionOnUp,
32
33
  shouldEnterAgentSelection,
33
34
  shouldUseUcodeTui,
35
+ splitStreamingLogChunk,
34
36
  stripLeakedEscapeTags,
35
37
  } = fmt;
36
38
 
@@ -64,8 +66,10 @@ module.exports = {
64
66
  resolveHistoryDownTransition,
65
67
  filterSelectableAgents,
66
68
  stripLeakedEscapeTags,
69
+ splitStreamingLogChunk,
67
70
  createEscapeTagStripper,
68
71
  formatPendingElapsed,
72
+ appendToolMergeEntry,
69
73
  normalizeBashToolCommand,
70
74
  normalizeToolMergeEntry,
71
75
  buildMergedToolSummaryText,
@@ -518,6 +518,24 @@ function normalizeToolMergeEntry(entry = {}) {
518
518
  };
519
519
  }
520
520
 
521
+ function appendToolMergeEntry(currentMerge = null, entry = {}, scope = 0, nextId = 1) {
522
+ const toolEntry = normalizeToolMergeEntry(entry);
523
+ const current = currentMerge && typeof currentMerge === "object" ? currentMerge : null;
524
+ const normalizedScope = Number.isFinite(Number(scope)) ? Number(scope) : 0;
525
+ if (current && current.scope === normalizedScope && Array.isArray(current.entries)) {
526
+ return {
527
+ ...current,
528
+ entries: current.entries.concat([toolEntry]),
529
+ };
530
+ }
531
+ return {
532
+ id: Number.isFinite(Number(nextId)) ? Number(nextId) : 1,
533
+ scope: normalizedScope,
534
+ entries: [toolEntry],
535
+ expanded: false,
536
+ };
537
+ }
538
+
521
539
  function buildMergedToolSummaryText(entries = []) {
522
540
  const list = Array.isArray(entries)
523
541
  ? entries.map((item) => normalizeToolMergeEntry(item))
@@ -551,6 +569,27 @@ function buildMergedToolExpandedLines(entries = []) {
551
569
  });
552
570
  }
553
571
 
572
+ function splitStreamingLogChunk(buffer = "", chunk = "", options = {}) {
573
+ const previous = String(buffer || "");
574
+ const text = String(chunk || "");
575
+ const combined = `${previous}${text}`;
576
+ const parts = combined.split(/\r?\n/);
577
+ const lines = parts.slice(0, -1);
578
+ const dropLeadingBlank = Boolean(options.dropLeadingBlank) && previous === "";
579
+
580
+ if (dropLeadingBlank) {
581
+ while (lines.length > 0 && lines[0] === "") {
582
+ lines.shift();
583
+ }
584
+ }
585
+
586
+ return {
587
+ lines,
588
+ buffer: parts[parts.length - 1] || "",
589
+ sawVisible: /[^\s]/.test(text),
590
+ };
591
+ }
592
+
554
593
  // Composed live-row text for an in-flight tool group: shows the merged
555
594
  // summary, plus a "(Ctrl+O expand)" hint once at least two entries are
556
595
  // present.
@@ -937,6 +976,7 @@ module.exports = {
937
976
  TOOL_LABELS,
938
977
  UCODE_BANNER_LINES,
939
978
  UCODE_VERSION,
979
+ appendToolMergeEntry,
940
980
  buildMergedToolExpandedLines,
941
981
  buildMergedToolSummaryText,
942
982
  buildToolMergeRowText,
@@ -970,5 +1010,6 @@ module.exports = {
970
1010
  shouldClearAgentSelectionOnUp,
971
1011
  shouldEnterAgentSelection,
972
1012
  shouldUseUcodeTui,
1013
+ splitStreamingLogChunk,
973
1014
  stripLeakedEscapeTags,
974
1015
  };
@@ -78,6 +78,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
78
78
  const { stdout } = useStdout();
79
79
  const lineSeqRef = useRef(banner.length + 1);
80
80
  const mergeIdRef = useRef(0);
81
+ const toolMergeScopeRef = useRef(0);
81
82
 
82
83
  const targetAgent = agentSelectionMode && selectedAgentIndex >= 0
83
84
  ? agents[selectedAgentIndex]
@@ -261,13 +262,12 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
261
262
  const toolEntry = fmt.normalizeToolMergeEntry({ tool, detail, isError, errorText });
262
263
 
263
264
  setActiveMerge((current) => {
264
- let next;
265
- if (current) {
266
- next = { ...current, entries: current.entries.concat([toolEntry]) };
267
- } else {
265
+ const scope = toolMergeScopeRef.current;
266
+ const isNewScope = !(current && current.scope === scope);
267
+ if (isNewScope) {
268
268
  mergeIdRef.current += 1;
269
- next = { id: mergeIdRef.current, entries: [toolEntry], expanded: false };
270
269
  }
270
+ const next = fmt.appendToolMergeEntry(current, toolEntry, scope, mergeIdRef.current);
271
271
  if (next.entries.length >= 2) lastMergeRef.current = next;
272
272
  return next;
273
273
  });
@@ -313,6 +313,8 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
313
313
  const executeLine = useCallback(async (rawValue) => {
314
314
  const normalized = String(rawValue || "").replace(/\r?\n/g, " ").trim();
315
315
  if (!normalized) return;
316
+ toolMergeScopeRef.current += 1;
317
+ flushActiveMerge();
316
318
  appendLogLine(`› ${normalized}`);
317
319
 
318
320
  const runtimeWorkspace = String(
@@ -459,6 +461,8 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
459
461
  setNlStatus("Waiting for model...");
460
462
  let streamBuf = "";
461
463
  let sawStreamText = false;
464
+ let streamStarted = false;
465
+ let dropLeadingStreamBlank = false;
462
466
  let nlResult = null;
463
467
  try {
464
468
  nlResult = await props.runNaturalLanguageTask(result.task, props.state, {
@@ -477,13 +481,21 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
477
481
  onDelta: (delta) => {
478
482
  const text = String(delta || "");
479
483
  if (!text) return;
480
- if (/[^\s]/.test(text)) sawStreamText = true;
481
- streamBuf += text;
482
- const parts = streamBuf.split(/\r?\n/);
483
- while (parts.length > 1) {
484
- appendLogLine(parts.shift());
484
+ if (!streamStarted) {
485
+ flushActiveMerge();
486
+ streamStarted = true;
487
+ }
488
+ const split = fmt.splitStreamingLogChunk(streamBuf, text, {
489
+ dropLeadingBlank: dropLeadingStreamBlank,
490
+ });
491
+ if (split.sawVisible) {
492
+ sawStreamText = true;
493
+ dropLeadingStreamBlank = false;
494
+ }
495
+ for (const line of split.lines) {
496
+ appendLogLine(line);
485
497
  }
486
- streamBuf = parts[0];
498
+ streamBuf = split.buffer;
487
499
  },
488
500
  onToolLog: (entry) => {
489
501
  if (!entry || typeof entry !== "object") return;
@@ -491,6 +503,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
491
503
  const label = fmt.TOOL_LABELS[String(entry.tool || "").toLowerCase()] ||
492
504
  `Calling ${entry.tool}`;
493
505
  setNlStatus(`${label}...`);
506
+ dropLeadingStreamBlank = true;
494
507
  }
495
508
  logToolHint(entry, entry.result);
496
509
  },
@@ -516,6 +529,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
516
529
  const summary = props.formatNlResult(nlResult, false);
517
530
  if (summary) appendLogText(summary);
518
531
  }
532
+ flushActiveMerge();
519
533
  try {
520
534
  const persisted = props.persistSessionState(props.state);
521
535
  if (persisted && persisted.ok === false) {
@@ -531,7 +545,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
531
545
  default:
532
546
  if (result.output) appendLogText(result.output);
533
547
  }
534
- }, [appendLogLine, appendLogText, exit, props, logToolHint]);
548
+ }, [appendLogLine, appendLogText, exit, props, logToolHint, flushActiveMerge]);
535
549
  // ^ `props` is captured by the createUcodeApp closure on a single mount,
536
550
  // so its reference is stable across renders even though it looks like a
537
551
  // changing dep to React's exhaustive-deps lint.