u-foo 2.3.32 → 2.4.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 (235) hide show
  1. package/README.md +157 -213
  2. package/README.zh-CN.md +151 -197
  3. package/SKILLS/ufoo/SKILL.md +8 -8
  4. package/bin/uagy.js +69 -0
  5. package/bin/uclaude.js +2 -2
  6. package/bin/ucode.js +4 -4
  7. package/bin/ucodex.js +2 -2
  8. package/bin/ufoo.js +5 -23
  9. package/modules/AGENTS.template.md +1 -1
  10. package/modules/bus/SKILLS/ubus/SKILL.md +35 -10
  11. package/package.json +5 -5
  12. package/scripts/chat-app-smoke.js +1 -1
  13. package/scripts/global-chat-switch-benchmark.js +5 -5
  14. package/scripts/ink-demo.js +1 -1
  15. package/scripts/ink-smoke.js +1 -1
  16. package/scripts/ucode-app-smoke.js +1 -1
  17. package/src/{agent → agents/activity}/activityDetector.js +39 -2
  18. package/src/{agent → agents/activity}/activityStatePublisher.js +1 -1
  19. package/src/{agent → agents/activity}/activityStateWriter.js +2 -2
  20. package/src/{agent → agents/activity}/activityTracker.js +1 -1
  21. package/src/agents/activity/index.js +8 -0
  22. package/src/{agent → agents/controller}/controllerToolExecutor.js +4 -4
  23. package/src/agents/controller/index.js +8 -0
  24. package/src/{agent → agents/controller}/loopObservability.js +2 -2
  25. package/src/{agent → agents/controller}/loopRuntime.js +1 -1
  26. package/src/{agent → agents/controller}/ufooAgent.js +9 -9
  27. package/src/agents/index.js +10 -0
  28. package/src/agents/internal/index.js +3 -0
  29. package/src/{agent → agents/internal}/internalRunner.js +45 -22
  30. package/src/agents/launch/agyConversation.js +159 -0
  31. package/src/agents/launch/index.js +12 -0
  32. package/src/{agent → agents/launch}/launchEnvironment.js +2 -3
  33. package/src/{agent → agents/launch}/launcher.js +64 -21
  34. package/src/{agent → agents/launch}/notifier.js +23 -12
  35. package/src/{agent → agents/launch}/ptyRunner.js +44 -12
  36. package/src/{agent → agents/launch}/ptyWrapper.js +2 -2
  37. package/src/{agent → agents/launch}/publisherRouting.js +1 -1
  38. package/src/{agent → agents/launch}/readyDetector.js +23 -0
  39. package/src/{agent → agents/prompts}/defaultBootstrap.js +63 -4
  40. package/src/{group/bootstrap.js → agents/prompts/groupBootstrap.js} +41 -6
  41. package/src/agents/prompts/index.js +8 -0
  42. package/src/{code/prompts → agents/prompts/native}/index.js +1 -1
  43. package/src/{agent → agents/providers}/claudeThreadProvider.js +1 -1
  44. package/src/{agent → agents/providers}/codexThreadProvider.js +1 -1
  45. package/src/{agent → agents/providers}/directAuthStatus.js +184 -1
  46. package/src/agents/providers/index.js +13 -0
  47. package/src/{agent → agents/providers}/upstreamTransport.js +2 -2
  48. package/src/{chat → app/chat}/agentSockets.js +1 -1
  49. package/src/{chat → app/chat}/commandExecutor.js +50 -26
  50. package/src/{chat → app/chat}/commands.js +119 -5
  51. package/src/{chat → app/chat}/daemonConnection.js +1 -1
  52. package/src/{chat → app/chat}/daemonMessageRouter.js +45 -3
  53. package/src/{chat → app/chat}/dashboardView.js +2 -1
  54. package/src/app/chat/index.js +6 -0
  55. package/src/{chat → app/chat}/inputSubmitHandler.js +4 -13
  56. package/src/{chat → app/chat}/internalAgentLogHistory.js +1 -1
  57. package/src/app/chat/multiWindow/index.js +268 -0
  58. package/src/app/chat/multiWindow/paneLayout.js +84 -0
  59. package/src/app/chat/multiWindow/paneManager.js +299 -0
  60. package/src/app/chat/multiWindow/renderer.js +384 -0
  61. package/src/app/chat/multiWindow/virtualTerminal.js +327 -0
  62. package/src/{chat → app/chat}/transport.js +1 -1
  63. package/src/{cli → app/cli}/ctxCoreCommands.js +3 -3
  64. package/src/{doctor/index.js → app/cli/features/doctor.js} +1 -1
  65. package/src/{init/index.js → app/cli/features/init.js} +14 -32
  66. package/src/{cli → app/cli}/groupCoreCommands.js +2 -2
  67. package/src/app/cli/index.js +9 -0
  68. package/src/{cli → app/cli}/onlineCoreCommands.js +5 -5
  69. package/src/{cli.js → app/cli/run.js} +59 -57
  70. package/src/app/index.js +6 -0
  71. package/src/code/agent.js +10 -9
  72. package/src/code/index.js +2 -0
  73. package/src/code/launcher/index.js +9 -0
  74. package/src/{agent → code/launcher}/ucode.js +7 -8
  75. package/src/{agent → code/launcher}/ucodeBootstrap.js +3 -3
  76. package/src/{agent → code/launcher}/ucodeBuild.js +2 -2
  77. package/src/{agent → code/launcher}/ucodeDoctor.js +2 -2
  78. package/src/{agent → code/launcher}/ucodeRuntimeConfig.js +1 -2
  79. package/src/code/nativeRunner.js +4 -4
  80. package/src/code/tui.js +3 -1454
  81. package/src/config.js +15 -2
  82. package/src/{bus → coordination/bus}/activate.js +2 -2
  83. package/src/{bus → coordination/bus}/daemon.js +15 -5
  84. package/src/coordination/bus/envelope.js +173 -0
  85. package/src/{bus → coordination/bus}/index.js +7 -3
  86. package/src/{bus → coordination/bus}/inject.js +11 -3
  87. package/src/{bus → coordination/bus}/message.js +1 -1
  88. package/src/coordination/bus/messageMeta.js +130 -0
  89. package/src/coordination/bus/promptEnvelope.js +65 -0
  90. package/src/{bus → coordination/bus}/shake.js +1 -1
  91. package/src/{bus → coordination/bus}/store.js +3 -3
  92. package/src/{bus → coordination/bus}/subscriber.js +2 -2
  93. package/src/{bus → coordination/bus}/utils.js +2 -2
  94. package/src/{history → coordination/history}/inputTimeline.js +5 -5
  95. package/src/coordination/index.js +10 -0
  96. package/src/{memory → coordination/memory}/historySearch.js +1 -1
  97. package/src/{memory → coordination/memory}/index.js +3 -3
  98. package/src/{report → coordination/report}/store.js +2 -2
  99. package/src/{status → coordination/status}/index.js +3 -3
  100. package/src/online/bridge.js +2 -2
  101. package/src/{controller → orchestration/controller}/flags.js +1 -1
  102. package/src/{controller → orchestration/controller}/gateRouter.js +1 -1
  103. package/src/orchestration/controller/index.js +10 -0
  104. package/src/{controller → orchestration/controller}/shadowGuard.js +1 -1
  105. package/src/orchestration/groups/bootstrap.js +3 -0
  106. package/src/orchestration/groups/index.js +10 -0
  107. package/src/orchestration/groups/promptProfiles.js +3 -0
  108. package/src/{group → orchestration/groups}/templates.js +1 -1
  109. package/src/{group → orchestration/groups}/validateTemplate.js +1 -1
  110. package/src/orchestration/index.js +7 -0
  111. package/src/orchestration/solo/index.js +3 -0
  112. package/src/{daemon → runtime/daemon}/agentProcessManager.js +1 -1
  113. package/src/{daemon → runtime/daemon}/cronOps.js +3 -2
  114. package/src/{daemon → runtime/daemon}/groupOrchestrator.js +26 -9
  115. package/src/{daemon → runtime/daemon}/index.js +105 -53
  116. package/src/{daemon → runtime/daemon}/ipcServer.js +1 -1
  117. package/src/{daemon → runtime/daemon}/nicknameScope.js +6 -3
  118. package/src/{daemon → runtime/daemon}/ops.js +48 -61
  119. package/src/{daemon → runtime/daemon}/promptLoop.js +1 -1
  120. package/src/{daemon → runtime/daemon}/promptRequest.js +7 -7
  121. package/src/runtime/daemon/providerSessions.js +230 -0
  122. package/src/{daemon → runtime/daemon}/reporting.js +4 -4
  123. package/src/{daemon → runtime/daemon}/run.js +4 -4
  124. package/src/{daemon → runtime/daemon}/soloBootstrap.js +7 -7
  125. package/src/{daemon → runtime/daemon}/status.js +5 -5
  126. package/src/runtime/index.js +10 -0
  127. package/src/{projects → runtime/projects}/registry.js +1 -1
  128. package/src/{terminal → runtime/terminal}/adapterRouter.js +0 -10
  129. package/src/{terminal → runtime/terminal}/adapters/internalAdapter.js +0 -4
  130. package/src/tools/handlers/common.js +1 -1
  131. package/src/tools/handlers/listAgents.js +1 -1
  132. package/src/tools/handlers/memory.js +3 -3
  133. package/src/tools/handlers/readBusSummary.js +1 -1
  134. package/src/tools/handlers/readOpenDecisions.js +1 -1
  135. package/src/tools/handlers/readProjectRegistry.js +1 -1
  136. package/src/tools/handlers/readPromptHistory.js +2 -2
  137. package/src/tools/schemaFixtures.js +1 -1
  138. package/src/ui/MIGRATION.md +42 -88
  139. package/src/ui/format/index.js +5 -28
  140. package/src/ui/index.js +1 -1
  141. package/src/ui/{components → ink}/ChatApp.js +812 -88
  142. package/src/ui/ink/DashboardBar.js +685 -0
  143. package/src/ui/{components → ink}/MultilineInput.js +230 -5
  144. package/src/ui/{components → ink}/UcodeApp.js +16 -7
  145. package/src/ui/{components → ink}/agentMirror.js +24 -19
  146. package/src/ui/{components → ink}/chatReducer.js +29 -7
  147. package/src/bus/messageMeta.js +0 -52
  148. package/src/chat/agentViewController.js +0 -1072
  149. package/src/chat/chatLogController.js +0 -138
  150. package/src/chat/completionController.js +0 -533
  151. package/src/chat/dashboardKeyController.js +0 -533
  152. package/src/chat/index.js +0 -2222
  153. package/src/chat/inputHistoryController.js +0 -135
  154. package/src/chat/inputListenerController.js +0 -470
  155. package/src/chat/layout.js +0 -186
  156. package/src/chat/pasteController.js +0 -81
  157. package/src/chat/statusLineController.js +0 -223
  158. package/src/chat/streamTracker.js +0 -156
  159. package/src/code/config +0 -0
  160. package/src/daemon/providerSessions.js +0 -488
  161. package/src/terminal/adapters/internalPtyAdapter.js +0 -42
  162. package/src/ui/components/DashboardBar.js +0 -417
  163. /package/src/{code/prompts → agents/prompts/native}/actions.js +0 -0
  164. /package/src/{code/prompts → agents/prompts/native}/efficiency.js +0 -0
  165. /package/src/{code/prompts → agents/prompts/native}/environment.js +0 -0
  166. /package/src/{code/prompts → agents/prompts/native}/identity.js +0 -0
  167. /package/src/{code/prompts → agents/prompts/native}/safety.js +0 -0
  168. /package/src/{code/prompts → agents/prompts/native}/sections.js +0 -0
  169. /package/src/{code/prompts → agents/prompts/native}/system.js +0 -0
  170. /package/src/{code/prompts → agents/prompts/native}/tasks.js +0 -0
  171. /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/bash.js +0 -0
  172. /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/edit.js +0 -0
  173. /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/read.js +0 -0
  174. /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/write.js +0 -0
  175. /package/src/{code/prompts → agents/prompts/native}/ufoo.js +0 -0
  176. /package/src/{group → agents/prompts}/promptProfiles.js +0 -0
  177. /package/src/{agent → agents/providers}/claudeEventTranslator.js +0 -0
  178. /package/src/{agent → agents/providers}/claudeOauthTokenReader.js +0 -0
  179. /package/src/{agent → agents/providers}/claudeSessionFiles.js +0 -0
  180. /package/src/{agent → agents/providers}/codexEventTranslator.js +0 -0
  181. /package/src/{agent → agents/providers}/credentials/claude.js +0 -0
  182. /package/src/{agent → agents/providers}/credentials/codex.js +0 -0
  183. /package/src/{agent → agents/providers}/credentials/index.js +0 -0
  184. /package/src/{chat → app/chat}/agentBar.js +0 -0
  185. /package/src/{chat → app/chat}/agentDirectory.js +0 -0
  186. /package/src/{chat → app/chat}/cronScheduler.js +0 -0
  187. /package/src/{chat → app/chat}/daemonCoordinator.js +0 -0
  188. /package/src/{chat → app/chat}/daemonReconnect.js +0 -0
  189. /package/src/{chat → app/chat}/daemonTransport.js +0 -0
  190. /package/src/{chat → app/chat}/daemonTransportDefaults.js +0 -0
  191. /package/src/{chat → app/chat}/inputMath.js +0 -0
  192. /package/src/{chat → app/chat}/projectCloseController.js +0 -0
  193. /package/src/{chat → app/chat}/rawKeyMap.js +0 -0
  194. /package/src/{chat → app/chat}/settingsController.js +0 -0
  195. /package/src/{chat → app/chat}/shellCommand.js +0 -0
  196. /package/src/{chat → app/chat}/text.js +0 -0
  197. /package/src/{chat → app/chat}/transientAgentState.js +0 -0
  198. /package/src/{cli → app/cli}/busCoreCommands.js +0 -0
  199. /package/src/{skills/index.js → app/cli/features/skills.js} +0 -0
  200. /package/src/{bus → coordination/bus}/nickname.js +0 -0
  201. /package/src/{bus → coordination/bus}/queue.js +0 -0
  202. /package/src/{context → coordination/context}/decisions.js +0 -0
  203. /package/src/{context → coordination/context}/doctor.js +0 -0
  204. /package/src/{context → coordination/context}/index.js +0 -0
  205. /package/src/{context → coordination/context}/sync.js +0 -0
  206. /package/src/{ufoo → coordination/state}/agentRegistryDiagnostics.js +0 -0
  207. /package/src/{ufoo → coordination/state}/agentsStore.js +0 -0
  208. /package/src/{ufoo → coordination/state}/paths.js +0 -0
  209. /package/src/{controller → orchestration/controller}/launchRouting.js +0 -0
  210. /package/src/{controller → orchestration/controller}/routerFastPath.js +0 -0
  211. /package/src/{controller → orchestration/controller}/routerFinalize.js +0 -0
  212. /package/src/{group → orchestration/groups}/diagram.js +0 -0
  213. /package/src/{group → orchestration/groups}/templateValidation.js +0 -0
  214. /package/src/{solo → orchestration/solo}/commands.js +0 -0
  215. /package/src/{shared → runtime/contracts}/eventContract.js +0 -0
  216. /package/src/{shared → runtime/contracts}/ptySocketContract.js +0 -0
  217. /package/src/{providerapi → runtime/privacy}/redactor.js +0 -0
  218. /package/src/{providerapi → runtime/privacy}/shadowDiff.js +0 -0
  219. /package/src/{utils → runtime/process}/nodeExecutable.js +0 -0
  220. /package/src/{projects → runtime/projects}/identity.js +0 -0
  221. /package/src/{projects → runtime/projects}/index.js +0 -0
  222. /package/src/{projects → runtime/projects}/projectId.js +0 -0
  223. /package/src/{projects → runtime/projects}/runtimes.js +0 -0
  224. /package/src/{terminal → runtime/terminal}/adapterContract.js +0 -0
  225. /package/src/{terminal → runtime/terminal}/adapters/externalAdapter.js +0 -0
  226. /package/src/{terminal → runtime/terminal}/adapters/hostAdapter.js +0 -0
  227. /package/src/{terminal → runtime/terminal}/adapters/internalQueueAdapter.js +0 -0
  228. /package/src/{terminal → runtime/terminal}/adapters/terminalAdapter.js +0 -0
  229. /package/src/{terminal → runtime/terminal}/adapters/tmuxAdapter.js +0 -0
  230. /package/src/{terminal → runtime/terminal}/detect.js +0 -0
  231. /package/src/{terminal → runtime/terminal}/index.js +0 -0
  232. /package/src/{terminal → runtime/terminal}/iterm2.js +0 -0
  233. /package/src/{utils → ui/format}/banner.js +0 -0
  234. /package/src/{shared → ui/format}/markdownRenderer.js +0 -0
  235. /package/src/ui/{components → ink}/InkDemo.js +0 -0
