@zakmandhro/bunti 0.1.5 → 0.2.1

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 (192) hide show
  1. package/AGENTS.md +65 -0
  2. package/README.md +86 -11
  3. package/dist/cli.d.ts +44 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +147 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/colors.d.ts +77 -7
  8. package/dist/colors.d.ts.map +1 -1
  9. package/dist/colors.js +238 -39
  10. package/dist/colors.js.map +1 -1
  11. package/dist/components/Box.d.ts +6 -0
  12. package/dist/components/Box.d.ts.map +1 -1
  13. package/dist/components/Box.js +5 -0
  14. package/dist/components/Box.js.map +1 -1
  15. package/dist/components/Button.d.ts +15 -2
  16. package/dist/components/Button.d.ts.map +1 -1
  17. package/dist/components/Button.js +45 -25
  18. package/dist/components/Button.js.map +1 -1
  19. package/dist/components/Card.d.ts +24 -2
  20. package/dist/components/Card.d.ts.map +1 -1
  21. package/dist/components/Card.js +56 -14
  22. package/dist/components/Card.js.map +1 -1
  23. package/dist/components/Header.d.ts +11 -1
  24. package/dist/components/Header.d.ts.map +1 -1
  25. package/dist/components/Header.js +12 -8
  26. package/dist/components/Header.js.map +1 -1
  27. package/dist/components/Input.d.ts +10 -1
  28. package/dist/components/Input.d.ts.map +1 -1
  29. package/dist/components/Input.js +176 -41
  30. package/dist/components/Input.js.map +1 -1
  31. package/dist/components/Link.d.ts +21 -0
  32. package/dist/components/Link.d.ts.map +1 -0
  33. package/dist/components/Link.js +38 -0
  34. package/dist/components/Link.js.map +1 -0
  35. package/dist/components/Modal.d.ts +25 -0
  36. package/dist/components/Modal.d.ts.map +1 -1
  37. package/dist/components/Modal.js +88 -24
  38. package/dist/components/Modal.js.map +1 -1
  39. package/dist/components/Progress.d.ts +27 -0
  40. package/dist/components/Progress.d.ts.map +1 -0
  41. package/dist/components/Progress.js +39 -0
  42. package/dist/components/Progress.js.map +1 -0
  43. package/dist/components/Spinner.d.ts +19 -0
  44. package/dist/components/Spinner.d.ts.map +1 -0
  45. package/dist/components/Spinner.js +27 -0
  46. package/dist/components/Spinner.js.map +1 -0
  47. package/dist/components/index.d.ts +3 -0
  48. package/dist/components/index.d.ts.map +1 -1
  49. package/dist/components/index.js +3 -0
  50. package/dist/components/index.js.map +1 -1
  51. package/dist/data/nf-glyphs.d.ts +11 -0
  52. package/dist/data/nf-glyphs.d.ts.map +1 -0
  53. package/dist/data/nf-glyphs.js +535 -0
  54. package/dist/data/nf-glyphs.js.map +1 -0
  55. package/dist/data/nf-names.d.ts +8 -0
  56. package/dist/data/nf-names.d.ts.map +1 -0
  57. package/dist/data/nf-names.js +2 -0
  58. package/dist/data/nf-names.js.map +1 -0
  59. package/dist/demo-registry.d.ts +27 -0
  60. package/dist/demo-registry.d.ts.map +1 -0
  61. package/dist/demo-registry.js +57 -0
  62. package/dist/demo-registry.js.map +1 -0
  63. package/dist/demos/2048.ts +743 -0
  64. package/dist/demos/animation.ts +246 -0
  65. package/dist/demos/demo-layout.ts +124 -0
  66. package/dist/demos/engine.ts +374 -0
  67. package/dist/demos/interaction.ts +91 -0
  68. package/dist/demos/login.ts +203 -0
  69. package/dist/demos/mission-control.ts +558 -0
  70. package/dist/demos/showcase.ts +224 -0
  71. package/dist/detect.d.ts +92 -3
  72. package/dist/detect.d.ts.map +1 -1
  73. package/dist/detect.js +218 -40
  74. package/dist/detect.js.map +1 -1
  75. package/dist/diagnostics.d.ts +73 -0
  76. package/dist/diagnostics.d.ts.map +1 -0
  77. package/dist/diagnostics.js +225 -0
  78. package/dist/diagnostics.js.map +1 -0
  79. package/dist/dsl/context.d.ts +11 -0
  80. package/dist/dsl/context.d.ts.map +1 -0
  81. package/dist/dsl/context.js +658 -0
  82. package/dist/dsl/context.js.map +1 -0
  83. package/dist/dsl/hooks.d.ts +12 -0
  84. package/dist/dsl/hooks.d.ts.map +1 -0
  85. package/dist/dsl/hooks.js +148 -0
  86. package/dist/dsl/hooks.js.map +1 -0
  87. package/dist/dsl/interaction.d.ts +31 -0
  88. package/dist/dsl/interaction.d.ts.map +1 -0
  89. package/dist/dsl/interaction.js +150 -0
  90. package/dist/dsl/interaction.js.map +1 -0
  91. package/dist/dsl/layers.d.ts +14 -0
  92. package/dist/dsl/layers.d.ts.map +1 -0
  93. package/dist/dsl/layers.js +87 -0
  94. package/dist/dsl/layers.js.map +1 -0
  95. package/dist/dsl/motion.d.ts +10 -0
  96. package/dist/dsl/motion.d.ts.map +1 -0
  97. package/dist/dsl/motion.js +150 -0
  98. package/dist/dsl/motion.js.map +1 -0
  99. package/dist/dsl/render.d.ts +22 -0
  100. package/dist/dsl/render.d.ts.map +1 -0
  101. package/dist/dsl/render.js +69 -0
  102. package/dist/dsl/render.js.map +1 -0
  103. package/dist/dsl/types.d.ts +555 -0
  104. package/dist/dsl/types.d.ts.map +1 -0
  105. package/dist/dsl/types.js +33 -0
  106. package/dist/dsl/types.js.map +1 -0
  107. package/dist/dsl.d.ts +13 -154
  108. package/dist/dsl.d.ts.map +1 -1
  109. package/dist/dsl.js +12 -658
  110. package/dist/dsl.js.map +1 -1
  111. package/dist/easing.d.ts +41 -1
  112. package/dist/easing.d.ts.map +1 -1
  113. package/dist/easing.js +105 -2
  114. package/dist/easing.js.map +1 -1
  115. package/dist/geometry.d.ts +39 -0
  116. package/dist/geometry.d.ts.map +1 -1
  117. package/dist/geometry.js +20 -0
  118. package/dist/geometry.js.map +1 -1
  119. package/dist/icons-full.d.ts +5 -0
  120. package/dist/icons-full.d.ts.map +1 -0
  121. package/dist/icons-full.js +31 -0
  122. package/dist/icons-full.js.map +1 -0
  123. package/dist/icons.d.ts +426 -5
  124. package/dist/icons.d.ts.map +1 -1
  125. package/dist/icons.js +144 -15
  126. package/dist/icons.js.map +1 -1
  127. package/dist/index.d.ts +95 -23
  128. package/dist/index.d.ts.map +1 -1
  129. package/dist/index.js +17 -7
  130. package/dist/index.js.map +1 -1
  131. package/dist/input.d.ts +177 -0
  132. package/dist/input.d.ts.map +1 -0
  133. package/dist/input.js +533 -0
  134. package/dist/input.js.map +1 -0
  135. package/dist/layout.d.ts +153 -7
  136. package/dist/layout.d.ts.map +1 -1
  137. package/dist/layout.js +221 -34
  138. package/dist/layout.js.map +1 -1
  139. package/dist/render.d.ts +33 -1
  140. package/dist/render.d.ts.map +1 -1
  141. package/dist/render.js +431 -116
  142. package/dist/render.js.map +1 -1
  143. package/dist/state.d.ts +156 -3
  144. package/dist/state.d.ts.map +1 -1
  145. package/dist/state.js +25 -0
  146. package/dist/state.js.map +1 -1
  147. package/dist/theme.d.ts +129 -0
  148. package/dist/theme.d.ts.map +1 -0
  149. package/dist/theme.js +167 -0
  150. package/dist/theme.js.map +1 -0
  151. package/dist/themes/catppuccin-mocha.d.ts +19 -0
  152. package/dist/themes/catppuccin-mocha.d.ts.map +1 -0
  153. package/dist/themes/catppuccin-mocha.js +36 -0
  154. package/dist/themes/catppuccin-mocha.js.map +1 -0
  155. package/dist/themes/dracula.d.ts +21 -0
  156. package/dist/themes/dracula.d.ts.map +1 -0
  157. package/dist/themes/dracula.js +37 -0
  158. package/dist/themes/dracula.js.map +1 -0
  159. package/dist/themes/github-light.d.ts +21 -0
  160. package/dist/themes/github-light.d.ts.map +1 -0
  161. package/dist/themes/github-light.js +36 -0
  162. package/dist/themes/github-light.js.map +1 -0
  163. package/dist/themes/index.d.ts +19 -0
  164. package/dist/themes/index.d.ts.map +1 -0
  165. package/dist/themes/index.js +17 -0
  166. package/dist/themes/index.js.map +1 -0
  167. package/dist/themes/nord.d.ts +23 -0
  168. package/dist/themes/nord.d.ts.map +1 -0
  169. package/dist/themes/nord.js +38 -0
  170. package/dist/themes/nord.js.map +1 -0
  171. package/dist/themes/one-dark-pro.d.ts +21 -0
  172. package/dist/themes/one-dark-pro.d.ts.map +1 -0
  173. package/dist/themes/one-dark-pro.js +36 -0
  174. package/dist/themes/one-dark-pro.js.map +1 -0
  175. package/dist/themes/tokyo-night.d.ts +20 -0
  176. package/dist/themes/tokyo-night.d.ts.map +1 -0
  177. package/dist/themes/tokyo-night.js +37 -0
  178. package/dist/themes/tokyo-night.js.map +1 -0
  179. package/dist/utils.d.ts +1 -0
  180. package/dist/utils.d.ts.map +1 -1
  181. package/dist/utils.js +1 -0
  182. package/dist/utils.js.map +1 -1
  183. package/dist/vendor/colors.d.ts +45 -0
  184. package/dist/vendor/colors.d.ts.map +1 -0
  185. package/dist/vendor/colors.js +72 -0
  186. package/dist/vendor/colors.js.map +1 -0
  187. package/llms.txt +79 -0
  188. package/package.json +23 -7
  189. package/dist/data/glyphs.d.ts +0 -5
  190. package/dist/data/glyphs.d.ts.map +0 -1
  191. package/dist/data/glyphs.js +0 -29
  192. package/dist/data/glyphs.js.map +0 -1
