u-foo 1.0.6 → 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 (149) hide show
  1. package/README.md +44 -4
  2. package/SKILLS/ufoo/SKILL.md +17 -2
  3. package/SKILLS/uinit/SKILL.md +8 -3
  4. package/bin/ucode-core.js +15 -0
  5. package/bin/ucode.js +125 -0
  6. package/bin/ufoo-assistant-agent.js +5 -0
  7. package/bin/ufoo-engine.js +25 -0
  8. package/bin/ufoo.js +4 -0
  9. package/modules/AGENTS.template.md +14 -4
  10. package/modules/bus/README.md +8 -5
  11. package/modules/bus/SKILLS/ubus/SKILL.md +5 -4
  12. package/modules/context/SKILLS/uctx/SKILL.md +3 -1
  13. package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
  14. package/package.json +12 -3
  15. package/scripts/import-pi-mono.js +124 -0
  16. package/scripts/postinstall.js +20 -49
  17. package/scripts/sync-claude-skills.sh +21 -0
  18. package/src/agent/cliRunner.js +524 -31
  19. package/src/agent/internalRunner.js +76 -9
  20. package/src/agent/launcher.js +97 -45
  21. package/src/agent/normalizeOutput.js +1 -1
  22. package/src/agent/notifier.js +144 -4
  23. package/src/agent/ptyRunner.js +480 -10
  24. package/src/agent/ptyWrapper.js +28 -3
  25. package/src/agent/readyDetector.js +16 -0
  26. package/src/agent/ucode.js +443 -0
  27. package/src/agent/ucodeBootstrap.js +113 -0
  28. package/src/agent/ucodeBuild.js +67 -0
  29. package/src/agent/ucodeDoctor.js +184 -0
  30. package/src/agent/ucodeRuntimeConfig.js +129 -0
  31. package/src/agent/ufooAgent.js +11 -2
  32. package/src/assistant/agent.js +260 -0
  33. package/src/assistant/bridge.js +172 -0
  34. package/src/assistant/engine.js +252 -0
  35. package/src/assistant/stdio.js +58 -0
  36. package/src/assistant/ufooEngineCli.js +306 -0
  37. package/src/bus/activate.js +27 -11
  38. package/src/bus/daemon.js +133 -5
  39. package/src/bus/index.js +137 -80
  40. package/src/bus/inject.js +47 -17
  41. package/src/bus/message.js +145 -17
  42. package/src/bus/nickname.js +3 -1
  43. package/src/bus/queue.js +6 -1
  44. package/src/bus/store.js +189 -0
  45. package/src/bus/subscriber.js +20 -4
  46. package/src/bus/utils.js +9 -3
  47. package/src/chat/agentBar.js +117 -0
  48. package/src/chat/agentDirectory.js +88 -0
  49. package/src/chat/agentSockets.js +225 -0
  50. package/src/chat/agentViewController.js +298 -0
  51. package/src/chat/chatLogController.js +115 -0
  52. package/src/chat/commandExecutor.js +700 -0
  53. package/src/chat/commands.js +132 -0
  54. package/src/chat/completionController.js +414 -0
  55. package/src/chat/cronScheduler.js +160 -0
  56. package/src/chat/daemonConnection.js +166 -0
  57. package/src/chat/daemonCoordinator.js +64 -0
  58. package/src/chat/daemonMessageRouter.js +257 -0
  59. package/src/chat/daemonReconnect.js +41 -0
  60. package/src/chat/daemonTransport.js +36 -0
  61. package/src/chat/daemonTransportDefaults.js +10 -0
  62. package/src/chat/dashboardKeyController.js +480 -0
  63. package/src/chat/dashboardView.js +154 -0
  64. package/src/chat/index.js +935 -2909
  65. package/src/chat/inputHistoryController.js +105 -0
  66. package/src/chat/inputListenerController.js +304 -0
  67. package/src/chat/inputMath.js +104 -0
  68. package/src/chat/inputSubmitHandler.js +171 -0
  69. package/src/chat/layout.js +165 -0
  70. package/src/chat/pasteController.js +81 -0
  71. package/src/chat/rawKeyMap.js +42 -0
  72. package/src/chat/settingsController.js +132 -0
  73. package/src/chat/statusLineController.js +177 -0
  74. package/src/chat/streamTracker.js +138 -0
  75. package/src/chat/text.js +70 -0
  76. package/src/chat/transport.js +61 -0
  77. package/src/cli/busCoreCommands.js +59 -0
  78. package/src/cli/ctxCoreCommands.js +199 -0
  79. package/src/cli/onlineCoreCommands.js +379 -0
  80. package/src/cli.js +741 -238
  81. package/src/code/README.md +29 -0
  82. package/src/code/UCODE_PROMPT.md +32 -0
  83. package/src/code/agent.js +1651 -0
  84. package/src/code/cli.js +158 -0
  85. package/src/code/config +0 -0
  86. package/src/code/dispatch.js +42 -0
  87. package/src/code/index.js +70 -0
  88. package/src/code/nativeRunner.js +1213 -0
  89. package/src/code/runtime.js +154 -0
  90. package/src/code/sessionStore.js +162 -0
  91. package/src/code/taskDecomposer.js +269 -0
  92. package/src/code/tools/bash.js +53 -0
  93. package/src/code/tools/common.js +42 -0
  94. package/src/code/tools/edit.js +70 -0
  95. package/src/code/tools/read.js +44 -0
  96. package/src/code/tools/write.js +35 -0
  97. package/src/code/tui.js +1580 -0
  98. package/src/config.js +47 -1
  99. package/src/context/decisions.js +12 -2
  100. package/src/context/index.js +18 -1
  101. package/src/context/sync.js +127 -0
  102. package/src/daemon/agentProcessManager.js +74 -0
  103. package/src/daemon/cronOps.js +241 -0
  104. package/src/daemon/index.js +661 -488
  105. package/src/daemon/ipcServer.js +99 -0
  106. package/src/daemon/ops.js +417 -179
  107. package/src/daemon/promptLoop.js +319 -0
  108. package/src/daemon/promptRequest.js +101 -0
  109. package/src/daemon/providerSessions.js +32 -17
  110. package/src/daemon/reporting.js +90 -0
  111. package/src/daemon/run.js +2 -5
  112. package/src/daemon/status.js +24 -1
  113. package/src/init/index.js +68 -14
  114. package/src/online/bridge.js +663 -0
  115. package/src/online/client.js +245 -0
  116. package/src/online/runner.js +253 -0
  117. package/src/online/server.js +992 -0
  118. package/src/online/tokens.js +103 -0
  119. package/src/report/store.js +331 -0
  120. package/src/shared/eventContract.js +35 -0
  121. package/src/shared/ptySocketContract.js +21 -0
  122. package/src/status/index.js +50 -17
  123. package/src/terminal/adapterContract.js +87 -0
  124. package/src/terminal/adapterRouter.js +84 -0
  125. package/src/terminal/adapters/externalAdapter.js +14 -0
  126. package/src/terminal/adapters/internalAdapter.js +13 -0
  127. package/src/terminal/adapters/internalPtyAdapter.js +42 -0
  128. package/src/terminal/adapters/internalQueueAdapter.js +37 -0
  129. package/src/terminal/adapters/terminalAdapter.js +31 -0
  130. package/src/terminal/adapters/tmuxAdapter.js +30 -0
  131. package/src/ufoo/agentsStore.js +69 -3
  132. package/src/utils/banner.js +5 -2
  133. package/scripts/.archived/bash-to-js-migration/README.md +0 -46
  134. package/scripts/.archived/bash-to-js-migration/banner.sh +0 -89
  135. package/scripts/.archived/bash-to-js-migration/bus-alert.sh +0 -6
  136. package/scripts/.archived/bash-to-js-migration/bus-autotrigger.sh +0 -6
  137. package/scripts/.archived/bash-to-js-migration/bus-daemon.sh +0 -231
  138. package/scripts/.archived/bash-to-js-migration/bus-inject.sh +0 -176
  139. package/scripts/.archived/bash-to-js-migration/bus-listen.sh +0 -6
  140. package/scripts/.archived/bash-to-js-migration/bus.sh +0 -986
  141. package/scripts/.archived/bash-to-js-migration/context-decisions.sh +0 -167
  142. package/scripts/.archived/bash-to-js-migration/context-doctor.sh +0 -72
  143. package/scripts/.archived/bash-to-js-migration/context-lint.sh +0 -110
  144. package/scripts/.archived/bash-to-js-migration/doctor.sh +0 -22
  145. package/scripts/.archived/bash-to-js-migration/init.sh +0 -247
  146. package/scripts/.archived/bash-to-js-migration/skills.sh +0 -113
  147. package/scripts/.archived/bash-to-js-migration/status.sh +0 -125
  148. package/scripts/banner.sh +0 -2
  149. package/src/bus/API_DESIGN.md +0 -204
