u-foo 3.0.9 → 3.0.11

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 (65) hide show
  1. package/README.md +25 -9
  2. package/README.zh-CN.md +22 -9
  3. package/dist/tui/darwin-arm64/ufoo-tui +0 -0
  4. package/dist/tui/darwin-x64/ufoo-tui +0 -0
  5. package/dist/tui/linux-arm64/ufoo-tui +0 -0
  6. package/dist/tui/linux-x64/ufoo-tui +0 -0
  7. package/package.json +12 -4
  8. package/scripts/pack-tui.js +112 -0
  9. package/scripts/postinstall.js +11 -0
  10. package/src/agents/activity/activityReconcile.js +106 -0
  11. package/src/agents/activity/activityStatePublisher.js +31 -2
  12. package/src/agents/activity/index.js +1 -0
  13. package/src/agents/launch/launcher.js +19 -0
  14. package/src/agents/launch/ptyRunner.js +20 -1
  15. package/src/app/chat/ChatController.js +433 -0
  16. package/src/app/chat/agentDirectory.js +63 -0
  17. package/src/app/chat/agentEnter.js +70 -0
  18. package/src/app/chat/agentIdentity.js +50 -0
  19. package/src/app/chat/bootstrap.js +66 -0
  20. package/src/app/chat/commandExecutor.js +108 -0
  21. package/src/app/chat/commands.js +38 -1
  22. package/src/app/chat/dashboardView.js +6 -2
  23. package/src/app/chat/historyStore.js +181 -0
  24. package/src/app/chat/index.js +14 -2
  25. package/src/app/chat/inputSubmitHandler.js +21 -7
  26. package/src/app/chat/ipcBuilders.js +52 -0
  27. package/src/app/chat/multiWindow/paneManager.js +10 -1
  28. package/src/app/chat/multiWindow/renderer.js +1 -1
  29. package/src/app/chat/multiWindow/vtFrame.js +93 -0
  30. package/src/app/chat/streamState.js +182 -0
  31. package/src/app/cli/features/doctor.js +22 -0
  32. package/src/code/UcodeController.js +156 -0
  33. package/src/code/context/planGraphService.js +4 -0
  34. package/src/code/repl.js +4 -3
  35. package/src/code/runtime/taskLoop.js +46 -50
  36. package/src/code/tui.js +13 -2
  37. package/src/code/ucodeSlashDispatch.js +241 -0
  38. package/src/coordination/bus/activate.js +3 -0
  39. package/src/runtime/contracts/schemas/ufoo-ui-v1/envelope.json +28 -0
  40. package/src/runtime/contracts/uiProtocol.js +190 -0
  41. package/src/ui/{ink/chatLogModel.js → chatLogModel.js} +2 -2
  42. package/src/ui/dashboardBridge.js +81 -0
  43. package/src/ui/format/index.js +2 -2
  44. package/src/ui/index.js +8 -4
  45. package/src/ui/multiPaneBusMirror.js +137 -0
  46. package/src/ui/multiWindowHandoff.js +232 -0
  47. package/src/ui/ptyHandoff.js +23 -0
  48. package/src/ui/rustChatHost.js +1520 -0
  49. package/src/ui/rustMultiSession.js +497 -0
  50. package/src/ui/rustUcodeHost.js +999 -0
  51. package/src/ui/scrollbackReplay.js +82 -0
  52. package/src/ui/settingsBridge.js +49 -0
  53. package/src/ui/toolMergeBridge.js +66 -0
  54. package/src/ui/tuiLauncher.js +105 -0
  55. package/src/ui/ucodeStatusLine.js +74 -0
  56. package/src/ui/uiHostServer.js +339 -0
  57. package/src/ui/MIGRATION.md +0 -334
  58. package/src/ui/ink/ChatApp.js +0 -4152
  59. package/src/ui/ink/DashboardBar.js +0 -691
  60. package/src/ui/ink/InkDemo.js +0 -96
  61. package/src/ui/ink/MultilineInput.js +0 -662
  62. package/src/ui/ink/UcodeApp.js +0 -1675
  63. package/src/ui/ink/agentMirror.js +0 -730
  64. package/src/ui/ink/chatReducer.js +0 -473
  65. package/src/ui/runInk.js +0 -66
