turbollm 1.6.3 → 1.6.5

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 (102) hide show
  1. package/README.md +16 -9
  2. package/dist/cli.js +255 -32
  3. package/dist/webdist/assets/{AgentsScreen-DSJoisHr.js → AgentsScreen-D0J8ZEtf.js} +1 -1
  4. package/dist/webdist/assets/ChatScreen-7bIHB35Z.js +85 -0
  5. package/dist/webdist/assets/CustomizeScreen-z5568762.js +4 -0
  6. package/dist/webdist/assets/{DeveloperScreen-fnTDP0tf.js → DeveloperScreen-C5opqGrE.js} +1 -1
  7. package/dist/webdist/assets/{EnginesScreen-rlz0E1mk.js → EnginesScreen-NqAMc4rS.js} +6 -6
  8. package/dist/webdist/assets/ModelDetailDialog-CtjNu2Qc.js +29 -0
  9. package/dist/webdist/assets/ModelDirs-C-riHLWK.js +1 -0
  10. package/dist/webdist/assets/{ModelsScreen-BQF-ZHhj.js → ModelsScreen-DnASH2bF.js} +2 -2
  11. package/dist/webdist/assets/SettingsScreen-v-LAgJFH.js +1 -0
  12. package/dist/webdist/assets/{arc-DJj3Lyu5.js → arc-OPogq9vx.js} +1 -1
  13. package/dist/webdist/assets/{architectureDiagram-3BPJPVTR-BcgkjjsC.js → architectureDiagram-3BPJPVTR-DI3zlQZC.js} +1 -1
  14. package/dist/webdist/assets/{arrow-left-C8---sig.js → arrow-left-DWtpbvUs.js} +1 -1
  15. package/dist/webdist/assets/{blockDiagram-GPEHLZMM-C5iJ2c-N.js → blockDiagram-GPEHLZMM-bbqbctKR.js} +1 -1
  16. package/dist/webdist/assets/{c4Diagram-AAUBKEIU-BnsiE4U5.js → c4Diagram-AAUBKEIU-CGj5BFj-.js} +1 -1
  17. package/dist/webdist/assets/channel-aq4Rip4D.js +1 -0
  18. package/dist/webdist/assets/check-CMGnr_Ph.js +1 -0
  19. package/dist/webdist/assets/chevron-right-CIKvJUaG.js +1 -0
  20. package/dist/webdist/assets/{chunk-2J33WTMH-D_Hm4Lii.js → chunk-2J33WTMH-CZKSYaHn.js} +1 -1
  21. package/dist/webdist/assets/{chunk-4BX2VUAB-Cooqn_zH.js → chunk-4BX2VUAB-D3SQCGwz.js} +1 -1
  22. package/dist/webdist/assets/{chunk-55IACEB6-C2XDZnC0.js → chunk-55IACEB6-DDmZTA0L.js} +1 -1
  23. package/dist/webdist/assets/{chunk-727SXJPM-CCEeI48H.js → chunk-727SXJPM-DRutYNQM.js} +1 -1
  24. package/dist/webdist/assets/{chunk-AQP2D5EJ-7FdU3GXV.js → chunk-AQP2D5EJ-B_u0ow9I.js} +1 -1
  25. package/dist/webdist/assets/{chunk-FMBD7UC4-Dpyt8woB.js → chunk-FMBD7UC4-Cu73-Koh.js} +1 -1
  26. package/dist/webdist/assets/{chunk-ND2GUHAM-ildY-Wx8.js → chunk-ND2GUHAM-Z71l0U4D.js} +1 -1
  27. package/dist/webdist/assets/{chunk-QZHKN3VN-DfdDD-h6.js → chunk-QZHKN3VN-D2PeraEx.js} +1 -1
  28. package/dist/webdist/assets/circle-check-DXOwCXEz.js +1 -0
  29. package/dist/webdist/assets/circle-x-caNFmCHm.js +1 -0
  30. package/dist/webdist/assets/classDiagram-4FO5ZUOK-C5KHEB9t.js +1 -0
  31. package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-C5KHEB9t.js +1 -0
  32. package/dist/webdist/assets/common-CQQGhjlM.js +1 -0
  33. package/dist/webdist/assets/{copy-button-Sp-U8ULJ.js → copy-button-Dt4Afcgb.js} +1 -1
  34. package/dist/webdist/assets/{cose-bilkent-S5V4N54A-DggDgWVR.js → cose-bilkent-S5V4N54A-D3rxBv0J.js} +1 -1
  35. package/dist/webdist/assets/{dagre-BM42HDAG-C3owUH7E.js → dagre-BM42HDAG--G2XvGhI.js} +1 -1
  36. package/dist/webdist/assets/{diagram-2AECGRRQ-Ch2E5js7.js → diagram-2AECGRRQ-Te1rwFql.js} +1 -1
  37. package/dist/webdist/assets/{diagram-5GNKFQAL-DLeGX2xz.js → diagram-5GNKFQAL-B5vALx3-.js} +1 -1
  38. package/dist/webdist/assets/{diagram-KO2AKTUF-DpxkKgZH.js → diagram-KO2AKTUF-CBxjZi0b.js} +1 -1
  39. package/dist/webdist/assets/{diagram-LMA3HP47-ia4luRgC.js → diagram-LMA3HP47-BGiirNkw.js} +1 -1
  40. package/dist/webdist/assets/{diagram-OG6HWLK6-BMmkGNMK.js → diagram-OG6HWLK6-BVgMIJkt.js} +1 -1
  41. package/dist/webdist/assets/dialog-coezioYj.js +1 -0
  42. package/dist/webdist/assets/dropdown-menu-Ce842M-W.js +1 -0
  43. package/dist/webdist/assets/{erDiagram-TEJ5UH35-DkKBBG05.js → erDiagram-TEJ5UH35-DowgN2O9.js} +1 -1
  44. package/dist/webdist/assets/{flowDiagram-I6XJVG4X-kUtoUBI6.js → flowDiagram-I6XJVG4X-BD0xs8q_.js} +1 -1
  45. package/dist/webdist/assets/{ganttDiagram-6RSMTGT7-DQReYR3Z.js → ganttDiagram-6RSMTGT7-BHHhhU_7.js} +1 -1
  46. package/dist/webdist/assets/{gitGraphDiagram-PVQCEYII-B2kU7JBk.js → gitGraphDiagram-PVQCEYII-Brchy1Vz.js} +1 -1
  47. package/dist/webdist/assets/index-CVyS_zSS.css +1 -0
  48. package/dist/webdist/assets/{index-CjGc01NF.js → index-oSSYX1Lk.js} +4 -4
  49. package/dist/webdist/assets/{infoDiagram-5YYISTIA-k1S105HR.js → infoDiagram-5YYISTIA-D4HdfOcs.js} +1 -1
  50. package/dist/webdist/assets/{ishikawaDiagram-YF4QCWOH-BgPQRD_p.js → ishikawaDiagram-YF4QCWOH-wmMjgjbp.js} +1 -1
  51. package/dist/webdist/assets/{journeyDiagram-JHISSGLW-_aNOepbu.js → journeyDiagram-JHISSGLW-CaF09KKI.js} +1 -1
  52. package/dist/webdist/assets/{kanban-definition-UN3LZRKU-ooXgg2sY.js → kanban-definition-UN3LZRKU-BuBI9QG2.js} +1 -1
  53. package/dist/webdist/assets/{linear-B6ijN2hK.js → linear-DHVGKuTX.js} +1 -1
  54. package/dist/webdist/assets/{mermaid.core-C39fo6yP.js → mermaid.core-CrU-xSQY.js} +4 -4
  55. package/dist/webdist/assets/{mindmap-definition-RKZ34NQL-Ci1PW-rt.js → mindmap-definition-RKZ34NQL-cBLMFEJ9.js} +1 -1
  56. package/dist/webdist/assets/pdf.worker.min-DEtVeC4l.mjs +29 -0
  57. package/dist/webdist/assets/pencil-BBioAa4x.js +1 -0
  58. package/dist/webdist/assets/{personas-CIW1sEuT.js → personas-DaUrwq7m.js} +9 -5
  59. package/dist/webdist/assets/{pieDiagram-4H26LBE5-SVL4rTNg.js → pieDiagram-4H26LBE5-BJnfOXlL.js} +1 -1
  60. package/dist/webdist/assets/plus-C8MMVqVA.js +1 -0
  61. package/dist/webdist/assets/{quadrantDiagram-W4KKPZXB-sTVWSdlQ.js → quadrantDiagram-W4KKPZXB-Cm-qlQtE.js} +1 -1
  62. package/dist/webdist/assets/{radix-9WP-OkMY.js → radix-BY-fjzwm.js} +8 -8
  63. package/dist/webdist/assets/refresh-cw-DTqN58WZ.js +1 -0
  64. package/dist/webdist/assets/{requirementDiagram-4Y6WPE33-3R4K8jZs.js → requirementDiagram-4Y6WPE33-DHY3RUt1.js} +1 -1
  65. package/dist/webdist/assets/{sankeyDiagram-5OEKKPKP-BMozn5XV.js → sankeyDiagram-5OEKKPKP-DQkDAi3z.js} +1 -1
  66. package/dist/webdist/assets/save-PN9--sf1.js +1 -0
  67. package/dist/webdist/assets/{sequenceDiagram-3UESZ5HK-B0bhq6qo.js → sequenceDiagram-3UESZ5HK-OJf-ZzIm.js} +1 -1
  68. package/dist/webdist/assets/skeleton-Dpg8YusE.js +1 -0
  69. package/dist/webdist/assets/{stateDiagram-AJRCARHV-CXaf-H6k.js → stateDiagram-AJRCARHV-QupYS2FV.js} +1 -1
  70. package/dist/webdist/assets/stateDiagram-v2-BHNVJYJU-DPP0kwbC.js +1 -0
  71. package/dist/webdist/assets/{timeline-definition-PNZ67QCA-BUO22RTP.js → timeline-definition-PNZ67QCA-B7kdKKeO.js} +1 -1
  72. package/dist/webdist/assets/trash-2-C5mszq8p.js +1 -0
  73. package/dist/webdist/assets/{vennDiagram-CIIHVFJN-B6l8WK-i.js → vennDiagram-CIIHVFJN-DCckmGsZ.js} +1 -1
  74. package/dist/webdist/assets/{wardley-L42UT6IY-nBQZIDHO.js → wardley-L42UT6IY-C66hVI6t.js} +1 -1
  75. package/dist/webdist/assets/{wardleyDiagram-YWT4CUSO-Bl5nA5Sj.js → wardleyDiagram-YWT4CUSO-0WD2JYDy.js} +1 -1
  76. package/dist/webdist/assets/{wrench-2q903Ji-.js → wrench-BotIMKYs.js} +1 -1
  77. package/dist/webdist/assets/{xychartDiagram-2RQKCTM6-BfRB7FkA.js → xychartDiagram-2RQKCTM6-Dc0w49oN.js} +1 -1
  78. package/dist/webdist/index.html +3 -3
  79. package/package.json +1 -1
  80. package/dist/webdist/assets/ChatScreen-CPTQTPDq.js +0 -29
  81. package/dist/webdist/assets/CustomizeScreen-CveMZwK6.js +0 -4
  82. package/dist/webdist/assets/ModelDetailDialog-b1xxFqb8.js +0 -29
  83. package/dist/webdist/assets/ModelDirs-DPJoCOcI.js +0 -1
  84. package/dist/webdist/assets/SettingsScreen-Z4AoirUg.js +0 -1
  85. package/dist/webdist/assets/alert-dialog-DzOwQljM.js +0 -1
  86. package/dist/webdist/assets/channel-DidzH_JR.js +0 -1
  87. package/dist/webdist/assets/check-CRM4LOGX.js +0 -1
  88. package/dist/webdist/assets/chevron-right-C18nBpLx.js +0 -1
  89. package/dist/webdist/assets/circle-check-C9nr7e7f.js +0 -1
  90. package/dist/webdist/assets/circle-x-CrulSiVz.js +0 -1
  91. package/dist/webdist/assets/classDiagram-4FO5ZUOK-DrgybMsY.js +0 -1
  92. package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-DrgybMsY.js +0 -1
  93. package/dist/webdist/assets/common-B6XHnwTG.js +0 -1
  94. package/dist/webdist/assets/dialog-DLncpxeJ.js +0 -1
  95. package/dist/webdist/assets/index-5NYGYU_t.css +0 -1
  96. package/dist/webdist/assets/pencil-CvIU0pkN.js +0 -1
  97. package/dist/webdist/assets/plus-3YM8ko_h.js +0 -1
  98. package/dist/webdist/assets/refresh-cw-BrUDSVRs.js +0 -1
  99. package/dist/webdist/assets/save-CeI7y6CK.js +0 -1
  100. package/dist/webdist/assets/skeleton-Dm2BPYbG.js +0 -1
  101. package/dist/webdist/assets/stateDiagram-v2-BHNVJYJU-BYFRKnq-.js +0 -1
  102. package/dist/webdist/assets/trash-2-D1fUAajM.js +0 -1
