mlx-bun 0.5.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (640) hide show
  1. package/README.md +1449 -109
  2. package/THIRD_PARTY_NOTICES.md +882 -0
  3. package/bin/mlx-bun.convert.mjs +4 -0
  4. package/bin/mlx-bun.fuse.mjs +4 -0
  5. package/bin/mlx-bun.generate.mjs +4 -0
  6. package/bin/mlx-bun.lora.mjs +4 -0
  7. package/bin/mlx-bun.mjs +13 -21
  8. package/bin/mlx-bun.server.mjs +4 -0
  9. package/bin/mlx-bun.upload.mjs +4 -0
  10. package/dist/web/app.js +9584 -0
  11. package/package.json +50 -53
  12. package/src/cli/ane-install.ts +76 -0
  13. package/src/cli/args.ts +179 -0
  14. package/src/cli/engine-entry.ts +68 -0
  15. package/src/cli/inference.ts +214 -0
  16. package/src/cli/job-entry.ts +97 -0
  17. package/src/cli/library-host.ts +117 -0
  18. package/src/cli/main.ts +57 -0
  19. package/src/cli/memory-engine.ts +163 -0
  20. package/src/cli/memory.ts +36 -0
  21. package/src/cli/mlx-lm-aliases.ts +309 -0
  22. package/src/cli/model-choice.ts +33 -0
  23. package/src/cli/model-selection.ts +84 -0
  24. package/src/cli/module-host.ts +11 -0
  25. package/src/cli/module-verbs.ts +50 -0
  26. package/src/cli/numerical-policy.ts +12 -0
  27. package/src/cli/perplexity.ts +89 -0
  28. package/src/cli/serve-host.ts +423 -0
  29. package/src/cli/serve-isolated.ts +217 -0
  30. package/src/cli/serve-options.ts +106 -0
  31. package/src/cli/serve-state.ts +205 -0
  32. package/src/cli/serve.ts +500 -0
  33. package/src/cli/served-model-host.ts +102 -0
  34. package/src/cli/server-entry.ts +148 -0
  35. package/src/cli/serving-unit.ts +177 -0
  36. package/src/{tui.ts → cli/terminal.ts} +13 -8
  37. package/src/cli/worker-adapters.ts +34 -0
  38. package/src/cli/worker-entry.ts +272 -0
  39. package/src/cli/worker-unit.ts +179 -0
  40. package/src/engine/adapter-operation.ts +26 -0
  41. package/src/engine/cache-services.ts +114 -0
  42. package/src/engine/completion.ts +39 -0
  43. package/src/engine/generation-gateway.ts +358 -0
  44. package/src/engine/index.ts +59 -0
  45. package/src/engine/kv-budget.ts +120 -0
  46. package/src/engine/media-preparation.ts +232 -0
  47. package/src/engine/model-binding.ts +24 -0
  48. package/src/engine/model-host.ts +513 -0
  49. package/src/engine/model-serving.ts +89 -0
  50. package/src/{serve → engine}/preparation.ts +7 -5
  51. package/src/{serve → engine}/session-completion-engine.ts +6 -6
  52. package/src/engine/telemetry.ts +109 -0
  53. package/src/hub/downloads.ts +116 -0
  54. package/src/jobs/db.ts +16 -7
  55. package/src/jobs/events.ts +29 -0
  56. package/src/jobs/executable.ts +5 -0
  57. package/src/jobs/host.ts +51 -0
  58. package/src/jobs/in-process.ts +55 -0
  59. package/src/{contracts/jobs.ts → jobs/protocol.ts} +7 -9
  60. package/src/jobs/runner.ts +150 -231
  61. package/src/jobs/service.ts +75 -0
  62. package/src/jobs/sse.ts +59 -23
  63. package/src/jobs/worker-memory.ts +46 -0
  64. package/src/jobs/worker-process.ts +163 -0
  65. package/src/jobs/worker-supervisor.ts +258 -0
  66. package/src/modules.ts +58 -0
  67. package/src/publishing/catalog-hub.ts +18 -0
  68. package/src/publishing/credentials.ts +31 -0
  69. package/src/publishing/upload.ts +37 -0
  70. package/src/residency/leased-adapters.ts +21 -0
  71. package/src/residency/local-records.ts +16 -0
  72. package/src/residency/model-residency.ts +405 -0
  73. package/src/residency/record-index.ts +28 -0
  74. package/src/residency/resident-estimate.ts +50 -0
  75. package/src/{anthropic.ts → server/anthropic.ts} +3 -81
  76. package/src/server/cache-routes.ts +36 -0
  77. package/src/{serve → server}/chat-request.ts +10 -10
  78. package/src/{serve → server}/chat-stage.ts +14 -14
  79. package/src/{client.ts → server/client.ts} +28 -2
  80. package/src/{serve → server}/completion-executor.ts +33 -41
  81. package/src/server/decision-body.ts +55 -0
  82. package/src/server/decision-routes.ts +134 -0
  83. package/src/server/decision-wire.ts +228 -0
  84. package/src/{serve → server}/discovery-routes.ts +25 -82
  85. package/src/{serve → server}/http.ts +25 -13
  86. package/src/{serve → server}/inference-request.ts +5 -5
  87. package/src/server/job-routes.ts +20 -0
  88. package/src/server/media-prompt.ts +63 -0
  89. package/src/server/memory-completion-client.ts +46 -0
  90. package/src/server/model-routes.ts +192 -0
  91. package/src/{serve → server}/openai-wire.ts +3 -2
  92. package/src/{serve → server}/pipeline.ts +1 -1
  93. package/src/server/prompt-builder.ts +13 -0
  94. package/src/server/prompt-contracts.ts +17 -0
  95. package/src/server/proxy-routes.ts +187 -0
  96. package/src/server/publishing-routes.ts +41 -0
  97. package/src/{serve → server}/request-plan.ts +6 -8
  98. package/src/{serve → server}/request-prep.ts +47 -50
  99. package/src/{serve → server}/responses-client.ts +30 -13
  100. package/src/{responses.ts → server/responses.ts} +12 -93
  101. package/src/server/routes.ts +205 -0
  102. package/src/server/start.ts +72 -0
  103. package/src/server/status-routes.ts +108 -0
  104. package/src/{serve → server}/text-completion-stage.ts +25 -4
  105. package/src/server/text-prompt.ts +12 -0
  106. package/src/{serve → server}/token-streams.ts +36 -38
  107. package/src/server/worker-routes.ts +311 -0
  108. package/src/storage/paths.ts +61 -0
  109. package/src/web/assets.ts +37 -0
  110. package/src/web/{src → browser}/api.ts +5 -23
  111. package/src/web/browser/chat-host.ts +17 -0
  112. package/src/web/browser/installed-panels.ts +6 -0
  113. package/src/web/browser/main.ts +55 -0
  114. package/src/web/browser/markdown.ts +279 -0
  115. package/src/web/browser/memory-panel.ts +9 -0
  116. package/src/web/browser/palette.ts +64 -0
  117. package/src/web/browser/protocol.ts +13 -0
  118. package/src/web/browser/shell.ts +467 -0
  119. package/src/web/{src → browser}/status.ts +32 -36
  120. package/src/web/build.ts +51 -0
  121. package/src/web/public/app.html +1054 -0
  122. package/src/web/{sw.js → public/sw.js} +4 -3
  123. package/src/web/{vendor → public/vendor}/hljs-theme.css +2 -2
  124. package/src/web/{vendor → public/vendor}/hljs.js +2 -2
  125. package/THIRD_PARTY_LICENSES.md +0 -51
  126. package/docs/README.md +0 -107
  127. package/docs/archive/hlg-sampling.md +0 -350
  128. package/docs/archive/investigations/audio-input-plan.md +0 -356
  129. package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +0 -3617
  130. package/docs/archive/investigations/chunk-finetune-experiment.md +0 -84
  131. package/docs/archive/investigations/colibri-glm52-port.md +0 -1486
  132. package/docs/archive/investigations/colibri-oracle-pin.md +0 -1284
  133. package/docs/archive/investigations/curve-bisector-routing.md +0 -123
  134. package/docs/archive/investigations/decode-roofline-lookagain.md +0 -348
  135. package/docs/archive/investigations/diffusion-gemma-port.md +0 -602
  136. package/docs/archive/investigations/dspark-handoff.md +0 -224
  137. package/docs/archive/investigations/expert-offload-single-user-moe.md +0 -292
  138. package/docs/archive/investigations/faithful-l1-consolidation.md +0 -310
  139. package/docs/archive/investigations/hlg-sampling-investigation.md +0 -970
  140. package/docs/archive/investigations/indexshare-performance-spike.md +0 -357
  141. package/docs/archive/investigations/kernel-perf-review-2026-07.md +0 -171
  142. package/docs/archive/investigations/lab-build-journal.md +0 -157
  143. package/docs/archive/investigations/minicpm5-decode-megakernel.md +0 -489
  144. package/docs/archive/investigations/openjarvis-fit.md +0 -198
  145. package/docs/archive/investigations/openwiki-evaluation.md +0 -249
  146. package/docs/archive/investigations/optimization_plan.md +0 -346
  147. package/docs/archive/investigations/orpo-base-uf-experiment-and-directions.md +0 -120
  148. package/docs/archive/investigations/orpo-flash-cce-pin-leak.md +0 -167
  149. package/docs/archive/investigations/orpo-uf-testing-handoff.md +0 -126
  150. package/docs/archive/investigations/pareto-specialized-runtime-findings.md +0 -92
  151. package/docs/archive/investigations/pi-builtin-investigation.md +0 -308
  152. package/docs/archive/investigations/pre-colibri-stabilization.md +0 -329
  153. package/docs/archive/investigations/qwen38-27b-campaign-log.md +0 -1092
  154. package/docs/archive/investigations/segmented-backward-handoff.md +0 -148
  155. package/docs/archive/investigations/starter-model-port-handoff.md +0 -342
  156. package/docs/archive/investigations/steel-flash-cce-handoff.md +0 -489
  157. package/docs/archive/investigations/the-dreaming-handoff.md +0 -141
  158. package/docs/archive/investigations/trainer-validation-experiment.md +0 -173
  159. package/docs/archive/planning/chat-ui-vision-memory-first.md +0 -199
  160. package/docs/archive/planning/chat-ui-vision.md +0 -50
  161. package/docs/archive/planning/chunk-task-roadmap.md +0 -77
  162. package/docs/archive/planning/curve-sampler-research-plan.md +0 -331
  163. package/docs/archive/planning/journal.md +0 -316
  164. package/docs/archive/planning/memory-docs-and-dag-plan.md +0 -194
  165. package/docs/archive/planning/pi-integration-review.md +0 -178
  166. package/docs/archive/planning/release-notes-v0.0.10.md +0 -57
  167. package/docs/archive/planning/release-notes-v0.0.11.md +0 -239
  168. package/docs/archive/planning/release-notes-v0.0.12.md +0 -80
  169. package/docs/archive/planning/release-notes-v0.0.13.md +0 -77
  170. package/docs/archive/planning/release-notes-v0.0.9.md +0 -122
  171. package/docs/archive/planning/release-notes-v0.2.0.md +0 -49
  172. package/docs/archive/planning/release-notes-v0.3.0.md +0 -35
  173. package/docs/archive/planning/release-notes-v0.4.0.md +0 -45
  174. package/docs/archive/planning/release-notes-v0.5.0.md +0 -37
  175. package/docs/archive/planning/web-ui-pass-plan.md +0 -213
  176. package/docs/archive/planning/website-readme-pass-plan.md +0 -224
  177. package/docs/design/batching.md +0 -2054
  178. package/docs/design/decode-speed-program.md +0 -2404
  179. package/docs/design/dreaming-nightly-pipeline.md +0 -684
  180. package/docs/design/generic-model-support.md +0 -821
  181. package/docs/design/kv-cache.md +0 -993
  182. package/docs/design/orpo-training.md +0 -680
  183. package/docs/design/speculative-decoding.md +0 -2129
  184. package/docs/design/turboquant.md +0 -2225
  185. package/docs/design/unified-engine-frontier-plan.md +0 -1987
  186. package/docs/design/web-chat-redesign.md +0 -695
  187. package/docs/planning/PRODUCT_ROADMAP.md +0 -593
  188. package/docs/planning/ResearchTopics.md +0 -119
  189. package/docs/planning/public-readiness-handoff.md +0 -244
  190. package/docs/planning/public-readiness.md +0 -149
  191. package/docs/planning/video-series.md +0 -421
  192. package/docs/reference/benchmarks.md +0 -3587
  193. package/docs/reference/cli.md +0 -646
  194. package/docs/reference/distribution.md +0 -380
  195. package/docs/reference/environment.md +0 -494
  196. package/docs/reference/glossary.md +0 -39
  197. package/docs/reference/library-api.md +0 -688
  198. package/docs/reference/memory.md +0 -260
  199. package/docs/reference/models.md +0 -346
  200. package/docs/reference/server-api.md +0 -1945
  201. package/docs/reference/server-config.md +0 -973
  202. package/docs/reference/training.md +0 -462
  203. package/docs/reference/troubleshooting.md +0 -158
  204. package/src/admission-plan.ts +0 -18
  205. package/src/audio/audiotoolbox.ts +0 -112
  206. package/src/audio/conformer.ts +0 -655
  207. package/src/audio/decode.ts +0 -133
  208. package/src/audio/features.ts +0 -202
  209. package/src/audio/mic-capture.ts +0 -98
  210. package/src/audio/silero-vad.ts +0 -379
  211. package/src/audio/transcode.ts +0 -67
  212. package/src/audio/whisper-decode.ts +0 -774
  213. package/src/audio/whisper-fast.ts +0 -732
  214. package/src/audio/whisper-format.ts +0 -73
  215. package/src/audio/whisper-mel.ts +0 -185
  216. package/src/audio/whisper-timing.ts +0 -274
  217. package/src/audio/whisper-tokenizer.ts +0 -239
  218. package/src/audio/whisper-transcribe.ts +0 -410
  219. package/src/backends/mlx/assistant-target.ts +0 -46
  220. package/src/backends/mlx/autoregressive-draft-rows.ts +0 -120
  221. package/src/backends/mlx/autoregressive.ts +0 -129
  222. package/src/backends/mlx/batch-group.ts +0 -1874
  223. package/src/backends/mlx/cache-attachment.ts +0 -67
  224. package/src/backends/mlx/cache-layout.ts +0 -31
  225. package/src/backends/mlx/checkpoint-state.ts +0 -53
  226. package/src/backends/mlx/continuation-persistence.ts +0 -107
  227. package/src/backends/mlx/continuation-request.ts +0 -43
  228. package/src/backends/mlx/continuation.ts +0 -77
  229. package/src/backends/mlx/diffusion.ts +0 -39
  230. package/src/backends/mlx/draft-target.ts +0 -32
  231. package/src/backends/mlx/encoder-cache.ts +0 -25
  232. package/src/backends/mlx/evaluation.ts +0 -105
  233. package/src/backends/mlx/fill-append.ts +0 -58
  234. package/src/backends/mlx/fill-group.ts +0 -497
  235. package/src/backends/mlx/full-kv-row-append.ts +0 -32
  236. package/src/backends/mlx/full-kv-row-donor.ts +0 -54
  237. package/src/backends/mlx/full-transitioning-kv-rows.ts +0 -65
  238. package/src/backends/mlx/gateway-binding.ts +0 -174
  239. package/src/backends/mlx/graph.ts +0 -38
  240. package/src/backends/mlx/kv-codec.ts +0 -9
  241. package/src/backends/mlx/kv-maintenance.ts +0 -161
  242. package/src/backends/mlx/kv-tensor-rows.ts +0 -212
  243. package/src/backends/mlx/memory-client.ts +0 -189
  244. package/src/backends/mlx/methods.ts +0 -90
  245. package/src/backends/mlx/mixed-iteration.ts +0 -62
  246. package/src/backends/mlx/model-prompt.ts +0 -178
  247. package/src/backends/mlx/model-serving.ts +0 -80
  248. package/src/backends/mlx/paged-cache-codec.ts +0 -27
  249. package/src/backends/mlx/prefill-cohort.ts +0 -100
  250. package/src/backends/mlx/prefill-policy.ts +0 -26
  251. package/src/backends/mlx/prefill-rows.ts +0 -244
  252. package/src/backends/mlx/prefill.ts +0 -33
  253. package/src/backends/mlx/projected-context-rows.ts +0 -72
  254. package/src/backends/mlx/prompt-input.ts +0 -29
  255. package/src/backends/mlx/qwen-prompt-input.ts +0 -40
  256. package/src/backends/mlx/request-policy.ts +0 -20
  257. package/src/backends/mlx/request-state-policy.ts +0 -54
  258. package/src/backends/mlx/rollback.ts +0 -92
  259. package/src/backends/mlx/row-sampling.ts +0 -49
  260. package/src/backends/mlx/serial-executor.ts +0 -251
  261. package/src/backends/mlx/speculative-group.ts +0 -438
  262. package/src/backends/mlx/speculative-round.ts +0 -161
  263. package/src/backends/mlx/speculative.ts +0 -131
  264. package/src/backends/mlx/state-rows.ts +0 -104
  265. package/src/backends/mlx/state-views.ts +0 -46
  266. package/src/backends/mlx/target-layout-capability.ts +0 -35
  267. package/src/backends/mlx/text-engine.ts +0 -73
  268. package/src/backends/mlx/transitioning-kv-rows.ts +0 -173
  269. package/src/chat-template.ts +0 -382
  270. package/src/cli.ts +0 -3490
  271. package/src/config.ts +0 -399
  272. package/src/contracts/completion.ts +0 -16
  273. package/src/contracts/execution.ts +0 -70
  274. package/src/contracts/generation.ts +0 -103
  275. package/src/contracts/host.ts +0 -7
  276. package/src/contracts/object-cache.ts +0 -8
  277. package/src/contracts/pi-provider.ts +0 -30
  278. package/src/contracts/pi.ts +0 -261
  279. package/src/contracts/prefix-cache.ts +0 -33
  280. package/src/contracts/resources.ts +0 -25
  281. package/src/contracts/sampling.ts +0 -23
  282. package/src/contracts/scheduling.ts +0 -54
  283. package/src/contracts/tsconfig.json +0 -14
  284. package/src/dataset/generators.ts +0 -1087
  285. package/src/dataset/index.ts +0 -12
  286. package/src/dataset/job.ts +0 -81
  287. package/src/dataset/llm.ts +0 -71
  288. package/src/dataset/registry.ts +0 -528
  289. package/src/diffusion/diffusion-generate.ts +0 -507
  290. package/src/download.ts +0 -505
  291. package/src/embed.ts +0 -77
  292. package/src/engine/admission.ts +0 -59
  293. package/src/engine/cancellation.ts +0 -38
  294. package/src/engine/engine.ts +0 -56
  295. package/src/engine/execution-coordinator.ts +0 -70
  296. package/src/engine/execution-plan.ts +0 -64
  297. package/src/engine/execution-tasks.ts +0 -44
  298. package/src/engine/namespaced-cache.ts +0 -13
  299. package/src/engine/resources.ts +0 -53
  300. package/src/engine/scheduler.ts +0 -92
  301. package/src/engine/session.ts +0 -247
  302. package/src/eval/README.md +0 -130
  303. package/src/eval/ifeval.ts +0 -81
  304. package/src/eval/kl-prompts.ts +0 -29
  305. package/src/eval/kl.ts +0 -590
  306. package/src/eval/perplexity.ts +0 -164
  307. package/src/eval/quality-db.ts +0 -92
  308. package/src/eval/reference-logits.ts +0 -207
  309. package/src/eval/runner.ts +0 -124
  310. package/src/eval/score.ts +0 -49
  311. package/src/eval/tasks/bfcl.ts +0 -535
  312. package/src/eval/tasks/gsm8k.ts +0 -98
  313. package/src/eval/tasks/hashhop.ts +0 -139
  314. package/src/eval/tasks/humaneval.ts +0 -275
  315. package/src/eval/tasks/ifeval.ts +0 -525
  316. package/src/eval/tasks/mmlu.ts +0 -157
  317. package/src/eval/ultrafeedback.ts +0 -60
  318. package/src/evaldb.ts +0 -199
  319. package/src/expert-io.ts +0 -393
  320. package/src/expert-offload-build.ts +0 -77
  321. package/src/expert-offload.ts +0 -76
  322. package/src/expert-residency.ts +0 -1134
  323. package/src/expert-usage.ts +0 -415
  324. package/src/fill/echo-index.ts +0 -188
  325. package/src/fill/fill-session.ts +0 -463
  326. package/src/fill/proposal.ts +0 -68
  327. package/src/fill/schema-rows.ts +0 -506
  328. package/src/fill/tool-boundary.ts +0 -103
  329. package/src/fit.ts +0 -249
  330. package/src/generate.ts +0 -1352
  331. package/src/grammar.ts +0 -604
  332. package/src/harness-pi.ts +0 -184
  333. package/src/hf-push.ts +0 -628
  334. package/src/hub-rest.ts +0 -242
  335. package/src/index.ts +0 -63
  336. package/src/inference/denoising.ts +0 -17
  337. package/src/inference/draft-acceptance.ts +0 -41
  338. package/src/inference/draft-output.ts +0 -49
  339. package/src/inference/graph.ts +0 -33
  340. package/src/inference/prefill.ts +0 -45
  341. package/src/inference/rollback.ts +0 -7
  342. package/src/jobs/index.ts +0 -31
  343. package/src/jobs/job-entry.ts +0 -107
  344. package/src/jobs/task-client.ts +0 -19
  345. package/src/jobs/types.ts +0 -2
  346. package/src/kv-scheme.ts +0 -279
  347. package/src/kv-store.ts +0 -1135
  348. package/src/lab/README.md +0 -22
  349. package/src/lab/curve/curve-designer.html +0 -303
  350. package/src/lab/curve/curve-sampler.ts +0 -122
  351. package/src/lab/expert-trace/expert-trace.ts +0 -69
  352. package/src/lab/paged-kv/paged-attention.ts +0 -117
  353. package/src/lab/paged-kv/paged-kv-rows.ts +0 -111
  354. package/src/lab/paged-kv/paged-kv.ts +0 -336
  355. package/src/library.ts +0 -40
  356. package/src/lora.ts +0 -408
  357. package/src/md-modules.d.ts +0 -54
  358. package/src/media-fetch.ts +0 -442
  359. package/src/memory/article.ts +0 -558
  360. package/src/memory/chunk-validation.ts +0 -150
  361. package/src/memory/chunk.ts +0 -318
  362. package/src/memory/cluster.ts +0 -351
  363. package/src/memory/coverage.ts +0 -134
  364. package/src/memory/crosslink.ts +0 -0
  365. package/src/memory/db.ts +0 -253
  366. package/src/memory/entity.ts +0 -203
  367. package/src/memory/gate.ts +0 -187
  368. package/src/memory/history.ts +0 -152
  369. package/src/memory/ingest.ts +0 -335
  370. package/src/memory/model.ts +0 -183
  371. package/src/memory/normalize.ts +0 -671
  372. package/src/memory/parse.ts +0 -22
  373. package/src/memory/pipeline.ts +0 -275
  374. package/src/memory/prompts.ts +0 -65
  375. package/src/memory/query.ts +0 -334
  376. package/src/memory/reconcile.ts +0 -734
  377. package/src/memory/redirect.ts +0 -108
  378. package/src/memory/reindex.ts +0 -325
  379. package/src/memory/resolve.ts +0 -307
  380. package/src/memory/rest.ts +0 -245
  381. package/src/memory/route.ts +0 -272
  382. package/src/memory/schedule.ts +0 -165
  383. package/src/memory/stages.ts +0 -777
  384. package/src/memory/synthesize.ts +0 -1638
  385. package/src/memory/tools.ts +0 -749
  386. package/src/memory/vault.ts +0 -1194
  387. package/src/memory/wikify.ts +0 -1064
  388. package/src/mlx/array.ts +0 -425
  389. package/src/mlx/autograd.ts +0 -304
  390. package/src/mlx/checkpoint.ts +0 -149
  391. package/src/mlx/compile.ts +0 -168
  392. package/src/mlx/custom-vjp.ts +0 -188
  393. package/src/mlx/ffi.ts +0 -531
  394. package/src/mlx/materialize.ts +0 -23
  395. package/src/mlx/metal-kernel.ts +0 -159
  396. package/src/mlx/normalized-argmax.ts +0 -116
  397. package/src/mlx/ops.ts +0 -1108
  398. package/src/mlx/quantized-rows.ts +0 -36
  399. package/src/mlx/token-bitmask.ts +0 -52
  400. package/src/mlx/turboquant-kv-decode.ts +0 -82
  401. package/src/mlx/turboquant-kv-inverse.ts +0 -122
  402. package/src/mlx/turboquant-ops.ts +0 -861
  403. package/src/mlx/turboquant-tables.ts +0 -237
  404. package/src/mmap.ts +0 -90
  405. package/src/model/artifact-identity.ts +0 -73
  406. package/src/model/batched-kv.ts +0 -235
  407. package/src/model/batched-mask.ts +0 -279
  408. package/src/model/batched-quant.ts +0 -212
  409. package/src/model/batched-quantized-kv.ts +0 -144
  410. package/src/model/batched-rotating-quant.ts +0 -353
  411. package/src/model/batched-rotating-state.ts +0 -149
  412. package/src/model/batched-rotating.ts +0 -338
  413. package/src/model/batched-row-storage.ts +0 -179
  414. package/src/model/batched-ssm.ts +0 -52
  415. package/src/model/batched-turboquant-kv.ts +0 -94
  416. package/src/model/compiled-decode.ts +0 -712
  417. package/src/model/decoded-kv-donor.ts +0 -12
  418. package/src/model/delayed-quantized-kv.ts +0 -72
  419. package/src/model/delayed-rotating-quantized-kv.ts +0 -93
  420. package/src/model/delayed-turboquant-kv.ts +0 -57
  421. package/src/model/diffusion-gemma.ts +0 -1026
  422. package/src/model/factory.ts +0 -215
  423. package/src/model/fingerprint.ts +0 -45
  424. package/src/model/flash-attention.ts +0 -694
  425. package/src/model/full-prefill-padding.ts +0 -56
  426. package/src/model/full-prefill-row.ts +0 -105
  427. package/src/model/gemma4-base.ts +0 -2370
  428. package/src/model/gemma4.ts +0 -1395
  429. package/src/model/generated/gemma4-12b.ts +0 -429
  430. package/src/model/generated/gemma4-26b.ts +0 -448
  431. package/src/model/generated/gemma4-e4b.ts +0 -717
  432. package/src/model/generated/index.ts +0 -18
  433. package/src/model/glm52-atlas.ts +0 -511
  434. package/src/model/glm52-cache.ts +0 -792
  435. package/src/model/glm52-config.ts +0 -262
  436. package/src/model/glm52-container.ts +0 -305
  437. package/src/model/glm52-coupling.ts +0 -388
  438. package/src/model/glm52-dsa.ts +0 -606
  439. package/src/model/glm52-expert-layout.ts +0 -253
  440. package/src/model/glm52-layout.ts +0 -152
  441. package/src/model/glm52-memory.ts +0 -500
  442. package/src/model/glm52-mla.ts +0 -792
  443. package/src/model/glm52-moe.ts +0 -223
  444. package/src/model/glm52-pilot.ts +0 -537
  445. package/src/model/glm52-reference.ts +0 -445
  446. package/src/model/glm52-residency.ts +0 -557
  447. package/src/model/glm52-resident-weights.ts +0 -135
  448. package/src/model/glm52-streamed-experts.ts +0 -453
  449. package/src/model/glm52-streamed-metal.ts +0 -610
  450. package/src/model/glm52-weights.ts +0 -503
  451. package/src/model/glm52.ts +0 -1132
  452. package/src/model/implementation.ts +0 -69
  453. package/src/model/kv-attention-view.ts +0 -70
  454. package/src/model/minicpm5.ts +0 -376
  455. package/src/model/profile.ts +0 -468
  456. package/src/model/quantized-append-attention.ts +0 -31
  457. package/src/model/qwen-append.ts +0 -12
  458. package/src/model/qwen-conv.ts +0 -52
  459. package/src/model/qwen3-delta-state.ts +0 -74
  460. package/src/model/qwen3-delta.ts +0 -451
  461. package/src/model/qwen3-moe.ts +0 -404
  462. package/src/model/qwen3-mrope.ts +0 -250
  463. package/src/model/qwen3.ts +0 -277
  464. package/src/model/qwen3_5-checkpoint.ts +0 -164
  465. package/src/model/qwen3_5.ts +0 -881
  466. package/src/model/rotating-kv-donor.ts +0 -35
  467. package/src/model/rotating-kv-layout.ts +0 -262
  468. package/src/model/rotating-row-transaction.ts +0 -35
  469. package/src/model/speculative-rotating-kv.ts +0 -101
  470. package/src/model/ssm-prefill-padding.ts +0 -39
  471. package/src/model/support.ts +0 -110
  472. package/src/model/token-groups.ts +0 -66
  473. package/src/model/trellis-balanced-scatter.ts +0 -51
  474. package/src/model/trellis-linear.ts +0 -672
  475. package/src/model/trellis-shared-m.ts +0 -151
  476. package/src/model/trellis-shared-scatter.ts +0 -144
  477. package/src/model/trellis-splitk-prefill.ts +0 -158
  478. package/src/model/trellis-tiled-prefill.ts +0 -136
  479. package/src/model/trellis-vector-expand.ts +0 -62
  480. package/src/model/trellis-wide-prefill.ts +0 -94
  481. package/src/model/turboquant-codec.ts +0 -89
  482. package/src/model/universal/archs.ts +0 -332
  483. package/src/model/universal/dense.ts +0 -612
  484. package/src/model/universal/modules.ts +0 -170
  485. package/src/model/universal/rope.ts +0 -381
  486. package/src/model/whisper.ts +0 -435
  487. package/src/native/expert_io.c +0 -523
  488. package/src/native/frame_extract.swift +0 -140
  489. package/src/native/mic_capture.swift +0 -92
  490. package/src/native-pack.ts +0 -163
  491. package/src/pi-provider.ts +0 -53
  492. package/src/pi-session.ts +0 -81
  493. package/src/pi-terminal.ts +0 -274
  494. package/src/pi-web.ts +0 -1940
  495. package/src/preflight.ts +0 -158
  496. package/src/prompt-cache.ts +0 -682
  497. package/src/quantize/allocator.ts +0 -627
  498. package/src/quantize/atomic-output.ts +0 -38
  499. package/src/quantize/calibration.ts +0 -181
  500. package/src/quantize/config-writer.ts +0 -145
  501. package/src/quantize/index.ts +0 -84
  502. package/src/quantize/job.ts +0 -162
  503. package/src/quantize/quantizer.ts +0 -666
  504. package/src/quantize/rotate.ts +0 -603
  505. package/src/quantize/safetensors-writer.ts +0 -249
  506. package/src/quantize/sensitivity.ts +0 -216
  507. package/src/quantize/trellis.ts +0 -586
  508. package/src/quantize/weight-transform.ts +0 -292
  509. package/src/registry.ts +0 -602
  510. package/src/runtime-config.ts +0 -77
  511. package/src/safetensors.ts +0 -165
  512. package/src/sampler.ts +0 -1012
  513. package/src/serve/admin-routes.ts +0 -184
  514. package/src/serve/assets/curve-terrain.html +0 -105
  515. package/src/serve/assets/training-inference-map.html +0 -575
  516. package/src/serve/audio-routes.ts +0 -323
  517. package/src/serve/aux-routes.ts +0 -132
  518. package/src/serve/batch-scheduler.ts +0 -8
  519. package/src/serve/checkpoint-identity.ts +0 -64
  520. package/src/serve/generation-gateway.ts +0 -537
  521. package/src/serve/isolate.ts +0 -609
  522. package/src/serve/kv-budget.ts +0 -14
  523. package/src/serve/lab-routes.ts +0 -341
  524. package/src/serve/lane-registry.ts +0 -45
  525. package/src/serve/model-admin-routes.ts +0 -155
  526. package/src/serve/model-binding.ts +0 -49
  527. package/src/serve/model-host.ts +0 -511
  528. package/src/serve/parent-application.ts +0 -56
  529. package/src/serve/prompt-response-trace.ts +0 -161
  530. package/src/serve/response-lease.ts +0 -24
  531. package/src/serve/session-search.ts +0 -326
  532. package/src/serve/static-routes.ts +0 -101
  533. package/src/serve/transcription-server.ts +0 -64
  534. package/src/serve/transcription-service.ts +0 -502
  535. package/src/server.ts +0 -1282
  536. package/src/spec/assistant-rows.ts +0 -93
  537. package/src/spec/assistant-source.ts +0 -126
  538. package/src/spec/cache-identity.ts +0 -22
  539. package/src/spec/deepspec-rows.ts +0 -29
  540. package/src/spec/deepspec-source.ts +0 -145
  541. package/src/spec/dflash-source.ts +0 -149
  542. package/src/spec/drafter.ts +0 -304
  543. package/src/spec/dspark/ab-stats.ts +0 -136
  544. package/src/spec/dspark/calibration.ts +0 -112
  545. package/src/spec/dspark/data-dflash.ts +0 -156
  546. package/src/spec/dspark/data.ts +0 -201
  547. package/src/spec/dspark/deepspec-module.ts +0 -776
  548. package/src/spec/dspark/generate-dflash.ts +0 -245
  549. package/src/spec/dspark/generate.ts +0 -290
  550. package/src/spec/dspark/loader.ts +0 -23
  551. package/src/spec/dspark/loss.ts +0 -213
  552. package/src/spec/dspark/module-dflash.ts +0 -560
  553. package/src/spec/dspark/module.ts +0 -540
  554. package/src/spec/dspark/quantize-drafter.ts +0 -91
  555. package/src/spec/dspark/sample.ts +0 -112
  556. package/src/spec/generate.ts +0 -276
  557. package/src/spec/glm52-mtp-graph.ts +0 -29
  558. package/src/spec/glm52-mtp-rows.ts +0 -132
  559. package/src/spec/glm52-mtp-source.ts +0 -318
  560. package/src/spec/glm52-mtp-state.ts +0 -33
  561. package/src/spec/ngram-source.ts +0 -226
  562. package/src/spec/projected-draft-rows.ts +0 -73
  563. package/src/spec/qwen-mtp-module.ts +0 -171
  564. package/src/spec/qwen-mtp-rows.ts +0 -230
  565. package/src/spec/qwen-mtp-source.ts +0 -386
  566. package/src/spec/qwen-mtp-state.ts +0 -30
  567. package/src/spec/serve-loop.ts +0 -587
  568. package/src/spec/source.ts +0 -228
  569. package/src/spec/two-model-rows.ts +0 -62
  570. package/src/spec/two-model.ts +0 -113
  571. package/src/ssd-cache.ts +0 -431
  572. package/src/ssd-durability.ts +0 -209
  573. package/src/storage/host-buffer.ts +0 -24
  574. package/src/storage/kv-writer.ts +0 -83
  575. package/src/storage/kv-writer.worker.js +0 -264
  576. package/src/storage/retention-policy.ts +0 -37
  577. package/src/tiered-prompt-cache.ts +0 -45
  578. package/src/tokenizer.ts +0 -335
  579. package/src/tool-approvals.ts +0 -131
  580. package/src/tool-call.ts +0 -695
  581. package/src/train/dataset.ts +0 -484
  582. package/src/train/diffusion-lora.ts +0 -216
  583. package/src/train/export.ts +0 -32
  584. package/src/train/flash-cce.ts +0 -1234
  585. package/src/train/forward.ts +0 -266
  586. package/src/train/fuse.ts +0 -243
  587. package/src/train/index.ts +0 -46
  588. package/src/train/job.ts +0 -205
  589. package/src/train/lora-params.ts +0 -254
  590. package/src/train/loss.ts +0 -1591
  591. package/src/train/merge.ts +0 -195
  592. package/src/train/optimizer.ts +0 -183
  593. package/src/train/prefix-shared.ts +0 -490
  594. package/src/train/rank.ts +0 -137
  595. package/src/train/segmented.ts +0 -1828
  596. package/src/train/steel-qmm-header.ts +0 -1461
  597. package/src/train/trainer.ts +0 -1221
  598. package/src/train/watch.ts +0 -542
  599. package/src/vision/diffusion-vision.ts +0 -620
  600. package/src/vision/embedder.ts +0 -176
  601. package/src/vision/preprocess.ts +0 -247
  602. package/src/vision/prompt.ts +0 -409
  603. package/src/vision/qwen3vl-preprocess.ts +0 -409
  604. package/src/vision/qwen3vl-prompt.ts +0 -233
  605. package/src/vision/qwen3vl-tower.ts +0 -566
  606. package/src/vision/siglip.ts +0 -607
  607. package/src/vision/video-frames.ts +0 -148
  608. package/src/web/app.html +0 -2155
  609. package/src/web/app.js +0 -5863
  610. package/src/web/skills/memory/SKILL.md +0 -120
  611. package/src/web/skills/web-research/SKILL.md +0 -46
  612. package/src/web/skills.ts +0 -97
  613. package/src/web/src/adapters-panel.ts +0 -288
  614. package/src/web/src/assistant.ts +0 -451
  615. package/src/web/src/chat.ts +0 -1060
  616. package/src/web/src/composer.ts +0 -962
  617. package/src/web/src/dataset.ts +0 -132
  618. package/src/web/src/finetune.ts +0 -267
  619. package/src/web/src/hub.ts +0 -428
  620. package/src/web/src/main.ts +0 -98
  621. package/src/web/src/markdown.ts +0 -0
  622. package/src/web/src/memory-panel.ts +0 -572
  623. package/src/web/src/model-picker.ts +0 -159
  624. package/src/web/src/palette.ts +0 -346
  625. package/src/web/src/protocol.ts +0 -26
  626. package/src/web/src/quantize.ts +0 -167
  627. package/src/web/src/rag.ts +0 -274
  628. package/src/web/src/sessions.ts +0 -469
  629. package/src/web/src/shell.ts +0 -804
  630. package/src/web/src/ui-catalog.ts +0 -78
  631. package/src/web/src/voice.ts +0 -141
  632. package/src/web/vendor/README.md +0 -61
  633. package/src/web-assets.ts +0 -47
  634. package/src/web-tools.ts +0 -478
  635. package/src/weights.ts +0 -255
  636. /package/src/{serve → server}/completion-sink.ts +0 -0
  637. /package/src/{serve → server}/generated-token-history.ts +0 -0
  638. /package/src/web/{icon.svg → public/icon.svg} +0 -0
  639. /package/src/web/{manifest.webmanifest → public/manifest.webmanifest} +0 -0
  640. /package/src/web/{vendor → public/vendor}/hljs-LICENSE +0 -0
