u-foo 1.4.1 → 1.5.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.
@@ -22,18 +22,101 @@ function ensureAtPrefix(value) {
22
22
  return text.startsWith("@") ? text : `@${text}`;
23
23
  }
24
24
 
25
- function computeDashboardContent(options = {}) {
25
+ function buildSummaryLine(options = {}) {
26
26
  const {
27
- focusMode = "input",
27
+ activeAgents = [],
28
+ getAgentLabel = (id) => id,
29
+ launchMode = "terminal",
30
+ agentProvider = "codex-cli",
31
+ assistantEngine = "auto",
32
+ cronTasks = [],
33
+ } = options;
34
+ const agents = activeAgents.length > 0
35
+ ? activeAgents.slice(0, 3).map((id) => ensureAtPrefix(getAgentLabel(id))).join(", ") + (activeAgents.length > 3 ? ` +${activeAgents.length - 3}` : "")
36
+ : "none";
37
+ return `{gray-fg}Agents:{/gray-fg} {cyan-fg}${agents}{/cyan-fg}`
38
+ + ` {gray-fg}Mode:{/gray-fg} {cyan-fg}${launchMode}{/cyan-fg}`
39
+ + ` {gray-fg}Agent:{/gray-fg} {cyan-fg}${providerLabel(agentProvider)}{/cyan-fg}`
40
+ + ` {gray-fg}Assistant:{/gray-fg} {cyan-fg}${assistantLabel(assistantEngine)}{/cyan-fg}`
41
+ + ` {gray-fg}Cron:{/gray-fg} {cyan-fg}${Array.isArray(cronTasks) ? cronTasks.length : 0}{/cyan-fg}`;
42
+ }
43
+
44
+ function buildProjectRailLine(options = {}) {
45
+ const {
46
+ projects = [],
47
+ selectedProjectIndex = -1,
48
+ projectListWindowStart = 0,
49
+ maxProjectWindow = 5,
50
+ activeProjectRoot = "",
51
+ projectsFocused = false,
52
+ } = options;
53
+ const rows = Array.isArray(projects) ? projects : [];
54
+ let windowStart = projectListWindowStart;
55
+ if (rows.length === 0) {
56
+ return {
57
+ hasProjects: false,
58
+ line: " {gray-fg}Projects:{/gray-fg} {cyan-fg}none{/cyan-fg}",
59
+ windowStart,
60
+ };
61
+ }
62
+
63
+ const activeRoot = String(activeProjectRoot || "");
64
+ const fallbackIndex = rows.findIndex((row) => String((row || {}).project_root || "") === activeRoot);
65
+ const normalizedSelectedIndex = Number.isFinite(selectedProjectIndex)
66
+ ? Math.trunc(selectedProjectIndex)
67
+ : -1;
68
+ const safeSelectedIndex = normalizedSelectedIndex >= 0 && normalizedSelectedIndex < rows.length
69
+ ? normalizedSelectedIndex
70
+ : (fallbackIndex >= 0 ? fallbackIndex : 0);
71
+
72
+ windowStart = clampAgentWindowWithSelection({
73
+ activeCount: rows.length,
74
+ maxWindow: Math.max(1, maxProjectWindow),
75
+ windowStart,
76
+ selectionIndex: safeSelectedIndex,
77
+ });
78
+
79
+ const maxItems = Math.max(1, Math.min(Math.max(1, maxProjectWindow), rows.length));
80
+ const start = windowStart;
81
+ const end = start + maxItems;
82
+ const visibleRows = rows.slice(start, end);
83
+ const projectParts = visibleRows.map((row, i) => {
84
+ const absoluteIndex = start + i;
85
+ const name = String((row && row.project_name) || (row && row.project_root) || "-");
86
+ const rowRoot = String((row && row.project_root) || "");
87
+ const isActiveProject = Boolean(activeRoot && rowRoot === activeRoot);
88
+ const isSelected = absoluteIndex === safeSelectedIndex;
89
+ const displayName = isActiveProject ? `[${name}]` : name;
90
+ if (projectsFocused && isSelected) {
91
+ return `{inverse}${displayName}{/inverse}`;
92
+ }
93
+ if (isActiveProject) {
94
+ return `{cyan-fg}${displayName}{/cyan-fg}`;
95
+ }
96
+ if (isSelected) {
97
+ return `{inverse}${displayName}{/inverse}`;
98
+ }
99
+ return `{cyan-fg}${displayName}{/cyan-fg}`;
100
+ });
101
+
102
+ const leftMore = start > 0 ? "{gray-fg}<{/gray-fg} " : "";
103
+ const rightMore = end < rows.length ? " {gray-fg}>{/gray-fg}" : "";
104
+ return {
105
+ hasProjects: true,
106
+ line: ` {gray-fg}Projects:{/gray-fg} ${leftMore}${projectParts.join(" ")}${rightMore}`,
107
+ windowStart,
108
+ };
109
+ }
110
+
111
+ function buildDashboardDetailLine(options = {}) {
112
+ const {
113
+ globalMode = false,
28
114
  dashboardView = "agents",
29
115
  activeAgents = [],
30
116
  selectedAgentIndex = -1,
31
117
  agentListWindowStart = 0,
32
118
  maxAgentWindow = 4,
33
119
  getAgentLabel = (id) => id,
34
- launchMode = "terminal",
35
- agentProvider = "codex-cli",
36
- assistantEngine = "auto",
37
120
  selectedModeIndex = 0,
38
121
  selectedProviderIndex = 0,
39
122
  selectedAssistantIndex = 0,
@@ -49,105 +132,218 @@ function computeDashboardContent(options = {}) {
49
132
  let content = " ";
50
133
  let windowStart = agentListWindowStart;
51
134
 
52
- if (focusMode === "dashboard") {
53
- if (dashboardView === "mode") {
54
- const modeParts = modeOptions.map((mode, i) => {
55
- if (i === selectedModeIndex) {
56
- return `{inverse}${mode}{/inverse}`;
57
- }
58
- return `{cyan-fg}${mode}{/cyan-fg}`;
59
- });
60
- content += `{gray-fg}Mode:{/gray-fg} ${modeParts.join(" ")}`;
61
- content += ` {gray-fg}│ ${dashHints.mode || ""}{/gray-fg}`;
62
- return { content, windowStart };
63
- }
135
+ if (dashboardView === "mode") {
136
+ const modeParts = modeOptions.map((mode, i) => {
137
+ if (i === selectedModeIndex) {
138
+ return `{inverse}${mode}{/inverse}`;
139
+ }
140
+ return `{cyan-fg}${mode}{/cyan-fg}`;
141
+ });
142
+ content += `{gray-fg}Mode:{/gray-fg} ${modeParts.join(" ")}`;
143
+ content += ` {gray-fg} ${dashHints.mode || ""}{/gray-fg}`;
144
+ return { content, windowStart };
145
+ }
64
146
 
65
- if (dashboardView === "provider") {
66
- const providerParts = providerOptions.map((opt, i) => {
67
- if (i === selectedProviderIndex) {
68
- return `{inverse}${opt.label}{/inverse}`;
69
- }
70
- return `{cyan-fg}${opt.label}{/cyan-fg}`;
71
- });
72
- content += `{gray-fg}Agent:{/gray-fg} ${providerParts.join(" ")}`;
73
- content += ` {gray-fg}│ ${dashHints.provider || ""}{/gray-fg}`;
74
- return { content, windowStart };
75
- }
147
+ if (dashboardView === "provider") {
148
+ const providerParts = providerOptions.map((opt, i) => {
149
+ if (i === selectedProviderIndex) {
150
+ return `{inverse}${opt.label}{/inverse}`;
151
+ }
152
+ return `{cyan-fg}${opt.label}{/cyan-fg}`;
153
+ });
154
+ content += `{gray-fg}Agent:{/gray-fg} ${providerParts.join(" ")}`;
155
+ content += ` {gray-fg}│ ${dashHints.provider || ""}{/gray-fg}`;
156
+ return { content, windowStart };
157
+ }
76
158
 
77
- if (dashboardView === "assistant") {
78
- const assistantParts = assistantOptions.map((opt, i) => {
79
- if (i === selectedAssistantIndex) {
80
- return `{inverse}${opt.label}{/inverse}`;
81
- }
82
- return `{cyan-fg}${opt.label}{/cyan-fg}`;
83
- });
84
- content += `{gray-fg}Assistant:{/gray-fg} ${assistantParts.join(" ")}`;
85
- content += ` {gray-fg}│ ${dashHints.assistant || ""}{/gray-fg}`;
86
- return { content, windowStart };
87
- }
159
+ if (dashboardView === "assistant") {
160
+ const assistantParts = assistantOptions.map((opt, i) => {
161
+ if (i === selectedAssistantIndex) {
162
+ return `{inverse}${opt.label}{/inverse}`;
163
+ }
164
+ return `{cyan-fg}${opt.label}{/cyan-fg}`;
165
+ });
166
+ content += `{gray-fg}Assistant:{/gray-fg} ${assistantParts.join(" ")}`;
167
+ content += ` {gray-fg}│ ${dashHints.assistant || ""}{/gray-fg}`;
168
+ return { content, windowStart };
169
+ }
88
170
 
89
- if (dashboardView === "resume") {
90
- const resumeParts = resumeOptions.map((opt, i) => {
91
- if (i === selectedResumeIndex) {
92
- return `{inverse}${opt.label}{/inverse}`;
93
- }
94
- return `{cyan-fg}${opt.label}{/cyan-fg}`;
95
- });
96
- content += `{gray-fg}Resume:{/gray-fg} ${resumeParts.join(" ")}`;
97
- content += ` {gray-fg}│ ${dashHints.resume || ""}{/gray-fg}`;
98
- return { content, windowStart };
99
- }
171
+ if (dashboardView === "resume") {
172
+ const resumeParts = resumeOptions.map((opt, i) => {
173
+ if (i === selectedResumeIndex) {
174
+ return `{inverse}${opt.label}{/inverse}`;
175
+ }
176
+ return `{cyan-fg}${opt.label}{/cyan-fg}`;
177
+ });
178
+ content += `{gray-fg}Resume:{/gray-fg} ${resumeParts.join(" ")}`;
179
+ content += ` {gray-fg}│ ${dashHints.resume || ""}{/gray-fg}`;
180
+ return { content, windowStart };
181
+ }
182
+
183
+ if (dashboardView === "cron") {
184
+ const items = Array.isArray(cronTasks) ? cronTasks : [];
185
+ const summary = items.length > 0
186
+ ? items.map((item) => item.summary || item.id || "").filter(Boolean).join(", ")
187
+ : "none";
188
+ content += `{gray-fg}Cron:{/gray-fg} {cyan-fg}${summary}{/cyan-fg}`;
189
+ content += ` {gray-fg}│ ${dashHints.cron || ""}{/gray-fg}`;
190
+ return { content, windowStart };
191
+ }
192
+
193
+ if (activeAgents.length > 0) {
194
+ windowStart = clampAgentWindowWithSelection({
195
+ activeCount: activeAgents.length,
196
+ maxWindow: maxAgentWindow,
197
+ windowStart,
198
+ selectionIndex: selectedAgentIndex,
199
+ });
200
+ const maxItems = Math.max(1, Math.min(maxAgentWindow, activeAgents.length));
201
+ const start = windowStart;
202
+ const end = start + maxItems;
203
+ const visibleAgents = activeAgents.slice(start, end);
204
+ const agentParts = visibleAgents.map((agent, i) => {
205
+ const absoluteIndex = start + i;
206
+ const label = ensureAtPrefix(getAgentLabel(agent));
207
+ if (absoluteIndex === selectedAgentIndex) {
208
+ return `{inverse}${label}{/inverse}`;
209
+ }
210
+ return `{cyan-fg}${label}{/cyan-fg}`;
211
+ });
212
+ const leftMore = start > 0 ? "{gray-fg}<{/gray-fg} " : "";
213
+ const rightMore = end < activeAgents.length ? " {gray-fg}>{/gray-fg}" : "";
214
+ content += `{gray-fg}Agents:{/gray-fg} ${leftMore}${agentParts.join(" ")}${rightMore}`;
215
+ const agentsHint = globalMode
216
+ ? (dashHints.agentsGlobal || dashHints.agents || "")
217
+ : (dashHints.agents || "");
218
+ content += ` {gray-fg}│ ${agentsHint}{/gray-fg}`;
219
+ } else {
220
+ content += "{gray-fg}Agents:{/gray-fg} {cyan-fg}none{/cyan-fg}";
221
+ content += ` {gray-fg}│ ${dashHints.agentsEmpty || ""}{/gray-fg}`;
222
+ }
223
+ return { content, windowStart };
224
+ }
225
+
226
+ function computeDashboardContent(options = {}) {
227
+ const {
228
+ globalMode = false,
229
+ focusMode = "input",
230
+ dashboardView = "agents",
231
+ activeAgents = [],
232
+ projects = [],
233
+ selectedProjectIndex = -1,
234
+ projectListWindowStart = 0,
235
+ maxProjectWindow = 5,
236
+ activeProjectRoot = "",
237
+ selectedAgentIndex = -1,
238
+ agentListWindowStart = 0,
239
+ maxAgentWindow = 4,
240
+ getAgentLabel = (id) => id,
241
+ launchMode = "terminal",
242
+ agentProvider = "codex-cli",
243
+ assistantEngine = "auto",
244
+ selectedModeIndex = 0,
245
+ selectedProviderIndex = 0,
246
+ selectedAssistantIndex = 0,
247
+ selectedResumeIndex = 0,
248
+ cronTasks = [],
249
+ providerOptions = [],
250
+ assistantOptions = [],
251
+ resumeOptions = [],
252
+ dashHints = {},
253
+ modeOptions = DEFAULT_MODE_OPTIONS,
254
+ } = options;
100
255
 
101
- if (dashboardView === "cron") {
102
- const items = Array.isArray(cronTasks) ? cronTasks : [];
103
- const summary = items.length > 0
104
- ? items.map((item) => item.summary || item.id || "").filter(Boolean).join(", ")
105
- : "none";
106
- content += `{gray-fg}Cron:{/gray-fg} {cyan-fg}${summary}{/cyan-fg}`;
107
- content += ` {gray-fg}│ ${dashHints.cron || ""}{/gray-fg}`;
108
- return { content, windowStart };
256
+ if (globalMode) {
257
+ const projectsFocused = focusMode === "dashboard" && dashboardView === "projects";
258
+ const rail = buildProjectRailLine({
259
+ projects,
260
+ selectedProjectIndex,
261
+ projectListWindowStart,
262
+ maxProjectWindow,
263
+ activeProjectRoot,
264
+ projectsFocused,
265
+ });
266
+ if (!rail.hasProjects) {
267
+ const line2 = ` {gray-fg}${dashHints.projectsEmpty || "Run ufoo chat/daemon in projects to populate registry"}{/gray-fg}`;
268
+ return {
269
+ content: `${rail.line}\n${line2}`,
270
+ windowStart: rail.windowStart,
271
+ };
109
272
  }
110
273
 
111
- if (activeAgents.length > 0) {
112
- windowStart = clampAgentWindowWithSelection({
113
- activeCount: activeAgents.length,
114
- maxWindow: maxAgentWindow,
115
- windowStart,
116
- selectionIndex: selectedAgentIndex,
274
+ if (focusMode !== "dashboard" || projectsFocused) {
275
+ const line2 = buildSummaryLine({
276
+ activeAgents,
277
+ getAgentLabel,
278
+ launchMode,
279
+ agentProvider,
280
+ assistantEngine,
281
+ cronTasks,
117
282
  });
118
- const maxItems = Math.max(1, Math.min(maxAgentWindow, activeAgents.length));
119
- const start = windowStart;
120
- const end = start + maxItems;
121
- const visibleAgents = activeAgents.slice(start, end);
122
- const agentParts = visibleAgents.map((agent, i) => {
123
- const absoluteIndex = start + i;
124
- const label = ensureAtPrefix(getAgentLabel(agent));
125
- if (absoluteIndex === selectedAgentIndex) {
126
- return `{inverse}${label}{/inverse}`;
127
- }
128
- return `{cyan-fg}${label}{/cyan-fg}`;
129
- });
130
- const leftMore = start > 0 ? "{gray-fg}<{/gray-fg} " : "";
131
- const rightMore = end < activeAgents.length ? " {gray-fg}>{/gray-fg}" : "";
132
- content += `{gray-fg}Agents:{/gray-fg} ${leftMore}${agentParts.join(" ")}${rightMore}`;
133
- content += ` {gray-fg}│ ${dashHints.agents || ""}{/gray-fg}`;
134
- } else {
135
- content += "{gray-fg}Agents:{/gray-fg} {cyan-fg}none{/cyan-fg}";
136
- content += ` {gray-fg}│ ${dashHints.agentsEmpty || ""}{/gray-fg}`;
283
+ return {
284
+ content: `${rail.line}\n ${line2}`,
285
+ windowStart: rail.windowStart,
286
+ };
137
287
  }
138
- return { content, windowStart };
288
+
289
+ const detail = buildDashboardDetailLine({
290
+ globalMode,
291
+ dashboardView,
292
+ activeAgents,
293
+ selectedAgentIndex,
294
+ agentListWindowStart,
295
+ maxAgentWindow,
296
+ getAgentLabel,
297
+ selectedModeIndex,
298
+ selectedProviderIndex,
299
+ selectedAssistantIndex,
300
+ selectedResumeIndex,
301
+ cronTasks,
302
+ providerOptions,
303
+ assistantOptions,
304
+ resumeOptions,
305
+ dashHints,
306
+ modeOptions,
307
+ });
308
+ return {
309
+ content: `${rail.line}\n${detail.content}`,
310
+ windowStart: detail.windowStart,
311
+ };
139
312
  }
140
313
 
141
- const agents = activeAgents.length > 0
142
- ? activeAgents.slice(0, 3).map((id) => ensureAtPrefix(getAgentLabel(id))).join(", ") + (activeAgents.length > 3 ? ` +${activeAgents.length - 3}` : "")
143
- : "none";
144
- content += `{gray-fg}Agents:{/gray-fg} {cyan-fg}${agents}{/cyan-fg}`;
145
- content += ` {gray-fg}Mode:{/gray-fg} {cyan-fg}${launchMode}{/cyan-fg}`;
146
- content += ` {gray-fg}Agent:{/gray-fg} {cyan-fg}${providerLabel(agentProvider)}{/cyan-fg}`;
147
- content += ` {gray-fg}Assistant:{/gray-fg} {cyan-fg}${assistantLabel(assistantEngine)}{/cyan-fg}`;
148
- content += ` {gray-fg}Cron:{/gray-fg} {cyan-fg}${Array.isArray(cronTasks) ? cronTasks.length : 0}{/cyan-fg}`;
314
+ if (focusMode === "dashboard") {
315
+ return buildDashboardDetailLine({
316
+ globalMode,
317
+ dashboardView,
318
+ activeAgents,
319
+ selectedAgentIndex,
320
+ agentListWindowStart,
321
+ maxAgentWindow,
322
+ getAgentLabel,
323
+ selectedModeIndex,
324
+ selectedProviderIndex,
325
+ selectedAssistantIndex,
326
+ selectedResumeIndex,
327
+ cronTasks,
328
+ providerOptions,
329
+ assistantOptions,
330
+ resumeOptions,
331
+ dashHints,
332
+ modeOptions,
333
+ });
334
+ }
149
335
 
150
- return { content, windowStart };
336
+ let content = " ";
337
+ content += buildSummaryLine({
338
+ activeAgents,
339
+ getAgentLabel,
340
+ launchMode,
341
+ agentProvider,
342
+ assistantEngine,
343
+ cronTasks,
344
+ });
345
+
346
+ return { content, windowStart: agentListWindowStart };
151
347
  }
152
348
 
153
349
  module.exports = {