package/README.md CHANGED
@@ -148,7 +148,8 @@ the detail:
148
148
  picker so you can choose which one to download.
149
149
  - **Quant recommendation per GPU** and a **VRAM-fit verdict** so you pick a quant that
150
150
  actually fits before you commit.
151
- - **Primary download folder**, real-time **measured t/s per model**, and **delete-from-disk**.
151
+ - **Primary download folder**, real-time **measured t/s per model**, **delete-from-disk**, and
152
+ **pin your favourites to the top of the list**.
152
153
 
153
154
  </details>
154
155
 
@@ -158,20 +159,24 @@ the detail:
158
159
  <br/>
159
160
 
160
161
  - **Auto-benchmark on load** derives fast defaults for your exact GPU.
161
- - **Recommended sampling from the model card** — auto-tune reads the model's Hugging Face card
162
- (falling back to the original model behind a requant) and prefills the author's recommended
163
- `temperature / top_k / top_p / min_p`. No recommendation your sampling is left untouched.
162
+ - **Recommended sampling from Hugging Face** — auto-tune checks a repo's structured params /
163
+ `generation_config.json` sidecar first when the quantizer publishes one (exact values, no
164
+ guessing), then falls back to reading the model's card (and the original model behind a
165
+ requant) and prefills the author's recommended `temperature / top_k / top_p / min_p`. No
166
+ recommendation → your sampling is left untouched.
164
167
  - **Real measured tokens/sec** in the model list — **live** while generating, **last-session**
165
168
  when idle (never a synthetic estimate).
166
169
  - **Full load-parameter UI**, a superset of what other tools expose: context length, GPU offload
167
170
  (`-ngl`), **MoE CPU-offload (`--n-cpu-moe`)**, parallel slots, **KV-cache quant type** (incl.
168
171
  low-bit on supporting forks), CPU threads, flash attention, and **speculative decoding (NextN /
169
- MTP / draft)**.
172
+ MTP / draft, with a configurable draft min/max window)**.
170
173
  - **Fast by default:** flash attention on, NextN self-speculative decoding on for models that
171
174
  carry a draft head, threads auto — safely gated to what your engine actually accepts.
172
175
  - **Multi-GPU, per model** — split a model across cards (layer/row split + main-GPU pick on
173
176
  llama.cpp, tensor-parallel on vLLM). Defaults are no-ops, so single-GPU rigs are untouched.
174
- - **Saved per-model profiles** — tune once, and it loads that way every time.
177
+ - **Saved per-model profiles, per engine** — tune once per (model, engine) pair, so
178
+ switching engines (or between two installs of the same engine, e.g. a fork) never
179
+ overwrites another engine's tuning for the same model.
175
180
 
176
181
  </details>
177
182
 
@@ -193,11 +198,13 @@ the detail:
193
198
  Creative · Research) per conversation, no prompt-wrangling required. The **Designer** persona
194
199
  produces polished, self-contained, previewable designs by default.
195
200
  - **Edit, regenerate, delete, copy** any message; **persistent, searchable conversations**
196
- with rename, delete, and **auto-generated titles**.
201
+ with rename, delete, and **auto-generated titles**, organized into **drag-resizable,
202
+ collapsible folders** you create/rename/move conversations into.
197
203
  - **Per-chat system prompt** and **per-chat sampling** overrides — temperature, top-p/k, min-p,
198
204
  repeat/presence/frequency penalties, and **stop strings**.
199
- - **Image input** for vision models, and **TurboLLM Expert** a built-in assistant that knows
200
- the app and your hardware for onboarding and troubleshooting without leaving the UI.
205
+ - **Image input** for vision models, **PDF and code/text attachments** (real extracted text,
206
+ not raw bytes), and **TurboLLM Expert** a built-in assistant that knows the app and your
207
+ hardware for onboarding and troubleshooting without leaving the UI.
201
208
  - **Agentic tools** — built-in `web_search` (Tavily), `fetch_url`, and sandboxed `run_code`, plus
202
209
  an **MCP marketplace** in Customize: one-click connect for hosted MCPs (GitHub, Linear, Stripe,
203
210
  Atlassian, Neon, Supabase, Cloudflare, Zapier, Apify, Mixpanel) and open-source local MCPs
