@zakmandhro/bunti 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/dist/cli.d.ts +2 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +33 -9
- package/dist/cli.js.map +1 -1
- package/dist/colors.d.ts +2 -2
- package/dist/colors.js +2 -2
- package/dist/components/Box.d.ts +1 -1
- package/dist/components/Button.d.ts +1 -1
- package/dist/components/Button.js +2 -2
- package/dist/components/Card.d.ts +1 -1
- package/dist/components/Card.js +3 -3
- package/dist/components/Header.d.ts +1 -1
- package/dist/components/Header.js +2 -2
- package/dist/components/Input.d.ts +2 -2
- package/dist/components/Input.js +1 -1
- package/dist/components/Link.d.ts +2 -2
- package/dist/components/Link.js +2 -2
- package/dist/components/Modal.d.ts +1 -1
- package/dist/components/Modal.js +2 -2
- package/dist/components/Progress.d.ts +2 -2
- package/dist/components/Progress.js +2 -2
- package/dist/components/Spinner.d.ts +2 -2
- package/dist/components/index.d.ts +9 -9
- package/dist/components/index.js +9 -9
- package/dist/data/nf-glyphs.d.ts +2 -2
- package/dist/demo-registry.d.ts.map +1 -1
- package/dist/demo-registry.js +6 -1
- package/dist/demo-registry.js.map +1 -1
- package/dist/demos/2048.js +559 -0
- package/dist/demos/2048.ts +1 -1
- package/dist/demos/animation.js +177 -0
- package/dist/demos/animation.ts +1 -1
- package/dist/demos/demo-layout.js +85 -0
- package/dist/demos/demo-layout.ts +6 -1
- package/dist/demos/engine.js +244 -0
- package/dist/demos/interaction.js +74 -0
- package/dist/demos/interaction.ts +1 -1
- package/dist/demos/login.js +175 -0
- package/dist/demos/login.ts +1 -1
- package/dist/demos/mission-control.js +427 -0
- package/dist/demos/platformer-physics.js +122 -0
- package/dist/demos/platformer-physics.ts +284 -0
- package/dist/demos/platformer.js +519 -0
- package/dist/demos/platformer.ts +761 -0
- package/dist/demos/showcase.js +181 -0
- package/dist/demos/showcase.ts +1 -1
- package/dist/detect.d.ts +24 -7
- package/dist/detect.d.ts.map +1 -1
- package/dist/detect.js +73 -12
- package/dist/detect.js.map +1 -1
- package/dist/diagnostics.d.ts +8 -2
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +11 -0
- package/dist/diagnostics.js.map +1 -1
- package/dist/dsl/context.d.ts +2 -2
- package/dist/dsl/context.d.ts.map +1 -1
- package/dist/dsl/context.js +14 -14
- package/dist/dsl/context.js.map +1 -1
- package/dist/dsl/hooks.d.ts +2 -2
- package/dist/dsl/hooks.js +1 -1
- package/dist/dsl/interaction.d.ts +3 -3
- package/dist/dsl/layers.d.ts +2 -2
- package/dist/dsl/motion.d.ts +2 -2
- package/dist/dsl/motion.js +2 -2
- package/dist/dsl/render.d.ts +2 -2
- package/dist/dsl/render.d.ts.map +1 -1
- package/dist/dsl/render.js +4 -8
- package/dist/dsl/render.js.map +1 -1
- package/dist/dsl/types.d.ts +11 -10
- package/dist/dsl/types.d.ts.map +1 -1
- package/dist/dsl/types.js.map +1 -1
- package/dist/dsl.d.ts +4 -4
- package/dist/dsl.js +3 -3
- package/dist/easing.d.ts +1 -1
- package/dist/geometry.d.ts +1 -1
- package/dist/geometry.js +1 -1
- package/dist/icons-full.d.ts +2 -2
- package/dist/icons-full.js +3 -3
- package/dist/icons.d.ts +4 -4
- package/dist/icons.js +3 -3
- package/dist/index.d.ts +45 -36
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -24
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts +18 -6
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +15 -6
- package/dist/layout.js.map +1 -1
- package/dist/pixels.d.ts +51 -0
- package/dist/pixels.d.ts.map +1 -0
- package/dist/pixels.js +82 -0
- package/dist/pixels.js.map +1 -0
- package/dist/render.d.ts +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +51 -11
- package/dist/render.js.map +1 -1
- package/dist/state.d.ts +4 -4
- package/dist/state.js +1 -1
- package/dist/theme.d.ts +2 -2
- package/dist/theme.js +1 -1
- package/dist/themes/catppuccin-mocha.d.ts +1 -1
- package/dist/themes/catppuccin-mocha.js +1 -1
- package/dist/themes/dracula.d.ts +1 -1
- package/dist/themes/dracula.js +1 -1
- package/dist/themes/github-light.d.ts +1 -1
- package/dist/themes/github-light.js +1 -1
- package/dist/themes/index.d.ts +7 -7
- package/dist/themes/index.js +6 -6
- package/dist/themes/nord.d.ts +1 -1
- package/dist/themes/nord.js +1 -1
- package/dist/themes/one-dark-pro.d.ts +1 -1
- package/dist/themes/one-dark-pro.js +1 -1
- package/dist/themes/tokyo-night.d.ts +1 -1
- package/dist/themes/tokyo-night.js +1 -1
- package/dist/utils.d.ts +6 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +68 -3
- package/dist/utils.js.map +1 -1
- package/llms.txt +4 -2
- 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
|
+
});
|
package/dist/demos/login.ts
CHANGED
|
@@ -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
|
+
});
|