u-foo 1.0.3 → 1.1.9

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 (179) hide show
  1. package/README.md +110 -11
  2. package/README.zh-CN.md +9 -7
  3. package/SKILLS/ufoo/SKILL.md +132 -0
  4. package/SKILLS/uinit/SKILL.md +78 -0
  5. package/SKILLS/ustatus/SKILL.md +36 -0
  6. package/bin/uclaude.js +13 -0
  7. package/bin/ucode-core.js +15 -0
  8. package/bin/ucode.js +125 -0
  9. package/bin/ucodex.js +13 -0
  10. package/bin/ufoo +9 -31
  11. package/bin/ufoo-assistant-agent.js +5 -0
  12. package/bin/ufoo-engine.js +25 -0
  13. package/bin/ufoo.js +17 -0
  14. package/modules/AGENTS.template.md +29 -11
  15. package/modules/bus/README.md +33 -25
  16. package/modules/bus/SKILLS/ubus/SKILL.md +19 -8
  17. package/modules/context/README.md +18 -40
  18. package/modules/context/SKILLS/uctx/SKILL.md +63 -1
  19. package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
  20. package/package.json +25 -4
  21. package/scripts/import-pi-mono.js +124 -0
  22. package/scripts/postinstall.js +30 -0
  23. package/scripts/sync-claude-skills.sh +21 -0
  24. package/src/agent/cliRunner.js +554 -33
  25. package/src/agent/internalRunner.js +150 -56
  26. package/src/agent/launcher.js +754 -0
  27. package/src/agent/normalizeOutput.js +1 -1
  28. package/src/agent/notifier.js +340 -0
  29. package/src/agent/ptyRunner.js +847 -0
  30. package/src/agent/ptyWrapper.js +379 -0
  31. package/src/agent/readyDetector.js +175 -0
  32. package/src/agent/ucode.js +443 -0
  33. package/src/agent/ucodeBootstrap.js +113 -0
  34. package/src/agent/ucodeBuild.js +67 -0
  35. package/src/agent/ucodeDoctor.js +184 -0
  36. package/src/agent/ucodeRuntimeConfig.js +129 -0
  37. package/src/agent/ufooAgent.js +46 -42
  38. package/src/assistant/agent.js +260 -0
  39. package/src/assistant/bridge.js +172 -0
  40. package/src/assistant/engine.js +252 -0
  41. package/src/assistant/stdio.js +58 -0
  42. package/src/assistant/ufooEngineCli.js +306 -0
  43. package/src/bus/activate.js +172 -0
  44. package/src/bus/daemon.js +436 -0
  45. package/src/bus/index.js +842 -0
  46. package/src/bus/inject.js +315 -0
  47. package/src/bus/message.js +430 -0
  48. package/src/bus/nickname.js +88 -0
  49. package/src/bus/queue.js +136 -0
  50. package/src/bus/shake.js +26 -0
  51. package/src/bus/store.js +189 -0
  52. package/src/bus/subscriber.js +312 -0
  53. package/src/bus/utils.js +363 -0
  54. package/src/chat/agentBar.js +117 -0
  55. package/src/chat/agentDirectory.js +88 -0
  56. package/src/chat/agentSockets.js +225 -0
  57. package/src/chat/agentViewController.js +298 -0
  58. package/src/chat/chatLogController.js +115 -0
  59. package/src/chat/commandExecutor.js +700 -0
  60. package/src/chat/commands.js +132 -0
  61. package/src/chat/completionController.js +414 -0
  62. package/src/chat/cronScheduler.js +160 -0
  63. package/src/chat/daemonConnection.js +166 -0
  64. package/src/chat/daemonCoordinator.js +64 -0
  65. package/src/chat/daemonMessageRouter.js +257 -0
  66. package/src/chat/daemonReconnect.js +41 -0
  67. package/src/chat/daemonTransport.js +36 -0
  68. package/src/chat/daemonTransportDefaults.js +10 -0
  69. package/src/chat/dashboardKeyController.js +480 -0
  70. package/src/chat/dashboardView.js +154 -0
  71. package/src/chat/index.js +1011 -1392
  72. package/src/chat/inputHistoryController.js +105 -0
  73. package/src/chat/inputListenerController.js +304 -0
  74. package/src/chat/inputMath.js +104 -0
  75. package/src/chat/inputSubmitHandler.js +171 -0
  76. package/src/chat/layout.js +165 -0
  77. package/src/chat/pasteController.js +81 -0
  78. package/src/chat/rawKeyMap.js +42 -0
  79. package/src/chat/settingsController.js +132 -0
  80. package/src/chat/statusLineController.js +177 -0
  81. package/src/chat/streamTracker.js +138 -0
  82. package/src/chat/text.js +70 -0
  83. package/src/chat/transport.js +61 -0
  84. package/src/cli/busCoreCommands.js +59 -0
  85. package/src/cli/ctxCoreCommands.js +199 -0
  86. package/src/cli/onlineCoreCommands.js +379 -0
  87. package/src/cli.js +1162 -96
  88. package/src/code/README.md +29 -0
  89. package/src/code/UCODE_PROMPT.md +32 -0
  90. package/src/code/agent.js +1651 -0
  91. package/src/code/cli.js +158 -0
  92. package/src/code/config +0 -0
  93. package/src/code/dispatch.js +42 -0
  94. package/src/code/index.js +70 -0
  95. package/src/code/nativeRunner.js +1213 -0
  96. package/src/code/runtime.js +154 -0
  97. package/src/code/sessionStore.js +162 -0
  98. package/src/code/taskDecomposer.js +269 -0
  99. package/src/code/tools/bash.js +53 -0
  100. package/src/code/tools/common.js +42 -0
  101. package/src/code/tools/edit.js +70 -0
  102. package/src/code/tools/read.js +44 -0
  103. package/src/code/tools/write.js +35 -0
  104. package/src/code/tui.js +1580 -0
  105. package/src/config.js +56 -3
  106. package/src/context/decisions.js +324 -0
  107. package/src/context/doctor.js +183 -0
  108. package/src/context/index.js +55 -0
  109. package/src/context/sync.js +127 -0
  110. package/src/daemon/agentProcessManager.js +74 -0
  111. package/src/daemon/cronOps.js +241 -0
  112. package/src/daemon/index.js +998 -170
  113. package/src/daemon/ipcServer.js +99 -0
  114. package/src/daemon/ops.js +630 -48
  115. package/src/daemon/promptLoop.js +319 -0
  116. package/src/daemon/promptRequest.js +101 -0
  117. package/src/daemon/providerSessions.js +306 -0
  118. package/src/daemon/reporting.js +90 -0
  119. package/src/daemon/run.js +31 -1
  120. package/src/daemon/status.js +48 -8
  121. package/src/doctor/index.js +50 -0
  122. package/src/init/index.js +318 -0
  123. package/src/online/bridge.js +663 -0
  124. package/src/online/client.js +245 -0
  125. package/src/online/runner.js +253 -0
  126. package/src/online/server.js +992 -0
  127. package/src/online/tokens.js +103 -0
  128. package/src/report/store.js +331 -0
  129. package/src/shared/eventContract.js +35 -0
  130. package/src/shared/ptySocketContract.js +21 -0
  131. package/src/skills/index.js +159 -0
  132. package/src/status/index.js +285 -0
  133. package/src/terminal/adapterContract.js +87 -0
  134. package/src/terminal/adapterRouter.js +84 -0
  135. package/src/terminal/adapters/externalAdapter.js +14 -0
  136. package/src/terminal/adapters/internalAdapter.js +13 -0
  137. package/src/terminal/adapters/internalPtyAdapter.js +42 -0
  138. package/src/terminal/adapters/internalQueueAdapter.js +37 -0
  139. package/src/terminal/adapters/terminalAdapter.js +31 -0
  140. package/src/terminal/adapters/tmuxAdapter.js +30 -0
  141. package/src/terminal/detect.js +64 -0
  142. package/src/terminal/index.js +8 -0
  143. package/src/terminal/iterm2.js +126 -0
  144. package/src/ufoo/agentsStore.js +107 -0
  145. package/src/ufoo/paths.js +46 -0
  146. package/src/utils/banner.js +76 -0
  147. package/bin/uclaude +0 -65
  148. package/bin/ucodex +0 -65
  149. package/modules/bus/scripts/bus-alert.sh +0 -185
  150. package/modules/bus/scripts/bus-listen.sh +0 -117
  151. package/modules/context/ASSUMPTIONS.md +0 -7
  152. package/modules/context/CONSTRAINTS.md +0 -7
  153. package/modules/context/CONTEXT-STRUCTURE.md +0 -49
  154. package/modules/context/DECISION-PROTOCOL.md +0 -62
  155. package/modules/context/HANDOFF.md +0 -33
  156. package/modules/context/RULES.md +0 -15
  157. package/modules/context/SKILLS/README.md +0 -14
  158. package/modules/context/SYSTEM.md +0 -18
  159. package/modules/context/TEMPLATES/assumptions.md +0 -4
  160. package/modules/context/TEMPLATES/constraints.md +0 -4
  161. package/modules/context/TEMPLATES/decision.md +0 -16
  162. package/modules/context/TEMPLATES/project-context-readme.md +0 -6
  163. package/modules/context/TEMPLATES/system.md +0 -3
  164. package/modules/context/TEMPLATES/terminology.md +0 -4
  165. package/modules/context/TERMINOLOGY.md +0 -10
  166. package/scripts/banner.sh +0 -89
  167. package/scripts/bus-alert.sh +0 -6
  168. package/scripts/bus-autotrigger.sh +0 -6
  169. package/scripts/bus-daemon.sh +0 -231
  170. package/scripts/bus-inject.sh +0 -144
  171. package/scripts/bus-listen.sh +0 -6
  172. package/scripts/bus.sh +0 -984
  173. package/scripts/context-decisions.sh +0 -167
  174. package/scripts/context-doctor.sh +0 -72
  175. package/scripts/context-lint.sh +0 -110
  176. package/scripts/doctor.sh +0 -22
  177. package/scripts/init.sh +0 -247
  178. package/scripts/skills.sh +0 -113
  179. package/scripts/status.sh +0 -125