package/src/bus/queue.js CHANGED
@@ -15,6 +15,7 @@ class QueueManager {
15
15
  constructor(busDir) {
16
16
  this.busDir = busDir;
17
17
  this.queuesDir = path.join(busDir, "queues");
18
+ this.offsetsDir = path.join(busDir, "offsets");
18
19
  }
19
20
 
20
21
  /**
@@ -38,7 +39,7 @@ class QueueManager {
38
39
  * 获取 offset 文件路径
39
40
  */
40
41
  getOffsetPath(subscriber) {
41
- return path.join(this.queuesDir, `${subscriberToSafeName(subscriber)}.offset`);
42
+ return path.join(this.offsetsDir, `${subscriberToSafeName(subscriber)}.offset`);
42
43
  }
43
44
 
44
45
  /**
@@ -91,6 +92,10 @@ class QueueManager {
91
92
  this.ensureQueueDir(subscriber);
92
93
  const pendingPath = this.getPendingPath(subscriber);
93
94
  appendJSONL(pendingPath, event);
95
+ if (event && event.event === "wake") {
96
+ const wakePath = path.join(this.getQueueDir(subscriber), "wake");
97
+ fs.writeFileSync(wakePath, String(event.seq || Date.now()), "utf8");
98
+ }
94
99
  }
95
100
 
96
101
  /**
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { getTimestamp, ensureDir, safeNameToSubscriber, getTtyProcessInfo } = require("./utils");
6
+ const { getUfooPaths } = require("../ufoo/paths");
7
+ const { loadAgentsData, saveAgentsData } = require("../ufoo/agentsStore");
8
+
9
+ function readQueueTty(queueDir) {
10
+ try {
11
+ const value = fs.readFileSync(path.join(queueDir, "tty"), "utf8").trim();
12
+ return value || "";
13
+ } catch {
14
+ return "";
15
+ }
16
+ }
17
+
18
+ function nicknamePrefixForType(agentType = "") {
19
+ return agentType === "claude-code" ? "claude"
20
+ : agentType === "ufoo-code" ? "ucode"
21
+ : String(agentType || "agent");
22
+ }
23
+
24
+ function isRecoverableSessionId(sessionId = "") {
25
+ const text = String(sessionId || "").trim();
26
+ if (!text) return false;
27
+ if (text.includes(":") || text.includes("_")) return false;
28
+ return true;
29
+ }
30
+
31
+ function buildUsedNicknameSet(agents = {}) {
32
+ const set = new Set();
33
+ for (const meta of Object.values(agents || {})) {
34
+ if (!meta || meta.status !== "active") continue;
35
+ const nick = meta && typeof meta.nickname === "string" ? meta.nickname : "";
36
+ if (nick) set.add(nick);
37
+ }
38
+ return set;
39
+ }
40
+
41
+ function allocateRecoveredNickname(agentType, used) {
42
+ const prefix = nicknamePrefixForType(agentType);
43
+ let idx = 1;
44
+ while (used.has(`${prefix}-${idx}`)) idx += 1;
45
+ const nick = `${prefix}-${idx}`;
46
+ used.add(nick);
47
+ return nick;
48
+ }
49
+
50
+ class BusStore {
51
+ constructor(projectRoot) {
52
+ this.projectRoot = projectRoot;
53
+ this.paths = getUfooPaths(projectRoot);
54
+ this.busDir = this.paths.busDir;
55
+ this.agentsFile = this.paths.agentsFile;
56
+ this.eventsDir = this.paths.busEventsDir;
57
+ this.logsDir = this.paths.busLogsDir;
58
+ }
59
+
60
+ ensure() {
61
+ if (!fs.existsSync(this.busDir) || !fs.existsSync(this.paths.agentDir)) {
62
+ throw new Error(
63
+ "Event bus not initialized. Please run: ufoo bus init or /uinit"
64
+ );
65
+ }
66
+ }
67
+
68
+ load() {
69
+ const data = loadAgentsData(this.agentsFile);
70
+ if (!data.agents || typeof data.agents !== "object") {
71
+ data.agents = {};
72
+ }
73
+
74
+ const queueRoot = path.join(this.busDir, "queues");
75
+ if (!fs.existsSync(queueRoot)) return data;
76
+
77
+ const usedNicknames = buildUsedNicknameSet(data.agents);
78
+ const now = getTimestamp();
79
+ let recovered = false;
80
+
81
+ for (const entry of fs.readdirSync(queueRoot)) {
82
+ const queueDir = path.join(queueRoot, entry);
83
+ let stat;
84
+ try {
85
+ stat = fs.statSync(queueDir);
86
+ } catch {
87
+ continue;
88
+ }
89
+ if (!stat.isDirectory()) continue;
90
+
91
+ const subscriber = safeNameToSubscriber(entry);
92
+ const parts = subscriber.split(":");
93
+ if (parts.length !== 2) continue;
94
+ const [agentType, sessionId] = parts;
95
+ if (!agentType || !sessionId) continue;
96
+ if (!isRecoverableSessionId(sessionId)) continue;
97
+ if (data.agents[subscriber]) continue;
98
+
99
+ const tty = readQueueTty(queueDir);
100
+ const ttyInfo = tty ? getTtyProcessInfo(tty) : null;
101
+ const activeByTty = Boolean(ttyInfo && ttyInfo.alive && ttyInfo.hasAgent);
102
+ const nickname = activeByTty ? allocateRecoveredNickname(agentType, usedNicknames) : "";
103
+
104
+ data.agents[subscriber] = {
105
+ agent_type: agentType,
106
+ nickname,
107
+ status: activeByTty ? "active" : "inactive",
108
+ joined_at: now,
109
+ last_seen: now,
110
+ pid: 0,
111
+ tty,
112
+ tty_shell_pid: ttyInfo && ttyInfo.shellPid ? ttyInfo.shellPid : 0,
113
+ tmux_pane: "",
114
+ launch_mode: "",
115
+ };
116
+ recovered = true;
117
+ }
118
+
119
+ if (recovered) {
120
+ saveAgentsData(this.agentsFile, data);
121
+ }
122
+ return data;
123
+ }
124
+
125
+ save(busData) {
126
+ if (busData) {
127
+ saveAgentsData(this.agentsFile, busData);
128
+ }
129
+ }
130
+
131
+ init() {
132
+ ensureDir(this.busDir);
133
+ ensureDir(this.paths.agentDir);
134
+ ensureDir(this.eventsDir);
135
+ ensureDir(path.join(this.busDir, "queues"));
136
+ ensureDir(this.logsDir);
137
+ ensureDir(path.join(this.busDir, "offsets"));
138
+ ensureDir(this.paths.busDaemonDir);
139
+ ensureDir(this.paths.busDaemonCountsDir);
140
+
141
+ if (!fs.existsSync(this.agentsFile)) {
142
+ const busData = {
143
+ created_at: getTimestamp(),
144
+ agents: {},
145
+ };
146
+ saveAgentsData(this.agentsFile, busData);
147
+ }
148
+ }
149
+
150
+ getCurrentSubscriber(busData) {
151
+ if (process.env.UFOO_SUBSCRIBER_ID) {
152
+ return process.env.UFOO_SUBSCRIBER_ID;
153
+ }
154
+
155
+ if (!fs.existsSync(this.agentsFile)) {
156
+ return null;
157
+ }
158
+
159
+ const sessionFile = path.join(this.paths.agentDir, "session.txt");
160
+ if (fs.existsSync(sessionFile)) {
161
+ const sessionId = fs.readFileSync(sessionFile, "utf8").trim();
162
+ if (sessionId) {
163
+ return sessionId;
164
+ }
165
+ }
166
+
167
+ let currentTty = null;
168
+ try {
169
+ const ttyPath = fs.realpathSync("/dev/tty");
170
+ if (ttyPath && ttyPath.startsWith("/dev/")) {
171
+ currentTty = ttyPath;
172
+ }
173
+ } catch {
174
+ // tty not available
175
+ }
176
+
177
+ if (currentTty && busData && busData.agents) {
178
+ for (const [id, meta] of Object.entries(busData.agents)) {
179
+ if (meta.tty === currentTty) {
180
+ return id;
181
+ }
182
+ }
183
+ }
184
+
185
+ return null;
186
+ }
187
+ }
188
+
189
+ module.exports = { BusStore };
@@ -3,6 +3,13 @@ const { getTimestamp, isAgentPidAlive, isMetaActive, isValidTty, getTtyProcessIn
3
3
  const NicknameManager = require("./nickname");
4
4
  const { spawnSync } = require("child_process");
5
5
 
6
+ function detectTerminalAppFromEnv() {
7
+ const termProgram = process.env.TERM_PROGRAM || "";
8
+ if (process.env.ITERM_SESSION_ID || termProgram === "iTerm.app") return "iterm2";
9
+ if (termProgram === "Apple_Terminal") return "terminal";
10
+ return termProgram || "";
11
+ }
12
+
6
13
  /**
7
14
  * 获取当前终端的 tty 路径
8
15
  */
@@ -43,6 +50,10 @@ function tryTtyWithStdin(fd) {
43
50
  }
44
51
 
45
52
  function getTtyPath() {
53
+ // 0) Honor explicit ttyPath from node stdio if present (useful for tests)
54
+ const stdinTtyPath = normalizeTty(process.stdin?.ttyPath || "");
55
+ if (stdinTtyPath) return stdinTtyPath;
56
+
46
57
  // 1) Try stdin directly (inherits real tty if present)
47
58
  let ttyPath = tryTtyWithStdin(0);
48
59
  if (ttyPath) return ttyPath;
@@ -145,8 +156,9 @@ class SubscriberManager {
145
156
  }
146
157
  finalNickname = nickname;
147
158
  } else {
148
- // 自动生成昵称
159
+ // 自动生成昵称(并标记占用,避免并发重复)
149
160
  finalNickname = nicknameManager.generateAutoNickname(agentType);
161
+ nicknameManager.setNickname(subscriber, finalNickname);
150
162
  }
151
163
 
152
164
  const launchMode = options.launchMode || process.env.UFOO_LAUNCH_MODE || "";
@@ -186,6 +198,11 @@ class SubscriberManager {
186
198
  launch_mode: launchMode,
187
199
  };
188
200
 
201
+ const terminalApp = options.terminalApp || detectTerminalAppFromEnv();
202
+ if (terminalApp) {
203
+ this.busData.agents[subscriber].terminal_app = terminalApp;
204
+ }
205
+
189
206
  // 如果传入了 providerSessionId(从旧 session 恢复),设置它
190
207
  if (options.providerSessionId) {
191
208
  this.busData.agents[subscriber].provider_session_id = options.providerSessionId;
@@ -284,10 +301,9 @@ class SubscriberManager {
284
301
  if (!this.busData.agents) return;
285
302
 
286
303
  for (const [id, meta] of Object.entries(this.busData.agents)) {
287
- if (meta.status !== "active") continue;
288
- // PID 已死则直接标记 inactive(不依赖 tty 检测,因为 tty 可能被新 agent 复用)
289
- if (meta.pid && !isAgentPidAlive(meta.pid)) {
304
+ if (meta.status === "active" && !isMetaActive(meta)) {
290
305
  meta.status = "inactive";
306
+ meta.last_seen = getTimestamp();
291
307
  }
292
308
  }
293
309
  }
package/src/bus/utils.js CHANGED
@@ -86,7 +86,7 @@ function isAgentPidAlive(pid) {
86
86
  if (!isPidAlive(pid)) return false;
87
87
  const cmd = getPidCommand(pid);
88
88
  if (!cmd) return true;
89
- return /(claude|codex|node)/i.test(cmd);
89
+ return /(claude|codex|node|pi-mono|ufoo|ucode)/i.test(cmd);
90
90
  }
91
91
 
92
92
  /**
@@ -150,7 +150,7 @@ function getTtyProcessInfo(ttyPath) {
150
150
  return { alive: false, idle: false, hasAgent: false, shellPid: 0, processes: [] };
151
151
  }
152
152
  const shellNames = new Set(["zsh", "bash", "fish", "sh", "login"]);
153
- const hasAgent = processes.some((p) => /(codex|claude|node)/i.test(p.comm));
153
+ const hasAgent = processes.some((p) => /(codex|claude|node|pi-mono|ufoo|ucode)/i.test(p.comm));
154
154
  const nonShell = processes.filter((p) => !shellNames.has(p.comm));
155
155
  const idle = !hasAgent && nonShell.length === 0;
156
156
  const shell = processes.find((p) => shellNames.has(p.comm));
@@ -184,7 +184,8 @@ function appendFileAtomic(filePath, content) {
184
184
  */
185
185
  function writeFileAtomic(filePath, content) {
186
186
  ensureDir(path.dirname(filePath));
187
- const tmpFile = `${filePath}.tmp.${Date.now()}`;
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")}`;
188
189
  fs.writeFileSync(tmpFile, content, "utf8");
189
190
  fs.renameSync(tmpFile, filePath);
190
191
  }
@@ -254,6 +255,10 @@ function truncateFile(filePath) {
254
255
  }
255
256
  }
256
257
 
258
+ function sleep(ms) {
259
+ return new Promise((resolve) => setTimeout(resolve, ms));
260
+ }
261
+
257
262
  /**
258
263
  * 日志输出(带颜色)
259
264
  */
@@ -349,6 +354,7 @@ module.exports = {
349
354
  appendJSONL,
350
355
  readLastLine,
351
356
  truncateFile,
357
+ sleep,
352
358
  logInfo,
353
359
  logOk,
354
360
  logWarn,
@@ -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
+ };