u-foo 2.3.25 → 2.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "2.3.25",
3
+ "version": "2.3.26",
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",
@@ -349,6 +349,27 @@ async function handleEvent(
349
349
  await busSender.flush();
350
350
  }
351
351
 
352
+ function compactToolDetail(value = "", maxLength = 120) {
353
+ const text = String(value || "").replace(/\s+/g, " ").trim();
354
+ if (!text) return "";
355
+ if (text.length <= maxLength) return text;
356
+ return `${text.slice(0, Math.max(0, maxLength - 3))}...`;
357
+ }
358
+
359
+ function summarizeThreadToolCall(event = {}) {
360
+ const name = String(event.name || event.tool || event.tool_name || "tool").trim() || "tool";
361
+ const args = event.args && typeof event.args === "object" ? event.args : {};
362
+ const detail = args.command
363
+ || args.cmd
364
+ || args.code
365
+ || args.path
366
+ || args.file
367
+ || args.target
368
+ || args.query
369
+ || "";
370
+ return [name, compactToolDetail(detail)].filter(Boolean).join(" · ");
371
+ }
372
+
352
373
  async function handleThreadedEvent({
353
374
  agentType,
354
375
  provider,
@@ -372,6 +393,11 @@ async function handleThreadedEvent({
372
393
  } else {
373
394
  plainReplyParts.push(String(event.delta));
374
395
  }
396
+ } else if (event.type === "tool_call") {
397
+ const summary = summarizeThreadToolCall(event);
398
+ if (streamToPublisher && summary) {
399
+ emitStreamDelta(`\nTool: ${summary}\n`);
400
+ }
375
401
  } else if (event.type === "turn_failed") {
376
402
  throw new Error(event.error || `thread turn failed for ${agentType}`);
377
403
  }
@@ -3,6 +3,12 @@ const { version: packageVersion } = require("../../package.json");
3
3
 
4
4
  const ANSI_RESET = "\x1b[0m";
5
5
  const CLAUDE_ORANGE = "\x1b[38;2;217;119;87m";
6
+ const BUS_STATUS_INDICATORS = {
7
+ working: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
8
+ starting: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
9
+ waiting_input: ["∙", "∙∙", "∙∙∙", "∙∙", "∙"],
10
+ blocked: ["!"],
11
+ };
6
12
 
7
13
  function createAgentViewController(options = {}) {
8
14
  const {
@@ -11,6 +17,8 @@ function createAgentViewController(options = {}) {
11
17
  processStdout = process.stdout,
12
18
  now = () => Date.now(),
13
19
  setTimeoutFn = setTimeout,
20
+ setIntervalFn = setInterval,
21
+ clearIntervalFn = clearInterval,
14
22
  computeAgentBar = () => ({ bar: "", windowStart: 0 }),
15
23
  agentBarHints = { normal: "", dashboard: "" },
16
24
  maxAgentWindow = 4,
@@ -23,6 +31,7 @@ function createAgentViewController(options = {}) {
23
31
  setAgentListWindowStart = () => {},
24
32
  getAgentLabel = (id) => id,
25
33
  getAgentStates = () => ({}),
34
+ getAgentActivityMeta = () => ({}),
26
35
  getProjectRoot = () => process.cwd(),
27
36
  setDashboardView = () => {},
28
37
  setScreenGrabKeys = (value) => {
@@ -62,6 +71,10 @@ function createAgentViewController(options = {}) {
62
71
  let busStartupAgentId = "";
63
72
  let busStartupLineCount = 0;
64
73
  let busAgentReplyActive = false;
74
+ let busStatusInterval = null;
75
+ let busStatusIndex = 0;
76
+ let busStatusKey = "";
77
+ let busStatusLocalStartedAt = 0;
65
78
  const originalRender = screen.render.bind(screen);
66
79
  let renderFrozen = false;
67
80
 
@@ -202,6 +215,130 @@ function createAgentViewController(options = {}) {
202
215
  return hasAnsi(text) ? fitAnsiText(text, normalizedWidth) : plainLine(text, normalizedWidth);
203
216
  }
204
217
 
218
+ function parseTimeMs(value) {
219
+ if (Number.isFinite(value)) return Number(value);
220
+ const text = String(value || "").trim();
221
+ if (!text) return NaN;
222
+ const parsed = Date.parse(text);
223
+ return Number.isFinite(parsed) ? parsed : NaN;
224
+ }
225
+
226
+ function formatElapsed(ms = 0) {
227
+ const totalSeconds = Math.max(0, Math.floor(Number(ms) / 1000));
228
+ return `${totalSeconds} s`;
229
+ }
230
+
231
+ function normalizeActivityState(value = "") {
232
+ const state = String(value || "").trim().toLowerCase();
233
+ if (state === "waiting") return "waiting_input";
234
+ if (state === "busy" || state === "processing") return "working";
235
+ return state;
236
+ }
237
+
238
+ function getActivityLabel(state = "") {
239
+ if (state === "working") return "working";
240
+ if (state === "waiting_input") return "waiting";
241
+ if (state === "blocked") return "blocked";
242
+ if (state === "starting") return "starting";
243
+ if (state === "idle" || state === "ready") return "ready";
244
+ return state || "ready";
245
+ }
246
+
247
+ function isTimedActivityState(state = "") {
248
+ return state === "working"
249
+ || state === "waiting_input"
250
+ || state === "blocked"
251
+ || state === "starting";
252
+ }
253
+
254
+ function asActivityObject(value) {
255
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
256
+ }
257
+
258
+ function pickActivityDetail(meta = {}) {
259
+ const candidates = [
260
+ meta.activity_detail,
261
+ meta.detail,
262
+ meta.status_text,
263
+ meta.command,
264
+ meta.tool_name,
265
+ meta.tool,
266
+ ];
267
+ return String(candidates.find((item) => String(item || "").trim()) || "").trim();
268
+ }
269
+
270
+ function getViewingAgentActivity() {
271
+ const states = getAgentStates() || {};
272
+ const stateEntry = viewingAgent && states ? states[viewingAgent] : "";
273
+ const stateObject = asActivityObject(stateEntry);
274
+ const meta = {
275
+ ...(stateObject || {}),
276
+ ...(asActivityObject(getAgentActivityMeta(viewingAgent)) || {}),
277
+ };
278
+ const state = normalizeActivityState(meta.activity_state || meta.state || (stateObject ? "" : stateEntry) || "");
279
+ const detail = pickActivityDetail(meta);
280
+ const sinceMs = parseTimeMs(meta.activity_since || meta.since || meta.updated_at || meta.updatedAt);
281
+ return { state: state || "ready", detail, sinceMs };
282
+ }
283
+
284
+ function resolveBusStatus() {
285
+ const activity = getViewingAgentActivity();
286
+ const state = activity.state || "ready";
287
+ const timed = isTimedActivityState(state);
288
+ const key = `${viewingAgent || ""}:${state}:${activity.detail || ""}`;
289
+ if (key !== busStatusKey) {
290
+ busStatusKey = key;
291
+ busStatusIndex = 0;
292
+ busStatusLocalStartedAt = now();
293
+ }
294
+ const startedAt = timed && Number.isFinite(activity.sinceMs)
295
+ ? activity.sinceMs
296
+ : busStatusLocalStartedAt;
297
+ return {
298
+ ...activity,
299
+ state,
300
+ label: getActivityLabel(state),
301
+ timed,
302
+ startedAt,
303
+ };
304
+ }
305
+
306
+ function buildBusStatusLine(width = 80, status = resolveBusStatus()) {
307
+ const normalizedWidth = Math.max(1, width);
308
+ const detail = status.detail ? ` · ${status.detail}` : "";
309
+ if (status.timed) {
310
+ const indicators = BUS_STATUS_INDICATORS[status.state] || BUS_STATUS_INDICATORS.working;
311
+ const indicator = indicators[busStatusIndex % indicators.length] || "";
312
+ const elapsed = formatElapsed(now() - status.startedAt);
313
+ return fitText(`${indicator} ${status.label} · ${elapsed}${detail}`, normalizedWidth);
314
+ }
315
+ if (normalizedWidth < 32) return fitText(`ufoo · ${status.label}`, normalizedWidth);
316
+ if (normalizedWidth < 48) return fitText(`ufoo · ${status.label} · Enter send`, normalizedWidth);
317
+ return fitText(`ufoo · ${status.label} · Enter send · Esc back${detail}`, normalizedWidth);
318
+ }
319
+
320
+ function stopBusStatusTimer() {
321
+ if (!busStatusInterval) return;
322
+ clearIntervalFn(busStatusInterval);
323
+ busStatusInterval = null;
324
+ }
325
+
326
+ function syncBusStatusTimer(status) {
327
+ const shouldTick = currentView === "agent" && agentViewUsesBus && status && status.timed;
328
+ if (!shouldTick) {
329
+ stopBusStatusTimer();
330
+ return;
331
+ }
332
+ if (busStatusInterval) return;
333
+ busStatusInterval = setIntervalFn(() => {
334
+ busStatusIndex += 1;
335
+ renderBusView();
336
+ }, 1000);
337
+ if (busStatusInterval && typeof busStatusInterval.unref === "function") {
338
+ busStatusInterval.unref();
339
+ }
340
+ }
341
+
205
342
  function sliceDisplayCells(text = "", startCell = 0, maxCells = 1) {
206
343
  const targetStart = Math.max(0, startCell);
207
344
  const targetWidth = Math.max(1, maxCells);
@@ -456,12 +593,16 @@ function createAgentViewController(options = {}) {
456
593
  const logContentTop = 1;
457
594
  const logContentBottom = Math.max(logContentTop, inputTop - 1);
458
595
  const logContentHeight = Math.max(1, logContentBottom - logContentTop + 1);
596
+ const status = resolveBusStatus();
597
+ const logRows = Math.max(0, logContentHeight - 1);
598
+ const statusRow = logContentTop + logRows;
459
599
 
460
600
  processStdout.write("\x1b[?25l");
461
- const visibleLines = getWrappedBusLogLines(width).slice(-logContentHeight);
462
- for (let i = 0; i < logContentHeight; i += 1) {
601
+ const visibleLines = getWrappedBusLogLines(width).slice(-logRows);
602
+ for (let i = 0; i < logRows; i += 1) {
463
603
  writeAt(logContentTop + i, logLine(visibleLines[i] || "", width));
464
604
  }
605
+ writeAt(statusRow, logLine(buildBusStatusLine(width, status), width));
465
606
 
466
607
  writeAt(inputTop, horizontalLine(width));
467
608
  const viewport = getBusInputViewport(width);
@@ -471,6 +612,7 @@ function createAgentViewController(options = {}) {
471
612
  renderAgentDashboard();
472
613
  const cursorCol = clamp(3 + viewport.cursorCol, 1, width);
473
614
  processStdout.write(`\x1b[${inputTop + 1};${cursorCol}H\x1b[?25h`);
615
+ syncBusStatusTimer(status);
474
616
  }
475
617
 
476
618
  function renderAgentDashboard() {
@@ -566,6 +708,7 @@ function createAgentViewController(options = {}) {
566
708
  agentViewUsesBus = false;
567
709
  agentOutputSuppressed = false;
568
710
  agentBarVisible = false;
711
+ stopBusStatusTimer();
569
712
  busInputValue = "";
570
713
  busInputCursor = 0;
571
714
  busLogLines = [];
@@ -871,6 +1014,16 @@ function createAgentViewController(options = {}) {
871
1014
  }
872
1015
  }
873
1016
 
1017
+ function refreshAgentView() {
1018
+ if (currentView !== "agent") return false;
1019
+ if (agentViewUsesBus) {
1020
+ renderBusView();
1021
+ } else {
1022
+ renderAgentDashboard();
1023
+ }
1024
+ return true;
1025
+ }
1026
+
874
1027
  function isAgentBarVisible() {
875
1028
  return agentBarVisible;
876
1029
  }
@@ -882,6 +1035,7 @@ function createAgentViewController(options = {}) {
882
1035
  getAgentInputSuppressUntil,
883
1036
  getAgentOutputSuppressed,
884
1037
  setAgentOutputSuppressed,
1038
+ refreshAgentView,
885
1039
  isAgentBarVisible,
886
1040
  renderAgentDashboard,
887
1041
  setAgentBarVisible,
@@ -76,7 +76,7 @@ function createDaemonMessageRouter(options = {}) {
76
76
  const key = typeof data.key === "string" ? data.key : "";
77
77
  if (isLikelySubscriberId(key)) {
78
78
  if (data.phase === BUS_STATUS_PHASES.START) {
79
- setTransientAgentState(key, "working");
79
+ setTransientAgentState(key, "working", { detail: text });
80
80
  } else if (data.phase === BUS_STATUS_PHASES.DONE || data.phase === BUS_STATUS_PHASES.ERROR) {
81
81
  clearTransientAgentState(key);
82
82
  }
@@ -354,6 +354,18 @@ function createDaemonMessageRouter(options = {}) {
354
354
  function handleBusMessage(msg) {
355
355
  const data = msg.data || {};
356
356
  if (data.event === "activity_state_changed") {
357
+ const agentId = String(data.subscriber || data.publisher || "").trim();
358
+ const state = String(data.state || data.activity_state || "").trim();
359
+ const detailSource = data.detail || (data.data && data.data.detail) || data.message || "";
360
+ if (agentId && state) {
361
+ const normalized = state.toLowerCase();
362
+ if (normalized === "idle" || normalized === "ready") {
363
+ clearTransientAgentState(agentId);
364
+ } else {
365
+ setTransientAgentState(agentId, state, { detail: detailSource });
366
+ }
367
+ refreshDashboard();
368
+ }
357
369
  requestStatus();
358
370
  return true;
359
371
  }
package/src/chat/index.js CHANGED
@@ -61,6 +61,7 @@ const { loadPromptProfileRegistry } = require("../group/promptProfiles");
61
61
  const {
62
62
  DEFAULT_TRANSIENT_AGENT_STATE_TTL_MS,
63
63
  setTransientAgentState: setTransientAgentStateValue,
64
+ getTransientAgentStateEntry,
64
65
  getTransientAgentState,
65
66
  pruneTransientAgentStates,
66
67
  } = require("./transientAgentState");
@@ -1340,7 +1341,11 @@ async function runChat(projectRoot, options = {}) {
1340
1341
  selectedAgentIndex = 0;
1341
1342
  }
1342
1343
  }
1343
- renderAgentDashboard();
1344
+ if (agentViewController && typeof agentViewController.refreshAgentView === "function") {
1345
+ agentViewController.refreshAgentView();
1346
+ } else {
1347
+ renderAgentDashboard();
1348
+ }
1344
1349
  return;
1345
1350
  }
1346
1351
  if (focusMode === "dashboard") {
@@ -1537,13 +1542,39 @@ async function runChat(projectRoot, options = {}) {
1537
1542
  getAgentLabel,
1538
1543
  getAgentStates: () => {
1539
1544
  const states = {};
1540
- if (activeAgentMetaMap) {
1541
- for (const [id, meta] of activeAgentMetaMap) {
1542
- if (meta && meta.activity_state) states[id] = meta.activity_state;
1545
+ for (const id of activeAgents) {
1546
+ let state = "";
1547
+ if (activeAgentMetaMap) {
1548
+ const meta = activeAgentMetaMap.get(id);
1549
+ if (meta && meta.activity_state) state = meta.activity_state;
1550
+ }
1551
+ if (!state) {
1552
+ state = getTransientAgentState(transientAgentStateMap, id, {
1553
+ ttlMs: DEFAULT_TRANSIENT_AGENT_STATE_TTL_MS,
1554
+ });
1543
1555
  }
1556
+ if (state) states[id] = state;
1544
1557
  }
1545
1558
  return states;
1546
1559
  },
1560
+ getAgentActivityMeta: (agentId) => {
1561
+ const id = String(agentId || "").trim();
1562
+ const meta = activeAgentMetaMap && activeAgentMetaMap.get(id)
1563
+ ? { ...activeAgentMetaMap.get(id) }
1564
+ : {};
1565
+ const transient = getTransientAgentStateEntry(transientAgentStateMap, id, {
1566
+ ttlMs: DEFAULT_TRANSIENT_AGENT_STATE_TTL_MS,
1567
+ });
1568
+ if (transient) {
1569
+ const previousState = meta.activity_state;
1570
+ meta.activity_state = transient.state;
1571
+ if ((!meta.activity_since || previousState !== transient.state) && Number.isFinite(transient.updatedAt)) {
1572
+ meta.activity_since = new Date(transient.updatedAt).toISOString();
1573
+ }
1574
+ if (transient.detail) meta.activity_detail = transient.detail;
1575
+ }
1576
+ return meta;
1577
+ },
1547
1578
  getProjectRoot: () => activeProjectRoot,
1548
1579
  setDashboardView: (value) => {
1549
1580
  dashboardView = value;
@@ -1643,9 +1674,9 @@ async function runChat(projectRoot, options = {}) {
1643
1674
  appendStreamDelta,
1644
1675
  finalizeStream,
1645
1676
  hasStream: (publisher) => streamTracker.hasStream(publisher),
1646
- setTransientAgentState: (agentId, state) => {
1677
+ setTransientAgentState: (agentId, state, options) => {
1647
1678
  if (!agentId || !state) return;
1648
- setTransientAgentStateValue(transientAgentStateMap, agentId, state);
1679
+ setTransientAgentStateValue(transientAgentStateMap, agentId, state, options);
1649
1680
  },
1650
1681
  clearTransientAgentState: (agentId) => {
1651
1682
  if (!agentId) return;
@@ -1653,7 +1684,11 @@ async function runChat(projectRoot, options = {}) {
1653
1684
  },
1654
1685
  refreshDashboard: () => {
1655
1686
  if (getCurrentView() === "agent") {
1656
- renderAgentDashboard();
1687
+ if (agentViewController && typeof agentViewController.refreshAgentView === "function") {
1688
+ agentViewController.refreshAgentView();
1689
+ } else {
1690
+ renderAgentDashboard();
1691
+ }
1657
1692
  return;
1658
1693
  }
1659
1694
  renderDashboard();
@@ -6,23 +6,38 @@ function normalizeNow(now) {
6
6
  return Number.isFinite(now) ? now : Date.now();
7
7
  }
8
8
 
9
- function setTransientAgentState(store, agentId, state, now = Date.now()) {
9
+ function normalizeSetOptions(nowOrOptions, detailArg = "") {
10
+ if (nowOrOptions && typeof nowOrOptions === "object") {
11
+ return {
12
+ now: normalizeNow(nowOrOptions.now),
13
+ detail: String(nowOrOptions.detail || "").trim(),
14
+ };
15
+ }
16
+ return {
17
+ now: normalizeNow(nowOrOptions),
18
+ detail: String(detailArg || "").trim(),
19
+ };
20
+ }
21
+
22
+ function setTransientAgentState(store, agentId, state, nowOrOptions = Date.now(), detailArg = "") {
10
23
  if (!(store instanceof Map)) return;
11
24
  const id = String(agentId || "").trim();
12
25
  const nextState = String(state || "").trim();
13
26
  if (!id || !nextState) return;
27
+ const options = normalizeSetOptions(nowOrOptions, detailArg);
14
28
  store.set(id, {
15
29
  state: nextState,
16
- updatedAt: normalizeNow(now),
30
+ updatedAt: options.now,
31
+ detail: options.detail,
17
32
  });
18
33
  }
19
34
 
20
- function getTransientAgentState(store, agentId, options = {}) {
21
- if (!(store instanceof Map)) return "";
35
+ function getTransientAgentStateEntry(store, agentId, options = {}) {
36
+ if (!(store instanceof Map)) return null;
22
37
  const id = String(agentId || "").trim();
23
- if (!id) return "";
38
+ if (!id) return null;
24
39
  const entry = store.get(id);
25
- if (!entry) return "";
40
+ if (!entry) return null;
26
41
 
27
42
  const ttlMs = Number.isFinite(options.ttlMs)
28
43
  ? Math.max(0, Math.trunc(options.ttlMs))
@@ -32,16 +47,23 @@ function getTransientAgentState(store, agentId, options = {}) {
32
47
  const updatedAt = typeof entry === "object" && Number.isFinite(entry.updatedAt)
33
48
  ? entry.updatedAt
34
49
  : now;
50
+ const detail = typeof entry === "object" ? String(entry.detail || "").trim() : "";
35
51
 
36
52
  if (!state) {
37
53
  store.delete(id);
38
- return "";
54
+ return null;
39
55
  }
40
56
  if (ttlMs > 0 && now - updatedAt > ttlMs) {
41
57
  store.delete(id);
42
- return "";
58
+ return null;
43
59
  }
44
- return state;
60
+ return { state, updatedAt, detail };
61
+ }
62
+
63
+ function getTransientAgentState(store, agentId, options = {}) {
64
+ const entry = getTransientAgentStateEntry(store, agentId, options);
65
+ if (!entry) return "";
66
+ return entry.state;
45
67
  }
46
68
 
47
69
  function pruneTransientAgentStates(store, activeAgentIds = [], options = {}) {
@@ -59,6 +81,7 @@ function pruneTransientAgentStates(store, activeAgentIds = [], options = {}) {
59
81
  module.exports = {
60
82
  DEFAULT_TRANSIENT_AGENT_STATE_TTL_MS,
61
83
  setTransientAgentState,
84
+ getTransientAgentStateEntry,
62
85
  getTransientAgentState,
63
86
  pruneTransientAgentStates,
64
87
  };