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
|
@@ -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 =
|
|
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,
|
package/src/code/nativeRunner.js
CHANGED
|
@@ -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 =
|
|
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:
|
|
574
|
+
timeoutMs: 0,
|
|
574
575
|
jsonOutput: false,
|
|
575
576
|
forceTui: false,
|
|
576
577
|
disableTui: false,
|
package/src/ui/ink/UcodeApp.js
CHANGED
|
@@ -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")
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
else if (event.type === "
|
|
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 || "");
|