turbollm 1.7.5 → 1.7.7

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 (100) hide show
  1. package/README.md +15 -0
  2. package/dist/cli.js +382 -6
  3. package/dist/webdist/assets/{AgentEditPage-CfiJQeRy.js → AgentEditPage-y0cZcIxD.js} +1 -1
  4. package/dist/webdist/assets/ChatScreen-N8xJshxm.js +65 -0
  5. package/dist/webdist/assets/{CustomizeScreen-BQMntB-C.js → CustomizeScreen-8x5UpMgo.js} +1 -1
  6. package/dist/webdist/assets/DeveloperScreen-Bq2FNjtI.js +1 -0
  7. package/dist/webdist/assets/EnginesScreen-BUUYSiw6.js +7 -0
  8. package/dist/webdist/assets/FsBrowser-BQd7_SH6.js +1 -0
  9. package/dist/webdist/assets/{MessageBubble-CxHvNrgj.js → MessageBubble-T4P9BnnR.js} +16 -16
  10. package/dist/webdist/assets/{ModelsScreen-RRK-uCW8.js → ModelsScreen-0yDp-U9Y.js} +2 -2
  11. package/dist/webdist/assets/SettingsScreen-BsEt3HT4.js +1 -0
  12. package/dist/webdist/assets/{SkillEditPage-CHh9An9Z.js → SkillEditPage-C947cp0W.js} +1 -1
  13. package/dist/webdist/assets/TokensScreen-ColA57XS.js +1 -0
  14. package/dist/webdist/assets/WorkspaceScreen-B88cgpDr.js +1 -0
  15. package/dist/webdist/assets/{agent-api-B0qitSjr.js → agent-api-DgdsmygN.js} +1 -1
  16. package/dist/webdist/assets/{arc-Cdcmyops.js → arc-CAcqzHUt.js} +1 -1
  17. package/dist/webdist/assets/{architectureDiagram-3BPJPVTR-BQlFr2uc.js → architectureDiagram-3BPJPVTR-uesx4C2M.js} +1 -1
  18. package/dist/webdist/assets/{arrow-left-BgPNNAIi.js → arrow-left-CixSWWD6.js} +1 -1
  19. package/dist/webdist/assets/badge-Bu2_JlLl.js +1 -0
  20. package/dist/webdist/assets/{blockDiagram-GPEHLZMM-CMRPD5BN.js → blockDiagram-GPEHLZMM-DePQ91-U.js} +1 -1
  21. package/dist/webdist/assets/{bot-CVbPHmTQ.js → bot-CLQbJ0hY.js} +1 -1
  22. package/dist/webdist/assets/{c4Diagram-AAUBKEIU-BYXwwlnp.js → c4Diagram-AAUBKEIU-Nx7n4BaG.js} +1 -1
  23. package/dist/webdist/assets/channel-Bl7LnSBM.js +1 -0
  24. package/dist/webdist/assets/{chat-api-x93Fzf3q.js → chat-api-v2HtltwV.js} +3 -3
  25. package/dist/webdist/assets/check-DWNXHB4-.js +1 -0
  26. package/dist/webdist/assets/{chevron-left-D03iirWA.js → chevron-left-ZSwpcWK4.js} +1 -1
  27. package/dist/webdist/assets/{chevron-right-B7Mj5eje.js → chevron-right-BX4x_YDg.js} +1 -1
  28. package/dist/webdist/assets/{chunk-2J33WTMH-BTHWkgj0.js → chunk-2J33WTMH-Li-0bh7s.js} +1 -1
  29. package/dist/webdist/assets/{chunk-4BX2VUAB-C0OdA31w.js → chunk-4BX2VUAB-D3JCha2O.js} +1 -1
  30. package/dist/webdist/assets/{chunk-55IACEB6-V6hn9Bpx.js → chunk-55IACEB6-DtexA9w4.js} +1 -1
  31. package/dist/webdist/assets/{chunk-727SXJPM-DCntVbqt.js → chunk-727SXJPM--FzMnyZQ.js} +1 -1
  32. package/dist/webdist/assets/{chunk-AQP2D5EJ-D4PhS_5c.js → chunk-AQP2D5EJ-DLjRb4q9.js} +1 -1
  33. package/dist/webdist/assets/{chunk-FMBD7UC4-qDBSNTHO.js → chunk-FMBD7UC4--oSGiB90.js} +1 -1
  34. package/dist/webdist/assets/{chunk-ND2GUHAM-D7KguwAx.js → chunk-ND2GUHAM-DHrBgMDK.js} +1 -1
  35. package/dist/webdist/assets/{chunk-QZHKN3VN-3zsu8TEd.js → chunk-QZHKN3VN-Czsg5U3B.js} +1 -1
  36. package/dist/webdist/assets/{circle-x-Bm98m9k6.js → circle-x-SeGmqGO2.js} +1 -1
  37. package/dist/webdist/assets/classDiagram-4FO5ZUOK-DPKa7mmC.js +1 -0
  38. package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-DPKa7mmC.js +1 -0
  39. package/dist/webdist/assets/{collapsible-CZm9yoBQ.js → collapsible-BxchjxT5.js} +1 -1
  40. package/dist/webdist/assets/{common-DhVKZnxd.js → common-BX2IDRWf.js} +1 -1
  41. package/dist/webdist/assets/{cose-bilkent-S5V4N54A-CdJ22dvm.js → cose-bilkent-S5V4N54A-BWS5BbS6.js} +1 -1
  42. package/dist/webdist/assets/{dagre-BM42HDAG-C0tsLnJj.js → dagre-BM42HDAG-BPo4IktH.js} +2 -2
  43. package/dist/webdist/assets/{diagram-2AECGRRQ-tReRMIGZ.js → diagram-2AECGRRQ-bBunxDsg.js} +1 -1
  44. package/dist/webdist/assets/{diagram-5GNKFQAL-BPDwtKWf.js → diagram-5GNKFQAL-moM1dOLS.js} +1 -1
  45. package/dist/webdist/assets/{diagram-KO2AKTUF-dxXvPo_e.js → diagram-KO2AKTUF-BwfV8JvH.js} +1 -1
  46. package/dist/webdist/assets/{diagram-LMA3HP47-BblbbKep.js → diagram-LMA3HP47-orftYm_u.js} +1 -1
  47. package/dist/webdist/assets/{diagram-OG6HWLK6-Bm5kmmiK.js → diagram-OG6HWLK6-D3M14loM.js} +1 -1
  48. package/dist/webdist/assets/{dialog-BS2ga4Li.js → dialog-dmjvLZWp.js} +1 -1
  49. package/dist/webdist/assets/{erDiagram-TEJ5UH35-DtBl4psP.js → erDiagram-TEJ5UH35-CE0GXqcn.js} +1 -1
  50. package/dist/webdist/assets/{flowDiagram-I6XJVG4X-inuE1WTM.js → flowDiagram-I6XJVG4X-BPewPsNy.js} +1 -1
  51. package/dist/webdist/assets/{folder-input-DPrdjjXF.js → folder-input-C_YBaI_4.js} +1 -1
  52. package/dist/webdist/assets/{ganttDiagram-6RSMTGT7-C1x80EJK.js → ganttDiagram-6RSMTGT7-D5yG5kD1.js} +1 -1
  53. package/dist/webdist/assets/{gitGraphDiagram-PVQCEYII-BHFBSO22.js → gitGraphDiagram-PVQCEYII-Cj6zpryV.js} +1 -1
  54. package/dist/webdist/assets/{index-DT-1Skfu.js → index-61gQc5Xl.js} +7 -7
  55. package/dist/webdist/assets/index-BAK-ZQCj.css +1 -0
  56. package/dist/webdist/assets/{index-BPqqiChM.js → index-DCrpX6kY.js} +1 -1
  57. package/dist/webdist/assets/{infoDiagram-5YYISTIA-DErEOAbT.js → infoDiagram-5YYISTIA-NDkQABZh.js} +1 -1
  58. package/dist/webdist/assets/{ishikawaDiagram-YF4QCWOH-CFYAcSCu.js → ishikawaDiagram-YF4QCWOH-BOB3l-Kg.js} +1 -1
  59. package/dist/webdist/assets/{journeyDiagram-JHISSGLW-BRF8x-vW.js → journeyDiagram-JHISSGLW-DVEE4D7Z.js} +1 -1
  60. package/dist/webdist/assets/{kanban-definition-UN3LZRKU-Bpxu1hZh.js → kanban-definition-UN3LZRKU-CNPvv9C-.js} +7 -7
  61. package/dist/webdist/assets/{linear-SOR4O2dK.js → linear-D7zDjIPx.js} +1 -1
  62. package/dist/webdist/assets/{mermaid.core-Gi-pYgdj.js → mermaid.core-CFRc9zhD.js} +5 -5
  63. package/dist/webdist/assets/{mindmap-definition-RKZ34NQL-DtatIfBM.js → mindmap-definition-RKZ34NQL-DZbmKw3G.js} +1 -1
  64. package/dist/webdist/assets/{pencil-Bi7z3_2g.js → pencil-DLTfIEAv.js} +1 -1
  65. package/dist/webdist/assets/{personas-CsvqnRpC.js → personas-BoG0K2t1.js} +18 -10
  66. package/dist/webdist/assets/{pieDiagram-4H26LBE5-DQYaIU8r.js → pieDiagram-4H26LBE5-CSeor3k5.js} +1 -1
  67. package/dist/webdist/assets/{quadrantDiagram-W4KKPZXB-CUu3EWq8.js → quadrantDiagram-W4KKPZXB-C4wIuoO1.js} +1 -1
  68. package/dist/webdist/assets/{refresh-cw-CkhIL2Hz.js → refresh-cw-8N7t89ea.js} +1 -1
  69. package/dist/webdist/assets/{requirementDiagram-4Y6WPE33-D54OOdDO.js → requirementDiagram-4Y6WPE33-DhUQacPK.js} +1 -1
  70. package/dist/webdist/assets/{rotate-ccw-BOQNX9cU.js → rotate-ccw-DCFomh67.js} +1 -1
  71. package/dist/webdist/assets/{sankeyDiagram-5OEKKPKP-yykV1xV0.js → sankeyDiagram-5OEKKPKP-FRUJ6EB_.js} +1 -1
  72. package/dist/webdist/assets/{sequenceDiagram-3UESZ5HK-CD2n3CVh.js → sequenceDiagram-3UESZ5HK-CdCLaBm7.js} +1 -1
  73. package/dist/webdist/assets/skeleton-DgTrr_PQ.js +1 -0
  74. package/dist/webdist/assets/{star-geKnFl6r.js → star-biRsiE3m.js} +1 -1
  75. package/dist/webdist/assets/{stateDiagram-AJRCARHV-C7hTc-QA.js → stateDiagram-AJRCARHV-v5tGLSi5.js} +1 -1
  76. package/dist/webdist/assets/stateDiagram-v2-BHNVJYJU-v4thUmx4.js +1 -0
  77. package/dist/webdist/assets/{switch-BIJmJ4CL.js → switch-BlL-WRhK.js} +1 -1
  78. package/dist/webdist/assets/{timeline-definition-PNZ67QCA-6kJpBXAe.js → timeline-definition-PNZ67QCA-HpDjURD-.js} +1 -1
  79. package/dist/webdist/assets/{trash-2-CT_92vaT.js → trash-2-B0HcfuWA.js} +1 -1
  80. package/dist/webdist/assets/useIsDesktop-BedxotK0.js +1 -0
  81. package/dist/webdist/assets/{vennDiagram-CIIHVFJN-B7h2KOpV.js → vennDiagram-CIIHVFJN-BcjzVaAC.js} +1 -1
  82. package/dist/webdist/assets/{wardley-L42UT6IY-CvmW62QW.js → wardley-L42UT6IY-DAXczRny.js} +1 -1
  83. package/dist/webdist/assets/{wardleyDiagram-YWT4CUSO-BNvpRPx5.js → wardleyDiagram-YWT4CUSO-BHpd_gyW.js} +1 -1
  84. package/dist/webdist/assets/{wrench-DNrLSGCv.js → wrench-B2_HvSp0.js} +1 -1
  85. package/dist/webdist/assets/{xychartDiagram-2RQKCTM6-BQI4t1tF.js → xychartDiagram-2RQKCTM6-C9tyViUt.js} +1 -1
  86. package/dist/webdist/index.html +2 -2
  87. package/package.json +1 -1
  88. package/dist/webdist/assets/ChatScreen-CyzSdlm7.js +0 -65
  89. package/dist/webdist/assets/DeveloperScreen-DKDEoZ_0.js +0 -1
  90. package/dist/webdist/assets/EnginesScreen-BGc09u0i.js +0 -7
  91. package/dist/webdist/assets/FsBrowser-CxCRvnU8.js +0 -1
  92. package/dist/webdist/assets/SettingsScreen-CDUlGnqu.js +0 -1
  93. package/dist/webdist/assets/WorkspaceScreen-BbjiYb6f.js +0 -1
  94. package/dist/webdist/assets/channel-B3OIXexj.js +0 -1
  95. package/dist/webdist/assets/check-Cx4ep7jU.js +0 -1
  96. package/dist/webdist/assets/classDiagram-4FO5ZUOK-CIneh01q.js +0 -1
  97. package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-CIneh01q.js +0 -1
  98. package/dist/webdist/assets/index-CuJf6kYG.css +0 -1
  99. package/dist/webdist/assets/stateDiagram-v2-BHNVJYJU-BEt8Mhcv.js +0 -1
  100. package/dist/webdist/assets/useIsDesktop-Dxs_CPEE.js +0 -1
