u-foo 1.9.8 → 2.1.0

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.
Files changed (68) hide show
  1. package/package.json +2 -4
  2. package/src/agent/claudeEventTranslator.js +267 -0
  3. package/src/agent/claudeOauthTokenReader.js +52 -0
  4. package/src/agent/claudeThreadProvider.js +343 -0
  5. package/src/agent/cliRunner.js +4 -16
  6. package/src/agent/codexEventTranslator.js +78 -0
  7. package/src/agent/codexThreadProvider.js +181 -0
  8. package/src/agent/controllerToolExecutor.js +233 -0
  9. package/src/agent/credentials/claude.js +324 -0
  10. package/src/agent/credentials/codex.js +203 -0
  11. package/src/agent/credentials/index.js +106 -0
  12. package/src/agent/internalRunner.js +333 -2
  13. package/src/agent/loopObservability.js +190 -0
  14. package/src/agent/loopRuntime.js +457 -0
  15. package/src/agent/ufooAgent.js +178 -120
  16. package/src/agent/upstreamTransport.js +464 -0
  17. package/src/bus/utils.js +3 -2
  18. package/src/chat/dashboardView.js +51 -1
  19. package/src/chat/index.js +3 -1
  20. package/src/config.js +53 -17
  21. package/src/controller/flags.js +160 -0
  22. package/src/controller/gateRouter.js +201 -0
  23. package/src/controller/routerFastPath.js +22 -0
  24. package/src/controller/shadowGuard.js +280 -0
  25. package/src/daemon/index.js +2 -3
  26. package/src/daemon/promptLoop.js +33 -224
  27. package/src/daemon/promptRequest.js +360 -5
  28. package/src/daemon/status.js +2 -0
  29. package/src/history/inputTimeline.js +9 -4
  30. package/src/memory/index.js +24 -0
  31. package/src/providerapi/redactor.js +87 -0
  32. package/src/providerapi/shadowDiff.js +174 -0
  33. package/src/report/store.js +4 -3
  34. package/src/tools/handlers/ackBus.js +26 -0
  35. package/src/tools/handlers/common.js +64 -0
  36. package/src/tools/handlers/dispatchMessage.js +81 -0
  37. package/src/tools/handlers/listAgents.js +14 -0
  38. package/src/tools/handlers/readBusSummary.js +34 -0
  39. package/src/tools/handlers/readOpenDecisions.js +26 -0
  40. package/src/tools/handlers/readProjectRegistry.js +20 -0
  41. package/src/tools/handlers/readPromptHistory.js +123 -0
  42. package/src/tools/handlers/tier2.js +134 -0
  43. package/src/tools/index.js +55 -0
  44. package/src/tools/registry.js +69 -0
  45. package/src/tools/schemaFixtures.js +415 -0
  46. package/src/tools/tier0/listAgents.js +14 -0
  47. package/src/tools/tier0/readBusSummary.js +14 -0
  48. package/src/tools/tier0/readOpenDecisions.js +14 -0
  49. package/src/tools/tier0/readProjectRegistry.js +14 -0
  50. package/src/tools/tier0/readPromptHistory.js +14 -0
  51. package/src/tools/tier1/ackBus.js +14 -0
  52. package/src/tools/tier1/dispatchMessage.js +14 -0
  53. package/src/tools/tier1/routeAgent.js +14 -0
  54. package/src/tools/tier2/closeAgent.js +14 -0
  55. package/src/tools/tier2/launchAgent.js +14 -0
  56. package/src/tools/tier2/manageCron.js +14 -0
  57. package/src/tools/tier2/renameAgent.js +14 -0
  58. package/src/tools/types.js +75 -0
  59. package/src/tools/unimplemented.js +13 -0
  60. package/src/ufoo/paths.js +4 -0
  61. package/bin/ufoo-assistant-agent.js +0 -5
  62. package/bin/ufoo-engine.js +0 -25
  63. package/src/assistant/agent.js +0 -261
  64. package/src/assistant/bridge.js +0 -178
  65. package/src/assistant/constants.js +0 -15
  66. package/src/assistant/engine.js +0 -252
  67. package/src/assistant/stdio.js +0 -58
  68. package/src/assistant/ufooEngineCli.js +0 -312
