pudu-ai 0.2.18 → 0.2.21

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.
@@ -0,0 +1,812 @@
1
+ import {
2
+ GRADE_MEANING,
3
+ WORK_KINDS,
4
+ canInstallGrade,
5
+ decideLaunch,
6
+ executeDockerOllama,
7
+ executeLaunch,
8
+ formatBytes,
9
+ idsLikelyMatch,
10
+ installWithBrew,
11
+ localCompatibility,
12
+ memoryLabel,
13
+ planTasks,
14
+ pullOllamaModel,
15
+ resolveOllamaTag,
16
+ resultText,
17
+ runBenchmark,
18
+ tasksText
19
+ } from "./chunk-6C5EPKRI.js";
20
+ import {
21
+ formatNumber,
22
+ formatPercent,
23
+ formatTokensPerSec,
24
+ t
25
+ } from "./chunk-QGEXKMQ5.js";
26
+ import "./chunk-SS7OXXQQ.js";
27
+
28
+ // src/tui/render.tsx
29
+ import { render } from "ink";
30
+
31
+ // src/tui/App.tsx
32
+ import { useApp as useApp2, useInput as useInput4 } from "ink";
33
+ import { useMemo as useMemo2, useRef as useRef2, useState as useState4 } from "react";
34
+
35
+ // src/tui/views/Dashboard.tsx
36
+ import { Box as Box2, Text as Text2 } from "ink";
37
+
38
+ // src/tui/theme.ts
39
+ function gradeColor(grade) {
40
+ if (grade === "S" || grade === "A") return "green";
41
+ if (grade === "B") return "yellow";
42
+ if (grade === "C") return "magenta";
43
+ if (grade === "D" || grade === "F") return "red";
44
+ return "gray";
45
+ }
46
+ function bar(percent, width = 10) {
47
+ if (percent === void 0 || Number.isNaN(percent)) return `${"\u2591".repeat(width)} N/A`;
48
+ const filled = Math.max(0, Math.min(width, Math.round(percent / 100 * width)));
49
+ return `${"\u2588".repeat(filled)}${"\u2591".repeat(width - filled)}`;
50
+ }
51
+ function sparkline(values, width = 24) {
52
+ const glyphs = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
53
+ if (!values.length) return glyphs[0].repeat(width);
54
+ const slice = values.slice(-width);
55
+ const max = Math.max(...slice, 1);
56
+ return slice.map((v) => glyphs[Math.min(glyphs.length - 1, Math.floor(v / max * (glyphs.length - 1)))]).join("").padStart(width, glyphs[0]);
57
+ }
58
+
59
+ // src/tui/layout.tsx
60
+ import { Box, Text } from "ink";
61
+
62
+ // src/tui/width.ts
63
+ import { useStdout } from "ink";
64
+
65
+ // src/tui/fit.ts
66
+ function fit(text, width) {
67
+ if (text.length <= width) return text;
68
+ if (width <= 1) return "\u2026";
69
+ return `${text.slice(0, width - 1)}\u2026`;
70
+ }
71
+
72
+ // src/tui/width.ts
73
+ function useCols() {
74
+ const { stdout } = useStdout();
75
+ const cols = stdout?.columns ?? 80;
76
+ return Math.max(40, Math.min(cols - 2, 100));
77
+ }
78
+
79
+ // src/tui/layout.tsx
80
+ import { jsx, jsxs } from "react/jsx-runtime";
81
+ var NAV = [
82
+ { key: "S", label: "Setup", color: "yellow" },
83
+ { key: "B", label: "Bench", color: "green" },
84
+ { key: "M", label: "Models", color: "cyan" },
85
+ { key: "R", label: "Rec", color: "magenta" },
86
+ { key: "T", label: "Tasks", color: "blue" },
87
+ { key: "H", label: "HW", color: "white" },
88
+ { key: "C", label: "Cmp", color: "green" },
89
+ { key: "L", label: "Log", color: "cyan" },
90
+ { key: "Q", label: "Quit", color: "red" }
91
+ ];
92
+ function Frame({ children }) {
93
+ const cols = useCols();
94
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", width: cols, paddingX: 1, paddingY: 0, children });
95
+ }
96
+ function Header() {
97
+ const cols = useCols();
98
+ const compact = cols < 72;
99
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginBottom: 1, width: cols, children: /* @__PURE__ */ jsxs(
100
+ Box,
101
+ {
102
+ borderStyle: "round",
103
+ borderColor: "cyan",
104
+ paddingX: 1,
105
+ flexDirection: "column",
106
+ children: [
107
+ /* @__PURE__ */ jsxs(Box, { children: [
108
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: t("appTitle") }),
109
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
110
+ " ",
111
+ t("appSubtitle")
112
+ ] })
113
+ ] }),
114
+ /* @__PURE__ */ jsx(Box, { flexDirection: "row", flexWrap: "wrap", children: NAV.map((item) => /* @__PURE__ */ jsxs(Box, { marginRight: compact ? 1 : 2, children: [
115
+ /* @__PURE__ */ jsxs(Text, { color: item.color, bold: true, children: [
116
+ "[",
117
+ item.key,
118
+ "]"
119
+ ] }),
120
+ compact ? null : /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
121
+ " ",
122
+ item.label
123
+ ] })
124
+ ] }, item.key)) })
125
+ ]
126
+ }
127
+ ) });
128
+ }
129
+ function Panel(props) {
130
+ return /* @__PURE__ */ jsxs(
131
+ Box,
132
+ {
133
+ flexDirection: "column",
134
+ borderStyle: "round",
135
+ borderColor: props.color ?? "cyan",
136
+ paddingX: 1,
137
+ marginBottom: 1,
138
+ children: [
139
+ /* @__PURE__ */ jsx(Text, { bold: true, color: props.color ?? "cyan", children: props.title }),
140
+ props.children
141
+ ]
142
+ }
143
+ );
144
+ }
145
+ function Kv(props) {
146
+ return /* @__PURE__ */ jsxs(Box, { children: [
147
+ /* @__PURE__ */ jsx(Box, { width: 16, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: props.label }) }),
148
+ /* @__PURE__ */ jsx(Text, { color: props.color, children: props.value })
149
+ ] });
150
+ }
151
+
152
+ // src/tui/views/Dashboard.tsx
153
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
154
+ function Dashboard({ session }) {
155
+ const cols = useCols();
156
+ const h = session.hardware;
157
+ const ollama = session.runtimes.find((r) => r.id === "ollama");
158
+ const lm = session.runtimes.find((r) => r.id === "lmstudio");
159
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
160
+ /* @__PURE__ */ jsxs2(Panel, { title: t("machine"), children: [
161
+ /* @__PURE__ */ jsx2(Text2, { children: fit(
162
+ `${h.machineModel ?? t("unknownMachine")} \xB7 ${h.cpu.name ?? t("cpuNa")} \xB7 ${formatBytes(h.memory.totalBytes, 0)} ${memoryLabel(h)}`,
163
+ cols - 4
164
+ ) }),
165
+ /* @__PURE__ */ jsx2(Box2, { flexDirection: "row", flexWrap: "wrap", marginTop: 0, children: session.runtimes.map((r) => /* @__PURE__ */ jsx2(Box2, { marginRight: 2, children: /* @__PURE__ */ jsxs2(Text2, { color: r.detected ? "green" : "gray", children: [
166
+ r.detected ? "\u2713 " : "\u25CB ",
167
+ r.label
168
+ ] }) }, r.id)) })
169
+ ] }),
170
+ /* @__PURE__ */ jsxs2(Panel, { title: t("installedModels"), color: "green", children: [
171
+ session.rows.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: t("noModels") }) : null,
172
+ session.rows.slice(0, 6).map((row) => /* @__PURE__ */ jsxs2(Box2, { children: [
173
+ /* @__PURE__ */ jsx2(Box2, { width: 22, children: /* @__PURE__ */ jsx2(Text2, { color: "green", children: fit(row.local.name, 20) }) }),
174
+ /* @__PURE__ */ jsx2(Box2, { width: 4, children: /* @__PURE__ */ jsx2(Text2, { color: gradeColor(row.compatibility?.grade), children: row.compatibility?.grade ?? "\u2014" }) }),
175
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: row.lastBenchmark?.benchmark.generationTokensPerSecond ? formatTokensPerSec(row.lastBenchmark.benchmark.generationTokensPerSecond) : t("notTested") })
176
+ ] }, row.local.id))
177
+ ] }),
178
+ /* @__PURE__ */ jsx2(Panel, { title: t("recommended"), color: "magenta", children: session.recommendations.slice(0, 4).map((rec) => /* @__PURE__ */ jsxs2(Box2, { children: [
179
+ /* @__PURE__ */ jsx2(Box2, { width: 12, children: /* @__PURE__ */ jsx2(Text2, { color: "magenta", children: fit(rec.useCase, 10) }) }),
180
+ /* @__PURE__ */ jsx2(Box2, { width: 24, children: /* @__PURE__ */ jsx2(Text2, { children: fit(rec.model.name, 22) }) }),
181
+ /* @__PURE__ */ jsx2(Text2, { color: gradeColor(rec.grade), children: rec.grade })
182
+ ] }, `${rec.useCase}-${rec.model.id}`)) }),
183
+ /* @__PURE__ */ jsx2(Box2, { marginBottom: 1, children: /* @__PURE__ */ jsx2(Text2, { color: ollama?.detected ? "green" : "yellow", children: ollama?.detected ? "\u2713 Ollama" : `\u25CB Ollama ${t("reqOllama")}` }) }),
184
+ /* @__PURE__ */ jsx2(Box2, { marginBottom: 1, children: /* @__PURE__ */ jsx2(Text2, { color: lm?.detected ? "green" : "gray", children: lm?.detected ? "\u2713 LM Studio" : `\u25CB LM Studio ${t("reqLmStudio")}` }) }),
185
+ /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: t("setupCta") })
186
+ ] });
187
+ }
188
+
189
+ // src/tui/views/Models.tsx
190
+ import { Box as Box3, Text as Text3 } from "ink";
191
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
192
+ function ModelsView({ session }) {
193
+ const extras = session.catalog.filter((model) => !session.rows.some((row) => idsLikelyMatch(row.local.id, model.id))).map((model) => ({ model, fit: localCompatibility(session.hardware, model) })).sort((a, b) => a.fit.grade.localeCompare(b.fit.grade)).slice(0, 8);
194
+ return /* @__PURE__ */ jsxs3(Fragment2, { children: [
195
+ /* @__PURE__ */ jsxs3(Panel, { title: t("installedModels"), color: "green", children: [
196
+ /* @__PURE__ */ jsxs3(Box3, { children: [
197
+ /* @__PURE__ */ jsx3(Box3, { width: 22, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "MODEL" }) }),
198
+ /* @__PURE__ */ jsx3(Box3, { width: 6, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "FIT" }) }),
199
+ /* @__PURE__ */ jsx3(Box3, { width: 14, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "EST." }) }),
200
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "MEASURED" })
201
+ ] }),
202
+ session.rows.length === 0 ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: t("noModels") }) : null,
203
+ session.rows.map((row) => /* @__PURE__ */ jsxs3(Box3, { children: [
204
+ /* @__PURE__ */ jsx3(Box3, { width: 22, children: /* @__PURE__ */ jsx3(Text3, { color: "green", children: fit(row.local.name, 20) }) }),
205
+ /* @__PURE__ */ jsx3(Box3, { width: 6, children: /* @__PURE__ */ jsx3(Text3, { color: gradeColor(row.compatibility?.grade), children: row.compatibility?.grade ?? "\u2014" }) }),
206
+ /* @__PURE__ */ jsx3(Box3, { width: 14, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: row.compatibility?.estimatedTokensPerSecond ? `~${formatNumber(row.compatibility.estimatedTokensPerSecond)} t/s` : "\u2014" }) }),
207
+ /* @__PURE__ */ jsx3(Text3, { children: row.lastBenchmark?.benchmark.generationTokensPerSecond ? formatTokensPerSec(row.lastBenchmark.benchmark.generationTokensPerSecond) : t("notTested") })
208
+ ] }, row.local.id))
209
+ ] }),
210
+ /* @__PURE__ */ jsx3(Panel, { title: t("compatible"), color: "yellow", children: extras.map((extra) => /* @__PURE__ */ jsxs3(Box3, { children: [
211
+ /* @__PURE__ */ jsx3(Box3, { width: 22, children: /* @__PURE__ */ jsx3(Text3, { children: fit(extra.model.name, 20) }) }),
212
+ /* @__PURE__ */ jsx3(Box3, { width: 6, children: /* @__PURE__ */ jsx3(Text3, { color: gradeColor(extra.fit.grade), children: extra.fit.grade }) }),
213
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
214
+ extra.fit.estimatedTokensPerSecond ? `~${formatNumber(extra.fit.estimatedTokensPerSecond)} t/s` : "\u2014",
215
+ " ",
216
+ t("estimated")
217
+ ] })
218
+ ] }, extra.model.id)) }),
219
+ /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: t("modelsHint") })
220
+ ] });
221
+ }
222
+
223
+ // src/tui/views/Hardware.tsx
224
+ import { Text as Text4 } from "ink";
225
+ import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
226
+ function HardwareView({ session }) {
227
+ const h = session.hardware;
228
+ const ram = formatBytes(h.memory.totalBytes, 0);
229
+ const avail = h.memory.availableBytes ? formatBytes(h.memory.availableBytes) : "N/A";
230
+ return /* @__PURE__ */ jsxs4(Fragment3, { children: [
231
+ /* @__PURE__ */ jsxs4(Panel, { title: t("machine"), children: [
232
+ /* @__PURE__ */ jsx4(Kv, { label: "Model", value: h.machineModel ?? t("unknownMachine") }),
233
+ /* @__PURE__ */ jsx4(Kv, { label: "CPU", value: h.cpu.name ?? t("cpuNa") }),
234
+ /* @__PURE__ */ jsx4(
235
+ Kv,
236
+ {
237
+ label: "Cores",
238
+ value: `${h.cpu.physicalCores ?? "N/A"} (${h.cpu.performanceCores ?? "?"}P / ${h.cpu.efficiencyCores ?? "?"}E)`
239
+ }
240
+ ),
241
+ /* @__PURE__ */ jsx4(Kv, { label: "GPU", value: h.gpu.name ?? "N/A" }),
242
+ /* @__PURE__ */ jsx4(Kv, { label: memoryLabel(h), value: ram, color: "cyan" }),
243
+ /* @__PURE__ */ jsx4(Kv, { label: "Available", value: avail }),
244
+ /* @__PURE__ */ jsx4(Kv, { label: "OS", value: `${h.os} ${h.osVersion ?? h.arch}` })
245
+ ] }),
246
+ /* @__PURE__ */ jsx4(Panel, { title: t("runtimes"), color: "green", children: session.runtimes.map((r) => /* @__PURE__ */ jsx4(
247
+ Kv,
248
+ {
249
+ label: r.label,
250
+ value: r.detected ? r.version ?? t("detected") : t("notDetected"),
251
+ color: r.detected ? "green" : void 0
252
+ },
253
+ r.id
254
+ )) }),
255
+ /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: t("hardwareHint") })
256
+ ] });
257
+ }
258
+
259
+ // src/tui/views/Recommend.tsx
260
+ import { Box as Box4, Text as Text5 } from "ink";
261
+ import { Fragment as Fragment4, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
262
+ function RecommendView({ session }) {
263
+ return /* @__PURE__ */ jsxs5(Fragment4, { children: [
264
+ /* @__PURE__ */ jsxs5(Panel, { title: t("recommended"), color: "magenta", children: [
265
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: t("recommendedHint") }),
266
+ session.recommendations.map((rec) => /* @__PURE__ */ jsxs5(Box4, { children: [
267
+ /* @__PURE__ */ jsx5(Box4, { width: 12, children: /* @__PURE__ */ jsx5(Text5, { color: "magenta", children: fit(rec.useCase, 10) }) }),
268
+ /* @__PURE__ */ jsx5(Box4, { width: 24, children: /* @__PURE__ */ jsx5(Text5, { children: fit(rec.model.name, 22) }) }),
269
+ /* @__PURE__ */ jsx5(Box4, { width: 4, children: /* @__PURE__ */ jsx5(Text5, { color: gradeColor(rec.grade), children: rec.grade }) }),
270
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
271
+ GRADE_MEANING[rec.grade],
272
+ " ",
273
+ resolveOllamaTag(rec.model.id, rec.model.name) ?? "\u2014"
274
+ ] })
275
+ ] }, `${rec.useCase}-${rec.model.id}`))
276
+ ] }),
277
+ /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: t("setupCta") })
278
+ ] });
279
+ }
280
+
281
+ // src/tui/views/Setup.tsx
282
+ import { Box as Box5, Text as Text6, useApp, useInput } from "ink";
283
+ import { useState } from "react";
284
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
285
+ var TOOLS = [
286
+ { key: "1", id: "opencode", label: "OpenCode" },
287
+ { key: "2", id: "hermes", label: "Hermes" },
288
+ { key: "3", id: "openclaw", label: "OpenClaw" }
289
+ ];
290
+ function SetupView({ session }) {
291
+ const cols = useCols();
292
+ const { exit } = useApp();
293
+ const [cursor, setCursor] = useState(0);
294
+ const [log, setLog] = useState("");
295
+ const [busy, setBusy] = useState(false);
296
+ const recs = session.recommendations;
297
+ const rec = recs[cursor];
298
+ useInput((input, key) => {
299
+ if (busy) return;
300
+ if (key.upArrow) setCursor((c) => Math.max(0, c - 1));
301
+ if (key.downArrow) setCursor((c) => Math.min(Math.max(recs.length - 1, 0), c + 1));
302
+ if ((input.toLowerCase() === "i" || key.return) && rec) {
303
+ if (!canInstallGrade(rec.grade)) {
304
+ setLog(t("installGradeBlock", { grade: rec.grade }));
305
+ return;
306
+ }
307
+ setBusy(true);
308
+ setLog(t("launchPulling", { model: rec.model.name }));
309
+ void pullOllamaModel(rec.model.id, rec.model.name).then((result) => {
310
+ setLog(result.log);
311
+ setBusy(false);
312
+ });
313
+ }
314
+ if (input === "4") {
315
+ setBusy(true);
316
+ setLog(t("dockerStarting"));
317
+ void executeDockerOllama().then((result) => {
318
+ setLog(result.log);
319
+ setBusy(false);
320
+ });
321
+ return;
322
+ }
323
+ if (input === "5") {
324
+ setBusy(true);
325
+ setLog(t("brewInstalling", { pkg: "ollama" }));
326
+ void installWithBrew("ollama").then((result) => {
327
+ setLog(result.log);
328
+ setBusy(false);
329
+ });
330
+ return;
331
+ }
332
+ if (input === "6") {
333
+ setBusy(true);
334
+ setLog(t("brewInstalling", { pkg: "lm-studio" }));
335
+ void installWithBrew("lmstudio").then((result) => {
336
+ setLog(result.log);
337
+ setBusy(false);
338
+ });
339
+ return;
340
+ }
341
+ const tool = TOOLS.find((item) => item.key === input)?.id;
342
+ if (tool) {
343
+ const ollama = session.runtimes.find((r) => r.id === "ollama")?.detected;
344
+ const agent = session.agents.find((a) => a.id === tool);
345
+ if (!ollama) {
346
+ setBusy(true);
347
+ setLog(t("brewInstalling", { pkg: "ollama" }));
348
+ void installWithBrew("ollama").then((brewed) => {
349
+ setLog(brewed.log);
350
+ setBusy(false);
351
+ });
352
+ return;
353
+ }
354
+ if (agent && !agent.detected) {
355
+ setBusy(true);
356
+ setLog(t("linkingTool", { tool, model: rec?.model.id ?? "" }));
357
+ const decision = decideLaunch(session, tool);
358
+ void executeLaunch(
359
+ decision.eligible ? decision : { ...decision, eligible: true, ollamaTag: decision.ollamaTag ?? "qwen3:8b", installed: false }
360
+ ).then((result) => {
361
+ setLog(result.log);
362
+ setBusy(false);
363
+ });
364
+ return;
365
+ }
366
+ if (rec) {
367
+ const decision = decideLaunch(session, tool);
368
+ if (!decision.eligible) {
369
+ setLog(decision.reasons.join(" "));
370
+ return;
371
+ }
372
+ setBusy(true);
373
+ setLog(t("linkingTool", { tool, model: rec.model.id }));
374
+ void executeLaunch(decision).then((result) => {
375
+ setLog(result.log);
376
+ setBusy(false);
377
+ if (result.ok) exit();
378
+ });
379
+ }
380
+ }
381
+ });
382
+ const tag = rec ? resolveOllamaTag(rec.model.id, rec.model.name) : void 0;
383
+ return /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", width: cols, children: [
384
+ /* @__PURE__ */ jsxs6(Panel, { title: t("setupTitle"), color: "yellow", children: [
385
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: t("setupIntro") }),
386
+ recs.map((item, i) => /* @__PURE__ */ jsxs6(Box5, { children: [
387
+ /* @__PURE__ */ jsxs6(Text6, { color: i === cursor ? "cyan" : void 0, children: [
388
+ i === cursor ? "\u276F " : " ",
389
+ fit(item.model.name, 20),
390
+ " "
391
+ ] }),
392
+ /* @__PURE__ */ jsx6(Text6, { color: gradeColor(item.grade), children: item.grade }),
393
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
394
+ " ",
395
+ resolveOllamaTag(item.model.id, item.model.name) ?? "\u2014"
396
+ ] })
397
+ ] }, `${item.useCase}-${item.model.id}`))
398
+ ] }),
399
+ session.agents.map((agent, index) => /* @__PURE__ */ jsxs6(Text6, { color: agent.detected ? "green" : "yellow", children: [
400
+ "[",
401
+ index + 1,
402
+ "] ",
403
+ agent.detected ? "\u2713" : "\u25CB",
404
+ " ",
405
+ agent.label,
406
+ agent.detected ? "" : ` \u2192 ${agent.launch}`
407
+ ] }, agent.id)),
408
+ /* @__PURE__ */ jsxs6(Text6, { color: session.runtimes.find((r) => r.id === "docker")?.detected ? "green" : "yellow", children: [
409
+ "[4] ",
410
+ session.runtimes.find((r) => r.id === "docker")?.detected ? "\u2713" : "\u25CB",
411
+ " Docker",
412
+ session.runtimes.find((r) => r.id === "docker")?.detected ? ` \u2192 ${t("dockerHint")}` : ` \u2192 ${t("reqDocker")}`
413
+ ] }),
414
+ /* @__PURE__ */ jsxs6(Text6, { color: session.runtimes.find((r) => r.id === "ollama")?.detected ? "green" : "yellow", children: [
415
+ "[5] ",
416
+ session.runtimes.find((r) => r.id === "ollama")?.detected ? "\u2713" : "\u25CB",
417
+ " Ollama",
418
+ session.runtimes.find((r) => r.id === "ollama")?.detected ? "" : " \u2192 brew install ollama"
419
+ ] }),
420
+ /* @__PURE__ */ jsxs6(Text6, { color: session.runtimes.find((r) => r.id === "lmstudio")?.detected ? "green" : "yellow", children: [
421
+ "[6] ",
422
+ session.runtimes.find((r) => r.id === "lmstudio")?.detected ? "\u2713" : "\u25CB",
423
+ " LM Studio",
424
+ session.runtimes.find((r) => r.id === "lmstudio")?.detected ? "" : " \u2192 brew install --cask lm-studio"
425
+ ] }),
426
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: fit(`Enter=pull ${tag ?? ""} 1-3 agent 4 docker 5 ollama 6 LM Studio`, cols) }),
427
+ log ? /* @__PURE__ */ jsx6(Text6, { color: "green", children: fit(log, cols) }) : null
428
+ ] });
429
+ }
430
+
431
+ // src/tui/views/Compare.tsx
432
+ import { Box as Box6, Text as Text7 } from "ink";
433
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
434
+ function latest(records) {
435
+ const map = /* @__PURE__ */ new Map();
436
+ for (const record of records) map.set(record.model.id, record);
437
+ return [...map.values()].slice(0, 4);
438
+ }
439
+ function CompareView({ session }) {
440
+ const list = latest(session.history);
441
+ if (list.length < 2) {
442
+ return /* @__PURE__ */ jsxs7(Panel, { title: t("compareTitle"), color: "green", children: [
443
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: t("compareHow") }),
444
+ /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: t("compareNeedBench") })
445
+ ] });
446
+ }
447
+ const row = (label, pick) => /* @__PURE__ */ jsxs7(Box6, { children: [
448
+ /* @__PURE__ */ jsx7(Box6, { width: 18, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: label }) }),
449
+ list.map((r) => /* @__PURE__ */ jsx7(Box6, { width: 14, children: /* @__PURE__ */ jsx7(Text7, { children: fit(pick(r), 13) }) }, r.model.id))
450
+ ] }, label);
451
+ return /* @__PURE__ */ jsxs7(Panel, { title: t("compareTitle"), color: "green", children: [
452
+ /* @__PURE__ */ jsxs7(Box6, { children: [
453
+ /* @__PURE__ */ jsx7(Box6, { width: 18, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " " }) }),
454
+ list.map((r) => /* @__PURE__ */ jsx7(Box6, { width: 14, children: /* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: fit(r.model.id, 13) }) }, r.model.id))
455
+ ] }),
456
+ row("Generation t/s", (r) => formatNumber(r.benchmark.generationTokensPerSecond)),
457
+ row("Prompt t/s", (r) => formatNumber(r.benchmark.promptTokensPerSecond)),
458
+ row("Peak RAM", (r) => r.resources.peakMemoryGb ? `${r.resources.peakMemoryGb} GB` : "N/A"),
459
+ row("Avg CPU", (r) => r.resources.avgCpuPercent ? `${r.resources.avgCpuPercent}%` : "N/A"),
460
+ row("Power", (r) => r.resources.avgPackagePowerWatts ? `${r.resources.avgPackagePowerWatts} W` : "N/A"),
461
+ row("t/s/W", (r) => formatNumber(r.resources.tokensPerSecondPerWatt)),
462
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: t("qualityNote") })
463
+ ] });
464
+ }
465
+
466
+ // src/tui/views/Benchmark.tsx
467
+ import { Box as Box7, Text as Text8, useInput as useInput2 } from "ink";
468
+ import { useEffect, useRef, useState as useState2 } from "react";
469
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
470
+ function BenchmarkView(props) {
471
+ const [phase, setPhase] = useState2(props.models.length ? "select" : "error");
472
+ const [sample, setSample] = useState2();
473
+ const [elapsed, setElapsed] = useState2(0);
474
+ const [result, setResult] = useState2();
475
+ const [error, setError] = useState2(props.models.length ? "" : t("noGguf"));
476
+ const cpuHist = useRef([]);
477
+ const memHist = useRef([]);
478
+ const abortRef = useRef(void 0);
479
+ useInput2((input, key) => {
480
+ if (phase === "select") {
481
+ if (key.upArrow) props.setSelected(Math.max(0, props.selected - 1));
482
+ if (key.downArrow) props.setSelected(Math.min(props.models.length - 1, props.selected + 1));
483
+ if (key.return && props.models[props.selected]) setPhase("running");
484
+ if (input.toLowerCase() === "q" || key.escape) props.onBack();
485
+ } else if (phase === "running" && (input.toLowerCase() === "q" || key.escape)) {
486
+ abortRef.current?.abort();
487
+ } else if ((phase === "done" || phase === "error") && (input.toLowerCase() === "q" || key.escape || key.return)) {
488
+ props.onBack();
489
+ }
490
+ });
491
+ useEffect(() => {
492
+ if (phase !== "running") return;
493
+ const model2 = props.models[props.selected];
494
+ if (!model2) return;
495
+ const controller = new AbortController();
496
+ abortRef.current = controller;
497
+ void (async () => {
498
+ try {
499
+ const catalog = props.session.rows.find((r) => r.local.id === model2.id)?.catalog;
500
+ const out = await runBenchmark({
501
+ model: model2,
502
+ hardware: props.session.hardware,
503
+ preset: props.preset,
504
+ catalog,
505
+ signal: controller.signal,
506
+ onProgress: (p) => {
507
+ setElapsed(p.elapsedSeconds);
508
+ if (p.sample) {
509
+ setSample(p.sample);
510
+ if (p.sample.cpu?.utilizationPercent !== void 0) {
511
+ cpuHist.current = [...cpuHist.current, p.sample.cpu.utilizationPercent].slice(-32);
512
+ }
513
+ memHist.current = [...memHist.current, p.sample.memory.usedBytes].slice(-32);
514
+ }
515
+ }
516
+ });
517
+ setResult(out);
518
+ setPhase("done");
519
+ } catch (err) {
520
+ setError(err instanceof Error ? err.message : String(err));
521
+ setPhase("error");
522
+ }
523
+ })();
524
+ return () => controller.abort();
525
+ }, [phase, props.models, props.preset, props.selected, props.session]);
526
+ const h = props.session.hardware;
527
+ const model = props.models[props.selected];
528
+ if (phase === "select") {
529
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", paddingX: 1, children: [
530
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: "cyan", children: t("selectBenchmark") }),
531
+ props.models.map((m, i) => /* @__PURE__ */ jsxs8(Text8, { color: i === props.selected ? "cyan" : void 0, children: [
532
+ i === props.selected ? "\u276F " : " ",
533
+ m.name
534
+ ] }, m.id)),
535
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: t("enterToRun") })
536
+ ] });
537
+ }
538
+ if (phase === "done" && result) {
539
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
540
+ /* @__PURE__ */ jsx8(Text8, { children: resultText(result.record, result.assessment) }),
541
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
542
+ t("saved"),
543
+ " ",
544
+ result.path
545
+ ] })
546
+ ] });
547
+ }
548
+ if (phase === "error") {
549
+ return /* @__PURE__ */ jsx8(Box7, { flexDirection: "column", children: /* @__PURE__ */ jsx8(Text8, { color: "red", children: error }) });
550
+ }
551
+ const cpu = sample?.cpu?.utilizationPercent;
552
+ const gpu = sample?.gpu?.utilizationPercent;
553
+ const memUsed = sample?.memory.usedBytes;
554
+ const memTotal = h.memory.totalBytes;
555
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
556
+ /* @__PURE__ */ jsxs8(Box7, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1, children: [
557
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: t("benchmarkTitle") }),
558
+ /* @__PURE__ */ jsx8(Text8, { children: model?.name }),
559
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
560
+ h.cpu.name,
561
+ " \xB7 ",
562
+ formatBytes(h.memory.totalBytes, 0),
563
+ " ",
564
+ memoryLabel(h)
565
+ ] })
566
+ ] }),
567
+ /* @__PURE__ */ jsxs8(Box7, { marginTop: 1, flexDirection: "column", children: [
568
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: "cyan", children: t("performance") }),
569
+ /* @__PURE__ */ jsxs8(Text8, { children: [
570
+ t("elapsed"),
571
+ " ",
572
+ elapsed.toFixed(1),
573
+ " s"
574
+ ] }),
575
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: t("measuredAfter") })
576
+ ] }),
577
+ /* @__PURE__ */ jsxs8(Box7, { marginTop: 1, flexDirection: "column", children: [
578
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: "cyan", children: t("system") }),
579
+ /* @__PURE__ */ jsxs8(Text8, { children: [
580
+ "CPU ",
581
+ bar(cpu),
582
+ " ",
583
+ formatPercent(cpu)
584
+ ] }),
585
+ /* @__PURE__ */ jsxs8(Text8, { children: [
586
+ "GPU ",
587
+ bar(gpu),
588
+ " ",
589
+ formatPercent(gpu)
590
+ ] }),
591
+ /* @__PURE__ */ jsxs8(Text8, { children: [
592
+ "Memory ",
593
+ bar(memUsed && memTotal ? memUsed / memTotal * 100 : void 0),
594
+ " ",
595
+ memUsed ? formatBytes(memUsed) : "N/A",
596
+ " / ",
597
+ formatBytes(memTotal, 0)
598
+ ] }),
599
+ /* @__PURE__ */ jsxs8(Text8, { children: [
600
+ "Swap ",
601
+ bar(0),
602
+ " ",
603
+ sample ? formatBytes(sample.memory.swapUsedBytes) : "N/A"
604
+ ] })
605
+ ] }),
606
+ /* @__PURE__ */ jsxs8(Box7, { marginTop: 1, flexDirection: "column", children: [
607
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: "cyan", children: t("powerThermals") }),
608
+ /* @__PURE__ */ jsxs8(Text8, { children: [
609
+ "Package ",
610
+ sample?.packagePowerWatts ?? "N/A",
611
+ " W"
612
+ ] }),
613
+ /* @__PURE__ */ jsxs8(Text8, { children: [
614
+ "Temperature ",
615
+ sample?.thermal?.temperatureC ?? "N/A",
616
+ " \xB0C"
617
+ ] }),
618
+ /* @__PURE__ */ jsxs8(Text8, { children: [
619
+ "Thermal Pressure ",
620
+ sample?.thermal?.pressure ?? "N/A"
621
+ ] }),
622
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: t("permissionsHint") })
623
+ ] }),
624
+ /* @__PURE__ */ jsxs8(Text8, { children: [
625
+ sparkline(cpuHist.current),
626
+ " CPU"
627
+ ] }),
628
+ /* @__PURE__ */ jsxs8(Text8, { children: [
629
+ sparkline(memHist.current.map((v) => v / 1024 ** 3)),
630
+ " Memory"
631
+ ] }),
632
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: t("cancelHint") })
633
+ ] });
634
+ }
635
+
636
+ // src/tui/views/History.tsx
637
+ import { Box as Box8, Text as Text9 } from "ink";
638
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
639
+ function HistoryView({ session }) {
640
+ return /* @__PURE__ */ jsxs9(Panel, { title: t("measured"), color: "cyan", children: [
641
+ session.history.length === 0 ? /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: t("historyEmpty") }) : null,
642
+ session.history.map((r) => /* @__PURE__ */ jsxs9(Box8, { children: [
643
+ /* @__PURE__ */ jsx9(Box8, { width: 22, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: fit(r.timestamp, 20) }) }),
644
+ /* @__PURE__ */ jsx9(Box8, { width: 20, children: /* @__PURE__ */ jsx9(Text9, { children: fit(r.model.id, 18) }) }),
645
+ /* @__PURE__ */ jsx9(Box8, { width: 12, children: /* @__PURE__ */ jsx9(Text9, { color: "green", children: r.benchmark.generationTokensPerSecond ? formatTokensPerSec(r.benchmark.generationTokensPerSecond) : "N/A" }) }),
646
+ /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
647
+ "score ",
648
+ r.score?.total ?? "N/A"
649
+ ] })
650
+ ] }, `${r.timestamp}-${r.model.id}`))
651
+ ] });
652
+ }
653
+
654
+ // src/tui/views/Tasks.tsx
655
+ import { Text as Text10, useInput as useInput3 } from "ink";
656
+ import { useMemo, useState as useState3 } from "react";
657
+ import { Fragment as Fragment5, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
658
+ var KIND_LABEL = {
659
+ code: "tasksOptCode",
660
+ video: "tasksOptVideo",
661
+ image: "tasksOptImage",
662
+ transcription: "tasksOptTranscription",
663
+ chat: "tasksOptChat"
664
+ };
665
+ function TasksView({ session }) {
666
+ const [step, setStep] = useState3(0);
667
+ const [cursor, setCursor] = useState3(0);
668
+ const [kinds, setKinds] = useState3(["code", "chat"]);
669
+ const [scope, setScope] = useState3("installed");
670
+ const [priority, setPriority] = useState3("balanced");
671
+ const plans = useMemo(
672
+ () => planTasks(session, { kinds, scope, priority }),
673
+ [session, kinds, scope, priority]
674
+ );
675
+ useInput3((input, key) => {
676
+ if (step === 0) {
677
+ if (key.upArrow) setCursor((c) => Math.max(0, c - 1));
678
+ if (key.downArrow) setCursor((c) => Math.min(WORK_KINDS.length - 1, c + 1));
679
+ if (input === " " || input === "x") {
680
+ const kind = WORK_KINDS[cursor];
681
+ if (!kind) return;
682
+ setKinds(
683
+ (current) => current.includes(kind) ? current.filter((k) => k !== kind) : [...current, kind]
684
+ );
685
+ }
686
+ if (key.return && kinds.length) setStep(1);
687
+ } else if (step === 1) {
688
+ if (key.upArrow || key.downArrow) setScope((s) => s === "installed" ? "all" : "installed");
689
+ if (key.return) setStep(2);
690
+ } else if (step === 2) {
691
+ const order = ["speed", "balanced", "quality"];
692
+ if (key.upArrow) setPriority(order[Math.max(0, order.indexOf(priority) - 1)] ?? "speed");
693
+ if (key.downArrow) setPriority(order[Math.min(2, order.indexOf(priority) + 1)] ?? "quality");
694
+ if (key.return) setStep(3);
695
+ }
696
+ });
697
+ if (step === 3) {
698
+ return /* @__PURE__ */ jsx10(Panel, { title: t("tasksTitle"), color: "blue", children: /* @__PURE__ */ jsx10(Text10, { children: tasksText(plans, { kinds, scope, priority }) }) });
699
+ }
700
+ return /* @__PURE__ */ jsxs10(Panel, { title: t("tasksTitle"), color: "blue", children: [
701
+ step === 0 && /* @__PURE__ */ jsxs10(Fragment5, { children: [
702
+ /* @__PURE__ */ jsx10(Text10, { children: t("tasksQ1") }),
703
+ WORK_KINDS.map((kind, i) => /* @__PURE__ */ jsxs10(Text10, { color: i === cursor ? "cyan" : void 0, children: [
704
+ i === cursor ? "\u276F " : " ",
705
+ "[",
706
+ kinds.includes(kind) ? "x" : " ",
707
+ "] ",
708
+ t(KIND_LABEL[kind])
709
+ ] }, kind))
710
+ ] }),
711
+ step === 1 && /* @__PURE__ */ jsxs10(Fragment5, { children: [
712
+ /* @__PURE__ */ jsx10(Text10, { children: t("tasksQ2") }),
713
+ /* @__PURE__ */ jsxs10(Text10, { color: scope === "installed" ? "cyan" : void 0, children: [
714
+ scope === "installed" ? "\u276F " : " ",
715
+ t("tasksOptInstalled")
716
+ ] }),
717
+ /* @__PURE__ */ jsxs10(Text10, { color: scope === "all" ? "cyan" : void 0, children: [
718
+ scope === "all" ? "\u276F " : " ",
719
+ t("tasksOptAll")
720
+ ] })
721
+ ] }),
722
+ step === 2 && /* @__PURE__ */ jsxs10(Fragment5, { children: [
723
+ /* @__PURE__ */ jsx10(Text10, { children: t("tasksQ3") }),
724
+ ["speed", "balanced", "quality"].map((item) => /* @__PURE__ */ jsxs10(Text10, { color: priority === item ? "cyan" : void 0, children: [
725
+ priority === item ? "\u276F " : " ",
726
+ item === "speed" ? t("tasksOptSpeed") : item === "quality" ? t("tasksOptQuality") : t("tasksOptBalanced")
727
+ ] }, item))
728
+ ] })
729
+ ] });
730
+ }
731
+
732
+ // src/tui/keys.ts
733
+ function globalNav(letter) {
734
+ if (letter === "b") return { type: "screen", screen: "benchmark" };
735
+ if (letter === "m") return { type: "screen", screen: "models" };
736
+ if (letter === "r") return { type: "screen", screen: "recommend" };
737
+ if (letter === "h") return { type: "screen", screen: "hardware" };
738
+ if (letter === "c") return { type: "screen", screen: "compare" };
739
+ if (letter === "l") return { type: "screen", screen: "history" };
740
+ if (letter === "t") return { type: "screen", screen: "tasks" };
741
+ if (letter === "s") return { type: "screen", screen: "setup" };
742
+ return void 0;
743
+ }
744
+ function handleAppKey(screen, input, key) {
745
+ const letter = input.toLowerCase();
746
+ if (screen === "benchmark") return { type: "ignore" };
747
+ if (letter === "q") return { type: "quit" };
748
+ if (key.escape) return screen === "home" ? { type: "quit" } : { type: "home" };
749
+ if (screen === "setup") {
750
+ if ("i123456".includes(letter)) return { type: "ignore" };
751
+ }
752
+ const nav = globalNav(letter);
753
+ if (nav) return nav;
754
+ if (letter === "i" && screen === "home") return { type: "screen", screen: "setup" };
755
+ if (key.return && screen === "home") return { type: "screen", screen: "benchmark" };
756
+ return { type: "ignore" };
757
+ }
758
+
759
+ // src/tui/App.tsx
760
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
761
+ function App(props) {
762
+ const { exit } = useApp2();
763
+ const [screen, setScreen] = useState4(props.start ?? "home");
764
+ const screenRef = useRef2(screen);
765
+ screenRef.current = screen;
766
+ const [selected, setSelected] = useState4(0);
767
+ const benchable = useMemo2(
768
+ () => props.session.models.filter((m) => Boolean(m.artifactPath)),
769
+ [props.session.models]
770
+ );
771
+ useInput4((input, key) => {
772
+ const result = handleAppKey(screenRef.current, input, key);
773
+ if (result.type === "quit") exit();
774
+ if (result.type === "home") setScreen("home");
775
+ if (result.type === "screen") setScreen(result.screen);
776
+ });
777
+ return /* @__PURE__ */ jsxs11(Frame, { children: [
778
+ screen !== "benchmark" && /* @__PURE__ */ jsx11(Header, {}),
779
+ screen === "home" && /* @__PURE__ */ jsx11(Dashboard, { session: props.session }),
780
+ screen === "models" && /* @__PURE__ */ jsx11(ModelsView, { session: props.session }),
781
+ screen === "hardware" && /* @__PURE__ */ jsx11(HardwareView, { session: props.session }),
782
+ screen === "recommend" && /* @__PURE__ */ jsx11(RecommendView, { session: props.session }),
783
+ screen === "setup" && /* @__PURE__ */ jsx11(SetupView, { session: props.session }),
784
+ screen === "compare" && /* @__PURE__ */ jsx11(CompareView, { session: props.session }),
785
+ screen === "history" && /* @__PURE__ */ jsx11(HistoryView, { session: props.session }),
786
+ screen === "tasks" && /* @__PURE__ */ jsx11(TasksView, { session: props.session }),
787
+ screen === "benchmark" && /* @__PURE__ */ jsx11(
788
+ BenchmarkView,
789
+ {
790
+ session: props.session,
791
+ models: benchable,
792
+ selected,
793
+ setSelected,
794
+ preset: props.preset,
795
+ onBack: () => setScreen("home")
796
+ }
797
+ )
798
+ ] });
799
+ }
800
+
801
+ // src/tui/render.tsx
802
+ import { jsx as jsx12 } from "react/jsx-runtime";
803
+ async function renderDashboard(session, preset, start) {
804
+ const instance = render(/* @__PURE__ */ jsx12(App, { session, preset, start }), {
805
+ exitOnCtrlC: true,
806
+ patchConsole: true
807
+ });
808
+ await instance.waitUntilExit();
809
+ }
810
+ export {
811
+ renderDashboard
812
+ };