mlx-bun 0.4.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 (601) hide show
  1. package/README.md +1453 -88
  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 +10 -8
  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 +12 -10
  78. package/src/{serve → server}/chat-stage.ts +27 -22
  79. package/src/{client.ts → server/client.ts} +28 -2
  80. package/src/{serve → server}/completion-executor.ts +42 -42
  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 +42 -83
  85. package/src/server/generated-token-history.ts +57 -0
  86. package/src/{serve → server}/http.ts +25 -13
  87. package/src/{serve → server}/inference-request.ts +5 -5
  88. package/src/server/job-routes.ts +20 -0
  89. package/src/server/media-prompt.ts +63 -0
  90. package/src/server/memory-completion-client.ts +46 -0
  91. package/src/server/model-routes.ts +192 -0
  92. package/src/{serve → server}/openai-wire.ts +3 -2
  93. package/src/{serve → server}/pipeline.ts +1 -1
  94. package/src/server/prompt-builder.ts +13 -0
  95. package/src/server/prompt-contracts.ts +17 -0
  96. package/src/server/proxy-routes.ts +187 -0
  97. package/src/server/publishing-routes.ts +41 -0
  98. package/src/{serve → server}/request-plan.ts +8 -8
  99. package/src/{serve → server}/request-prep.ts +82 -68
  100. package/src/{serve → server}/responses-client.ts +30 -13
  101. package/src/{responses.ts → server/responses.ts} +12 -93
  102. package/src/server/routes.ts +205 -0
  103. package/src/server/start.ts +72 -0
  104. package/src/server/status-routes.ts +108 -0
  105. package/src/{serve → server}/text-completion-stage.ts +25 -4
  106. package/src/server/text-prompt.ts +12 -0
  107. package/src/{serve → server}/token-streams.ts +36 -38
  108. package/src/server/worker-routes.ts +311 -0
  109. package/src/storage/paths.ts +61 -0
  110. package/src/web/assets.ts +37 -0
  111. package/src/web/{src → browser}/api.ts +5 -23
  112. package/src/web/browser/chat-host.ts +17 -0
  113. package/src/web/browser/installed-panels.ts +6 -0
  114. package/src/web/browser/main.ts +55 -0
  115. package/src/web/browser/markdown.ts +279 -0
  116. package/src/web/browser/memory-panel.ts +9 -0
  117. package/src/web/browser/palette.ts +64 -0
  118. package/src/web/browser/protocol.ts +13 -0
  119. package/src/web/browser/shell.ts +467 -0
  120. package/src/web/{src → browser}/status.ts +32 -36
  121. package/src/web/build.ts +51 -0
  122. package/src/web/public/app.html +1054 -0
  123. package/src/web/{sw.js → public/sw.js} +4 -3
  124. package/src/web/{vendor → public/vendor}/hljs-theme.css +2 -2
  125. package/src/web/{vendor → public/vendor}/hljs.js +2 -2
  126. package/THIRD_PARTY_LICENSES.md +0 -51
  127. package/docs/README.md +0 -94
  128. package/docs/archive/hlg-sampling.md +0 -350
  129. package/docs/archive/investigations/audio-input-plan.md +0 -356
  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/web-ui-pass-plan.md +0 -213
  174. package/docs/archive/planning/website-readme-pass-plan.md +0 -224
  175. package/docs/design/batching.md +0 -1841
  176. package/docs/design/decode-speed-program.md +0 -2349
  177. package/docs/design/dreaming-nightly-pipeline.md +0 -684
  178. package/docs/design/generic-model-support.md +0 -705
  179. package/docs/design/kv-cache.md +0 -824
  180. package/docs/design/orpo-training.md +0 -680
  181. package/docs/design/speculative-decoding.md +0 -1990
  182. package/docs/design/turboquant.md +0 -2212
  183. package/docs/design/unified-engine-frontier-plan.md +0 -1880
  184. package/docs/design/web-chat-redesign.md +0 -695
  185. package/docs/planning/PRODUCT_ROADMAP.md +0 -594
  186. package/docs/planning/ResearchTopics.md +0 -119
  187. package/docs/planning/release-notes-v0.4.0.md +0 -45
  188. package/docs/planning/video-series.md +0 -421
  189. package/docs/reference/benchmarks.md +0 -3613
  190. package/docs/reference/cli.md +0 -585
  191. package/docs/reference/distribution.md +0 -355
  192. package/docs/reference/environment.md +0 -398
  193. package/docs/reference/glossary.md +0 -39
  194. package/docs/reference/library-api.md +0 -674
  195. package/docs/reference/memory.md +0 -260
  196. package/docs/reference/models.md +0 -334
  197. package/docs/reference/server-api.md +0 -1775
  198. package/docs/reference/server-config.md +0 -846
  199. package/docs/reference/training.md +0 -462
  200. package/docs/reference/troubleshooting.md +0 -158
  201. package/src/admission-plan.ts +0 -18
  202. package/src/audio/conformer.ts +0 -647
  203. package/src/audio/decode.ts +0 -133
  204. package/src/audio/features.ts +0 -202
  205. package/src/audio/transcode.ts +0 -67
  206. package/src/backends/mlx/assistant-target.ts +0 -46
  207. package/src/backends/mlx/autoregressive-draft-rows.ts +0 -120
  208. package/src/backends/mlx/autoregressive.ts +0 -108
  209. package/src/backends/mlx/batch-group.ts +0 -1790
  210. package/src/backends/mlx/cache-attachment.ts +0 -67
  211. package/src/backends/mlx/cache-layout.ts +0 -31
  212. package/src/backends/mlx/checkpoint-state.ts +0 -53
  213. package/src/backends/mlx/continuation-persistence.ts +0 -106
  214. package/src/backends/mlx/continuation-request.ts +0 -43
  215. package/src/backends/mlx/continuation.ts +0 -77
  216. package/src/backends/mlx/diffusion.ts +0 -39
  217. package/src/backends/mlx/draft-target.ts +0 -32
  218. package/src/backends/mlx/evaluation.ts +0 -105
  219. package/src/backends/mlx/fill-append.ts +0 -45
  220. package/src/backends/mlx/full-kv-row-append.ts +0 -32
  221. package/src/backends/mlx/full-kv-row-donor.ts +0 -54
  222. package/src/backends/mlx/full-transitioning-kv-rows.ts +0 -65
  223. package/src/backends/mlx/gateway-binding.ts +0 -130
  224. package/src/backends/mlx/graph.ts +0 -38
  225. package/src/backends/mlx/kv-codec.ts +0 -9
  226. package/src/backends/mlx/kv-maintenance.ts +0 -133
  227. package/src/backends/mlx/kv-tensor-rows.ts +0 -212
  228. package/src/backends/mlx/memory-client.ts +0 -189
  229. package/src/backends/mlx/methods.ts +0 -90
  230. package/src/backends/mlx/model-prompt.ts +0 -164
  231. package/src/backends/mlx/model-serving.ts +0 -79
  232. package/src/backends/mlx/prefill-cohort.ts +0 -89
  233. package/src/backends/mlx/prefill-rows.ts +0 -200
  234. package/src/backends/mlx/prefill.ts +0 -33
  235. package/src/backends/mlx/projected-context-rows.ts +0 -72
  236. package/src/backends/mlx/request-policy.ts +0 -20
  237. package/src/backends/mlx/request-state-policy.ts +0 -41
  238. package/src/backends/mlx/rollback.ts +0 -89
  239. package/src/backends/mlx/row-sampling.ts +0 -49
  240. package/src/backends/mlx/serial-executor.ts +0 -242
  241. package/src/backends/mlx/speculative-group.ts +0 -359
  242. package/src/backends/mlx/speculative-round.ts +0 -161
  243. package/src/backends/mlx/speculative.ts +0 -114
  244. package/src/backends/mlx/state-rows.ts +0 -104
  245. package/src/backends/mlx/state-views.ts +0 -46
  246. package/src/backends/mlx/target-layout-capability.ts +0 -35
  247. package/src/backends/mlx/text-engine.ts +0 -73
  248. package/src/backends/mlx/transitioning-kv-rows.ts +0 -165
  249. package/src/chat-template.ts +0 -382
  250. package/src/cli.ts +0 -3168
  251. package/src/config.ts +0 -399
  252. package/src/contracts/completion.ts +0 -16
  253. package/src/contracts/execution.ts +0 -60
  254. package/src/contracts/generation.ts +0 -103
  255. package/src/contracts/host.ts +0 -7
  256. package/src/contracts/pi-provider.ts +0 -30
  257. package/src/contracts/pi.ts +0 -259
  258. package/src/contracts/prefix-cache.ts +0 -23
  259. package/src/contracts/resources.ts +0 -25
  260. package/src/contracts/sampling.ts +0 -23
  261. package/src/contracts/scheduling.ts +0 -45
  262. package/src/contracts/tsconfig.json +0 -14
  263. package/src/dataset/generators.ts +0 -1087
  264. package/src/dataset/index.ts +0 -12
  265. package/src/dataset/job.ts +0 -81
  266. package/src/dataset/llm.ts +0 -71
  267. package/src/dataset/registry.ts +0 -528
  268. package/src/diffusion/diffusion-generate.ts +0 -507
  269. package/src/download.ts +0 -505
  270. package/src/embed.ts +0 -77
  271. package/src/engine/admission.ts +0 -59
  272. package/src/engine/cancellation.ts +0 -38
  273. package/src/engine/engine.ts +0 -56
  274. package/src/engine/execution-coordinator.ts +0 -70
  275. package/src/engine/execution-plan.ts +0 -53
  276. package/src/engine/resources.ts +0 -53
  277. package/src/engine/scheduler.ts +0 -72
  278. package/src/engine/session.ts +0 -247
  279. package/src/eval/README.md +0 -130
  280. package/src/eval/ifeval.ts +0 -81
  281. package/src/eval/kl-prompts.ts +0 -29
  282. package/src/eval/kl.ts +0 -590
  283. package/src/eval/perplexity.ts +0 -164
  284. package/src/eval/quality-db.ts +0 -92
  285. package/src/eval/reference-logits.ts +0 -207
  286. package/src/eval/runner.ts +0 -124
  287. package/src/eval/score.ts +0 -49
  288. package/src/eval/tasks/bfcl.ts +0 -535
  289. package/src/eval/tasks/gsm8k.ts +0 -98
  290. package/src/eval/tasks/hashhop.ts +0 -139
  291. package/src/eval/tasks/humaneval.ts +0 -275
  292. package/src/eval/tasks/ifeval.ts +0 -525
  293. package/src/eval/tasks/mmlu.ts +0 -157
  294. package/src/eval/ultrafeedback.ts +0 -60
  295. package/src/evaldb.ts +0 -199
  296. package/src/expert-io.ts +0 -393
  297. package/src/expert-offload-build.ts +0 -77
  298. package/src/expert-offload.ts +0 -76
  299. package/src/expert-residency.ts +0 -1134
  300. package/src/expert-usage.ts +0 -415
  301. package/src/fill/echo-index.ts +0 -188
  302. package/src/fill/fill-session.ts +0 -452
  303. package/src/fill/proposal.ts +0 -68
  304. package/src/fill/schema-rows.ts +0 -506
  305. package/src/fill/tool-boundary.ts +0 -103
  306. package/src/fit.ts +0 -249
  307. package/src/generate.ts +0 -1346
  308. package/src/grammar.ts +0 -591
  309. package/src/harness-pi.ts +0 -184
  310. package/src/hf-push.ts +0 -628
  311. package/src/hub-rest.ts +0 -242
  312. package/src/index.ts +0 -63
  313. package/src/inference/denoising.ts +0 -17
  314. package/src/inference/draft-acceptance.ts +0 -41
  315. package/src/inference/draft-output.ts +0 -49
  316. package/src/inference/graph.ts +0 -33
  317. package/src/inference/prefill.ts +0 -39
  318. package/src/inference/rollback.ts +0 -7
  319. package/src/jobs/index.ts +0 -31
  320. package/src/jobs/job-entry.ts +0 -107
  321. package/src/jobs/task-client.ts +0 -19
  322. package/src/jobs/types.ts +0 -2
  323. package/src/kv-scheme.ts +0 -257
  324. package/src/kv-store.ts +0 -1099
  325. package/src/lab/README.md +0 -18
  326. package/src/lab/curve/curve-designer.html +0 -303
  327. package/src/lab/curve/curve-sampler.ts +0 -122
  328. package/src/lab/expert-trace/expert-trace.ts +0 -69
  329. package/src/lab/paged-kv/paged-kv-rows.ts +0 -83
  330. package/src/lab/paged-kv/paged-kv.ts +0 -270
  331. package/src/library.ts +0 -40
  332. package/src/lora.ts +0 -408
  333. package/src/md-modules.d.ts +0 -49
  334. package/src/media-fetch.ts +0 -442
  335. package/src/memory/article.ts +0 -558
  336. package/src/memory/chunk-validation.ts +0 -150
  337. package/src/memory/chunk.ts +0 -318
  338. package/src/memory/cluster.ts +0 -351
  339. package/src/memory/coverage.ts +0 -134
  340. package/src/memory/crosslink.ts +0 -0
  341. package/src/memory/db.ts +0 -253
  342. package/src/memory/entity.ts +0 -203
  343. package/src/memory/gate.ts +0 -187
  344. package/src/memory/history.ts +0 -152
  345. package/src/memory/ingest.ts +0 -335
  346. package/src/memory/model.ts +0 -183
  347. package/src/memory/normalize.ts +0 -671
  348. package/src/memory/parse.ts +0 -22
  349. package/src/memory/pipeline.ts +0 -275
  350. package/src/memory/prompts.ts +0 -65
  351. package/src/memory/query.ts +0 -334
  352. package/src/memory/reconcile.ts +0 -734
  353. package/src/memory/redirect.ts +0 -108
  354. package/src/memory/reindex.ts +0 -325
  355. package/src/memory/resolve.ts +0 -307
  356. package/src/memory/rest.ts +0 -245
  357. package/src/memory/route.ts +0 -272
  358. package/src/memory/schedule.ts +0 -165
  359. package/src/memory/stages.ts +0 -777
  360. package/src/memory/synthesize.ts +0 -1638
  361. package/src/memory/tools.ts +0 -749
  362. package/src/memory/vault.ts +0 -1194
  363. package/src/memory/wikify.ts +0 -1064
  364. package/src/mlx/array.ts +0 -388
  365. package/src/mlx/autograd.ts +0 -304
  366. package/src/mlx/checkpoint.ts +0 -149
  367. package/src/mlx/compile.ts +0 -168
  368. package/src/mlx/custom-vjp.ts +0 -188
  369. package/src/mlx/ffi.ts +0 -512
  370. package/src/mlx/materialize.ts +0 -23
  371. package/src/mlx/metal-kernel.ts +0 -159
  372. package/src/mlx/ops.ts +0 -1021
  373. package/src/mlx/quantized-rows.ts +0 -36
  374. package/src/mlx/token-bitmask.ts +0 -52
  375. package/src/mlx/turboquant-kv-decode.ts +0 -82
  376. package/src/mlx/turboquant-kv-inverse.ts +0 -122
  377. package/src/mlx/turboquant-ops.ts +0 -861
  378. package/src/mlx/turboquant-tables.ts +0 -237
  379. package/src/mmap.ts +0 -90
  380. package/src/model/artifact-identity.ts +0 -73
  381. package/src/model/batched-kv.ts +0 -235
  382. package/src/model/batched-mask.ts +0 -279
  383. package/src/model/batched-quant.ts +0 -212
  384. package/src/model/batched-quantized-kv.ts +0 -144
  385. package/src/model/batched-rotating-quant.ts +0 -353
  386. package/src/model/batched-rotating-state.ts +0 -149
  387. package/src/model/batched-rotating.ts +0 -338
  388. package/src/model/batched-row-storage.ts +0 -179
  389. package/src/model/batched-ssm.ts +0 -52
  390. package/src/model/batched-turboquant-kv.ts +0 -93
  391. package/src/model/compiled-decode.ts +0 -712
  392. package/src/model/decoded-kv-donor.ts +0 -12
  393. package/src/model/delayed-quantized-kv.ts +0 -72
  394. package/src/model/delayed-rotating-quantized-kv.ts +0 -93
  395. package/src/model/delayed-turboquant-kv.ts +0 -55
  396. package/src/model/diffusion-gemma.ts +0 -1026
  397. package/src/model/factory.ts +0 -189
  398. package/src/model/fingerprint.ts +0 -33
  399. package/src/model/flash-attention.ts +0 -694
  400. package/src/model/full-prefill-padding.ts +0 -56
  401. package/src/model/full-prefill-row.ts +0 -105
  402. package/src/model/gemma4-base.ts +0 -2351
  403. package/src/model/gemma4.ts +0 -1315
  404. package/src/model/generated/gemma4-12b.ts +0 -429
  405. package/src/model/generated/gemma4-26b.ts +0 -448
  406. package/src/model/generated/gemma4-e4b.ts +0 -717
  407. package/src/model/generated/index.ts +0 -18
  408. package/src/model/glm52-atlas.ts +0 -511
  409. package/src/model/glm52-cache.ts +0 -792
  410. package/src/model/glm52-config.ts +0 -262
  411. package/src/model/glm52-container.ts +0 -305
  412. package/src/model/glm52-coupling.ts +0 -388
  413. package/src/model/glm52-dsa.ts +0 -606
  414. package/src/model/glm52-expert-layout.ts +0 -253
  415. package/src/model/glm52-layout.ts +0 -152
  416. package/src/model/glm52-memory.ts +0 -500
  417. package/src/model/glm52-mla.ts +0 -792
  418. package/src/model/glm52-moe.ts +0 -223
  419. package/src/model/glm52-pilot.ts +0 -537
  420. package/src/model/glm52-reference.ts +0 -445
  421. package/src/model/glm52-residency.ts +0 -557
  422. package/src/model/glm52-resident-weights.ts +0 -135
  423. package/src/model/glm52-streamed-experts.ts +0 -453
  424. package/src/model/glm52-streamed-metal.ts +0 -610
  425. package/src/model/glm52-weights.ts +0 -503
  426. package/src/model/glm52.ts +0 -1132
  427. package/src/model/implementation.ts +0 -69
  428. package/src/model/kv-attention-view.ts +0 -67
  429. package/src/model/minicpm5.ts +0 -376
  430. package/src/model/profile.ts +0 -443
  431. package/src/model/qwen-append.ts +0 -12
  432. package/src/model/qwen-conv.ts +0 -52
  433. package/src/model/qwen3-delta-state.ts +0 -74
  434. package/src/model/qwen3-delta.ts +0 -451
  435. package/src/model/qwen3-moe.ts +0 -404
  436. package/src/model/qwen3-mrope.ts +0 -252
  437. package/src/model/qwen3.ts +0 -277
  438. package/src/model/qwen3_5-checkpoint.ts +0 -164
  439. package/src/model/qwen3_5.ts +0 -819
  440. package/src/model/rotating-kv-donor.ts +0 -35
  441. package/src/model/rotating-kv-layout.ts +0 -262
  442. package/src/model/rotating-row-transaction.ts +0 -35
  443. package/src/model/speculative-rotating-kv.ts +0 -101
  444. package/src/model/ssm-prefill-padding.ts +0 -39
  445. package/src/model/support.ts +0 -102
  446. package/src/model/trellis-balanced-scatter.ts +0 -51
  447. package/src/model/trellis-linear.ts +0 -672
  448. package/src/model/trellis-shared-m.ts +0 -151
  449. package/src/model/trellis-shared-scatter.ts +0 -144
  450. package/src/model/trellis-splitk-prefill.ts +0 -158
  451. package/src/model/trellis-tiled-prefill.ts +0 -136
  452. package/src/model/trellis-vector-expand.ts +0 -62
  453. package/src/model/trellis-wide-prefill.ts +0 -94
  454. package/src/model/turboquant-codec.ts +0 -83
  455. package/src/model/universal/archs.ts +0 -332
  456. package/src/model/universal/dense.ts +0 -612
  457. package/src/model/universal/modules.ts +0 -170
  458. package/src/model/universal/rope.ts +0 -381
  459. package/src/native/expert_io.c +0 -523
  460. package/src/native/frame_extract.swift +0 -140
  461. package/src/native-pack.ts +0 -162
  462. package/src/pi-provider.ts +0 -53
  463. package/src/pi-session.ts +0 -81
  464. package/src/pi-terminal.ts +0 -274
  465. package/src/pi-web.ts +0 -1932
  466. package/src/preflight.ts +0 -158
  467. package/src/prompt-cache.ts +0 -460
  468. package/src/quantize/allocator.ts +0 -627
  469. package/src/quantize/atomic-output.ts +0 -38
  470. package/src/quantize/calibration.ts +0 -181
  471. package/src/quantize/config-writer.ts +0 -145
  472. package/src/quantize/index.ts +0 -84
  473. package/src/quantize/job.ts +0 -162
  474. package/src/quantize/quantizer.ts +0 -666
  475. package/src/quantize/rotate.ts +0 -603
  476. package/src/quantize/safetensors-writer.ts +0 -249
  477. package/src/quantize/sensitivity.ts +0 -216
  478. package/src/quantize/trellis.ts +0 -586
  479. package/src/quantize/weight-transform.ts +0 -292
  480. package/src/registry.ts +0 -602
  481. package/src/runtime-config.ts +0 -81
  482. package/src/safetensors.ts +0 -158
  483. package/src/sampler.ts +0 -1011
  484. package/src/serve/admin-routes.ts +0 -184
  485. package/src/serve/assets/curve-terrain.html +0 -105
  486. package/src/serve/assets/training-inference-map.html +0 -575
  487. package/src/serve/aux-routes.ts +0 -132
  488. package/src/serve/batch-scheduler.ts +0 -8
  489. package/src/serve/checkpoint-identity.ts +0 -64
  490. package/src/serve/generation-gateway.ts +0 -496
  491. package/src/serve/isolate.ts +0 -609
  492. package/src/serve/kv-budget.ts +0 -14
  493. package/src/serve/lab-routes.ts +0 -341
  494. package/src/serve/lane-registry.ts +0 -45
  495. package/src/serve/model-admin-routes.ts +0 -155
  496. package/src/serve/model-binding.ts +0 -43
  497. package/src/serve/model-host.ts +0 -511
  498. package/src/serve/parent-application.ts +0 -56
  499. package/src/serve/prompt-response-trace.ts +0 -147
  500. package/src/serve/response-lease.ts +0 -24
  501. package/src/serve/session-search.ts +0 -326
  502. package/src/serve/static-routes.ts +0 -101
  503. package/src/server.ts +0 -1314
  504. package/src/spec/assistant-rows.ts +0 -92
  505. package/src/spec/assistant-source.ts +0 -126
  506. package/src/spec/cache-identity.ts +0 -22
  507. package/src/spec/deepspec-rows.ts +0 -29
  508. package/src/spec/deepspec-source.ts +0 -145
  509. package/src/spec/dflash-source.ts +0 -149
  510. package/src/spec/drafter.ts +0 -304
  511. package/src/spec/dspark/ab-stats.ts +0 -136
  512. package/src/spec/dspark/calibration.ts +0 -112
  513. package/src/spec/dspark/data-dflash.ts +0 -156
  514. package/src/spec/dspark/data.ts +0 -201
  515. package/src/spec/dspark/deepspec-module.ts +0 -776
  516. package/src/spec/dspark/generate-dflash.ts +0 -245
  517. package/src/spec/dspark/generate.ts +0 -290
  518. package/src/spec/dspark/loader.ts +0 -23
  519. package/src/spec/dspark/loss.ts +0 -213
  520. package/src/spec/dspark/module-dflash.ts +0 -560
  521. package/src/spec/dspark/module.ts +0 -540
  522. package/src/spec/dspark/quantize-drafter.ts +0 -91
  523. package/src/spec/dspark/sample.ts +0 -112
  524. package/src/spec/generate.ts +0 -276
  525. package/src/spec/glm52-mtp-graph.ts +0 -29
  526. package/src/spec/glm52-mtp-rows.ts +0 -132
  527. package/src/spec/glm52-mtp-source.ts +0 -317
  528. package/src/spec/glm52-mtp-state.ts +0 -33
  529. package/src/spec/ngram-source.ts +0 -204
  530. package/src/spec/projected-draft-rows.ts +0 -73
  531. package/src/spec/qwen-mtp-module.ts +0 -171
  532. package/src/spec/qwen-mtp-rows.ts +0 -219
  533. package/src/spec/qwen-mtp-source.ts +0 -383
  534. package/src/spec/qwen-mtp-state.ts +0 -30
  535. package/src/spec/serve-loop.ts +0 -585
  536. package/src/spec/source.ts +0 -215
  537. package/src/spec/two-model-rows.ts +0 -61
  538. package/src/spec/two-model.ts +0 -113
  539. package/src/ssd-cache.ts +0 -372
  540. package/src/ssd-durability.ts +0 -211
  541. package/src/tokenizer.ts +0 -335
  542. package/src/tool-approvals.ts +0 -131
  543. package/src/tool-call.ts +0 -695
  544. package/src/train/dataset.ts +0 -484
  545. package/src/train/diffusion-lora.ts +0 -216
  546. package/src/train/export.ts +0 -32
  547. package/src/train/flash-cce.ts +0 -1234
  548. package/src/train/forward.ts +0 -266
  549. package/src/train/fuse.ts +0 -243
  550. package/src/train/index.ts +0 -46
  551. package/src/train/job.ts +0 -205
  552. package/src/train/lora-params.ts +0 -254
  553. package/src/train/loss.ts +0 -1591
  554. package/src/train/merge.ts +0 -195
  555. package/src/train/optimizer.ts +0 -183
  556. package/src/train/prefix-shared.ts +0 -490
  557. package/src/train/rank.ts +0 -137
  558. package/src/train/segmented.ts +0 -1828
  559. package/src/train/steel-qmm-header.ts +0 -1461
  560. package/src/train/trainer.ts +0 -1221
  561. package/src/train/watch.ts +0 -542
  562. package/src/vision/diffusion-vision.ts +0 -620
  563. package/src/vision/embedder.ts +0 -168
  564. package/src/vision/preprocess.ts +0 -247
  565. package/src/vision/prompt.ts +0 -373
  566. package/src/vision/qwen3vl-preprocess.ts +0 -409
  567. package/src/vision/qwen3vl-prompt.ts +0 -196
  568. package/src/vision/qwen3vl-tower.ts +0 -555
  569. package/src/vision/siglip.ts +0 -599
  570. package/src/vision/video-frames.ts +0 -148
  571. package/src/web/app.html +0 -2143
  572. package/src/web/app.js +0 -5703
  573. package/src/web/skills/memory/SKILL.md +0 -120
  574. package/src/web/skills/web-research/SKILL.md +0 -46
  575. package/src/web/skills.ts +0 -97
  576. package/src/web/src/adapters-panel.ts +0 -288
  577. package/src/web/src/assistant.ts +0 -451
  578. package/src/web/src/chat.ts +0 -1057
  579. package/src/web/src/composer.ts +0 -959
  580. package/src/web/src/dataset.ts +0 -132
  581. package/src/web/src/finetune.ts +0 -267
  582. package/src/web/src/hub.ts +0 -428
  583. package/src/web/src/main.ts +0 -98
  584. package/src/web/src/markdown.ts +0 -0
  585. package/src/web/src/memory-panel.ts +0 -572
  586. package/src/web/src/model-picker.ts +0 -159
  587. package/src/web/src/palette.ts +0 -346
  588. package/src/web/src/protocol.ts +0 -26
  589. package/src/web/src/quantize.ts +0 -167
  590. package/src/web/src/rag.ts +0 -274
  591. package/src/web/src/sessions.ts +0 -469
  592. package/src/web/src/shell.ts +0 -804
  593. package/src/web/src/ui-catalog.ts +0 -78
  594. package/src/web/vendor/README.md +0 -61
  595. package/src/web-assets.ts +0 -47
  596. package/src/web-tools.ts +0 -478
  597. package/src/weights.ts +0 -255
  598. /package/src/{serve → server}/completion-sink.ts +0 -0
  599. /package/src/web/{icon.svg → public/icon.svg} +0 -0
  600. /package/src/web/{manifest.webmanifest → public/manifest.webmanifest} +0 -0
  601. /package/src/web/{vendor → public/vendor}/hljs-LICENSE +0 -0