package/README.md CHANGED
@@ -170,6 +170,10 @@ the detail:
170
170
  (`-ngl`), **MoE CPU-offload (`--n-cpu-moe`)**, parallel slots, **KV-cache quant type** (incl.
171
171
  low-bit on supporting forks), CPU threads, flash attention, and **speculative decoding (NextN /
172
172
  MTP / draft, with a configurable draft min/max window)**.
173
+ - **Auto-fit GPU layers / MoE offload** — an optional toggle (off by default) that hands the
174
+ GPU/CPU split decision to llama.cpp's own memory-fitting logic at load time instead of a fixed
175
+ number, honored by Auto-tune too. Useful when a large context or model doesn't fit your usual
176
+ fixed setting.
173
177
  - **Fast by default:** flash attention on, NextN self-speculative decoding on for models that
174
178
  carry a draft head, threads auto — safely gated to what your engine actually accepts.
175
179
  - **Multi-GPU, per model** — split a model across cards (layer/row split + main-GPU pick on
@@ -225,6 +229,13 @@ the detail:
225
229
  - **Tool-call approval gate** — every tool call asks for your approval by default before it runs,
226
230
  with **Deny**, **Allow**, **Allow for this chat**, or **Always Allow** on an inline bar above the
227
231
  composer. Set per-tool defaults globally from Settings → Tools & safety.
232
+ - **Usage dashboard** — a GitHub-style activity heatmap of your local generation history (adaptive
233
+ 1h/12h/24h boxes for the 7-day/30-day/all-time views), streaks, peak hour, a per-model
234
+ breakdown, and a lifetime token-milestone tracker.
235
+ - **Auto-memory** *(experimental, off by default)* — silently extracts durable facts you mention
236
+ in chat (name, preferences, hardware) using your own loaded model, and carries them into future
237
+ new conversations. Nothing leaves your device; the full fact list is reviewable and deletable
238
+ from Settings → Memory, and turning the toggle off stops new chats from seeing it immediately.
228
239
 
229
240
  </details>
230
241
 
@@ -356,6 +367,10 @@ CUDA / a compiler — MSVC on Windows, gcc/clang on Linux), hands you the exact
356
367
  (or a 1-click **"Build it for me"** on Windows and Linux), then drops you into the folder scan
357
368
  above.
358
369
 
370
+ **Or skip the manual clone entirely** — Engines screen → **Add via git repo**: paste any
371
+ llama.cpp-compatible fork's git URL (+ optional branch, defaults to the repo's own default) and
372
+ build it in-app with the same 1-click flow, no separate "point at a folder" step needed.
373
+
359
374
  **Auto-provisioned default.** Don't want to fetch anything? On first run TurboLLM downloads
360
375
  the right upstream prebuilt for your GPU automatically — and a **backend picker** lets you
