@zakmandhro/bunti 0.2.0 → 0.3.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 (127) hide show
  1. package/README.md +16 -10
  2. package/dist/cli.d.ts +2 -2
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +33 -9
  5. package/dist/cli.js.map +1 -1
  6. package/dist/colors.d.ts +2 -2
  7. package/dist/colors.js +2 -2
  8. package/dist/components/Box.d.ts +1 -1
  9. package/dist/components/Button.d.ts +6 -3
  10. package/dist/components/Button.d.ts.map +1 -1
  11. package/dist/components/Button.js +12 -3
  12. package/dist/components/Button.js.map +1 -1
  13. package/dist/components/Card.d.ts +1 -1
  14. package/dist/components/Card.js +3 -3
  15. package/dist/components/Header.d.ts +1 -1
  16. package/dist/components/Header.js +2 -2
  17. package/dist/components/Input.d.ts +2 -2
  18. package/dist/components/Input.d.ts.map +1 -1
  19. package/dist/components/Input.js +5 -1
  20. package/dist/components/Input.js.map +1 -1
  21. package/dist/components/Link.d.ts +2 -2
  22. package/dist/components/Link.d.ts.map +1 -1
  23. package/dist/components/Link.js +12 -3
  24. package/dist/components/Link.js.map +1 -1
  25. package/dist/components/Modal.d.ts +1 -1
  26. package/dist/components/Modal.d.ts.map +1 -1
  27. package/dist/components/Modal.js +11 -6
  28. package/dist/components/Modal.js.map +1 -1
  29. package/dist/components/Progress.d.ts +2 -2
  30. package/dist/components/Progress.js +2 -2
  31. package/dist/components/Spinner.d.ts +2 -2
  32. package/dist/components/index.d.ts +9 -9
  33. package/dist/components/index.js +9 -9
  34. package/dist/data/nf-glyphs.d.ts +2 -2
  35. package/dist/demo-registry.d.ts.map +1 -1
  36. package/dist/demo-registry.js +6 -1
  37. package/dist/demo-registry.js.map +1 -1
  38. package/dist/demos/2048.js +559 -0
  39. package/dist/demos/2048.ts +1 -1
  40. package/dist/demos/animation.js +177 -0
  41. package/dist/demos/animation.ts +1 -1
  42. package/dist/demos/demo-layout.js +85 -0
  43. package/dist/demos/demo-layout.ts +6 -1
  44. package/dist/demos/engine.js +244 -0
  45. package/dist/demos/interaction.js +74 -0
  46. package/dist/demos/interaction.ts +1 -1
  47. package/dist/demos/login.js +175 -0
  48. package/dist/demos/login.ts +1 -1
  49. package/dist/demos/mission-control.js +427 -0
  50. package/dist/demos/platformer-physics.js +122 -0
  51. package/dist/demos/platformer-physics.ts +284 -0
  52. package/dist/demos/platformer.js +519 -0
  53. package/dist/demos/platformer.ts +761 -0
  54. package/dist/demos/showcase.js +181 -0
  55. package/dist/demos/showcase.ts +1 -1
  56. package/dist/diagnostics.d.ts +2 -2
  57. package/dist/dsl/context.d.ts +2 -2
  58. package/dist/dsl/context.d.ts.map +1 -1
  59. package/dist/dsl/context.js +122 -27
  60. package/dist/dsl/context.js.map +1 -1
  61. package/dist/dsl/hooks.d.ts +2 -2
  62. package/dist/dsl/hooks.js +1 -1
  63. package/dist/dsl/interaction.d.ts +19 -4
  64. package/dist/dsl/interaction.d.ts.map +1 -1
  65. package/dist/dsl/interaction.js +54 -5
  66. package/dist/dsl/interaction.js.map +1 -1
  67. package/dist/dsl/layers.d.ts +2 -2
  68. package/dist/dsl/motion.d.ts +2 -2
  69. package/dist/dsl/motion.js +2 -2
  70. package/dist/dsl/render.d.ts +2 -2
  71. package/dist/dsl/render.d.ts.map +1 -1
  72. package/dist/dsl/render.js +4 -8
  73. package/dist/dsl/render.js.map +1 -1
  74. package/dist/dsl/types.d.ts +58 -12
  75. package/dist/dsl/types.d.ts.map +1 -1
  76. package/dist/dsl/types.js.map +1 -1
  77. package/dist/dsl.d.ts +4 -4
  78. package/dist/dsl.js +3 -3
  79. package/dist/easing.d.ts +1 -1
  80. package/dist/geometry.d.ts +1 -1
  81. package/dist/geometry.js +1 -1
  82. package/dist/icons-full.d.ts +2 -2
  83. package/dist/icons-full.js +3 -3
  84. package/dist/icons.d.ts +4 -4
  85. package/dist/icons.js +3 -3
  86. package/dist/index.d.ts +44 -34
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +29 -24
  89. package/dist/index.js.map +1 -1
  90. package/dist/layout.d.ts +61 -8
  91. package/dist/layout.d.ts.map +1 -1
  92. package/dist/layout.js +77 -21
  93. package/dist/layout.js.map +1 -1
  94. package/dist/pixels.d.ts +51 -0
  95. package/dist/pixels.d.ts.map +1 -0
  96. package/dist/pixels.js +82 -0
  97. package/dist/pixels.js.map +1 -0
  98. package/dist/render.d.ts +1 -1
  99. package/dist/render.d.ts.map +1 -1
  100. package/dist/render.js +44 -10
  101. package/dist/render.js.map +1 -1
  102. package/dist/state.d.ts +22 -3
  103. package/dist/state.d.ts.map +1 -1
  104. package/dist/state.js +1 -1
  105. package/dist/state.js.map +1 -1
  106. package/dist/theme.d.ts +2 -2
  107. package/dist/theme.js +1 -1
  108. package/dist/themes/catppuccin-mocha.d.ts +1 -1
  109. package/dist/themes/catppuccin-mocha.js +1 -1
  110. package/dist/themes/dracula.d.ts +1 -1
  111. package/dist/themes/dracula.js +1 -1
  112. package/dist/themes/github-light.d.ts +1 -1
  113. package/dist/themes/github-light.js +1 -1
  114. package/dist/themes/index.d.ts +7 -7
  115. package/dist/themes/index.js +6 -6
  116. package/dist/themes/nord.d.ts +1 -1
  117. package/dist/themes/nord.js +1 -1
  118. package/dist/themes/one-dark-pro.d.ts +1 -1
  119. package/dist/themes/one-dark-pro.js +1 -1
  120. package/dist/themes/tokyo-night.d.ts +1 -1
  121. package/dist/themes/tokyo-night.js +1 -1
  122. package/dist/utils.d.ts +6 -1
  123. package/dist/utils.d.ts.map +1 -1
  124. package/dist/utils.js +68 -3
  125. package/dist/utils.js.map +1 -1
  126. package/llms.txt +4 -2
  127. package/package.json +8 -4
