u-foo 2.3.27 → 2.3.29

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.3.27",
3
+ "version": "2.3.29",
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",
@@ -52,6 +52,7 @@ function createAgentViewController(options = {}) {
52
52
  sendResize = () => {},
53
53
  requestScreenSnapshot = () => {},
54
54
  sendBusWatch = () => {},
55
+ getBusLogHistory = () => [],
55
56
  } = options;
56
57
 
57
58
  if (!screen || typeof screen.render !== "function") {
@@ -503,7 +504,14 @@ function createAgentViewController(options = {}) {
503
504
  busStartupAgentId = agentId || "";
504
505
  const label = getAgentLabel(agentId);
505
506
  const startupLines = staticStartupLines(agentId, label, getCols());
506
- busLogLines = startupLines.concat("");
507
+ let historyLines = [];
508
+ try {
509
+ const loaded = getBusLogHistory(agentId);
510
+ historyLines = Array.isArray(loaded) ? loaded : [];
511
+ } catch {
512
+ historyLines = [];
513
+ }
514
+ busLogLines = startupLines.concat("", historyLines);
507
515
  busStartupLineCount = startupLines.length;
508
516
  }
509
517
 
@@ -516,6 +524,13 @@ function createAgentViewController(options = {}) {
516
524
  busStartupLineCount = startupLines.length;
517
525
  }
518
526
 
527
+ function trimBusLogLines() {
528
+ if (busLogLines.length <= 1000) return;
529
+ const removed = busLogLines.length - 1000;
530
+ busLogLines = busLogLines.slice(-1000);
531
+ busStartupLineCount = Math.max(0, busStartupLineCount - removed);
532
+ }
533
+
519
534
  function appendBusLog(text = "") {
520
535
  const clean = stripAnsi(String(text || "")).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
521
536
  if (busLogLines.length === 0) busLogLines.push("");
@@ -526,9 +541,7 @@ function createAgentViewController(options = {}) {
526
541
  busLogLines[busLogLines.length - 1] += char;
527
542
  }
528
543
  }
529
- if (busLogLines.length > 1000) {
530
- busLogLines = busLogLines.slice(-1000);
531
- }
544
+ trimBusLogLines();
532
545
  if (clean.endsWith("\n")) {
533
546
  busAgentReplyActive = false;
534
547
  }
@@ -562,9 +575,7 @@ function createAgentViewController(options = {}) {
562
575
  }
563
576
  busLogLines[busLogLines.length - 1] += char;
564
577
  }
565
- if (busLogLines.length > 1000) {
566
- busLogLines = busLogLines.slice(-1000);
567
- }
578
+ trimBusLogLines();
568
579
  }
569
580
 