@@ -1,962 +0,0 @@
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
- // The composer's non-thread surface: attachments (files + images), the
5
- // sampling popover (full parameter set incl. Advanced), the LoRA adapter
6
- // selector, the reasoning-toggle presentation, and the perf strip (tok/s,
7
- // TTFT, context-fill, lane badge). Extracted from the original monolithic
8
- // controllers.chat closure in app.html — behavior identical. Owns its own
9
- // module-level state (there's only ever one composer on screen, same as
10
- // the original single IIFE), but every WS send goes through a callback
11
- // passed in from chat.ts, which owns the actual WebSocket.
12
-
13
- import type { ReadyGenDefaults, SamplingOverrides } from "../../contracts/pi";
14
- import type { Lane } from "../../contracts/pi";
15
- import { api } from "./api";
16
- import type { ApiEnvelope } from "./protocol";
17
- import {
18
- $, el, toast, trapFocus, type FocusTrap, setSamplingPopoverClose, setSysPromptPopoverClose,
19
- } from "./shell";
20
- import { buildContextBlock, retrieve, shouldRetrieve, type Citation } from "./rag";
21
-
22
- /* ────────────────────────────────────────────────────────────────────
23
- Attachments (files + images)
24
- ──────────────────────────────────────────────────────────────────── */
25
-
26
- export interface Attachment {
27
- id: number;
28
- kind: "image" | "text";
29
- name: string;
30
- mimeType?: string;
31
- data?: string;
32
- text?: string;
33
- truncated?: boolean;
34
- }
35
-
36
- const MAX_TEXT_BYTES = 256 * 1024;
37
- const TEXT_EXTS = /\.(txt|md|markdown|csv|tsv|json|jsonl|ya?ml|toml|ini|cfg|conf|log|xml|html?|css|scss|js|mjs|cjs|ts|tsx|jsx|py|rs|go|java|c|h|cpp|hpp|cc|cs|rb|php|sh|bash|zsh|fish|sql|swift|kt|lua|r|jl|tex|svg|env|gitignore|dockerfile|makefile)$/i;
38
-
39
- function isTextFile(f: File): boolean {
40
- if (f.type.startsWith("text/")) return true;
41
- if (/(json|xml|javascript|yaml|x-sh|x-toml|csv|markdown)/i.test(f.type)) return true;
42
- return TEXT_EXTS.test(f.name || "");
43
- }
44
- function readDataUrl(file: File): Promise<string> {
45
- return new Promise((res, rej) => {
46
- const r = new FileReader(); r.onerror = rej;
47
- r.onload = () => { const s = String(r.result); const i = s.indexOf(","); res(i >= 0 ? s.slice(i + 1) : s); };
48
- r.readAsDataURL(file);
49
- });
50
- }
51
- function readText(file: File): Promise<string> {
52
- return new Promise((res, rej) => { const r = new FileReader(); r.onerror = rej; r.onload = () => res(String(r.result)); r.readAsText(file); });
53
- }
54
-
55
- export class ComposerState {
56
- attachments: Attachment[] = [];
57
- visionCapable = false; // from the `ready` frame; gates image attachments
58
- transcriptionCapable = false; // from the `ready` frame; shows the hold-to-talk mic
59
- thinkingCapable = false; // from `ready`; gates the reasoning toggle
60
- // default on: with tools (read/web_search) the reasoning step gives the
61
- // small model far better tool judgment (answers math/writing itself,
62
- // only searches when needed). The thinking box defaults collapsed.
63
- thinkingOn = true;
64
- attachSeq = 0;
65
-
66
- // Per-request sampling. Each field is either a user override (a number)
67
- // or null = "use the recommended default" (resolved server-side).
68
- sampling: Required<SamplingOverrides> = {
69
- temperature: null, top_p: null, top_k: null, min_p: null,
70
- xtc_probability: null, xtc_threshold: null, repetition_penalty: null,
71
- repetition_context_size: null, presence_penalty: null, frequency_penalty: null,
72
- seed: null,
73
- };
74
- // Populated from the `ready` frame (ReadyGenDefaults); null entries fall
75
- // back to the built-in constants below until a ready frame arrives.
76
- genDefaults: ReadyGenDefaults = { temperature: null, topP: null, topK: null };
77
-
78
- // Per-message sampling scope (plan §9 Phase 3, beat matrix Axis 4): "session"
79
- // (default) behaves exactly as today — overrides persist until changed.
80
- // "next_turn" is a one-shot: the NEXT change the user makes is sent with
81
- // `scope:"next_turn"` and the server reverts to the prior session-level
82
- // sampling once that turn completes (turn_end). armedOneShot mirrors the
83
- // server's one-shot state locally purely for the indicator chip/dot — it's
84
- // cleared on turn_end (see clearOneShotArmed in chat.ts's handle()).
85
- samplingScope: "session" | "next_turn" = "session";
86
- oneShotArmed = false;
87
-
88
- // Per-chat custom system prompt (plan §9 Phase 2 item, beat matrix Axis 4),
89
- // layered onto the built-in surface prompt server-side (see pi-web.ts's
90
- // injectSystemPrompt/installSystemPromptHook). null/"" = none set.
91
- systemPrompt: string | null = null;
92
-
93
- recTemp(): number { return this.genDefaults.temperature != null ? this.genDefaults.temperature : (this.thinkingOn ? 0.9 : 0.7); }
94
- recTopP(): number { return this.genDefaults.topP != null ? this.genDefaults.topP : 0.95; }
95
- recTopK(): number { return this.genDefaults.topK != null ? this.genDefaults.topK : 0; }
96
- }
97
-
98
- export async function addFiles(state: ComposerState, files: File[]): Promise<void> {
99
- for (const f of files) {
100
- if (f.type.startsWith("image/")) {
101
- if (!state.visionCapable) { toast("This model can't see images — serve a vision-capable model (look for the vision tag in the Library).", "err"); continue; }
102
- try { state.attachments.push({ id: ++state.attachSeq, kind: "image", name: f.name || "image", mimeType: f.type, data: await readDataUrl(f) }); }
103
- catch { toast("Couldn't read " + (f.name || "image"), "err"); }
104
- } else if (isTextFile(f)) {
105
- try {
106
- let text = await readText(f); const truncated = text.length > MAX_TEXT_BYTES; if (truncated) text = text.slice(0, MAX_TEXT_BYTES);
107
- state.attachments.push({ id: ++state.attachSeq, kind: "text", name: f.name || "file", text, truncated });
108
- } catch { toast("Couldn't read " + (f.name || "file"), "err"); }
109
- } else {
110
- toast("Unsupported file type: " + (f.name || "file"), "err");
111
- }
112
- }
113
- renderAttachments(state);
114
- }
115
- export function renderAttachments(state: ComposerState): void {
116
- const box = $("chat-attach");
117
- box.innerHTML = "";
118
- box.style.display = state.attachments.length ? "flex" : "none";
119
- // Retrieval-mode tag (task brief item 4, transparency principle): whether
120
- // ANY text file currently gets RAG'd is a property of the WHOLE attached
121
- // set (buildMessageText sums all text-file chars together), so every text
122
- // chip shows the tag together, not per-file — a file isn't individually
123
- // "over threshold", the attachment set is.
124
- const totalTextChars = state.attachments.reduce((n, a) => n + (a.kind === "text" ? (a.text || "").length : 0), 0);
125
- const retrievalMode = shouldRetrieve(totalTextChars);
126
- for (const a of state.attachments) {
127
- const chip = el("div", "attach-chip", box);
128
- chip.dataset.attId = String(a.id); // mention-picker pulse target (composer.ts's initMentionPicker)
129
- if (a.kind === "image") { const im = el("img", "", chip); im.src = "data:" + a.mimeType + ";base64," + a.data; im.alt = a.name; }
130
- else el("span", "att-ico", chip).textContent = "📄";
131
- el("span", "att-name", chip).textContent = a.name + (a.truncated ? " (truncated)" : "");
132
- if (a.kind === "text" && retrievalMode) {
133
- const tag = el("span", "att-tag", chip);
134
- tag.textContent = "retrieval";
135
- tag.title = "This file is large enough that only the most relevant excerpts are sent, not the whole file";
136
- }
137
- const x = el("button", "att-x", chip); x.type = "button"; x.textContent = "✕";
138
- x.onclick = () => { state.attachments = state.attachments.filter((z) => z.id !== a.id); renderAttachments(state); };
139
- }
140
- }
141
- export function clearAttachments(state: ComposerState): void { state.attachments = []; renderAttachments(state); }
142
-
143
- /** Result of buildMessageText: the outgoing text the model sees, plus (when
144
- * retrieval mode fired) the citation list chat.ts needs to attach a Sources
145
- * panel to the just-sent turn. `citations` is empty for the common small-
146
- * attachment case — inline mode has nothing to cite, matching today's
147
- * behavior exactly (task brief item 2: "zero behavior change"). */
148
- export interface BuiltMessage {
149
- text: string;
150
- citations: Citation[];
151
- }
152
-
153
- /** Chat-with-files RAG v1 (plan §9 Phase 3, beat matrix Axis 5 — optiq
154
- * Lab's dependency-free BM25 bar). Size-aware retrieval over attached text
155
- * files, decided per send:
156
- * - total attached text <= INLINE_THRESHOLD_CHARS: inline every file
157
- * verbatim exactly as before this feature existed (LM Studio's
158
- * transparent "fits context -> just inline it" half of the dual mode).
159
- * - above threshold: retrieve the top-K chunks (src/web/src/rag.ts's
160
- * BM25 index) scored against the outgoing message text, and inject a
161
- * numbered [1]..[K] context block instead of the raw files. The index
162
- * is rebuilt fresh on every send (attachments can change between
163
- * turns) — cheap for the few-dozen-chunk scale this targets.
164
- * The user's visible message (what addUserMsg renders) is untouched
165
- * either way — the injection composes into the outgoing WS text only,
166
- * same as plain file-inlining does today. */
167
- export function buildMessageText(state: ComposerState, userText: string): BuiltMessage {
168
- const files = state.attachments.filter((a): a is Attachment & { text: string } => a.kind === "text" && a.text != null);
169
- if (!files.length) return { text: userText, citations: [] };
170
- const totalChars = files.reduce((n, a) => n + a.text.length, 0);
171
- if (!shouldRetrieve(totalChars)) {
172
- let pre = "";
173
- for (const a of files) pre += "Attached file: " + a.name + "\n```\n" + a.text + "\n```\n\n";
174
- return { text: pre + userText, citations: [] };
175
- }
176
- const citations = retrieve(files.map((a) => ({ id: a.id, name: a.name, text: a.text })), userText);
177
- if (!citations.length) return { text: userText, citations: [] }; // no term overlap at all — nothing useful to inject
178
- return { text: buildContextBlock(citations) + userText, citations };
179
- }
180
- export function updateAttachHint(state: ComposerState): void {
181
- const btn = $("chat-attach-btn");
182
- if (btn) btn.title = state.visionCapable ? "Attach files or images" : "Attach files (this model can't see images)";
183
- const mic = $("chat-mic-btn");
184
- if (mic) mic.style.display = state.transcriptionCapable ? "" : "none";
185
- }
186
-
187
- /* ────────────────────────────────────────────────────────────────────
188
- LoRA adapter selector
189
- ──────────────────────────────────────────────────────────────────── */
190
-
191
- export interface AdapterInfo {
192
- id: string;
193
- path: string;
194
- rank?: number;
195
- mounted?: boolean;
196
- compatible?: boolean;
197
- base_model?: string;
198
- }
199
-
200
- function escHtml(s: unknown): string {
201
- return String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" } as Record<string, string>)[c]!);
202
- }
203
-
204
- /** Pure: builds the <option> markup for the adapter <select> from the
205
- * /v1/adapters/available list. Split out from refreshAdapters() (which
206
- * does the fetch + DOM write) so tests/using/web-app.test.ts can exercise the
207
- * esc() discipline on this template directly without a DOM or network —
208
- * every interpolated field (id, rank, path, base_model) must be escaped,
209
- * since adapter ids/paths are user-controlled (on-disk directory names). */
210
- export function renderAdapterOptionsHtml(list: AdapterInfo[]): string {
211
- return '<option value="">no adapter</option>' + list.map((a) => {
212
- const label = escHtml(a.id) + (a.rank ? " · r" + escHtml(a.rank) : "") + (a.mounted ? " · mounted" : "");
213
- if (a.compatible) return `<option value="${escHtml(a.id)}" data-path="${escHtml(a.path)}">${label}</option>`;
214
- const why = a.base_model
215
- ? `trained for ${escHtml(a.base_model)}, not the currently-served model`
216
- : "not compatible with the currently-served model";
217
- return `<option value="${escHtml(a.id)}" disabled title="${why}">${label} (incompatible)</option>`;
218
- }).join("");
219
- }
220
-
221
- // — LoRA adapter selector. Pi-native: app.html lists + mounts adapters, then
222
- // tells pi-web which one is active; the before_provider_request hook injects
223
- // it into every provider request. Default "" = none (base model). Still a
224
- // single-select <select> this phase (the full three-state routing table is
225
- // Phase 2) — the presentation upgrade here is showing every adapter found
226
- // on disk (not just ones that fit the served model) with incompatible
227
- // entries grayed via the `compatible` flag /v1/adapters/available now
228
- // returns, each with a title explaining why it's unusable right now. —
229
- export async function refreshAdapters(): Promise<void> {
230
- const sel = $("chat-adapter") as HTMLSelectElement | null;
231
- if (!sel) return;
232
- let list: AdapterInfo[] = [];
233
- try { const d = await api("/v1/adapters/available"); list = (d as { adapters?: AdapterInfo[] }).adapters || []; } catch { /* leave list empty */ }
234
- const cur = sel.value;
235
- sel.innerHTML = renderAdapterOptionsHtml(list);
236
- if (cur && list.some((a) => a.id === cur)) sel.value = cur; // keep prior selection
237
- }
238
-
239
- export async function onSelectAdapter(sel: HTMLSelectElement, send: (obj: unknown) => boolean): Promise<void> {
240
- const id = sel.value;
241
- if (!id) { send({ type: "set_adapter", id: null }); return; } // none -> base model
242
- const opt = sel.options[sel.selectedIndex];
243
- const path = opt && opt.getAttribute("data-path");
244
- if (path) { // auto-load on select; mount() is idempotent server-side
245
- const r = await api("/v1/adapters", { method: "POST", body: { id, path } });
246
- if (r && (r.error || r.ok === false)) {
247
- toast("adapter: " + (r.error || "mount failed"), "err");
248
- sel.value = ""; send({ type: "set_adapter", id: null }); return;
249
- }
250
- }
251
- send({ type: "set_adapter", id });
252
- // Adapter-namespaced KV means a swap can never reuse the outgoing
253
- // adapter's cache — make that concrete instead of implicit (plan §5.2).
254
- toast("adapter mounted — new turns start a fresh KV segment", "ok");
255
- }
256
-
257
- export function updateThinkingToggle(state: ComposerState): void {
258
- const b = $("chat-think");
259
- if (!b) return;
260
- b.style.display = state.thinkingCapable ? "inline-flex" : "none"; // only for thinking-capable models
261
- b.classList.toggle("think-on", state.thinkingOn);
262
- b.title = state.thinkingOn ? "Reasoning on — click to answer directly" : "Reasoning off — click to let the model think";
263
- b.setAttribute("aria-pressed", String(state.thinkingOn));
264
- refreshSamplingRecs(state); // recommended temperature follows the thinking state
265
- }
266
-
267
- /* ────────────────────────────────────────────────────────────────────
268
- Sampling popover
269
- ──────────────────────────────────────────────────────────────────── */
270
-
271
- type SamplingField = keyof ComposerState["sampling"];
272
-
273
- interface SampFieldDef {
274
- id: string;
275
- field: SamplingField;
276
- valId: string;
277
- rec?: (state: ComposerState) => number;
278
- fmt: (v: number) => string;
279
- }
280
-
281
- // Each row is a slider + a live value readout. A field is "auto" (null
282
- // override -> the server uses its mode-aware default; temperature/top_p/top_k
283
- // recommendations come from genDefaults, resolved per-model) until the user
284
- // drags it, which records an explicit override. Reset returns every field to
285
- // auto. The three primary fields have a live "rec" (a meaningful recommended
286
- // number always shown dimmed); the Advanced fields have no natural default
287
- // number to display, so they just show "off" until overridden.
288
- const SAMP_FIELDS: SampFieldDef[] = [
289
- { id: "samp-temp", field: "temperature", valId: "samp-temp-val", rec: (s) => s.recTemp(), fmt: (v) => v.toFixed(2) },
290
- { id: "samp-topp", field: "top_p", valId: "samp-topp-val", rec: (s) => s.recTopP(), fmt: (v) => v.toFixed(2) },
291
- { id: "samp-topk", field: "top_k", valId: "samp-topk-val", rec: (s) => s.recTopK(), fmt: (v) => (v >= 1 ? String(Math.round(v)) : "off") },
292
- ];
293
- // Advanced fields: no meaningful "recommended" number — auto just reads "off".
294
- const SAMP_ADV_FIELDS: SampFieldDef[] = [
295
- { id: "samp-minp", field: "min_p", valId: "samp-minp-val", fmt: (v) => v.toFixed(2) },
296
- { id: "samp-xtcp", field: "xtc_probability", valId: "samp-xtcp-val", fmt: (v) => v.toFixed(2) },
297
- { id: "samp-xtct", field: "xtc_threshold", valId: "samp-xtct-val", fmt: (v) => v.toFixed(2) },
298
- { id: "samp-reppen", field: "repetition_penalty", valId: "samp-reppen-val", fmt: (v) => v.toFixed(2) },
299
- { id: "samp-repctx", field: "repetition_context_size", valId: "samp-repctx-val", fmt: (v) => String(Math.round(v)) },
300
- { id: "samp-prespen", field: "presence_penalty", valId: "samp-prespen-val", fmt: (v) => v.toFixed(2) },
301
- { id: "samp-freqpen", field: "frequency_penalty", valId: "samp-freqpen-val", fmt: (v) => v.toFixed(2) },
302
- ];
303
- const SAMP_SLIDER_FIELDS = SAMP_FIELDS.concat(SAMP_ADV_FIELDS);
304
-
305
- // Sync every slider + readout to current state: an overridden field shows its
306
- // set value (bright readout); an auto primary field shows the recommended
307
- // value (dimmed) and tracks it live — e.g. temperature when the thinking
308
- // toggle flips; an auto Advanced field just shows "off". Also syncs the seed
309
- // text input. Called on thinking-toggle flips, ready-frame arrival, popover
310
- // open, and reset.
311
- export function refreshSamplingRecs(state: ComposerState): void {
312
- for (const f of SAMP_FIELDS) {
313
- const slider = $(f.id) as HTMLInputElement | null, val = $(f.valId); if (!slider || !val) continue;
314
- const over = state.sampling[f.field];
315
- const v = over != null ? over : f.rec!(state);
316
- slider.value = String(v);
317
- val.textContent = f.fmt(v);
318
- val.classList.toggle("auto", over == null);
319
- }
320
- for (const f of SAMP_ADV_FIELDS) {
321
- const slider = $(f.id) as HTMLInputElement | null, val = $(f.valId); if (!slider || !val) continue;
322
- const over = state.sampling[f.field];
323
- slider.value = String(over != null ? over : slider.min);
324
- val.textContent = over != null ? f.fmt(over) : "off";
325
- val.classList.toggle("auto", over == null);
326
- }
327
- const seedInput = $("samp-seed") as HTMLInputElement | null;
328
- if (seedInput) seedInput.value = state.sampling.seed != null ? String(state.sampling.seed) : "";
329
- }
330
-
331
- // User dragged a slider -> record the explicit override, brighten the readout,
332
- // and push the whole sampling state to the server.
333
- function onSlide(state: ComposerState, f: SampFieldDef, send: (obj: unknown) => boolean): void {
334
- const slider = $(f.id) as HTMLInputElement | null; if (!slider) return;
335
- const v = Number(slider.value);
336
- (state.sampling[f.field] as number) = v;
337
- const val = $(f.valId); if (val) { val.textContent = f.fmt(v); val.classList.remove("auto"); }
338
- updateSamplingUi(state);
339
- pushSampling(state, send);
340
- }
341
- function onSeedInput(state: ComposerState, send: (obj: unknown) => boolean): void {
342
- const input = $("samp-seed") as HTMLInputElement | null; if (!input) return;
343
- const raw = input.value.trim();
344
- let seed: number | null = raw === "" ? null : Number(raw);
345
- if (seed != null && !Number.isFinite(seed)) seed = null;
346
- state.sampling.seed = seed;
347
- updateSamplingUi(state);
348
- pushSampling(state, send);
349
- }
350
- function pushSampling(state: ComposerState, send: (obj: unknown) => boolean): void {
351
- // Session mode omits `scope` entirely — identical wire shape to before
352
- // this feature existed ("session mode behaves exactly as today," task
353
- // brief item 2). Only next-turn mode adds the field, and only arms the
354
- // one-shot indicator once there's actually an override to apply for it
355
- // (sending scope:"next_turn" with every field null would arm a chip for
356
- // an override that doesn't exist).
357
- if (state.samplingScope === "next_turn") {
358
- send({ type: "set_sampling", scope: "next_turn", ...state.sampling });
359
- state.oneShotArmed = Object.values(state.sampling).some((v) => v != null);
360
- } else {
361
- send({ type: "set_sampling", ...state.sampling });
362
- state.oneShotArmed = false;
363
- }
364
- updateSamplingUi(state);
365
- }
366
-
367
- // The pill shows a lit dot whenever any field is overridden, and a distinct
368
- // "armed" dot state while a one-shot next-turn override is queued but hasn't
369
- // applied yet (task brief item 2: "the Sampling pill's dot shows a distinct
370
- // state while a one-shot override is armed").
371
- function updateSamplingUi(state: ComposerState): void {
372
- const dirty = Object.values(state.sampling).some((v) => v != null);
373
- const pill = $("chat-sampling");
374
- if (pill) {
375
- pill.classList.toggle("dirty", dirty && !state.oneShotArmed);
376
- pill.classList.toggle("armed", state.oneShotArmed);
377
- pill.title = state.oneShotArmed
378
- ? "Sampling override armed for your next message only"
379
- : dirty
380
- ? "Sampling overridden — click to edit or reset"
381
- : "Sampling controls (temperature · top_p · top_k · Advanced)";
382
- }
383
- const chip = $("samp-oneshot-chip");
384
- if (chip) chip.style.display = state.oneShotArmed ? "inline-flex" : "none";
385
- const scopeSelect = $("samp-scope") as HTMLSelectElement | null;
386
- if (scopeSelect && scopeSelect.value !== state.samplingScope) scopeSelect.value = state.samplingScope;
387
- }
388
-
389
- /** Called from chat.ts on every `turn_end` — the server applies a one-shot
390
- * next_turn override for exactly one turn then reverts, so the client-side
391
- * indicator must clear in lockstep rather than guessing a timeout. A no-op
392
- * when no one-shot was armed (the common case: session-mode sampling, or no
393
- * overrides set at all). Does NOT touch state.sampling — the server is the
394
- * source of truth for what the NEXT turn's session-level values revert to;
395
- * this only clears the local "armed" indicator and flips scope back to
396
- * session so the popover doesn't silently keep applying next-turn-only
397
- * semantics to values the user may tweak next without re-selecting scope. */
398
- export function clearOneShotArmed(state: ComposerState): void {
399
- if (!state.oneShotArmed) return;
400
- state.oneShotArmed = false;
401
- state.samplingScope = "session";
402
- updateSamplingUi(state);
403
- }
404
-
405
- export function resetSampling(state: ComposerState, send: (obj: unknown) => boolean): void {
406
- for (const k of Object.keys(state.sampling) as SamplingField[]) (state.sampling[k] as number | null) = null;
407
- state.samplingScope = "session";
408
- refreshSamplingRecs(state); updateSamplingUi(state);
409
- pushSampling(state, send);
410
- }
411
-
412
- export function initSampling(state: ComposerState, send: (obj: unknown) => boolean): void {
413
- const pill = $("chat-sampling"), pop = $("chat-sampling-pop");
414
- if (!pill || !pop) return;
415
- const trap: FocusTrap = trapFocus(pop, () => pop.classList.contains("open"));
416
- const setOpen = (open: boolean) => {
417
- const was = pop.classList.contains("open");
418
- if (open === was) return;
419
- if (open) trap.capture();
420
- pop.classList.toggle("open", open);
421
- pill.classList.toggle("on", open);
422
- pill.setAttribute("aria-expanded", String(open));
423
- if (open) refreshSamplingRecs(state);
424
- else trap.restore();
425
- };
426
- // Exposed so the global Escape sweep (closeTopOverlay) can close this
427
- // popover uniformly instead of keeping a second Escape listener.
428
- setSamplingPopoverClose(() => setOpen(false));
429
- const toggle = () => setOpen(!pop.classList.contains("open"));
430
- pill.onclick = (e) => { e.stopPropagation(); toggle(); };
431
- pill.addEventListener("keydown", (e) => { if ((e as KeyboardEvent).key === "Enter" || (e as KeyboardEvent).key === " ") { e.preventDefault(); toggle(); } });
432
- pop.addEventListener("click", (e) => e.stopPropagation());
433
- // Click-away closes the popover (Escape is handled globally, see
434
- // closeTopOverlay — kept out of this listener to avoid a second,
435
- // divergent Escape path).
436
- document.addEventListener("click", () => setOpen(false));
437
- for (const f of SAMP_SLIDER_FIELDS) {
438
- const e = $(f.id); if (!e) continue;
439
- e.addEventListener("input", () => onSlide(state, f, send));
440
- }
441
- const seedInput = $("samp-seed");
442
- if (seedInput) seedInput.addEventListener("input", () => onSeedInput(state, send));
443
- const reset = $("samp-reset"); if (reset) reset.onclick = (e) => { e.stopPropagation(); resetSampling(state, send); };
444
- // Scope control (task brief item 2): switching to "next message only"
445
- // doesn't itself send anything — it just changes what the NEXT slider
446
- // drag/seed edit means. Switching back to "this session" while a one-shot
447
- // was armed but not yet consumed just clears the local indicator; the
448
- // server-side revert already only ever happens at turn_end, so there's
449
- // nothing to un-arm server-side from a scope flip alone (no changed value
450
- // was ever sent for it to apply to session-persistently and no frame
451
- // corresponds to that alone).
452
- const scopeSelect = $("samp-scope") as HTMLSelectElement | null;
453
- if (scopeSelect) scopeSelect.addEventListener("change", (e) => {
454
- e.stopPropagation();
455
- state.samplingScope = scopeSelect.value === "next_turn" ? "next_turn" : "session";
456
- if (state.samplingScope === "session") { state.oneShotArmed = false; }
457
- updateSamplingUi(state);
458
- });
459
- updateSamplingUi(state); refreshSamplingRecs(state);
460
- }
461
-
462
- /* ────────────────────────────────────────────────────────────────────
463
- System prompt (plan §9 Phase 2, beat matrix Axis 4) + presets v1
464
- ──────────────────────────────────────────────────────────────────── */
465
-
466
- const SYS_PROMPT_MAX = 4000;
467
-
468
- /**
469
- * One saved preset bundle: a name, the system-prompt text, and a sampling
470
- * snapshot. Shaped so Phase 5's persona/bundle concept (system prompt +
471
- * adapter selection + tool allowlist + memory scope, plan §5.7/§6) is an
472
- * ADDITIVE extension of this same object — new optional fields, no
473
- * migration of the fields already here. Saved to localStorage, never sent
474
- * to the server (presets are a client-only convenience over the existing
475
- * set_system_prompt/set_sampling frames).
476
- */
477
- export interface Preset {
478
- name: string;
479
- systemPrompt: string | null;
480
- sampling: SamplingOverrides;
481
- // Phase 5 additive fields (not read/written by v1, documented here so the
482
- // shape doesn't need revisiting): adapterId?: string | null;
483
- // toolAllowlist?: string[]; memoryScope?: "off" | "read" | "read+write".
484
- }
485
-
486
- const PRESETS_KEY = "mlxbun.presets";
487
-
488
- /** Load saved presets from localStorage. Corrupt/missing storage -> empty
489
- * list (never throws — a settings-picker feature must never break the
490
- * composer it lives next to). */
491
- export function loadPresets(): Preset[] {
492
- try {
493
- const raw = localStorage.getItem(PRESETS_KEY);
494
- if (!raw) return [];
495
- const parsed = JSON.parse(raw);
496
- if (!Array.isArray(parsed)) return [];
497
- return parsed.filter((p): p is Preset =>
498
- !!p && typeof p === "object" && typeof (p as Preset).name === "string");
499
- } catch {
500
- return [];
501
- }
502
- }
503
-
504
- function savePresets(presets: Preset[]): void {
505
- try { localStorage.setItem(PRESETS_KEY, JSON.stringify(presets)); } catch { /* storage full/unavailable — best effort */ }
506
- }
507
-
508
- /** Build a preset snapshot from the current composer state. Pure. */
509
- export function presetFromState(name: string, state: ComposerState): Preset {
510
- return { name, systemPrompt: state.systemPrompt, sampling: { ...state.sampling } };
511
- }
512
-
513
- /** Insert-or-replace by name (case-sensitive), so re-saving "Concise" updates
514
- * it in place rather than piling up duplicates. Pure — returns the new
515
- * array, doesn't touch storage (callers persist via savePresets). */
516
- export function upsertPreset(presets: Preset[], preset: Preset): Preset[] {
517
- const i = presets.findIndex((p) => p.name === preset.name);
518
- if (i === -1) return [...presets, preset];
519
- const out = presets.slice();
520
- out[i] = preset;
521
- return out;
522
- }
523
-
524
- /** Remove a preset by name. Pure. */
525
- export function removePreset(presets: Preset[], name: string): Preset[] {
526
- return presets.filter((p) => p.name !== name);
527
- }
528
-
529
- /** Apply a preset's system prompt + sampling to the composer state and push
530
- * both through the EXISTING set_system_prompt/set_sampling frames (no new
531
- * wire shape for presets — they're a client-side convenience over frames
532
- * that already exist). */
533
- export function applyPreset(state: ComposerState, preset: Preset, send: (obj: unknown) => boolean): void {
534
- state.systemPrompt = preset.systemPrompt ?? null;
535
- state.sampling = {
536
- temperature: null, top_p: null, top_k: null, min_p: null,
537
- xtc_probability: null, xtc_threshold: null, repetition_penalty: null,
538
- repetition_context_size: null, presence_penalty: null, frequency_penalty: null,
539
- seed: null,
540
- ...preset.sampling,
541
- };
542
- refreshSystemPromptUi(state);
543
- pushSystemPrompt(state, send);
544
- refreshSamplingRecs(state);
545
- updateSamplingUi(state);
546
- pushSampling(state, send);
547
- }
548
-
549
- function pushSystemPrompt(state: ComposerState, send: (obj: unknown) => boolean): void {
550
- send({ type: "set_system_prompt", text: state.systemPrompt });
551
- }
552
-
553
- /** Sync the textarea, character count, and indicator pill to current state.
554
- * Called on popover open and after every edit/clear/preset-apply. */
555
- export function refreshSystemPromptUi(state: ComposerState): void {
556
- const ta = $("sysprompt-text") as HTMLTextAreaElement | null;
557
- const count = $("sysprompt-count");
558
- const text = state.systemPrompt ?? "";
559
- if (ta && ta.value !== text) ta.value = text;
560
- if (count) count.textContent = text.length + " / " + SYS_PROMPT_MAX;
561
- // The pill must always visibly show a custom prompt is shaping replies —
562
- // a trust rule, not a nicety (task brief item 2).
563
- const active = text.trim().length > 0;
564
- const pill = $("chat-sysprompt");
565
- if (pill) {
566
- pill.classList.toggle("dirty", active);
567
- pill.title = active
568
- ? "Custom system prompt active — click to edit or clear"
569
- : "System prompt (shapes how the assistant replies in this chat)";
570
- }
571
- }
572
-
573
- function onSystemPromptInput(state: ComposerState, send: (obj: unknown) => boolean): void {
574
- const ta = $("sysprompt-text") as HTMLTextAreaElement | null;
575
- if (!ta) return;
576
- const text = ta.value.slice(0, SYS_PROMPT_MAX);
577
- if (text !== ta.value) ta.value = text; // hard cap, mirrors the live count
578
- state.systemPrompt = text.length ? text : null;
579
- const count = $("sysprompt-count");
580
- if (count) count.textContent = text.length + " / " + SYS_PROMPT_MAX;
581
- const pill = $("chat-sysprompt");
582
- if (pill) {
583
- const active = text.trim().length > 0;
584
- pill.classList.toggle("dirty", active);
585
- }
586
- pushSystemPrompt(state, send);
587
- }
588
-
589
- export function clearSystemPrompt(state: ComposerState, send: (obj: unknown) => boolean): void {
590
- state.systemPrompt = null;
591
- refreshSystemPromptUi(state);
592
- pushSystemPrompt(state, send);
593
- }
594
-
595
- /** Pure: builds the <option> markup for the presets <select>. Split out for
596
- * the same reason renderAdapterOptionsHtml is — testable esc() discipline
597
- * without a DOM. Preset names are user-authored, so every interpolation
598
- * must be escaped. */
599
- export function renderPresetOptionsHtml(presets: Preset[]): string {
600
- return '<option value="">— presets —</option>' +
601
- presets.map((p) => `<option value="${escHtml(p.name)}">${escHtml(p.name)}</option>`).join("");
602
- }
603
-
604
- function renderPresetSelect(): void {
605
- const sel = $("sysprompt-preset-select") as HTMLSelectElement | null;
606
- if (!sel) return;
607
- const cur = sel.value;
608
- sel.innerHTML = renderPresetOptionsHtml(loadPresets());
609
- if (cur && [...sel.options].some((o) => o.value === cur)) sel.value = cur;
610
- }
611
-
612
- export function initSystemPrompt(state: ComposerState, send: (obj: unknown) => boolean): void {
613
- const pill = $("chat-sysprompt"), pop = $("chat-sysprompt-pop");
614
- if (!pill || !pop) return;
615
- const trap: FocusTrap = trapFocus(pop, () => pop.classList.contains("open"));
616
- const setOpen = (open: boolean) => {
617
- const was = pop.classList.contains("open");
618
- if (open === was) return;
619
- if (open) trap.capture();
620
- pop.classList.toggle("open", open);
621
- pill.classList.toggle("on", open);
622
- pill.setAttribute("aria-expanded", String(open));
623
- if (open) { refreshSystemPromptUi(state); renderPresetSelect(); }
624
- else trap.restore();
625
- };
626
- setSysPromptPopoverClose(() => setOpen(false));
627
- const toggle = () => setOpen(!pop.classList.contains("open"));
628
- pill.onclick = (e) => { e.stopPropagation(); toggle(); };
629
- pill.addEventListener("keydown", (e) => { if ((e as KeyboardEvent).key === "Enter" || (e as KeyboardEvent).key === " ") { e.preventDefault(); toggle(); } });
630
- pop.addEventListener("click", (e) => e.stopPropagation());
631
- document.addEventListener("click", () => setOpen(false));
632
-
633
- const ta = $("sysprompt-text");
634
- if (ta) ta.addEventListener("input", () => onSystemPromptInput(state, send));
635
- const clearBtn = $("sysprompt-clear");
636
- if (clearBtn) clearBtn.onclick = (e) => { e.stopPropagation(); clearSystemPrompt(state, send); };
637
-
638
- // — Presets v1: save-current / apply / delete over the select above. —
639
- const presetSelect = $("sysprompt-preset-select") as HTMLSelectElement | null;
640
- if (presetSelect) presetSelect.addEventListener("change", (e) => {
641
- e.stopPropagation();
642
- const name = presetSelect.value;
643
- if (!name) return;
644
- const preset = loadPresets().find((p) => p.name === name);
645
- if (preset) { applyPreset(state, preset, send); toast('Applied preset "' + name + '"', "ok"); }
646
- });
647
- const saveBtn = $("sysprompt-preset-save");
648
- if (saveBtn) saveBtn.onclick = (e) => {
649
- e.stopPropagation();
650
- const name = (window.prompt("Save current system prompt + sampling as a preset named:") || "").trim();
651
- if (!name) return;
652
- const presets = upsertPreset(loadPresets(), presetFromState(name, state));
653
- savePresets(presets);
654
- renderPresetSelect();
655
- if (presetSelect) presetSelect.value = name;
656
- toast('Saved preset "' + name + '"', "ok");
657
- };
658
- const delBtn = $("sysprompt-preset-delete");
659
- if (delBtn) delBtn.onclick = (e) => {
660
- e.stopPropagation();
661
- const name = presetSelect?.value;
662
- if (!name) { toast("Select a preset to delete first", "err"); return; }
663
- savePresets(removePreset(loadPresets(), name));
664
- renderPresetSelect();
665
- toast('Deleted preset "' + name + '"', "ok");
666
- };
667
-
668
- refreshSystemPromptUi(state);
669
- }
670
-
671
- /* ────────────────────────────────────────────────────────────────────
672
- Perf strip: tok/s, TTFT, context-fill, lane badge
673
- ──────────────────────────────────────────────────────────────────── */
674
-
675
- export function fmtTok(n: number | null | undefined): string {
676
- if (n == null) return "—";
677
- if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
678
- if (n >= 1e3) return (n / 1e3).toFixed(n >= 1e4 ? 0 : 1) + "k";
679
- return String(n);
680
- }
681
-
682
- export interface ContextFrame {
683
- tokens: number | null;
684
- contextWindow: number;
685
- percent: number | null;
686
- }
687
-
688
- export function renderContext(m: ContextFrame): void {
689
- const node = $("pf-fill");
690
- if (!node || !m.contextWindow) return;
691
- const pct = m.percent;
692
- const pctStr = pct == null ? "" : " (" + Math.round(pct) + "%)";
693
- node.textContent = "ctx " + fmtTok(m.tokens) + "/" + fmtTok(m.contextWindow) + pctStr;
694
- node.style.color = pct == null ? "var(--dimmer)" : pct >= 85 ? "var(--orange)" : pct >= 70 ? "var(--yellow)" : "var(--dimmer)";
695
- }
696
-
697
- // Lane badge (plan §5.2/§6.4, risk #5): server-driven only, from the
698
- // turn_end frame's optional `lane` field (src/serve/lane-registry.ts +
699
- // pi-web.ts). Absent on turns that never resolved a lane (aborted before
700
- // any model call) — the badge just stays hidden rather than guessing.
701
- const LANE_LABEL: Record<Lane, string> = { serial: "serial", "serial+spec": "spec-decode active", batched: "batched" };
702
- const LANE_CLASS: Record<Lane, string> = { serial: "serial", "serial+spec": "spec", batched: "batched" };
703
-
704
- export function renderLane(lane: Lane | undefined): void {
705
- const laneEl = $("pf-lane"), label = $("pf-lane-label");
706
- if (!laneEl || !label) return;
707
- if (!lane) { laneEl.style.display = "none"; return; }
708
- laneEl.className = "pf-lane " + (LANE_CLASS[lane] || "");
709
- label.textContent = LANE_LABEL[lane] || lane;
710
- laneEl.style.display = "inline-flex";
711
- }
712
-
713
- /* ────────────────────────────────────────────────────────────────────
714
- Unified "#" retrieval mention (plan §5.2/§9 Phase 2): typing "#" opens a
715
- picker over BOTH currently-attached files and vault articles (via
716
- /api/memory/search) as one retrieval gesture — Open WebUI's `#`-mention
717
- pattern, extended so "attach a file" and "search my memory" are the same
718
- interaction. Selecting a file pulses its existing attach chip (the file
719
- is already attached — nothing to insert); selecting an article inserts
720
- `[[Article Name]]` at the query span (the wikilink syntax
721
- src/memory/vault.ts's resolveWikilinkToStem/extractWikilinkTargets
722
- already resolves, so the agent's memory tools pick it up naturally, same
723
- as in the vault's own prose). No vault -> files-only picker. Pure
724
- query-detection/insertion helpers below are unit-tested directly
725
- (tests/using/web-app.test.ts); DOM wiring (initMentionPicker) owns the
726
- picker's open/close/keyboard-nav state.
727
- ──────────────────────────────────────────────────────────────────── */
728
-
729
- export interface MentionQuery {
730
- /** Index (in the textarea value) where the "#" itself sits. */
731
- hashIndex: number;
732
- /** The text typed after "#", up to the caret (no spaces — a space ends
733
- * the mention span, matching Open WebUI/Slack-style "#" pickers). */
734
- query: string;
735
- }
736
-
737
- /** Pure: does the caret currently sit inside an open "#mention" span? Scans
738
- * backward from the caret for a "#" that isn't preceded by a word
739
- * character (so "C#" or "issue#12" don't trigger it) and has no whitespace
740
- * between it and the caret. Returns null when there's no open mention. */
741
- export function detectMentionQuery(text: string, caret: number): MentionQuery | null {
742
- if (caret < 0 || caret > text.length) return null;
743
- let i = caret - 1;
744
- while (i >= 0) {
745
- const ch = text[i]!;
746
- if (ch === "#") {
747
- const before = i > 0 ? text[i - 1]! : "";
748
- if (/[A-Za-z0-9_]/.test(before)) return null; // "C#", "issue#12" — not a mention
749
- return { hashIndex: i, query: text.slice(i + 1, caret) };
750
- }
751
- if (/\s/.test(ch)) return null; // whitespace ends the mention span
752
- i--;
753
- }
754
- return null;
755
- }
756
-
757
- export type MentionItem =
758
- | { kind: "file"; id: number; name: string }
759
- | { kind: "article"; name: string; excerpt?: string };
760
-
761
- const MAX_MENTION_FILE_RESULTS = 6;
762
- const MAX_MENTION_ARTICLE_RESULTS = 8;
763
-
764
- /** Pure: filters currently-attached files by the mention query (case-
765
- * insensitive substring on name; empty query matches everything, so
766
- * opening the picker with just "#" shows all attachments first). */
767
- export function filterFileMentions(attachments: Attachment[], query: string): MentionItem[] {
768
- const q = query.trim().toLowerCase();
769
- const matches = attachments.filter((a) => !q || a.name.toLowerCase().includes(q));
770
- return matches.slice(0, MAX_MENTION_FILE_RESULTS).map((a) => ({ kind: "file", id: a.id, name: a.name }));
771
- }
772
-
773
- /** Pure: combines file matches (always first — already-attached context is
774
- * the cheapest/most relevant hit) with article search hits into one flat
775
- * list for keyboard nav. Article list is capped independently of files so
776
- * a vault with many hits doesn't crowd out attachments. */
777
- export function buildMentionItems(fileMatches: MentionItem[], articleNames: string[]): MentionItem[] {
778
- const articles: MentionItem[] = articleNames.slice(0, MAX_MENTION_ARTICLE_RESULTS).map((name) => ({ kind: "article", name }));
779
- return [...fileMatches, ...articles];
780
- }
781
-
782
- /** Pure: replaces the open "#query" span with the mention's insertion text
783
- * and returns the new full text + where the caret should land. Files
784
- * insert nothing (the file is already attached — the span is just
785
- * deleted); articles insert `[[Name]] ` (trailing space so typing
786
- * continues naturally, matching resolveWikilinkToStem's bracket syntax). */
787
- export function applyMention(text: string, q: MentionQuery, item: MentionItem, caret: number): { text: string; caret: number } {
788
- const insertion = item.kind === "article" ? "[[" + item.name + "]] " : "";
789
- const before = text.slice(0, q.hashIndex);
790
- const after = text.slice(caret);
791
- const newText = before + insertion + after;
792
- return { text: newText, caret: before.length + insertion.length };
793
- }
794
-
795
- function escMention(s: unknown): string { return escHtml(s); }
796
-
797
- /** Pure: builds the picker's inner HTML for a given item list + selected
798
- * index. Split out for esc()-discipline testing without a DOM, same
799
- * reasoning as renderAdapterOptionsHtml/renderPresetOptionsHtml above —
800
- * file/article names are user/vault data and must be escaped. */
801
- export function renderMentionListHtml(items: MentionItem[], selected: number, memoryEnabled: boolean): string {
802
- if (!items.length) {
803
- return '<div class="mention-empty">' +
804
- (memoryEnabled ? "No matching files or memory articles." : "No matching attached files.") +
805
- "</div>";
806
- }
807
- let filesHtml = "", articlesHtml = "";
808
- items.forEach((item, i) => {
809
- const active = i === selected ? " active" : "";
810
- const row = item.kind === "file"
811
- ? '<div class="mention-row' + active + '" data-idx="' + i + '" role="option">' +
812
- '<span class="mention-ico" aria-hidden="true">📄</span>' +
813
- '<span class="mention-label">' + escMention(item.name) + "</span>" +
814
- '<span class="mention-tag">attached</span></div>'
815
- : '<div class="mention-row' + active + '" data-idx="' + i + '" role="option">' +
816
- '<span class="mention-ico" aria-hidden="true">◆</span>' +
817
- '<span class="mention-label">' + escMention(item.name.replace(/_/g, " ")) + "</span>" +
818
- '<span class="mention-tag">memory</span></div>';
819
- if (item.kind === "file") filesHtml += row; else articlesHtml += row;
820
- });
821
- const sections: string[] = [];
822
- if (filesHtml) sections.push('<div class="mention-sec-title">Attached files</div>' + filesHtml);
823
- if (articlesHtml) sections.push('<div class="mention-sec-title">Memory</div>' + articlesHtml);
824
- return sections.join("");
825
- }
826
-
827
- type MentionSearchResp = ApiEnvelope & { summaries?: { article: string }[]; enabled?: boolean };
828
-
829
- /** DOM wiring for the picker: opens on "#", filters attachments locally +
830
- * debounces a /api/memory/search round trip, keyboard nav (arrows/Enter/
831
- * Escape), click-to-select, and returns focus to the textarea at the
832
- * right caret position on every path (selection, Escape, click-away). */
833
- export function initMentionPicker(state: ComposerState, box: HTMLTextAreaElement, opts: { onFileSelected: (a: Attachment) => void }): void {
834
- const pop = $("chat-mention-pop");
835
- if (!pop) return;
836
- let items: MentionItem[] = [];
837
- let selected = 0;
838
- let activeQuery: MentionQuery | null = null;
839
- let memoryEnabled = true; // optimistic; a failed/empty search just yields no article rows
840
- let debounce: ReturnType<typeof setTimeout> | undefined;
841
- let searchSeq = 0;
842
-
843
- function render(): void {
844
- pop.innerHTML = renderMentionListHtml(items, selected, memoryEnabled);
845
- pop.querySelectorAll<HTMLElement>(".mention-row").forEach((row) => {
846
- row.addEventListener("mousedown", (e) => { e.preventDefault(); const idx = Number(row.dataset.idx); if (Number.isFinite(idx)) select(idx); });
847
- });
848
- }
849
-
850
- function close(): void {
851
- pop.classList.remove("open");
852
- activeQuery = null;
853
- items = [];
854
- selected = 0;
855
- }
856
-
857
- function select(idx: number): void {
858
- const item = items[idx];
859
- if (!item || !activeQuery) { close(); return; }
860
- if (item.kind === "file") {
861
- const att = state.attachments.find((a) => a.id === item.id);
862
- const q = activeQuery;
863
- // File mentions don't rewrite the text — the file is already attached
864
- // context — just clear the typed "#query" span and pulse its chip.
865
- const before = box.value.slice(0, q.hashIndex);
866
- const after = box.value.slice(box.selectionStart ?? box.value.length);
867
- box.value = before + after;
868
- box.selectionStart = box.selectionEnd = before.length;
869
- if (att) opts.onFileSelected(att);
870
- } else {
871
- const q = activeQuery;
872
- const caret = box.selectionStart ?? box.value.length;
873
- const result = applyMention(box.value, q, item, caret);
874
- box.value = result.text;
875
- box.selectionStart = box.selectionEnd = result.caret;
876
- }
877
- close();
878
- box.focus();
879
- box.dispatchEvent(new Event("input")); // resize + any other input-driven UI stays in sync
880
- }
881
-
882
- async function runSearch(query: string): Promise<void> {
883
- const seq = ++searchSeq;
884
- if (!query.trim()) { updateItems([]); return; }
885
- try {
886
- const d = await api<MentionSearchResp>("/api/memory/search?q=" + encodeURIComponent(query));
887
- if (seq !== searchSeq) return; // stale response — a newer keystroke already superseded it
888
- // enabled:false is the REST layer's explicit "no vault yet" signal
889
- // (src/memory/rest.ts's noVault()) — only THAT latches the picker into
890
- // files-only mode; any other failure (a transient search error) just
891
- // yields an empty article list for this keystroke, not a permanent
892
- // downgrade for the rest of the picker session.
893
- if (d.enabled === false) { memoryEnabled = false; updateItems([]); return; }
894
- if (d.ok === false) { updateItems([]); return; }
895
- updateItems((d.summaries || []).map((s) => s.article));
896
- } catch {
897
- if (seq !== searchSeq) return;
898
- updateItems([]);
899
- }
900
- }
901
-
902
- function updateItems(articleNames: string[]): void {
903
- if (!activeQuery) return;
904
- const fileMatches = filterFileMentions(state.attachments, activeQuery.query);
905
- items = buildMentionItems(fileMatches, articleNames);
906
- selected = 0;
907
- render();
908
- }
909
-
910
- function openFor(q: MentionQuery): void {
911
- activeQuery = q;
912
- pop.classList.add("open");
913
- const fileMatches = filterFileMentions(state.attachments, q.query);
914
- items = buildMentionItems(fileMatches, []);
915
- selected = 0;
916
- render();
917
- clearTimeout(debounce);
918
- debounce = setTimeout(() => runSearch(q.query), 180);
919
- }
920
-
921
- box.addEventListener("input", () => {
922
- const caret = box.selectionStart ?? box.value.length;
923
- const q = detectMentionQuery(box.value, caret);
924
- if (!q) { close(); return; }
925
- if (!activeQuery || activeQuery.hashIndex !== q.hashIndex) { openFor(q); return; }
926
- activeQuery = q;
927
- const fileMatches = filterFileMentions(state.attachments, q.query);
928
- items = buildMentionItems(fileMatches, items.filter((i) => i.kind === "article").map((i) => (i as { name: string }).name));
929
- selected = 0;
930
- render();
931
- clearTimeout(debounce);
932
- debounce = setTimeout(() => runSearch(q.query), 180);
933
- });
934
-
935
- box.addEventListener("keydown", (e) => {
936
- if (!pop.classList.contains("open") || !items.length) return;
937
- if (e.key === "ArrowDown") { e.preventDefault(); selected = Math.min(selected + 1, items.length - 1); render(); }
938
- else if (e.key === "ArrowUp") { e.preventDefault(); selected = Math.max(selected - 1, 0); render(); }
939
- else if (e.key === "Enter" || e.key === "Tab") { e.preventDefault(); select(selected); }
940
- else if (e.key === "Escape") { e.preventDefault(); close(); }
941
- });
942
-
943
- box.addEventListener("blur", () => {
944
- // Delay so a click on a .mention-row (which also blurs the textarea)
945
- // still registers via its mousedown handler before we tear the popover
946
- // down — mousedown fires before blur, but keep this defensive.
947
- setTimeout(() => { if (document.activeElement !== box) close(); }, 120);
948
- });
949
- }
950
-
951
- export function renderQueue(q: { steering?: readonly string[]; followUp?: readonly string[] }): void {
952
- const bar = $("chat-queue");
953
- const parts: string[] = [];
954
- // steering/followUp are arrays (pi-web queue_update) — an empty array is
955
- // truthy, so gate on .length; show the latest steering note and one pill
956
- // per queued follow-up.
957
- const steering = q.steering || [];
958
- if (steering.length) parts.push('<span class="qtag">steering: <b>' + escHtml(String(steering[steering.length - 1]).slice(0, 60)) + "</b></span>");
959
- for (const f of q.followUp || []) parts.push('<span class="qtag">queued: <b>' + escHtml(String(f).slice(0, 60)) + "</b></span>");
960
- bar.innerHTML = parts.join("");
961
- bar.style.display = parts.length ? "flex" : "none";
962
- }