@@ -0,0 +1,363 @@
1
+ const crypto = require("crypto");
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+ const { spawnSync } = require("child_process");
5
+
6
+ /**
7
+ * 获取当前 UTC 时间戳(ISO 8601 格式)
8
+ */
9
+ function getTimestamp() {
10
+ return new Date().toISOString();
11
+ }
12
+
13
+ /**
14
+ * 获取当前日期(YYYY-MM-DD 格式)
15
+ */
16
+ function getDate() {
17
+ return new Date().toISOString().split("T")[0];
18
+ }
19
+
20
+ /**
21
+ * 生成实例 ID(8 位十六进制)
22
+ */
23
+ function generateInstanceId() {
24
+ return crypto.randomBytes(4).toString("hex");
25
+ }
26
+
27
+ /**
28
+ * 将订阅者 ID 转换为安全的文件名
29
+ * 例如:claude-code:abc123 -> claude-code_abc123
30
+ */
31
+ function subscriberToSafeName(subscriber) {
32
+ return subscriber.replace(/:/g, "_");
33
+ }
34
+
35
+ /**
36
+ * 将安全文件名转换回订阅者 ID
37
+ * 例如:claude-code_abc123 -> claude-code:abc123
38
+ */
39
+ function safeNameToSubscriber(safeName) {
40
+ // 只替换第一个下划线为冒号
41
+ const match = safeName.match(/^([^_]+)_(.+)$/);
42
+ if (match) {
43
+ return `${match[1]}:${match[2]}`;
44
+ }
45
+ return safeName;
46
+ }
47
+
48
+ /**
49
+ * 检查进程是否存活
50
+ */
51
+ function isPidAlive(pid) {
52
+ if (!pid || pid === 0) return false;
53
+ try {
54
+ process.kill(pid, 0);
55
+ return true;
56
+ } catch (err) {
57
+ // Sandbox can return EPERM for other processes even if they are alive.
58
+ if (err && err.code === "EPERM") return true;
59
+ return false;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * 获取进程命令名(用于校验 PID 是否仍属于 agent 进程)
65
+ */
66
+ function getPidCommand(pid) {
67
+ if (!pid || pid === 0) return "";
68
+ try {
69
+ const res = spawnSync("ps", ["-p", String(pid), "-o", "comm="], {
70
+ encoding: "utf8",
71
+ stdio: ["ignore", "pipe", "ignore"],
72
+ });
73
+ if (res.status === 0) {
74
+ return (res.stdout || "").trim();
75
+ }
76
+ } catch {
77
+ // ignore
78
+ }
79
+ return "";
80
+ }
81
+
82
+ /**
83
+ * 判断 PID 是否为可识别的 agent 进程
84
+ */
85
+ function isAgentPidAlive(pid) {
86
+ if (!isPidAlive(pid)) return false;
87
+ const cmd = getPidCommand(pid);
88
+ if (!cmd) return true;
89
+ return /(claude|codex|node|pi-mono|ufoo|ucode)/i.test(cmd);
90
+ }
91
+
92
+ /**
93
+ * 检查 tty 路径是否有效(用于注入)
94
+ */
95
+ function isValidTty(ttyPath) {
96
+ if (!ttyPath) return false;
97
+ if (ttyPath === "/dev/tty") return false;
98
+ if (!ttyPath.startsWith("/dev/")) return false;
99
+ return true;
100
+ }
101
+
102
+ function normalizeTty(ttyPath) {
103
+ if (!ttyPath) return "";
104
+ const trimmed = String(ttyPath).trim();
105
+ if (!trimmed || trimmed === "not a tty") return "";
106
+ if (trimmed === "/dev/tty") return "";
107
+ return trimmed;
108
+ }
109
+
110
+ function getCurrentTty() {
111
+ try {
112
+ const res = spawnSync("tty", {
113
+ stdio: [0, "pipe", "ignore"],
114
+ encoding: "utf8",
115
+ });
116
+ if (res && res.status === 0) {
117
+ const tty = normalizeTty(res.stdout || "");
118
+ return isValidTty(tty) ? tty : "";
119
+ }
120
+ } catch {
121
+ // ignore
122
+ }
123
+ return "";
124
+ }
125
+
126
+ function parsePsLines(raw) {
127
+ const lines = String(raw || "").trim().split(/\r?\n/).filter(Boolean);
128
+ return lines.map((line) => {
129
+ const match = line.trim().match(/^(\d+)\s+(.+)$/);
130
+ if (!match) return null;
131
+ return { pid: parseInt(match[1], 10), comm: match[2].trim() };
132
+ }).filter(Boolean);
133
+ }
134
+
135
+ function getTtyProcessInfo(ttyPath) {
136
+ if (!isValidTty(ttyPath)) {
137
+ return { alive: false, idle: false, hasAgent: false, shellPid: 0, processes: [] };
138
+ }
139
+ const ttyName = ttyPath.replace("/dev/", "");
140
+ try {
141
+ const res = spawnSync("ps", ["-t", ttyName, "-o", "pid=,comm="], {
142
+ encoding: "utf8",
143
+ stdio: ["ignore", "pipe", "ignore"],
144
+ });
145
+ if (res.status !== 0) {
146
+ return { alive: false, idle: false, hasAgent: false, shellPid: 0, processes: [] };
147
+ }
148
+ const processes = parsePsLines(res.stdout || "");
149
+ if (processes.length === 0) {
150
+ return { alive: false, idle: false, hasAgent: false, shellPid: 0, processes: [] };
151
+ }
152
+ const shellNames = new Set(["zsh", "bash", "fish", "sh", "login"]);
153
+ const hasAgent = processes.some((p) => /(codex|claude|node|pi-mono|ufoo|ucode)/i.test(p.comm));
154
+ const nonShell = processes.filter((p) => !shellNames.has(p.comm));
155
+ const idle = !hasAgent && nonShell.length === 0;
156
+ const shell = processes.find((p) => shellNames.has(p.comm));
157
+ return { alive: true, idle, hasAgent, shellPid: shell ? shell.pid : 0, processes };
158
+ } catch {
159
+ return { alive: false, idle: false, hasAgent: false, shellPid: 0, processes: [] };
160
+ }
161
+ }
162
+
163
+ /**
164
+ * 确保目录存在
165
+ */
166
+ function ensureDir(dir) {
167
+ if (!fs.existsSync(dir)) {
168
+ fs.mkdirSync(dir, { recursive: true });
169
+ }
170
+ }
171
+
172
+ /**
173
+ * 原子性地追加内容到文件
174
+ * @param {string} filePath - 文件路径
175
+ * @param {string} content - 要追加的内容
176
+ */
177
+ function appendFileAtomic(filePath, content) {
178
+ ensureDir(path.dirname(filePath));
179
+ fs.appendFileSync(filePath, content, { encoding: "utf8", flag: "a" });
180
+ }
181
+
182
+ /**
183
+ * 原子性地写入文件
184
+ */
185
+ function writeFileAtomic(filePath, content) {
186
+ ensureDir(path.dirname(filePath));
187
+ // Include pid + random suffix to avoid collisions across concurrent writers in the same millisecond.
188
+ const tmpFile = `${filePath}.tmp.${process.pid}.${Date.now()}.${crypto.randomBytes(4).toString("hex")}`;
189
+ fs.writeFileSync(tmpFile, content, "utf8");
190
+ fs.renameSync(tmpFile, filePath);
191
+ }
192
+
193
+ /**
194
+ * 读取 JSON 文件
195
+ */
196
+ function readJSON(filePath, defaultValue = null) {
197
+ try {
198
+ if (!fs.existsSync(filePath)) return defaultValue;
199
+ const content = fs.readFileSync(filePath, "utf8");
200
+ return JSON.parse(content);
201
+ } catch (err) {
202
+ return defaultValue;
203
+ }
204
+ }
205
+
206
+ /**
207
+ * 写入 JSON 文件(格式化)
208
+ */
209
+ function writeJSON(filePath, data) {
210
+ const content = JSON.stringify(data, null, 2);
211
+ writeFileAtomic(filePath, content);
212
+ }
213
+
214
+ /**
215
+ * 读取 JSONL 文件的所有行
216
+ */
217
+ function readJSONL(filePath) {
218
+ if (!fs.existsSync(filePath)) return [];
219
+ const content = fs.readFileSync(filePath, "utf8").trim();
220
+ if (!content) return [];
221
+ return content.split("\n").map((line) => {
222
+ try {
223
+ return JSON.parse(line);
224
+ } catch {
225
+ return null;
226
+ }
227
+ }).filter(Boolean);
228
+ }
229
+
230
+ /**
231
+ * 追加一行到 JSONL 文件
232
+ */
233
+ function appendJSONL(filePath, data) {
234
+ const line = JSON.stringify(data);
235
+ appendFileAtomic(filePath, `${line}\n`);
236
+ }
237
+
238
+ /**
239
+ * 读取文件的最后一行
240
+ */
241
+ function readLastLine(filePath) {
242
+ if (!fs.existsSync(filePath)) return null;
243
+ const content = fs.readFileSync(filePath, "utf8").trim();
244
+ if (!content) return null;
245
+ const lines = content.split("\n");
246
+ return lines[lines.length - 1];
247
+ }
248
+
249
+ /**
250
+ * 清空文件内容
251
+ */
252
+ function truncateFile(filePath) {
253
+ if (fs.existsSync(filePath)) {
254
+ fs.writeFileSync(filePath, "", "utf8");
255
+ }
256
+ }
257
+
258
+ function sleep(ms) {
259
+ return new Promise((resolve) => setTimeout(resolve, ms));
260
+ }
261
+
262
+ /**
263
+ * 日志输出(带颜色)
264
+ */
265
+ const colors = {
266
+ reset: "\x1b[0m",
267
+ red: "\x1b[0;31m",
268
+ green: "\x1b[0;32m",
269
+ yellow: "\x1b[0;33m",
270
+ blue: "\x1b[0;34m",
271
+ cyan: "\x1b[0;36m",
272
+ };
273
+
274
+ function logInfo(message) {
275
+ console.log(`${colors.blue}[bus]${colors.reset} ${message}`);
276
+ }
277
+
278
+ function logOk(message) {
279
+ console.log(`${colors.green}[bus]${colors.reset} ${message}`);
280
+ }
281
+
282
+ function logWarn(message) {
283
+ console.log(`${colors.yellow}[bus]${colors.reset} ${message}`);
284
+ }
285
+
286
+ function logError(message) {
287
+ console.error(`${colors.red}[bus]${colors.reset} ${message}`);
288
+ }
289
+
290
+ /**
291
+ * 统一的 agent 存活检测逻辑
292
+ * buildStatus() 和 getActiveSubscribers() 共用此函数
293
+ *
294
+ * 检测顺序:
295
+ * 1. status=inactive → 离线
296
+ * 2. PID 存活 → 在线
297
+ * 3. TTY 上有 agent 进程 → 在线
298
+ * 4. PID 存在但 dead(且 TTY 也没有 agent)→ 离线
299
+ * 5. 无 PID 时,last_seen 在 30s 内 → 在线(心跳兜底)
300
+ * 6. 其余 → 离线
301
+ */
302
+ const HEARTBEAT_TIMEOUT_MS = 30 * 1000; // notifier 每 2s 心跳,15 次余量
303
+
304
+ function isMetaActive(meta) {
305
+ if (!meta) return false;
306
+
307
+ // 1. 显式标记为 inactive
308
+ if (meta.status === "inactive") return false;
309
+
310
+ // 2. PID 存活(最可靠)
311
+ if (meta.pid && isAgentPidAlive(meta.pid)) return true;
312
+
313
+ // 3. TTY 上有 agent 进程
314
+ if (meta.tty) {
315
+ const ttyInfo = getTtyProcessInfo(meta.tty);
316
+ if (ttyInfo && ttyInfo.hasAgent) return true;
317
+ }
318
+
319
+ // 4. PID 存在但 dead(且 TTY 也没有 agent)→ 离线
320
+ if (meta.pid) return false;
321
+
322
+ // 5. 无 PID,用 last_seen 心跳超时兜底
323
+ if (meta.status === "active" && meta.last_seen) {
324
+ const age = Date.now() - new Date(meta.last_seen).getTime();
325
+ return age <= HEARTBEAT_TIMEOUT_MS;
326
+ }
327
+
328
+ // 6. status=active 但无任何信息
329
+ if (meta.status === "active") return true;
330
+
331
+ return false;
332
+ }
333
+
334
+ module.exports = {
335
+ getTimestamp,
336
+ getDate,
337
+ generateInstanceId,
338
+ subscriberToSafeName,
339
+ safeNameToSubscriber,
340
+ isPidAlive,
341
+ getPidCommand,
342
+ isAgentPidAlive,
343
+ isMetaActive,
344
+ HEARTBEAT_TIMEOUT_MS,
345
+ isValidTty,
346
+ getCurrentTty,
347
+ getTtyProcessInfo,
348
+ ensureDir,
349
+ appendFileAtomic,
350
+ writeFileAtomic,
351
+ readJSON,
352
+ writeJSON,
353
+ readJSONL,
354
+ appendJSONL,
355
+ readLastLine,
356
+ truncateFile,
357
+ sleep,
358
+ logInfo,
359
+ logOk,
360
+ logWarn,
361
+ logError,
362
+ colors,
363
+ };
@@ -0,0 +1,117 @@
1
+ const { stripAnsi, truncateAnsi } = require("./text");
2
+
3
+ function computeAgentBar(options = {}) {
4
+ const {
5
+ cols = 80,
6
+ hintText = "",
7
+ focusMode = "input",
8
+ selectedAgentIndex = -1,
9
+ activeAgents = [],
10
+ viewingAgent = null,
11
+ agentListWindowStart = 0,
12
+ maxAgentWindow = 4,
13
+ getAgentLabel = (id) => id,
14
+ } = options;
15
+
16
+ const hintAnsi = `\x1b[90m│ ${hintText}\x1b[0m`;
17
+ const selectionIndex = focusMode === "dashboard"
18
+ ? (selectedAgentIndex > 0 ? selectedAgentIndex - 1 : -1)
19
+ : activeAgents.indexOf(viewingAgent);
20
+ const maxAgentLen = Math.max(0, cols - 1 - 2 - stripAnsi(hintAnsi).length);
21
+ let windowItems = Math.max(1, Math.min(maxAgentWindow, activeAgents.length));
22
+ let start = agentListWindowStart;
23
+ const ufooItem = focusMode === "dashboard" && selectedAgentIndex === 0
24
+ ? "\x1b[90;7mufoo\x1b[0m"
25
+ : "\x1b[36mufoo\x1b[0m";
26
+ const ufooLen = stripAnsi(ufooItem).length;
27
+
28
+ const computeStart = (items) => {
29
+ if (activeAgents.length === 0) return 0;
30
+ let s = start;
31
+ if (selectionIndex >= 0) {
32
+ if (selectionIndex < s) {
33
+ s = selectionIndex;
34
+ } else if (selectionIndex >= s + items) {
35
+ s = selectionIndex - items + 1;
36
+ }
37
+ }
38
+ const maxStart = Math.max(0, activeAgents.length - items);
39
+ if (s > maxStart) s = maxStart;
40
+ if (s < 0) s = 0;
41
+ return s;
42
+ };
43
+
44
+ const truncateLabel = (label, maxLen) => {
45
+ const text = String(label || "");
46
+ if (maxLen <= 0) return "";
47
+ if (text.length <= maxLen) return text;
48
+ if (maxLen <= 3) return text.slice(0, maxLen);
49
+ return `${text.slice(0, maxLen - 3)}...`;
50
+ };
51
+
52
+ const buildAgentSegment = (items, maxLabelLen) => {
53
+ const s = computeStart(items);
54
+ const e = s + items;
55
+ const visible = activeAgents.slice(s, e);
56
+ const leftMore = s > 0 ? "\x1b[90m<\x1b[0m " : "";
57
+ const rightMore = e < activeAgents.length ? " \x1b[90m>\x1b[0m" : "";
58
+ let agentParts = [];
59
+ if (activeAgents.length > 0) {
60
+ agentParts = visible.map((agent, i) => {
61
+ const rawLabel = getAgentLabel(agent);
62
+ const label = maxLabelLen ? truncateLabel(rawLabel, maxLabelLen) : rawLabel;
63
+ const idx = s + i + 1; // +1 for ufoo at index 0
64
+ if (focusMode === "dashboard" && idx === selectedAgentIndex) {
65
+ return `\x1b[90;7m${label}\x1b[0m`;
66
+ }
67
+ if (agent === viewingAgent) return `\x1b[1;36m${label}\x1b[0m`;
68
+ return `\x1b[36m${label}\x1b[0m`;
69
+ });
70
+ }
71
+ const agentsText = activeAgents.length > 0
72
+ ? `${leftMore}${agentParts.join(" ")}${rightMore}`
73
+ : "\x1b[36mnone\x1b[0m";
74
+ return { segment: `${ufooItem} ${agentsText}`, start: s };
75
+ };
76
+
77
+ let segmentInfo = buildAgentSegment(windowItems, 0);
78
+ while (windowItems > 0) {
79
+ const s = computeStart(windowItems);
80
+ const e = s + windowItems;
81
+ const hasLeft = s > 0;
82
+ const hasRight = e < activeAgents.length;
83
+ const spacingLen = windowItems > 1 ? (windowItems - 1) * 2 : 0;
84
+ const overhead = ufooLen + 2 + (hasLeft ? 2 : 0) + (hasRight ? 2 : 0) + spacingLen;
85
+ const available = maxAgentLen - overhead;
86
+ let maxLabelLen = windowItems > 0 ? Math.floor(available / windowItems) : 0;
87
+ if (windowItems > 1 && maxLabelLen < 3) {
88
+ windowItems -= 1;
89
+ segmentInfo = buildAgentSegment(windowItems, 0);
90
+ continue;
91
+ }
92
+ if (maxLabelLen < 1) maxLabelLen = 1;
93
+ segmentInfo = buildAgentSegment(windowItems, maxLabelLen);
94
+ if (stripAnsi(segmentInfo.segment).length <= maxAgentLen || windowItems === 1) break;
95
+ windowItems -= 1;
96
+ segmentInfo = buildAgentSegment(windowItems, 0);
97
+ }
98
+ start = segmentInfo.start;
99
+ const agentSegment = segmentInfo.segment;
100
+
101
+ let bar = ` ${agentSegment} ${hintAnsi}`;
102
+ let barLen = stripAnsi(bar).length;
103
+ if (barLen > cols) {
104
+ bar = truncateAnsi(bar, cols);
105
+ barLen = stripAnsi(bar).length;
106
+ }
107
+ const pad = Math.max(0, cols - barLen);
108
+
109
+ return {
110
+ bar: `${bar}${" ".repeat(pad)}`,
111
+ windowStart: start,
112
+ };
113
+ }
114
+
115
+ module.exports = {
116
+ computeAgentBar,
117
+ };
@@ -0,0 +1,88 @@
1
+ function buildAgentMaps(activeAgents = [], metaList = [], fallbackMap = null) {
2
+ const labelMap = new Map();
3
+ const metaMap = new Map();
4
+ const metaById = new Map();
5
+
6
+ for (const meta of metaList) {
7
+ if (!meta || !meta.id) continue;
8
+ metaById.set(meta.id, meta);
9
+ }
10
+
11
+ for (const id of activeAgents) {
12
+ const meta = metaById.get(id);
13
+ const label = meta && meta.nickname
14
+ ? meta.nickname
15
+ : (fallbackMap && fallbackMap.get(id)) || id;
16
+ labelMap.set(id, label);
17
+ if (meta) {
18
+ metaMap.set(id, meta);
19
+ }
20
+ }
21
+
22
+ return { labelMap, metaMap };
23
+ }
24
+
25
+ function getAgentLabel(labelMap, agentId) {
26
+ return labelMap.get(agentId) || agentId;
27
+ }
28
+
29
+ function resolveAgentId({ label, activeAgents = [], labelMap = new Map(), lookupNickname = null }) {
30
+ if (!label) return null;
31
+ if (activeAgents.includes(label)) return label;
32
+
33
+ for (const [id, name] of labelMap.entries()) {
34
+ if (name === label) return id;
35
+ }
36
+
37
+ if (typeof lookupNickname === "function") {
38
+ const resolved = lookupNickname(label);
39
+ if (resolved) return resolved;
40
+ }
41
+
42
+ return null;
43
+ }
44
+
45
+ function resolveAgentDisplayName({ publisher, labelMap = new Map(), lookupNicknameById = null }) {
46
+ let displayName = publisher;
47
+ if (publisher && publisher.includes(":")) {
48
+ if (labelMap && labelMap.has(publisher)) {
49
+ displayName = labelMap.get(publisher);
50
+ } else if (typeof lookupNicknameById === "function") {
51
+ const resolved = lookupNicknameById(publisher);
52
+ if (resolved) displayName = resolved;
53
+ }
54
+ }
55
+ return displayName;
56
+ }
57
+
58
+ function clampAgentWindowWithSelection({
59
+ activeCount = 0,
60
+ maxWindow = 4,
61
+ windowStart = 0,
62
+ selectionIndex = -1,
63
+ }) {
64
+ if (activeCount <= 0) {
65
+ return 0;
66
+ }
67
+ const maxItems = Math.max(1, Math.min(maxWindow, activeCount));
68
+ let nextStart = windowStart;
69
+ if (selectionIndex >= 0) {
70
+ if (selectionIndex < nextStart) {
71
+ nextStart = selectionIndex;
72
+ } else if (selectionIndex >= nextStart + maxItems) {
73
+ nextStart = selectionIndex - maxItems + 1;
74
+ }
75
+ }
76
+ const maxStart = Math.max(0, activeCount - maxItems);
77
+ if (nextStart > maxStart) nextStart = maxStart;
78
+ if (nextStart < 0) nextStart = 0;
79
+ return nextStart;
80
+ }
81
+
82
+ module.exports = {
83
+ buildAgentMaps,
84
+ getAgentLabel,
85
+ resolveAgentId,
86
+ resolveAgentDisplayName,
87
+ clampAgentWindowWithSelection,
88
+ };