@@ -1,691 +0,0 @@
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 inputTokens = Number(loopSummary.input_tokens) || 0;
78
- const cacheReadTokens = Number(loopSummary.cache_read_tokens) || 0;
79
- const cacheCreationTokens = Number(loopSummary.cache_creation_tokens) || 0;
80
- const terminalReason = String(loopSummary.terminal_reason || "").trim();
81
- const toolDistribution = formatToolDistribution(loopSummary.tools);
82
- if (rounds <= 0 && toolCalls <= 0 && totalTokens <= 0 && !terminalReason && !toolDistribution) return "";
83
- const parts = [`r${rounds}`, `tc${toolCalls}`, `tok${totalTokens}`];
84
- if (cacheReadTokens > 0 || cacheCreationTokens > 0) {
85
- let cachePart = `cache${cacheReadTokens}/${cacheCreationTokens}`;
86
- if (cacheReadTokens > 0) {
87
- const hitRate = Math.round((cacheReadTokens / (cacheReadTokens + inputTokens)) * 100);
88
- cachePart += `(${hitRate}%)`;
89
- }
90
- parts.push(cachePart);
91
- }
92
- if (toolDistribution) parts.push(toolDistribution);
93
- if (terminalReason) parts.push(terminalReason);
94
- return parts.join(" ");
95
- }
96
-
97
- function projectName(row) {
98
- return String(
99
- (row && (row.project_name || row.label || row.id || row.project_root || row.root)) || "-"
100
- );
101
- }
102
-
103
- function projectRoot(row) {
104
- return String((row && (row.project_root || row.root)) || "");
105
- }
106
-
107
- /**
108
- * Generic chip row planner. Returns the visible slice that fits inside
109
- * `maxWidth` (caption + chips + optional `< / >` overflow markers + optional
110
- * trailing hint). Hint is dropped first when budget is tight; chips are
111
- * windowed around `selectedIndex`. Empty rail (no items) is handled by the
112
- * caller via `emptyLabel`.
113
- */
114
- function planChipsRow({
115
- caption,
116
- labels,
117
- selectedIndex = -1,
118
- windowStart = 0,
119
- hint = "",
120
- maxWidth = 80,
121
- reserveHintWhenFocused = false,
122
- } = {}) {
123
- const items = Array.isArray(labels) ? labels.map(String) : [];
124
- const totalBudget = Math.max(1, Math.floor(Number(maxWidth) || 80));
125
- const captionText = `${caption}: `;
126
- const captionWidth = displayCellWidth(captionText);
127
- const railBudget = Math.max(1, totalBudget - captionWidth);
128
- const hintText = String(hint || "");
129
- const hintWidth = hintText ? HINT_PREFIX_WIDTH + displayCellWidth(hintText) : 0;
130
- const minChipCells = reserveHintWhenFocused ? 4 : 1;
131
- const canShowHint = hintText && railBudget - hintWidth >= minChipCells;
132
- const finalHint = canShowHint ? hintText : "";
133
- const railOnlyBudget = Math.max(1, railBudget - (finalHint ? hintWidth : 0));
134
- const planned = planProjectsRail({
135
- labels: items,
136
- selectedIndex,
137
- windowStart: windowStart || 0,
138
- maxCells: railOnlyBudget,
139
- });
140
- return {
141
- captionText,
142
- visible: planned.items,
143
- leftMore: planned.leftMore,
144
- rightMore: planned.rightMore,
145
- windowStart: planned.windowStart,
146
- hint: finalHint,
147
- };
148
- }
149
-
150
- function buildSummaryRow(options = {}) {
151
- const {
152
- activeAgents = [],
153
- activeAgentId = "",
154
- getAgentLabel = (id) => id,
155
- getAgentState = () => "",
156
- launchMode = "terminal",
157
- agentProvider = "codex-cli",
158
- cronTasks = [],
159
- loopSummary = null,
160
- } = options;
161
- // agentItems carries the full active list so the renderer can greedy-fit
162
- // chips into whatever cell budget the terminal gives us. The legacy
163
- // 3-chip + "+N more" form is preserved on `parts[0].value` for callers
164
- // that consume the plain text (chat history, banner, tests).
165
- const allItems = activeAgents.map((id) => {
166
- const active = Boolean(activeAgentId && id === activeAgentId);
167
- return {
168
- label: withActivityMarker(ensureAtPrefix(getAgentLabel(id)), getAgentState(id)),
169
- selected: false,
170
- active,
171
- };
172
- });
173
- const agentItems = allItems;
174
- const visibleForText = allItems.slice(0, 3);
175
- const agents = activeAgents.length > 0
176
- ? visibleForText.map((item) => item.label).join(", ")
177
- + (activeAgents.length > 3 ? ` +${activeAgents.length - 3}` : "")
178
- : "none";
179
- const parts = [
180
- { label: "Agents", value: agents },
181
- { label: "Mode", value: launchMode },
182
- { label: "Agent", value: providerLabel(agentProvider) },
183
- { label: "Cron", value: String(Array.isArray(cronTasks) ? cronTasks.length : 0) },
184
- ];
185
- const loopPart = formatLoopSummary(loopSummary);
186
- if (loopPart) parts.push({ label: "Loop", value: loopPart });
187
- return {
188
- kind: "summary",
189
- agentItems,
190
- agentExtraCount: 0,
191
- parts,
192
- };
193
- }
194
-
195
- function buildProjectRow(options = {}) {
196
- const {
197
- projects = [],
198
- selectedProjectIndex = -1,
199
- projectListWindowStart = 0,
200
- maxWidth = 80,
201
- activeProjectRoot = "",
202
- focused = false,
203
- globalScope = "controller",
204
- } = options;
205
- const rows = Array.isArray(projects) ? projects : [];
206
- if (rows.length === 0) {
207
- return {
208
- kind: "chips",
209
- caption: "Projects",
210
- items: [],
211
- emptyLabel: "none",
212
- hint: options.dashHints && options.dashHints.projectsEmpty
213
- ? options.dashHints.projectsEmpty
214
- : "Run ufoo chat or ufoo daemon start in project directories",
215
- windowStart: projectListWindowStart,
216
- };
217
- }
218
- const fallbackIndex = rows.findIndex((row) => projectRoot(row) === String(activeProjectRoot || ""));
219
- const selected = selectedProjectIndex >= 0 && selectedProjectIndex < rows.length
220
- ? selectedProjectIndex
221
- : (fallbackIndex >= 0 ? fallbackIndex : 0);
222
- const requestedHint = focused ? (globalScope === "controller" ? "Enter→project" : "Esc→global") : "";
223
- const planned = planChipsRow({
224
- caption: "Projects",
225
- labels: rows.map(projectName),
226
- selectedIndex: focused ? selected : -1,
227
- windowStart: projectListWindowStart || 0,
228
- hint: requestedHint,
229
- maxWidth,
230
- reserveHintWhenFocused: focused,
231
- });
232
- return {
233
- kind: "chips",
234
- caption: "Projects",
235
- leftMore: planned.leftMore,
236
- rightMore: planned.rightMore,
237
- windowStart: planned.windowStart,
238
- hint: planned.hint,
239
- items: planned.visible.map((item) => {
240
- const idx = item.absoluteIndex;
241
- const row = rows[idx];
242
- const root = projectRoot(row);
243
- return {
244
- label: item.label,
245
- selected: focused && idx === selected,
246
- active: Boolean(activeProjectRoot && root === activeProjectRoot),
247
- };
248
- }),
249
- };
250
- }
251
-
252
- function buildDetailRow(options = {}) {
253
- const {
254
- dashboardView = "agents",
255
- globalMode = false,
256
- activeAgents = [],
257
- activeAgentId = "",
258
- selectedAgentIndex = -1,
259
- agentListWindowStart = 0,
260
- maxAgentWindow = 4,
261
- maxWidth = 80,
262
- getAgentLabel = (id) => id,
263
- getAgentState = () => "",
264
- selectedModeIndex = 0,
265
- selectedProviderIndex = 0,
266
- selectedCronIndex = -1,
267
- modeOptions = [],
268
- providerOptions = [],
269
- cronTasks = [],
270
- dashHints = {},
271
- focused = false,
272
- } = options;
273
-
274
- if (dashboardView === "mode") {
275
- const labels = (modeOptions || []).map((label) => String(label));
276
- const planned = planChipsRow({
277
- caption: "Mode",
278
- labels,
279
- selectedIndex: focused ? selectedModeIndex : -1,
280
- hint: dashHints.mode || "",
281
- maxWidth,
282
- reserveHintWhenFocused: focused,
283
- });
284
- return {
285
- kind: "chips",
286
- caption: "Mode",
287
- hint: planned.hint,
288
- leftMore: planned.leftMore,
289
- rightMore: planned.rightMore,
290
- items: planned.visible.map((item) => ({
291
- label: item.label,
292
- selected: focused && item.absoluteIndex === selectedModeIndex,
293
- })),
294
- };
295
- }
296
- if (dashboardView === "provider") {
297
- const opts = Array.isArray(providerOptions) ? providerOptions : [];
298
- const labels = opts.map((opt) => String(opt && opt.label != null ? opt.label : opt));
299
- const planned = planChipsRow({
300
- caption: "Agent",
301
- labels,
302
- selectedIndex: focused ? selectedProviderIndex : -1,
303
- hint: dashHints.provider || "",
304
- maxWidth,
305
- reserveHintWhenFocused: focused,
306
- });
307
- return {
308
- kind: "chips",
309
- caption: "Agent",
310
- hint: planned.hint,
311
- leftMore: planned.leftMore,
312
- rightMore: planned.rightMore,
313
- items: planned.visible.map((item) => ({
314
- label: item.label,
315
- selected: focused && item.absoluteIndex === selectedProviderIndex,
316
- })),
317
- };
318
- }
319
- if (dashboardView === "cron") {
320
- const items = Array.isArray(cronTasks) ? cronTasks : [];
321
- const labels = items
322
- .map((item) => String(item.label || item.summary || item.id || ""))
323
- .filter(Boolean);
324
- if (labels.length === 0) {
325
- return {
326
- kind: "chips",
327
- caption: "Cron",
328
- hint: dashHints.cron || "",
329
- emptyLabel: "none",
330
- items: [],
331
- };
332
- }
333
- const planned = planChipsRow({
334
- caption: "Cron",
335
- labels,
336
- selectedIndex: focused ? selectedCronIndex : -1,
337
- hint: dashHints.cron || "",
338
- maxWidth,
339
- reserveHintWhenFocused: focused,
340
- });
341
- return {
342
- kind: "chips",
343
- caption: "Cron",
344
- hint: planned.hint,
345
- leftMore: planned.leftMore,
346
- rightMore: planned.rightMore,
347
- items: planned.visible.map((item) => ({
348
- label: item.label,
349
- selected: focused && item.absoluteIndex === selectedCronIndex,
350
- })),
351
- };
352
- }
353
-
354
- if (!activeAgents.length) {
355
- return {
356
- kind: "chips",
357
- caption: "Agents",
358
- emptyLabel: "none",
359
- hint: dashHints.agentsEmpty || "",
360
- items: [],
361
- windowStart: agentListWindowStart,
362
- };
363
- }
364
- const labels = activeAgents.map((agentId) => withActivityMarker(
365
- ensureAtPrefix(getAgentLabel(agentId)),
366
- getAgentState(agentId)
367
- ));
368
- // Keep the legacy `maxAgentWindow` as an upper bound on visible chips even
369
- // when there's plenty of horizontal room, so the agents row scrolls for
370
- // long lists in the same way the legacy blessed view did.
371
- const cap = Math.max(1, Math.min(maxAgentWindow || labels.length, labels.length));
372
- let cappedStart = clampAgentWindowWithSelection({
373
- activeCount: labels.length,
374
- maxWindow: cap,
375
- windowStart: agentListWindowStart || 0,
376
- selectionIndex: selectedAgentIndex,
377
- });
378
- const cappedLabels = labels.slice(cappedStart, cappedStart + cap);
379
- const cappedSelected = focused && selectedAgentIndex >= cappedStart
380
- ? selectedAgentIndex - cappedStart
381
- : -1;
382
- const planned = planChipsRow({
383
- caption: "Agents",
384
- labels: cappedLabels,
385
- selectedIndex: cappedSelected,
386
- windowStart: 0,
387
- hint: globalMode ? (dashHints.agentsGlobal || dashHints.agents || "") : (dashHints.agents || ""),
388
- maxWidth,
389
- reserveHintWhenFocused: focused,
390
- });
391
- return {
392
- kind: "chips",
393
- caption: "Agents",
394
- leftMore: cappedStart > 0 || planned.leftMore,
395
- rightMore: (cappedStart + cap < labels.length) || planned.rightMore,
396
- windowStart: cappedStart,
397
- hint: planned.hint,
398
- items: planned.visible.map((item) => {
399
- const cappedIdx = item.absoluteIndex;
400
- const absolute = cappedStart + cappedIdx;
401
- const agentId = activeAgents[absolute];
402
- const active = Boolean(activeAgentId && agentId === activeAgentId);
403
- return {
404
- label: item.label,
405
- selected: focused && absolute === selectedAgentIndex,
406
- ...(active ? { active: true } : {}),
407
- };
408
- }),
409
- };
410
- }
411
-
412
- function buildDashboardRows(options = {}) {
413
- const {
414
- globalMode = false,
415
- globalScope = "controller",
416
- focusMode = "input",
417
- dashboardView = "agents",
418
- projects = [],
419
- dashHints = {},
420
- } = options;
421
- const focused = focusMode === "dashboard";
422
- if (globalMode) {
423
- const projectsFocused = focused && dashboardView === "projects";
424
- const projectRow = buildProjectRow({
425
- ...options,
426
- focused: projectsFocused,
427
- globalScope,
428
- });
429
- if (!focused || projectsFocused) {
430
- return [projectRow, buildSummaryRow(options)];
431
- }
432
- return [projectRow, buildDetailRow({ ...options, focused })];
433
- }
434
- if (focused) return [buildDetailRow({ ...options, focused })];
435
- return [buildSummaryRow(options)];
436
- }
437
-
438
- /**
439
- * Render a chip row to plain text + ANSI color spans, sized to fit
440
- * `maxWidth`. Includes the caption, optional `< / >` markers, optional
441
- * `emptyLabel`, and the trailing hint when the planner kept it. Used as the
442
- * sole text payload of a row's <Text wrap="truncate">.
443
- */
444
- function renderChipRowText(row, maxWidth = 80) {
445
- const budget = Math.max(1, Math.floor(Number(maxWidth) || 80));
446
- const { caption = "", items = [], hint = "", leftMore, rightMore, emptyLabel } = row || {};
447
- const captionText = `${caption}: `;
448
- let out = chalk.gray(captionText);
449
- let used = displayCellWidth(captionText);
450
-
451
- if (leftMore) {
452
- out += chalk.gray("< ");
453
- used += 2;
454
- }
455
- if (items.length === 0 && emptyLabel) {
456
- const remaining = Math.max(0, budget - used - (hint ? HINT_PREFIX_WIDTH + displayCellWidth(hint) : 0));
457
- const trimmedEmpty = truncateToCells(emptyLabel, remaining);
458
- out += chalk.cyan(trimmedEmpty);
459
- used += displayCellWidth(trimmedEmpty);
460
- }
461
- for (let i = 0; i < items.length; i += 1) {
462
- if (i > 0) {
463
- out += chalk.gray(CHIP_SEP);
464
- used += CHIP_SEP_WIDTH;
465
- }
466
- const item = items[i];
467
- const label = String(item.label || "");
468
- if (item.selected) {
469
- out += chalk.inverse(label);
470
- } else if (item.active) {
471
- out += chalk.bold.cyan(label);
472
- } else {
473
- out += chalk.cyan(label);
474
- }
475
- used += displayCellWidth(label);
476
- }
477
- if (rightMore) {
478
- out += chalk.gray(" >");
479
- used += 2;
480
- }
481
- if (hint) {
482
- const remaining = Math.max(0, budget - used);
483
- if (remaining > HINT_PREFIX_WIDTH + 1) {
484
- const hintBody = truncateToCells(hint, remaining - HINT_PREFIX_WIDTH);
485
- out += chalk.gray(`${HINT_PREFIX}${hintBody}`);
486
- }
487
- }
488
- return out;
489
- }
490
-
491
- function renderSummaryRowText(row, maxWidth = 80) {
492
- const budget = Math.max(1, Math.floor(Number(maxWidth) || 80));
493
- const { parts = [], agentItems = [] } = row || {};
494
-
495
- // Pre-render the non-Agents parts so we know how many cells they will
496
- // claim. Agents is special: it carries the full active list and we want
497
- // to fit as many chips as the remaining budget allows.
498
- const tailParts = parts.slice(1).map((part) => {
499
- const labelText = `${part.label}: `;
500
- const labelWidth = displayCellWidth(labelText);
501
- const value = String(part.value || "");
502
- return {
503
- label: part.label,
504
- labelText,
505
- labelColored: chalk.gray(labelText),
506
- labelWidth,
507
- value,
508
- width: labelWidth + displayCellWidth(value),
509
- colored: chalk.gray(labelText) + chalk.cyan(value),
510
- truncatable: part.label === "Loop",
511
- };
512
- });
513
-
514
- // How many cells would tail parts ideally claim (with their leading gap)?
515
- // We use this to reserve room for them when packing Agents chips, but we
516
- // never reserve so much that Agents can't fit at least one short chip
517
- // (otherwise narrow terminals show "Agents: +N" with zero names).
518
- let tailIdealWidth = 0;
519
- for (const tp of tailParts) {
520
- tailIdealWidth += SUMMARY_GAP_WIDTH + tp.width;
521
- }
522
- // Cap reservation so Agents always gets at least ~12 cells to play with
523
- // when there's any agent to show — enough for "@a +N" on the narrowest
524
- // displays. The remaining tail parts will simply be dropped one by one.
525
- const minAgentRoom = 12;
526
- const captionWidth = displayCellWidth("Agents: ");
527
- const tailReserve = Math.min(
528
- tailIdealWidth,
529
- Math.max(0, budget - captionWidth - minAgentRoom)
530
- );
531
-
532
- let out = "";
533
- let used = 0;
534
-
535
- const agentsPart = parts[0];
536
- if (agentsPart && agentsPart.label === "Agents") {
537
- const labelText = "Agents: ";
538
- const labelWidth = displayCellWidth(labelText);
539
- out += chalk.gray(labelText);
540
- used += labelWidth;
541
-
542
- if (agentItems.length === 0) {
543
- const noneText = "none";
544
- out += chalk.cyan(noneText);
545
- used += displayCellWidth(noneText);
546
- } else {
547
- // Reserve room for the worst-case " +N" overflow tail so we never have
548
- // to backtrack and pop a chip after committing to it.
549
- const worstOverflow = ` +${agentItems.length}`;
550
- const worstOverflowWidth = displayCellWidth(worstOverflow);
551
- const agentBudget = Math.max(0, budget - used - tailReserve);
552
- let fittedCount = 0;
553
- let agentsUsed = 0;
554
- for (let i = 0; i < agentItems.length; i += 1) {
555
- const item = agentItems[i];
556
- const label = String(item.label || "");
557
- const sepWidth = i === 0 ? 0 : displayCellWidth(", ");
558
- const remainingItems = agentItems.length - i - 1;
559
- const reserveOverflow = remainingItems > 0 ? worstOverflowWidth : 0;
560
- const labelWidthInner = displayCellWidth(label);
561
- if (agentsUsed + sepWidth + labelWidthInner + reserveOverflow > agentBudget) break;
562
- if (i > 0) {
563
- out += chalk.gray(", ");
564
- agentsUsed += sepWidth;
565
- }
566
- if (item.active) out += chalk.bold.cyan(label);
567
- else out += chalk.cyan(label);
568
- agentsUsed += labelWidthInner;
569
- fittedCount += 1;
570
- }
571
- const overflow = agentItems.length - fittedCount;
572
- if (overflow > 0) {
573
- const tail = ` +${overflow}`;
574
- out += chalk.cyan(tail);
575
- agentsUsed += displayCellWidth(tail);
576
- }
577
- used += agentsUsed;
578
- }
579
- }
580
-
581
- for (let i = 0; i < tailParts.length; i += 1) {
582
- const part = tailParts[i];
583
- const remaining = budget - used - SUMMARY_GAP_WIDTH;
584
- if (remaining <= 0) break;
585
- if (part.width <= remaining) {
586
- out += chalk.gray(SUMMARY_GAP) + part.colored;
587
- used += SUMMARY_GAP_WIDTH + part.width;
588
- continue;
589
- }
590
- if (part.truncatable && remaining >= 6) {
591
- const valueRoom = Math.max(1, remaining - part.labelWidth);
592
- const trimmedValue = truncateToCells(part.value, valueRoom);
593
- out += chalk.gray(SUMMARY_GAP) + part.labelColored + chalk.cyan(trimmedValue);
594
- used += SUMMARY_GAP_WIDTH + part.labelWidth + displayCellWidth(trimmedValue);
595
- }
596
- break;
597
- }
598
- return out;
599
- }
600
-
601
- function createDashboardBar({ React, ink }) {
602
- const { Box, Text } = ink;
603
- const h = React.createElement;
604
-
605
- return function DashboardBar({
606
- dashboardView = "agents",
607
- focusMode = "input",
608
- globalMode = false,
609
- globalScope = "controller",
610
- activeAgents = [],
611
- activeAgentId = "",
612
- selectedAgentIndex = -1,
613
- agentListWindowStart = 0,
614
- maxAgentWindow = 4,
615
- projectListWindowStart = 0,
616
- maxProjectWindow = 5,
617
- maxWidth = 80,
618
- getAgentLabel = (id) => id,
619
- getAgentState = () => "",
620
- launchMode = "terminal",
621
- agentProvider = "codex-cli",
622
- modeOptions = [],
623
- selectedModeIndex = 0,
624
- providerOptions = [],
625
- selectedProviderIndex = 0,
626
- cronTasks = [],
627
- loopSummary = null,
628
- selectedCronIndex = -1,
629
- projects = [],
630
- selectedProjectIndex = -1,
631
- activeProjectRoot = "",
632
- dashHints = {},
633
- }) {
634
- const rows = buildDashboardRows({
635
- dashboardView,
636
- focusMode,
637
- globalMode,
638
- globalScope,
639
- activeAgents,
640
- activeAgentId,
641
- selectedAgentIndex,
642
- agentListWindowStart,
643
- maxAgentWindow,
644
- projectListWindowStart,
645
- maxProjectWindow,
646
- maxWidth,
647
- getAgentLabel,
648
- getAgentState,
649
- launchMode,
650
- agentProvider,
651
- modeOptions,
652
- selectedModeIndex,
653
- providerOptions,
654
- selectedProviderIndex,
655
- cronTasks,
656
- loopSummary,
657
- selectedCronIndex,
658
- projects,
659
- selectedProjectIndex,
660
- activeProjectRoot,
661
- dashHints,
662
- }).map((row, idx) => {
663
- let text;
664
- if (row.kind === "summary") {
665
- text = renderSummaryRowText(row, maxWidth);
666
- } else if (row.kind === "message") {
667
- const value = String(row.text || "");
668
- text = chalk.gray(truncateToCells(value, maxWidth));
669
- } else {
670
- text = renderChipRowText(row, maxWidth);
671
- }
672
- return h(Box, { key: `dr-${idx}`, width: "100%" },
673
- h(Text, { wrap: "truncate" }, text || " "));
674
- });
675
-
676
- if (rows.length === 1) return h(Box, { width: "100%" }, rows[0]);
677
- return h(Box, { flexDirection: "column", width: "100%" }, ...rows);
678
- };
679
- }
680
-
681
- function renderDashboardLines(params) {
682
- const maxWidth = params.maxWidth || 80;
683
- const rows = buildDashboardRows(params);
684
- return rows.map((row) => {
685
- if (row.kind === "summary") return renderSummaryRowText(row, maxWidth);
686
- if (row.kind === "message") return chalk.gray(truncateToCells(String(row.text || ""), maxWidth));
687
- return renderChipRowText(row, maxWidth);
688
- });
689
- }
690
-
691
- module.exports = { createDashboardBar, buildDashboardRows, renderDashboardLines, formatLoopSummary };