@@ -1,1057 +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 chat controller: WS wiring, thread render, streaming, tool cards,
5
- // approval card, queue bar. This is the orchestrator for composer.ts and
6
- // sessions.ts — it owns the WebSocket and the per-turn mutable state
7
- // (curAssistant, lastUserMsgEl, etc.) that both of those modules act on via
8
- // explicit function calls rather than shared closures (a deliberate change
9
- // from the original single-IIFE app.html: the split's whole point is
10
- // typed, checkable seams between these concerns).
11
- //
12
- // Typed WS contract (plan §9 Phase 2 item 2): ClientMessage/ServerMessage
13
- // and the history/session shapes are imported TYPE-ONLY from ../../contracts/pi
14
- // so this module (and the whole bundle) contains zero server code — the
15
- // bundler errors on bun:ffi if a value import ever sneaks in here, which is
16
- // the intended guard (see scripts/build-web.ts).
17
-
18
- import type {
19
- ClientMessage, HistoryItem, ReadyGenDefaults, ServerMessage, SessionListItem,
20
- } from "../../contracts/pi";
21
- import type { Lane } from "../../contracts/pi";
22
- import {
23
- $, el, toast, currentRoute, closeDrawer,
24
- storedCodingToolsPreference, renderCodingToolsState, renderToolApprovals,
25
- } from "./shell";
26
- import { esc, mdToHtml, highlightIn, linkifyCitations, makeFrameScheduler, renderBlocksIncremental, wireCanvasToggle, captureCanvasViewStates, restoreCanvasViewStates, type BlockState } from "./markdown";
27
- import {
28
- ComposerState, addFiles, buildMessageText, clearAttachments, clearOneShotArmed, initMentionPicker,
29
- initSampling, initSystemPrompt, onSelectAdapter, refreshAdapters as composerRefreshAdapters, refreshSamplingRecs,
30
- renderAttachments, renderContext, renderLane, renderQueue, updateAttachHint,
31
- updateThinkingToggle, type ContextFrame,
32
- } from "./composer";
33
- import {
34
- addMsgActions, newSiblingInfo, newSidebarState, renderSessions, renderSiblingToggle,
35
- switchSiblingTarget, applySessSearch, type SidebarState, type SiblingInfo,
36
- } from "./sessions";
37
- import { AdaptersPanelState, initAdaptersPanel, refreshAdaptersPanel } from "./adapters-panel";
38
- import { initMemoryPanel, isMemoryToolName, memoryToolChip, type MemChipHandle } from "./memory-panel";
39
- import { api } from "./api";
40
- import type { ApiEnvelope } from "./protocol";
41
- import type { Citation } from "./rag";
42
- import { buildAppContext, resolveSpotlightTarget, showSpotlight, type UiSnapshot } from "./assistant";
43
- import { isRouteId } from "./ui-catalog";
44
-
45
- /* ────────────────────────────────────────────────────────────────────
46
- Chat-with-files RAG v1 — Sources panel (plan §9 Phase 3, beat matrix
47
- Axis 5). Pure HTML builder so tests/using/web-app.test.ts can exercise the
48
- esc() discipline directly, same reasoning as renderAdapterOptionsHtml
49
- etc. in composer.ts.
50
- ──────────────────────────────────────────────────────────────────── */
51
-
52
- /** Pure: builds the collapsed "Sources · K" summary line + expandable list
53
- * of [n] filename + snippet for a turn's citations. Snippets are
54
- * truncated (a citation's chunk can be ~1200 chars — the panel is a
55
- * pointer back to the source, not a document viewer). */
56
- export function renderSourcesHtml(citations: Citation[]): string {
57
- if (!citations.length) return "";
58
- const rows = citations.map((c) => {
59
- const snippet = c.text.length > 240 ? c.text.slice(0, 240).trim() + "…" : c.text.trim();
60
- return '<div class="src-row" data-cite-row="' + c.n + '">' +
61
- '<span class="src-n">[' + c.n + "]</span>" +
62
- '<span class="src-meta"><span class="src-file">' + esc(c.fileName) + '</span>' +
63
- '<span class="src-range">chars ' + c.start + "–" + c.end + "</span></span>" +
64
- '<div class="src-snippet">' + esc(snippet) + "</div></div>";
65
- }).join("");
66
- return '<details class="sources"><summary>Sources · ' + citations.length + "</summary>" +
67
- '<div class="src-list">' + rows + "</div></details>";
68
- }
69
-
70
- /* ────────────────────────────────────────────────────────────────────
71
- Tool arg formatting helpers (shared by streaming + replayed history)
72
- ──────────────────────────────────────────────────────────────────── */
73
-
74
- function argSummary(args: unknown): string {
75
- if (!args) return "";
76
- if (typeof args === "string") return args.slice(0, 120);
77
- const a = args as Record<string, unknown>;
78
- if (a.command || a.cmd) return String(a.command || a.cmd).slice(0, 120);
79
- if (a.file_path || a.path) return String(a.file_path || a.path).slice(0, 120);
80
- if (a.query || a.url || a.location) return String(a.query || a.url || a.location).slice(0, 120);
81
- const s = JSON.stringify(args); return s.length > 120 ? s.slice(0, 117) + "…" : s;
82
- }
83
- function prettyArgs(args: unknown): string {
84
- if (args == null) return "";
85
- if (typeof args === "string") return args;
86
- try { return JSON.stringify(args, null, 2); } catch { return String(args); }
87
- }
88
- function diffView(args: unknown): string {
89
- // Best-effort diff-ish view for edit/write tools.
90
- if (!args) return "";
91
- const a = args as Record<string, unknown>;
92
- if (a.old_string != null || a.new_string != null) {
93
- const o = String(a.old_string ?? "").split("\n").map((l) => '<span class="diffdel">- ' + esc(l) + "</span>").join("\n");
94
- const n = String(a.new_string ?? "").split("\n").map((l) => '<span class="diffadd">+ ' + esc(l) + "</span>").join("\n");
95
- return (a.file_path ? esc(a.file_path) + "\n\n" : "") + o + (o && n ? "\n" : "") + n;
96
- }
97
- if (a.content != null) {
98
- return (a.file_path ? esc(a.file_path) + "\n\n" : "") +
99
- String(a.content).split("\n").map((l) => '<span class="diffadd">+ ' + esc(l) + "</span>").join("\n");
100
- }
101
- return esc(prettyArgs(args));
102
- }
103
-
104
- /* ────────────────────────────────────────────────────────────────────
105
- Per-assistant-message streaming state
106
- ──────────────────────────────────────────────────────────────────── */
107
-
108
- // Two distinct card kinds share one map (curAssistant.tools): the generic
109
- // "wrench" tool card and the memory provenance chip (plan §5.4.2 — the
110
- // single highest-leverage rendering change: a citation, not a shell
111
- // command). Dispatch on `.kind` at each lifecycle point (toolCard/
112
- // toolUpdate/toolEnd) rather than keeping two parallel maps, since a card's
113
- // kind is fixed at tool_start and every later frame (tool_update/tool_end)
114
- // only carries the callId to look it up by.
115
- interface WrenchCardState {
116
- kind: "wrench";
117
- wrap: HTMLElement;
118
- res: HTMLElement;
119
- resBlk: HTMLElement;
120
- label: HTMLElement;
121
- chunks: string;
122
- }
123
- interface MemChipCardState {
124
- kind: "memchip";
125
- wrap: HTMLElement;
126
- handle: MemChipHandle;
127
- chunks: string;
128
- }
129
- type ToolCardState = WrenchCardState | MemChipCardState;
130
-
131
- interface AssistantMsgState {
132
- m: HTMLElement;
133
- bubble: HTMLElement;
134
- thinkBox: HTMLDetailsElement;
135
- thinkBody: HTMLElement;
136
- textNode: HTMLElement;
137
- cursor: HTMLElement;
138
- meta: HTMLElement;
139
- tools: Map<string, ToolCardState>;
140
- text: string;
141
- thinking: string;
142
- t0: number;
143
- tFirst: number;
144
- tokens: number;
145
- settled: boolean;
146
- blockState: BlockState;
147
- scheduleText: () => void;
148
- scheduleThinking: () => void;
149
- // Chat-with-files RAG v1 (plan §9 Phase 3, beat matrix Axis 5): the
150
- // citation list retrieved for the prompt that started THIS turn, if any
151
- // (composer.ts's buildMessageText only returns a non-empty list when
152
- // retrieval mode fired). Empty for the common case — no Sources panel,
153
- // no [n] linkification, zero visual change from before this feature.
154
- citations: Citation[];
155
- }
156
-
157
- export function createChatController() {
158
- let ws: WebSocket | null = null, connected = false, reconnectTimer: ReturnType<typeof setTimeout> | undefined, manualClose = false;
159
- let curAssistant: AssistantMsgState | null = null;
160
- let turnActive = false;
161
- // App-aware assistant (plan §6.6): the last uiSnapshot pushed to the
162
- // server, kept client-side too so spotlight_ui's ref resolution
163
- // (resolveSpotlightTarget) doesn't need a round-trip — the browser
164
- // already has the freshest snapshot it just sent.
165
- let lastSnapshot: UiSnapshot | null = null;
166
- // Citations retrieved for the prompt currently in flight (set in submit(),
167
- // just before send()) — startAssistant() picks this up when turn_start
168
- // arrives and attaches it to the new AssistantMsgState, then clears it so
169
- // a later turn with no attachments doesn't inherit a stale citation set.
170
- let pendingCitations: Citation[] = [];
171
- // This connection's active session path (from `sessions` frames). Each
172
- // connect gets a fresh server session; on a transient reconnect we re-open
173
- // our own session so a blip doesn't strand us on a blank one.
174
- let currentSessionPath: string | null = null, pendingResumePath: string | null = null;
175
- const thread = () => $("chat-thread");
176
- const composer = new ComposerState();
177
- const sidebar: SidebarState = newSidebarState();
178
- const adaptersPanel = new AdaptersPanelState();
179
-
180
- function wsUrl(): string {
181
- const proto = location.protocol === "https:" ? "wss:" : "ws:";
182
- return proto + "//" + location.host + "/ws/chat";
183
- }
184
-
185
- function connect(): void {
186
- manualClose = false;
187
- // Remember our session across this (re)connect: if we were already in a
188
- // session, re-open it once the new connection is ready (the server starts
189
- // each connection on a fresh session by default).
190
- pendingResumePath = currentSessionPath;
191
- try { ws = new WebSocket(wsUrl()); } catch { scheduleReconnect(); return; }
192
- ws.onopen = () => { connected = true; setChatStatus("connected"); };
193
- ws.onclose = () => { connected = false; setChatStatus("disconnected"); if (!manualClose) scheduleReconnect(); };
194
- ws.onerror = () => { setChatStatus("error"); };
195
- ws.onmessage = (ev) => { let m: ServerMessage; try { m = JSON.parse(ev.data); } catch { return; } handle(m); };
196
- }
197
- function scheduleReconnect(): void {
198
- clearTimeout(reconnectTimer);
199
- reconnectTimer = setTimeout(() => { if (currentRoute() === "chat") connect(); }, 2200);
200
- }
201
- function send(obj: ClientMessage): boolean {
202
- if (ws && ws.readyState === 1) { ws.send(JSON.stringify(obj)); return true; }
203
- return false;
204
- }
205
-
206
- /* ── App-aware assistant (plan §6.6, §9 Phase 3, beat matrix Axis 12) ──
207
- "See": push a structured context frame on every route change and every
208
- wizard-step change. Never on a timer — only these two triggers, so the
209
- server's ambient one-liner always reflects a real, user-caused state
210
- change. */
211
-
212
- /** Which non-route overlay (if any) is currently open, layered on top of
213
- * whatever route is active underneath — same "where is the user, really"
214
- * question navigate_app/spotlight_ui need answered. Checked in a fixed
215
- * order; the first open one wins (only one of these opens at a time in
216
- * practice — each is closed by shell.ts's closeTopOverlay sweep before
217
- * another opens). */
218
- function currentView(): string | null {
219
- if ($("mem-overlay")?.classList.contains("open")) return "memory-panel";
220
- if ($("hub-overlay")?.classList.contains("open")) return "hub-panel";
221
- if ($("adapters-overlay")?.classList.contains("open")) return "adapters-panel";
222
- return null;
223
- }
224
-
225
- function pushAppContext(): void {
226
- const route = currentRoute();
227
- if (!isRouteId(route)) return; // "routes" (DAG diagram) isn't a useful assistant destination
228
- const ctx = buildAppContext(route, currentView());
229
- lastSnapshot = ctx.snapshot;
230
- send({ type: "context", context: ctx });
231
- }
232
-
233
- /** MutationObserver on the three wizard step-indicator containers: their
234
- * innerHTML is fully rewritten by markdown.ts's renderSteps() on every
235
- * show(n) call in quantize.ts/finetune.ts/dataset.ts, so a childList+
236
- * subtree observer fires exactly on step change — no callback hook
237
- * needed in those three controllers (outside this wave's file scope). */
238
- function watchWizardSteps(): void {
239
- const mo = new MutationObserver(() => pushAppContext());
240
- for (const id of ["q-steps", "f-steps", "d-steps"]) {
241
- const el = $(id);
242
- if (el) mo.observe(el, { childList: true, subtree: true });
243
- }
244
- }
245
-
246
- function setChatStatus(s: "connected" | "disconnected" | "error"): void {
247
- const line = $("chat-status-line");
248
- // The "#" hint lives HERE, not in the composer placeholder — a long
249
- // placeholder wraps + clips at phone widths (the 2026-07-06 jank class).
250
- // This status line ellipsizes gracefully instead (.chat-hint CSS).
251
- if (s === "connected") line.textContent = "connected · type to send · Shift+Enter for newline · # attaches files or recalls memory";
252
- else if (s === "disconnected") line.textContent = "reconnecting to agent…";
253
- else if (s === "error") line.textContent = "connection error — retrying";
254
- }
255
-
256
- /* — message actions: copy / regenerate / edit-and-resend (plan §5.2) —
257
- * Only the LAST assistant message gets "regenerate" and only the LAST
258
- * user message gets "edit" + the sibling `< i/n >` toggle. Both "last"
259
- * pointers are tracked module-side and re-derived whenever a `history`
260
- * frame rebuilds #chat-thread wholesale (see renderHistory/endTurn below).
261
- * Regenerate/edit-resend re-prompt on a new sibling branch WITHOUT a
262
- * `history` frame (resendFrom in pi-web.ts), so their click handlers below
263
- * drop the stale lastAssistantMsgEl (and, for edit, update the surviving
264
- * user bubble's text) themselves before the new reply streams in. */
265
- let lastUserMsgEl: HTMLElement | null = null; // the <div class="msg user"> for the last user message
266
- let lastUserEntryId: string | null = null; // its session entryId (from history/siblings frames)
267
- let siblingInfo: SiblingInfo = newSiblingInfo();
268
- let lastAssistantMsgEl: HTMLElement | null = null; // the <div class="msg assistant"> eligible for regenerate
269
- // Citations for the reply currently sitting in lastAssistantMsgEl (kept in
270
- // lockstep with it — set alongside it in endTurn, cleared alongside it
271
- // everywhere it's nulled). Regenerate/edit-resend re-prompt the SAME
272
- // underlying (possibly RAG'd) user turn without going through submit()/
273
- // buildMessageText, so pendingCitations would otherwise be the stale `[]`
274
- // left over from consumption at the prior startAssistant() call — see the
275
- // pendingCitations declaration below. Re-seeding from here means a
276
- // citation-bearing turn keeps its Sources panel across regenerate/edit.
277
- let lastAssistantCitations: Citation[] = [];
278
-
279
- function doRegenerate(): void {
280
- if (turnActive) return;
281
- if (!send({ type: "regenerate" })) { toast("Not connected to the agent yet.", "err"); return; }
282
- // The server re-prompts on a new sibling branch — it does not send a
283
- // `history` frame for this (see resendFrom in pi-web.ts), so the old
284
- // reply would otherwise linger above the new one as it streams in.
285
- // Drop it now; startAssistant() appends the fresh reply right after.
286
- if (lastAssistantMsgEl) { lastAssistantMsgEl.remove(); lastAssistantMsgEl = null; }
287
- pendingCitations = lastAssistantCitations;
288
- }
289
-
290
- function switchSiblingDir(delta: number): void {
291
- const target = switchSiblingTarget(siblingInfo, delta);
292
- if (!target) return;
293
- send({ type: "switch_sibling", entryId: target });
294
- }
295
-
296
- /** Turn the last user message bubble into an inline editor. Cancel restores
297
- * the original text view; Send fires edit_resend, updates the surviving
298
- * user bubble to the edited text in place, and drops the stale assistant
299
- * reply — the server re-prompts on a new sibling branch without sending a
300
- * `history` frame (see resendFrom in pi-web.ts), so the old reply/text
301
- * would otherwise linger next to the new one as it streams in. */
302
- function startEditLast(m: HTMLElement): void {
303
- if (turnActive) return;
304
- const bubble = m.querySelector(".bubble") as HTMLElement;
305
- const textEl = bubble.querySelector(".msg-text") as HTMLElement | null;
306
- if (!textEl) return;
307
- const original = textEl.textContent || "";
308
- const box = document.createElement("textarea");
309
- box.className = "msg-edit-box";
310
- box.value = original;
311
- const actions = el("div", "msg-edit-actions", null);
312
- const cancelBtn = el("button", "btn ghost sm", actions);
313
- cancelBtn.type = "button"; cancelBtn.textContent = "Cancel";
314
- const sendBtn = el("button", "btn primary sm", actions);
315
- sendBtn.type = "button"; sendBtn.textContent = "Send";
316
- textEl.replaceWith(box);
317
- box.after(actions);
318
- box.focus(); box.setSelectionRange(box.value.length, box.value.length);
319
- const restore = () => { box.replaceWith(textEl); actions.remove(); };
320
- const doSend = () => {
321
- const text = box.value.trim();
322
- if (!text) return;
323
- if (!send({ type: "edit_resend", text })) { toast("Not connected to the agent yet.", "err"); return; }
324
- textEl.textContent = text;
325
- box.replaceWith(textEl); actions.remove();
326
- if (lastAssistantMsgEl) { lastAssistantMsgEl.remove(); lastAssistantMsgEl = null; }
327
- pendingCitations = lastAssistantCitations;
328
- };
329
- cancelBtn.onclick = restore;
330
- sendBtn.onclick = doSend;
331
- box.addEventListener("keydown", (e) => {
332
- if (e.key === "Escape") { e.preventDefault(); restore(); }
333
- else if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); doSend(); }
334
- });
335
- }
336
-
337
- /* — message + tool DOM construction — */
338
- function addUserMsg(text: string, atts: { kind: string; data?: string; mimeType?: string; name: string }[] | null, opts: { isLast?: boolean; entryId?: string }): void {
339
- const m = el("div", "msg user", thread());
340
- el("div", "who", m).textContent = "you";
341
- const bubble = el("div", "bubble", m);
342
- if (atts && atts.length) {
343
- const wrap = el("div", "msg-atts", bubble);
344
- for (const a of atts) {
345
- if (a.kind === "image") {
346
- const im = el("img", "msg-att-img", wrap);
347
- im.src = "data:" + a.mimeType + ";base64," + a.data; im.alt = a.name;
348
- } else {
349
- el("span", "msg-att-file", wrap).textContent = "📎 " + a.name;
350
- }
351
- }
352
- }
353
- if (text) el("div", "msg-text", bubble).textContent = text;
354
- if (opts.isLast) {
355
- lastUserMsgEl = m;
356
- lastUserEntryId = opts.entryId || null;
357
- addMsgActions(m, text, { edit: true }, { onEdit: startEditLast });
358
- renderSiblingToggle(lastUserMsgEl, lastUserEntryId, siblingInfo, switchSiblingDir);
359
- }
360
- stick(true);
361
- }
362
- function startAssistant(): void {
363
- $("chat-hello").style.display = "none";
364
- const m = el("div", "msg assistant", thread());
365
- el("div", "who", m).textContent = "local agent";
366
- const bubble = el("div", "bubble", m);
367
- const thinkBox = el("details", "thinkbox", bubble);
368
- thinkBox.open = false; // collapsed by default; the reasoning stays tucked behind "Thinking" — click to expand
369
- el("summary", "", thinkBox).textContent = "Thinking";
370
- const thinkBody = el("div", "thinkbody", thinkBox);
371
- thinkBox.style.display = "none";
372
- const textNode = el("span", "atext", bubble);
373
- const cursor = el("span", "cursor", bubble);
374
- const meta = el("div", "meta", m);
375
- curAssistant = {
376
- m, bubble, thinkBox, thinkBody, textNode, cursor, meta, tools: new Map(),
377
- text: "", thinking: "", t0: performance.now(), tFirst: 0, tokens: 0, settled: false,
378
- // Block-memoization state (see renderBlocksIncremental) for the main
379
- // markdown text stream, plus a shared rAF scheduler so a burst of
380
- // deltas within one frame (e.g. spec-decode emitting several tokens
381
- // back-to-back) collapses into a single re-render instead of one per
382
- // token. The thinking channel is plain text (no markdown — see
383
- // appendThinkingDelta), so it only needs the textContent set
384
- // throttled, not block splitting; it gets its own scheduler since
385
- // both channels can stream concurrently (interleaved thinking/text).
386
- blockState: { blocks: [] },
387
- scheduleText: () => {},
388
- scheduleThinking: () => {},
389
- citations: pendingCitations,
390
- };
391
- // Consumed — a later turn with no RAG'd attachments must not inherit
392
- // this turn's citation set (see the pendingCitations declaration above).
393
- pendingCitations = [];
394
- const assistant = curAssistant;
395
- assistant.scheduleText = makeFrameScheduler(
396
- () => {
397
- if (!assistant.settled) renderBlocksIncremental(assistant.textNode, assistant.text, assistant.blockState);
398
- },
399
- atBottom, stick);
400
- assistant.scheduleThinking = makeFrameScheduler(
401
- () => {
402
- if (!assistant.settled) assistant.thinkBody.textContent = assistant.thinking;
403
- },
404
- atBottom, stick);
405
- stick(true);
406
- }
407
- function ensureAssistant(): void { if (!curAssistant) startAssistant(); }
408
-
409
- function appendDelta(delta: string): void {
410
- ensureAssistant();
411
- const a = curAssistant!;
412
- if (!a.tFirst) a.tFirst = performance.now();
413
- a.tokens++;
414
- a.text += delta;
415
- // Block-memoized + rAF-throttled render (see renderBlocksIncremental):
416
- // only the live tail block re-parses per delta; completed blocks are
417
- // never touched again. A final full mdToHtml pass runs at turn-end
418
- // (finishStreaming) to guarantee the streamed result is byte-identical
419
- // to a non-streaming render. Autoscroll is handled INSIDE the scheduler
420
- // (its shouldStick/doStick args) — not here — because the render itself
421
- // is rAF-deferred: checking atBottom() synchronously at this point would
422
- // measure scrollHeight from before the queued render grows it, which
423
- // desyncs autoscroll once several deltas land within one frame.
424
- a.scheduleText();
425
- updateTps();
426
- }
427
- function appendThinkingDelta(delta: string): void {
428
- ensureAssistant();
429
- const a = curAssistant!;
430
- if (!a.tFirst) a.tFirst = performance.now();
431
- a.tokens++;
432
- a.thinking += delta;
433
- a.thinkBox.style.display = a.thinking.trim() ? "" : "none";
434
- // Plain text (no markdown here), but still rAF-throttled: a long
435
- // thinking trace can otherwise do one textContent-set per token.
436
- // Autoscroll: see the comment in appendDelta above — handled inside the
437
- // scheduler, not with a synchronous atBottom() check here.
438
- a.scheduleThinking();
439
- updateTps();
440
- }
441
-
442
- /* tool card lifecycle — dispatches to the memory provenance chip (plan
443
- * §5.4.2) for memory_ and reference_ tool calls, the generic wrench card
444
- * for everything else. Both kinds live in the same a.tools map (see
445
- * ToolCardState above). */
446
- function toolCard(callId: string, tool: string, args: unknown): ToolCardState {
447
- ensureAssistant();
448
- const a = curAssistant!;
449
- let t = a.tools.get(callId);
450
- if (t) return t;
451
- if (isMemoryToolName(tool || "")) {
452
- const handle = memoryToolChip(a.bubble, tool, args);
453
- t = { kind: "memchip", wrap: handle.wrap, handle, chunks: "" };
454
- a.tools.set(callId, t);
455
- stick();
456
- return t;
457
- }
458
- const wrap = el("div", "tool running open", a.bubble);
459
- wrap.innerHTML =
460
- '<div class="thead"><div class="ticon">⚙</div>' +
461
- '<span class="tname">' + esc(tool || "tool") + '</span>' +
462
- '<span class="targs">' + esc(argSummary(args)) + '</span>' +
463
- '<span class="tstat"><span class="sdot"></span><span class="slabel">running</span></span>' +
464
- '<span class="caret">›</span></div>' +
465
- '<div class="tbody">' +
466
- '<div class="blk"><div class="blbl">arguments</div><pre class="aargs">' + esc(prettyArgs(args)) + '</pre></div>' +
467
- '<div class="blk tresult" style="display:none"><div class="blbl">result</div><pre class="ares"></pre></div>' +
468
- '</div>';
469
- wrap.querySelector(".thead")!.addEventListener("click", () => wrap.classList.toggle("open"));
470
- t = {
471
- kind: "wrench", wrap,
472
- res: wrap.querySelector(".ares") as HTMLElement,
473
- resBlk: wrap.querySelector(".tresult") as HTMLElement,
474
- label: wrap.querySelector(".slabel") as HTMLElement,
475
- chunks: "",
476
- };
477
- a.tools.set(callId, t);
478
- stick();
479
- return t;
480
- }
481
- function toolUpdate(callId: string, chunk: string): void {
482
- const t = curAssistant && curAssistant.tools.get(callId);
483
- if (!t) return;
484
- t.chunks += chunk;
485
- if (t.kind === "memchip") { t.handle.setResult(t.chunks); if (atBottom()) stick(); return; }
486
- t.resBlk.style.display = "";
487
- t.res.textContent = t.chunks.slice(-8000);
488
- if (atBottom()) stick();
489
- }
490
- function toolEnd(callId: string, ok: boolean, result: unknown): void {
491
- const t = curAssistant && curAssistant.tools.get(callId);
492
- if (!t) return;
493
- if (t.kind === "memchip") {
494
- t.wrap.classList.toggle("fail", !ok);
495
- t.handle.setResult(result != null ? result : t.chunks);
496
- return;
497
- }
498
- t.wrap.classList.remove("running");
499
- t.wrap.classList.add(ok ? "ok" : "fail");
500
- t.label.textContent = ok ? "done" : "failed";
501
- const body = result != null ? (typeof result === "string" ? result : JSON.stringify(result, null, 2)) : t.chunks;
502
- if (body) { t.resBlk.style.display = ""; t.res.textContent = String(body).slice(-12000); }
503
- if (!ok) t.wrap.classList.add("open");
504
- }
505
-
506
- /* approval dialog (plan §5.4/§6.5: editable arguments + durable "always
507
- * allow this tool", on top of the existing running/ok/fail visual
508
- * language). The diff view (edit/write) stays as a read-only visual aid
509
- * ABOVE the editable textarea — losing the diff would make large
510
- * file-content proposals unreadable as raw JSON — but the textarea,
511
- * pre-filled with the proposed args as JSON, is what actually gets sent:
512
- * editing it and clicking Allow re-parses it and ships the edited object
513
- * as `editedArgs`, which pi-web.ts mutates onto the SDK's ToolCallEvent.input
514
- * in place (the SDK's documented tool_call mutation contract). */
515
- function approvalCard(callId: string, tool: string, args: unknown): void {
516
- ensureAssistant();
517
- const a = curAssistant!;
518
- const wrap = el("div", "approval", a.bubble);
519
- const isBash = /bash|shell|exec|run/i.test(tool || "");
520
- const isEdit = /edit|write|patch|create/i.test(tool || "");
521
- const diffHtml = isEdit ? '<pre class="a-diff">' + diffView(args) + '</pre>' : "";
522
- const argsJson = prettyArgs(args);
523
- wrap.innerHTML =
524
- '<div class="ahead"><div class="ai">⚠</div><div class="at">Approval required</div>' +
525
- '<div class="as">' + esc(tool || "tool") + '</div></div>' +
526
- '<div class="abody">' + diffHtml +
527
- '<div class="blbl" style="margin-bottom:7px">' +
528
- (isBash ? "command (edit before approving if you like)" : isEdit ? "proposed change — arguments below, editable" : "arguments — editable before approving") +
529
- '</div>' +
530
- '<textarea class="a-args" spellcheck="false">' + esc(argsJson) + '</textarea>' +
531
- '<div class="a-argerr" style="display:none"></div>' +
532
- '</div>' +
533
- '<label class="chk a-always"><input type="checkbox" class="a-always-cb">Always allow ' + esc(tool || "this tool") + ' (skip this card next time)</label>' +
534
- '<div class="actions">' +
535
- '<button class="btn ghost sm a-deny">Deny</button>' +
536
- '<button class="btn primary sm a-allow">Allow</button></div>';
537
- const textarea = wrap.querySelector(".a-args") as HTMLTextAreaElement;
538
- const errEl = wrap.querySelector(".a-argerr") as HTMLElement;
539
- const alwaysCb = wrap.querySelector(".a-always-cb") as HTMLInputElement;
540
- const resolve = (decision: "allow" | "deny") => {
541
- let editedArgs: Record<string, unknown> | undefined;
542
- if (decision === "allow") {
543
- const raw = textarea.value.trim();
544
- if (raw && raw !== argsJson.trim()) {
545
- try {
546
- const parsed = JSON.parse(raw);
547
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
548
- editedArgs = parsed as Record<string, unknown>;
549
- } else {
550
- errEl.textContent = "Arguments must be a JSON object — sending the original instead.";
551
- errEl.style.display = "";
552
- }
553
- } catch {
554
- errEl.textContent = "Could not parse the edited arguments as JSON — sending the original instead.";
555
- errEl.style.display = "";
556
- }
557
- }
558
- }
559
- send({ type: "approval", callId, decision, editedArgs, alwaysAllow: decision === "allow" && alwaysCb.checked });
560
- wrap.querySelector(".actions")!.remove();
561
- wrap.querySelector(".a-always")?.remove();
562
- const r = el("div", "resolved " + (decision === "allow" ? "allow" : "deny"), wrap);
563
- r.textContent = decision === "allow" ? (editedArgs ? "✓ allowed (edited)" : "✓ allowed") : "✕ denied";
564
- };
565
- (wrap.querySelector(".a-allow") as HTMLButtonElement).onclick = () => resolve("allow");
566
- (wrap.querySelector(".a-deny") as HTMLButtonElement).onclick = () => resolve("deny");
567
- stick(true);
568
- }
569
-
570
- function showAgentError(message: string): void {
571
- const m = el("div", "msg assistant", thread());
572
- el("div", "who", m).textContent = "local agent";
573
- const bubble = el("div", "bubble", m);
574
- bubble.innerHTML = '<strong style="color:var(--red)">Agent error</strong><br>' + esc(message || "unknown error");
575
- stick(true);
576
- }
577
-
578
- /** Chat-with-files RAG v1 (plan §9 Phase 3, beat matrix Axis 5): append a
579
- * collapsed "Sources · K" panel under the bubble and linkify any [n]
580
- * markers the model actually used in its reply. Only ever called with a
581
- * non-empty citation list (finishStreaming guards this) — an ordinary
582
- * reply with no attachments gets neither the panel nor any [n]
583
- * rewriting, so a message that happens to contain literal "[1]" text
584
- * never gets a false citation link. */
585
- function attachSourcesPanel(a: AssistantMsgState): void {
586
- const validNs = new Set(a.citations.map((c) => c.n));
587
- a.textNode.innerHTML = linkifyCitations(a.textNode.innerHTML, validNs);
588
- const panel = el("div", "", null);
589
- panel.innerHTML = renderSourcesHtml(a.citations);
590
- const detailsEl = panel.firstElementChild as HTMLElement | null;
591
- if (!detailsEl) return;
592
- a.bubble.appendChild(detailsEl);
593
- // Clicking a [n] marker in the rendered text expands the panel (if
594
- // collapsed) and scrolls/highlights the matching source row.
595
- a.textNode.querySelectorAll<HTMLElement>(".cite-mark").forEach((btn) => {
596
- btn.addEventListener("click", () => {
597
- const n = btn.dataset.cite;
598
- (detailsEl as HTMLDetailsElement).open = true;
599
- const row = detailsEl.querySelector<HTMLElement>('.src-row[data-cite-row="' + n + '"]');
600
- if (row) {
601
- row.scrollIntoView({ block: "nearest", behavior: "smooth" });
602
- row.classList.add("pulse");
603
- setTimeout(() => row.classList.remove("pulse"), 1000);
604
- }
605
- });
606
- });
607
- }
608
-
609
- /** Turn-end finalization for the streamed text channel: replace the
610
- * block-memoized incremental render with ONE full non-memoized
611
- * mdToHtml(fullText) pass. This is the acceptance test for block
612
- * memoization — whatever the incremental path did while streaming,
613
- * the final DOM must match exactly what a plain, non-streaming
614
- * render of the same text would have produced. Cheap (happens once
615
- * per turn, not per token) and removes any residual "done" block
616
- * wrappers so the settled message's markup matches replayed history
617
- * (renderAssistantStatic / renderHistory), which also call mdToHtml
618
- * directly on the full string. */
619
- function finishStreaming(a: AssistantMsgState | null): void {
620
- if (!a) return;
621
- a.settled = true;
622
- // Preserve any Canvas Preview toggle the user made mid-stream (Principle
623
- // 9: this full non-memoized re-render must not throw away user
624
- // interaction state) — mdCodeBlock always emits qualifying fences back
625
- // in their default Source view, so without this a live Preview click
626
- // silently reverts the instant the turn finalizes.
627
- const previewIdx = captureCanvasViewStates(a.textNode);
628
- a.textNode.innerHTML = mdToHtml(a.text);
629
- restoreCanvasViewStates(a.textNode, previewIdx);
630
- highlightIn(a.textNode);
631
- a.thinkBody.textContent = a.thinking;
632
- if (a.citations.length) attachSourcesPanel(a);
633
- }
634
-
635
- /** Demote the previously-"last" assistant message: only ONE message ever
636
- * shows Regenerate (plan §5.2 scopes it to the last assistant turn). */
637
- function clearRegenerateAffordance(): void {
638
- if (!lastAssistantMsgEl) return;
639
- const row = lastAssistantMsgEl.querySelector(".msg-actions");
640
- const btn = row && [...row.querySelectorAll(".maction")].find((b) => b.textContent === "Regenerate");
641
- if (btn) btn.remove();
642
- }
643
-
644
- function endTurn(): void {
645
- turnActive = false;
646
- if (curAssistant) {
647
- finishStreaming(curAssistant);
648
- curAssistant.cursor.remove();
649
- if (!curAssistant.text && !curAssistant.thinking && curAssistant.tools.size === 0) {
650
- curAssistant.m.remove();
651
- } else {
652
- // A settled assistant reply becomes the new regenerate target.
653
- // Works after abort/stop too — a partial reply still gets the
654
- // action row, and the server resolves regenerate from the session's
655
- // last user message regardless of whether this reply finished.
656
- clearRegenerateAffordance();
657
- addMsgActions(curAssistant.m, curAssistant.text, { regenerate: true }, { onRegenerate: doRegenerate });
658
- lastAssistantMsgEl = curAssistant.m;
659
- lastAssistantCitations = curAssistant.citations;
660
- }
661
- }
662
- curAssistant = null;
663
- ($("chat-send") as HTMLButtonElement).disabled = false;
664
- $("chat-stop").style.display = "none";
665
- renderQueue({});
666
- }
667
-
668
- // Perf strip (plan §5.2/§6.4): tok/s + TTFT update live during streaming;
669
- // context-fill is driven separately by the `context` frame (renderContext).
670
- // All client-computed from data the composer already has — the lane badge
671
- // is the only server-driven field (see renderLane / the turn_end handler).
672
- function updateTps(): void {
673
- const a = curAssistant; if (!a) return;
674
- const secs = (performance.now() - (a.tFirst || a.t0)) / 1000;
675
- if (a.tokens > 1 && secs > 0) {
676
- const tps = $("pf-tps"); if (tps) tps.innerHTML = "<b style='color:var(--green)'>" + (a.tokens / secs).toFixed(1) + " tok/s</b>";
677
- }
678
- if (a.tFirst) {
679
- const ttft = $("pf-ttft"); if (ttft) ttft.textContent = "TTFT " + Math.round(a.tFirst - a.t0) + "ms";
680
- }
681
- }
682
- function finalizeMeta(): void {
683
- const a = curAssistant; if (!a) return;
684
- const secs = (performance.now() - (a.tFirst || a.t0)) / 1000;
685
- if (a.tokens > 1 && secs > 0)
686
- a.meta.innerHTML = "<b>" + (a.tokens / secs).toFixed(1) + " tok/s</b> · " + a.tokens +
687
- " tokens · first token " + Math.round((a.tFirst - a.t0)) + "ms";
688
- }
689
-
690
- function newChat(): void {
691
- if (!send({ type: "new_session" })) { toast("Not connected to the agent yet.", "err"); return; }
692
- $("chat-box").focus();
693
- closeDrawer(); // a fresh chat is the natural "I'm done browsing" signal on mobile
694
- }
695
-
696
- /** Render a finished assistant turn from replayed history (no streaming).
697
- * `opts.isLast` attaches the Regenerate action (plan §5.2: last turn only). */
698
- function renderAssistantStatic(item: HistoryItem, opts: { isLast?: boolean }): void {
699
- const m = el("div", "msg assistant", thread());
700
- el("div", "who", m).textContent = "local agent";
701
- const bubble = el("div", "bubble", m);
702
- if (item.thinking && item.thinking.trim()) {
703
- const tb = el("details", "thinkbox", bubble); tb.open = false;
704
- el("summary", "", tb).textContent = "Thinking";
705
- el("div", "thinkbody", tb).textContent = item.thinking;
706
- }
707
- if (item.text) { const tn = el("span", "atext", bubble); tn.innerHTML = mdToHtml(item.text); highlightIn(tn); }
708
- for (const t of (item.tools || [])) {
709
- if (isMemoryToolName(t.name || "")) {
710
- const handle = memoryToolChip(bubble, t.name, t.args);
711
- if (t.result) handle.setResult(t.result);
712
- continue;
713
- }
714
- const wrap = el("div", "tool ok", bubble); // collapsed; click to expand
715
- wrap.innerHTML =
716
- '<div class="thead"><div class="ticon">⚙</div>' +
717
- '<span class="tname">' + esc(t.name || "tool") + '</span>' +
718
- '<span class="targs">' + esc(argSummary(t.args)) + '</span>' +
719
- '<span class="tstat"><span class="sdot"></span><span class="slabel">done</span></span>' +
720
- '<span class="caret">›</span></div>' +
721
- '<div class="tbody">' +
722
- '<div class="blk"><div class="blbl">arguments</div><pre class="aargs">' + esc(prettyArgs(t.args)) + '</pre></div>' +
723
- (t.result ? '<div class="blk tresult"><div class="blbl">result</div><pre class="ares">' + esc(String(t.result).slice(-12000)) + '</pre></div>' : '') +
724
- '</div>';
725
- wrap.querySelector(".thead")!.addEventListener("click", () => wrap.classList.toggle("open"));
726
- }
727
- if (item.text) {
728
- addMsgActions(m, item.text, { regenerate: !!opts.isLast }, { onRegenerate: doRegenerate });
729
- if (opts.isLast) {
730
- lastAssistantMsgEl = m;
731
- // History replay doesn't carry citations (HistoryItem has no
732
- // citations field — the server doesn't persist them across
733
- // reload), so there's nothing to restore here; this keeps
734
- // lastAssistantCitations honest (no stale carry-over from a
735
- // previous session) rather than claiming a citation set for a
736
- // reply that regenerate can't actually attribute one to.
737
- lastAssistantCitations = [];
738
- }
739
- }
740
- }
741
-
742
- /** Authoritative thread rebuild from a server `history` frame. */
743
- function renderHistory(items: HistoryItem[]): void {
744
- turnActive = false;
745
- curAssistant = null;
746
- ($("chat-send") as HTMLButtonElement).disabled = false;
747
- $("chat-stop").style.display = "none";
748
- renderQueue({});
749
- thread().innerHTML = "";
750
- lastUserMsgEl = null; lastAssistantMsgEl = null;
751
- if (!items || !items.length) { $("chat-hello").style.display = ""; return; }
752
- $("chat-hello").style.display = "none";
753
- const lastUserIdx = items.map((it) => it.role).lastIndexOf("user");
754
- const lastAssistantIdx = items.map((it) => it.role).lastIndexOf("assistant");
755
- items.forEach((it, i) => {
756
- if (it.role === "user") addUserMsg(it.text, null, { isLast: i === lastUserIdx, entryId: it.entryId });
757
- else renderAssistantStatic(it, { isLast: i === lastAssistantIdx });
758
- });
759
- stick(true);
760
- }
761
-
762
- /* — server -> client dispatch — */
763
- function handle(m: ServerMessage): void {
764
- switch (m.type) {
765
- case "ready":
766
- $("nav-model").textContent = m.model || ($("nav-model").textContent as string) || "no model";
767
- composer.visionCapable = !!m.vision; updateAttachHint(composer);
768
- composer.thinkingCapable = !!m.thinking; updateThinkingToggle(composer);
769
- // Per-model recommended sampling (generation_config.json, server-CLI
770
- // overrides applied) — replaces the old hardcoded SAMP_REC shape.
771
- composer.genDefaults = m.genDefaults || ({ temperature: null, topP: null, topK: null } as ReadyGenDefaults);
772
- refreshSamplingRecs(composer);
773
- if (composer.thinkingCapable) send({ type: "set_thinking", enabled: composer.thinkingOn });
774
- composerRefreshAdapters();
775
- // Restore this browser's codingTools preference (plan §5.4/§6.5).
776
- // Sent every `ready` (including reconnects): a fresh WS session
777
- // always starts codingToolsRequested=false server-side, so a
778
- // previously-on browser must re-assert it, and the coding_tools
779
- // frame that follows will report whether it's active yet or only
780
- // pending for the next new/opened chat.
781
- send({ type: "set_coding_tools", enabled: storedCodingToolsPreference() });
782
- // App-aware assistant (plan §6.6): a fresh WS session starts with no
783
- // stored context server-side (currentAppContext is null until the
784
- // first push) — every `ready` (initial connect AND reconnect) must
785
- // re-push so get_current_app_context / the ambient line are never
786
- // blind while a browser tab sits idle on a non-chat route.
787
- pushAppContext();
788
- // Reconnect: re-open the session we were in (fresh load has none).
789
- if (pendingResumePath) { send({ type: "open_session", path: pendingResumePath }); pendingResumePath = null; }
790
- break;
791
- case "coding_tools": renderCodingToolsState(m.active, m.pending); break;
792
- case "tool_approvals":
793
- renderToolApprovals(m.alwaysAllow, (tool) => forgetToolApproval(tool));
794
- break;
795
- case "history": renderHistory(m.items); break;
796
- case "sessions":
797
- currentSessionPath = m.activePath || null;
798
- renderSessions(sidebar, m.items, m.activePath, {
799
- onOpen: (path) => { send({ type: "open_session", path }); closeDrawer(); },
800
- onFork: (path) => send({ type: "fork_session", path }),
801
- onDelete: (path) => send({ type: "delete_session", path }),
802
- });
803
- break;
804
- case "context": renderContext(m as ContextFrame); break;
805
- // Sibling group for the last user message (edit-and-resend's `< i/n >`
806
- // toggle, plan §5.2) — see renderSiblingToggle/switchSiblingDir.
807
- case "siblings":
808
- siblingInfo = { entryId: m.entryId || null, index: m.index, count: m.count, siblingIds: m.siblingIds || [] };
809
- lastUserEntryId = siblingInfo.entryId;
810
- renderSiblingToggle(lastUserMsgEl, lastUserEntryId, siblingInfo, switchSiblingDir);
811
- break;
812
- case "turn_start": turnActive = true; startAssistant(); ($("chat-send") as HTMLButtonElement).disabled = true; $("chat-stop").style.display = "block"; break;
813
- case "text_delta": appendDelta(m.delta || ""); break;
814
- case "thinking_delta": appendThinkingDelta(m.delta || ""); break;
815
- case "tool_start": toolCard(m.callId, m.tool, m.args); break;
816
- case "tool_approval_request": approvalCard(m.callId, m.tool, m.args); break;
817
- case "tool_update": toolUpdate(m.callId, (m.chunk as string) || ""); break;
818
- case "tool_end": toolEnd(m.callId, m.ok, m.result); break;
819
- // clearOneShotArmed: a one-shot next_turn sampling override (if any
820
- // was armed) has now been consumed by the server for exactly this
821
- // turn — clear the local indicator in lockstep (plan §9 Phase 3 item 2).
822
- case "turn_end": finalizeMeta(); renderLane(m.lane as Lane | undefined); clearOneShotArmed(composer); endTurn(); break;
823
- case "queue_update": renderQueue(m); break;
824
- // A prompt/regenerate/edit_resend that fails before the server ever
825
- // reaches turn_start (no model selected, auth failure, etc.) never
826
- // sends turn_end, so this is also where a one-shot next_turn sampling
827
- // override must be cleared client-side — the server clears its own
828
- // copy on this same failure path (clearArmedOneShotOnFailedPrompt in
829
- // pi-web.ts), so the two stay in lockstep instead of the UI's "armed"
830
- // pill lingering after the override it referred to is already gone.
831
- case "error": showAgentError(m.message || "agent error"); toast(m.message || "agent error", "err"); clearOneShotArmed(composer); if (turnActive) endTurn(); break;
832
- // App-aware assistant (plan §6.6): navigate_app / spotlight_ui tool
833
- // calls arrive as these two frames. Navigation always routes through
834
- // location.hash (the SAME mechanism a nav-tab click uses — shell.ts's
835
- // hashchange listener does the actual section toggle), so it's
836
- // indistinguishable from the user clicking the tab themselves.
837
- case "ui_navigate":
838
- location.hash = `#/${m.route}`;
839
- break;
840
- // spotlight_ui: navigate first if the tool asked (route present), THEN
841
- // resolve+show — every route's section already lives in the DOM,
842
- // toggled by CSS (shell.ts's router()), so no waitForElement polling
843
- // is needed the way a client-side-routed app would require.
844
- case "ui_spotlight": {
845
- if (m.route) location.hash = `#/${m.route}`;
846
- const resolved = resolveSpotlightTarget(
847
- { ref: m.ref, label: m.label, selector: m.selector, target: m.target, message: m.message },
848
- lastSnapshot,
849
- );
850
- if (!resolved || !showSpotlight(resolved)) {
851
- toast("Couldn't find that on screen to point at.", "err");
852
- }
853
- break;
854
- }
855
- }
856
- }
857
-
858
- /* — composer submit — */
859
- function submit(): void {
860
- const box = $("chat-box") as HTMLTextAreaElement;
861
- const text = box.value.trim();
862
- // Client-side slash commands for session control (not sent to the model).
863
- const cmd = text.toLowerCase();
864
- if (cmd === "/reload" || cmd === "/new" || cmd === "/clear") {
865
- box.value = ""; box.style.height = "auto"; clearAttachments(composer); newChat();
866
- return;
867
- }
868
- if (!text && composer.attachments.length === 0) return;
869
- if (!connected) { toast("Not connected to the agent yet — reconnecting.", "err"); return; }
870
- const imgs = composer.attachments.filter((a) => a.kind === "image").map((a) => ({ data: a.data as string, mimeType: a.mimeType as string }));
871
- const combined = buildMessageText(composer, text);
872
- // entryId isn't known yet (assigned server-side); the next `siblings`
873
- // frame (sent on turn_end) backfills it via renderSiblingToggle's
874
- // entryId-mismatch check, same as a fresh page load's history replay.
875
- addUserMsg(text, composer.attachments as unknown as { kind: string; data?: string; mimeType?: string; name: string }[], { isLast: true });
876
- box.value = ""; box.style.height = "auto";
877
- // buildMessageText returns { text, citations } (RAG v1, plan §9 Phase 3
878
- // Axis 5): .text is the WS-outgoing prompt string (with the numbered
879
- // context block injected when retrieval mode fired); .citations stash
880
- // into pendingCitations so startAssistant() can attach them to the
881
- // AssistantMsgState this prompt's reply streams into once turn_start
882
- // arrives (empty array — the common case — is a no-op all the way
883
- // through: no Sources panel, no [n] linkification).
884
- pendingCitations = combined.citations;
885
- const frame: ClientMessage = imgs.length ? { type: "prompt", text: combined.text, images: imgs } : { type: "prompt", text: combined.text };
886
- // A composer submission is always a new user turn. If the model is still
887
- // streaming, the server passes it to pi with streamingBehavior:"followUp"
888
- // so it becomes the next turn instead of steering/mutating the current one.
889
- send(frame);
890
- clearAttachments(composer);
891
- }
892
- const scroll = () => $("chat-scroll");
893
- const atBottom = () => scroll().scrollHeight - scroll().scrollTop - scroll().clientHeight < 90;
894
- function stick(force?: boolean): void { if (force || atBottom()) scroll().scrollTop = scroll().scrollHeight; }
895
-
896
- /** Cmd/Ctrl+Shift+C (plan §7): reuses the exact per-message Copy button
897
- * (addMsgActions) on the last assistant message rather than duplicating
898
- * its clipboard logic — a synthetic click keeps the "Copied" flash
899
- * feedback consistent with the mouse path. */
900
- function copyLastResponse(): void {
901
- if (!lastAssistantMsgEl) { toast("No response to copy yet.", "err"); return; }
902
- const btn = lastAssistantMsgEl.querySelector(".msg-actions .maction") as HTMLButtonElement | null;
903
- if (btn) btn.click();
904
- }
905
-
906
- /** Cross-controller call from shell.ts's settings toggle (plan §5.4/§6.5)
907
- * — shell.ts owns the checkbox + localStorage mirror but no WebSocket, so
908
- * it calls this via controllers.chat.setCodingTools, same pattern as
909
- * newChat/copyLastResponse below. */
910
- function setCodingTools(enabled: boolean): void {
911
- send({ type: "set_coding_tools", enabled });
912
- }
913
-
914
- /** "Forget" a durable always-allow entry from the settings panel. REST,
915
- * not a WS frame — src/tool-approvals.ts is a plain server-side file, no
916
- * live session state to touch, matching the hf-token settings routes'
917
- * own GET/POST-over-fetch pattern. Re-renders the list from the response
918
- * rather than waiting on a tool_approvals WS frame (this connection's
919
- * session may not be the one that granted it, and REST is instant). */
920
- async function forgetToolApproval(tool: string): Promise<void> {
921
- const d = await api("/api/settings/tool-approvals", { method: "DELETE", body: { tool } })
922
- .catch((): ApiEnvelope => ({ ok: false, error: "request failed" }));
923
- if (!d.ok) { toast(d.error || "could not forget that tool", "err"); return; }
924
- renderToolApprovals((d as { alwaysAllow?: string[] }).alwaysAllow || [], (t) => forgetToolApproval(t));
925
- toast("Forgot “" + tool + "” — it will ask again next time", "ok");
926
- }
927
-
928
- return {
929
- init() {
930
- ($("chat-send") as HTMLButtonElement).onclick = submit;
931
- ($("chat-new") as HTMLButtonElement).onclick = newChat;
932
- ($("chat-stop") as HTMLButtonElement).onclick = () => { send({ type: "abort" }); $("chat-stop").style.display = "none"; };
933
- const thinkToggle = () => { composer.thinkingOn = !composer.thinkingOn; updateThinkingToggle(composer); send({ type: "set_thinking", enabled: composer.thinkingOn }); };
934
- ($("chat-think") as HTMLButtonElement).onclick = thinkToggle;
935
- $("chat-think").addEventListener("keydown", (e) => { if ((e as KeyboardEvent).key === "Enter" || (e as KeyboardEvent).key === " ") { e.preventDefault(); thinkToggle(); } });
936
- const adapterSel = $("chat-adapter") as HTMLSelectElement | null;
937
- if (adapterSel) adapterSel.onchange = () => {
938
- // Keep the routing table's notion of "selected for this chat" in
939
- // sync with the quick <select> — one source of truth for what's
940
- // selected, read by both surfaces (plan §5.6: "keep them consistent").
941
- adaptersPanel.selectedSpec = adapterSel.value || null;
942
- adaptersPanel.stackPicks.clear();
943
- onSelectAdapter(adapterSel, send as (obj: unknown) => boolean);
944
- };
945
- initAdaptersPanel(adaptersPanel, send as (obj: unknown) => boolean);
946
- initSampling(composer, send as (obj: unknown) => boolean);
947
- initSystemPrompt(composer, send as (obj: unknown) => boolean);
948
- composerRefreshAdapters();
949
- const sessSearch = $("chat-sess-search");
950
- if (sessSearch) sessSearch.addEventListener("input", () => applySessSearch(sidebar));
951
- const box = $("chat-box") as HTMLTextAreaElement;
952
- // Mention-picker keydown (arrows/Enter/Escape) must see Enter BEFORE
953
- // the submit-on-Enter handler below — addEventListener preserves
954
- // registration order, and initMentionPicker's handler only
955
- // preventDefault()s when its popover is actually open, so a plain
956
- // Enter with no picker open falls through to submit() untouched.
957
- initMentionPicker(composer, box, {
958
- onFileSelected: (att) => {
959
- const chip = document.querySelector<HTMLElement>('.attach-chip[data-att-id="' + att.id + '"]');
960
- if (chip) { chip.scrollIntoView({ block: "nearest", behavior: "smooth" }); chip.classList.add("pulse"); setTimeout(() => chip.classList.remove("pulse"), 1000); }
961
- },
962
- });
963
- box.addEventListener("keydown", (e) => {
964
- if ($("chat-mention-pop").classList.contains("open")) return; // picker owns Enter/arrows/Escape while open
965
- if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); submit(); }
966
- });
967
- box.addEventListener("input", () => { box.style.height = "auto"; box.style.height = Math.min(box.scrollHeight, 200) + "px"; });
968
- // Delegated (not per-chip) so the personalized chips memory-panel.ts
969
- // inserts on top of these four static ones (plan §5.1) work with zero
970
- // extra wiring — #chat-hello-chips is stable even though its contents
971
- // change (static chips today, +2 personalized ones once the first
972
- // /api/memory/status + /v1/adapters/available round trip resolves).
973
- $("chat-hello-chips").addEventListener("click", (e) => {
974
- const chip = (e.target as HTMLElement).closest<HTMLElement>(".chip");
975
- if (!chip) return;
976
- ($("chat-box") as HTMLTextAreaElement).value = chip.dataset.q || "";
977
- submit();
978
- });
979
- initMemoryPanel();
980
- // Attachments: + button, file picker, clipboard paste, drag-and-drop.
981
- ($("chat-attach-btn") as HTMLButtonElement).onclick = () => ($("chat-file-input") as HTMLInputElement).click();
982
- $("chat-file-input").addEventListener("change", (e) => { addFiles(composer, [...(e.target as HTMLInputElement).files || []]); (e.target as HTMLInputElement).value = ""; });
983
- box.addEventListener("paste", (e) => {
984
- const items = e.clipboardData && e.clipboardData.items;
985
- if (!items) return;
986
- const files: File[] = [];
987
- for (const it of items) if (it.kind === "file") { const f = it.getAsFile(); if (f) files.push(f); }
988
- if (files.length) { e.preventDefault(); addFiles(composer, files); }
989
- });
990
- const composerEl = document.querySelector<HTMLElement>("#s-chat .composer");
991
- if (composerEl) {
992
- ["dragenter", "dragover"].forEach((ev) => composerEl.addEventListener(ev, (e) => { e.preventDefault(); composerEl.classList.add("dragover"); }));
993
- ["dragleave", "drop"].forEach((ev) => composerEl.addEventListener(ev, (e) => {
994
- e.preventDefault();
995
- const de = e as DragEvent;
996
- if (ev === "drop" && de.dataTransfer && de.dataTransfer.files.length) addFiles(composer, [...de.dataTransfer.files]);
997
- if (ev === "drop" || !composerEl.contains(de.relatedTarget as Node)) composerEl.classList.remove("dragover");
998
- }));
999
- }
1000
- // Delegated copy buttons for rendered code blocks (the thread element
1001
- // is stable even though its contents re-render on every token).
1002
- thread().addEventListener("click", (e) => {
1003
- const target = e.target as HTMLElement;
1004
- const btn = target.closest && target.closest(".cbcopy");
1005
- if (!btn) return;
1006
- const code = btn.closest(".codeblock")?.querySelector("code");
1007
- if (!code || !navigator.clipboard) return;
1008
- navigator.clipboard.writeText(code.textContent || "").then(() => {
1009
- btn.textContent = "Copied"; setTimeout(() => { btn.textContent = "Copy"; }, 1200);
1010
- }).catch(() => {});
1011
- });
1012
- // Canvas v1 (plan §9 Phase 3): Preview|Source toggle for html/svg
1013
- // fences, delegated the same way as the copy button above.
1014
- wireCanvasToggle(thread());
1015
-
1016
- // App-aware assistant (plan §6.6): registered at document/window level
1017
- // (not scoped to the chat route being active) since chat.ts owns the
1018
- // one WebSocket for the whole app and must push context on every
1019
- // route, regardless of which route the user navigates TO or FROM.
1020
- // hashchange covers route changes; the MutationObserver covers wizard
1021
- // step changes (quantize/finetune/dataset); a MutationObserver on each
1022
- // overlay's `class` attribute covers view changes (memory/hub/adapters
1023
- // panel open/close) without touching those three modules.
1024
- window.addEventListener("hashchange", pushAppContext);
1025
- watchWizardSteps();
1026
- const overlayMo = new MutationObserver(() => pushAppContext());
1027
- for (const id of ["mem-overlay", "hub-overlay", "adapters-overlay"]) {
1028
- const el = $(id);
1029
- if (el) overlayMo.observe(el, { attributes: true, attributeFilter: ["class"] });
1030
- }
1031
- },
1032
- enter() {
1033
- if (!ws || ws.readyState > 1) connect();
1034
- $("chat-box").focus();
1035
- // Refresh the adapter list on every chat enter (web-ui-pass-plan.md
1036
- // #15's staleness half): a training job that completed while the user
1037
- // was elsewhere, or a manual drop into the adapters directory, should
1038
- // show up without needing a full page reload. Cheap (one GET) and the
1039
- // list is short.
1040
- composerRefreshAdapters();
1041
- },
1042
- leave() { /* keep WS warm; closing on every nav would drop in-flight turns */ },
1043
- // Exposed so the finetune controller can refresh the chat adapter chip
1044
- // immediately when a training job finishes, instead of waiting for the
1045
- // user to navigate back to Chat (which would also refresh it via enter()).
1046
- refreshAdapters: composerRefreshAdapters,
1047
- // Exposed for the global keyboard-shortcut sweep (plan §7): Cmd/Ctrl+Shift+O
1048
- // and Cmd/Ctrl+Shift+C, bound at document level so they work regardless
1049
- // of which control has focus while on the chat route.
1050
- newChat,
1051
- copyLastResponse,
1052
- // Exposed for shell.ts's settings modal (plan §5.4/§6.5) — it owns the
1053
- // checkbox/list DOM and localStorage, but has no WebSocket of its own.
1054
- setCodingTools,
1055
- forgetToolApproval,
1056
- };
1057
- }