@@ -0,0 +1,175 @@
1
+ import { Button, Card, Input, Link, Progress } from "../components/index.js";
2
+ import { demo } from "./demo-layout.js";
3
+ demo("AUTHENTICATION", (ctx, bounds) => {
4
+ const { color, icon, useState, lastKey, focus, isFocused, requestStop } = ctx;
5
+ const [email] = useState("input-email", "");
6
+ const [password] = useState("input-password", "");
7
+ const [status, setStatus] = useState("login-status", "idle");
8
+ const [error, setError] = useState("login-error", "");
9
+ const [progress, setProgress] = useState("login-progress", 0);
10
+ const [forgotNotice, setForgotNotice] = useState("forgot-notice", "");
11
+ const subtleTitle = { r: 140, g: 140, b: 140 };
12
+ const mutedText = { r: 110, g: 110, b: 110 };
13
+ const linkColor = { r: 37, g: 99, b: 235 };
14
+ const handleLogin = () => {
15
+ if (!email.includes("@")) {
16
+ setError("Please enter a valid email address.");
17
+ return;
18
+ }
19
+ if (password.length < 4) {
20
+ setError("Password must be at least 4 characters.");
21
+ return;
22
+ }
23
+ setError("");
24
+ setForgotNotice("");
25
+ setStatus("loading");
26
+ setProgress(0);
27
+ };
28
+ if (lastKey === "q")
29
+ requestStop();
30
+ if (status === "loading") {
31
+ if (progress < 1) {
32
+ const step = ctx.flicker(0.1) ? 0.05 : 0.01;
33
+ setProgress(Math.min(1, progress + step));
34
+ } else {
35
+ setStatus("success");
36
+ }
37
+ }
38
+ if (status === "success") {
39
+ const W = Math.min(bounds.w - 8, 60);
40
+ const card = bounds.place({
41
+ y: 4,
42
+ width: W,
43
+ height: 12
44
+ });
45
+ Card(ctx, {
46
+ x: card.x,
47
+ y: card.y,
48
+ width: card.width,
49
+ height: card.height,
50
+ title: "Welcome back",
51
+ variant: "accent",
52
+ bgColor: { r: 250, g: 249, b: 246 }
53
+ }, (sub) => {
54
+ sub.text(`
55
+ ${icon("check")} ${color.bold("You're signed in")}
56
+
57
+ `);
58
+ sub.text(` Hi ${color.bold(email.split("@")[0])}, your session is now active.
59
+
60
+ `);
61
+ Button(sub, {
62
+ id: "btn-logout",
63
+ label: "Sign out",
64
+ icon: icon("exit"),
65
+ variant: "danger",
66
+ onClick: () => {
67
+ setStatus("idle");
68
+ setProgress(0);
69
+ setError("");
70
+ }
71
+ });
72
+ });
73
+ return;
74
+ }
75
+ const W = Math.min(bounds.w - 8, 50);
76
+ const form = bounds.place({
77
+ width: W,
78
+ height: 26
79
+ });
80
+ Card(ctx, {
81
+ id: "auth-card",
82
+ x: form.x,
83
+ y: form.y,
84
+ width: form.width,
85
+ height: form.height,
86
+ border: "rounded",
87
+ variant: error ? "danger" : "accent",
88
+ bgColor: { r: 250, g: 249, b: 246 }
89
+ }, (sub) => {
90
+ const brandVisible = 2 + 1 + 5;
91
+ const logo = sub.resolveLocalRect({ width: brandVisible, height: 1 });
92
+ const logoPad = " ".repeat(logo.x);
93
+ const purple = { r: 147, g: 51, b: 234 };
94
+ const brand = color.fg(purple, color.bold(`${icon("bunti")} Bunti`));
95
+ sub.text(`${logoPad + brand}
96
+
97
+ `);
98
+ sub.text(color.fg(mutedText, `Sign in to your Bunti account to continue.
99
+ `));
100
+ sub.text(color.fg(mutedText, `Use ${color.bold("Tab")} or your mouse to move between fields.
101
+
102
+ `));
103
+ sub.text(`${color.fg(subtleTitle, color.bold("Email address"))}
104
+ `);
105
+ Input(sub, {
106
+ id: "input-email",
107
+ placeholder: "you@bunti.net",
108
+ width: "100%"
109
+ });
110
+ sub.text(`
111
+ `);
112
+ sub.text(`${color.fg(subtleTitle, color.bold("Password"))}
113
+ `);
114
+ Input(sub, {
115
+ id: "input-password",
116
+ placeholder: "Enter your password",
117
+ type: "password",
118
+ width: "100%"
119
+ });
120
+ if (isFocused("input-email") && lastKey === "enter")
121
+ focus("input-password");
122
+ if (isFocused("input-password") && lastKey === "enter")
123
+ handleLogin();
124
+ sub.text(`
125
+
126
+ `);
127
+ if (status === "loading") {
128
+ const barW = W - 26;
129
+ sub.text(` ${icon("loading")} ${color.dim("Signing in ")} `);
130
+ Progress(sub, {
131
+ value: progress,
132
+ width: barW,
133
+ fillColor: { r: 37, g: 99, b: 235 },
134
+ trackColor: { r: 210, g: 208, b: 200 },
135
+ showPercent: true
136
+ });
137
+ } else {
138
+ Button(sub, {
139
+ id: "btn-login",
140
+ label: "Sign in",
141
+ icon: icon("lock"),
142
+ variant: "primary",
143
+ width: "100%",
144
+ onClick: handleLogin
145
+ });
146
+ }
147
+ sub.text(`
148
+ `);
149
+ const linkText = "Forgot password?";
150
+ const link = sub.resolveLocalRect({
151
+ y: sub.cursorY,
152
+ width: linkText.length,
153
+ height: 1
154
+ });
155
+ Link(sub, {
156
+ id: "forgot-password",
157
+ label: linkText,
158
+ color: linkColor,
159
+ x: link.x,
160
+ y: link.y,
161
+ onClick: () => {
162
+ setForgotNotice("Password reset link sent to your email.");
163
+ }
164
+ });
165
+ if (error) {
166
+ sub.text(`
167
+
168
+ ${color.fg("error", `${icon("warning")} ${error}`)}`);
169
+ } else if (forgotNotice) {
170
+ sub.text(`
171
+
172
+ ${color.fg(mutedText, `${icon("check")} ${forgotNotice}`)}`);
173
+ }
174
+ });
175
+ });
@@ -1,5 +1,5 @@
1
1
  import { Button, Card, Input, Link, Progress } from '../components/index.js';