570
581
  function getBusInputViewport(width) {
@@ -654,18 +665,22 @@ function createAgentViewController(options = {}) {
654
665
 
655
666
  function enterAgentView(agentId, options = {}) {
656
667
  if (currentView === "agent" && viewingAgent === agentId) return;
668
+ const wasInAgentView = currentView === "agent";
657
669
  if (currentView === "agent") {
658
670
  if (agentViewUsesBus && viewingAgent) sendBusWatch(viewingAgent, false);
659
671
  disconnectAgentOutput();
660
672
  disconnectAgentInput();
673
+ stopBusStatusTimer();
661
674
  }
662
675
 
663
676
  currentView = "agent";
664
677
  viewingAgent = agentId;
665
678
  setFocusMode("input");
666
679
 
667
- detachedChildren = [...screen.children];
668
- for (const child of detachedChildren) screen.remove(child);
680
+ if (!wasInAgentView) {
681
+ detachedChildren = [...screen.children];
682
+ for (const child of detachedChildren) screen.remove(child);
683
+ }
669
684
 
670
685
  renderFrozen = true;
671
686
 
package/src/chat/index.js CHANGED
@@ -38,6 +38,7 @@ const { createDaemonMessageRouter } = require("./daemonMessageRouter");
38
38
  const { createChatLogController } = require("./chatLogController");
39
39
  const { createPasteController } = require("./pasteController");
40
40
  const { createAgentViewController } = require("./agentViewController");
41
+ const { loadInternalAgentLogHistory } = require("./internalAgentLogHistory");
41
42
  const { createSettingsController } = require("./settingsController");
42
43
  const { createProjectCloseController } = require("./projectCloseController");
43
44
  const { createChatLayout } = require("./layout");
@@ -1628,6 +1629,7 @@ async function runChat(projectRoot, options = {}) {
1628
1629
  requestScreenSnapshot: () => {
1629
1630
  requestSnapshotWithCapabilities();
1630
1631
  },
1632
+ getBusLogHistory: (agentId) => loadInternalAgentLogHistory(activeProjectRoot, agentId),
1631
1633
  });
1632
1634
 
1633
1635
  function requestStatus() {
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { getUfooPaths } = require("../ufoo/paths");
6
+
7
+ function stripAnsi(text = "") {
8
+ return String(text || "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "")
9
+ .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
10
+ }
11
+
12
+ function decodeEscapedNewlines(text = "") {
13
+ return String(text || "").replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n").replace(/\\r/g, "\n");
14
+ }
15
+
16
+ function normalizeText(text = "") {
17
+ return stripAnsi(decodeEscapedNewlines(text)).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
18
+ }
19
+
20
+ function readMatchingEventsFromFileReverse(filePath, aliases, limit) {
21
+ if (limit <= 0) return [];
22
+ try {
23
+ const lines = fs.readFileSync(filePath, "utf8").split(/\r?\n/);
24
+ const events = [];
25
+ for (let index = lines.length - 1; index >= 0 && events.length < limit; index -= 1) {
26
+ const line = lines[index];
27
+ if (!line) continue;
28
+ let evt = null;
29
+ try {
30
+ evt = JSON.parse(line);
31
+ } catch {
32
+ continue;
33
+ }
34
+ if (isEventForAgent(evt, aliases)) events.push(evt);
35
+ }
36
+ return events;
37
+ } catch {
38
+ return [];
39
+ }
40
+ }
41
+
42
+ function buildAgentAliases(agentId = "", agents = {}) {
43
+ const aliases = new Set();
44
+ const id = String(agentId || "").trim();
45
+ if (!id) return aliases;
46
+ aliases.add(id);
47
+ const meta = agents && agents[id] ? agents[id] : null;
48
+ if (meta) {
49
+ for (const key of ["nickname", "scoped_nickname", "display_nickname"]) {
50
+ if (meta[key]) aliases.add(String(meta[key]));
51
+ }
52
+ }
53
+ return aliases;
54
+ }
55
+
56
+ function eventData(evt = {}) {
57
+ return evt && evt.data && typeof evt.data === "object" ? evt.data : {};
58
+ }
59
+
60
+ function isEventForAgent(evt, aliases) {
61
+ if (!evt || evt.event !== "message") return false;
62
+ const data = eventData(evt);
63
+ const candidates = [
64
+ evt.publisher,
65
+ evt.target,
66
+ data.publisher,
67
+ data.target,
68
+ data.subscriber,
69
+ ].filter(Boolean).map(String);
70
+ return candidates.some((value) => aliases.has(value));
71
+ }
72
+
73
+ function parseStreamMessage(raw = "") {
74
+ try {
75
+ const parsed = JSON.parse(raw);
76
+ if (!parsed || typeof parsed !== "object" || !parsed.stream) return null;
77
+ if (typeof parsed.delta === "string") return normalizeText(parsed.delta);
78
+ if (parsed.done) return "";
79
+ return "";
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+
85
+ function appendPrefixedText(lines, prefix, text) {
86
+ const clean = normalizeText(text);
87
+ if (!clean) return;
88
+ const parts = clean.split("\n");
89
+ parts.forEach((part, index) => {
90
+ if (index === parts.length - 1 && part === "") return;
91
+ lines.push(`${index === 0 ? prefix : " "}${part}`);
92
+ });
93
+ }
94
+
95
+ function appendUserText(lines, text, state) {
96
+ appendPrefixedText(lines, "> ", text);
97
+ state.replyActive = false;
98
+ }
99
+
100
+ function ensureAgentReplyPrefix(lines, prefix = "• ") {
101
+ if (lines.length === 0) {
102
+ lines.push(prefix);
103
+ return;
104
+ }
105
+ if (lines[lines.length - 1] === "") {
106
+ lines[lines.length - 1] = prefix;
107
+ return;
108
+ }
109
+ lines.push(prefix);
110
+ }
111
+
112
+ function appendAgentStreamText(lines, text, state) {
113
+ const clean = normalizeText(text);
114
+ if (!clean) return;
115
+ for (const char of clean) {
116
+ if (char === "\n") {
117
+ lines.push("");
118
+ continue;
119
+ }
120
+ if (!state.replyActive) {
121
+ ensureAgentReplyPrefix(lines, "• ");
122
+ state.replyActive = true;
123
+ } else if (lines.length === 0 || lines[lines.length - 1] === "") {
124
+ ensureAgentReplyPrefix(lines, " ");
125
+ }
126
+ lines[lines.length - 1] += char;
127
+ }
128
+ }
129
+
130
+ function appendAgentMessage(lines, text, state) {
131
+ appendPrefixedText(lines, "• ", text);
132
+ state.replyActive = false;
133
+ }
134
+
135
+ function loadInternalAgentLogHistory(projectRoot, agentId, options = {}) {
136
+ const paths = getUfooPaths(projectRoot || process.cwd());
137
+ const maxEvents = Number.isFinite(options.maxEvents) ? Math.max(1, options.maxEvents) : 400;
138
+ const maxLines = Number.isFinite(options.maxLines) ? Math.max(1, options.maxLines) : 1000;
139
+ let agents = {};
140
+ try {
141
+ const bus = JSON.parse(fs.readFileSync(paths.agentsFile, "utf8"));
142
+ agents = bus.agents || {};
143
+ } catch {
144
+ agents = {};
145
+ }
146
+
147
+ const aliases = buildAgentAliases(agentId, agents);
148
+ if (aliases.size === 0) return [];
149
+
150
+ let files = [];
151
+ try {
152
+ files = fs.readdirSync(paths.busEventsDir)
153
+ .filter((name) => name.endsWith(".jsonl"))
154
+ .sort()
155
+ .map((name) => path.join(paths.busEventsDir, name));
156
+ } catch {
157
+ return [];
158
+ }
159
+
160
+ const reversedEvents = [];
161
+ for (const file of files.slice(-7).reverse()) {
162
+ if (reversedEvents.length >= maxEvents) break;
163
+ const remaining = maxEvents - reversedEvents.length;
164
+ reversedEvents.push(...readMatchingEventsFromFileReverse(file, aliases, remaining));
165
+ }
166
+
167
+ const lines = [];
168
+ const state = { replyActive: false };
169
+ for (const evt of reversedEvents.reverse()) {
170
+ const data = eventData(evt);
171
+ const rawMessage = typeof data.message === "string" ? data.message : "";
172
+ const streamDelta = parseStreamMessage(rawMessage);
173
+ const publisher = String(evt.publisher || "");
174
+ const target = String(evt.target || data.target || "");
175
+ const isFromAgent = aliases.has(publisher);
176
+ const isToAgent = aliases.has(target) || aliases.has(String(data.subscriber || ""));
177
+
178
+ if (streamDelta !== null) {
179
+ if (isFromAgent && streamDelta) {
180
+ appendAgentStreamText(lines, streamDelta, state);
181
+ } else if (isFromAgent) {
182
+ state.replyActive = false;
183
+ }
184
+ continue;
185
+ }
186
+
187
+ if (isFromAgent) {
188
+ appendAgentMessage(lines, rawMessage, state);
189
+ } else if (isToAgent) {
190
+ appendUserText(lines, rawMessage, state);
191
+ }
192
+ }
193
+
194
+ return lines.slice(-maxLines);
195
+ }
196
+
197
+ module.exports = {
198
+ loadInternalAgentLogHistory,
199
+ buildAgentAliases,
200
+ };
@@ -9,8 +9,11 @@ const { getBashToolDescription } = require("./prompts/toolDescriptions/bash");
9
9
  const CORE_TOOL_NAMES = new Set(["read", "write", "edit", "bash"]);
10
10
  const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
11
11
  const DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
12
- const DEFAULT_MAX_NATIVE_TOOL_CALLS = 12;
13
- const DEFAULT_MAX_NATIVE_TOOL_ERRORS = 1;
12
+ // Claude Code SDK defaults to no turn limit; built-in agents cap at 30 (DreamTask)
13
+ // to 200 (fork). We count individual tool calls (not turns), so 100 leaves headroom
14
+ // for non-trivial tasks while still catching runaway loops. Override via env.
15
+ const DEFAULT_MAX_NATIVE_TOOL_CALLS = 100;
16
+ const DEFAULT_MAX_NATIVE_TOOL_ERRORS = 5;
14
17
 
15
18
  function nowMs() {
16
19
  return Date.now();