mlx-bun 0.0.10 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +66 -21
  2. package/THIRD_PARTY_LICENSES.md +1 -0
  3. package/docs/archive/mlx-bun-lab-report.html +275 -0
  4. package/docs/archive/wwdc-mlx-bun.html +848 -0
  5. package/docs/design/audio-input-plan.md +356 -0
  6. package/docs/design/batching-perf-path.md +58 -35
  7. package/docs/design/batching-v2-plan.md +23 -9
  8. package/docs/design/bucketing-stage.md +2 -1
  9. package/docs/design/compat-cli-surface-design.md +4 -3
  10. package/docs/design/decode-speed-program.md +172 -0
  11. package/docs/design/docs-reorg-plan.md +1 -1
  12. package/docs/design/dspark-serving-program.md +371 -0
  13. package/docs/design/dspark-speculative-decoding.md +167 -5
  14. package/docs/design/faithful-l1-consolidation.md +310 -0
  15. package/docs/design/generic-model-support.md +8 -5
  16. package/docs/design/grammar-spec-batching-integration.md +301 -0
  17. package/docs/design/minicpm5-decode-megakernel.md +16 -11
  18. package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
  19. package/docs/design/omlx-adoption-map.md +14 -6
  20. package/docs/design/orpo-training.md +34 -30
  21. package/docs/design/paged-kv-cache.md +214 -0
  22. package/docs/design/parallel-slots.md +82 -49
  23. package/docs/design/parity-tier-dag.md +43 -33
  24. package/docs/design/repo-cleanup-plan.md +189 -0
  25. package/docs/design/runtime-isolation.md +19 -3
  26. package/docs/design/segmented-backward-training.md +3 -2
  27. package/docs/design/ssd-kv-cold-tier.md +130 -0
  28. package/docs/design/structured-output.md +484 -0
  29. package/docs/design/the-dreaming-master-plan.md +2 -3
  30. package/docs/design/turboquant-kv.md +235 -0
  31. package/docs/design/unified-engine-frontier-plan.md +774 -0
  32. package/docs/design/web-chat-beat-matrix.md +315 -0
  33. package/docs/design/web-chat-redesign.md +1112 -0
  34. package/docs/investigations/dspark-handoff.md +182 -16
  35. package/docs/investigations/lab-build-journal.md +2 -2
  36. package/docs/investigations/openwiki-evaluation.md +249 -0
  37. package/docs/planning/IDEAS.md +14 -0
  38. package/docs/planning/PRODUCT_ROADMAP.md +2 -3
  39. package/docs/planning/chat-ui-vision-memory-first.md +199 -0
  40. package/docs/planning/chat-ui-vision.md +50 -0
  41. package/docs/planning/memory-docs-and-dag-plan.md +15 -6
  42. package/docs/planning/release-notes-v0.0.11.md +239 -0
  43. package/docs/reference/cli.md +35 -16
  44. package/docs/reference/distribution.md +13 -6
  45. package/docs/reference/embedding.md +35 -14
  46. package/docs/reference/features-matrix.md +97 -0
  47. package/docs/reference/library-api.md +30 -9
  48. package/docs/reference/memory.md +26 -6
  49. package/docs/reference/models.md +11 -1
  50. package/docs/reference/orpo-quickstart.md +3 -4
  51. package/docs/reference/server-api.md +634 -22
  52. package/docs/reference/server-config.md +250 -121
  53. package/docs/reference/training.md +14 -15
  54. package/package.json +5 -3
  55. package/src/anthropic.ts +13 -0
  56. package/src/audio/conformer.ts +647 -0
  57. package/src/audio/decode.ts +133 -0
  58. package/src/audio/features.ts +202 -0
  59. package/src/audio/transcode.ts +67 -0
  60. package/src/cli.ts +356 -105
  61. package/src/config.ts +49 -2
  62. package/src/download.ts +40 -0
  63. package/src/eval/README.md +7 -6
  64. package/src/eval/kl.ts +59 -3
  65. package/src/eval/quality-db.ts +2 -2
  66. package/src/eval/runner.ts +3 -3
  67. package/src/expert-offload.ts +15 -2
  68. package/src/flags.ts +13 -0
  69. package/src/generate.ts +330 -24
  70. package/src/grammar.ts +585 -0
  71. package/src/hub-rest.ts +242 -0
  72. package/src/kv-store.ts +442 -88
  73. package/src/lora.ts +8 -0
  74. package/src/md-modules.d.ts +42 -0
  75. package/src/media-fetch.ts +284 -0
  76. package/src/memory/rest.ts +245 -0
  77. package/src/memory/vault.ts +74 -0
  78. package/src/mlx/array.ts +106 -19
  79. package/src/mlx/autograd.ts +2 -2
  80. package/src/mlx/ffi.ts +27 -3
  81. package/src/mlx/metal-kernel.ts +7 -4
  82. package/src/mlx/ops.ts +153 -23
  83. package/src/mlx/turboquant-ops.ts +831 -0
  84. package/src/mlx/turboquant-tables.ts +237 -0
  85. package/src/model/batched-mask.ts +62 -1
  86. package/src/model/batched-quant.ts +206 -0
  87. package/src/model/batched-rotating-quant.ts +346 -0
  88. package/src/model/batched-rotating.ts +46 -1
  89. package/src/model/compiled-decode.ts +4 -15
  90. package/src/model/factory.ts +10 -4
  91. package/src/model/gemma4-base.ts +363 -39
  92. package/src/model/gemma4.ts +122 -33
  93. package/src/model/generated/gemma4-12b.ts +9 -13
  94. package/src/model/generated/gemma4-26b.ts +10 -5
  95. package/src/model/generated/gemma4-e4b.ts +14 -9
  96. package/src/model/minicpm5.ts +58 -102
  97. package/src/model/paged-kv.ts +253 -0
  98. package/src/model/qwen3-delta.ts +76 -15
  99. package/src/model/qwen3-moe.ts +404 -0
  100. package/src/model/qwen3.ts +9 -8
  101. package/src/model/qwen3_5.ts +88 -23
  102. package/src/model/support.ts +12 -1
  103. package/src/model/universal/dense.ts +40 -5
  104. package/src/model/universal/rope.ts +36 -21
  105. package/src/pi-web.ts +1299 -82
  106. package/src/preflight.ts +27 -8
  107. package/src/prompt-cache.ts +293 -33
  108. package/src/quantize/quantizer.ts +16 -1
  109. package/src/registry.ts +57 -2
  110. package/src/serve/batch-scheduler.ts +965 -72
  111. package/src/serve/generation-gateway.ts +214 -11
  112. package/src/serve/isolate.ts +417 -0
  113. package/src/serve/lane-registry.ts +44 -0
  114. package/src/serve/session-search.ts +326 -0
  115. package/src/server.ts +1380 -135
  116. package/src/spec/assistant-source.ts +150 -0
  117. package/src/spec/deepspec-source.ts +143 -0
  118. package/src/spec/dflash-source.ts +124 -0
  119. package/src/spec/dspark/ab-stats.ts +136 -0
  120. package/src/spec/dspark/calibration.ts +112 -0
  121. package/src/spec/dspark/deepspec-module.ts +801 -0
  122. package/src/spec/dspark/generate-dflash.ts +72 -20
  123. package/src/spec/dspark/loader.ts +23 -0
  124. package/src/spec/dspark/module-dflash.ts +278 -36
  125. package/src/spec/dspark/quantize-drafter.ts +91 -0
  126. package/src/spec/generate.ts +59 -25
  127. package/src/spec/ngram-source.ts +132 -0
  128. package/src/spec/serve-loop.ts +536 -0
  129. package/src/spec/source.ts +93 -0
  130. package/src/spec/two-model.ts +171 -0
  131. package/src/ssd-cache.ts +107 -35
  132. package/src/tokenizer.ts +288 -2
  133. package/src/tool-approvals.ts +131 -0
  134. package/src/tool-call.ts +408 -20
  135. package/src/train/flash-cce.ts +2 -2
  136. package/src/train/job.ts +0 -2
  137. package/src/train/trainer.ts +1 -4
  138. package/src/vision/prompt.ts +217 -48
  139. package/src/web/app.html +1184 -1789
  140. package/src/web/app.js +5667 -0
  141. package/src/web/icon.svg +13 -0
  142. package/src/web/manifest.webmanifest +24 -0
  143. package/src/web/src/adapters-panel.ts +288 -0
  144. package/src/web/src/api.ts +57 -0
  145. package/src/web/src/assistant.ts +485 -0
  146. package/src/web/src/chat.ts +1050 -0
  147. package/src/web/src/composer.ts +959 -0
  148. package/src/web/src/dataset.ts +132 -0
  149. package/src/web/src/finetune.ts +267 -0
  150. package/src/web/src/hub.ts +428 -0
  151. package/src/web/src/main.ts +98 -0
  152. package/src/web/src/markdown.ts +0 -0
  153. package/src/web/src/memory-panel.ts +572 -0
  154. package/src/web/src/model-picker.ts +159 -0
  155. package/src/web/src/palette.ts +346 -0
  156. package/src/web/src/protocol.ts +62 -0
  157. package/src/web/src/quantize.ts +167 -0
  158. package/src/web/src/rag.ts +274 -0
  159. package/src/web/src/sessions.ts +469 -0
  160. package/src/web/src/shell.ts +804 -0
  161. package/src/web/src/status.ts +252 -0
  162. package/src/web/src/ui-catalog.ts +92 -0
  163. package/src/web/sw.js +71 -0
  164. package/src/web/vendor/README.md +61 -0
  165. package/src/web/vendor/hljs-LICENSE +29 -0
  166. package/src/web/vendor/hljs-theme.css +59 -0
  167. package/src/web/vendor/hljs.js +26 -0
  168. package/src/model/fused-decode-kernel.ts +0 -312
  169. package/src/model/fused-geglu-kernel.ts +0 -198
  170. package/src/model/fused-mlp-kernel.ts +0 -169
  171. package/src/model/fused-swiglu-kernel.ts +0 -240
  172. package/src/model/steel-linear-kernel.ts +0 -96
  173. package/src/status-page.html +0 -497