@@ -0,0 +1,558 @@
1
+ /**
2
+ * Agent Fleet Mission Control — the Bunti launch hero demo.
3
+ *
4
+ * A fleet of AI coding agents working tasks across repos: live agent list,
5
+ * task detail with gradient progress + tokens/min sparkline, and a streaming
6
+ * activity log. Every cell is painted from ctx.theme tokens, so number keys
7
+ * 1-8 restyle the whole dashboard live via ctx.setTheme.
8
+ *
9
+ * Keys: 1-8 themes · up/down or click to select an agent · q quit.
10
+ * Headless: --once renders a single frame; --size 100x32 fakes a viewport.
11
+ */
12
+
13
+ import { Progress, Spinner } from '../components/index.js';
14
+ import {
15
+ darkTheme,
16
+ easeOutCubic,
17
+ lightTheme,
18
+ type RGB,
19
+ render,
20
+ type Theme,
21
+ type ThemeColor,
22
+ truncate,
23
+ visibleWidth,
24
+ } from '../index.js';
25
+ import { themes } from '../themes/index.js';
26
+
27
+ // --- CLI flags (headless testing / recording) -------------------------------
28
+
29
+ const once = process.argv.includes('--once');
30
+ const sizeArg = process.argv[process.argv.indexOf('--size') + 1];
31
+ if (process.argv.includes('--size') && sizeArg?.includes('x')) {
32
+ const [c, r] = sizeArg.split('x').map((n) => Number.parseInt(n, 10));
33
+ if (c && r) {
34
+ Object.defineProperty(process.stdout, 'columns', {
35
+ value: c,
36
+ configurable: true,
37
+ });
38
+ Object.defineProperty(process.stdout, 'rows', {
39
+ value: r,
40
+ configurable: true,
41
+ });
42
+ }
43
+ }
44
+
45
+ const THEMES: Theme[] = [
46
+ darkTheme,
47
+ lightTheme,
48
+ themes.dracula!,
49
+ themes['tokyo-night']!,
50
+ themes['catppuccin-mocha']!,
51
+ themes.nord!,
52
+ themes['one-dark-pro']!,
53
+ themes['github-light']!,
54
+ ];
55
+
56
+ // --- Fleet simulation --------------------------------------------------------
57
+
58
+ type Status = 'coding' | 'review' | 'blocked' | 'idle';
59
+
60
+ interface Agent {
61
+ name: string;
62
+ repo: string;
63
+ branch: string;
64
+ task: string;
65
+ status: Status;
66
+ progress: number;
67
+ rate: number; // tokens/min
68
+ history: number[]; // rolling rate series (reused array)
69
+ tokens: number;
70
+ files: number;
71
+ startedAt: number;
72
+ }
73
+
74
+ interface FleetEvent {
75
+ ts: string;
76
+ kind: 'commit' | 'tests' | 'merge' | 'ship' | 'stall' | 'plan';
77
+ agent: string;
78
+ msg: string;
79
+ born: number;
80
+ }
81
+
82
+ const TASKS = [
83
+ 'refactoring theme engine',
84
+ 'wiring OAuth callbacks',
85
+ 'chasing a flaky e2e suite',
86
+ 'tuning the diff renderer',
87
+ 'porting the icons pipeline',
88
+ 'writing migration codemods',
89
+ 'profiling the render loop',
90
+ 'reviewing PR #214',
91
+ 'drafting release notes',
92
+ 'untangling circular imports',
93
+ ];
94
+
95
+ const agents: Agent[] = [
96
+ ['nova', 'bunti', 'feat/motion-engine', 'coding'],
97
+ ['luna', 'lit-factory', 'feat/agent-inbox', 'coding'],
98
+ ['sona', 'pullflow-web', 'fix/review-sync', 'review'],
99
+ ['atlas', 'space-station', 'chore/planet-gc', 'coding'],
100
+ ['vega', 'bunti-docs', 'docs/quickstart', 'idle'],
101
+ ['orion', 'gstack', 'fix/browse-daemon', 'blocked'],
102
+ ['mira', 'lit-factory', 'feat/task-router', 'coding'],
103
+ ].map(([name, repo, branch, status], i) => ({
104
+ name: name as string,
105
+ repo: repo as string,
106
+ branch: branch as string,
107
+ task: TASKS[i % TASKS.length]!,
108
+ status: status as Status,
109
+ progress: 0.15 + i * 0.11,
110
+ rate: 90 + i * 25,
111
+ history: [] as number[],
112
+ tokens: 180_000 + i * 90_000,
113
+ files: 3 + (i % 6),
114
+ startedAt: Date.now() - (i + 2) * 7 * 60_000,
115
+ }));
116
+
117
+ const events: FleetEvent[] = [];
118
+ const fleetHistory: number[] = [];
119
+ const HISTORY_CAP = 48;
120
+ const EVENT_CAP = 48;
121
+ let taskCursor = agents.length;
122
+
123
+ const clock = (d = new Date()) => d.toTimeString().slice(0, 8);
124
+
125
+ function pushEvent(kind: FleetEvent['kind'], agent: string, msg: string) {
126
+ events.push({ ts: clock(), kind, agent, msg, born: Date.now() });
127
+ if (events.length > EVENT_CAP) events.shift();
128
+ }
129
+
130
+ const CHATTER: [FleetEvent['kind'], (a: Agent) => string][] = [
131
+ ['commit', (a) => `pushed 2 commits to ${a.branch}`],
132
+ ['tests', (a) => `${a.repo} suite green in 41s`],
133
+ ['plan', (a) => `split "${a.task}" into 3 steps`],
134
+ ['commit', (a) => `rebased ${a.branch} on main`],
135
+ ['merge', (a) => `merged PR #${180 + (((a.tokens / 1000) | 0) % 90)}`],
136
+ ['tests', (a) => `added 6 cases for ${a.repo}`],
137
+ ];
138
+
139
+ function chatter() {
140
+ const a = agents[Math.floor(Math.random() * agents.length)]!;
141
+ if (a.status === 'blocked') {
142
+ pushEvent('stall', a.name, `waiting on CI for ${a.branch}`);
143
+ } else {
144
+ const [kind, msg] = CHATTER[Math.floor(Math.random() * CHATTER.length)]!;
145
+ pushEvent(kind, a.name, msg(a));
146
+ }
147
+ }
148
+
149
+ function tick(): number {
150
+ let fleetRate = 0;
151
+ for (const a of agents) {
152
+ const active = a.status === 'coding' || a.status === 'review';
153
+ if (active) {
154
+ a.progress += 0.02 + Math.random() * 0.05;
155
+ a.rate = Math.min(
156
+ 320,
157
+ Math.max(45, a.rate + (Math.random() - 0.48) * 46),
158
+ );
159
+ a.tokens += (a.rate / 60) * 0.9 * 1000;
160
+ } else {
161
+ a.rate = Math.max(12, a.rate * 0.86);
162
+ }
163
+ fleetRate += a.rate;
164
+ a.history.push(a.rate);
165
+ if (a.history.length > HISTORY_CAP) a.history.shift();
166
+
167
+ if (a.progress >= 1) {
168
+ pushEvent('ship', a.name, `shipped "${a.task}"`);
169
+ a.progress = 0;
170
+ a.task = TASKS[taskCursor++ % TASKS.length]!;
171
+ a.files = 2 + Math.floor(Math.random() * 9);
172
+ a.startedAt = Date.now();
173
+ if (a.status === 'review') a.status = 'coding';
174
+ }
175
+ }
176
+ fleetHistory.push(fleetRate);
177
+ if (fleetHistory.length > HISTORY_CAP) fleetHistory.shift();
178
+
179
+ if (Math.random() < 0.85) chatter();
180
+ if (Math.random() < 0.35) chatter();
181
+ return Date.now();
182
+ }
183
+
184
+ // Seed histories + a first screen of log lines so frame one is never empty.
185
+ for (let i = 0; i < 24; i++) {
186
+ let fleetRate = 0;
187
+ for (const a of agents) {
188
+ const sample = a.rate + Math.sin(i * 0.7 + a.rate) * 30;
189
+ a.history.push(sample);
190
+ fleetRate += sample;
191
+ }
192
+ fleetHistory.push(fleetRate);
193
+ }
194
+ pushEvent('plan', 'nova', 'fleet online — 7 agents reporting');
195
+ pushEvent('commit', 'luna', 'pushed 2 commits to feat/agent-inbox');
196
+ pushEvent('tests', 'atlas', 'space-station suite green in 38s');
197
+ pushEvent('merge', 'sona', 'merged PR #209 into pullflow-web');
198
+ pushEvent('commit', 'mira', 'rebased feat/task-router on main');
199
+
200
+ // --- Small render helpers ----------------------------------------------------
201
+
202
+ const SPARK = '▁▂▃▄▅▆▇█';
203
+
204
+ function sparkline(series: number[], width: number): string {
205
+ const n = Math.min(width, series.length);
206
+ if (n === 0) return '';
207
+ let min = Number.POSITIVE_INFINITY;
208
+ let max = Number.NEGATIVE_INFINITY;
209
+ for (let i = series.length - n; i < series.length; i++) {
210
+ const v = series[i]!;
211
+ if (v < min) min = v;
212
+ if (v > max) max = v;
213
+ }
214
+ const span = max - min || 1;
215
+ let out = '';
216
+ for (let i = series.length - n; i < series.length; i++) {
217
+ out += SPARK[Math.round(((series[i]! - min) / span) * 7)];
218
+ }
219
+ return out;
220
+ }
221
+
222
+ const fmtTokens = (n: number) =>
223
+ n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : `${Math.round(n / 1e3)}k`;
224
+
225
+ function fmtUptime(since: number): string {
226
+ const s = Math.max(0, Math.floor((Date.now() - since) / 1000));
227
+ const h = Math.floor(s / 3600);
228
+ const m = Math.floor((s % 3600) / 60);
229
+ return h > 0 ? `${h}h ${String(m).padStart(2, '0')}m` : `${m}m ${s % 60}s`;
230
+ }
231
+
232
+ const STATUS_META: Record<
233
+ Status,
234
+ { icon: string; token: keyof Theme & string; label: string }
235
+ > = {
236
+ coding: { icon: 'robot', token: 'info', label: 'coding' },
237
+ review: { icon: 'pr', token: 'accent', label: 'review' },
238
+ blocked: { icon: 'warning', token: 'warning', label: 'blocked' },
239
+ idle: { icon: 'pause', token: 'muted', label: 'idle' },
240
+ };
241
+
242
+ const EVENT_META: Record<
243
+ FleetEvent['kind'],
244
+ { icon: string; token: keyof Theme & string }
245
+ > = {
246
+ commit: { icon: 'commit', token: 'info' },
247
+ tests: { icon: 'check', token: 'success' },
248
+ merge: { icon: 'merge', token: 'accent' },
249
+ ship: { icon: 'rocket', token: 'primary' },
250
+ stall: { icon: 'warning', token: 'warning' },
251
+ plan: { icon: 'list', token: 'muted' },
252
+ };
253
+
254
+ // --- The dashboard -----------------------------------------------------------
255
+
256
+ render(
257
+ (ctx) => {
258
+ const t = ctx.theme;
259
+ const { width: W, height: H, color, icon } = ctx;
260
+ ctx.wallpaper(t.background);
261
+
262
+ // Input -------------------------------------------------------------
263
+ if (ctx.lastKey === 'q' || ctx.lastKey === 'escape') ctx.requestStop();
264
+ if (ctx.lastKey === 'r') ctx.restartAnimation('entrance');
265
+ const pick = Number.parseInt(ctx.lastKey ?? '', 10);
266
+ if (pick >= 1 && pick <= THEMES.length) ctx.setTheme(THEMES[pick - 1]!);
267
+
268
+ if (W < 72 || H < 20) {
269
+ const msg = 'mission control needs at least 72x20';
270
+ ctx.blit(Math.max(0, (W - msg.length) >> 1), H >> 1, t.muted(msg));
271
+ return;
272
+ }
273
+
274
+ // Simulation heartbeat (useAsync-fed fake telemetry) -----------------
275
+ ctx.useAsync('fleet-tick', async () => tick(), { interval: 900 });
276
+
277
+ // Entrance choreography ----------------------------------------------
278
+ // Panels stagger in over the first second; theme swaps later restyle
279
+ // in place (no replay), and `r` replays the cascade (the 'entrance' id
280
+ // clock). --once skips it so a single frame shows the full UI.
281
+ const enter = (i: number) =>
282
+ once
283
+ ? 1
284
+ : ctx.stagger(i + 1, {
285
+ id: 'entrance',
286
+ delay: 150,
287
+ duration: 240,
288
+ easing: easeOutCubic,
289
+ });
290
+ const mix = (token: ThemeColor, p: number): RGB =>
291
+ p >= 1 ? token.rgb : ctx.fade(t.background.rgb, token.rgb, p);
292
+ const ink =
293
+ (token: ThemeColor, p: number) =>
294
+ (s: string): string =>
295
+ color.fg(mix(token, p), s);
296
+
297
+ // Geometry ------------------------------------------------------------
298
+ const margin = W >= 100 ? 2 : 1;
299
+ const wide = W >= 100;
300
+ const headerH = 3;
301
+ const bodyY = headerH + 1;
302
+ const bodyH = H - bodyY - 2;
303
+ const leftW = wide ? 34 : 27;
304
+ const body = { x: margin, y: bodyY, width: W - margin * 2, height: bodyH };
305
+ const fleet = { x: body.x, y: body.y, width: leftW, height: body.height };
306
+ const right = {
307
+ x: body.x + leftW + 2,
308
+ y: body.y,
309
+ width: body.width - leftW - 2,
310
+ height: body.height,
311
+ };
312
+ const detailH = H >= 32 ? 12 : 9;
313
+ const logY = right.y + detailH + 1;
314
+ const logH = right.height - detailH - 1;
315
+
316
+ // Selection (shared state key with the agents list) -------------------
317
+ const [selected] = ctx.useState('agents_index', 0);
318
+ const agent = agents[Math.min(selected, agents.length - 1)]!;
319
+ const [prevSel, setPrevSel] = ctx.useState('detail_prev', selected);
320
+ if (prevSel !== selected) {
321
+ setPrevSel(selected);
322
+ ctx.restartAnimation('detail-fade');
323
+ }
324
+ const swap = ctx.animate(220, { id: 'detail-fade', easing: easeOutCubic });
325
+
326
+ // 1 · Header bar -------------------------------------------------------
327
+ const pHeader = enter(0);
328
+ ctx.box(
329
+ {
330
+ x: 0,
331
+ y: 0,
332
+ width: W,
333
+ height: headerH,
334
+ border: 'none',
335
+ bgColor: mix(t.surfaceRaised, pHeader),
336
+ padding: [0, margin + 1],
337
+ valign: 'middle',
338
+ },
339
+ (b) => {
340
+ if (pHeader < 0.5) return;
341
+ const left = `${icon('bunti')} `;
342
+ const leftTitle = 'AGENT FLEET';
343
+ const leftSub = ' mission control';
344
+ const rightPlain = `. fleet live ${clock()} · ${t.name}`;
345
+ const pad = Math.max(
346
+ 1,
347
+ W -
348
+ (margin + 1) * 2 -
349
+ visibleWidth(left + leftTitle + leftSub) -
350
+ visibleWidth(rightPlain),
351
+ );
352
+ b.text(ink(t.primary, pHeader)(left));
353
+ b.text(color.bold(ink(t.foreground, pHeader)(leftTitle)));
354
+ b.text(ink(t.muted, pHeader)(leftSub));
355
+ b.text(' '.repeat(pad));
356
+ Spinner(b, { label: '', color: mix(t.accent, pHeader) });
357
+ b.text(ink(t.success, pHeader)(' fleet live'));
358
+ b.text(ink(t.muted, pHeader)(` ${clock()} · `));
359
+ b.text(ink(t.accent, pHeader)(t.name));
360
+ },
361
+ );
362
+
363
+ // 2 · Agent list --------------------------------------------------------
364
+ const pFleet = enter(1);
365
+ const fleetY = fleet.y + Math.round((1 - pFleet) * 2);
366
+ ctx.box(
367
+ {
368
+ x: fleet.x,
369
+ y: fleetY,
370
+ width: fleet.width,
371
+ height: fleet.height,
372
+ border: 'rounded',
373
+ borderColor: mix(t.border, pFleet),
374
+ title: ' Agents ',
375
+ titleStyle: ink(t.muted, pFleet),
376
+ bgColor: mix(t.surface, pFleet),
377
+ padding: [1, 2],
378
+ },
379
+ (b) => {
380
+ if (pFleet < 0.6) return;
381
+ const inner = fleet.width - 6;
382
+ const repoW = Math.max(6, inner - 16);
383
+ const rows = agents.map((a) => {
384
+ const meta = STATUS_META[a.status];
385
+ const working = a.status === 'coding' || a.status === 'review';
386
+ const pct = working
387
+ ? `${String(Math.round(a.progress * 100)).padStart(3)}%`
388
+ : ' · ';
389
+ const name = working ? t.foreground : t.muted;
390
+ return (
391
+ `${(t[meta.token] as ThemeColor)(icon(meta.icon))} ` +
392
+ `${name(a.name.padEnd(7))}` +
393
+ `${t.muted(truncate(a.repo, repoW).padEnd(repoW))} ` +
394
+ `${t.muted(pct)}`
395
+ );
396
+ });
397
+ b.list('agents', rows, { width: '100%' });
398
+
399
+ // Fleet throughput block, pinned to the panel's bottom edge.
400
+ const innerH = fleet.height - 4;
401
+ const pad = innerH - agents.length - 3;
402
+ if (pad >= 1) {
403
+ const active = agents.filter((a) => a.status !== 'idle').length;
404
+ const counts = `${active}/${agents.length} active`;
405
+ const label = wide ? 'FLEET THROUGHPUT' : 'FLEET';
406
+ b.text('\n'.repeat(pad + 1));
407
+ b.text(t.muted(label));
408
+ b.text(' '.repeat(Math.max(1, inner - label.length - counts.length)));
409
+ b.text(t.muted(counts));
410
+ b.text('\n');
411
+ b.text(t.info(sparkline(fleetHistory, inner)));
412
+ b.text('\n');
413
+ const sum = fleetHistory[fleetHistory.length - 1] ?? 0;
414
+ b.text(t.muted('Σ '));
415
+ b.text(t.foreground(`${Math.round(sum)}`));
416
+ b.text(t.muted(' tok/min'));
417
+ }
418
+ },
419
+ );
420
+
421
+ // 3 · Mission detail ----------------------------------------------------
422
+ const pDetail = enter(2);
423
+ const detailY = right.y + Math.round((1 - pDetail) * 2);
424
+ ctx.box(
425
+ {
426
+ x: right.x,
427
+ y: detailY,
428
+ width: right.width,
429
+ height: detailH,
430
+ border: 'rounded',
431
+ borderColor: mix(t.border, pDetail),
432
+ title: ' Mission Detail ',
433
+ titleStyle: ink(t.muted, pDetail),
434
+ bgColor: mix(t.surface, pDetail),
435
+ padding: [1, 2],
436
+ },
437
+ (b) => {
438
+ if (pDetail < 0.6) return;
439
+ const dw = right.width - 6;
440
+ const rows = detailH - 4; // inner rows between border + padding
441
+ const meta = STATUS_META[agent.status];
442
+ const statusInk = ink(t[meta.token] as ThemeColor, swap);
443
+ const fgInk = ink(t.foreground, swap);
444
+ const mutedInk = ink(t.muted, swap);
445
+ const barW = Math.max(10, dw - 16);
446
+
447
+ b.text(color.bold(ink(t.primary, swap)(agent.name)));
448
+ b.text(mutedInk(' · '));
449
+ b.text(statusInk(`${icon(meta.icon)} ${meta.label}`));
450
+ b.text('\n');
451
+ b.text(fgInk(truncate(agent.task, dw)));
452
+ b.text('\n');
453
+ b.text(
454
+ mutedInk(
455
+ truncate(
456
+ `${icon('repo')} ${agent.repo} ${icon('branch')} ${agent.branch}`,
457
+ dw,
458
+ ),
459
+ ),
460
+ );
461
+ b.text('\n');
462
+ if (rows >= 6) b.text('\n');
463
+
464
+ b.text(mutedInk('progress '));
465
+ Progress(b, {
466
+ value:
467
+ agent.status === 'coding' || agent.status === 'review'
468
+ ? agent.progress
469
+ : 0,
470
+ width: barW,
471
+ gradient: [t.primary, t.accent],
472
+ trackColor: mix(t.border, Math.min(pDetail, swap)),
473
+ showPercent: true,
474
+ });
475
+ b.text('\n');
476
+
477
+ if (rows >= 8 && wide) {
478
+ b.text(mutedInk('tok/min '));
479
+ b.text(ink(t.accent, swap)(sparkline(agent.history, barW)));
480
+ b.text(color.bold(fgInk(` ${Math.round(agent.rate)}`)));
481
+ b.text('\n');
482
+ }
483
+ if (rows >= 7) b.text('\n');
484
+ const sep = rows >= 7 ? ' · ' : ' · ';
485
+ b.text(
486
+ mutedInk(
487
+ `tokens ${fmtTokens(agent.tokens)}${sep}files ${agent.files}${sep}up ${fmtUptime(agent.startedAt)}`,
488
+ ),
489
+ );
490
+ },
491
+ );
492
+
493
+ // 4 · Activity log --------------------------------------------------------
494
+ const pLog = enter(3);
495
+ const logYAnim = logY + Math.round((1 - pLog) * 2);
496
+ ctx.box(
497
+ {
498
+ x: right.x,
499
+ y: logYAnim,
500
+ width: right.width,
501
+ height: logH,
502
+ border: 'rounded',
503
+ borderColor: mix(t.border, pLog),
504
+ title: ' Activity ',
505
+ titleStyle: ink(t.muted, pLog),
506
+ bgColor: mix(t.surface, pLog),
507
+ padding: [1, 2],
508
+ },
509
+ (b) => {
510
+ if (pLog < 0.6) return;
511
+ const lw = right.width - 6;
512
+ const visible = Math.max(1, logH - 4);
513
+ const shown = events.slice(-visible);
514
+ const now = Date.now();
515
+ for (let i = 0; i < shown.length; i++) {
516
+ const e = shown[i]!;
517
+ const meta = EVENT_META[e.kind];
518
+ // New lines slide in from the left over ~240ms of their life.
519
+ const pe = Math.min(1, (now - e.born) / 240);
520
+ const slide = ' '.repeat(Math.round((1 - easeOutCubic(pe)) * 6));
521
+ const head = `${e.ts} `;
522
+ const badge = `${icon(meta.icon)} ${e.kind.padEnd(7)}`;
523
+ const who = `${e.agent.padEnd(7)}`;
524
+ const room = lw - slide.length - head.length - 9 - who.length;
525
+ b.text(slide);
526
+ b.text(t.muted(head));
527
+ b.text((t[meta.token] as ThemeColor)(badge));
528
+ b.text(t.foreground(who));
529
+ b.text(t.muted(truncate(e.msg, Math.max(4, room))));
530
+ if (i < shown.length - 1) b.text('\n');
531
+ }
532
+ },
533
+ );
534
+
535
+ // 5 · Keybar ----------------------------------------------------------------
536
+ const pBar = enter(4);
537
+ if (pBar > 0) {
538
+ const idx = THEMES.findIndex((th) => th.name === t.name) + 1;
539
+ const hint = '1-8 themes · ↑/↓ or click agents · q quit';
540
+ const status = `${idx > 0 ? idx : '·'}/8 ${t.name}`;
541
+ ctx.blit(margin + 1, H - 1, ink(t.muted, pBar)(hint));
542
+ ctx.blit(
543
+ Math.max(0, W - margin - 1 - visibleWidth(status)),
544
+ H - 1,
545
+ ink(t.accent, pBar)(status),
546
+ );
547
+ }
548
+ },
549
+ {
550
+ fps: 30,
551
+ keyboard: true,
552
+ mouse: true,
553
+ hideCursor: true,
554
+ alternateBuffer: !once,
555
+ theme: darkTheme,
556
+ once,
557
+ },
558
+ );