2
- import { demo } from './demo-layout';
2
+ import { demo } from './demo-layout.js';
3
3
 
4
4
  /**
5
5
  * Sign-In Demo — web-app style auth form rendered in the terminal.
@@ -0,0 +1,427 @@
1
+ import { Progress, Spinner } from "../components/index.js";
2
+ import {
3
+ darkTheme,
4
+ easeOutCubic,
5
+ lightTheme,
6
+ render,
7
+ truncate,
8
+ visibleWidth
9
+ } from "../index.js";
10
+ import { themes } from "../themes/index.js";
11
+ const once = process.argv.includes("--once");
12
+ const sizeArg = process.argv[process.argv.indexOf("--size") + 1];
13
+ if (process.argv.includes("--size") && sizeArg?.includes("x")) {
14
+ const [c, r] = sizeArg.split("x").map((n) => Number.parseInt(n, 10));
15
+ if (c && r) {
16
+ Object.defineProperty(process.stdout, "columns", {
17
+ value: c,
18
+ configurable: true
19
+ });
20
+ Object.defineProperty(process.stdout, "rows", {
21
+ value: r,
22
+ configurable: true
23
+ });
24
+ }
25
+ }
26
+ const THEMES = [
27
+ darkTheme,
28
+ lightTheme,
29
+ themes.dracula,
30
+ themes["tokyo-night"],
31
+ themes["catppuccin-mocha"],
32
+ themes.nord,
33
+ themes["one-dark-pro"],
34
+ themes["github-light"]
35
+ ];
36
+ const TASKS = [
37
+ "refactoring theme engine",
38
+ "wiring OAuth callbacks",
39
+ "chasing a flaky e2e suite",
40
+ "tuning the diff renderer",
41
+ "porting the icons pipeline",
42
+ "writing migration codemods",
43
+ "profiling the render loop",
44
+ "reviewing PR #214",
45
+ "drafting release notes",
46
+ "untangling circular imports"
47
+ ];
48
+ const agents = [
49
+ ["nova", "bunti", "feat/motion-engine", "coding"],
50
+ ["luna", "lit-factory", "feat/agent-inbox", "coding"],
51
+ ["sona", "pullflow-web", "fix/review-sync", "review"],
52
+ ["atlas", "space-station", "chore/planet-gc", "coding"],
53
+ ["vega", "bunti-docs", "docs/quickstart", "idle"],
54
+ ["orion", "gstack", "fix/browse-daemon", "blocked"],
55
+ ["mira", "lit-factory", "feat/task-router", "coding"]
56
+ ].map(([name, repo, branch, status], i) => ({
57
+ name,
58
+ repo,
59
+ branch,
60
+ task: TASKS[i % TASKS.length],
61
+ status,
62
+ progress: 0.15 + i * 0.11,
63
+ rate: 90 + i * 25,
64
+ history: [],
65
+ tokens: 180000 + i * 90000,
66
+ files: 3 + i % 6,
67
+ startedAt: Date.now() - (i + 2) * 7 * 60000
68
+ }));
69
+ const events = [];
70
+ const fleetHistory = [];
71
+ const HISTORY_CAP = 48;
72
+ const EVENT_CAP = 48;
73
+ let taskCursor = agents.length;
74
+ const clock = (d = new Date) => d.toTimeString().slice(0, 8);
75
+ function pushEvent(kind, agent, msg) {
76
+ events.push({ ts: clock(), kind, agent, msg, born: Date.now() });
77
+ if (events.length > EVENT_CAP)
78
+ events.shift();
79
+ }
80
+ const CHATTER = [
81
+ ["commit", (a) => `pushed 2 commits to ${a.branch}`],
82
+ ["tests", (a) => `${a.repo} suite green in 41s`],
83
+ ["plan", (a) => `split "${a.task}" into 3 steps`],
84
+ ["commit", (a) => `rebased ${a.branch} on main`],
85
+ ["merge", (a) => `merged PR #${180 + (a.tokens / 1000 | 0) % 90}`],
86
+ ["tests", (a) => `added 6 cases for ${a.repo}`]
87
+ ];
88
+ function chatter() {
89
+ const a = agents[Math.floor(Math.random() * agents.length)];
90
+ if (a.status === "blocked") {
91
+ pushEvent("stall", a.name, `waiting on CI for ${a.branch}`);
92
+ } else {
93
+ const [kind, msg] = CHATTER[Math.floor(Math.random() * CHATTER.length)];
94
+ pushEvent(kind, a.name, msg(a));
95
+ }
96
+ }
97
+ function tick() {
98
+ let fleetRate = 0;
99
+ for (const a of agents) {
100
+ const active = a.status === "coding" || a.status === "review";
101
+ if (active) {
102
+ a.progress += 0.02 + Math.random() * 0.05;
103
+ a.rate = Math.min(320, Math.max(45, a.rate + (Math.random() - 0.48) * 46));
104
+ a.tokens += a.rate / 60 * 0.9 * 1000;
105
+ } else {
106
+ a.rate = Math.max(12, a.rate * 0.86);
107
+ }
108
+ fleetRate += a.rate;
109
+ a.history.push(a.rate);
110
+ if (a.history.length > HISTORY_CAP)
111
+ a.history.shift();
112
+ if (a.progress >= 1) {
113
+ pushEvent("ship", a.name, `shipped "${a.task}"`);
114
+ a.progress = 0;
115
+ a.task = TASKS[taskCursor++ % TASKS.length];
116
+ a.files = 2 + Math.floor(Math.random() * 9);
117
+ a.startedAt = Date.now();
118
+ if (a.status === "review")
119
+ a.status = "coding";
120
+ }
121
+ }
122
+ fleetHistory.push(fleetRate);
123
+ if (fleetHistory.length > HISTORY_CAP)
124
+ fleetHistory.shift();
125
+ if (Math.random() < 0.85)
126
+ chatter();
127
+ if (Math.random() < 0.35)
128
+ chatter();
129
+ return Date.now();
130
+ }
131
+ for (let i = 0;i < 24; i++) {
132
+ let fleetRate = 0;
133
+ for (const a of agents) {
134
+ const sample = a.rate + Math.sin(i * 0.7 + a.rate) * 30;
135
+ a.history.push(sample);
136
+ fleetRate += sample;
137
+ }
138
+ fleetHistory.push(fleetRate);
139
+ }
140
+ pushEvent("plan", "nova", "fleet online — 7 agents reporting");
141
+ pushEvent("commit", "luna", "pushed 2 commits to feat/agent-inbox");
142
+ pushEvent("tests", "atlas", "space-station suite green in 38s");
143
+ pushEvent("merge", "sona", "merged PR #209 into pullflow-web");
144
+ pushEvent("commit", "mira", "rebased feat/task-router on main");
145
+ const SPARK = "▁▂▃▄▅▆▇█";
146
+ function sparkline(series, width) {
147
+ const n = Math.min(width, series.length);
148
+ if (n === 0)
149
+ return "";
150
+ let min = Number.POSITIVE_INFINITY;
151
+ let max = Number.NEGATIVE_INFINITY;
152
+ for (let i = series.length - n;i < series.length; i++) {
153
+ const v = series[i];
154
+ if (v < min)
155
+ min = v;
156
+ if (v > max)
157
+ max = v;
158
+ }
159
+ const span = max - min || 1;
160
+ let out = "";
161
+ for (let i = series.length - n;i < series.length; i++) {
162
+ out += SPARK[Math.round((series[i] - min) / span * 7)];
163
+ }
164
+ return out;
165
+ }
166
+ const fmtTokens = (n) => n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : `${Math.round(n / 1000)}k`;
167
+ function fmtUptime(since) {
168
+ const s = Math.max(0, Math.floor((Date.now() - since) / 1000));
169
+ const h = Math.floor(s / 3600);
170
+ const m = Math.floor(s % 3600 / 60);
171
+ return h > 0 ? `${h}h ${String(m).padStart(2, "0")}m` : `${m}m ${s % 60}s`;
172
+ }
173
+ const STATUS_META = {
174
+ coding: { icon: "robot", token: "info", label: "coding" },
175
+ review: { icon: "pr", token: "accent", label: "review" },
176
+ blocked: { icon: "warning", token: "warning", label: "blocked" },
177
+ idle: { icon: "pause", token: "muted", label: "idle" }
178
+ };
179
+ const EVENT_META = {
180
+ commit: { icon: "commit", token: "info" },
181
+ tests: { icon: "check", token: "success" },
182
+ merge: { icon: "merge", token: "accent" },
183
+ ship: { icon: "rocket", token: "primary" },
184
+ stall: { icon: "warning", token: "warning" },
185
+ plan: { icon: "list", token: "muted" }
186
+ };
187
+ render((ctx) => {
188
+ const t = ctx.theme;
189
+ const { width: W, height: H, color, icon } = ctx;
190
+ ctx.wallpaper(t.background);
191
+ if (ctx.lastKey === "q" || ctx.lastKey === "escape")
192
+ ctx.requestStop();
193
+ if (ctx.lastKey === "r")
194
+ ctx.restartAnimation("entrance");
195
+ const pick = Number.parseInt(ctx.lastKey ?? "", 10);
196
+ if (pick >= 1 && pick <= THEMES.length)
197
+ ctx.setTheme(THEMES[pick - 1]);
198
+ if (W < 72 || H < 20) {
199
+ const msg = "mission control needs at least 72x20";
200
+ ctx.blit(Math.max(0, W - msg.length >> 1), H >> 1, t.muted(msg));
201
+ return;
202
+ }
203
+ ctx.useAsync("fleet-tick", async () => tick(), { interval: 900 });
204
+ const enter = (i) => once ? 1 : ctx.stagger(i + 1, {
205
+ id: "entrance",
206
+ delay: 150,
207
+ duration: 240,
208
+ easing: easeOutCubic
209
+ });
210
+ const mix = (token, p) => p >= 1 ? token.rgb : ctx.fade(t.background.rgb, token.rgb, p);
211
+ const ink = (token, p) => (s) => color.fg(mix(token, p), s);
212
+ const margin = W >= 100 ? 2 : 1;
213
+ const wide = W >= 100;
214
+ const headerH = 3;
215
+ const bodyY = headerH + 1;
216
+ const bodyH = H - bodyY - 2;
217
+ const leftW = wide ? 34 : 27;
218
+ const body = { x: margin, y: bodyY, width: W - margin * 2, height: bodyH };
219
+ const fleet = { x: body.x, y: body.y, width: leftW, height: body.height };
220
+ const right = {
221
+ x: body.x + leftW + 2,
222
+ y: body.y,
223
+ width: body.width - leftW - 2,
224
+ height: body.height
225
+ };
226
+ const detailH = H >= 32 ? 12 : 9;
227
+ const logY = right.y + detailH + 1;
228
+ const logH = right.height - detailH - 1;
229
+ const [selected] = ctx.useState("agents_index", 0);
230
+ const agent = agents[Math.min(selected, agents.length - 1)];
231
+ const [prevSel, setPrevSel] = ctx.useState("detail_prev", selected);
232
+ if (prevSel !== selected) {
233
+ setPrevSel(selected);
234
+ ctx.restartAnimation("detail-fade");
235
+ }
236
+ const swap = ctx.animate(220, { id: "detail-fade", easing: easeOutCubic });
237
+ const pHeader = enter(0);
238
+ ctx.box({
239
+ x: 0,
240
+ y: 0,
241
+ width: W,
242
+ height: headerH,
243
+ border: "none",
244
+ bgColor: mix(t.surfaceRaised, pHeader),
245
+ padding: [0, margin + 1],
246
+ valign: "middle"
247
+ }, (b) => {
248
+ if (pHeader < 0.5)
249
+ return;
250
+ const left = `${icon("bunti")} `;
251
+ const leftTitle = "AGENT FLEET";
252
+ const leftSub = " mission control";
253
+ const rightPlain = `. fleet live ${clock()} · ${t.name}`;
254
+ const pad = Math.max(1, W - (margin + 1) * 2 - visibleWidth(left + leftTitle + leftSub) - visibleWidth(rightPlain));
255
+ b.text(ink(t.primary, pHeader)(left));
256
+ b.text(color.bold(ink(t.foreground, pHeader)(leftTitle)));
257
+ b.text(ink(t.muted, pHeader)(leftSub));
258
+ b.text(" ".repeat(pad));
259
+ Spinner(b, { label: "", color: mix(t.accent, pHeader) });
260
+ b.text(ink(t.success, pHeader)(" fleet live"));
261
+ b.text(ink(t.muted, pHeader)(` ${clock()} · `));
262
+ b.text(ink(t.accent, pHeader)(t.name));
263
+ });
264
+ const pFleet = enter(1);
265
+ const fleetY = fleet.y + Math.round((1 - pFleet) * 2);
266
+ ctx.box({
267
+ x: fleet.x,
268
+ y: fleetY,
269
+ width: fleet.width,
270
+ height: fleet.height,
271
+ border: "rounded",
272
+ borderColor: mix(t.border, pFleet),
273
+ title: " Agents ",
274
+ titleStyle: ink(t.muted, pFleet),
275
+ bgColor: mix(t.surface, pFleet),
276
+ padding: [1, 2]
277
+ }, (b) => {
278
+ if (pFleet < 0.6)
279
+ return;
280
+ const inner = fleet.width - 6;
281
+ const repoW = Math.max(6, inner - 16);
282
+ const rows = agents.map((a) => {
283
+ const meta = STATUS_META[a.status];
284
+ const working = a.status === "coding" || a.status === "review";
285
+ const pct = working ? `${String(Math.round(a.progress * 100)).padStart(3)}%` : " · ";
286
+ const name = working ? t.foreground : t.muted;
287
+ return `${t[meta.token](icon(meta.icon))} ` + `${name(a.name.padEnd(7))}` + `${t.muted(truncate(a.repo, repoW).padEnd(repoW))} ` + `${t.muted(pct)}`;
288
+ });
289
+ b.list("agents", rows, { width: "100%" });
290
+ const innerH = fleet.height - 4;
291
+ const pad = innerH - agents.length - 3;
292
+ if (pad >= 1) {
293
+ const active = agents.filter((a) => a.status !== "idle").length;
294
+ const counts = `${active}/${agents.length} active`;
295
+ const label = wide ? "FLEET THROUGHPUT" : "FLEET";
296
+ b.text(`
297
+ `.repeat(pad + 1));
298
+ b.text(t.muted(label));
299
+ b.text(" ".repeat(Math.max(1, inner - label.length - counts.length)));
300
+ b.text(t.muted(counts));
301
+ b.text(`
302
+ `);
303
+ b.text(t.info(sparkline(fleetHistory, inner)));
304
+ b.text(`
305
+ `);
306
+ const sum = fleetHistory[fleetHistory.length - 1] ?? 0;
307
+ b.text(t.muted("Σ "));
308
+ b.text(t.foreground(`${Math.round(sum)}`));
309
+ b.text(t.muted(" tok/min"));
310
+ }
311
+ });
312
+ const pDetail = enter(2);
313
+ const detailY = right.y + Math.round((1 - pDetail) * 2);
314
+ ctx.box({
315
+ x: right.x,
316
+ y: detailY,
317
+ width: right.width,
318
+ height: detailH,
319
+ border: "rounded",
320
+ borderColor: mix(t.border, pDetail),
321
+ title: " Mission Detail ",
322
+ titleStyle: ink(t.muted, pDetail),
323
+ bgColor: mix(t.surface, pDetail),
324
+ padding: [1, 2]
325
+ }, (b) => {
326
+ if (pDetail < 0.6)
327
+ return;
328
+ const dw = right.width - 6;
329
+ const rows = detailH - 4;
330
+ const meta = STATUS_META[agent.status];
331
+ const statusInk = ink(t[meta.token], swap);
332
+ const fgInk = ink(t.foreground, swap);
333
+ const mutedInk = ink(t.muted, swap);
334
+ const barW = Math.max(10, dw - 16);
335
+ b.text(color.bold(ink(t.primary, swap)(agent.name)));
336
+ b.text(mutedInk(" · "));
337
+ b.text(statusInk(`${icon(meta.icon)} ${meta.label}`));
338
+ b.text(`
339
+ `);
340
+ b.text(fgInk(truncate(agent.task, dw)));
341
+ b.text(`
342
+ `);
343
+ b.text(mutedInk(truncate(`${icon("repo")} ${agent.repo} ${icon("branch")} ${agent.branch}`, dw)));
344
+ b.text(`
345
+ `);
346
+ if (rows >= 6)
347
+ b.text(`
348
+ `);
349
+ b.text(mutedInk("progress "));
350
+ Progress(b, {
351
+ value: agent.status === "coding" || agent.status === "review" ? agent.progress : 0,
352
+ width: barW,
353
+ gradient: [t.primary, t.accent],
354
+ trackColor: mix(t.border, Math.min(pDetail, swap)),
355
+ showPercent: true
356
+ });
357
+ b.text(`
358
+ `);
359
+ if (rows >= 8 && wide) {
360
+ b.text(mutedInk("tok/min "));
361
+ b.text(ink(t.accent, swap)(sparkline(agent.history, barW)));
362
+ b.text(color.bold(fgInk(` ${Math.round(agent.rate)}`)));
363
+ b.text(`
364
+ `);
365
+ }
366
+ if (rows >= 7)
367
+ b.text(`
368
+ `);
369
+ const sep = rows >= 7 ? " · " : " · ";
370
+ b.text(mutedInk(`tokens ${fmtTokens(agent.tokens)}${sep}files ${agent.files}${sep}up ${fmtUptime(agent.startedAt)}`));
371
+ });
372
+ const pLog = enter(3);
373
+ const logYAnim = logY + Math.round((1 - pLog) * 2);
374
+ ctx.box({
375
+ x: right.x,
376
+ y: logYAnim,
377
+ width: right.width,
378
+ height: logH,
379
+ border: "rounded",
380
+ borderColor: mix(t.border, pLog),
381
+ title: " Activity ",
382
+ titleStyle: ink(t.muted, pLog),
383
+ bgColor: mix(t.surface, pLog),
384
+ padding: [1, 2]
385
+ }, (b) => {
386
+ if (pLog < 0.6)
387
+ return;
388
+ const lw = right.width - 6;
389
+ const visible = Math.max(1, logH - 4);
390
+ const shown = events.slice(-visible);
391
+ const now = Date.now();
392
+ for (let i = 0;i < shown.length; i++) {
393
+ const e = shown[i];
394
+ const meta = EVENT_META[e.kind];
395
+ const pe = Math.min(1, (now - e.born) / 240);
396
+ const slide = " ".repeat(Math.round((1 - easeOutCubic(pe)) * 6));
397
+ const head = `${e.ts} `;
398
+ const badge = `${icon(meta.icon)} ${e.kind.padEnd(7)}`;
399
+ const who = `${e.agent.padEnd(7)}`;
400
+ const room = lw - slide.length - head.length - 9 - who.length;
401
+ b.text(slide);
402
+ b.text(t.muted(head));
403
+ b.text(t[meta.token](badge));
404
+ b.text(t.foreground(who));
405
+ b.text(t.muted(truncate(e.msg, Math.max(4, room))));
406
+ if (i < shown.length - 1)
407
+ b.text(`
408
+ `);
409
+ }
410
+ });
411
+ const pBar = enter(4);
412
+ if (pBar > 0) {
413
+ const idx = THEMES.findIndex((th) => th.name === t.name) + 1;
414
+ const hint = "1-8 themes · ↑/↓ or click agents · q quit";
415
+ const status = `${idx > 0 ? idx : "·"}/8 ${t.name}`;
416
+ ctx.blit(margin + 1, H - 1, ink(t.muted, pBar)(hint));
417
+ ctx.blit(Math.max(0, W - margin - 1 - visibleWidth(status)), H - 1, ink(t.accent, pBar)(status));
418
+ }
419
+ }, {
420
+ fps: 30,
421
+ keyboard: true,
422
+ mouse: true,
423
+ hideCursor: true,
424
+ alternateBuffer: !once,
425
+ theme: darkTheme,
426
+ once
427
+ });