@@ -0,0 +1,464 @@
1
+ "use strict";
2
+
3
+ const { randomUUID } = require("crypto");
4
+ const { loadConfig } = require("../config");
5
+ const {
6
+ resolveRuntimeConfig,
7
+ resolveCompletionUrl,
8
+ resolveAnthropicMessagesUrl,
9
+ } = require("../code/nativeRunner");
10
+ const { resolveClaudeUpstreamCredentials } = require("./credentials/claude");
11
+ const { resolveCodexUpstreamCredentials } = require("./credentials/codex");
12
+ const { buildUpstreamAuthFromCredential } = require("./credentials");
13
+
14
+ function normalizeProvider(value = "") {
15
+ const text = String(value || "").trim().toLowerCase();
16
+ if (!text) return "ucode";
17
+ if (text === "codex-cli" || text === "codex-code" || text === "codex" || text === "openai") return "codex";
18
+ if (text === "claude-cli" || text === "claude-code" || text === "claude" || text === "anthropic") return "claude";
19
+ if (text === "ucode") return "ucode";
20
+ return text;
21
+ }
22
+
23
+ function clipText(value = "", maxChars = 500) {
24
+ const text = String(value || "");
25
+ if (text.length <= maxChars) return text;
26
+ return `${text.slice(0, maxChars)}...[truncated]`;
27
+ }
28
+
29
+ const CODEX_DEFAULT_BASE_URL = "https://chatgpt.com/backend-api/codex";
30
+ const CODEX_DEFAULT_USER_AGENT = "codex-tui/0.118.0 (Mac OS 26.3.1; arm64) iTerm.app/3.6.9 (codex-tui; 0.118.0)";
31
+ const CODEX_DEFAULT_ORIGINATOR = "codex-tui";
32
+
33
+ function buildOpenAiChatRequest({
34
+ model = "",
35
+ systemPrompt = "",
36
+ prompt = "",
37
+ messages = [],
38
+ tools = [],
39
+ temperature = 0,
40
+ } = {}) {
41
+ const requestMessages = Array.isArray(messages) ? messages.map((message) => ({ ...message })) : [];
42
+ if (!requestMessages.length) {
43
+ if (systemPrompt) requestMessages.push({ role: "system", content: String(systemPrompt) });
44
+ requestMessages.push({ role: "user", content: String(prompt || "") });
45
+ }
46
+ const request = {
47
+ model: String(model || "").trim(),
48
+ messages: requestMessages,
49
+ temperature,
50
+ };
51
+ if (Array.isArray(tools) && tools.length > 0) {
52
+ request.tools = tools.slice();
53
+ }
54
+ return request;
55
+ }
56
+
57
+ function buildAnthropicMessagesRequest({
58
+ model = "",
59
+ systemPrompt = "",
60
+ prompt = "",
61
+ messages = [],
62
+ tools = [],
63
+ maxTokens = 4096,
64
+ temperature = 0,
65
+ } = {}) {
66
+ const requestMessages = Array.isArray(messages) ? messages.map((message) => ({ ...message })) : [];
67
+ if (!requestMessages.length) {
68
+ requestMessages.push({ role: "user", content: String(prompt || "") });
69
+ }
70
+ const request = {
71
+ model: String(model || "").trim(),
72
+ max_tokens: maxTokens,
73
+ messages: requestMessages,
74
+ temperature,
75
+ };
76
+ if (systemPrompt) request.system = systemPrompt;
77
+ if (Array.isArray(tools) && tools.length > 0) {
78
+ request.tools = tools.slice();
79
+ }
80
+ return request;
81
+ }
82
+
83
+ function normalizeCodexContentPart(role = "user", text = "") {
84
+ return {
85
+ type: role === "assistant" ? "output_text" : "input_text",
86
+ text: String(text || ""),
87
+ };
88
+ }
89
+
90
+ function normalizeCodexMessage(role = "user", content = "") {
91
+ return {
92
+ type: "message",
93
+ role: role === "system" ? "developer" : role,
94
+ content: [normalizeCodexContentPart(role, content)],
95
+ };
96
+ }
97
+
98
+ function buildCodexResponsesRequest({
99
+ model = "",
100
+ systemPrompt = "",
101
+ prompt = "",
102
+ messages = [],
103
+ } = {}) {
104
+ const input = [];
105
+ const history = Array.isArray(messages) ? messages : [];
106
+ for (const message of history) {
107
+ if (!message || typeof message !== "object") continue;
108
+ const role = String(message.role || "user").trim() || "user";
109
+ let content = message.content;
110
+ if (Array.isArray(content)) {
111
+ content = content
112
+ .map((item) => {
113
+ if (!item || typeof item !== "object") return "";
114
+ return String(item.text || item.content || "");
115
+ })
116
+ .join("");
117
+ }
118
+ input.push(normalizeCodexMessage(role, content));
119
+ }
120
+ input.push(normalizeCodexMessage("user", prompt));
121
+
122
+ return {
123
+ model: String(model || "").trim(),
124
+ instructions: String(systemPrompt || ""),
125
+ stream: true,
126
+ store: false,
127
+ parallel_tool_calls: true,
128
+ include: ["reasoning.encrypted_content"],
129
+ reasoning: {
130
+ effort: "medium",
131
+ summary: "auto",
132
+ },
133
+ input,
134
+ };
135
+ }
136
+
137
+ function resolveCodexResponseOutput(response = {}) {
138
+ const output = Array.isArray(response.output) ? response.output : [];
139
+ return output
140
+ .filter((item) => item && item.type === "message")
141
+ .flatMap((item) => (Array.isArray(item.content) ? item.content : []))
142
+ .filter((part) => part && part.type === "output_text")
143
+ .map((part) => String(part.text || ""))
144
+ .join("");
145
+ }
146
+
147
+ function parseCodexSsePayload(payload = "") {
148
+ const lines = String(payload || "").split(/\r?\n/);
149
+ const chunks = [];
150
+ let text = "";
151
+ let responseObject = null;
152
+ let usage = null;
153
+
154
+ for (const line of lines) {
155
+ const trimmed = line.trim();
156
+ if (!trimmed.startsWith("data:")) continue;
157
+ const dataText = trimmed.slice(5).trim();
158
+ if (!dataText || dataText === "[DONE]") continue;
159
+ let event;
160
+ try {
161
+ event = JSON.parse(dataText);
162
+ } catch {
163
+ continue;
164
+ }
165
+ chunks.push(event);
166
+ const type = String(event.type || "");
167
+ if (type === "response.output_text.delta" && typeof event.delta === "string") {
168
+ text += event.delta;
169
+ continue;
170
+ }
171
+ if (type === "response.output_item.done" && event.item && event.item.type === "message" && !text) {
172
+ const content = Array.isArray(event.item.content) ? event.item.content : [];
173
+ text += content
174
+ .filter((part) => part && part.type === "output_text")
175
+ .map((part) => String(part.text || ""))
176
+ .join("");
177
+ continue;
178
+ }
179
+ if (type === "response.completed" && event.response && typeof event.response === "object") {
180
+ responseObject = event.response;
181
+ usage = event.response.usage && typeof event.response.usage === "object"
182
+ ? event.response.usage
183
+ : null;
184
+ if (!text) {
185
+ text = resolveCodexResponseOutput(event.response);
186
+ }
187
+ }
188
+ }
189
+
190
+ return {
191
+ text: String(text || "").trim(),
192
+ response: responseObject,
193
+ usage,
194
+ events: chunks,
195
+ };
196
+ }
197
+
198
+ async function resolveUpstreamRuntime({
199
+ projectRoot,
200
+ provider = "",
201
+ model = "",
202
+ env = process.env,
203
+ loadConfigImpl = loadConfig,
204
+ } = {}) {
205
+ const normalizedProvider = normalizeProvider(provider);
206
+ const config = loadConfigImpl(projectRoot);
207
+
208
+ if (normalizedProvider === "codex") {
209
+ const credential = await resolveCodexUpstreamCredentials({
210
+ authPath: config.codexAuthPath,
211
+ env,
212
+ });
213
+ const useCodexResponses = credential.credentialKind === "oauth" && Boolean(credential.accessToken);
214
+ const baseUrl = useCodexResponses
215
+ ? String(env.UFOO_CODEX_BASE_URL || "").trim() || CODEX_DEFAULT_BASE_URL
216
+ : String(env.OPENAI_BASE_URL || "").trim() || "https://api.openai.com/v1";
217
+ const resolvedModel = String(model || config.routerModel || config.agentModel || "").trim();
218
+ return {
219
+ provider: "codex",
220
+ transport: useCodexResponses ? "codex-responses" : "openai-chat",
221
+ model: resolvedModel,
222
+ baseUrl,
223
+ credential,
224
+ auth: buildUpstreamAuthFromCredential(credential),
225
+ credentialSource: String(credential.source || ""),
226
+ };
227
+ }
228
+
229
+ if (normalizedProvider === "claude") {
230
+ const credential = await resolveClaudeUpstreamCredentials({
231
+ profile: config.claudeOauthProfile,
232
+ tokenPath: config.claudeOauthTokenPath,
233
+ refreshWindowMs: Number(config.claudeOauthRefreshWindowSec || 300) * 1000,
234
+ env,
235
+ });
236
+ const baseUrl = String(env.ANTHROPIC_BASE_URL || "").trim() || "https://api.anthropic.com/v1";
237
+ const resolvedModel = String(model || config.routerModel || config.agentModel || "").trim();
238
+ return {
239
+ provider: "claude",
240
+ transport: "anthropic-messages",
241
+ model: resolvedModel,
242
+ baseUrl,
243
+ credential,
244
+ auth: buildUpstreamAuthFromCredential(credential),
245
+ credentialSource: String(credential.source || ""),
246
+ };
247
+ }
248
+
249
+ const runtime = resolveRuntimeConfig({
250
+ workspaceRoot: projectRoot,
251
+ provider: normalizedProvider === "ucode" ? "" : normalizedProvider,
252
+ model,
253
+ });
254
+ const auth = runtime.apiKey ? { apiKey: String(runtime.apiKey || "").trim() } : { headers: {} };
255
+ return {
256
+ provider: String(runtime.provider || normalizedProvider || "ucode"),
257
+ transport: String(runtime.transport || "openai-chat"),
258
+ model: String(runtime.model || "").trim(),
259
+ baseUrl: String(runtime.baseUrl || "").trim(),
260
+ credential: null,
261
+ auth,
262
+ credentialSource: runtime.apiKey ? "runtime-api-key" : "",
263
+ };
264
+ }
265
+
266
+ async function sendUpstreamRequest({
267
+ runtime,
268
+ request,
269
+ timeoutMs = 120000,
270
+ fetchImpl = global.fetch,
271
+ } = {}) {
272
+ if (typeof fetchImpl !== "function") {
273
+ return { ok: false, error: "fetch is unavailable" };
274
+ }
275
+ const resolvedRuntime = runtime && typeof runtime === "object" ? runtime : {};
276
+ const requestModel = String((request && request.model) || resolvedRuntime.model || "").trim();
277
+ if (!requestModel) {
278
+ return { ok: false, error: `${resolvedRuntime.provider || "provider"} model is not configured` };
279
+ }
280
+
281
+ const isAnthropic = resolvedRuntime.transport === "anthropic-messages";
282
+ const isCodexResponses = resolvedRuntime.transport === "codex-responses";
283
+ const url = isAnthropic
284
+ ? resolveAnthropicMessagesUrl(resolvedRuntime.baseUrl)
285
+ : isCodexResponses
286
+ ? `${String(resolvedRuntime.baseUrl || "").replace(/\/+$/, "")}/responses`
287
+ : resolveCompletionUrl(resolvedRuntime.baseUrl);
288
+
289
+ if (!url) {
290
+ return { ok: false, error: `${resolvedRuntime.provider || "provider"} baseUrl is not configured` };
291
+ }
292
+
293
+ const headers = { "content-type": "application/json" };
294
+ if (resolvedRuntime.auth && resolvedRuntime.auth.headers && typeof resolvedRuntime.auth.headers === "object") {
295
+ Object.assign(headers, resolvedRuntime.auth.headers);
296
+ }
297
+ if (isAnthropic) {
298
+ headers["anthropic-version"] = "2023-06-01";
299
+ if (resolvedRuntime.auth && resolvedRuntime.auth.apiKey) headers["x-api-key"] = resolvedRuntime.auth.apiKey;
300
+ } else if (isCodexResponses) {
301
+ headers.Accept = "text/event-stream";
302
+ headers.Connection = "Keep-Alive";
303
+ headers["User-Agent"] = CODEX_DEFAULT_USER_AGENT;
304
+ headers.Originator = CODEX_DEFAULT_ORIGINATOR;
305
+ if (resolvedRuntime.credential && resolvedRuntime.credential.accountId) {
306
+ headers["Chatgpt-Account-Id"] = String(resolvedRuntime.credential.accountId);
307
+ }
308
+ headers.Session_id = randomUUID();
309
+ } else {
310
+ if (resolvedRuntime.auth && resolvedRuntime.auth.apiKey) headers.authorization = `Bearer ${resolvedRuntime.auth.apiKey}`;
311
+ }
312
+ const body = JSON.stringify(request || {});
313
+
314
+ const controller = new AbortController();
315
+ const timer = setTimeout(() => {
316
+ try { controller.abort(); } catch {}
317
+ }, timeoutMs);
318
+
319
+ try {
320
+ const response = await fetchImpl(url, {
321
+ method: "POST",
322
+ headers,
323
+ body,
324
+ signal: controller.signal,
325
+ });
326
+
327
+ if (!response.ok) {
328
+ const errBody = await response.text().catch(() => "");
329
+ return {
330
+ ok: false,
331
+ error: `provider request failed (${response.status}): ${clipText(errBody)}`,
332
+ provider: resolvedRuntime.provider,
333
+ model: requestModel,
334
+ transport: resolvedRuntime.transport,
335
+ credentialSource: resolvedRuntime.credentialSource,
336
+ };
337
+ }
338
+
339
+ if (isCodexResponses) {
340
+ const raw = await response.text();
341
+ const parsed = parseCodexSsePayload(raw);
342
+ return {
343
+ ok: true,
344
+ output: parsed.text,
345
+ provider: String(resolvedRuntime.provider || ""),
346
+ model: requestModel,
347
+ transport: resolvedRuntime.transport,
348
+ credentialSource: resolvedRuntime.credentialSource,
349
+ data: parsed.response,
350
+ usage: parsed.usage,
351
+ };
352
+ }
353
+
354
+ const data = await response.json();
355
+ let text = "";
356
+ if (isAnthropic) {
357
+ const content = Array.isArray(data.content) ? data.content : [];
358
+ text = content
359
+ .filter((item) => item && item.type === "text")
360
+ .map((item) => String(item.text || ""))
361
+ .join("");
362
+ } else {
363
+ const choice = data.choices && data.choices[0];
364
+ text = choice && choice.message && typeof choice.message.content === "string"
365
+ ? choice.message.content
366
+ : "";
367
+ }
368
+
369
+ return {
370
+ ok: true,
371
+ output: text.trim(),
372
+ provider: String(resolvedRuntime.provider || ""),
373
+ model: requestModel,
374
+ transport: resolvedRuntime.transport,
375
+ credentialSource: resolvedRuntime.credentialSource,
376
+ data,
377
+ usage: data && typeof data === "object" && data.usage && typeof data.usage === "object"
378
+ ? data.usage
379
+ : null,
380
+ };
381
+ } catch (err) {
382
+ const message = err && err.message ? err.message : "upstream request failed";
383
+ return {
384
+ ok: false,
385
+ error: message,
386
+ provider: resolvedRuntime.provider,
387
+ model: requestModel,
388
+ transport: resolvedRuntime.transport,
389
+ credentialSource: resolvedRuntime.credentialSource,
390
+ };
391
+ } finally {
392
+ clearTimeout(timer);
393
+ }
394
+ }
395
+
396
+ async function sendUpstreamPrompt({
397
+ projectRoot,
398
+ prompt,
399
+ systemPrompt,
400
+ provider = "",
401
+ model = "",
402
+ messages = [],
403
+ tools = [],
404
+ maxTokens = 4096,
405
+ temperature = 0,
406
+ timeoutMs = 120000,
407
+ fetchImpl = global.fetch,
408
+ env = process.env,
409
+ loadConfigImpl = loadConfig,
410
+ } = {}) {
411
+ const runtime = await resolveUpstreamRuntime({
412
+ projectRoot,
413
+ provider,
414
+ model,
415
+ env,
416
+ loadConfigImpl,
417
+ });
418
+
419
+ const requestModel = String(runtime.model || "").trim();
420
+ const request = runtime.transport === "anthropic-messages"
421
+ ? buildAnthropicMessagesRequest({
422
+ model: requestModel,
423
+ systemPrompt,
424
+ prompt,
425
+ messages,
426
+ tools,
427
+ maxTokens,
428
+ temperature,
429
+ })
430
+ : runtime.transport === "codex-responses"
431
+ ? buildCodexResponsesRequest({
432
+ model: requestModel,
433
+ systemPrompt,
434
+ prompt,
435
+ messages,
436
+ })
437
+ : buildOpenAiChatRequest({
438
+ model: requestModel,
439
+ systemPrompt,
440
+ prompt,
441
+ messages,
442
+ tools,
443
+ temperature,
444
+ });
445
+
446
+ return sendUpstreamRequest({
447
+ runtime,
448
+ request,
449
+ timeoutMs,
450
+ fetchImpl,
451
+ });
452
+ }
453
+
454
+ module.exports = {
455
+ buildAnthropicMessagesRequest,
456
+ buildCodexResponsesRequest,
457
+ buildOpenAiChatRequest,
458
+ normalizeProvider,
459
+ parseCodexSsePayload,
460
+ resolveCodexResponseOutput,
461
+ resolveUpstreamRuntime,
462
+ sendUpstreamRequest,
463
+ sendUpstreamPrompt,
464
+ };
package/src/bus/utils.js CHANGED
@@ -2,6 +2,7 @@ const crypto = require("crypto");
2
2
  const fs = require("fs");