@@ -0,0 +1,252 @@
1
+ // GENERATED-ADJACENT source module — part of the src/web/src/* split (plan
2
+ // §7/§9 Phase 2). Built into src/web/app.js by scripts/build-web.ts.
3
+ //
4
+ // STATUS CONTROLLER — ported dashboard (poll /stats + /v1/models + /fit +
5
+ // /library + /downloads). Behavior-identical port of the original
6
+ // controllers.status IIFE in app.html.
7
+
8
+ import { $, gb, mb, num } from "./shell";
9
+ import { esc } from "./markdown";
10
+
11
+ interface DownloadRow {
12
+ repoId: string;
13
+ state: "active" | "done" | "error";
14
+ totalBytes: number;
15
+ receivedBytes: number;
16
+ bytesPerSec?: number;
17
+ currentFile?: string;
18
+ filesDone: number;
19
+ filesTotal: number;
20
+ startedAt: number;
21
+ error?: string;
22
+ }
23
+
24
+ interface LibraryModel {
25
+ repo_id: string;
26
+ size_bytes: number;
27
+ serving?: boolean;
28
+ supported?: boolean;
29
+ model_type?: string;
30
+ vision?: boolean;
31
+ assessment?: {
32
+ fits?: boolean;
33
+ max_safe_context?: number;
34
+ predicted_decode_tps?: number;
35
+ };
36
+ }
37
+
38
+ interface SkuRow {
39
+ sku: string;
40
+ ram_gb: number;
41
+ fits: boolean;
42
+ max_context: number;
43
+ decode_tps: number;
44
+ }
45
+
46
+ export function createStatusController() {
47
+ let timer: ReturnType<typeof setInterval> | null = null, libTimer: ReturnType<typeof setInterval> | null = null, fitLoaded = false;
48
+ const dlRates: Record<string, { bytes: number; t: number }> = {};
49
+ // Library staleness (web-ui-pass-plan.md #3): /library is server-cached for
50
+ // 30s, but the client used to fetch it exactly once per page load
51
+ // (gated on `fitLoaded`) — a quantize job finishing, a fresh download
52
+ // landing, or a manual drop into the HF cache never showed up without a
53
+ // hard reload. Poll it independently on a ~15s cadence while this view is
54
+ // visible (started in enter()/stopped in leave(), same as the main tick).
55
+
56
+ async function tick(): Promise<void> {
57
+ try {
58
+ const [stats, models] = await Promise.all([
59
+ fetch("/stats").then((r) => r.json()),
60
+ fetch("/v1/models").then((r) => r.json()),
61
+ ]);
62
+ const m = models.data && models.data[0];
63
+ $("st-model-id").textContent = (m && m.id) || "—";
64
+ const a = stats.admission || {};
65
+ $("st-model-meta").textContent = num(m && m.context_window) + " token window · weights " + gb(a.weights_bytes);
66
+ $("st-safe-ctx").textContent = num(a.max_safe_context);
67
+
68
+ $("st-weights").textContent = gb(a.weights_bytes);
69
+ $("st-usable").textContent = gb(a.usable_bytes);
70
+ $("st-budget").textContent = a.memory_budget_bytes ? gb(a.memory_budget_bytes) : "off";
71
+ $("st-mem-bar").style.width = a.usable_bytes ? Math.min(100, 100 * a.weights_bytes / a.usable_bytes) + "%" : "0%";
72
+
73
+ const pc = stats.prompt_cache || {};
74
+ const lookups = (pc.hits || 0) + (pc.misses || 0);
75
+ $("st-pc-hits").textContent = lookups ? Math.round(100 * pc.hits / lookups) + "%" : "no lookups";
76
+ $("st-pc-entries").textContent = num(pc.entries);
77
+ $("st-pc-bytes").textContent = mb(pc.bytes) + " / " + mb(pc.max_bytes);
78
+ $("st-pc-bar").style.width = pc.max_bytes ? Math.min(100, 100 * pc.bytes / pc.max_bytes) + "%" : "0%";
79
+
80
+ const rs = stats.response_store || {};
81
+ $("st-rs-entries").textContent = num(rs.entries);
82
+ $("st-rs-bytes").textContent = mb(rs.bytes) + " / " + mb(rs.max_bytes);
83
+ $("st-rs-ttl").textContent = rs.ttl_ms ? (rs.ttl_ms / 60000) + " min" : "—";
84
+ $("st-rs-bar").style.width = rs.max_bytes ? Math.min(100, 100 * rs.bytes / rs.max_bytes) + "%" : "0%";
85
+
86
+ const kv = stats.kv_quant || {};
87
+ $("st-kv-mode").innerHTML = "<code>" + esc(kv.mode || "—") + "</code>";
88
+ const att = kv.attention;
89
+ $("st-kv-layers").innerHTML = Object.entries(kv.layers || {})
90
+ .map(([k, v]) => '<div class="kv"><b>' + esc(k) + ' layers</b><span class="num">' + esc(v) + "</span></div>").join("") +
91
+ (att ? '<div class="kv"><b>attention</b><span class="num">' + esc(att.global) + " global · " + esc(att.sliding_window) + " sliding</span></div>" : "");
92
+
93
+ await loadDownloads();
94
+ if (!fitLoaded) { await loadFit(); await loadLibrary(); }
95
+ if (stats.server && stats.server.started_at) {
96
+ const up = (Date.now() - stats.server.started_at) / 1000;
97
+ $("st-uptime").textContent = up < 90 ? Math.round(up) + "s" : up < 5400 ? Math.round(up / 60) + "m" : (up / 3600).toFixed(1) + "h";
98
+ }
99
+ $("st-updated").textContent = new Date().toLocaleTimeString();
100
+ } catch {
101
+ /* nav pill already reflects connectivity; leave last-good values on screen */
102
+ }
103
+ }
104
+
105
+ async function loadDownloads(): Promise<void> {
106
+ let downloads: DownloadRow[] | undefined;
107
+ try { ({ downloads } = await fetch("/downloads").then((r) => r.json())); } catch { return; }
108
+ const card = $("st-dl-card");
109
+ if (!downloads || downloads.length === 0) { card.style.display = "none"; return; }
110
+ card.style.display = "";
111
+ const order = downloads.slice().sort((a, b) =>
112
+ (a.state === "active" ? 0 : 1) - (b.state === "active" ? 0 : 1) || b.startedAt - a.startedAt);
113
+ $("st-dl-list").innerHTML = order.map((d) => {
114
+ const pct = d.totalBytes ? Math.min(100, 100 * d.receivedBytes / d.totalBytes) : 0;
115
+ let detail: string, barCls = "";
116
+ if (d.state === "active") {
117
+ let bps = d.bytesPerSec || 0;
118
+ if (!bps) {
119
+ const prev = dlRates[d.repoId], now = performance.now();
120
+ if (prev && now > prev.t) bps = (d.receivedBytes - prev.bytes) / ((now - prev.t) / 1000);
121
+ dlRates[d.repoId] = { bytes: d.receivedBytes, t: now };
122
+ }
123
+ const speed = bps > 0 ? " · " + mb(bps) + "/s · ~" + Math.max(1, Math.round((d.totalBytes - d.receivedBytes) / bps / 60)) + " min left" : "";
124
+ detail = gb(d.receivedBytes) + " / " + gb(d.totalBytes) + " · " + pct.toFixed(0) + "%" + speed +
125
+ (d.currentFile ? " · <code>" + esc(d.currentFile) + "</code>" : "");
126
+ barCls = "warm";
127
+ } else if (d.state === "done") {
128
+ detail = gb(d.totalBytes) + ' · <span style="color:var(--green);font-weight:600">complete</span>';
129
+ } else {
130
+ detail = '<span style="color:var(--red);font-weight:600">error</span> · ' + esc(d.error || "");
131
+ }
132
+ return '<div style="padding:10px 0">' +
133
+ '<div class="kv" style="border:none"><b>' + esc(d.repoId) + "</b>" +
134
+ '<span class="num">' + d.filesDone + "/" + d.filesTotal + " files</span></div>" +
135
+ '<div style="font-size:13.5px;color:var(--dim)" class="num">' + detail + "</div>" +
136
+ '<div class="meter ' + barCls + '"><i style="width:' + pct + '%"></i></div></div>';
137
+ }).join("");
138
+ }
139
+
140
+ async function loadLibrary(): Promise<void> {
141
+ let models: LibraryModel[] | undefined;
142
+ try { ({ models } = await fetch("/library").then((r) => r.json())); } catch { return; }
143
+ if (!models) return;
144
+ $("st-lib-body").innerHTML = models.slice()
145
+ .sort((a, b) => (b.serving ? 1 : 0) - (a.serving ? 1 : 0) || b.size_bytes - a.size_bytes)
146
+ .map((m) => {
147
+ const a = m.assessment;
148
+ let status: string;
149
+ if (m.serving) status = '<span style="color:var(--blue);font-weight:700">SERVING</span>';
150
+ else if (!m.supported) status = '<span style="color:var(--dimmer)">unsupported (' + esc(m.model_type) + ")</span>";
151
+ else if (a && a.fits) status = '<span style="color:var(--green);font-weight:600">fits</span>';
152
+ else status = '<span style="color:var(--red);font-weight:600">too big</span>';
153
+ // web-ui-pass-plan.md #20: max_safe_context is meaningful whenever
154
+ // it's > 0, even when `fits` is false at the library's default 8192
155
+ // probe context — e.g. a model that doesn't fit at 8192 tokens but
156
+ // does fit at a smaller context still gets a real number here rather
157
+ // than an uninformative "—". predicted_decode_tps stays gated on
158
+ // `fits` since it's only computed for the fitting case server-side.
159
+ const safeCtx = m.supported && a && (a.max_safe_context ?? 0) > 0 ? num(a.max_safe_context) : "—";
160
+ return "<tr" + (m.serving ? ' class="you"' : "") + "><td>" + esc(m.repo_id) +
161
+ (m.vision ? ' · <span style="color:var(--dim)">vision</span>' : "") +
162
+ '</td><td class="num">' + gb(m.size_bytes) + "</td><td>" + status + "</td>" +
163
+ '<td class="num">' + safeCtx + "</td>" +
164
+ '<td class="num">' + (m.supported && a && a.fits ? (a.predicted_decode_tps ?? 0).toFixed(0) + " tok/s" : "—") + "</td></tr>";
165
+ }).join("");
166
+ }
167
+
168
+ async function loadFit(): Promise<void> {
169
+ interface FitResponse {
170
+ report?: {
171
+ fits?: boolean;
172
+ weights_bytes?: number; kv_bytes?: number; transient_bytes?: number;
173
+ total_bytes?: number; usable_bytes?: number; predicted_decode_tps?: number;
174
+ };
175
+ measured_decode_tps?: number;
176
+ typical_decode_tps?: number;
177
+ typical_context_tokens?: number;
178
+ context_tokens?: number;
179
+ machine?: { ram_bytes?: number; bandwidth_gbs?: number; chip?: string };
180
+ sku_matrix_ctx?: number;
181
+ sku_matrix?: SkuRow[];
182
+ }
183
+ let f: FitResponse;
184
+ try { f = await fetch("/fit").then((r) => r.json()); } catch { return; }
185
+ fitLoaded = true;
186
+ const r = f.report || {};
187
+ if (f.measured_decode_tps) {
188
+ $("st-tps").textContent = f.measured_decode_tps.toFixed(1);
189
+ $("st-tps").closest(".card")!.querySelector("h3")!.textContent = "Decode · measured";
190
+ $("st-tps-cap").textContent = "mlx-bun benchmark on this machine · predicted " + (f.typical_decode_tps || 0).toFixed(1) + " at " + num(f.typical_context_tokens) + " ctx";
191
+ } else if (f.typical_decode_tps) {
192
+ $("st-tps").textContent = f.typical_decode_tps.toFixed(1);
193
+ $("st-tps-cap").textContent = "at " + num(f.typical_context_tokens) + " context · " + (r.predicted_decode_tps || 0).toFixed(1) + " tok/s at the " + num(f.context_tokens) + " max (full-KV reads)";
194
+ } else {
195
+ $("st-tps").textContent = (r.predicted_decode_tps || 0).toFixed(1);
196
+ }
197
+ $("st-m-ram").textContent = gb(f.machine && f.machine.ram_bytes);
198
+ $("st-m-bw").textContent = (f.machine && f.machine.bandwidth_gbs || "—") + " GB/s";
199
+ $("st-m-fits").innerHTML = r.fits ? '<span style="color:var(--green);font-weight:700">FITS</span>' : '<span style="color:var(--red);font-weight:700">DOES NOT FIT</span>';
200
+ $("st-fit-ctx").textContent = num(f.context_tokens);
201
+ $("st-f-weights").textContent = gb(r.weights_bytes);
202
+ $("st-f-kv").textContent = gb(r.kv_bytes);
203
+ $("st-f-transient").textContent = gb(r.transient_bytes);
204
+ $("st-f-total").textContent = gb(r.total_bytes) + " / " + gb(r.usable_bytes);
205
+ $("st-fit-bar").style.width = r.usable_bytes ? Math.min(100, 100 * (r.total_bytes ?? 0) / r.usable_bytes) + "%" : "0%";
206
+
207
+ $("st-sku-ctx").textContent = num(f.sku_matrix_ctx);
208
+ const myRam = Math.round((f.machine && f.machine.ram_bytes || 0) / 2 ** 30);
209
+ const myChip = (f.machine && f.machine.chip) || null;
210
+ const rows = f.sku_matrix || [];
211
+ let youIdx = rows.findIndex((row) => myChip && row.sku === myChip && row.ram_gb === myRam);
212
+ if (youIdx < 0 && myChip) {
213
+ let best = Infinity;
214
+ rows.forEach((row, i) => { if (row.sku === myChip && Math.abs(row.ram_gb - myRam) < best) { best = Math.abs(row.ram_gb - myRam); youIdx = i; } });
215
+ }
216
+ if (youIdx < 0) youIdx = rows.findIndex((row) => row.ram_gb === myRam);
217
+ $("st-sku-body").innerHTML = rows.map((row, i) => {
218
+ const you = i === youIdx;
219
+ const dimCls = row.fits ? "num" : "num fit-no";
220
+ return "<tr" + (you ? ' class="you"' : "") + "><td>" + esc(row.sku) + (you ? " · <b>this machine</b>" : "") +
221
+ '</td><td class="num">' + row.ram_gb + " GB</td>" +
222
+ '<td class="' + (row.fits ? "fit-yes" : "fit-no") + '">' + (row.fits ? "fits" : "no") + "</td>" +
223
+ '<td class="' + dimCls + '">' + (row.max_context > 0 ? num(row.max_context) : "0 — weights alone don’t fit") + "</td>" +
224
+ '<td class="' + dimCls + '">' + (row.decode_tps > 0 ? "~" + row.decode_tps.toFixed(0) + " tok/s" : "—") + "</td></tr>";
225
+ }).join("");
226
+ }
227
+
228
+ return {
229
+ init() {
230
+ // trigger scroll reveals once mounted
231
+ const page = $("status-page");
232
+ requestAnimationFrame(() => page.classList.add("lit"));
233
+ page.classList.add("lit");
234
+ },
235
+ enter() {
236
+ tick();
237
+ timer = setInterval(tick, 2000);
238
+ loadLibrary();
239
+ libTimer = setInterval(loadLibrary, 15_000);
240
+ },
241
+ leave() {
242
+ if (timer) clearInterval(timer); timer = null;
243
+ if (libTimer) clearInterval(libTimer); libTimer = null;
244
+ },
245
+ // Exposed so a quantize job's `done` event (this same controller's own
246
+ // quantize flow, see controllers.quantize below) can force an immediate
247
+ // refresh instead of waiting up to 15s — the server invalidates its
248
+ // /library cache the moment the job completes (server.ts onComplete),
249
+ // so an immediate re-fetch is guaranteed fresh, not a race.
250
+ refreshLibrary: loadLibrary,
251
+ };
252
+ }
@@ -0,0 +1,92 @@
1
+ // GENERATED-ADJACENT source module — part of the src/web/src/* split (plan
2
+ // §7/§9 Phase 3, §6.6 "app-aware assistant"). Built into src/web/app.js by
3
+ // scripts/build-web.ts.
4
+ //
5
+ // The frontend half of the app-aware assistant's route/spotlight catalog.
6
+ // Mirrors PortfolioManager's ui-catalog.json pattern (client/src/lib/
7
+ // ui-catalog.ts + server/src/agent/ui-catalog.ts, read together with
8
+ // server/src/agent/portfolio-tools.ts as the reference implementation) but
9
+ // inlined as a TS module (not a shared JSON file) since mlx-bun's server and
10
+ // browser bundles are separate build outputs (scripts/build-web.ts vs bun's
11
+ // own TS execution) with no shared-JSON-import convention today — pi-web.ts
12
+ // mirrors ROUTE_IDS/SPOTLIGHT_TARGETS as plain string arrays for its own
13
+ // server-side validation (navigate_app/spotlight_ui reject anything not in
14
+ // that list BEFORE touching the browser) so route validity is checked at
15
+ // both ends without a build-time shared asset.
16
+ //
17
+ // Keep this list's ROUTE_IDS in sync with shell.ts's ROUTES tuple (chat,
18
+ // quantize, finetune, dataset, status) plus the Developer-only overlays that
19
+ // aren't hash-routes (memory panel, hub panel, settings) — those are
20
+ // `view`s, not `route`s (see UiContext.view below), reached by opening an
21
+ // overlay rather than changing the URL hash.
22
+
23
+ /** A hash-routed page (shell.ts's Route union, minus "routes" — the DAG
24
+ * diagram tab isn't a useful assistant destination and is itself
25
+ * feature-detected/hidden when unavailable). */
26
+ export const ROUTE_IDS = ["chat", "quantize", "finetune", "dataset", "status"] as const;
27
+ export type RouteId = (typeof ROUTE_IDS)[number];
28
+
29
+ /** A non-route overlay/panel the assistant can navigate to or spotlight
30
+ * within. Opening one of these does not change location.hash. */
31
+ export const VIEW_IDS = ["memory-panel", "hub-panel", "settings", "adapters-panel"] as const;
32
+ export type ViewId = (typeof VIEW_IDS)[number];
33
+
34
+ export function isRouteId(v: string): v is RouteId {
35
+ return (ROUTE_IDS as readonly string[]).includes(v);
36
+ }
37
+ export function isViewId(v: string): v is ViewId {
38
+ return (VIEW_IDS as readonly string[]).includes(v);
39
+ }
40
+
41
+ /** Human labels for routes, used in the "unknown route" tool-error message
42
+ * and nowhere else — kept tiny on purpose. */
43
+ export const ROUTE_LABELS: Record<RouteId, string> = {
44
+ chat: "Chat",
45
+ quantize: "Quantize",
46
+ finetune: "Fine-tune",
47
+ dataset: "Build Dataset",
48
+ status: "Status",
49
+ };
50
+
51
+ /** A curated spotlight target: a stable id -> the selector to highlight +
52
+ * a human label for the popover title, plus which route/view it lives on
53
+ * (spotlight_ui navigates there first if the browser isn't already on it).
54
+ * This is the "catalog target" resolution path in resolve-spotlight.ts's
55
+ * terms — the fallback when ref/label/selector don't resolve from the live
56
+ * DOM or the last uiSnapshot. Anchors added to app.html via data-spotlight
57
+ * in this same wave; ids are stable and boring (element id minus "chat-"
58
+ * prefix noise where it reads fine either way). */
59
+ export interface SpotlightTarget {
60
+ selector: string;
61
+ label: string;
62
+ route?: RouteId;
63
+ view?: ViewId;
64
+ }
65
+
66
+ export const SPOTLIGHT_TARGETS: Record<string, SpotlightTarget> = {
67
+ composer: { selector: "#chat-box", label: "Message box", route: "chat" },
68
+ send: { selector: "#chat-send", label: "Send", route: "chat" },
69
+ "new-chat": { selector: "#chat-new", label: "New chat", route: "chat" },
70
+ "session-search": { selector: "#chat-sess-search", label: "Search chats", route: "chat" },
71
+ "sampling-pill": { selector: "#chat-sampling", label: "Sampling", route: "chat" },
72
+ "prompt-pill": { selector: "#chat-sysprompt", label: "System prompt", route: "chat" },
73
+ "adapter-select": { selector: "#chat-adapter", label: "LoRA adapter", route: "chat" },
74
+ "memory-entry": { selector: "#chat-memory-entry", label: "Memory", route: "chat" },
75
+ "developer-toggle": { selector: "#nav-developer", label: "Developer toggle" },
76
+ "model-picker": { selector: "#nav-model", label: "Active model" },
77
+ "hub-browse": { selector: "#model-pop-browse", label: "Browse models", view: "hub-panel" },
78
+ "quantize-source": { selector: "#q-model", label: "Source model", route: "quantize" },
79
+ "finetune-base": { selector: "#f-model", label: "Base model", route: "finetune" },
80
+ };
81
+
82
+ export function getSpotlightTarget(id: string): SpotlightTarget | null {
83
+ return SPOTLIGHT_TARGETS[id] ?? null;
84
+ }
85
+
86
+ /** Resolve a free-form route/page string (either a bare route id or a
87
+ * "#/route" hash) the way navigate_app's tool param is described to the
88
+ * model. Returns null for anything not in ROUTE_IDS. */
89
+ export function resolveRouteId(routeOrPage: string): RouteId | null {
90
+ const bare = routeOrPage.trim().replace(/^#\/?/, "");
91
+ return isRouteId(bare) ? bare : null;
92
+ }
package/src/web/sw.js ADDED
@@ -0,0 +1,71 @@
1
+ // mlx-bun service worker — SHELL ONLY, not offline chat.
2
+ //
3
+ // Scope (plan §9 Phase 3, beat-matrix Axis 10 "PWA installability"):
4
+ // installability + instant shell load. The app is useless without the
5
+ // local inference server actually running (every chat turn is a live
6
+ // WebSocket to this same origin's model process), so caching API/WS
7
+ // traffic would be actively misleading — a "successful" cached response
8
+ // with no model behind it. This worker therefore caches exactly the
9
+ // static shell files needed to paint the UI instantly on repeat visits,
10
+ // and explicitly does NOT intercept /api/*, /v1/*, /ws/*, /downloads, or
11
+ // any other dynamic route — those always go straight to the network,
12
+ // cache-worker or not.
13
+ //
14
+ // Strategy: cache-first for the shell assets below (instant paint from
15
+ // cache, revalidate in the background), network-first fallthrough for
16
+ // everything else (a cache miss or a non-shell request just fetches
17
+ // normally, same as if this worker didn't exist).
18
+ //
19
+ // Versioned by CACHE_NAME so a deploy invalidates old shells; bump the
20
+ // suffix here if the shell file set changes.
21
+ const CACHE_NAME = "mlxbun-shell-v1";
22
+ const SHELL_FILES = [
23
+ "/",
24
+ "/assets/app.js",
25
+ "/assets/hljs.js",
26
+ "/assets/hljs.css",
27
+ ];
28
+
29
+ self.addEventListener("install", (event) => {
30
+ event.waitUntil(
31
+ caches.open(CACHE_NAME).then((cache) => cache.addAll(SHELL_FILES)).then(() => self.skipWaiting()),
32
+ );
33
+ });
34
+
35
+ self.addEventListener("activate", (event) => {
36
+ event.waitUntil(
37
+ caches.keys()
38
+ .then((names) => Promise.all(names.filter((n) => n !== CACHE_NAME).map((n) => caches.delete(n))))
39
+ .then(() => self.clients.claim()),
40
+ );
41
+ });
42
+
43
+ self.addEventListener("fetch", (event) => {
44
+ const req = event.request;
45
+ // Only ever handle GET; never intercept the WebSocket upgrade or any
46
+ // mutating request.
47
+ if (req.method !== "GET") return;
48
+
49
+ const url = new URL(req.url);
50
+ // Same-origin only, and only the exact shell files — everything else
51
+ // (including /api/*, /v1/*, /downloads, /curves, /dag, and any future
52
+ // route) falls through to the network untouched. This is the load-
53
+ // bearing line: it's what keeps "offline chat" from silently
54
+ // half-working with stale data instead of failing honestly.
55
+ if (url.origin !== self.location.origin || !SHELL_FILES.includes(url.pathname)) return;
56
+
57
+ event.respondWith(
58
+ caches.match(req).then((cached) => {
59
+ const network = fetch(req)
60
+ .then((res) => {
61
+ if (res && res.ok) caches.open(CACHE_NAME).then((cache) => cache.put(req, res.clone()));
62
+ return res;
63
+ })
64
+ .catch(() => cached); // offline and nothing cached yet — let it reject naturally
65
+ // Cache-first: serve the cached shell instantly if we have it, and
66
+ // still revalidate in the background so the next load picks up a
67
+ // rebuilt app.js. No cache yet (first visit) -> wait on the network.
68
+ return cached || network;
69
+ }),
70
+ );
71
+ });
@@ -0,0 +1,61 @@
1
+ # src/web/vendor/ — vendored, no-CDN web assets
2
+
3
+ Assets here are bundled locally and served same-origin by `src/server.ts`
4
+ (`GET /assets/<name>`, `with { type: "text" }` import — same pattern as
5
+ `src/web/app.html` itself). Nothing in this app loads a script or
6
+ stylesheet from a CDN; this directory is where that tradeoff lives.
7
+
8
+ ## hljs.js — syntax highlighting
9
+
10
+ `hljs.js` is [highlight.js](https://github.com/highlightjs/highlight.js)
11
+ (BSD-3-Clause, license text in `hljs-LICENSE`) core engine + a curated
12
+ language set: javascript/typescript, python, bash/shell, json, html/xml,
13
+ css, c/cpp, rust, go, sql, yaml, markdown, diff, plaintext. Registered
14
+ languages expose themselves as `class="language-<name>"` on `<code>`
15
+ elements — see `mdCodeBlock()` and `highlightIn()` in `app.html`.
16
+
17
+ To rebuild (e.g. to bump the version or change the language set):
18
+
19
+ ```sh
20
+ mkdir /tmp/hljs-build && cd /tmp/hljs-build
21
+ npm init -y
22
+ npm install highlight.js@<version> --no-save
23
+ ```
24
+
25
+ Write an `entry.js` that imports `highlight.js/lib/core` and
26
+ `highlight.js/lib/languages/<lang>` for each language you want, calling
27
+ `hljs.registerLanguage(name, lang)` for each (see mlx-bun's own build log /
28
+ commit that introduced this file for the exact entry.js used), then:
29
+
30
+ ```sh
31
+ globalThis.hljs = hljs; # inside entry.js, after registering
32
+ bun build ./entry.js --minify --outfile=hljs.bundle.js
33
+ ```
34
+
35
+ Prepend a short license/provenance header comment (BSD-3-Clause,
36
+ upstream URL, rebuild instructions — copy the one already at the top of
37
+ `hljs.js`) and copy the result over `src/web/vendor/hljs.js`.
38
+
39
+ **Size discipline:** the hygiene gate (`scripts/check-hygiene.ts`) fails
40
+ any tracked file over 1 MB unless it's on the explicit allowlist. The
41
+ current bundle (core + 14 languages) is ~83 KB — comfortably under the
42
+ cap. If you add enough languages to bust 1 MB, either trim the language
43
+ list or add a justified, size-capped `ALLOW` entry in
44
+ `scripts/check-hygiene.ts` (see the existing entries there for the
45
+ pattern) — don't silently let it grow past the cap.
46
+
47
+ `hljs-theme.css` is NOT generated from highlight.js's stock themes — it's
48
+ hand-written, mapping the standard `hljs-*` token classes onto mlx-bun's
49
+ own design tokens (see the file's own header comment) so code blocks match
50
+ the app's palette in both light and dark.
51
+
52
+ ## KaTeX (math) — deliberately not vendored
53
+
54
+ Investigated for the same treatment; skipped. KaTeX's CSS depends on its
55
+ own bundled math font (`dist/fonts/`, ~60 files / ~1.1 MB of woff2/woff/ttf)
56
+ for correct glyph metrics — there's no clean "system font fallback" path
57
+ that renders math correctly. Vendoring it would mean adding ~1 MB of
58
+ binary font files, which the hygiene gate is specifically designed to keep
59
+ out of this repo (see `scripts/check-hygiene.ts`'s header). Needs the
60
+ build-step/asset-hosting decision from `docs/design/web-chat-redesign.md`
61
+ §7 before it's worth revisiting.
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2006, Ivan Sagalaev.
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ * Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ * Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ * Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,59 @@
1
+ /* Vendored highlight.js theme, hand-derived from mlx-bun's own design
2
+ * tokens (see the :root / [data-theme="light"] blocks in src/web/app.html)
3
+ * rather than one of highlight.js's stock themes — keeps code-block colors
4
+ * on the same palette as the rest of the app in both themes. Code blocks
5
+ * deliberately keep a dark chrome in BOTH themes (see app.html's
6
+ * --code-bg/--code-ink comment), so this stylesheet targets a fixed dark
7
+ * surface rather than swapping with [data-theme="light"] itself; only the
8
+ * accent hues are shared with the live token set.
9
+ *
10
+ * Applies to `.bubble pre code.hljs` (see the highlightIn() hook in
11
+ * app.html) — every `<span class="hljs-*">` produced by the vendored
12
+ * hljs.js bundle (src/web/vendor/hljs.js, BSD-3-Clause, see its header).
13
+ */
14
+
15
+ .bubble pre code.hljs{ display:block; }
16
+
17
+ .hljs-comment,
18
+ .hljs-quote{ color:#86868b; font-style:italic; }
19
+
20
+ .hljs-keyword,
21
+ .hljs-selector-tag,
22
+ .hljs-subst,
23
+ .hljs-doctag{ color:#ff375f; }
24
+
25
+ .hljs-string,
26
+ .hljs-regexp,
27
+ .hljs-addition,
28
+ .hljs-attr,
29
+ .hljs-meta-string{ color:#30d158; }
30
+
31
+ .hljs-number,
32
+ .hljs-literal,
33
+ .hljs-deletion{ color:#ff9f0a; }
34
+
35
+ .hljs-title,
36
+ .hljs-title.function_,
37
+ .hljs-title.class_,
38
+ .hljs-section{ color:#64d2ff; }
39
+
40
+ .hljs-type,
41
+ .hljs-class .hljs-title,
42
+ .hljs-built_in{ color:#bf5af2; }
43
+
44
+ .hljs-name,
45
+ .hljs-tag,
46
+ .hljs-selector-id,
47
+ .hljs-selector-class{ color:#0a84ff; }
48
+
49
+ .hljs-variable,
50
+ .hljs-template-variable,
51
+ .hljs-attribute{ color:#ffd60a; }
52
+
53
+ .hljs-symbol,
54
+ .hljs-bullet,
55
+ .hljs-link,
56
+ .hljs-meta{ color:#5e5ce6; }
57
+
58
+ .hljs-emphasis{ font-style:italic; }
59
+ .hljs-strong{ font-weight:700; }