@@ -0,0 +1,685 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * DashboardBar — the bottom 1-2 rows in chat showing the current dashboard
5
+ * mode (projects rail in global mode + one of agents/mode/provider/cron).
6
+ * Each row is laid out inside a hard cell budget so narrow terminals never
7
+ * spill content onto the next line: chips are dropped into "<…>" overflow
8
+ * markers, the trailing hint is sacrificed first, and Loop summary fields
9
+ * are progressively trimmed. The final row is folded into a single
10
+ * <Text wrap="truncate"> so ink truncates predictably regardless of the
11
+ * inline color spans.
12
+ */
13
+
14
+ const chalk = require("chalk");
15
+
16
+ const { clampAgentWindowWithSelection } = require("../../app/chat/agentDirectory");
17
+ const { providerLabel } = require("../../app/chat/dashboardView");
18
+ const { displayCellWidth, planProjectsRail } = require("../format");
19
+
20
+ const CHIP_SEP = " ";
21
+ const CHIP_SEP_WIDTH = displayCellWidth(CHIP_SEP);
22
+ const SUMMARY_GAP = " ";
23
+ const SUMMARY_GAP_WIDTH = displayCellWidth(SUMMARY_GAP);
24
+ const HINT_PREFIX = " · ";
25
+ const HINT_PREFIX_WIDTH = displayCellWidth(HINT_PREFIX);
26
+
27
+ function truncateToCells(text = "", cells = 0) {
28
+ const limit = Math.max(0, Math.floor(Number(cells) || 0));
29
+ const value = String(text || "");
30
+ if (limit <= 0) return "";
31
+ if (displayCellWidth(value) <= limit) return value;
32
+ if (limit <= 1) return "…";
33
+ let out = "";
34
+ let used = 0;
35
+ const body = limit - 1;
36
+ for (const ch of value) {
37
+ const w = displayCellWidth(ch);
38
+ if (used + w > body) break;
39
+ out += ch;
40
+ used += w;
41
+ }
42
+ return `${out || value.slice(0, 1)}…`;
43
+ }
44
+
45
+ function ensureAtPrefix(value) {
46
+ const text = String(value || "").trim();
47
+ if (!text) return text;
48
+ return text.startsWith("@") ? text : `@${text}`;
49
+ }
50
+
51
+ function activityMarker(state = "") {
52
+ const normalized = String(state || "").trim().toLowerCase();
53
+ if (normalized === "working") return "*";
54
+ if (normalized === "waiting_input") return "?";
55
+ if (normalized === "blocked") return "!";
56
+ return "";
57
+ }
58
+
59
+ function withActivityMarker(label = "", state = "") {
60
+ const marker = activityMarker(state);
61
+ return marker ? `${marker}${label}` : label;
62
+ }
63
+
64
+ function formatToolDistribution(items = []) {
65
+ const tools = Array.isArray(items) ? items : [];
66
+ if (tools.length === 0) return "";
67
+ const visible = tools.slice(0, 2).map((item) => `${item.name}x${item.count}`);
68
+ if (tools.length > 2) visible.push(`+${tools.length - 2}`);
69
+ return visible.join(",");
70
+ }
71
+
72
+ function formatLoopSummary(loopSummary) {
73
+ if (!loopSummary || typeof loopSummary !== "object") return "";
74
+ const rounds = Number(loopSummary.rounds) || 0;
75
+ const toolCalls = Number(loopSummary.tool_calls) || 0;
76
+ const totalTokens = Number(loopSummary.total_tokens) || 0;
77
+ const cacheReadTokens = Number(loopSummary.cache_read_tokens) || 0;
78
+ const cacheCreationTokens = Number(loopSummary.cache_creation_tokens) || 0;
79
+ const terminalReason = String(loopSummary.terminal_reason || "").trim();
80
+ const toolDistribution = formatToolDistribution(loopSummary.tools);
81
+ if (rounds <= 0 && toolCalls <= 0 && totalTokens <= 0 && !terminalReason && !toolDistribution) return "";
82
+ const parts = [`r${rounds}`, `tc${toolCalls}`, `tok${totalTokens}`];
83
+ if (cacheReadTokens > 0 || cacheCreationTokens > 0) {
84
+ parts.push(`cache${cacheReadTokens}/${cacheCreationTokens}`);
85
+ }
86
+ if (toolDistribution) parts.push(toolDistribution);
87
+ if (terminalReason) parts.push(terminalReason);
88
+ return parts.join(" ");
89
+ }
90
+
91
+ function projectName(row) {
92
+ return String(
93
+ (row && (row.project_name || row.label || row.id || row.project_root || row.root)) || "-"
94
+ );
95
+ }
96
+
97
+ function projectRoot(row) {
98
+ return String((row && (row.project_root || row.root)) || "");
99
+ }
100
+
101
+ /**
102
+ * Generic chip row planner. Returns the visible slice that fits inside
103
+ * `maxWidth` (caption + chips + optional `< / >` overflow markers + optional
104
+ * trailing hint). Hint is dropped first when budget is tight; chips are
105
+ * windowed around `selectedIndex`. Empty rail (no items) is handled by the
106
+ * caller via `emptyLabel`.
107
+ */
108
+ function planChipsRow({
109
+ caption,
110
+ labels,
111
+ selectedIndex = -1,
112
+ windowStart = 0,
113
+ hint = "",
114
+ maxWidth = 80,
115
+ reserveHintWhenFocused = false,
116
+ } = {}) {
117
+ const items = Array.isArray(labels) ? labels.map(String) : [];
118
+ const totalBudget = Math.max(1, Math.floor(Number(maxWidth) || 80));
119
+ const captionText = `${caption}: `;
120
+ const captionWidth = displayCellWidth(captionText);
121
+ const railBudget = Math.max(1, totalBudget - captionWidth);
122
+ const hintText = String(hint || "");
123
+ const hintWidth = hintText ? HINT_PREFIX_WIDTH + displayCellWidth(hintText) : 0;
124
+ const minChipCells = reserveHintWhenFocused ? 4 : 1;
125
+ const canShowHint = hintText && railBudget - hintWidth >= minChipCells;
126
+ const finalHint = canShowHint ? hintText : "";
127
+ const railOnlyBudget = Math.max(1, railBudget - (finalHint ? hintWidth : 0));
128
+ const planned = planProjectsRail({
129
+ labels: items,
130
+ selectedIndex,
131
+ windowStart: windowStart || 0,
132
+ maxCells: railOnlyBudget,
133
+ });
134
+ return {
135
+ captionText,
136
+ visible: planned.items,
137
+ leftMore: planned.leftMore,
138
+ rightMore: planned.rightMore,
139
+ windowStart: planned.windowStart,
140
+ hint: finalHint,
141
+ };
142
+ }
143
+
144
+ function buildSummaryRow(options = {}) {
145
+ const {
146
+ activeAgents = [],
147
+ activeAgentId = "",
148
+ getAgentLabel = (id) => id,
149
+ getAgentState = () => "",
150
+ launchMode = "terminal",
151
+ agentProvider = "codex-cli",
152
+ cronTasks = [],
153
+ loopSummary = null,
154
+ } = options;
155
+ // agentItems carries the full active list so the renderer can greedy-fit
156
+ // chips into whatever cell budget the terminal gives us. The legacy
157
+ // 3-chip + "+N more" form is preserved on `parts[0].value` for callers
158
+ // that consume the plain text (chat history, banner, tests).
159
+ const allItems = activeAgents.map((id) => {
160
+ const active = Boolean(activeAgentId && id === activeAgentId);
161
+ return {
162
+ label: withActivityMarker(ensureAtPrefix(getAgentLabel(id)), getAgentState(id)),
163
+ selected: false,
164
+ active,
165
+ };
166
+ });
167
+ const agentItems = allItems;
168
+ const visibleForText = allItems.slice(0, 3);
169
+ const agents = activeAgents.length > 0
170
+ ? visibleForText.map((item) => item.label).join(", ")
171
+ + (activeAgents.length > 3 ? ` +${activeAgents.length - 3}` : "")
172
+ : "none";
173
+ const parts = [
174
+ { label: "Agents", value: agents },
175
+ { label: "Mode", value: launchMode },
176
+ { label: "Agent", value: providerLabel(agentProvider) },
177
+ { label: "Cron", value: String(Array.isArray(cronTasks) ? cronTasks.length : 0) },
178
+ ];
179
+ const loopPart = formatLoopSummary(loopSummary);
180
+ if (loopPart) parts.push({ label: "Loop", value: loopPart });
181
+ return {
182
+ kind: "summary",
183
+ agentItems,
184
+ agentExtraCount: 0,
185
+ parts,
186
+ };
187
+ }
188
+
189
+ function buildProjectRow(options = {}) {
190
+ const {
191
+ projects = [],
192
+ selectedProjectIndex = -1,
193
+ projectListWindowStart = 0,
194
+ maxWidth = 80,
195
+ activeProjectRoot = "",
196
+ focused = false,
197
+ globalScope = "controller",
198
+ } = options;
199
+ const rows = Array.isArray(projects) ? projects : [];
200
+ if (rows.length === 0) {
201
+ return {
202
+ kind: "chips",
203
+ caption: "Projects",
204
+ items: [],
205
+ emptyLabel: "none",
206
+ hint: options.dashHints && options.dashHints.projectsEmpty
207
+ ? options.dashHints.projectsEmpty
208
+ : "Run ufoo chat or ufoo daemon start in project directories",
209
+ windowStart: projectListWindowStart,
210
+ };
211
+ }
212
+ const fallbackIndex = rows.findIndex((row) => projectRoot(row) === String(activeProjectRoot || ""));
213
+ const selected = selectedProjectIndex >= 0 && selectedProjectIndex < rows.length
214
+ ? selectedProjectIndex
215
+ : (fallbackIndex >= 0 ? fallbackIndex : 0);
216
+ const requestedHint = focused ? (globalScope === "controller" ? "Enter→project" : "Esc→global") : "";
217
+ const planned = planChipsRow({
218
+ caption: "Projects",
219
+ labels: rows.map(projectName),
220
+ selectedIndex: focused ? selected : -1,
221
+ windowStart: projectListWindowStart || 0,
222
+ hint: requestedHint,
223
+ maxWidth,
224
+ reserveHintWhenFocused: focused,
225
+ });
226
+ return {
227
+ kind: "chips",
228
+ caption: "Projects",
229
+ leftMore: planned.leftMore,
230
+ rightMore: planned.rightMore,
231
+ windowStart: planned.windowStart,
232
+ hint: planned.hint,
233
+ items: planned.visible.map((item) => {
234
+ const idx = item.absoluteIndex;
235
+ const row = rows[idx];
236
+ const root = projectRoot(row);
237
+ return {
238
+ label: item.label,
239
+ selected: focused && idx === selected,
240
+ active: Boolean(activeProjectRoot && root === activeProjectRoot),
241
+ };
242
+ }),
243
+ };
244
+ }
245
+
246
+ function buildDetailRow(options = {}) {
247
+ const {
248
+ dashboardView = "agents",
249
+ globalMode = false,
250
+ activeAgents = [],
251
+ activeAgentId = "",
252
+ selectedAgentIndex = -1,
253
+ agentListWindowStart = 0,
254
+ maxAgentWindow = 4,
255
+ maxWidth = 80,
256
+ getAgentLabel = (id) => id,
257
+ getAgentState = () => "",
258
+ selectedModeIndex = 0,
259
+ selectedProviderIndex = 0,
260
+ selectedCronIndex = -1,
261
+ modeOptions = [],
262
+ providerOptions = [],
263
+ cronTasks = [],
264
+ dashHints = {},
265
+ focused = false,
266
+ } = options;
267
+
268
+ if (dashboardView === "mode") {
269
+ const labels = (modeOptions || []).map((label) => String(label));
270
+ const planned = planChipsRow({
271
+ caption: "Mode",
272
+ labels,
273
+ selectedIndex: focused ? selectedModeIndex : -1,
274
+ hint: dashHints.mode || "",
275
+ maxWidth,
276
+ reserveHintWhenFocused: focused,
277
+ });
278
+ return {
279
+ kind: "chips",
280
+ caption: "Mode",
281
+ hint: planned.hint,
282
+ leftMore: planned.leftMore,
283
+ rightMore: planned.rightMore,
284
+ items: planned.visible.map((item) => ({
285
+ label: item.label,
286
+ selected: focused && item.absoluteIndex === selectedModeIndex,
287
+ })),
288
+ };
289
+ }
290
+ if (dashboardView === "provider") {
291
+ const opts = Array.isArray(providerOptions) ? providerOptions : [];
292
+ const labels = opts.map((opt) => String(opt && opt.label != null ? opt.label : opt));
293
+ const planned = planChipsRow({
294
+ caption: "Agent",
295
+ labels,
296
+ selectedIndex: focused ? selectedProviderIndex : -1,
297
+ hint: dashHints.provider || "",
298
+ maxWidth,
299
+ reserveHintWhenFocused: focused,
300
+ });
301
+ return {
302
+ kind: "chips",
303
+ caption: "Agent",
304
+ hint: planned.hint,
305
+ leftMore: planned.leftMore,
306
+ rightMore: planned.rightMore,
307
+ items: planned.visible.map((item) => ({
308
+ label: item.label,
309
+ selected: focused && item.absoluteIndex === selectedProviderIndex,
310
+ })),
311
+ };
312
+ }
313
+ if (dashboardView === "cron") {
314
+ const items = Array.isArray(cronTasks) ? cronTasks : [];
315
+ const labels = items
316
+ .map((item) => String(item.label || item.summary || item.id || ""))
317
+ .filter(Boolean);
318
+ if (labels.length === 0) {
319
+ return {
320
+ kind: "chips",
321
+ caption: "Cron",
322
+ hint: dashHints.cron || "",
323
+ emptyLabel: "none",
324
+ items: [],
325
+ };
326
+ }
327
+ const planned = planChipsRow({
328
+ caption: "Cron",
329
+ labels,
330
+ selectedIndex: focused ? selectedCronIndex : -1,
331
+ hint: dashHints.cron || "",
332
+ maxWidth,
333
+ reserveHintWhenFocused: focused,
334
+ });
335
+ return {
336
+ kind: "chips",
337
+ caption: "Cron",
338
+ hint: planned.hint,
339
+ leftMore: planned.leftMore,
340
+ rightMore: planned.rightMore,
341
+ items: planned.visible.map((item) => ({
342
+ label: item.label,
343
+ selected: focused && item.absoluteIndex === selectedCronIndex,
344
+ })),
345
+ };
346
+ }
347
+
348
+ if (!activeAgents.length) {
349
+ return {
350
+ kind: "chips",
351
+ caption: "Agents",
352
+ emptyLabel: "none",
353
+ hint: dashHints.agentsEmpty || "",
354
+ items: [],
355
+ windowStart: agentListWindowStart,
356
+ };
357
+ }
358
+ const labels = activeAgents.map((agentId) => withActivityMarker(
359
+ ensureAtPrefix(getAgentLabel(agentId)),
360
+ getAgentState(agentId)
361
+ ));
362
+ // Keep the legacy `maxAgentWindow` as an upper bound on visible chips even
363
+ // when there's plenty of horizontal room, so the agents row scrolls for
364
+ // long lists in the same way the legacy blessed view did.
365
+ const cap = Math.max(1, Math.min(maxAgentWindow || labels.length, labels.length));
366
+ let cappedStart = clampAgentWindowWithSelection({
367
+ activeCount: labels.length,
368
+ maxWindow: cap,
369
+ windowStart: agentListWindowStart || 0,
370
+ selectionIndex: selectedAgentIndex,
371
+ });
372
+ const cappedLabels = labels.slice(cappedStart, cappedStart + cap);
373
+ const cappedSelected = focused && selectedAgentIndex >= cappedStart
374
+ ? selectedAgentIndex - cappedStart
375
+ : -1;
376
+ const planned = planChipsRow({
377
+ caption: "Agents",
378
+ labels: cappedLabels,
379
+ selectedIndex: cappedSelected,
380
+ windowStart: 0,
381
+ hint: globalMode ? (dashHints.agentsGlobal || dashHints.agents || "") : (dashHints.agents || ""),
382
+ maxWidth,
383
+ reserveHintWhenFocused: focused,
384
+ });
385
+ return {
386
+ kind: "chips",
387
+ caption: "Agents",
388
+ leftMore: cappedStart > 0 || planned.leftMore,
389
+ rightMore: (cappedStart + cap < labels.length) || planned.rightMore,
390
+ windowStart: cappedStart,
391
+ hint: planned.hint,
392
+ items: planned.visible.map((item) => {
393
+ const cappedIdx = item.absoluteIndex;
394
+ const absolute = cappedStart + cappedIdx;
395
+ const agentId = activeAgents[absolute];
396
+ const active = Boolean(activeAgentId && agentId === activeAgentId);
397
+ return {
398
+ label: item.label,
399
+ selected: focused && absolute === selectedAgentIndex,
400
+ ...(active ? { active: true } : {}),
401
+ };
402
+ }),
403
+ };
404
+ }
405
+
406
+ function buildDashboardRows(options = {}) {
407
+ const {
408
+ globalMode = false,
409
+ globalScope = "controller",
410
+ focusMode = "input",
411
+ dashboardView = "agents",
412
+ projects = [],
413
+ dashHints = {},
414
+ } = options;
415
+ const focused = focusMode === "dashboard";
416
+ if (globalMode) {
417
+ const projectsFocused = focused && dashboardView === "projects";
418
+ const projectRow = buildProjectRow({
419
+ ...options,
420
+ focused: projectsFocused,
421
+ globalScope,
422
+ });
423
+ if (!focused || projectsFocused) {
424
+ return [projectRow, buildSummaryRow(options)];
425
+ }
426
+ return [projectRow, buildDetailRow({ ...options, focused })];
427
+ }
428
+ if (focused) return [buildDetailRow({ ...options, focused })];
429
+ return [buildSummaryRow(options)];
430
+ }
431
+
432
+ /**
433
+ * Render a chip row to plain text + ANSI color spans, sized to fit
434
+ * `maxWidth`. Includes the caption, optional `< / >` markers, optional
435
+ * `emptyLabel`, and the trailing hint when the planner kept it. Used as the
436
+ * sole text payload of a row's <Text wrap="truncate">.
437
+ */
438
+ function renderChipRowText(row, maxWidth = 80) {
439
+ const budget = Math.max(1, Math.floor(Number(maxWidth) || 80));
440
+ const { caption = "", items = [], hint = "", leftMore, rightMore, emptyLabel } = row || {};
441
+ const captionText = `${caption}: `;
442
+ let out = chalk.gray(captionText);
443
+ let used = displayCellWidth(captionText);
444
+
445
+ if (leftMore) {
446
+ out += chalk.gray("< ");
447
+ used += 2;
448
+ }
449
+ if (items.length === 0 && emptyLabel) {
450
+ const remaining = Math.max(0, budget - used - (hint ? HINT_PREFIX_WIDTH + displayCellWidth(hint) : 0));
451
+ const trimmedEmpty = truncateToCells(emptyLabel, remaining);
452
+ out += chalk.cyan(trimmedEmpty);
453
+ used += displayCellWidth(trimmedEmpty);
454
+ }
455
+ for (let i = 0; i < items.length; i += 1) {
456
+ if (i > 0) {
457
+ out += chalk.gray(CHIP_SEP);
458
+ used += CHIP_SEP_WIDTH;
459
+ }
460
+ const item = items[i];
461
+ const label = String(item.label || "");
462
+ if (item.selected) {
463
+ out += chalk.inverse(label);
464
+ } else if (item.active) {
465
+ out += chalk.bold.cyan(label);
466
+ } else {
467
+ out += chalk.cyan(label);
468
+ }
469
+ used += displayCellWidth(label);
470
+ }
471
+ if (rightMore) {
472
+ out += chalk.gray(" >");
473
+ used += 2;
474
+ }
475
+ if (hint) {
476
+ const remaining = Math.max(0, budget - used);
477
+ if (remaining > HINT_PREFIX_WIDTH + 1) {
478
+ const hintBody = truncateToCells(hint, remaining - HINT_PREFIX_WIDTH);
479
+ out += chalk.gray(`${HINT_PREFIX}${hintBody}`);
480
+ }
481
+ }
482
+ return out;
483
+ }
484
+
485
+ function renderSummaryRowText(row, maxWidth = 80) {
486
+ const budget = Math.max(1, Math.floor(Number(maxWidth) || 80));
487
+ const { parts = [], agentItems = [] } = row || {};
488
+
489
+ // Pre-render the non-Agents parts so we know how many cells they will
490
+ // claim. Agents is special: it carries the full active list and we want
491
+ // to fit as many chips as the remaining budget allows.
492
+ const tailParts = parts.slice(1).map((part) => {
493
+ const labelText = `${part.label}: `;
494
+ const labelWidth = displayCellWidth(labelText);
495
+ const value = String(part.value || "");
496
+ return {
497
+ label: part.label,
498
+ labelText,
499
+ labelColored: chalk.gray(labelText),
500
+ labelWidth,
501
+ value,
502
+ width: labelWidth + displayCellWidth(value),
503
+ colored: chalk.gray(labelText) + chalk.cyan(value),
504
+ truncatable: part.label === "Loop",
505
+ };
506
+ });
507
+
508
+ // How many cells would tail parts ideally claim (with their leading gap)?
509
+ // We use this to reserve room for them when packing Agents chips, but we
510
+ // never reserve so much that Agents can't fit at least one short chip
511
+ // (otherwise narrow terminals show "Agents: +N" with zero names).
512
+ let tailIdealWidth = 0;
513
+ for (const tp of tailParts) {
514
+ tailIdealWidth += SUMMARY_GAP_WIDTH + tp.width;
515
+ }
516
+ // Cap reservation so Agents always gets at least ~12 cells to play with
517
+ // when there's any agent to show — enough for "@a +N" on the narrowest
518
+ // displays. The remaining tail parts will simply be dropped one by one.
519
+ const minAgentRoom = 12;
520
+ const captionWidth = displayCellWidth("Agents: ");
521
+ const tailReserve = Math.min(
522
+ tailIdealWidth,
523
+ Math.max(0, budget - captionWidth - minAgentRoom)
524
+ );
525
+
526
+ let out = "";
527
+ let used = 0;
528
+
529
+ const agentsPart = parts[0];
530
+ if (agentsPart && agentsPart.label === "Agents") {
531
+ const labelText = "Agents: ";
532
+ const labelWidth = displayCellWidth(labelText);
533
+ out += chalk.gray(labelText);
534
+ used += labelWidth;
535
+
536
+ if (agentItems.length === 0) {
537
+ const noneText = "none";
538
+ out += chalk.cyan(noneText);
539
+ used += displayCellWidth(noneText);
540
+ } else {
541
+ // Reserve room for the worst-case " +N" overflow tail so we never have
542
+ // to backtrack and pop a chip after committing to it.
543
+ const worstOverflow = ` +${agentItems.length}`;
544
+ const worstOverflowWidth = displayCellWidth(worstOverflow);
545
+ const agentBudget = Math.max(0, budget - used - tailReserve);
546
+ let fittedCount = 0;
547
+ let agentsUsed = 0;
548
+ for (let i = 0; i < agentItems.length; i += 1) {
549
+ const item = agentItems[i];
550
+ const label = String(item.label || "");
551
+ const sepWidth = i === 0 ? 0 : displayCellWidth(", ");
552
+ const remainingItems = agentItems.length - i - 1;
553
+ const reserveOverflow = remainingItems > 0 ? worstOverflowWidth : 0;
554
+ const labelWidthInner = displayCellWidth(label);
555
+ if (agentsUsed + sepWidth + labelWidthInner + reserveOverflow > agentBudget) break;
556
+ if (i > 0) {
557
+ out += chalk.gray(", ");
558
+ agentsUsed += sepWidth;
559
+ }
560
+ if (item.active) out += chalk.bold.cyan(label);
561
+ else out += chalk.cyan(label);
562
+ agentsUsed += labelWidthInner;
563
+ fittedCount += 1;
564
+ }
565
+ const overflow = agentItems.length - fittedCount;
566
+ if (overflow > 0) {
567
+ const tail = ` +${overflow}`;
568
+ out += chalk.cyan(tail);
569
+ agentsUsed += displayCellWidth(tail);
570
+ }
571
+ used += agentsUsed;
572
+ }
573
+ }
574
+
575
+ for (let i = 0; i < tailParts.length; i += 1) {
576
+ const part = tailParts[i];
577
+ const remaining = budget - used - SUMMARY_GAP_WIDTH;
578
+ if (remaining <= 0) break;
579
+ if (part.width <= remaining) {
580
+ out += chalk.gray(SUMMARY_GAP) + part.colored;
581
+ used += SUMMARY_GAP_WIDTH + part.width;
582
+ continue;
583
+ }
584
+ if (part.truncatable && remaining >= 6) {
585
+ const valueRoom = Math.max(1, remaining - part.labelWidth);
586
+ const trimmedValue = truncateToCells(part.value, valueRoom);
587
+ out += chalk.gray(SUMMARY_GAP) + part.labelColored + chalk.cyan(trimmedValue);
588
+ used += SUMMARY_GAP_WIDTH + part.labelWidth + displayCellWidth(trimmedValue);
589
+ }
590
+ break;
591
+ }
592
+ return out;
593
+ }
594
+
595
+ function createDashboardBar({ React, ink }) {
596
+ const { Box, Text } = ink;
597
+ const h = React.createElement;
598
+
599
+ return function DashboardBar({
600
+ dashboardView = "agents",
601
+ focusMode = "input",
602
+ globalMode = false,
603
+ globalScope = "controller",
604
+ activeAgents = [],
605
+ activeAgentId = "",
606
+ selectedAgentIndex = -1,
607
+ agentListWindowStart = 0,
608
+ maxAgentWindow = 4,
609
+ projectListWindowStart = 0,
610
+ maxProjectWindow = 5,
611
+ maxWidth = 80,
612
+ getAgentLabel = (id) => id,
613
+ getAgentState = () => "",
614
+ launchMode = "terminal",
615
+ agentProvider = "codex-cli",
616
+ modeOptions = [],
617
+ selectedModeIndex = 0,
618
+ providerOptions = [],
619
+ selectedProviderIndex = 0,
620
+ cronTasks = [],
621
+ loopSummary = null,
622
+ selectedCronIndex = -1,
623
+ projects = [],
624
+ selectedProjectIndex = -1,
625
+ activeProjectRoot = "",
626
+ dashHints = {},
627
+ }) {
628
+ const rows = buildDashboardRows({
629
+ dashboardView,
630
+ focusMode,
631
+ globalMode,
632
+ globalScope,
633
+ activeAgents,
634
+ activeAgentId,
635
+ selectedAgentIndex,
636
+ agentListWindowStart,
637
+ maxAgentWindow,
638
+ projectListWindowStart,
639
+ maxProjectWindow,
640
+ maxWidth,
641
+ getAgentLabel,
642
+ getAgentState,
643
+ launchMode,
644
+ agentProvider,
645
+ modeOptions,
646
+ selectedModeIndex,
647
+ providerOptions,
648
+ selectedProviderIndex,
649
+ cronTasks,
650
+ loopSummary,
651
+ selectedCronIndex,
652
+ projects,
653
+ selectedProjectIndex,
654
+ activeProjectRoot,
655
+ dashHints,
656
+ }).map((row, idx) => {
657
+ let text;
658
+ if (row.kind === "summary") {
659
+ text = renderSummaryRowText(row, maxWidth);
660
+ } else if (row.kind === "message") {
661
+ const value = String(row.text || "");
662
+ text = chalk.gray(truncateToCells(value, maxWidth));
663
+ } else {
664
+ text = renderChipRowText(row, maxWidth);
665
+ }
666
+ return h(Box, { key: `dr-${idx}`, width: "100%" },
667
+ h(Text, { wrap: "truncate" }, text || " "));
668
+ });
669
+
670
+ if (rows.length === 1) return h(Box, { width: "100%" }, rows[0]);
671
+ return h(Box, { flexDirection: "column", width: "100%" }, ...rows);
672
+ };
673
+ }
674
+
675
+ function renderDashboardLines(params) {
676
+ const maxWidth = params.maxWidth || 80;
677
+ const rows = buildDashboardRows(params);
678
+ return rows.map((row) => {
679
+ if (row.kind === "summary") return renderSummaryRowText(row, maxWidth);
680
+ if (row.kind === "message") return chalk.gray(truncateToCells(String(row.text || ""), maxWidth));
681
+ return renderChipRowText(row, maxWidth);
682
+ });
683
+ }
684
+
685
+ module.exports = { createDashboardBar, buildDashboardRows, renderDashboardLines, formatLoopSummary };