package/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ import { randomUUID } from "crypto";
12
12
  import { cpSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
13
13
  import { homedir } from "os";
14
14
  import { dirname, join } from "path";
15
- var SCHEMA_VERSION = 2;
15
+ var SCHEMA_VERSION = 3;
16
16
  var ValueError = class extends Error {
17
17
  constructor(field, msg2) {
18
18
  super(`${field}: ${msg2}`);
@@ -176,7 +176,7 @@ function migrate(raw, _from) {
176
176
  }
177
177
  cfg2.version = SCHEMA_VERSION;
178
178
  for (const [k, v] of Object.entries(raw)) {
179
- if (!(k in cfg2) && k !== "engine" && k !== "host" && k !== "port") {
179
+ if (k !== "engine" && k !== "host" && k !== "port" && k !== "version") {
180
180
  ;
181
181
  cfg2[k] = v;
182
182
  }
@@ -217,6 +217,11 @@ function normalize(c) {
217
217
  c.modelDirs ??= [];
218
218
  c.primaryModelDir ??= "";
219
219
  c.modelProfiles ??= {};
220
+ for (const [modelKey, val] of Object.entries(c.modelProfiles)) {
221
+ if (val && typeof val === "object" && typeof val.ctx === "number") {
222
+ c.modelProfiles[modelKey] = { "*": { profile: val, updatedAt: (/* @__PURE__ */ new Date()).toISOString() } };
223
+ }
224
+ }
220
225
  c.benchResults ??= {};
221
226
  c.autoLoadOnStart ??= false;
222
227
  c.featuredOverrideUrl ??= "";
@@ -299,6 +304,27 @@ function isAbsolutePath(p) {
299
304
  function findEngine(engines, id) {
300
305
  return engines.find((e) => e.id === id);
301
306
  }
307
+ function getModelProfile(cfg2, modelKey, engineId) {
308
+ const byEngine = cfg2.modelProfiles[modelKey];
309
+ if (!byEngine) return void 0;
310
+ const exact = byEngine[engineId];
311
+ if (exact) return exact.profile;
312
+ let mostRecent;
313
+ for (const entry of Object.values(byEngine)) {
314
+ if (!mostRecent || entry.updatedAt >= mostRecent.updatedAt) mostRecent = entry;
315
+ }
316
+ return mostRecent?.profile;
317
+ }
318
+ function setModelProfile(cfg2, modelKey, engineId, profile) {
319
+ ;
320
+ (cfg2.modelProfiles[modelKey] ??= {})[engineId] = { profile, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
321
+ }
322
+ function deleteModelProfile(cfg2, modelKey, engineId) {
323
+ const byEngine = cfg2.modelProfiles[modelKey];
324
+ if (!byEngine) return;
325
+ delete byEngine[engineId];
326
+ if (Object.keys(byEngine).length === 0) delete cfg2.modelProfiles[modelKey];
327
+ }
302
328
  function clampMaxTokens(requested, limit) {
303
329
  if (!Number.isFinite(limit) || limit <= 0) return requested ?? void 0;
304
330
  if (requested == null || !Number.isFinite(requested) || requested <= 0) return limit;
@@ -2649,7 +2675,10 @@ function detectGpus() {
2649
2675
  const [name, mb] = line.split(",");
2650
2676
  return { name: (name ?? "").trim(), vramMb: parseInt((mb ?? "").trim(), 10) || 0, vendor: "nvidia" };
2651
2677
  }).filter((g) => g.vramMb > 0);
2652
- if (gpus.length) return gpus;
2678
+ if (gpus.length) {
2679
+ console.log(`[sysinfo] nvidia-smi enumerated ${gpus.length} GPU(s): ${gpus.map((g) => `${g.name}=${g.vramMb}MB`).join(", ")}`);
2680
+ return gpus;
2681
+ }
2653
2682
  } catch {
2654
2683
  }
2655
2684
  if (process.platform === "darwin") {
@@ -2670,6 +2699,11 @@ function detectGpus() {
2670
2699
  return [];
2671
2700
  }
2672
2701
  function enumWindowsGpus() {
2702
+ try {
2703
+ const amd = rocmSmiGpus();
2704
+ if (amd.length) return amd;
2705
+ } catch {
2706
+ }
2673
2707
  const ps = 'Get-CimInstance Win32_VideoController | ForEach-Object { "$($_.Name)|$($_.AdapterRAM)" }';
2674
2708
  const out = execFileSync2("powershell", ["-NoProfile", "-NonInteractive", "-Command", ps], {
2675
2709
  timeout: 8e3,
@@ -2682,6 +2716,37 @@ function enumWindowsGpus() {
2682
2716
  return { name: nm, vramMb: bytes > 0 ? Math.round(bytes / 1e6) : 0, vendor: classifyVendor(nm) };
2683
2717
  }).filter((g) => g.name && g.vendor !== "unknown");
2684
2718
  }
2719
+ function rocmSmiGpus() {
2720
+ const memJson = execFileSync2("rocm-smi", ["--showmeminfo", "vram", "--json"], {
2721
+ timeout: 8e3,
2722
+ windowsHide: true
2723
+ }).toString();
2724
+ let nameJson = "";
2725
+ try {
2726
+ nameJson = execFileSync2("rocm-smi", ["--showproductname", "--json"], {
2727
+ timeout: 8e3,
2728
+ windowsHide: true
2729
+ }).toString();
2730
+ } catch {
2731
+ }
2732
+ return parseRocmSmi(memJson, nameJson);
2733
+ }
2734
+ function parseRocmSmi(memJson, nameJson = "") {
2735
+ const mem = JSON.parse(memJson);
2736
+ let names = {};
2737
+ if (nameJson.trim()) names = JSON.parse(nameJson);
2738
+ const gpus = [];
2739
+ for (const [card, fields] of Object.entries(mem)) {
2740
+ const totalKey = Object.keys(fields).find((k) => /VRAM Total Memory/i.test(k));
2741
+ const bytes = totalKey ? parseInt(String(fields[totalKey]).trim(), 10) || 0 : 0;
2742
+ if (bytes <= 0) continue;
2743
+ const nameFields = names[card] ?? {};
2744
+ const nameKey = Object.keys(nameFields).find((k) => /(Card Series|Card Model|Product Name|Device Name)/i.test(k));
2745
+ const name = (nameKey ? String(nameFields[nameKey]).trim() : "") || "AMD Radeon GPU";
2746
+ gpus.push({ name, vramMb: Math.round(bytes / 1e6), vendor: "amd" });
2747
+ }
2748
+ return gpus;
2749
+ }
2685
2750
  function enumLinuxGpus() {
2686
2751
  const out = execFileSync2("sh", ["-c", "lspci -mm 2>/dev/null | grep -iE 'VGA|3D|Display'"], {
2687
2752
  timeout: 8e3
@@ -3303,8 +3368,10 @@ var Scanner = class {
3303
3368
  const m = basename(f.path).match(SPLIT_RE);
3304
3369
  if (m) {
3305
3370
  const gkey = `${m[1]}|${m[3]}`;
3306
- const g = splits.get(gkey) ?? { shards: [], total: Number(m[3]) };
3307
- g.shards.push(f);
3371
+ const g = splits.get(gkey) ?? { shards: /* @__PURE__ */ new Map(), total: Number(m[3]) };
3372
+ const idx = Number(m[2]);
3373
+ const prev = g.shards.get(idx);
3374
+ if (!prev || f.path.localeCompare(prev.path) < 0) g.shards.set(idx, f);
3308
3375
  splits.set(gkey, g);
3309
3376
  } else {
3310
3377
  singles.push(f);
@@ -3315,10 +3382,17 @@ var Scanner = class {
3315
3382
  await tick();
3316
3383
  }
3317
3384
  for (const { shards, total } of splits.values()) {
3318
- shards.sort((a, b) => a.path.localeCompare(b.path));
3319
- const first = shards[0];
3320
- const totalSize = shards.reduce((s, x) => s + x.size, 0);
3321
- const incomplete = shards.length !== total;
3385
+ const present = [...shards.values()].sort((a, b) => a.path.localeCompare(b.path));
3386
+ const first = present[0];
3387
+ const totalSize = present.reduce((s, x) => s + x.size, 0);
3388
+ let incomplete = shards.size !== total;
3389
+ if (!incomplete) {
3390
+ for (let i = 1; i <= total; i++)
3391
+ if (!shards.has(i)) {
3392
+ incomplete = true;
3393
+ break;
3394
+ }
3395
+ }
3322
3396
  entries.push(await this.entryFor(first.path, totalSize, first.mtime, dir, mmprojPath, incomplete));
3323
3397
  await tick();
3324
3398
  }
@@ -3795,16 +3869,24 @@ function profileToArgs(p, m, caps, cores = 0) {
3795
3869
  if (p.chatTemplateFile && has("--chat-template-file")) a.push("--chat-template-file", p.chatTemplateFile);
3796
3870
  const specType = has("--spec-type");
3797
3871
  const specAccepts = (v) => caps.flags.length === 0 || caps.flags.includes(`spec-type:${v}`);
3872
+ let specActive = false;
3798
3873
  if (p.speculative === "mtp" && p.mtpHeadPath && has("--mtp-head")) {
3799
3874
  if (specType) a.push("--spec-type", "mtp");
3800
3875
  a.push("--mtp-head", p.mtpHeadPath);
3876
+ specActive = true;
3801
3877
  } else if (p.speculative === "nextn" && specType && has("--model-draft")) {
3802
3878
  const nextnVal = ["nextn", "draft-mtp"].find((v) => specAccepts(v));
3803
- if (nextnVal) a.push("--spec-type", nextnVal, "--model-draft", m.path);
3879
+ if (nextnVal) {
3880
+ a.push("--spec-type", nextnVal, "--model-draft", m.path);
3881
+ specActive = true;
3882
+ }
3804
3883
  } else if (p.speculative === "draft" && p.draftModelPath && has("--model-draft")) {
3805
3884
  if (specType) a.push("--spec-type", "draft");
3806
- a.push("--model-draft", p.draftModelPath, "--draft-max", "16", "--draft-min", "1");
3885
+ a.push("--model-draft", p.draftModelPath);
3886
+ specActive = true;
3807
3887
  }
3888
+ if (specActive && has("--draft-max")) a.push("--draft-max", String(p.draftMax ?? 16));
3889
+ if (specActive && has("--draft-min")) a.push("--draft-min", String(p.draftMin ?? 1));
3808
3890
  if (p.sampling.temp !== 0.8 && has("--temp")) a.push("--temp", String(p.sampling.temp));
3809
3891
  if (p.sampling.topP !== 0.95 && has("--top-p")) a.push("--top-p", String(p.sampling.topP));
3810
3892
  if (p.sampling.topK !== 40 && has("--top-k")) a.push("--top-k", String(p.sampling.topK));
@@ -3851,7 +3933,10 @@ function safeJson(s) {
3851
3933
  }
3852
3934
  }
3853
3935
  function rowToConv(r) {
3854
- return { id: r.id, title: r.title, systemPrompt: r.system_prompt, modelKey: r.model_key, sampling: safeJson(r.sampling), expertMode: r.expert_mode === 1, toolPolicy: r.tool_policy ?? void 0, kind: r.kind === "agent" ? "agent" : "chat", createdAt: r.created_at, updatedAt: r.updated_at };
3936
+ return { id: r.id, title: r.title, systemPrompt: r.system_prompt, modelKey: r.model_key, sampling: safeJson(r.sampling), expertMode: r.expert_mode === 1, toolPolicy: r.tool_policy ?? void 0, kind: r.kind === "agent" ? "agent" : "chat", folderId: r.folder_id ?? null, createdAt: r.created_at, updatedAt: r.updated_at };
3937
+ }
3938
+ function rowToFolder(r) {
3939
+ return { id: r.id, name: r.name, sortOrder: r.sort_order, createdAt: r.created_at, updatedAt: r.updated_at };
3855
3940
  }
3856
3941
  function rowToAgentRun(r) {
3857
3942
  return {
@@ -3960,6 +4045,18 @@ var ConversationStore = class {
3960
4045
  PRAGMA user_version = 9;
3961
4046
  `);
3962
4047
  }
4048
+ if (v < 10) {
4049
+ this.db.exec(`
4050
+ CREATE TABLE IF NOT EXISTS folders (
4051
+ id TEXT PRIMARY KEY, name TEXT NOT NULL,
4052
+ sort_order INTEGER NOT NULL DEFAULT 0,
4053
+ created_at TEXT NOT NULL, updated_at TEXT NOT NULL
4054
+ );
4055
+ ALTER TABLE conversations ADD COLUMN folder_id TEXT;
4056
+ CREATE INDEX IF NOT EXISTS idx_conversations_folder ON conversations(folder_id);
4057
+ PRAGMA user_version = 10;
4058
+ `);
4059
+ }
3963
4060
  }
3964
4061
  listConversations(q, kind = "all") {
3965
4062
  if (q) {
@@ -3981,7 +4078,7 @@ var ConversationStore = class {
3981
4078
  createConversation(partial) {
3982
4079
  const now = (/* @__PURE__ */ new Date()).toISOString();
3983
4080
  const id = randomUUID3();
3984
- this.db.prepare(`INSERT INTO conversations (id,title,system_prompt,model_key,sampling,expert_mode,tool_policy,kind,created_at,updated_at) VALUES ($id,$title,$sp,$mk,$samp,$expert,$tp,$kind,$now,$now)`).run({ $id: id, $title: partial?.title ?? "New chat", $sp: partial?.systemPrompt ?? "", $mk: partial?.modelKey ?? "", $samp: JSON.stringify(partial?.sampling ?? {}), $expert: partial?.expertMode ? 1 : 0, $tp: partial?.toolPolicy ?? null, $kind: partial?.kind ?? "chat", $now: now });
4081
+ this.db.prepare(`INSERT INTO conversations (id,title,system_prompt,model_key,sampling,expert_mode,tool_policy,kind,folder_id,created_at,updated_at) VALUES ($id,$title,$sp,$mk,$samp,$expert,$tp,$kind,$fid,$now,$now)`).run({ $id: id, $title: partial?.title ?? "New chat", $sp: partial?.systemPrompt ?? "", $mk: partial?.modelKey ?? "", $samp: JSON.stringify(partial?.sampling ?? {}), $expert: partial?.expertMode ? 1 : 0, $tp: partial?.toolPolicy ?? null, $kind: partial?.kind ?? "chat", $fid: partial?.folderId ?? null, $now: now });
3985
4082
  return this.getConversation(id);
3986
4083
  }
3987
4084
  getConversation(id, withMessages = false) {
@@ -4092,6 +4189,41 @@ var ConversationStore = class {
4092
4189
  const row = this.db.prepare(`SELECT * FROM messages WHERE conv_id = $id ORDER BY seq DESC LIMIT 1`).get({ $id: convId });
4093
4190
  return row ? rowToMsg(row) : null;
4094
4191
  }
4192
+ // ── Folder methods (v10 migration) ────────────────────────────────────────
4193
+ /** All folders in insertion order (sort_order asc, then created_at). */
4194
+ listFolders() {
4195
+ return this.db.prepare(`SELECT * FROM folders ORDER BY sort_order ASC, created_at ASC`).all().map(rowToFolder);
4196
+ }
4197
+ getFolder(id) {
4198
+ const row = this.db.prepare(`SELECT * FROM folders WHERE id = $id`).get({ $id: id });
4199
+ return row ? rowToFolder(row) : null;
4200
+ }
4201
+ createFolder(name) {
4202
+ const id = randomUUID3();
4203
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4204
+ this.db.prepare(`INSERT INTO folders (id,name,sort_order,created_at,updated_at) VALUES ($id,$name,0,$now,$now)`).run({ $id: id, $name: name, $now: now });
4205
+ return this.getFolder(id);
4206
+ }
4207
+ renameFolder(id, name) {
4208
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4209
+ return this.db.prepare(`UPDATE folders SET name = $name, updated_at = $now WHERE id = $id`).run({ $id: id, $name: name, $now: now }).changes > 0;
4210
+ }
4211
+ /** Delete a folder. Member conversations are UNASSIGNED (folder_id → NULL), never
4212
+ * cascade-deleted. Returns false if the folder did not exist. */
4213
+ deleteFolder(id) {
4214
+ this.db.prepare(`UPDATE conversations SET folder_id = NULL WHERE folder_id = $id`).run({ $id: id });
4215
+ return this.db.prepare(`DELETE FROM folders WHERE id = $id`).run({ $id: id }).changes > 0;
4216
+ }
4217
+ /** Move a conversation into a folder (or out of any folder when folderId is null).
4218
+ * When folderId is non-null the folder must exist. Returns:
4219
+ * - { ok: true } on success,
4220
+ * - { ok: false, reason } when the conversation or the target folder is missing. */
4221
+ moveConversationToFolder(convId, folderId) {
4222
+ if (folderId !== null && !this.getFolder(folderId)) return { ok: false, reason: "folder_not_found" };
4223
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4224
+ const changed = this.db.prepare(`UPDATE conversations SET folder_id = $fid, updated_at = $now WHERE id = $id`).run({ $id: convId, $fid: folderId, $now: now }).changes > 0;
4225
+ return changed ? { ok: true } : { ok: false, reason: "conversation_not_found" };
4226
+ }
4095
4227
  // ── Agent run methods (v8 migration) ──────────────────────────────────────
4096
4228
  createAgentRun(params) {
4097
4229
  const id = randomUUID3();
@@ -4303,6 +4435,31 @@ var HfClient = class {
4303
4435
  fetchModelCard(repo) {
4304
4436
  return this.getCard(repo, CARD_EXTRACT_MAX);
4305
4437
  }
4438
+ /** Fetch a repo's structured generation-params sidecar, if it has one — some quantizers
4439
+ * (e.g. unsloth) publish a root-level `params` JSON file with the exact recommended
4440
+ * sampling values (`{"temperature":1.0,"top_k":0,"top_p":1.0,"min_p":0.0,"stop":[…]}`)
4441
+ * alongside the GGUF shards. When present this is a strictly better signal than the
4442
+ * README heuristic/LLM extraction (exact values, no parsing/inference needed) — auto-tune
4443
+ * tries it first. Tries `params` then the more standard `generation_config.json` name.
4444
+ * Best-effort: '' when neither exists or the repo is unreachable, never throws. */
4445
+ async fetchGenerationParams(repo) {
4446
+ for (const name of ["params", "generation_config.json"]) {
4447
+ const url = `${BASE}/${repo}/raw/main/${name}`;
4448
+ const now = Date.now();
4449
+ const hit = this.cache.get(url);
4450
+ if (hit && now - hit.at < CACHE_TTL_MS) return hit.value;
4451
+ try {
4452
+ const res = await fetch(url, { headers: this.authHeaders(), redirect: "follow" });
4453
+ if (!res.ok) continue;
4454
+ const raw = await res.text();
4455
+ this.cache.set(url, { at: now, value: raw });
4456
+ return raw;
4457
+ } catch {
4458
+ continue;
4459
+ }
4460
+ }
4461
+ return "";
4462
+ }
4306
4463
  /** Resolve the upstream base model for a repo (ADR-099 base-model fallback): the `base_model`
4307
4464
  * declared in the repo's HF card metadata. Most local GGUFs are third-party requants
4308
4465
  * (lmstudio-community / unsloth / noctrex / …) whose card omits the author's recommended
@@ -4959,6 +5116,29 @@ function parseCardSampling(card) {
4959
5116
  function hasAnySampling(s) {
4960
5117
  return s.temp != null || s.topP != null || s.topK != null || s.minP != null;
4961
5118
  }
5119
+ function parseGenerationParams(raw) {
5120
+ if (!raw) return {};
5121
+ let obj;
5122
+ try {
5123
+ obj = JSON.parse(raw);
5124
+ } catch {
5125
+ return {};
5126
+ }
5127
+ if (typeof obj !== "object" || obj === null) return {};
5128
+ const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
5129
+ const out = {};
5130
+ const map = [
5131
+ ["temperature", "temp"],
5132
+ ["top_p", "topP"],
5133
+ ["top_k", "topK"],
5134
+ ["min_p", "minP"]
5135
+ ];
5136
+ for (const [jsonKey, field] of map) {
5137
+ const v = num(obj[jsonKey]);
5138
+ if (v !== null) out[field] = v;
5139
+ }
5140
+ return clampCardSampling(out);
5141
+ }
4962
5142
  function relevantCardExcerpt(card, maxLen = 8e3) {
4963
5143
  if (card.length <= maxLen) return card;
4964
5144
  const strong = /\b(?:recommended\s+(?:settings|sampling|parameters)|sampling\s+(?:settings|parameters|params)|best\s+practices?|generation\s+config)\b/i.exec(card);
@@ -5092,7 +5272,7 @@ var BenchRunner = class {
5092
5272
  saveResult() {
5093
5273
  const w = this.winning;
5094
5274
  if (!w) return false;
5095
- const record = this.persistBest(w.modelKey, w.profile, w.cand);
5275
+ const record = this.persistBest(w.modelKey, w.profile, w.cand, w.engineId);
5096
5276
  this.queueTelemetry(record, w.entry, w.sys, this.version, w.engineVersion);
5097
5277
  this.winning = null;
5098
5278
  this.state = { ...this.state, result: void 0 };
@@ -5161,7 +5341,7 @@ var BenchRunner = class {
5161
5341
  this.runLogMeta = { modelKey, startedAt: (/* @__PURE__ */ new Date()).toISOString(), sys };
5162
5342
  const active = this.registry.active();
5163
5343
  const caps = active?.capabilities ?? { flags: [], kvTypes: [] };
5164
- const saved = this.store.snapshot().modelProfiles[modelKey];
5344
+ const saved = getModelProfile(this.store.snapshot(), modelKey, active?.id ?? "*");
5165
5345
  const defaults = this.store.snapshot().modelDefaults;
5166
5346
  const resolved = resolveProfile(entry, sys, saved, base2, defaults);
5167
5347
  const baseProfile = { ...resolved, speculative: "off", mtpHeadPath: "", draftModelPath: "" };
@@ -5197,7 +5377,7 @@ var BenchRunner = class {
5197
5377
  return;
5198
5378
  }
5199
5379
  const profile = recommended && hasAnySampling(recommended) ? { ...best.profile, sampling: { ...best.profile.sampling, ...recommended } } : best.profile;
5200
- this.winning = { modelKey, profile, cand: best.cand, entry, sys, engineVersion: active?.version ?? "" };
5380
+ this.winning = { modelKey, profile, cand: best.cand, entry, sys, engineVersion: active?.version ?? "", engineId: active?.id ?? "" };
5201
5381
  this.logWinner = { params: best.cand.params, tps: best.cand.tps ?? 0, prefillTps: best.cand.prefillTps, ttftMs: best.cand.ttftMs ?? 0, vramMb: best.cand.vramMb };
5202
5382
  this.state = {
5203
5383
  running: false,
@@ -5663,6 +5843,9 @@ var BenchRunner = class {
5663
5843
  const repo = inferRepoFromPath(entry.path, this.store.snapshot().modelDirs);
5664
5844
  if (!repo) return void 0;
5665
5845
  this.state = { ...this.state, step: "Reading model-card recommendations\u2026" };
5846
+ const genParams = await this.hf.fetchGenerationParams(repo).catch(() => "");
5847
+ const paramsH = parseGenerationParams(genParams);
5848
+ if (hasAnySampling(paramsH)) return paramsH;
5666
5849
  const localCard = await this.hf.fetchModelCard(repo).catch(() => "");
5667
5850
  const localH = parseCardSampling(localCard);
5668
5851
  if (hasAnySampling(localH)) return localH;
@@ -5744,7 +5927,7 @@ var BenchRunner = class {
5744
5927
  }
5745
5928
  /** Save the winning profile as the model's saved profile (tunedBy:'bench') and
5746
5929
  * persist a benchResults row. Both via the same ConfigStore the route uses. */
5747
- persistBest(modelKey, profile, cand) {
5930
+ persistBest(modelKey, profile, cand, engineId) {
5748
5931
  const record = {
5749
5932
  modelKey,
5750
5933
  tps: cand.tps ?? 0,
@@ -5755,7 +5938,7 @@ var BenchRunner = class {
5755
5938
  };
5756
5939
  const tuned = { ...profile, tunedBy: "bench" };
5757
5940
  this.store.update((cfg2) => {
5758
- cfg2.modelProfiles[modelKey] = tuned;
5941
+ setModelProfile(cfg2, modelKey, engineId || "*", tuned);
5759
5942
  cfg2.benchResults[modelKey] = record;
5760
5943
  });
5761
5944
  return record;
@@ -6075,7 +6258,7 @@ var ModelRouter = class {
6075
6258
  const cfg2 = this.store.snapshot();
6076
6259
  const sys = getSysInfo();
6077
6260
  if (entry.format !== "gguf") {
6078
- const savedProfile = cfg2.modelProfiles[entry.key];
6261
+ const savedProfile = getModelProfile(cfg2, entry.key, engine.id);
6079
6262
  return {
6080
6263
  engine,
6081
6264
  model: { key: entry.key, name: entry.name, quant: entry.quant, ctx: entry.nativeCtx, vision: false },
@@ -6085,7 +6268,7 @@ var ModelRouter = class {
6085
6268
  tensorParallelSize: savedProfile?.gpu?.tensorParallelSize
6086
6269
  };
6087
6270
  }
6088
- const saved = cfg2.modelProfiles[entry.key];
6271
+ const saved = getModelProfile(cfg2, entry.key, engine.id);
6089
6272
  const profile = resolveProfile(entry, sys, saved, void 0, cfg2.modelDefaults);
6090
6273
  const extraArgs = engine.kind === "koboldcpp" ? koboldcppProfileToArgs(profile, sys.gpus[0]?.vendor ?? "unknown", sys.gpus.length > 0) : profileToArgs(profile, entry, engine.capabilities, sys.cores);
6091
6274
  return {
@@ -8383,6 +8566,28 @@ async function body(c) {
8383
8566
  }
8384
8567
  function registerChatRoutes(app2, d) {
8385
8568
  const { db: db2 } = d;
8569
+ app2.get("/api/v1/folders", (c) => {
8570
+ return c.json({ folders: db2.listFolders() });
8571
+ });
8572
+ app2.post("/api/v1/folders", async (c) => {
8573
+ const b = await body(c);
8574
+ const name = (b.name ?? "").trim();
8575
+ if (!name) return err(c, 400, "invalid_input", "Folder name required.");
8576
+ return c.json(db2.createFolder(name), 201);
8577
+ });
8578
+ app2.patch("/api/v1/folders/:id", async (c) => {
8579
+ const b = await body(c);
8580
+ const name = (b.name ?? "").trim();
8581
+ if (!name) return err(c, 400, "invalid_input", "Folder name required.");
8582
+ const ok = db2.renameFolder(c.req.param("id"), name);
8583
+ if (!ok) return err(c, 404, "not_found", "Folder not found.");
8584
+ return c.json(db2.getFolder(c.req.param("id")));
8585
+ });
8586
+ app2.delete("/api/v1/folders/:id", (c) => {
8587
+ const ok = db2.deleteFolder(c.req.param("id"));
8588
+ if (!ok) return err(c, 404, "not_found", "Folder not found.");
8589
+ return c.json({ ok: true });
8590
+ });
8386
8591
  app2.get("/api/v1/conversations", (c) => {
8387
8592
  const q = c.req.query("q");
8388
8593
  return c.json({ conversations: db2.listConversations(q, "chat") });
@@ -8408,6 +8613,16 @@ function registerChatRoutes(app2, d) {
8408
8613
  if (!ok) return err(c, 404, "not_found", "Conversation not found.");
8409
8614
  return c.json({ ok: true });
8410
8615
  });
8616
+ app2.patch("/api/v1/conversations/:id/folder", async (c) => {
8617
+ const b = await body(c);
8618
+ const folderId = b.folderId ?? null;
8619
+ const res = db2.moveConversationToFolder(c.req.param("id"), folderId);
8620
+ if (!res.ok) {
8621
+ if (res.reason === "folder_not_found") return err(c, 404, "not_found", "Folder not found.");
8622
+ return err(c, 404, "not_found", "Conversation not found.");
8623
+ }
8624
+ return c.json(db2.getConversation(c.req.param("id")));
8625
+ });
8411
8626
  app2.post("/api/v1/conversations/:id/messages", async (c) => {
8412
8627
  const convId = c.req.param("id");
8413
8628
  const b = await body(c);
@@ -9561,10 +9776,11 @@ import { promisify as promisify5 } from "util";
9561
9776
  var execFileP5 = promisify5(execFile8);
9562
9777
  function buildEnv(toolchainDirs = []) {
9563
9778
  const dirs = toolchainDirs.map((d) => d.trim()).filter(Boolean);
9564
- if (dirs.length === 0) return { ...process.env };
9565
9779
  const env = { ...process.env };
9566
9780
  const key = Object.keys(env).find((k) => k.toLowerCase() === "path") ?? "PATH";
9567
- env[key] = [...dirs, env[key] ?? ""].filter(Boolean).join(delimiter);
9781
+ const existing = (env[key] ?? "").split(delimiter);
9782
+ const segments = [...dirs, ...existing].map((s) => s.trim()).filter(Boolean);
9783
+ if (env[key] !== void 0 || dirs.length > 0) env[key] = segments.join(delimiter);
9568
9784
  return env;
9569
9785
  }
9570
9786
  var INSTALL_URLS = {
@@ -9856,6 +10072,9 @@ async function findVcvarsall() {
9856
10072
  return null;
9857
10073
  }
9858
10074
  }
10075
+ function winComSpec() {
10076
+ return process.env.ComSpec || "cmd.exe";
10077
+ }
9859
10078
  function runStep(cmd, args, opts) {
9860
10079
  return new Promise((resolve2, reject) => {
9861
10080
  const child = spawn4(cmd, args, {
@@ -10006,7 +10225,7 @@ async function runBuild(req, hooks, signal) {
10006
10225
  if (isWindows) {
10007
10226
  const configureBat = join19(buildRoot, "_tllm_configure.bat");
10008
10227
  writeFileSync8(configureBat, vcvarsBatch(vcvars, configureArgs));
10009
- await runStep("cmd.exe", ["/c", configureBat], { cwd: buildRoot, env, signal, onLine: hooks.log });
10228
+ await runStep(winComSpec(), ["/c", configureBat], { cwd: buildRoot, env, signal, onLine: hooks.log });
10010
10229
  } else {
10011
10230
  await runStep("cmake", configureArgs, { cwd: buildRoot, env, signal, onLine: hooks.log });
10012
10231
  }
@@ -10015,7 +10234,7 @@ async function runBuild(req, hooks, signal) {
10015
10234
  if (isWindows) {
10016
10235
  const compileBat = join19(buildRoot, "_tllm_build.bat");
10017
10236
  writeFileSync8(compileBat, vcvarsBatch(vcvars, compileArgs));
10018
- await runStep("cmd.exe", ["/c", compileBat], { cwd: buildRoot, env, signal, onLine: hooks.log });
10237
+ await runStep(winComSpec(), ["/c", compileBat], { cwd: buildRoot, env, signal, onLine: hooks.log });
10019
10238
  } else {
10020
10239
  await runStep("cmake", compileArgs, { cwd: buildRoot, env, signal, onLine: hooks.log });
10021
10240
  }
@@ -10151,7 +10370,7 @@ function detectHardware(info = getSysInfo()) {
10151
10370
  (best, g) => !best || g.vramMb > best.vramMb ? g : best,
10152
10371
  void 0
10153
10372
  );
10154
- const vramMb = info.gpus.reduce((max, g) => g.vramMb > max ? g.vramMb : max, 0);
10373
+ const vramMb = ofVendor.reduce((sum, g) => sum + g.vramMb, 0);
10155
10374
  return {
10156
10375
  platform: process.platform,
10157
10376
  arch: arch2,
@@ -11078,7 +11297,7 @@ function registerApi(app2, d) {
11078
11297
  }
11079
11298
  let opts2;
11080
11299
  if (entry.format !== "gguf") {
11081
- const savedProfile = cfg2.modelProfiles[entry.key];
11300
+ const savedProfile = getModelProfile(cfg2, entry.key, active.id);
11082
11301
  const extraArgs = active.kind === "mlx" ? mlxSamplingArgs(savedProfile?.sampling) : active.kind === "vllm" ? vllmProfileToArgs(resolveProfile(entry, sys, savedProfile, b.profileOverrides, cfg2.modelDefaults)) : [];
11083
11302
  opts2 = {
11084
11303
  engine: active,
@@ -11088,7 +11307,7 @@ function registerApi(app2, d) {
11088
11307
  tensorParallelSize: savedProfile?.gpu?.tensorParallelSize
11089
11308
  };
11090
11309
  } else {
11091
- const saved = cfg2.modelProfiles[entry.key];
11310
+ const saved = getModelProfile(cfg2, entry.key, active.id);
11092
11311
  const profile = resolveProfile(entry, sys, saved, b.profileOverrides, cfg2.modelDefaults);
11093
11312
  const extraArgs = active.kind === "koboldcpp" ? koboldcppProfileToArgs(profile, primaryVendor(sys), sys.gpus.length > 0) : profileToArgs(profile, entry, active.capabilities, sys.cores);
11094
11313
  opts2 = {
@@ -11279,7 +11498,8 @@ function registerApi(app2, d) {
11279
11498
  if (!e) return err2(c, 404, "no_such_model", "No model with that key.");
11280
11499
  const sys = getSysInfo();
11281
11500
  const snap = d.store.snapshot();
11282
- const saved = snap.modelProfiles[e.key];
11501
+ const engineId = c.req.query("engine") || d.registry.active()?.id || "*";
11502
+ const saved = getModelProfile(snap, e.key, engineId);
11283
11503
  const profile = resolveProfile(e, sys, saved, void 0, snap.modelDefaults);
11284
11504
  return c.json({ ...overlayModel(e, d), profile, vramFit: estimateVram(profile, e, sys), gpu: sys.gpus[0] ?? null, gpus: sys.gpus, cores: sys.cores });
11285
11505
  });
@@ -11306,15 +11526,17 @@ function registerApi(app2, d) {
11306
11526
  return err2(c, 400, "invalid_profile_value", "gpu.tensorParallelSize must be an integer \u2265 1.");
11307
11527
  }
11308
11528
  }
11529
+ const engineId = c.req.query("engine") || d.registry.active()?.id || "*";
11309
11530
  d.store.update((cfg2) => {
11310
- cfg2.modelProfiles[key] = p;
11531
+ setModelProfile(cfg2, key, engineId, p);
11311
11532
  });
11312
11533
  return c.json(p);
11313
11534
  });
11314
11535
  app2.post("/api/v1/models/:key/profile/reset", (c) => {
11315
11536
  const key = decodeURIComponent(c.req.param("key"));
11537
+ const engineId = c.req.query("engine") || d.registry.active()?.id || "*";
11316
11538
  d.store.update((cfg2) => {
11317
- delete cfg2.modelProfiles[key];
11539
+ deleteModelProfile(cfg2, key, engineId);
11318
11540
  });
11319
11541
  return c.json({ ok: true });
11320
11542
  });
@@ -11716,7 +11938,8 @@ function overlayModel(e, d, lastTpsMap) {
11716
11938
  const compatibleWithActiveEngine = active ? engineAcceptsFormat(active.kind, e.format) : true;
11717
11939
  const provRepo = d.downloads.provenance().find((p) => p.dest === e.path && p.repo)?.repo;
11718
11940
  const sourceRepo = provRepo ?? inferRepoFromPath(e.path, snap.modelDirs);
11719
- return { ...e, loaded, hasProfile: e.key in profiles, lastTps, liveTps, benchTps, compatibleWithActiveEngine, sourceRepo };
11941
+ const hasProfile = !!profiles[e.key] && Object.keys(profiles[e.key]).length > 0;
11942
+ return { ...e, loaded, hasProfile, lastTps, liveTps, benchTps, compatibleWithActiveEngine, sourceRepo };
11720
11943
  }
11721
11944
  function formatMismatchMessage(engineKind, format) {
11722
11945
  if (engineKind === "mlx")
@@ -13053,7 +13276,7 @@ void (async () => {
13053
13276
  extraArgs: []
13054
13277
  };
13055
13278
  } else {
13056
- const saved = cfg.modelProfiles[entry.key];
13279
+ const saved = getModelProfile(cfg, entry.key, active.id);
13057
13280
  const profile = resolveProfile(entry, sys, saved, void 0, cfg.modelDefaults);
13058
13281
  opts = {
13059
13282
  engine: active,
@@ -1,2 +1,2 @@
1
- import{j as e}from"./radix-9WP-OkMY.js";import{r as u}from"./vendor-W5GIuFcv.js";import{a as w,d as k,A as T,u as v,L as h,e as S,c as b,f as C,g as q,X as A}from"./index-CjGc01NF.js";import{P as N}from"./plus-3YM8ko_h.js";import{C as y}from"./circle-x-CrulSiVz.js";import{C as O}from"./circle-check-C9nr7e7f.js";import{W as E}from"./wrench-2q903Ji-.js";const _=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["circle",{cx:"12",cy:"12",r:"1",key:"41hilf"}]],D=w("circle-dot",_);const L=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 6v6l4 2",key:"mmk7yg"}]],P=w("clock",L),p={all:["agent-runs"],list:()=>[...p.all,"list"],detail:t=>[...p.all,"detail",t]};async function g(t,a){const l={Accept:"application/json",...k(),...a?.headers??{}},i=await fetch(t,{...a,headers:l});if(i.status===204)return;const r=await i.text(),o=r?JSON.parse(r):void 0;if(!i.ok){const d=o;throw new T(d?.error?.code??"http_error",d?.error?.message??`Request failed with status ${i.status}.`,i.status)}return o}async function $(){return g("/api/v1/agents/runs")}async function I(t){return g(`/api/v1/agents/runs/${t}`)}async function Q(t){return g("/api/v1/agents/runs",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})}async function K(t){await g(`/api/v1/agents/runs/${t}`,{method:"DELETE"})}async function*F(t,a=0,l){const i=`/api/v1/agents/runs/${t}/stream?fromSeq=${a}`,r=await fetch(i,{headers:{Accept:"text/event-stream",...k()},signal:l});if(!r.ok||!r.body)throw new Error(`Stream error: ${r.status}`);const o=r.body.getReader(),d=new TextDecoder;let m="";try{for(;;){const{done:s,value:x}=await o.read();if(s)break;m+=d.decode(x,{stream:!0});const c=m.split(`
1
+ import{j as e}from"./radix-BY-fjzwm.js";import{r as u}from"./vendor-W5GIuFcv.js";import{c as w,d as k,A as T,u as v,L as h,e as S,a as b,f as C,g as q,X as A}from"./index-oSSYX1Lk.js";import{P as N}from"./plus-C8MMVqVA.js";import{C as y}from"./circle-x-caNFmCHm.js";import{C as O}from"./circle-check-DXOwCXEz.js";import{W as E}from"./wrench-BotIMKYs.js";const _=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["circle",{cx:"12",cy:"12",r:"1",key:"41hilf"}]],D=w("circle-dot",_);const L=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 6v6l4 2",key:"mmk7yg"}]],P=w("clock",L),p={all:["agent-runs"],list:()=>[...p.all,"list"],detail:t=>[...p.all,"detail",t]};async function g(t,a){const l={Accept:"application/json",...k(),...a?.headers??{}},i=await fetch(t,{...a,headers:l});if(i.status===204)return;const r=await i.text(),o=r?JSON.parse(r):void 0;if(!i.ok){const d=o;throw new T(d?.error?.code??"http_error",d?.error?.message??`Request failed with status ${i.status}.`,i.status)}return o}async function $(){return g("/api/v1/agents/runs")}async function I(t){return g(`/api/v1/agents/runs/${t}`)}async function Q(t){return g("/api/v1/agents/runs",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})}async function K(t){await g(`/api/v1/agents/runs/${t}`,{method:"DELETE"})}async function*F(t,a=0,l){const i=`/api/v1/agents/runs/${t}/stream?fromSeq=${a}`,r=await fetch(i,{headers:{Accept:"text/event-stream",...k()},signal:l});if(!r.ok||!r.body)throw new Error(`Stream error: ${r.status}`);const o=r.body.getReader(),d=new TextDecoder;let m="";try{for(;;){const{done:s,value:x}=await o.read();if(s)break;m+=d.decode(x,{stream:!0});const c=m.split(`
2
2
  `);m=c.pop()??"";let n="message";for(const f of c){if(f.startsWith("event: ")){n=f.slice(7).trim();continue}if(f.startsWith("data: ")){const j=f.slice(6).trim();if(j==="[DONE]")return;try{const R=JSON.parse(j);if(yield{event:n,data:R},n==="done"||n==="error")return}catch{}n="message"}}}}finally{o.cancel()}}const J={queued:e.jsx(P,{size:12}),running:e.jsx(h,{size:12,className:"animate-spin"}),done:e.jsx(O,{size:12}),failed:e.jsx(y,{size:12}),cancelled:e.jsx(y,{size:12}),interrupted:e.jsx(y,{size:12})},M={queued:"text-muted",running:"text-accent",done:"text-green-500",failed:"text-red-500",cancelled:"text-muted",interrupted:"text-orange-500"};function z({status:t}){return e.jsxs("span",{className:b("flex items-center gap-1 text-[11px] capitalize",M[t]),children:[J[t],t]})}function W(t){const a=Date.now()-new Date(t).getTime();return a<6e4?"just now":a<36e5?`${Math.floor(a/6e4)}m ago`:a<864e5?`${Math.floor(a/36e5)}h ago`:new Date(t).toLocaleDateString()}const U=[{id:"web_search",label:"Web search",hint:"Search the web (needs a provider configured in Settings)"},{id:"fetch_url",label:"Fetch URL",hint:"Read the contents of a web page"},{id:"run_code",label:"Run code",hint:"Execute JS in a sandbox — no mid-run confirmation, opt in only if you trust the task"}];function B({onClose:t,onCreated:a}){const[l,i]=u.useState(""),[r,o]=u.useState(""),[d,m]=u.useState({web_search:!0,fetch_url:!0,run_code:!1}),s=C(),x=n=>m(f=>({...f,[n]:!f[n]})),c=q({mutationFn:()=>Q({title:l.slice(0,60)||"Agent run",systemPrompt:r||void 0,userMessage:l,allowedTools:Object.entries(d).filter(([,n])=>n).map(([n])=>n)}),onSuccess:n=>{s.invalidateQueries({queryKey:p.list()}),a(n.id),t()}});return e.jsx("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/40",onClick:t,children:e.jsxs("div",{className:"w-full max-w-lg rounded-xl border border-border bg-panel p-5 shadow-xl",onClick:n=>n.stopPropagation(),children:[e.jsxs("div",{className:"mb-4 flex items-center justify-between",children:[e.jsx("h2",{className:"text-[15px] font-semibold",children:"New agent run"}),e.jsx("button",{type:"button",onClick:t,className:"text-faint hover:text-ink",children:e.jsx(A,{size:16})})]}),e.jsx("label",{className:"mb-1 block text-[12px] text-muted",children:"Task"}),e.jsx("textarea",{autoFocus:!0,value:l,onChange:n=>i(n.target.value),placeholder:"Describe what the agent should do…",rows:4,className:"mb-3 w-full resize-none rounded-lg border border-border bg-input px-3 py-2 text-[13px] text-ink placeholder:text-faint focus:border-accent focus:outline-none"}),e.jsxs("label",{className:"mb-1 block text-[12px] text-muted",children:["System prompt ",e.jsx("span",{className:"text-faint",children:"(optional)"})]}),e.jsx("textarea",{value:r,onChange:n=>o(n.target.value),placeholder:"You are a helpful assistant…",rows:2,className:"mb-4 w-full resize-none rounded-lg border border-border bg-input px-3 py-2 text-[13px] text-ink placeholder:text-faint focus:border-accent focus:outline-none"}),e.jsx("label",{className:"mb-1.5 block text-[12px] text-muted",children:"Tools"}),e.jsx("div",{className:"mb-4 space-y-1.5",children:U.map(n=>e.jsxs("label",{className:"flex cursor-pointer items-start gap-2 rounded-lg border border-border px-2.5 py-2 hover:bg-panel-2",children:[e.jsx("input",{type:"checkbox",checked:d[n.id],onChange:()=>x(n.id),className:"mt-0.5 accent-[var(--accent)]"}),e.jsxs("span",{className:"min-w-0",children:[e.jsx("span",{className:"block text-[13px] font-medium text-ink",children:n.label}),e.jsx("span",{className:"block text-[11px] leading-snug text-faint",children:n.hint})]})]},n.id))}),e.jsxs("div",{className:"flex items-center justify-end gap-2",children:[e.jsx("button",{type:"button",onClick:t,className:"rounded-lg px-3 py-1.5 text-[13px] text-muted hover:text-ink",children:"Cancel"}),e.jsxs("button",{type:"button",disabled:!l.trim()||c.isPending,onClick:()=>c.mutate(),className:"flex items-center gap-1.5 rounded-lg bg-accent px-3 py-1.5 text-[13px] font-medium text-white disabled:opacity-50",children:[c.isPending?e.jsx(h,{size:13,className:"animate-spin"}):e.jsx(S,{size:13}),"Run agent"]})]}),c.isError&&e.jsx("p",{className:"mt-2 text-[12px]",style:{color:"var(--err)"},children:c.error.message})]})})}function X({call:t}){const[a,l]=u.useState(!1),i=typeof t.args?.query=="string"?t.args.query:typeof t.args?.url=="string"?t.args.url:JSON.stringify(t.args??{});return e.jsxs("div",{className:"w-full max-w-[85%] rounded-lg border border-border bg-panel px-2.5 py-1.5 text-[12px]",children:[e.jsxs("button",{type:"button",onClick:()=>l(r=>!r),className:"flex w-full items-center gap-1.5 text-left text-muted hover:text-ink",children:[e.jsx(E,{size:12,className:"shrink-0 text-accent"}),e.jsx("span",{className:"font-medium text-ink",children:t.name}),e.jsx("span",{className:"truncate text-faint",children:i})]}),a&&e.jsx("pre",{className:"mt-1.5 max-h-48 overflow-auto whitespace-pre-wrap break-words rounded bg-panel-2 p-2 text-[11px] text-muted",children:t.result})]})}function H({run:t}){const a=C(),l=u.useRef(null),[i,r]=u.useState(""),o=u.useRef(null),d=q({mutationFn:()=>K(t.id),onSuccess:()=>{a.invalidateQueries({queryKey:p.list()})}});u.useEffect(()=>{if(t.status!=="running"&&t.status!=="queued")return;const s=new AbortController;l.current=s;let x="";return(async()=>{try{for await(const c of F(t.id,0,s.signal))if(c.event==="delta")x+=c.data.delta,r(x);else if(c.event==="done"||c.event==="error"){r(""),a.invalidateQueries({queryKey:p.detail(t.id)}),a.invalidateQueries({queryKey:p.list()});break}}catch{}})(),()=>{s.abort(),l.current=null}},[t.id,t.status,a]),u.useEffect(()=>{o.current?.scrollIntoView({behavior:"smooth"})},[i,t.messages?.length]);const m=t.messages??[];return e.jsxs("div",{className:"flex h-full flex-col",children:[e.jsxs("div",{className:"flex items-center justify-between border-b border-border px-4 py-2.5",children:[e.jsxs("div",{children:[e.jsx("p",{className:"text-[14px] font-medium text-ink",children:t.title}),e.jsx(z,{status:t.status})]}),(t.status==="running"||t.status==="queued")&&e.jsx("button",{type:"button",onClick:()=>d.mutate(),className:"rounded-lg border border-border px-3 py-1 text-[12px] text-muted transition-colors hover:border-red-400 hover:text-red-500",children:d.isPending?e.jsx(h,{size:12,className:"inline animate-spin"}):"Cancel"})]}),e.jsxs("div",{className:"flex-1 space-y-3 overflow-y-auto p-4",children:[m.map(s=>e.jsxs("div",{className:b("flex flex-col gap-1.5",s.role==="user"?"items-end":"items-start"),children:[s.toolCalls?.map(x=>e.jsx(X,{call:x},x.id)),(s.content||s.role==="user"||t.status==="running")&&e.jsx("div",{className:b("max-w-[85%] rounded-xl px-3 py-2 text-[13px] leading-relaxed",s.role==="user"?"bg-accent/15 text-ink":"bg-panel-2 text-ink"),children:e.jsx("p",{className:"whitespace-pre-wrap break-words",children:s.content||(s.role==="assistant"&&t.status==="running"?"…":"")})})]},s.id)),i&&e.jsx("div",{className:"flex gap-2",children:e.jsx("div",{className:"max-w-[85%] rounded-xl bg-panel-2 px-3 py-2 text-[13px] leading-relaxed text-ink",children:e.jsx("p",{className:"whitespace-pre-wrap break-words",children:i})})}),t.error&&e.jsxs("div",{className:"rounded-lg bg-red-500/10 px-3 py-2 text-[12px] text-red-500",children:["Error: ",t.error]}),e.jsx("div",{ref:o})]})]})}function ne(){const[t,a]=u.useState(null),[l,i]=u.useState(!1),r=v({queryKey:p.list(),queryFn:$,refetchInterval:s=>(s.state.data??[]).some(n=>n.status==="queued"||n.status==="running")?3e3:!1}),o=v({queryKey:p.detail(t??""),queryFn:()=>I(t),enabled:!!t,refetchInterval:s=>{const x=s.state.data;return x?.status==="queued"||x?.status==="running"?5e3:!1}}),d=r.data??[],m=o.data;return e.jsxs("div",{className:"flex h-full",children:[e.jsxs("div",{className:"flex w-64 shrink-0 flex-col border-r border-border",children:[e.jsxs("div",{className:"flex items-center justify-between border-b border-border px-3 py-2.5",children:[e.jsx("span",{className:"text-[13px] font-semibold text-ink",children:"Agents"}),e.jsxs("button",{type:"button",title:"New agent run",onClick:()=>i(!0),className:"flex items-center gap-1 rounded-lg bg-accent px-2 py-1 text-[11px] font-medium text-white transition-opacity hover:opacity-90",children:[e.jsx(N,{size:11})," New"]})]}),e.jsxs("div",{className:"flex-1 overflow-y-auto",children:[r.isLoading&&e.jsx("div",{className:"flex h-20 items-center justify-center",children:e.jsx(h,{size:16,className:"animate-spin text-faint"})}),d.length===0&&!r.isLoading&&e.jsxs("div",{className:"px-4 py-8 text-center",children:[e.jsx(S,{size:28,className:"mx-auto mb-2 text-faint"}),e.jsx("p",{className:"text-[12px] text-muted",children:"No agent runs yet."}),e.jsx("p",{className:"mt-0.5 text-[11px] text-faint",children:"Click “New” to start one."})]}),d.map(s=>e.jsxs("button",{type:"button",onClick:()=>a(s.id),className:b("w-full border-b border-border px-3 py-2.5 text-left transition-colors hover:bg-panel-2",t===s.id&&"bg-panel-2"),children:[e.jsx("p",{className:"mb-0.5 truncate text-[13px] font-medium text-ink",children:s.title}),e.jsxs("div",{className:"flex items-center justify-between",children:[e.jsx(z,{status:s.status}),e.jsx("span",{className:"text-[10px] text-faint",children:W(s.createdAt)})]})]},s.id))]})]}),e.jsx("div",{className:"flex min-w-0 flex-1 flex-col",children:t?o.isLoading?e.jsx("div",{className:"flex h-full items-center justify-center",children:e.jsx(h,{size:20,className:"animate-spin text-faint"})}):m?e.jsx(H,{run:m}):null:e.jsxs("div",{className:"flex h-full flex-col items-center justify-center text-center",children:[e.jsx(D,{size:32,className:"mb-3 text-faint"}),e.jsx("p",{className:"text-[14px] font-medium text-ink",children:"Select a run"}),e.jsx("p",{className:"mt-1 text-[12px] text-muted",children:"or create a new agent run to get started"}),e.jsxs("button",{type:"button",onClick:()=>i(!0),className:"mt-4 flex items-center gap-1.5 rounded-lg bg-accent px-3 py-1.5 text-[13px] font-medium text-white hover:opacity-90",children:[e.jsx(N,{size:13})," New agent run"]})]})}),l&&e.jsx(B,{onClose:()=>i(!1),onCreated:s=>{a(s),r.refetch()}})]})}export{ne as AgentsScreen};