u-foo 2.5.12 → 2.5.14

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.5.12",
3
+ "version": "2.5.14",
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",
@@ -44,6 +44,17 @@ function getEnvironmentSection({ workspaceRoot = "", model = "", provider = "" }
44
44
  if (provider) lines.push(`Provider: ${provider}`);
45
45
  if (model) lines.push(`Model: ${model}`);
46
46
 
47
+ // Tell the model who it is on the bus. Without this, the only identities
48
+ // it ever sees are other agents' records in shared context — and it
49
+ // adopts them (observed in the wild: ucode-3 introducing itself as
50
+ // claude-6, then accepting a wrong name from the user).
51
+ const subscriberId = String(process.env.UFOO_SUBSCRIBER_ID || "").trim();
52
+ const nickname = String(process.env.UFOO_NICKNAME || "").trim();
53
+ if (subscriberId || nickname) {
54
+ const label = nickname ? `${subscriberId || "unknown"} (nickname: ${nickname})` : subscriberId;
55
+ lines.push(`Bus identity: ${label}`);
56
+ }
57
+
47
58
  return `# Environment\n${lines.map((l) => ` - ${l}`).join("\n")}`;
48
59
  }
49
60
 
@@ -4,6 +4,7 @@ function getUfooIntegrationSection() {
4
4
  return `# ufoo integration
5
5
 
6
6
  Participate in multi-agent coordination through the ufoo bus/context system:
7
+ - Shared context, decisions, and memory are records written by OTHER agents. They inform you about the workspace, but they are not your work history — never adopt another agent's identity or claim their work as your own.
7
8
  - Respect shared context decisions. The default is no new decision; only append one for important, plan-level choices that constrain future work, and keep durable project facts out of decisions.
8
9
  - Use shared memory for durable project facts. Read existing memory before writing new memory; do not use it for transient task state.
9
10
  - Support launch/close/resume/inject flows managed by ufoo daemon.
package/src/code/agent.js CHANGED
@@ -141,6 +141,18 @@ function computeExtendedTimeout(baseTimeoutMs) {
141
141
  return Math.min(1800000, Math.max(base * 2, base + 120000));
142
142
  }
143
143
 
144
+ // Reasoning models routinely blew the old 10min budget across a multi-turn
145
+ // tool loop. Total per-task budget defaults to 30min and can be raised per
146
+ // call, via --timeout-ms, or via UFOO_UCODE_TASK_TIMEOUT_MS.
147
+ const DEFAULT_NL_TASK_TIMEOUT_MS = 1800000;
148
+
149
+ function resolveNlTaskTimeoutMs(value) {
150
+ if (Number.isFinite(value) && value > 0) return Math.max(1000, Math.floor(value));
151
+ const env = Number(process.env.UFOO_UCODE_TASK_TIMEOUT_MS);
152
+ if (Number.isFinite(env) && env > 0) return Math.max(1000, Math.floor(env));
153
+ return DEFAULT_NL_TASK_TIMEOUT_MS;
154
+ }
155
+
144
156
  function enrichNativeError(errorMessage = "") {
145
157
  const text = String(errorMessage || "").trim();
146
158
  if (!text) return "nl task failed";
@@ -168,6 +180,9 @@ function enrichNativeError(errorMessage = "") {
168
180
  ) {
169
181
  return `${text}. Check provider/url/key via /settings ucode show.`;
170
182
  }
183
+ if (lower.includes("cli timeout")) {
184
+ return `${text}. Task budget exceeded; raise it with --timeout-ms or UFOO_UCODE_TASK_TIMEOUT_MS.`;
185
+ }
171
186
  return text;
172
187
  }
173
188
 
@@ -394,7 +409,7 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
394
409
  state.provider || process.env.UFOO_UCODE_PROVIDER || ""
395
410
  );
396
411
  const model = String(state.model || process.env.UFOO_UCODE_MODEL || "").trim();
397
- const timeoutMs = Number.isFinite(state.timeoutMs) ? state.timeoutMs : 600000;
412
+ const timeoutMs = resolveNlTaskTimeoutMs(state.timeoutMs);
398
413
  let streamed = false;
399
414
  let streamLastChar = "";
400
415
  let toolEventsThisAttempt = 0;
@@ -715,6 +730,8 @@ module.exports = {
715
730
  resolvePlannerProvider,
716
731
  extractJsonSummary,
717
732
  enrichNativeError,
733
+ resolveNlTaskTimeoutMs,
734
+ DEFAULT_NL_TASK_TIMEOUT_MS,
718
735
  resolveUcodeProviderModel,
719
736
  buildSessionSnapshotFromState,
720
737
  persistSessionState,
@@ -26,6 +26,11 @@ const DEFAULT_MAX_NATIVE_TOOL_ERRORS = 5;
26
26
  // via UFOO_UCODE_MAX_TOKENS (positive integer).
27
27
  const DEFAULT_OPENAI_MAX_TOKENS = 131072;
28
28
  const DEFAULT_ANTHROPIC_MAX_TOKENS = 64000;
29
+ // Extended thinking is on by default for the anthropic transport; the budget
30
+ // stays well below the 64K max_tokens cap as the Messages API requires.
31
+ // UFOO_UCODE_THINKING_BUDGET_TOKENS overrides; 0 or a non-numeric value
32
+ // disables thinking (the payload then omits the field entirely).
33
+ const DEFAULT_ANTHROPIC_THINKING_BUDGET_TOKENS = 10000;
29
34
  // Prompt caching is GA on the current Messages API: cache_control blocks need
30
35
  // no anthropic-beta header. Kept as a constant so the marker shape stays in
31
36
  // one place (system block + last history message, 2 of the 4 allowed
@@ -59,6 +64,16 @@ function resolveMaxTokens(fallback) {
59
64
  return normalizePositiveInt(process.env.UFOO_UCODE_MAX_TOKENS, fallback);
60
65
  }
61
66
 
67
+ function resolveThinkingBudgetTokens() {
68
+ const raw = process.env.UFOO_UCODE_THINKING_BUDGET_TOKENS;
69
+ if (raw === undefined || raw === null || String(raw).trim() === "") {
70
+ return DEFAULT_ANTHROPIC_THINKING_BUDGET_TOKENS;
71
+ }
72
+ const parsed = Number.parseInt(String(raw), 10);
73
+ if (!Number.isFinite(parsed) || parsed <= 0) return 0;
74
+ return Math.floor(parsed);
75
+ }
76
+
62
77
  function toUsageInt(value) {
63
78
  const parsed = Number(value);
64
79
  if (!Number.isFinite(parsed) || parsed <= 0) return 0;
@@ -794,6 +809,13 @@ function normalizeAnthropicMessageContent(raw = []) {
794
809
  text: String(item.text || ""),
795
810
  };
796
811
  }
812
+ if (item.type === "thinking") {
813
+ return {
814
+ type: "thinking",
815
+ thinking: String(item.thinking || ""),
816
+ signature: String(item.signature || ""),
817
+ };
818
+ }
797
819
  if (item.type === "tool_use") {
798
820
  return {
799
821
  type: "tool_use",
@@ -882,6 +904,10 @@ async function runAnthropicTurn({
882
904
  tools: buildAnthropicToolSpecs(),
883
905
  stream: true,
884
906
  };
907
+ const thinkingBudget = resolveThinkingBudgetTokens();
908
+ if (thinkingBudget > 0) {
909
+ payload.thinking = { type: "enabled", budget_tokens: thinkingBudget };
910
+ }
885
911
  const systemText = String(systemPrompt || "").trim();
886
912
  if (systemText) {
887
913
  // Block form with a cache breakpoint; the system prompt is the most
@@ -994,6 +1020,7 @@ async function runAnthropicTurn({
994
1020
  order: index,
995
1021
  type: "thinking",
996
1022
  text: String(contentBlock.thinking || ""),
1023
+ signature: String(contentBlock.signature || ""),
997
1024
  });
998
1025
  } else if (contentBlock.type === "tool_use") {
999
1026
  blockMap.set(index, {
@@ -1058,6 +1085,16 @@ async function runAnthropicTurn({
1058
1085
  return;
1059
1086
  }
1060
1087
 
1088
+ if (delta.type === "signature_delta") {
1089
+ // Signed thinking blocks must be replayed verbatim on later turns
1090
+ // (tool-use continuation contract), so accumulate the signature
1091
+ // alongside the thinking text.
1092
+ current.type = "thinking";
1093
+ current.signature = `${String(current.signature || "")}${String(delta.signature || "")}`;
1094
+ blockMap.set(index, current);
1095
+ return;
1096
+ }
1097
+
1061
1098
  if (delta.type === "input_json_delta") {
1062
1099
  current.type = "tool_use";
1063
1100
  current.inputJson = `${String(current.inputJson || "")}${String(delta.partial_json || "")}`;
@@ -1069,8 +1106,17 @@ async function runAnthropicTurn({
1069
1106
  buildResult: () => {
1070
1107
  const assistantContent = Array.from(blockMap.values())
1071
1108
  .sort((a, b) => a.order - b.order)
1072
- .filter((item) => item.type !== "thinking")
1073
1109
  .map((item) => {
1110
+ if (item.type === "thinking") {
1111
+ // Kept (with signature) so tool-use continuation turns can
1112
+ // replay the thinking blocks the API requires.
1113
+ return {
1114
+ type: "thinking",
1115
+ thinking: String(item.text || ""),
1116
+ signature: String(item.signature || ""),
1117
+ };
1118
+ }
1119
+
1074
1120
  if (item.type === "text") {
1075
1121
  return {
1076
1122
  type: "text",
package/src/code/repl.js CHANGED
@@ -249,7 +249,7 @@ async function runUcodeCoreAgent({
249
249
  appendSystemPrompt = "",
250
250
  systemPrompt = "",
251
251
  sessionId = "",
252
- timeoutMs = 600000,
252
+ timeoutMs = 0,
253
253
  jsonOutput = false,
254
254
  forceTui = false,
255
255
  disableTui = false,
@@ -261,6 +261,7 @@ async function runUcodeCoreAgent({
261
261
  formatNlResult,
262
262
  persistSessionState,
263
263
  resumeSessionState,
264
+ resolveNlTaskTimeoutMs,
264
265
  resolveUcodeProviderModel,
265
266
  runNaturalLanguageTask,
266
267
  } = require("./agent");
@@ -284,7 +285,7 @@ async function runUcodeCoreAgent({
284
285
  }),
285
286
  nlMessages: [],
286
287
  sessionId: resolveSessionId(String(sessionId || "").trim()),
287
- timeoutMs,
288
+ timeoutMs: resolveNlTaskTimeoutMs(Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : NaN),
288
289
  jsonOutput,
289
290
  };
290
291
  persistSessionState(state);
@@ -570,7 +571,7 @@ function parseAgentArgs(argv = []) {
570
571
  appendSystemPrompt: "",
571
572
  systemPrompt: "",
572
573
  sessionId: "",
573
- timeoutMs: 600000,
574
+ timeoutMs: 0,
574
575
  jsonOutput: false,
575
576
  forceTui: false,
576
577
  disableTui: false,
@@ -18,6 +18,27 @@ const { runInk } = require("../runInk");
18
18
  const fmt = require("../format");
19
19
  const { createMultilineInput } = require("./MultilineInput");
20
20
 
21
+ // Throttle for the live thinking-chain status line: rapid thinking_delta
22
+ // chunks would otherwise re-render the footer on every SSE event.
23
+ const THINKING_STATUS_THROTTLE_MS = 120;
24
+
25
+ // Log line kinds drive the color treatment of scrollback rows. Kind is pure
26
+ // presentation metadata — the stored text never changes.
27
+ const LOG_LINE_TEXT_PROPS = {
28
+ user: { color: "green", bold: true },
29
+ assistant: {},
30
+ system: { color: "gray", dimColor: true },
31
+ error: { color: "red" },
32
+ toolDetail: { color: "gray", dimColor: true },
33
+ bus: { color: "cyan" },
34
+ };
35
+
36
+ // Resolve a log line kind to ink <Text> props. Unknown/missing kinds (e.g.
37
+ // the banner, which already carries chalk ANSI styling) render uncolored.
38
+ function resolveLogLineTextProps(kind) {
39
+ return LOG_LINE_TEXT_PROPS[kind] || LOG_LINE_TEXT_PROPS.assistant;
40
+ }
41
+
21
42
  function createUcodeApp({ React, ink, props, interactive = true }) {
22
43
  const { useEffect, useState, useCallback, useRef } = React;
23
44
  const { Box, Text, useInput, useApp, useStdout } = ink;
@@ -79,6 +100,13 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
79
100
  const lineSeqRef = useRef(banner.length + 1);
80
101
  const mergeIdRef = useRef(0);
81
102
  const toolMergeScopeRef = useRef(0);
103
+ // thinkingTailRef accumulates raw thinking_delta text for the live
104
+ // status line; the collapsed tail is pushed through a throttled
105
+ // trailing flush (thinkingTimerRef) so fast streams don't re-render
106
+ // the footer on every chunk.
107
+ const thinkingTailRef = useRef("");
108
+ const thinkingFlushAtRef = useRef(0);
109
+ const thinkingTimerRef = useRef(null);
82
110
 
83
111
  const targetAgent = agentSelectionMode && selectedAgentIndex >= 0
84
112
  ? agents[selectedAgentIndex]
@@ -225,11 +253,11 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
225
253
  setSelectedAgentIndex(next);
226
254
  }, [agents, agentSelectionMode, selectedAgentIndex]);
227
255
 
228
- const appendLogLine = useCallback((text) => {
256
+ const appendLogLine = useCallback((text, kind = "assistant") => {
229
257
  setLogLines((prev) => {
230
258
  const id = `l-${lineSeqRef.current}`;
231
259
  lineSeqRef.current += 1;
232
- const next = prev.concat([{ id, text: String(text || "") }]);
260
+ const next = prev.concat([{ id, text: String(text || ""), kind }]);
233
261
  return next.length > 1000 ? next.slice(-1000) : next;
234
262
  });
235
263
  }, []);
@@ -273,7 +301,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
273
301
  });
274
302
  }, []);
275
303
 
276
- const appendLogText = useCallback((text) => {
304
+ const appendLogText = useCallback((text, kind = "assistant") => {
277
305
  // Multi-line text → split into separate log entries so <Static> keys
278
306
  // stay stable when streaming arrives line-by-line. Always promote any
279
307
  // in-flight tool group first so it freezes above the new text.
@@ -281,7 +309,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
281
309
  if (!raw) return;
282
310
  flushActiveMerge();
283
311
  const lines = raw.split(/\r?\n/);
284
- for (const line of lines) appendLogLine(line);
312
+ for (const line of lines) appendLogLine(line, kind);
285
313
  }, [appendLogLine, flushActiveMerge]);
286
314
 
287
315
  const expandLastMerge = useCallback(() => {
@@ -299,7 +327,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
299
327
  const lines = fmt.buildMergedToolExpandedLines(candidate.entries);
300
328
  for (let i = 0; i < lines.length; i += 1) {
301
329
  const branch = i === lines.length - 1 ? "└" : "│";
302
- appendLogLine(`${branch} ${lines[i]}`);
330
+ appendLogLine(`${branch} ${lines[i]}`, "toolDetail");
303
331
  }
304
332
  candidate.expanded = true;
305
333
  if (active && active.id === candidate.id) setActiveMerge(null);
@@ -315,7 +343,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
315
343
  if (!normalized) return;
316
344
  toolMergeScopeRef.current += 1;
317
345
  flushActiveMerge();
318
- appendLogLine(`› ${normalized}`);
346
+ appendLogLine(`› ${normalized}`, "user");
319
347
 
320
348
  const runtimeWorkspace = String(
321
349
  (props.state && props.state.workspaceRoot) || props.workspaceRoot || process.cwd()
@@ -325,7 +353,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
325
353
  try {
326
354
  result = props.runSingleCommand(normalized, runtimeWorkspace);
327
355
  } catch (err) {
328
- appendLogText(`Error: ${err && err.message ? err.message : "command parse failed"}`);
356
+ appendLogText(`Error: ${err && err.message ? err.message : "command parse failed"}`, "error");
329
357
  return;
330
358
  }
331
359
  if (!result || typeof result !== "object") return;
@@ -350,26 +378,26 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
350
378
  workspaceRoot: runtimeWorkspace,
351
379
  onMessageReceived: (msg) => {
352
380
  const nickname = extractAgentNickname(msg && msg.from) || (msg && msg.from) || "bus";
353
- appendLogText(`${nickname}: ${(msg && msg.task) || ""}`);
381
+ appendLogText(`${nickname}: ${(msg && msg.task) || ""}`, "bus");
354
382
  },
355
383
  });
356
384
  if (!ubusResult || !ubusResult.ok) {
357
- appendLogText(`Error: ${(ubusResult && ubusResult.error) || "ubus failed"}`);
385
+ appendLogText(`Error: ${(ubusResult && ubusResult.error) || "ubus failed"}`, "error");
358
386
  return;
359
387
  }
360
388
  const exchanges = Array.isArray(ubusResult.messageExchanges) ? ubusResult.messageExchanges : [];
361
389
  if (exchanges.length > 0) {
362
390
  for (const exchange of exchanges) {
363
391
  const nickname = extractAgentNickname(exchange && exchange.from) || (exchange && exchange.from) || "bus";
364
- appendLogText(`@${nickname} ${(exchange && exchange.reply) || ""}`);
392
+ appendLogText(`@${nickname} ${(exchange && exchange.reply) || ""}`, "bus");
365
393
  }
366
394
  } else if (Number(ubusResult.handled) === 0) {
367
- appendLogText("ubus: no pending messages.");
395
+ appendLogText("ubus: no pending messages.", "system");
368
396
  }
369
397
  if (typeof props.persistSessionState === "function") {
370
398
  const persisted = props.persistSessionState(props.state);
371
399
  if (!persisted || persisted.ok === false) {
372
- appendLogText(`Error: failed to persist session ${(props.state && props.state.sessionId) || ""}: ${(persisted && persisted.error) || "unknown error"}`);
400
+ appendLogText(`Error: failed to persist session ${(props.state && props.state.sessionId) || ""}: ${(persisted && persisted.error) || "unknown error"}`, "error");
373
401
  }
374
402
  }
375
403
  } finally {
@@ -379,15 +407,15 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
379
407
  }
380
408
  case "resume": {
381
409
  if (typeof props.resumeSessionState !== "function") {
382
- appendLogText("Error: resume unsupported");
410
+ appendLogText("Error: resume unsupported", "error");
383
411
  return;
384
412
  }
385
413
  const resumed = props.resumeSessionState(props.state, result.sessionId, runtimeWorkspace);
386
414
  if (!resumed || !resumed.ok) {
387
- appendLogText(`Error: ${(resumed && resumed.error) || "resume failed"}`);
415
+ appendLogText(`Error: ${(resumed && resumed.error) || "resume failed"}`, "error");
388
416
  return;
389
417
  }
390
- appendLogText(`Resumed session ${resumed.sessionId} (${resumed.restoredMessages} messages).`);
418
+ appendLogText(`Resumed session ${resumed.sessionId} (${resumed.restoredMessages} messages).`, "system");
391
419
  return;
392
420
  }
393
421
  case "tool": {
@@ -413,7 +441,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
413
441
  backgroundTasksRef.current.set(jobId, taskRecord);
414
442
  bumpBackground();
415
443
  setStatus({ message: "", type: "thinking", showTimer: false, startedAt: 0 });
416
- appendLogText(`[${jobId}] started in background.`);
444
+ appendLogText(`[${jobId}] started in background.`, "system");
417
445
 
418
446
  const bgState = {
419
447
  workspaceRoot: props.state && props.state.workspaceRoot,
@@ -434,13 +462,13 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
434
462
  taskRecord.finishedAt = Date.now();
435
463
  taskRecord.summary = String(props.formatNlResult(nlResult, false) || "").trim();
436
464
  const title = taskRecord.status === "done" ? "done" : "failed";
437
- appendLogText(`[${jobId}] ${title}: ${taskRecord.summary || "no summary"}`);
465
+ appendLogText(`[${jobId}] ${title}: ${taskRecord.summary || "no summary"}`, "system");
438
466
  })
439
467
  .catch((err) => {
440
468
  taskRecord.status = "failed";
441
469
  taskRecord.finishedAt = Date.now();
442
470
  taskRecord.summary = err && err.message ? String(err.message) : "background task failed";
443
- appendLogText(`[${jobId}] failed: ${taskRecord.summary}`);
471
+ appendLogText(`[${jobId}] failed: ${taskRecord.summary}`, "system");
444
472
  })
445
473
  .finally(() => {
446
474
  bumpBackground();
@@ -458,6 +486,16 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
458
486
  showTimer: true,
459
487
  startedAt,
460
488
  });
489
+ const cancelThinkingFlush = () => {
490
+ if (thinkingTimerRef.current) {
491
+ clearTimeout(thinkingTimerRef.current);
492
+ thinkingTimerRef.current = null;
493
+ }
494
+ };
495
+ const flushThinkingStatus = () => {
496
+ thinkingFlushAtRef.current = Date.now();
497
+ setNlStatus(collapseThinkingTail(thinkingTailRef.current) || "Thinking...");
498
+ };
461
499
  setNlStatus("Waiting for model...");
462
500
  let streamBuf = "";
463
501
  let sawStreamText = false;
@@ -469,10 +507,28 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
469
507
  signal: abortController.signal,
470
508
  onPhase: (event) => {
471
509
  if (!event || typeof event !== "object") return;
472
- if (event.type === "request_start") setNlStatus("Waiting for model...");
473
- else if (event.type === "thinking_delta") setNlStatus("Thinking...");
474
- else if (event.type === "text_delta") setNlStatus("Generating response...");
475
- else if (event.type === "tool_request") {
510
+ if (event.type === "request_start") {
511
+ cancelThinkingFlush();
512
+ setNlStatus("Waiting for model...");
513
+ } else if (event.type === "thinking_delta") {
514
+ thinkingTailRef.current += String(event.text || "");
515
+ const elapsed = Date.now() - thinkingFlushAtRef.current;
516
+ if (elapsed >= THINKING_STATUS_THROTTLE_MS) {
517
+ cancelThinkingFlush();
518
+ flushThinkingStatus();
519
+ } else if (!thinkingTimerRef.current) {
520
+ // Trailing flush guarantees the final tail lands even
521
+ // when the stream ends inside a throttle window.
522
+ thinkingTimerRef.current = setTimeout(() => {
523
+ thinkingTimerRef.current = null;
524
+ flushThinkingStatus();
525
+ }, THINKING_STATUS_THROTTLE_MS - elapsed);
526
+ }
527
+ } else if (event.type === "text_delta") {
528
+ cancelThinkingFlush();
529
+ setNlStatus("Generating response...");
530
+ } else if (event.type === "tool_request") {
531
+ cancelThinkingFlush();
476
532
  const label = fmt.TOOL_LABELS[String(event.name || "").toLowerCase()] ||
477
533
  `Calling ${event.name}`;
478
534
  setNlStatus(`${label}...`);
@@ -509,10 +565,12 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
509
565
  },
510
566
  });
511
567
  } catch (err) {
512
- appendLogText(`Error: ${err && err.message ? err.message : "agent loop failed"}`);
568
+ appendLogText(`Error: ${err && err.message ? err.message : "agent loop failed"}`, "error");
513
569
  return;
514
570
  } finally {
515
571
  pendingTaskRef.current = null;
572
+ cancelThinkingFlush();
573
+ thinkingTailRef.current = "";
516
574
  setStatus({ message: "", type: "thinking", showTimer: false, startedAt: 0 });
517
575
  }
518
576
  if (streamBuf) {
@@ -534,7 +592,8 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
534
592
  const persisted = props.persistSessionState(props.state);
535
593
  if (persisted && persisted.ok === false) {
536
594
  appendLogText(
537
- `Error: failed to persist session ${(props.state && props.state.sessionId) || ""}: ${persisted.error || "unknown error"}`
595
+ `Error: failed to persist session ${(props.state && props.state.sessionId) || ""}: ${persisted.error || "unknown error"}`,
596
+ "error"
538
597
  );
539
598
  }
540
599
  } catch {
@@ -579,7 +638,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
579
638
  signal: abortController.signal,
580
639
  onMessageReceived: (msg) => {
581
640
  const nickname = extractAgentNickname(msg && msg.from) || (msg && msg.from) || "bus";
582
- appendLogText(`${nickname}: ${(msg && msg.task) || ""}`);
641
+ appendLogText(`${nickname}: ${(msg && msg.task) || ""}`, "bus");
583
642
  setStatus({
584
643
  message: "Working on task...",
585
644
  type: "thinking",
@@ -593,7 +652,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
593
652
  const nextError = String((ubusResult && ubusResult.error) || "ubus failed");
594
653
  if (nextError !== autoBusErrorRef.current) {
595
654
  autoBusErrorRef.current = nextError;
596
- appendLogText(`Error: ${nextError}`);
655
+ appendLogText(`Error: ${nextError}`, "error");
597
656
  }
598
657
  return;
599
658
  }
@@ -602,12 +661,12 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
602
661
  const exchanges = Array.isArray(ubusResult.messageExchanges) ? ubusResult.messageExchanges : [];
603
662
  for (const exchange of exchanges) {
604
663
  const nickname = extractAgentNickname(exchange && exchange.from) || (exchange && exchange.from) || "bus";
605
- appendLogText(`@${nickname} ${(exchange && exchange.reply) || ""}`);
664
+ appendLogText(`@${nickname} ${(exchange && exchange.reply) || ""}`, "bus");
606
665
  }
607
666
  if (Number(ubusResult.handled) > 0 && typeof props.persistSessionState === "function") {
608
667
  const persisted = props.persistSessionState(props.state);
609
668
  if (!persisted || persisted.ok === false) {
610
- appendLogText(`Error: failed to persist session ${(props.state && props.state.sessionId) || ""}: ${(persisted && persisted.error) || "unknown error"}`);
669
+ appendLogText(`Error: failed to persist session ${(props.state && props.state.sessionId) || ""}: ${(persisted && persisted.error) || "unknown error"}`, "error");
611
670
  }
612
671
  }
613
672
  } finally {
@@ -627,7 +686,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
627
686
  autoBusQueuedRef.current = true;
628
687
  runChainRef.current = runChainRef.current
629
688
  .then(() => runAutoBusOnce())
630
- .catch((err) => appendLogText(`Error: ${err && err.message ? err.message : "ubus failed"}`))
689
+ .catch((err) => appendLogText(`Error: ${err && err.message ? err.message : "ubus failed"}`, "error"))
631
690
  .finally(() => {
632
691
  autoBusQueuedRef.current = false;
633
692
  });
@@ -651,7 +710,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
651
710
  // Serialize executions so streaming tasks don't interleave.
652
711
  runChainRef.current = runChainRef.current
653
712
  .then(() => executeLine(value))
654
- .catch((err) => appendLogText(`Error: ${err && err.message ? err.message : err}`));
713
+ .catch((err) => appendLogText(`Error: ${err && err.message ? err.message : err}`, "error"));
655
714
  }, [draft, executeLine, appendLogText]);
656
715
 
657
716
  useEffect(() => {
@@ -690,7 +749,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
690
749
  return h(Box, { flexDirection: "column", width: "100%" },
691
750
  h(Box, { flexDirection: "column", width: "100%" },
692
751
  ...logLines.map((item) =>
693
- h(Text, { key: item.id }, item.text || " ")
752
+ h(Text, { key: item.id, ...resolveLogLineTextProps(item.kind) }, item.text || " ")
694
753
  )
695
754
  ),
696
755
  activeMerge ? h(Box, null,
@@ -716,7 +775,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
716
775
  const pending = pendingTaskRef.current;
717
776
  if (pending && pending.abortController && !pending.abortController.signal.aborted) {
718
777
  try { pending.abortController.abort(); } catch { /* ignore */ }
719
- appendLogLine("⚙ Cancellation requested. Stopping the current task...");
778
+ appendLogLine("⚙ Cancellation requested. Stopping the current task...", "system");
720
779
  setStatus({
721
780
  message: "Cancelling...",
722
781
  type: "waiting",
@@ -813,7 +872,7 @@ function runUcodeInkTui(props = {}) {
813
872
  });
814
873
  }
815
874
 
816
- module.exports = { runUcodeInkTui, createUcodeApp, computeStatusText };
875
+ module.exports = { runUcodeInkTui, createUcodeApp, computeStatusText, collapseThinkingTail, resolveLogLineTextProps };
817
876
 
818
877
  function inferStatusType(text = "", requestedType = "") {
819
878
  const type = String(requestedType || "").trim().toLowerCase();
@@ -837,6 +896,14 @@ function inferStatusType(text = "", requestedType = "") {
837
896
  * combination while a task is in flight, mirroring updateStatus() in the
838
897
  * blessed implementation.
839
898
  */
899
+ function collapseThinkingTail(text, maxChars = 80) {
900
+ const collapsed = String(text || "").replace(/\s+/g, " ").trim();
901
+ const parsed = Number(maxChars);
902
+ const limit = Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 80;
903
+ if (collapsed.length <= limit) return collapsed;
904
+ return collapsed.slice(collapsed.length - limit);
905
+ }
906
+
840
907
  function computeStatusText(status, spinnerTick, backgroundSuffix = "") {
841
908
  const message = String((status && status.message) || "");
842
909
  const suffix = String(backgroundSuffix || "");