3
3
  const path = require("path");
4
4
  const { spawnSync } = require("child_process");
5
+ const { redactSecrets } = require("../providerapi/redactor");
5
6
 
6
7
  /**
7
8
  * 获取当前 UTC 时间戳(ISO 8601 格式)
@@ -207,7 +208,7 @@ function readJSON(filePath, defaultValue = null) {
207
208
  * 写入 JSON 文件(格式化)
208
209
  */
209
210
  function writeJSON(filePath, data) {
210
- const content = JSON.stringify(data, null, 2);
211
+ const content = JSON.stringify(redactSecrets(data), null, 2);
211
212
  writeFileAtomic(filePath, content);
212
213
  }
213
214
 
@@ -231,7 +232,7 @@ function readJSONL(filePath) {
231
232
  * 追加一行到 JSONL 文件
232
233
  */
233
234
  function appendJSONL(filePath, data) {
234
- const line = JSON.stringify(data);
235
+ const line = JSON.stringify(redactSecrets(data));
235
236
  appendFileAtomic(filePath, `${line}\n`);
236
237
  }
237
238
 
@@ -36,6 +36,7 @@ function buildSummaryLine(options = {}) {
36
36
  launchMode = "terminal",
37
37
  agentProvider = "codex-cli",
38
38
  cronTasks = [],
39
+ loopSummary = null,
39
40
  } = options;
40
41
  const agents = activeAgents.length > 0
41
42
  ? activeAgents.slice(0, 3)
@@ -43,10 +44,56 @@ function buildSummaryLine(options = {}) {
43
44
  .join(", ")
44
45
  + (activeAgents.length > 3 ? ` +${activeAgents.length - 3}` : "")
45
46
  : "none";
46
- return `{gray-fg}Agents:{/gray-fg} {cyan-fg}${agents}{/cyan-fg}`
47
+ let line = `{gray-fg}Agents:{/gray-fg} {cyan-fg}${agents}{/cyan-fg}`
47
48
  + ` {gray-fg}Mode:{/gray-fg} {cyan-fg}${launchMode}{/cyan-fg}`
48
49
  + ` {gray-fg}Agent:{/gray-fg} {cyan-fg}${providerLabel(agentProvider)}{/cyan-fg}`
49
50
  + ` {gray-fg}Cron:{/gray-fg} {cyan-fg}${Array.isArray(cronTasks) ? cronTasks.length : 0}{/cyan-fg}`;
51
+ const loopPart = formatLoopSummary(loopSummary);
52
+ if (loopPart) {
53
+ line += ` {gray-fg}Loop:{/gray-fg} {cyan-fg}${loopPart}{/cyan-fg}`;
54
+ }
55
+ return line;
56
+ }
57
+
58
+ function formatToolDistribution(items = []) {
59
+ const tools = Array.isArray(items) ? items : [];
60
+ if (tools.length === 0) return "";
61
+ const visible = tools.slice(0, 2).map((item) => `${item.name}x${item.count}`);
62
+ if (tools.length > 2) {
63
+ visible.push(`+${tools.length - 2}`);
64
+ }
65
+ return visible.join(",");
66
+ }
67
+
68
+ function formatLoopSummary(loopSummary) {
69
+ if (!loopSummary || typeof loopSummary !== "object") return "";
70
+ const rounds = Number(loopSummary.rounds) || 0;
71
+ const toolCalls = Number(loopSummary.tool_calls) || 0;
72
+ const totalTokens = Number(loopSummary.total_tokens) || 0;
73
+ const cacheReadTokens = Number(loopSummary.cache_read_tokens) || 0;
74
+ const cacheCreationTokens = Number(loopSummary.cache_creation_tokens) || 0;
75
+ const terminalReason = String(loopSummary.terminal_reason || "").trim();
76
+ const toolDistribution = formatToolDistribution(loopSummary.tools);
77
+
78
+ if (rounds <= 0 && toolCalls <= 0 && totalTokens <= 0 && !terminalReason && !toolDistribution) {
79
+ return "";
80
+ }
81
+
82
+ const parts = [
83
+ `r${rounds}`,
84
+ `tc${toolCalls}`,
85
+ `tok${totalTokens}`,
86
+ ];
87
+ if (cacheReadTokens > 0 || cacheCreationTokens > 0) {
88
+ parts.push(`cache${cacheReadTokens}/${cacheCreationTokens}`);
89
+ }
90
+ if (toolDistribution) {
91
+ parts.push(toolDistribution);
92
+ }
93
+ if (terminalReason) {
94
+ parts.push(terminalReason);
95
+ }
96
+ return parts.join(" ");
50
97
  }
51
98
 
52
99
  function buildProjectRailLine(options = {}) {
@@ -251,6 +298,7 @@ function computeDashboardContent(options = {}) {
251
298
  selectedResumeIndex = 0,
252
299
  selectedCronIndex = -1,
253
300
  cronTasks = [],
301
+ loopSummary = null,
254
302
  pendingReports = 0,
255
303
  providerOptions = [],
256
304
  resumeOptions = [],
@@ -290,6 +338,7 @@ function computeDashboardContent(options = {}) {
290
338
  launchMode,
291
339
  agentProvider,
292
340
  cronTasks,
341
+ loopSummary,
293
342
  });
294
343
  return {
295
344
  content: `${rail.line}\n ${line2}`,
@@ -352,6 +401,7 @@ function computeDashboardContent(options = {}) {
352
401
  launchMode,
353
402
  agentProvider,
354
403
  cronTasks,
404
+ loopSummary,
355
405
  });
356
406
 
357
407
  return { content, windowStart: agentListWindowStart };
package/src/chat/index.js CHANGED
@@ -133,6 +133,7 @@ async function runChat(projectRoot, options = {}) {
133
133
  let agentProvider = config.agentProvider;
134
134
  let autoResume = config.autoResume !== false;
135
135
  let cronTasks = [];
136
+ let loopSummary = null;
136
137
 
137
138
  // Dynamic input height settings.
138
139
  // Layout: dashboard(N) + inputBottom(1) + content + inputTop(1) + status(1)
@@ -726,7 +727,6 @@ async function runChat(projectRoot, options = {}) {
726
727
  const providerOptions = [
727
728
  { label: "codex", value: "codex-cli" },
728
729
  { label: "claude", value: "claude-cli" },
729
- { label: "ucode", value: "ucode" },
730
730
  ];
731
731
  let selectedProviderIndex = Math.max(0, providerOptions.findIndex((opt) => opt.value === agentProvider));
732
732
  const resumeOptions = [
@@ -1219,6 +1219,7 @@ async function runChat(projectRoot, options = {}) {
1219
1219
  selectedResumeIndex,
1220
1220
  selectedCronIndex,
1221
1221
  cronTasks,
1222
+ loopSummary,
1222
1223
  providerOptions,
1223
1224
  resumeOptions,
1224
1225
  dashHints: DASH_HINTS,
@@ -1265,6 +1266,7 @@ async function runChat(projectRoot, options = {}) {
1265
1266
  refreshProjectRuntimes();
1266
1267
  }
1267
1268
  cronTasks = Array.isArray(status?.cron?.tasks) ? status.cron.tasks : [];
1269
+ loopSummary = status && status.loop && typeof status.loop === "object" ? status.loop : null;
1268
1270
  const metaList = Array.isArray(status.active_meta) ? status.active_meta : [];
1269
1271
  let fallbackMap = null;
1270
1272
  if (metaList.length === 0 && activeAgents.length > 0) {