361
376
  switch between CUDA / ROCm / Metal / SYCL / Vulkan / CPU at any time (it downloads the variant
package/dist/cli.js CHANGED
@@ -95,7 +95,8 @@ function defaultConfig() {
95
95
  idleTtlMinutes: 60,
96
96
  openBrowserOnStart: true,
97
97
  theme: "system",
98
- autoGenerateTitles: true
98
+ autoGenerateTitles: true,
99
+ autoMemoryEnabled: false
99
100
  },
100
101
  telemetry: { level: "unset", machineId: "" },
101
102
  apiKeys: [],
@@ -2882,6 +2883,14 @@ function classifyVendor(name) {
2882
2883
  if (/apple/.test(n)) return "apple";
2883
2884
  return "unknown";
2884
2885
  }
2886
+ function isIntegratedGpuName(name) {
2887
+ const n = name.toLowerCase();
2888
+ if (/iris|uhd graphics|hd graphics/.test(n)) return true;
2889
+ if (/\barc\b/.test(n) && !/\b(?:pro\s+)?[ab]\d{2,4}\b/.test(n)) return true;
2890
+ if (/radeon.*graphics/.test(n) && !/\b(rx|pro|instinct|vega\s*\d|firepro)\b/.test(n)) return true;
2891
+ if (/intel/.test(n) && /graphics/.test(n) && !/\barc\b/.test(n)) return true;
2892
+ return false;
2893
+ }
2885
2894
  function detectGpus() {
2886
2895
  try {
2887
2896
  const out = execFileSync2("nvidia-smi", ["--query-gpu=name,memory.total", "--format=csv,noheader,nounits"], {
@@ -2930,7 +2939,8 @@ function enumWindowsGpus() {
2930
2939
  const [name, ram] = line.split("|");
2931
2940
  const nm = (name ?? "").trim();
2932
2941
  const bytes = parseInt((ram ?? "").trim(), 10) || 0;
2933
- return { name: nm, vramMb: bytes > 0 ? Math.round(bytes / 1e6) : 0, vendor: classifyVendor(nm) };
2942
+ const vramMb = isIntegratedGpuName(nm) ? Math.round(os.totalmem() / 1e6 * 0.5) : bytes > 0 ? Math.round(bytes / 1e6) : 0;
2943
+ return { name: nm, vramMb, vendor: classifyVendor(nm) };
2934
2944
  }).filter((g) => g.name && g.vendor !== "unknown");
2935
2945
  }
2936
2946
  function rocmSmiGpus() {
@@ -2971,7 +2981,10 @@ function enumLinuxGpus() {
2971
2981
  return out.trim().split("\n").filter(Boolean).map((line) => {
2972
2982
  const vendor = classifyVendor(line);
2973
2983
  const slot = line.trim().split(/\s+/)[0] ?? "";
2974
- return { name: line.replace(/"/g, "").trim().slice(0, 80), vramMb: linuxVramMb(slot), vendor };
2984
+ const name = line.replace(/"/g, "").trim().slice(0, 80);
2985
+ const sysfsVramMb = linuxVramMb(slot);
2986
+ const vramMb = sysfsVramMb > 0 ? sysfsVramMb : isIntegratedGpuName(name) ? Math.round(os.totalmem() / 1e6 * 0.5) : 0;
2987
+ return { name, vramMb, vendor };
2975
2988
  }).filter((g) => g.vendor !== "unknown");
2976
2989
  }
2977
2990
  function linuxVramMb(pciSlot) {
@@ -4013,7 +4026,7 @@ function resolveProfile(m, sys, saved, overrides, defaults) {
4013
4026
  function profileToArgs(p, m, caps, cores = 0) {
4014
4027
  const has = (flag) => caps.flags.length === 0 || caps.flags.includes(flag);
4015
4028
  const a = ["-c", String(p.ctx)];
4016
- if (p.ngl > 0) a.push("-ngl", String(p.ngl));
4029
+ if ((!p.nglFit || m.moe) && p.ngl > 0) a.push("-ngl", String(p.ngl));
4017
4030
  const g = p.gpu;
4018
4031
  if (g) {
4019
4032
  if (g.splitMode !== "layer" && has("--split-mode")) a.push("--split-mode", g.splitMode);
@@ -4024,7 +4037,7 @@ function profileToArgs(p, m, caps, cores = 0) {
4024
4037
  }
4025
4038
  if (has("--parallel")) a.push("--parallel", String(p.parallel));
4026
4039
  if (p.parallel > 1 && p.kvUnified && has("--kv-unified")) a.push("--kv-unified");
4027
- if (m.moe && p.nCpuMoe > 0 && has("--n-cpu-moe")) a.push("--n-cpu-moe", String(p.nCpuMoe));
4040
+ if (m.moe && !p.nCpuMoeFit && p.nCpuMoe > 0 && has("--n-cpu-moe")) a.push("--n-cpu-moe", String(p.nCpuMoe));
4028
4041
  const kvOk = (t) => caps.kvTypes.includes(t);
4029
4042
  if (p.kvTypeK !== "f16" && has("--cache-type-k") && kvOk(p.kvTypeK)) a.push("--cache-type-k", p.kvTypeK);
4030
4043
  if (p.kvTypeV !== "f16" && has("--cache-type-v") && kvOk(p.kvTypeV)) a.push("--cache-type-v", p.kvTypeV);
@@ -4106,6 +4119,24 @@ function vllmProfileToArgs(p) {
4106
4119
  import { DatabaseSync } from "node:sqlite";
4107
4120
  import { join as join14 } from "path";
4108
4121
  import { randomUUID as randomUUID3 } from "crypto";
4122
+ var RANGE_WINDOW_DAYS = { "30d": 30, "7d": 7 };
4123
+ var TOKEN_MILESTONES = [
4124
+ 1e3,
4125
+ 1e4,
4126
+ 1e5,
4127
+ 5e5,
4128
+ 1e6,
4129
+ 5e6,
4130
+ 1e7,
4131
+ 5e7,
4132
+ 1e8,
4133
+ 5e8,
4134
+ 1e9
4135
+ ];
4136
+ function titleCaseModelName(modelKey) {
4137
+ const raw = modelKey.split("|")[0] ?? modelKey;
4138
+ return raw.replace(/\b\w/g, (c) => c.toUpperCase());
4139
+ }
4109
4140
  function safeJson(s) {
4110
4141
  try {
4111
4142
  return JSON.parse(s);
@@ -4370,6 +4401,18 @@ var ConversationStore = class {
4370
4401
  if (!this.hasColumn("messages", "edited")) this.db.exec(`ALTER TABLE messages ADD COLUMN edited INTEGER NOT NULL DEFAULT 0;`);
4371
4402
  this.db.exec(`PRAGMA user_version = 26;`);
4372
4403
  }
4404
+ if (v < 27) {
4405
+ this.db.exec(`
4406
+ CREATE TABLE IF NOT EXISTS memory_facts (
4407
+ id TEXT PRIMARY KEY,
4408
+ fact_text TEXT NOT NULL,
4409
+ source_conv_id TEXT,
4410
+ created_at TEXT NOT NULL
4411
+ );
4412
+ CREATE INDEX IF NOT EXISTS idx_memory_facts_created ON memory_facts(created_at);
4413
+ PRAGMA user_version = 27;
4414
+ `);
4415
+ }
4373
4416
  }
4374
4417
  listConversations(q, kind = "all") {
4375
4418
  if (q) {
@@ -4526,6 +4569,200 @@ var ConversationStore = class {
4526
4569
  }
4527
4570
  return out;
4528
4571
  }
4572
+ /** Token usage dashboard (Release 3): sessions/messages/tokens/active-days scoped to
4573
+ * `range`, lifetime streaks (a streak isn't a "last N days" concept), the local hour
4574
+ * with the most activity, a per-model breakdown, and the heatmap day buckets — all from
4575
+ * one pass over `messages`. Days are bucketed by LOCAL calendar date (this daemon and its
4576
+ * user are always the same machine), not UTC, so a late-night session lands on the day
4577
+ * the user actually thinks of as "today". */
4578
+ tokenUsageStats(range = "all") {
4579
+ const rows = this.db.prepare(`
4580
+ SELECT conv_id, created_at, stats, model_key FROM messages
4581
+ WHERE role = 'assistant'
4582
+ ORDER BY created_at ASC
4583
+ `).all();
4584
+ if (rows.length === 0) {
4585
+ return {
4586
+ range,
4587
+ sessions: 0,
4588
+ messages: 0,
4589
+ totalTokens: 0,
4590
+ activeDays: 0,
4591
+ currentStreak: 0,
4592
+ longestStreak: 0,
4593
+ peakHour: null,
4594
+ favoriteModel: null,
4595
+ firstMessageAt: null,
4596
+ lifetimeTotalTokens: 0,
4597
+ milestone: { achieved: null, next: null, progressPct: null },
4598
+ activity: { granularityHours: 24, buckets: [] },
4599
+ dailyByModel: [],
4600
+ byModel: []
4601
+ };
4602
+ }
4603
+ const dayKey = (iso) => {
4604
+ const d = new Date(iso);
4605
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
4606
+ };
4607
+ const addDays = (key, n) => {
4608
+ const [y, m, d] = key.split("-").map(Number);
4609
+ const dt = new Date(y, m - 1, d + n);
4610
+ return `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, "0")}-${String(dt.getDate()).padStart(2, "0")}`;
4611
+ };
4612
+ const dayBuckets = /* @__PURE__ */ new Map();
4613
+ const firstMessageAt = rows[0].created_at;
4614
+ for (const r of rows) {
4615
+ const key = dayKey(r.created_at);
4616
+ const b = dayBuckets.get(key) ?? { promptTokens: 0, genTokens: 0, messageCount: 0 };
4617
+ const s = safeJson(r.stats);
4618
+ b.promptTokens += typeof s.promptTokens === "number" ? s.promptTokens : 0;
4619
+ b.genTokens += typeof s.genTokens === "number" ? s.genTokens : 0;
4620
+ b.messageCount++;
4621
+ dayBuckets.set(key, b);
4622
+ }
4623
+ const todayKey = dayKey((/* @__PURE__ */ new Date()).toISOString());
4624
+ const firstKey = dayKey(firstMessageAt);
4625
+ let longestStreak = 0, run = 0;
4626
+ for (let key = firstKey; key <= todayKey; key = addDays(key, 1)) {
4627
+ if (dayBuckets.has(key)) {
4628
+ run++;
4629
+ longestStreak = Math.max(longestStreak, run);
4630
+ } else {
4631
+ run = 0;
4632
+ }
4633
+ }
4634
+ let currentStreak = 0;
4635
+ {
4636
+ let key = dayBuckets.has(todayKey) ? todayKey : addDays(todayKey, -1);
4637
+ while (key >= firstKey && dayBuckets.has(key)) {
4638
+ currentStreak++;
4639
+ key = addDays(key, -1);
4640
+ }
4641
+ }
4642
+ const minAllStart = addDays(todayKey, -179);
4643
+ const heatStart = range === "all" ? firstKey < minAllStart ? firstKey : minAllStart : addDays(todayKey, -(RANGE_WINDOW_DAYS[range] - 1));
4644
+ const scopedRows = range === "all" ? rows : rows.filter((r) => dayKey(r.created_at) >= heatStart);
4645
+ const convIds = /* @__PURE__ */ new Set();
4646
+ const hourTally = /* @__PURE__ */ new Map();
4647
+ const modelTally = /* @__PURE__ */ new Map();
4648
+ const dayModelTally = /* @__PURE__ */ new Map();
4649
+ let totalTokens = 0;
4650
+ for (const r of scopedRows) {
4651
+ convIds.add(r.conv_id);
4652
+ const s = safeJson(r.stats);
4653
+ const pt = typeof s.promptTokens === "number" ? s.promptTokens : 0;
4654
+ const gt = typeof s.genTokens === "number" ? s.genTokens : 0;
4655
+ totalTokens += pt + gt;
4656
+ const hour = new Date(r.created_at).getHours();
4657
+ hourTally.set(hour, (hourTally.get(hour) ?? 0) + 1);
4658
+ if (r.model_key) {
4659
+ const m = modelTally.get(r.model_key) ?? { messageCount: 0, promptTokens: 0, genTokens: 0 };
4660
+ m.messageCount++;
4661
+ m.promptTokens += pt;
4662
+ m.genTokens += gt;
4663
+ modelTally.set(r.model_key, m);
4664
+ const day = dayKey(r.created_at);
4665
+ const dm = dayModelTally.get(day) ?? /* @__PURE__ */ new Map();
4666
+ dm.set(r.model_key, (dm.get(r.model_key) ?? 0) + pt + gt);
4667
+ dayModelTally.set(day, dm);
4668
+ }
4669
+ }
4670
+ let peakHour = null, peakHourCount = -1;
4671
+ for (const [hour, count] of hourTally) if (count > peakHourCount) {
4672
+ peakHour = hour;
4673
+ peakHourCount = count;
4674
+ }
4675
+ const byModel = [...modelTally.entries()].map(([modelKey, m]) => ({
4676
+ modelKey,
4677
+ displayName: titleCaseModelName(modelKey),
4678
+ messageCount: m.messageCount,
4679
+ promptTokens: m.promptTokens,
4680
+ genTokens: m.genTokens,
4681
+ totalTokens: m.promptTokens + m.genTokens
4682
+ })).sort((a, b) => b.totalTokens - a.totalTokens);
4683
+ const days = [];
4684
+ const dailyByModel = [];
4685
+ let activeDays = 0;
4686
+ for (let key = heatStart; key <= todayKey; key = addDays(key, 1)) {
4687
+ const b = dayBuckets.get(key);
4688
+ if (b && b.messageCount > 0) activeDays++;
4689
+ const dayTotalTokens = (b?.promptTokens ?? 0) + (b?.genTokens ?? 0);
4690
+ days.push({
4691
+ date: key,
4692
+ promptTokens: b?.promptTokens ?? 0,
4693
+ genTokens: b?.genTokens ?? 0,
4694
+ totalTokens: dayTotalTokens,
4695
+ messageCount: b?.messageCount ?? 0
4696
+ });
4697
+ const dm = dayModelTally.get(key);
4698
+ dailyByModel.push({
4699
+ date: key,
4700
+ totalTokens: dayTotalTokens,
4701
+ byModel: dm ? [...dm.entries()].map(([modelKey, tokens]) => ({ modelKey, tokens })) : []
4702
+ });
4703
+ }
4704
+ let activity;
4705
+ if (range === "all") {
4706
+ activity = {
4707
+ granularityHours: 24,
4708
+ buckets: days.map((d) => ({ start: d.date, totalTokens: d.totalTokens, messageCount: d.messageCount }))
4709
+ };
4710
+ } else {
4711
+ const granularityHours = range === "7d" ? 1 : 12;
4712
+ const subTally = /* @__PURE__ */ new Map();
4713
+ for (const r of scopedRows) {
4714
+ const d = new Date(r.created_at);
4715
+ const day = dayKey(r.created_at);
4716
+ const bucketKey = granularityHours === 1 ? `${day}T${String(d.getHours()).padStart(2, "0")}` : `${day}-${d.getHours() < 12 ? "AM" : "PM"}`;
4717
+ const s = safeJson(r.stats);
4718
+ const pt = typeof s.promptTokens === "number" ? s.promptTokens : 0;
4719
+ const gt = typeof s.genTokens === "number" ? s.genTokens : 0;
4720
+ const b = subTally.get(bucketKey) ?? { totalTokens: 0, messageCount: 0 };
4721
+ b.totalTokens += pt + gt;
4722
+ b.messageCount++;
4723
+ subTally.set(bucketKey, b);
4724
+ }
4725
+ const buckets = [];
4726
+ for (let key = heatStart; key <= todayKey; key = addDays(key, 1)) {
4727
+ if (granularityHours === 1) {
4728
+ for (let h = 0; h < 24; h++) {
4729
+ const bucketKey = `${key}T${String(h).padStart(2, "0")}`;
4730
+ const b = subTally.get(bucketKey);
4731
+ buckets.push({ start: bucketKey, totalTokens: b?.totalTokens ?? 0, messageCount: b?.messageCount ?? 0 });
4732
+ }
4733
+ } else {
4734
+ for (const half of ["AM", "PM"]) {
4735
+ const bucketKey = `${key}-${half}`;
4736
+ const b = subTally.get(bucketKey);
4737
+ buckets.push({ start: bucketKey, totalTokens: b?.totalTokens ?? 0, messageCount: b?.messageCount ?? 0 });
4738
+ }
4739
+ }
4740
+ }
4741
+ activity = { granularityHours, buckets };
4742
+ }
4743
+ let lifetimeTotalTokens = 0;
4744
+ for (const b of dayBuckets.values()) lifetimeTotalTokens += b.promptTokens + b.genTokens;
4745
+ const achieved = [...TOKEN_MILESTONES].reverse().find((m) => m <= lifetimeTotalTokens) ?? null;
4746
+ const next = TOKEN_MILESTONES.find((m) => m > lifetimeTotalTokens) ?? null;
4747
+ const progressPct = next !== null ? Math.round((lifetimeTotalTokens - (achieved ?? 0)) / (next - (achieved ?? 0)) * 1e3) / 10 : null;
4748
+ return {
4749
+ range,
4750
+ sessions: convIds.size,
4751
+ messages: scopedRows.length,
4752
+ totalTokens,
4753
+ activeDays,
4754
+ currentStreak,
4755
+ longestStreak,
4756
+ peakHour,
4757
+ favoriteModel: byModel[0]?.displayName ?? null,
4758
+ firstMessageAt,
4759
+ lifetimeTotalTokens,
4760
+ milestone: { achieved, next, progressPct },
4761
+ activity,
4762
+ dailyByModel,
4763
+ byModel
4764
+ };
4765
+ }
4529
4766
  /** Active branch only — matches getMessages(). Used to find "the last thing the user
4530
4767
  * currently sees" (e.g. to decide whether a fresh assistant placeholder follows it). */
4531
4768
  getLastMessage(convId) {
@@ -4749,6 +4986,21 @@ var ConversationStore = class {
4749
4986
  pruneAgentLessons(agentId) {
4750
4987
  this.db.prepare(`DELETE FROM agent_lessons WHERE agent_id = $a`).run({ $a: agentId });
4751
4988
  }
4989
+ // ── Auto-memory (Release 3) ───────────────────────────────────────────────────
4990
+ // Insert/list/delete only — no update. Facts are reviewed and removed, not hand-edited.
4991
+ addMemoryFact(row) {
4992
+ const id = randomUUID3();
4993
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4994
+ this.db.prepare(`INSERT INTO memory_facts (id,fact_text,source_conv_id,created_at) VALUES ($id,$ft,$sc,$now)`).run({ $id: id, $ft: row.factText, $sc: row.sourceConvId ?? null, $now: now });
4995
+ return { id, factText: row.factText, sourceConvId: row.sourceConvId, createdAt: now };
4996
+ }
4997
+ listMemoryFacts() {
4998
+ const rows = this.db.prepare(`SELECT * FROM memory_facts ORDER BY created_at DESC`).all();
4999
+ return rows.map((r) => ({ id: r.id, factText: r.fact_text, sourceConvId: r.source_conv_id ?? void 0, createdAt: r.created_at }));
5000
+ }
5001
+ deleteMemoryFact(id) {
5002
+ return this.db.prepare(`DELETE FROM memory_facts WHERE id = $id`).run({ $id: id }).changes > 0;
5003
+ }
4752
5004
  // ── Skills grown from experience (redesign §3.3, Voyager) ─────────────────────
4753
5005
  addAgentSkill(row) {
4754
5006
  const id = randomUUID3();
@@ -5920,6 +6172,7 @@ var BenchRunner = class {
5920
6172
  * LOAD-time property, so we find that ngl with cheap load-and-read-VRAM probes (no generation),
5921
6173
  * and only measure t/s at the winner. CPU-only machines skip straight to ngl=0. */
5922
6174
  async denseSearch(entry, sys, base2, caps, results, headroomMb) {
6175
+ if (base2.nglFit) return this.benchAt(entry, sys, base2, caps, results, "auto-fit");
5923
6176
  let bestNgl = 0;
5924
6177
  if (sys.gpus.length > 0) {
5925
6178
  const hi0 = entry.blockCount > 0 ? entry.blockCount : 99;
@@ -5947,6 +6200,7 @@ var BenchRunner = class {
5947
6200
  * still leaves the configured headroom. Found with cheap load-and-read-VRAM probes (no generation);
5948
6201
  * t/s is measured only at the winner. */
5949
6202
  async moeSearch(entry, sys, base2, caps, results, headroomMb) {
6203
+ if (base2.nCpuMoeFit) return this.benchAt(entry, sys, base2, caps, results, "auto-fit");
5950
6204
  const derived = deriveDefault(entry, sys);
5951
6205
  const maxN = entry.blockCount > 0 ? entry.blockCount : derived.nCpuMoe || 0;
5952
6206
  let lo = 0, hi = maxN;
@@ -6070,7 +6324,9 @@ var BenchRunner = class {
6070
6324
  const params = {
6071
6325
  ctx: profile.ctx,
6072
6326
  ngl: profile.ngl,
6327
+ nglFit: profile.nglFit,
6073
6328
  nCpuMoe: profile.nCpuMoe,
6329
+ nCpuMoeFit: profile.nCpuMoeFit,
6074
6330
  parallel: profile.parallel,
6075
6331
  kvTypeK: profile.kvTypeK,
6076
6332
  flashAttn: profile.flashAttn
@@ -8987,6 +9243,108 @@ function saveSkillFromConversation(d, convId) {
8987
9243
  return taskId;
8988
9244
  }
8989
9245
 
9246
+ // src/chat/memory.ts
9247
+ var MIN_LENGTH = 12;
9248
+ var MAX_FACTS_PER_TURN = 5;
9249
+ var MAX_FACT_LENGTH = 200;
9250
+ function extractionPrompt(userText) {
9251
+ return `Extract any durable personal facts the user stated about themselves in the message below \u2014 the kind of thing worth remembering for future conversations (name, role/job, location, preferences, hardware/software setup, ongoing projects, goals, recurring constraints).
9252
+
9253
+ Rules:
9254
+ - Only use what the user explicitly said about themselves in THIS message. Never infer, guess, or invent.
9255
+ - Skip one-off requests, questions, opinions about the current topic, or anything not durable (e.g. "I'm tired today" is not durable; "I work as a nurse" is).
9256
+ - Each fact must be a short, self-contained sentence (max ~15 words) that makes sense with no other context.
9257
+ - If there are no durable personal facts, reply with exactly: NONE
9258
+ - Otherwise reply with ONLY a list, one fact per line, each line starting with "- ". No preamble, no explanation, nothing else.
9259
+
9260
+ User's message:
9261
+ """
9262
+ ${userText.slice(0, 2e3)}
9263
+ """`;
9264
+ }
9265
+ var FACT_STOPWORDS = /* @__PURE__ */ new Set([
9266
+ "i",
9267
+ "a",
9268
+ "an",
9269
+ "the",
9270
+ "is",
9271
+ "am",
9272
+ "are",
9273
+ "was",
9274
+ "were",
9275
+ "in",
9276
+ "on",
9277
+ "at",
9278
+ "as",
9279
+ "my",
9280
+ "of",
9281
+ "to",
9282
+ "for",
9283
+ "and",
9284
+ "or",
9285
+ "with"
9286
+ ]);
9287
+ function isDuplicateFact(existing, candidate) {
9288
+ const norm = (s) => s.toLowerCase().trim().replace(/[.!?]+$/, "");
9289
+ const contentWords = (s) => new Set(s.split(/\s+/).filter((w) => w && !FACT_STOPWORDS.has(w)));
9290
+ const c = norm(candidate);
9291
+ if (!c) return true;
9292
+ for (const e of existing) {
9293
+ const ne = norm(e);
9294
+ if (!ne) continue;
9295
+ if (ne.includes(c) || c.includes(ne)) return true;
9296
+ const ws1 = contentWords(ne);
9297
+ const ws2 = contentWords(c);
9298
+ const intersection = [...ws1].filter((w) => ws2.has(w)).length;
9299
+ const union = (/* @__PURE__ */ new Set([...ws1, ...ws2])).size;
9300
+ if (union > 0 && intersection / union >= 0.6) return true;
9301
+ }
9302
+ return false;
9303
+ }
9304
+ function parseFacts(raw) {
9305
+ const cleaned = raw.replace(/<think>[\s\S]*?<\/think>/gi, "").trim();
9306
+ if (!cleaned || /^none$/i.test(cleaned)) return [];
9307
+ return cleaned.split("\n").map((l) => l.replace(/^[-*•]\s*/, "").trim()).filter((l) => l.length > 0 && l.length <= MAX_FACT_LENGTH).slice(0, MAX_FACTS_PER_TURN);
9308
+ }
9309
+ async function extractMemoryFacts(d, convId, userText, target) {
9310
+ try {
9311
+ if (userText.trim().length < MIN_LENGTH) return;
9312
+ const ms = d.manager.status();
9313
+ if (ms.state !== "running") return;
9314
+ const release = d.gate ? await d.gate.acquire("bg") : null;
9315
+ let res;
9316
+ try {
9317
+ res = await fetch(`${target}/v1/chat/completions`, {
9318
+ method: "POST",
9319
+ headers: { "Content-Type": "application/json" },
9320
+ body: JSON.stringify({
9321
+ model: engineModelAlias(d.registry.active()?.kind ?? "") ?? ms.model?.key,
9322
+ messages: [{ role: "user", content: extractionPrompt(userText) }],
9323
+ stream: false,
9324
+ temperature: 0.2,
9325
+ max_tokens: 200,
9326
+ reasoning_budget: 0,
9327
+ chat_template_kwargs: { enable_thinking: false }
9328
+ }),
9329
+ signal: AbortSignal.timeout(2e4)
9330
+ });
9331
+ } finally {
9332
+ release?.();
9333
+ }
9334
+ if (!res.ok) return;
9335
+ const data = await res.json();
9336
+ const facts = parseFacts(data.choices?.[0]?.message?.content ?? "");
9337
+ if (!facts.length) return;
9338
+ const existing = d.db.listMemoryFacts().map((f) => f.factText);
9339
+ for (const fact of facts) {
9340
+ if (isDuplicateFact(existing, fact)) continue;
9341
+ d.db.addMemoryFact({ factText: fact, sourceConvId: convId });
9342
+ existing.push(fact);
9343
+ }
9344
+ } catch {
9345
+ }
9346
+ }
9347
+
8990
9348
  // src/chat/chat-routes.ts
8991
9349
  var inflight = /* @__PURE__ */ new Map();
8992
9350
  function abortAllInFlightChats() {
@@ -9029,6 +9387,17 @@ function registerChatRoutes(app2, d) {
9029
9387
  if (!ok) return err(c, 404, "not_found", "Folder not found.");
9030
9388
  return c.json({ ok: true });
9031
9389
  });
9390
+ app2.get("/api/v1/memory", (c) => c.json({ facts: db2.listMemoryFacts() }));
9391
+ app2.delete("/api/v1/memory/:id", (c) => {
9392
+ const ok = db2.deleteMemoryFact(c.req.param("id"));
9393
+ if (!ok) return err(c, 404, "not_found", "Fact not found.");
9394
+ return c.json({ ok: true });
9395
+ });
9396
+ app2.get("/api/v1/tokens/usage", (c) => {
9397
+ const q = c.req.query("range");
9398
+ const range = q === "30d" || q === "7d" ? q : "all";
9399
+ return c.json(db2.tokenUsageStats(range));
9400
+ });
9032
9401
  app2.get("/api/v1/conversations", (c) => {
9033
9402
  const q = c.req.query("q");
9034
9403
  return c.json({ conversations: db2.listConversations(q, "chat") });
@@ -9158,7 +9527,7 @@ ${content}` : b.docContext : content;
9158
9527
  ...images.map((url) => ({ type: "image_url", image_url: { url } }))
9159
9528
  ] : fullContent;
9160
9529
  engineMessages.push({ role: "user", content: userContent });
9161
- await runGeneration(d, stream, { convId, conv, engineMessages, assistantMsg, ms, target, ac, disableThinking: b.disableThinking ?? false });
9530
+ await runGeneration(d, stream, { convId, conv, engineMessages, assistantMsg, ms, target, ac, disableThinking: b.disableThinking ?? false, userText: content });
9162
9531
  });
9163
9532
  });
9164
9533
  app2.post("/api/v1/conversations/:id/continue", async (c) => {
@@ -9874,6 +10243,11 @@ ${existing}`.trim() };
9874
10243
  void autoTitle(d, convId, ctx.engineMessages, fullContent, target);
9875
10244
  }, 1e3);
9876
10245
  }
10246
+ if (!aborted && ctx.userText && d.store.snapshot().daemon.autoMemoryEnabled) {
10247
+ setTimeout(() => {
10248
+ void extractMemoryFacts(d, convId, ctx.userText, target);
10249
+ }, 1500);
10250
+ }
9877
10251
  }
9878
10252
  async function autoTitle(d, convId, prevMessages, assistantReply, target) {
9879
10253
  try {
@@ -12213,6 +12587,7 @@ function registerApi(app2, d) {
12213
12587
  updates2.theme = b.theme;
12214
12588
  }
12215
12589
  if (b.autoGenerateTitles !== void 0) updates2.autoGenerateTitles = !!b.autoGenerateTitles;
12590
+ if (b.autoMemoryEnabled !== void 0) updates2.autoMemoryEnabled = !!b.autoMemoryEnabled;
12216
12591
  if (b.openBrowserOnStart !== void 0) updates2.openBrowserOnStart = !!b.openBrowserOnStart;
12217
12592
  let vramHeadroomMb;
12218
12593
  if (b.vramHeadroomMb !== void 0) {
@@ -12643,6 +13018,7 @@ function settingsPayload(d) {
12643
13018
  port: cfg2.daemon.port,
12644
13019
  theme: cfg2.daemon.theme,
12645
13020
  autoGenerateTitles: cfg2.daemon.autoGenerateTitles,
13021
+ autoMemoryEnabled: cfg2.daemon.autoMemoryEnabled,
12646
13022
  openBrowserOnStart: cfg2.daemon.openBrowserOnStart,
12647
13023
  autoLoadOnStart: cfg2.autoLoadOnStart,
12648
13024
  vramHeadroomMb: cfg2.vramHeadroomMb,
@@ -1 +1 @@
1
- import{j as e}from"./radix-krE_SGZJ.js";import{u as M,b as W,r as b}from"./vendor-BE0HADM8.js";import{u as $,b as G,c as H,d as Y,e as J,a as E,B as y,f as B,t as h,A as C}from"./index-DT-1Skfu.js";import{f as U}from"./chat-api-x93Fzf3q.js";import{f as V,a as X}from"./agent-api-B0qitSjr.js";import{P as z}from"./personas-CsvqnRpC.js";import{C as Z}from"./chevron-left-D03iirWA.js";import{R as ee}from"./rotate-ccw-BOQNX9cU.js";import{T as te}from"./trash-2-CT_92vaT.js";import{C as se}from"./chevron-right-B7Mj5eje.js";const ne=()=>({name:"",description:"",systemPrompt:"",skillIds:[],tools:[]});function oe(s){const d=[],x=new Map;for(const n of s){const o=/^mcp__([^_]+(?:_[^_]+)*)__/.exec(n.name);if(!o){d.push(n);continue}const i=o[1],p=/^\[([^\]]+)\]/.exec(n.description),c=p?p[1]:i.replace(/_/g," ");x.has(i)||x.set(i,{key:i,label:c,tools:[]}),x.get(i).tools.push(n)}return{builtin:d,groups:[...x.values()].sort((n,o)=>n.label.localeCompare(o.label))}}function ae(s){return s.replace(/^mcp__[^_]+(?:_[^_]+)*__/,"")}function le(s){return s.replace(/^\[[^\]]+\]\s*/,"")}function q({tool:s,checked:d,onToggle:x}){return e.jsxs("label",{className:"flex cursor-pointer items-start gap-2 text-[12px]",children:[e.jsx("input",{type:"checkbox",className:"mt-0.5",checked:d,onChange:x}),e.jsxs("span",{className:"flex flex-col",children:[e.jsx("span",{className:"font-mono text-ink",children:ae(s.name)}),s.description&&e.jsx("span",{className:"text-faint",children:le(s.description)})]})]})}function re({group:s,selected:d,onToggleAll:x,onToggleOne:n}){const[o,i]=b.useState(!1),p=s.tools.map(l=>l.name),c=p.filter(l=>d.has(l)).length,a=c===p.length,f=c>0&&!a,u=b.useRef(null);return b.useEffect(()=>{u.current&&(u.current.indeterminate=f)},[f]),e.jsxs("div",{className:"rounded-md border border-border",children:[e.jsxs("div",{className:"flex items-center gap-2 px-2.5 py-2",children:[e.jsx("input",{ref:u,type:"checkbox",checked:a,onChange:()=>x(p,!a)}),e.jsxs("button",{type:"button",onClick:()=>i(l=>!l),className:"flex flex-1 items-center gap-1.5 text-left text-[13px] text-ink",children:[e.jsx(se,{size:12,className:B("shrink-0 text-faint transition-transform",o&&"rotate-90")}),s.label,e.jsxs("span",{className:"text-[11px] text-faint",children:["(",c,"/",p.length,")"]})]})]}),o&&e.jsx("div",{className:"flex flex-col gap-1.5 border-t border-border px-3 py-2 pl-9",children:s.tools.map(l=>e.jsx(q,{tool:l,checked:d.has(l.name),onToggle:()=>n(l.name)},l.name))})]})}function ie({tools:s,selected:d,onChange:x}){const n=new Set(d),{builtin:o,groups:i}=oe(s),p=a=>x(n.has(a)?d.filter(f=>f!==a):[...d,a]),c=(a,f)=>{const u=d.filter(l=>!a.includes(l));x(f?[...u,...a]:u)};return s.length===0?e.jsx("p",{className:"text-[12px] text-faint",children:"No tools available yet."}):e.jsxs("div",{className:"flex flex-col gap-2",children:[o.length>0&&e.jsx("div",{className:"flex flex-col gap-2 rounded-md border border-border bg-panel-2 p-3",children:o.map(a=>e.jsx(q,{tool:a,checked:n.has(a.name),onToggle:()=>p(a.name)},a.name))}),i.map(a=>e.jsx(re,{group:a,selected:n,onToggleAll:c,onToggleOne:p},a.key))]})}function ke(){const{agentId:s="new"}=M(),d=W(),x=$(),n=s==="new",o=!n&&z.some(t=>t.id===s),i=o?z.find(t=>t.id===s):void 0,p=G(),c=!n&&!o?(p.data??[]).find(t=>t.id===s):void 0,a=H(),f=Y(),u=J(),l=o?f.data?.[s]:void 0,_=E({queryKey:X.list(),queryFn:V,staleTime:0}),k=_.data??[],I=E({queryKey:["available-tools"],queryFn:U}),j=I.data??[],[m,g]=b.useState(ne()),[S,T]=b.useState(!1),[A,P]=b.useState(!1),[D,w]=b.useState(!1),[R,L]=b.useState("skills");n&&!S&&(k.length||j.length)&&(g(t=>({...t,skillIds:k.map(r=>r.id),tools:j.map(r=>r.name)})),T(!0)),!n&&!o&&!S&&c&&(g({name:c.name,description:c.description,systemPrompt:c.systemPrompt,skillIds:c.skillIds,tools:c.tools}),T(!0)),o&&!S&&i&&f.isSuccess&&(k.length||j.length)&&(g({name:l?.name??i.name,description:l?.description??i.description,systemPrompt:l?.systemPrompt??i.systemPrompt,skillIds:l?.skillIds??k.map(t=>t.id),tools:l?.tools??j.map(t=>t.name)}),T(!0));const N=()=>d("/customize"),Q=t=>g(r=>({...r,skillIds:r.skillIds.includes(t)?r.skillIds.filter(v=>v!==t):[...r.skillIds,t]})),O=()=>{if(!m.name.trim()){h.error("Name is required.");return}P(!0);const t={name:m.name.trim(),description:m.description.trim(),systemPrompt:m.systemPrompt,skillIds:m.skillIds,tools:m.tools};if(o){u.save.mutate({id:s,override:t},{onSuccess:()=>{h.success("Agent saved."),N()},onError:v=>{h.error(v instanceof C?v.message:"Could not save agent."),P(!1)}});return}const r={onSuccess:()=>{x.invalidateQueries({queryKey:["chat-agents"]}),h.success(n?"Agent created.":"Agent saved."),N()},onError:v=>{h.error(v instanceof C?v.message:"Could not save agent."),P(!1)}};n?a.add.mutate(t,r):a.update.mutate({id:s,patch:t},r)},F=()=>{u.reset.mutate(s,{onSuccess:()=>{h.success("Reset to default."),i&&g({name:i.name,description:i.description,systemPrompt:i.systemPrompt,skillIds:k.map(t=>t.id),tools:j.map(t=>t.name)})},onError:t=>h.error(t instanceof C?t.message:"Could not reset agent.")})},K=()=>{a.remove.mutate(s,{onSuccess:()=>{h.success("Agent deleted."),N()},onError:t=>h.error(t instanceof C?t.message:"Could not delete agent.")})};return!n&&!o&&p.isSuccess&&!c?e.jsxs("div",{className:"flex w-full flex-col items-center gap-3 px-8 py-16",children:[e.jsx("p",{className:"text-[14px] text-muted",children:"That agent no longer exists."}),e.jsx(y,{size:"sm",variant:"outline",onClick:N,children:"Back to agents"})]}):e.jsxs("div",{className:"flex w-full max-w-4xl flex-col gap-5 px-4 py-6 md:px-8",children:[e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsx("button",{type:"button",onClick:N,className:"text-faint hover:text-ink",title:"Back to agents",children:e.jsx(Z,{size:18})}),e.jsx("span",{className:"text-[15px] font-medium text-ink",children:n?"New agent":o?l?"Edit agent (modified built-in)":"Edit agent (built-in)":"Edit agent"}),e.jsxs("div",{className:"ml-auto flex items-center gap-2",children:[o&&e.jsxs(y,{size:"sm",variant:"outline",onClick:F,disabled:u.reset.isPending,children:[e.jsx(ee,{size:13})," Reset to default"]}),e.jsx(y,{size:"sm",variant:"outline",onClick:N,children:"Cancel"}),e.jsx(y,{size:"sm",onClick:O,disabled:A||!m.name.trim(),children:A?"Saving…":"Save"})]})]}),o&&e.jsx("p",{className:"rounded-md border border-border bg-panel px-3 py-2 text-[12px] text-muted",children:"Built-in agents can be edited in place — Reset to default discards your changes and restores TurboLLM's original prompt and access."}),e.jsxs("div",{className:"flex flex-col gap-1.5",children:[e.jsx("label",{className:"text-[12px] font-medium text-muted",children:"Name"}),e.jsx("input",{className:"rounded-md border border-border bg-bg px-3 py-1.5 text-[13px] text-ink outline-none focus:border-accent placeholder:text-faint",placeholder:"Release Notes Writer",value:m.name,onChange:t=>g(r=>({...r,name:t.target.value}))})]}),e.jsxs("div",{className:"flex flex-col gap-1.5",children:[e.jsx("label",{className:"text-[12px] font-medium text-muted",children:"Description"}),e.jsx("input",{className:"rounded-md border border-border bg-bg px-3 py-1.5 text-[13px] text-ink outline-none focus:border-accent placeholder:text-faint",placeholder:"What this agent is for, in one line",value:m.description,onChange:t=>g(r=>({...r,description:t.target.value}))})]}),e.jsxs("div",{className:"flex flex-col gap-1.5",children:[e.jsx("label",{className:"text-[12px] font-medium text-muted",children:"System prompt"}),e.jsx("textarea",{className:"min-h-[160px] resize-y rounded-md border border-border bg-bg px-3 py-2 font-mono text-[12px] leading-relaxed text-ink outline-none focus:border-accent placeholder:text-faint",placeholder:"You are…",value:m.systemPrompt,onChange:t=>g(r=>({...r,systemPrompt:t.target.value}))})]}),e.jsxs("div",{className:"flex flex-col gap-2.5",children:[e.jsx("div",{className:"inline-flex w-fit rounded-md border border-border p-0.5",children:["skills","tools"].map(t=>e.jsx("button",{type:"button",onClick:()=>L(t),className:B("rounded px-3 py-1.5 text-[13px] font-medium transition-colors",R===t?"bg-accent/12 text-accent":"text-muted hover:text-ink"),children:t==="skills"?`Skills (${m.skillIds.length})`:`Tools (${m.tools.length})`},t))}),R==="skills"?_.isLoading?e.jsx("p",{className:"text-[12px] text-faint",children:"Loading…"}):k.length===0?e.jsx("p",{className:"text-[12px] text-faint",children:"No skills yet — create one under Customize → Skills."}):e.jsx("div",{className:"flex flex-col gap-2 rounded-md border border-border bg-panel-2 p-3",children:k.map(t=>e.jsxs("label",{className:"flex cursor-pointer items-start gap-2 text-[13px]",children:[e.jsx("input",{type:"checkbox",className:"mt-0.5",checked:m.skillIds.includes(t.id),onChange:()=>Q(t.id)}),e.jsxs("span",{className:"flex flex-col",children:[e.jsx("span",{className:"text-ink",children:t.name}),t.description&&e.jsx("span",{className:"text-[12px] text-muted",children:t.description})]})]},t.id))}):I.isLoading?e.jsx("p",{className:"text-[12px] text-faint",children:"Loading…"}):e.jsx(ie,{tools:j,selected:m.tools,onChange:t=>g(r=>({...r,tools:t}))})]}),!n&&!o&&(D?e.jsxs("div",{className:"flex items-center gap-2 self-start rounded-md border border-border bg-panel p-2 text-[12px]",children:[e.jsx("span",{className:"flex-1 text-muted",children:"Delete this agent?"}),e.jsx("button",{type:"button",onClick:K,className:"rounded px-2 py-1 text-[color:var(--err)] hover:bg-[color:color-mix(in_srgb,var(--err)_12%,transparent)]",children:"Delete"}),e.jsx("button",{type:"button",onClick:()=>w(!1),className:"rounded px-2 py-1 text-faint hover:text-ink",children:"Cancel"})]}):e.jsxs("button",{type:"button",onClick:()=>w(!0),className:"flex items-center gap-1.5 self-start text-[12px] text-faint hover:text-[color:var(--err)]",children:[e.jsx(te,{size:13})," Delete agent"]}))]})}export{ke as AgentEditPage};
1
+ import{j as e}from"./radix-krE_SGZJ.js";import{u as M,b as W,r as b}from"./vendor-BE0HADM8.js";import{u as $,b as G,c as H,d as Y,e as J,a as E,B as y,f as B,t as h,A as C}from"./index-61gQc5Xl.js";import{f as U}from"./chat-api-v2HtltwV.js";import{f as V,a as X}from"./agent-api-DgdsmygN.js";import{P as z}from"./personas-BoG0K2t1.js";import{C as Z}from"./chevron-left-ZSwpcWK4.js";import{R as ee}from"./rotate-ccw-DCFomh67.js";import{T as te}from"./trash-2-B0HcfuWA.js";import{C as se}from"./chevron-right-BX4x_YDg.js";const ne=()=>({name:"",description:"",systemPrompt:"",skillIds:[],tools:[]});function oe(s){const d=[],x=new Map;for(const n of s){const o=/^mcp__([^_]+(?:_[^_]+)*)__/.exec(n.name);if(!o){d.push(n);continue}const i=o[1],p=/^\[([^\]]+)\]/.exec(n.description),c=p?p[1]:i.replace(/_/g," ");x.has(i)||x.set(i,{key:i,label:c,tools:[]}),x.get(i).tools.push(n)}return{builtin:d,groups:[...x.values()].sort((n,o)=>n.label.localeCompare(o.label))}}function ae(s){return s.replace(/^mcp__[^_]+(?:_[^_]+)*__/,"")}function le(s){return s.replace(/^\[[^\]]+\]\s*/,"")}function q({tool:s,checked:d,onToggle:x}){return e.jsxs("label",{className:"flex cursor-pointer items-start gap-2 text-[12px]",children:[e.jsx("input",{type:"checkbox",className:"mt-0.5",checked:d,onChange:x}),e.jsxs("span",{className:"flex flex-col",children:[e.jsx("span",{className:"font-mono text-ink",children:ae(s.name)}),s.description&&e.jsx("span",{className:"text-faint",children:le(s.description)})]})]})}function re({group:s,selected:d,onToggleAll:x,onToggleOne:n}){const[o,i]=b.useState(!1),p=s.tools.map(l=>l.name),c=p.filter(l=>d.has(l)).length,a=c===p.length,f=c>0&&!a,u=b.useRef(null);return b.useEffect(()=>{u.current&&(u.current.indeterminate=f)},[f]),e.jsxs("div",{className:"rounded-md border border-border",children:[e.jsxs("div",{className:"flex items-center gap-2 px-2.5 py-2",children:[e.jsx("input",{ref:u,type:"checkbox",checked:a,onChange:()=>x(p,!a)}),e.jsxs("button",{type:"button",onClick:()=>i(l=>!l),className:"flex flex-1 items-center gap-1.5 text-left text-[13px] text-ink",children:[e.jsx(se,{size:12,className:B("shrink-0 text-faint transition-transform",o&&"rotate-90")}),s.label,e.jsxs("span",{className:"text-[11px] text-faint",children:["(",c,"/",p.length,")"]})]})]}),o&&e.jsx("div",{className:"flex flex-col gap-1.5 border-t border-border px-3 py-2 pl-9",children:s.tools.map(l=>e.jsx(q,{tool:l,checked:d.has(l.name),onToggle:()=>n(l.name)},l.name))})]})}function ie({tools:s,selected:d,onChange:x}){const n=new Set(d),{builtin:o,groups:i}=oe(s),p=a=>x(n.has(a)?d.filter(f=>f!==a):[...d,a]),c=(a,f)=>{const u=d.filter(l=>!a.includes(l));x(f?[...u,...a]:u)};return s.length===0?e.jsx("p",{className:"text-[12px] text-faint",children:"No tools available yet."}):e.jsxs("div",{className:"flex flex-col gap-2",children:[o.length>0&&e.jsx("div",{className:"flex flex-col gap-2 rounded-md border border-border bg-panel-2 p-3",children:o.map(a=>e.jsx(q,{tool:a,checked:n.has(a.name),onToggle:()=>p(a.name)},a.name))}),i.map(a=>e.jsx(re,{group:a,selected:n,onToggleAll:c,onToggleOne:p},a.key))]})}function ke(){const{agentId:s="new"}=M(),d=W(),x=$(),n=s==="new",o=!n&&z.some(t=>t.id===s),i=o?z.find(t=>t.id===s):void 0,p=G(),c=!n&&!o?(p.data??[]).find(t=>t.id===s):void 0,a=H(),f=Y(),u=J(),l=o?f.data?.[s]:void 0,_=E({queryKey:X.list(),queryFn:V,staleTime:0}),k=_.data??[],I=E({queryKey:["available-tools"],queryFn:U}),j=I.data??[],[m,g]=b.useState(ne()),[S,T]=b.useState(!1),[A,P]=b.useState(!1),[D,w]=b.useState(!1),[R,L]=b.useState("skills");n&&!S&&(k.length||j.length)&&(g(t=>({...t,skillIds:k.map(r=>r.id),tools:j.map(r=>r.name)})),T(!0)),!n&&!o&&!S&&c&&(g({name:c.name,description:c.description,systemPrompt:c.systemPrompt,skillIds:c.skillIds,tools:c.tools}),T(!0)),o&&!S&&i&&f.isSuccess&&(k.length||j.length)&&(g({name:l?.name??i.name,description:l?.description??i.description,systemPrompt:l?.systemPrompt??i.systemPrompt,skillIds:l?.skillIds??k.map(t=>t.id),tools:l?.tools??j.map(t=>t.name)}),T(!0));const N=()=>d("/customize"),Q=t=>g(r=>({...r,skillIds:r.skillIds.includes(t)?r.skillIds.filter(v=>v!==t):[...r.skillIds,t]})),O=()=>{if(!m.name.trim()){h.error("Name is required.");return}P(!0);const t={name:m.name.trim(),description:m.description.trim(),systemPrompt:m.systemPrompt,skillIds:m.skillIds,tools:m.tools};if(o){u.save.mutate({id:s,override:t},{onSuccess:()=>{h.success("Agent saved."),N()},onError:v=>{h.error(v instanceof C?v.message:"Could not save agent."),P(!1)}});return}const r={onSuccess:()=>{x.invalidateQueries({queryKey:["chat-agents"]}),h.success(n?"Agent created.":"Agent saved."),N()},onError:v=>{h.error(v instanceof C?v.message:"Could not save agent."),P(!1)}};n?a.add.mutate(t,r):a.update.mutate({id:s,patch:t},r)},F=()=>{u.reset.mutate(s,{onSuccess:()=>{h.success("Reset to default."),i&&g({name:i.name,description:i.description,systemPrompt:i.systemPrompt,skillIds:k.map(t=>t.id),tools:j.map(t=>t.name)})},onError:t=>h.error(t instanceof C?t.message:"Could not reset agent.")})},K=()=>{a.remove.mutate(s,{onSuccess:()=>{h.success("Agent deleted."),N()},onError:t=>h.error(t instanceof C?t.message:"Could not delete agent.")})};return!n&&!o&&p.isSuccess&&!c?e.jsxs("div",{className:"flex w-full flex-col items-center gap-3 px-8 py-16",children:[e.jsx("p",{className:"text-[14px] text-muted",children:"That agent no longer exists."}),e.jsx(y,{size:"sm",variant:"outline",onClick:N,children:"Back to agents"})]}):e.jsxs("div",{className:"flex w-full max-w-4xl flex-col gap-5 px-4 py-6 md:px-8",children:[e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsx("button",{type:"button",onClick:N,className:"text-faint hover:text-ink",title:"Back to agents",children:e.jsx(Z,{size:18})}),e.jsx("span",{className:"text-[15px] font-medium text-ink",children:n?"New agent":o?l?"Edit agent (modified built-in)":"Edit agent (built-in)":"Edit agent"}),e.jsxs("div",{className:"ml-auto flex items-center gap-2",children:[o&&e.jsxs(y,{size:"sm",variant:"outline",onClick:F,disabled:u.reset.isPending,children:[e.jsx(ee,{size:13})," Reset to default"]}),e.jsx(y,{size:"sm",variant:"outline",onClick:N,children:"Cancel"}),e.jsx(y,{size:"sm",onClick:O,disabled:A||!m.name.trim(),children:A?"Saving…":"Save"})]})]}),o&&e.jsx("p",{className:"rounded-md border border-border bg-panel px-3 py-2 text-[12px] text-muted",children:"Built-in agents can be edited in place — Reset to default discards your changes and restores TurboLLM's original prompt and access."}),e.jsxs("div",{className:"flex flex-col gap-1.5",children:[e.jsx("label",{className:"text-[12px] font-medium text-muted",children:"Name"}),e.jsx("input",{className:"rounded-md border border-border bg-bg px-3 py-1.5 text-[13px] text-ink outline-none focus:border-accent placeholder:text-faint",placeholder:"Release Notes Writer",value:m.name,onChange:t=>g(r=>({...r,name:t.target.value}))})]}),e.jsxs("div",{className:"flex flex-col gap-1.5",children:[e.jsx("label",{className:"text-[12px] font-medium text-muted",children:"Description"}),e.jsx("input",{className:"rounded-md border border-border bg-bg px-3 py-1.5 text-[13px] text-ink outline-none focus:border-accent placeholder:text-faint",placeholder:"What this agent is for, in one line",value:m.description,onChange:t=>g(r=>({...r,description:t.target.value}))})]}),e.jsxs("div",{className:"flex flex-col gap-1.5",children:[e.jsx("label",{className:"text-[12px] font-medium text-muted",children:"System prompt"}),e.jsx("textarea",{className:"min-h-[160px] resize-y rounded-md border border-border bg-bg px-3 py-2 font-mono text-[12px] leading-relaxed text-ink outline-none focus:border-accent placeholder:text-faint",placeholder:"You are…",value:m.systemPrompt,onChange:t=>g(r=>({...r,systemPrompt:t.target.value}))})]}),e.jsxs("div",{className:"flex flex-col gap-2.5",children:[e.jsx("div",{className:"inline-flex w-fit rounded-md border border-border p-0.5",children:["skills","tools"].map(t=>e.jsx("button",{type:"button",onClick:()=>L(t),className:B("rounded px-3 py-1.5 text-[13px] font-medium transition-colors",R===t?"bg-accent/12 text-accent":"text-muted hover:text-ink"),children:t==="skills"?`Skills (${m.skillIds.length})`:`Tools (${m.tools.length})`},t))}),R==="skills"?_.isLoading?e.jsx("p",{className:"text-[12px] text-faint",children:"Loading…"}):k.length===0?e.jsx("p",{className:"text-[12px] text-faint",children:"No skills yet — create one under Customize → Skills."}):e.jsx("div",{className:"flex flex-col gap-2 rounded-md border border-border bg-panel-2 p-3",children:k.map(t=>e.jsxs("label",{className:"flex cursor-pointer items-start gap-2 text-[13px]",children:[e.jsx("input",{type:"checkbox",className:"mt-0.5",checked:m.skillIds.includes(t.id),onChange:()=>Q(t.id)}),e.jsxs("span",{className:"flex flex-col",children:[e.jsx("span",{className:"text-ink",children:t.name}),t.description&&e.jsx("span",{className:"text-[12px] text-muted",children:t.description})]})]},t.id))}):I.isLoading?e.jsx("p",{className:"text-[12px] text-faint",children:"Loading…"}):e.jsx(ie,{tools:j,selected:m.tools,onChange:t=>g(r=>({...r,tools:t}))})]}),!n&&!o&&(D?e.jsxs("div",{className:"flex items-center gap-2 self-start rounded-md border border-border bg-panel p-2 text-[12px]",children:[e.jsx("span",{className:"flex-1 text-muted",children:"Delete this agent?"}),e.jsx("button",{type:"button",onClick:K,className:"rounded px-2 py-1 text-[color:var(--err)] hover:bg-[color:color-mix(in_srgb,var(--err)_12%,transparent)]",children:"Delete"}),e.jsx("button",{type:"button",onClick:()=>w(!1),className:"rounded px-2 py-1 text-faint hover:text-ink",children:"Cancel"})]}):e.jsxs("button",{type:"button",onClick:()=>w(!0),className:"flex items-center gap-1.5 self-start text-[12px] text-faint hover:text-[color:var(--err)]",children:[e.jsx(te,{size:13})," Delete agent"]}))]})}export{ke as AgentEditPage};