turbollm 1.8.2 → 1.8.4

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 (110) hide show
  1. package/README.md +10 -5
  2. package/dist/cli.js +279 -33
  3. package/dist/webdist/assets/{AgentEditPage-DrcwQcJE.js → AgentEditPage-VoVmMCf1.js} +1 -1
  4. package/dist/webdist/assets/{ChatScreen-BkpwUk-O.js → ChatScreen-DHGEKPZr.js} +1 -1
  5. package/dist/webdist/assets/{CodeComposer-DG4lgSoA.js → CodeComposer-Cxhvsus4.js} +1 -1
  6. package/dist/webdist/assets/CodeHomeScreen-BHIrDU_t.js +1 -0
  7. package/dist/webdist/assets/{CodeSessionScreen-aQesvCPO.js → CodeSessionScreen-CqogdVEN.js} +1 -1
  8. package/dist/webdist/assets/{CustomizeScreen-DRrw2hs7.js → CustomizeScreen-B4XGEyxA.js} +1 -1
  9. package/dist/webdist/assets/DeveloperScreen-jGamwaJG.js +1 -0
  10. package/dist/webdist/assets/EnginesScreen-BRkOTacr.js +7 -0
  11. package/dist/webdist/assets/{FsBrowser-D0rDep29.js → FsBrowser-5VN4Yj8z.js} +1 -1
  12. package/dist/webdist/assets/{ModelLoadMenu-K1Qolm9B.js → ModelLoadMenu-CgM5a39X.js} +1 -1
  13. package/dist/webdist/assets/{ModelsScreen-HfLjpGmf.js → ModelsScreen-GUtj_sux.js} +2 -2
  14. package/dist/webdist/assets/SettingsScreen-D65xd3_e.js +1 -0
  15. package/dist/webdist/assets/{SkillEditPage-W3ofhEQn.js → SkillEditPage-CkKqavr8.js} +1 -1
  16. package/dist/webdist/assets/TokensScreen-CCS_y7DZ.js +1 -0
  17. package/dist/webdist/assets/{ToolApprovalBar-BGRW2hFN.js → ToolApprovalBar-CRZPKrcR.js} +3 -3
  18. package/dist/webdist/assets/WorkspaceScreen-D060hDIn.js +1 -0
  19. package/dist/webdist/assets/{agent-api-C48x10WZ.js → agent-api-BZQuCIAd.js} +1 -1
  20. package/dist/webdist/assets/{arc-B6W63qO_.js → arc-CG_F6y0n.js} +1 -1
  21. package/dist/webdist/assets/{architectureDiagram-3BPJPVTR-BNxjHCm7.js → architectureDiagram-3BPJPVTR-bK0viBCO.js} +1 -1
  22. package/dist/webdist/assets/{arrow-left-DmGW0W4g.js → arrow-left-Cxqlqe4Q.js} +1 -1
  23. package/dist/webdist/assets/{badge-IsOHcYvf.js → badge-D1brk9rd.js} +1 -1
  24. package/dist/webdist/assets/{blockDiagram-GPEHLZMM-DrPlDIMe.js → blockDiagram-GPEHLZMM-DqMi7LYs.js} +1 -1
  25. package/dist/webdist/assets/{bot-DAa4xIIF.js → bot-CldCBtRY.js} +1 -1
  26. package/dist/webdist/assets/{c4Diagram-AAUBKEIU-BKtn6SbN.js → c4Diagram-AAUBKEIU-D5JOtr9l.js} +1 -1
  27. package/dist/webdist/assets/channel-C-X5wnqi.js +1 -0
  28. package/dist/webdist/assets/{chat-api-CtbwBsSY.js → chat-api-uAJNrEFg.js} +1 -1
  29. package/dist/webdist/assets/{chat-queries-BANbggZ1.js → chat-queries-OevUA_jH.js} +1 -1
  30. package/dist/webdist/assets/check-r_BeDgvE.js +1 -0
  31. package/dist/webdist/assets/{chevron-left-DScqk7lr.js → chevron-left-Dv-zay0l.js} +1 -1
  32. package/dist/webdist/assets/{chevron-right-DNc_yz-y.js → chevron-right-DvvJS6ye.js} +1 -1
  33. package/dist/webdist/assets/{chunk-2J33WTMH-B97vCEvd.js → chunk-2J33WTMH-D5FvNAKn.js} +1 -1
  34. package/dist/webdist/assets/{chunk-4BX2VUAB-B5mAgaq1.js → chunk-4BX2VUAB-PiXdGtQT.js} +1 -1
  35. package/dist/webdist/assets/{chunk-55IACEB6-CCZRTh0d.js → chunk-55IACEB6-BcFnIoE2.js} +1 -1
  36. package/dist/webdist/assets/{chunk-727SXJPM-tZT9XNMs.js → chunk-727SXJPM-BigbNnpE.js} +1 -1
  37. package/dist/webdist/assets/{chunk-AQP2D5EJ-CfIdLt8G.js → chunk-AQP2D5EJ-ksTbyzzH.js} +1 -1
  38. package/dist/webdist/assets/{chunk-FMBD7UC4-BRdScA0W.js → chunk-FMBD7UC4-DpDAzpmq.js} +1 -1
  39. package/dist/webdist/assets/{chunk-ND2GUHAM-BedHAIQl.js → chunk-ND2GUHAM-Ch7eIfE0.js} +1 -1
  40. package/dist/webdist/assets/{chunk-QZHKN3VN-ByAw17Nt.js → chunk-QZHKN3VN-DMDG8lEf.js} +1 -1
  41. package/dist/webdist/assets/{circle-check-BNIKoJDg.js → circle-check-BFtGWNjh.js} +1 -1
  42. package/dist/webdist/assets/{circle-x-DfIf4ChY.js → circle-x-BmwEPZU0.js} +1 -1
  43. package/dist/webdist/assets/classDiagram-4FO5ZUOK-Dw2O5mDo.js +1 -0
  44. package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-Dw2O5mDo.js +1 -0
  45. package/dist/webdist/assets/{common-BwXsHduN.js → common-BEcCBxuG.js} +1 -1
  46. package/dist/webdist/assets/{copy-button-BcFHHH9b.js → copy-button-B3sedfsF.js} +1 -1
  47. package/dist/webdist/assets/{cose-bilkent-S5V4N54A--x5WjVCv.js → cose-bilkent-S5V4N54A-BBGTUfOp.js} +1 -1
  48. package/dist/webdist/assets/{dagre-BM42HDAG-DWQy_ulr.js → dagre-BM42HDAG-D8W6jcq_.js} +1 -1
  49. package/dist/webdist/assets/{diagram-2AECGRRQ-DQo4yJeJ.js → diagram-2AECGRRQ-CvCrWf_H.js} +1 -1
  50. package/dist/webdist/assets/{diagram-5GNKFQAL-DwbynoQ2.js → diagram-5GNKFQAL-DZS4yZU1.js} +1 -1
  51. package/dist/webdist/assets/{diagram-KO2AKTUF-CjKV0fFm.js → diagram-KO2AKTUF-BlKSql5d.js} +1 -1
  52. package/dist/webdist/assets/{diagram-LMA3HP47-CXfjg5Fe.js → diagram-LMA3HP47-CKo-tRYQ.js} +1 -1
  53. package/dist/webdist/assets/{diagram-OG6HWLK6-CKEjI4NC.js → diagram-OG6HWLK6-NSePJGx1.js} +1 -1
  54. package/dist/webdist/assets/dialog-B-GLhgc2.js +1 -0
  55. package/dist/webdist/assets/{erDiagram-TEJ5UH35-D43kYg84.js → erDiagram-TEJ5UH35-haPVT0D3.js} +1 -1
  56. package/dist/webdist/assets/{external-link-jnEQCJGd.js → external-link-UbaLGQ_h.js} +1 -1
  57. package/dist/webdist/assets/{flowDiagram-I6XJVG4X-qGS0J-De.js → flowDiagram-I6XJVG4X-BzKZFBHV.js} +1 -1
  58. package/dist/webdist/assets/{ganttDiagram-6RSMTGT7-3Ud_YPtk.js → ganttDiagram-6RSMTGT7-CCmAecLV.js} +1 -1
  59. package/dist/webdist/assets/{git-branch-BZW2_OwW.js → git-branch-Bj6Xfs8n.js} +1 -1
  60. package/dist/webdist/assets/{gitGraphDiagram-PVQCEYII-BhOiYdR2.js → gitGraphDiagram-PVQCEYII-OD-Aumfw.js} +1 -1
  61. package/dist/webdist/assets/{index-DffknE3S.css → index-CfEenPkR.css} +1 -1
  62. package/dist/webdist/assets/{index-rezSzJo3.js → index-D4oBHgyO.js} +4 -4
  63. package/dist/webdist/assets/{infoDiagram-5YYISTIA-D2Yo845I.js → infoDiagram-5YYISTIA-CXiblcgy.js} +1 -1
  64. package/dist/webdist/assets/{ishikawaDiagram-YF4QCWOH-BoTTMzTX.js → ishikawaDiagram-YF4QCWOH-C8sCKBhs.js} +1 -1
  65. package/dist/webdist/assets/{journeyDiagram-JHISSGLW-B30FNQqG.js → journeyDiagram-JHISSGLW-B3be8ACK.js} +1 -1
  66. package/dist/webdist/assets/{kanban-definition-UN3LZRKU-DwuHMcyp.js → kanban-definition-UN3LZRKU-BbfHizAI.js} +1 -1
  67. package/dist/webdist/assets/{layers-BbMBWfEB.js → layers-BqbkDoHi.js} +1 -1
  68. package/dist/webdist/assets/{linear-BI-xSqy2.js → linear-C0lkBXID.js} +1 -1
  69. package/dist/webdist/assets/{mermaid.core-DjBtB3Ob.js → mermaid.core-CW2TnRwR.js} +4 -4
  70. package/dist/webdist/assets/{mindmap-definition-RKZ34NQL-P0Blz3ba.js → mindmap-definition-RKZ34NQL-h-ro0B1A.js} +1 -1
  71. package/dist/webdist/assets/{pencil-pd4qdmIY.js → pencil-BJsQ5uv5.js} +1 -1
  72. package/dist/webdist/assets/{personas-DGNsw4JH.js → personas-fIGMukzj.js} +14 -3
  73. package/dist/webdist/assets/{pieDiagram-4H26LBE5-D_RWWNYB.js → pieDiagram-4H26LBE5-DEGupnzV.js} +1 -1
  74. package/dist/webdist/assets/{plus-Co-23h9C.js → plus-BESpOgWu.js} +1 -1
  75. package/dist/webdist/assets/{quadrantDiagram-W4KKPZXB-0UW-qXoo.js → quadrantDiagram-W4KKPZXB-CMkivigQ.js} +1 -1
  76. package/dist/webdist/assets/{refresh-cw-DBHReAx0.js → refresh-cw-CuuCQSpX.js} +1 -1
  77. package/dist/webdist/assets/{requirementDiagram-4Y6WPE33-DQPViUOU.js → requirementDiagram-4Y6WPE33-DyChhWBw.js} +1 -1
  78. package/dist/webdist/assets/{rotate-ccw-L6hm18l-.js → rotate-ccw-D99lmEA-.js} +1 -1
  79. package/dist/webdist/assets/{sankeyDiagram-5OEKKPKP-CoFKXFxN.js → sankeyDiagram-5OEKKPKP-BDPQ7Ai5.js} +1 -1
  80. package/dist/webdist/assets/{sequenceDiagram-3UESZ5HK-CTKbZzaz.js → sequenceDiagram-3UESZ5HK-C_jdr66e.js} +1 -1
  81. package/dist/webdist/assets/{skeleton-CTqhU4nE.js → skeleton-nGnGDZ2Z.js} +1 -1
  82. package/dist/webdist/assets/{sliders-horizontal-CqA4xQwh.js → sliders-horizontal-BXU5PC3A.js} +1 -1
  83. package/dist/webdist/assets/{star-C4xvUEne.js → star-BGgeiEjL.js} +1 -1
  84. package/dist/webdist/assets/{stateDiagram-AJRCARHV-Bbgyioxb.js → stateDiagram-AJRCARHV-CwtPbKxE.js} +1 -1
  85. package/dist/webdist/assets/stateDiagram-v2-BHNVJYJU-DlCLpW5_.js +1 -0
  86. package/dist/webdist/assets/{switch-vVu6UIJB.js → switch-BQT_NipX.js} +1 -1
  87. package/dist/webdist/assets/{terminal-BqY0B6tG.js → terminal-CrCLNyjB.js} +1 -1
  88. package/dist/webdist/assets/{timeline-definition-PNZ67QCA-CNuDMLf0.js → timeline-definition-PNZ67QCA-B1bN1YFW.js} +1 -1
  89. package/dist/webdist/assets/{trash-2-CI-4vgjV.js → trash-2-CuUIi8kE.js} +1 -1
  90. package/dist/webdist/assets/useIsDesktop-BwBLPtTS.js +1 -0
  91. package/dist/webdist/assets/{vennDiagram-CIIHVFJN-5KfL6f8c.js → vennDiagram-CIIHVFJN-B23LrfT3.js} +1 -1
  92. package/dist/webdist/assets/{wardley-L42UT6IY-KSQpq69w.js → wardley-L42UT6IY-Bkk_DpTu.js} +1 -1
  93. package/dist/webdist/assets/{wardleyDiagram-YWT4CUSO-CRDC9-52.js → wardleyDiagram-YWT4CUSO-Di9GYhE4.js} +1 -1
  94. package/dist/webdist/assets/{wrench-B6O2XFxJ.js → wrench-CUU3qZM0.js} +1 -1
  95. package/dist/webdist/assets/{xychartDiagram-2RQKCTM6-CFJUnPty.js → xychartDiagram-2RQKCTM6-BF1BnBYj.js} +1 -1
  96. package/dist/webdist/index.html +2 -2
  97. package/package.json +1 -1
  98. package/dist/webdist/assets/CodeHomeScreen-Cqv5c0nM.js +0 -1
  99. package/dist/webdist/assets/DeveloperScreen-pTaZZ6r5.js +0 -1
  100. package/dist/webdist/assets/EnginesScreen-BvZtUKd0.js +0 -7
  101. package/dist/webdist/assets/SettingsScreen-z-UuH8MI.js +0 -1
  102. package/dist/webdist/assets/TokensScreen-dLb-UDmL.js +0 -1
  103. package/dist/webdist/assets/WorkspaceScreen-C5R7gzUi.js +0 -1
  104. package/dist/webdist/assets/channel-CDs60xNM.js +0 -1
  105. package/dist/webdist/assets/check-DeJoIInG.js +0 -1
  106. package/dist/webdist/assets/classDiagram-4FO5ZUOK-BLIfDCvc.js +0 -1
  107. package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-BLIfDCvc.js +0 -1
  108. package/dist/webdist/assets/dialog-uY3tk3E9.js +0 -1
  109. package/dist/webdist/assets/stateDiagram-v2-BHNVJYJU-DBMc27o_.js +0 -1
  110. package/dist/webdist/assets/useIsDesktop-B-UPxY57.js +0 -1
package/README.md CHANGED
@@ -202,9 +202,12 @@ the detail:
202
202
  - **Real measured tokens/sec** in the model list — **live** while generating, **last-session**
203
203
  when idle (never a synthetic estimate).
204
204
  - **Full load-parameter UI**, a superset of what other tools expose: context length, GPU offload
205
- (`-ngl`), **MoE CPU-offload (`--n-cpu-moe`)**, parallel slots, **KV-cache quant type** (incl.
206
- low-bit on supporting forks), CPU threads, flash attention, and **speculative decoding (NextN /
207
- MTP / draft, with a configurable draft min/max window)**.
205
+ (`-ngl`), **MoE CPU-offload (`--n-cpu-moe`)**, parallel slots, **independent K and V cache-quant
206
+ type** (incl. low-bit on supporting forks), CPU threads, flash attention, **speculative decoding
207
+ (NextN / MTP / draft, with a configurable draft min/max window)**, a **pinned engine port** per
208
+ model, and a **custom/raw flags** field for anything not exposed as its own control.
209
+ - **Copy the exact launch command** for a loaded model — runs the same config standalone with
210
+ llama.cpp/the fork directly, no TurboLLM required.
208
211
  - **Auto-fit GPU layers / MoE offload** — an optional toggle (off by default) that hands the
209
212
  GPU/CPU split decision to llama.cpp's own memory-fitting logic at load time instead of a fixed
210
213
  number, honored by Auto-tune too. Useful when a large context or model doesn't fit your usual
@@ -266,10 +269,12 @@ the detail:
266
269
  in every chat with no restart. A **Research** persona forces multi-step web search and cites sources inline.
267
270
  - **Tool-call approval gate** — every tool call asks for your approval by default before it runs,
268
271
  with **Deny**, **Allow**, **Allow for this chat**, or **Always Allow** on an inline bar above the
269
- composer. Set per-tool defaults globally from Settings → Tools & safety.
272
+ composer. Set per-tool defaults globally from Settings → Tools & safety, or flip **Auto-allow
273
+ all** to skip prompts entirely — a tool set to Deny still stays blocked either way.
270
274
  - **Usage dashboard** — a GitHub-style activity heatmap of your local generation history (adaptive
271
275
  1h/12h/24h boxes for the 7-day/30-day/all-time views), streaks, peak hour, a per-model
272
- breakdown, and a lifetime token-milestone tracker.
276
+ breakdown, a lifetime token-milestone tracker, and a separate **API tab** tracking tokens hitting
277
+ the gateway from external tools like Claude Code — not just in-app chat.
273
278
  - **Auto-memory** *(experimental, off by default)* — silently extracts durable facts you mention
274
279
  in chat (name, preferences, hardware) using your own loaded model, and carries them into future
275
280
  new conversations. Nothing leaves your device; the full fact list is reviewable and deletable
package/dist/cli.js CHANGED
@@ -300,6 +300,7 @@ function normalize(c) {
300
300
  toolPolicies.run_code = "allow";
301
301
  }
302
302
  c.tools.toolPolicies = toolPolicies;
303
+ c.tools.autoAllowAll = tl.autoAllowAll === true;
303
304
  const mc = c.mcp ?? {};
304
305
  c.mcp = {
305
306
  servers: Array.isArray(mc.servers) ? mc.servers.filter((s) => typeof s === "object" && s !== null && typeof s.id === "string" && typeof s.name === "string" && (s.transport === "stdio" || s.transport === "sse")) : []
@@ -1381,6 +1382,7 @@ var Manager = class _Manager {
1381
1382
  child = null;
1382
1383
  startedAt = 0;
1383
1384
  errInfo = null;
1385
+ lastCommand = null;
1384
1386
  lastActivity = 0;
1385
1387
  logPathStr = "";
1386
1388
  exited = Promise.resolve();
@@ -1450,7 +1452,7 @@ var Manager = class _Manager {
1450
1452
  return;
1451
1453
  }
1452
1454
  }
1453
- const port2 = await allocPort();
1455
+ const port2 = await allocPort(opts.preferredPort);
1454
1456
  const logPath = join10(this.store.dir(), "logs", `engine-${opts.engine.id}.log`);
1455
1457
  mkdirSync6(dirname2(logPath), { recursive: true });
1456
1458
  const logStream = createWriteStream2(logPath);
@@ -1468,6 +1470,7 @@ var Manager = class _Manager {
1468
1470
  }
1469
1471
  }
1470
1472
  const { cmd, args } = engineCommand(opts, port2, slotSavePath);
1473
+ this.lastCommand = { cmd, args };
1471
1474
  const spawned = needsShellWrapper(opts.engine.kind) ? shellWrapped(cmd, args) : { cmd, args };
1472
1475
  const child = spawn(spawned.cmd, spawned.args, { cwd: dirname2(cmd), windowsHide: true, env: pyEngineEnv(opts.engine.kind, this.store.dir(), opts.engine.binPath) });
1473
1476
  child.stdout?.pipe(logStream, { end: false });
@@ -1541,6 +1544,27 @@ var Manager = class _Manager {
1541
1544
  if (!opts?.modelPath) throw new Error("no_such_model");
1542
1545
  await this.load(opts);
1543
1546
  }
1547
+ /** The exact command last spawned, formatted for copy-paste into a terminal
1548
+ * (GitHub Discord ask — "copy exact launch command"). Only meaningful while this
1549
+ * engine is the one actually running; null once stopped/swapped so a stale
1550
+ * command from a PREVIOUS model can never be copied as if it were current.
1551
+ *
1552
+ * Deliberately NOT byte-identical to what TurboLLM itself spawned: the point of
1553
+ * copying it is running the SAME model+config standalone, with llama.cpp/the fork
1554
+ * directly and TurboLLM out of the picture entirely — so `--no-webui` (added only
1555
+ * because TurboLLM serves its own frontend and doesn't want llama-server's built-in
1556
+ * one competing with it, `buildArgs` in this file) is stripped here: for a standalone
1557
+ * run it's the only UI available, so silently carrying it over would hand the user a
1558
+ * server with no way to talk to it. Everything else (model path, offload, KV type,
1559
+ * sampling, extra flags) is copied verbatim — it's what makes the command reproduce
1560
+ * the exact same config. */
1561
+ launchCommand() {
1562
+ if (this.state !== "running" && this.state !== "starting") return null;
1563
+ if (!this.lastCommand) return null;
1564
+ const args = this.lastCommand.args.filter((a) => a !== "--no-webui");
1565
+ const quote = (s) => /[\s"]/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s;
1566
+ return [this.lastCommand.cmd, ...args].map(quote).join(" ");
1567
+ }
1544
1568
  status() {
1545
1569
  const st = { state: this.state, err: this.errInfo, port: this.port, pid: this.pid, model: null, loadElapsedMs: 0 };
1546
1570
  if ((this.state === "running" || this.state === "starting") && this.opts) {
@@ -1774,7 +1798,15 @@ function buildArgs(opts, port2, slotSavePath) {
1774
1798
  args.push(...opts.extraArgs);
1775
1799
  return args;
1776
1800
  }
1777
- function allocPort() {
1801
+ function portFree(p) {
1802
+ return new Promise((resolve5) => {
1803
+ const srv = createServer();
1804
+ srv.once("error", () => resolve5(false));
1805
+ srv.listen(p, "127.0.0.1", () => srv.close(() => resolve5(true)));
1806
+ });
1807
+ }
1808
+ async function allocPort(preferredPort) {
1809
+ if (preferredPort && preferredPort > 0 && await portFree(preferredPort)) return preferredPort;
1778
1810
  return new Promise((resolve5, reject) => {
1779
1811
  const tryPort = (p) => {
1780
1812
  if (p > 8181) return reject(new Error("no_free_port"));
@@ -4274,6 +4306,24 @@ var EMBED_FILE_RE = /\b(bge[-_]|nomic[-_]embed|all[-_]minilm|e5[-_]|gte[-_]|stel
4274
4306
  function isEmbeddingModel(arch2, name) {
4275
4307
  return EMBED_ARCHS.has(arch2.toLowerCase()) || EMBED_FILE_RE.test(name);
4276
4308
  }
4309
+ function commonPrefixLen(a, b) {
4310
+ let i = 0;
4311
+ while (i < a.length && i < b.length && a[i] === b[i]) i++;
4312
+ return i;
4313
+ }
4314
+ var MIN_MMPROJ_CORRELATION_PREFIX = 6;
4315
+ function resolveMmproj(modelFile, candidates) {
4316
+ if (candidates.length <= 1) return candidates[0];
4317
+ const modelName = basename(modelFile.path).toLowerCase();
4318
+ const correlated = candidates.filter(
4319
+ (c) => commonPrefixLen(modelName, basename(c.path).toLowerCase()) >= MIN_MMPROJ_CORRELATION_PREFIX
4320
+ );
4321
+ if (correlated.length === 1) return correlated[0];
4322
+ const pool = correlated.length > 1 ? correlated : candidates;
4323
+ return pool.reduce(
4324
+ (best, c) => Math.abs(c.mtime - modelFile.mtime) < Math.abs(best.mtime - modelFile.mtime) ? c : best
4325
+ );
4326
+ }
4277
4327
  var ScannerError = class extends Error {
4278
4328
  constructor(code, message) {
4279
4329
  super(message);
@@ -4371,9 +4421,6 @@ var Scanner = class {
4371
4421
  for (const [dir, group] of byDir) {
4372
4422
  const mmprojFiles = group.filter((f) => basename(f.path).toLowerCase().includes("mmproj"));
4373
4423
  const modelFiles = group.filter((f) => !basename(f.path).toLowerCase().includes("mmproj"));
4374
- const mmproj = mmprojFiles.sort((a, b) => b.size - a.size)[0];
4375
- const mmprojPath = mmproj?.path ?? null;
4376
- const mmprojSizeBytes = mmproj?.size ?? 0;
4377
4424
  const splits = /* @__PURE__ */ new Map();
4378
4425
  const singles = [];
4379
4426
  for (const f of modelFiles) {
@@ -4390,7 +4437,8 @@ var Scanner = class {
4390
4437
  }
4391
4438
  }
4392
4439
  for (const f of singles) {
4393
- entries.push(await this.entryFor(f.path, f.size, f.mtime, dir, mmprojPath, mmprojSizeBytes, false));
4440
+ const mmproj = resolveMmproj(f, mmprojFiles);
4441
+ entries.push(await this.entryFor(f.path, f.size, f.mtime, dir, mmproj?.path ?? null, mmproj?.size ?? 0, false));
4394
4442
  await tick();
4395
4443
  }
4396
4444
  for (const { shards, total } of splits.values()) {
@@ -4405,7 +4453,8 @@ var Scanner = class {
4405
4453
  break;
4406
4454
  }
4407
4455
  }
4408
- entries.push(await this.entryFor(first.path, totalSize, first.mtime, dir, mmprojPath, mmprojSizeBytes, incomplete));
4456
+ const mmproj = resolveMmproj(first, mmprojFiles);
4457
+ entries.push(await this.entryFor(first.path, totalSize, first.mtime, dir, mmproj?.path ?? null, mmproj?.size ?? 0, incomplete));
4409
4458
  await tick();
4410
4459
  }
4411
4460
  }
@@ -4808,7 +4857,8 @@ function estimateVram(p, m, sys) {
4808
4857
  const weightsMb = sizeMb * Math.max(0, Math.min(1, gpuFrac));
4809
4858
  const kvElems = kvCacheElems(m, p.ctx);
4810
4859
  const ssmMb = ssmStateElems(m) * 4 / 1e6;
4811
- const kvMb = p.kvOffload === false ? 0 : (kvElems * kvBytesPerElem(p.kvTypeK) / 1e6 + ssmMb) * (p.kvUnified ? 1 : Math.max(1, p.parallel));
4860
+ const kvBytesMb = (kvElems / 2 * kvBytesPerElem(p.kvTypeK) + kvElems / 2 * kvBytesPerElem(p.kvTypeV)) / 1e6;
4861
+ const kvMb = p.kvOffload === false ? 0 : (kvBytesMb + ssmMb) * (p.kvUnified ? 1 : Math.max(1, p.parallel));
4812
4862
  const mmprojMb = p.useMmproj && p.mmprojGpu && m.mmprojPath ? m.mmprojSizeBytes / 1e6 * 1.15 : 0;
4813
4863
  const estMb = Math.round(weightsMb + kvMb + 800 + mmprojMb);
4814
4864
  const pct = estMb / totalVramMb;
@@ -5015,6 +5065,12 @@ var TOKEN_MILESTONES = [
5015
5065
  5e8,
5016
5066
  1e9
5017
5067
  ];
5068
+ function milestoneFor(lifetimeTotalTokens) {
5069
+ const achieved = [...TOKEN_MILESTONES].reverse().find((m) => m <= lifetimeTotalTokens) ?? null;
5070
+ const next = TOKEN_MILESTONES.find((m) => m > lifetimeTotalTokens) ?? null;
5071
+ const progressPct = next !== null ? Math.round((lifetimeTotalTokens - (achieved ?? 0)) / (next - (achieved ?? 0)) * 1e3) / 10 : null;
5072
+ return { achieved, next, progressPct };
5073
+ }
5018
5074
  function titleCaseModelName(modelKey) {
5019
5075
  const raw = modelKey.split("|")[0] ?? modelKey;
5020
5076
  return raw.replace(/\b\w/g, (c) => c.toUpperCase());
@@ -5359,6 +5415,20 @@ var ConversationStore = class {
5359
5415
  if (!this.hasColumn("agent_runs", "reverted_from_message_id")) this.db.exec(`ALTER TABLE agent_runs ADD COLUMN reverted_from_message_id TEXT;`);
5360
5416
  this.db.exec(`PRAGMA user_version = 33;`);
5361
5417
  }
5418
+ if (v < 34) {
5419
+ this.db.exec(`
5420
+ CREATE TABLE IF NOT EXISTS api_usage (
5421
+ id TEXT PRIMARY KEY,
5422
+ created_at TEXT NOT NULL,
5423
+ source TEXT NOT NULL,
5424
+ model_key TEXT,
5425
+ prompt_tokens INTEGER NOT NULL DEFAULT 0,
5426
+ gen_tokens INTEGER NOT NULL DEFAULT 0
5427
+ );
5428
+ CREATE INDEX IF NOT EXISTS idx_api_usage_created ON api_usage(created_at);
5429
+ PRAGMA user_version = 34;
5430
+ `);
5431
+ }
5362
5432
  }
5363
5433
  listConversations(q, kind = "all") {
5364
5434
  if (q) {
@@ -5532,22 +5602,25 @@ var ConversationStore = class {
5532
5602
  ORDER BY created_at ASC
5533
5603
  `).all();
5534
5604
  if (rows.length === 0) {
5605
+ const api2 = this.apiUsageStats(range);
5606
+ const milestone2 = milestoneFor(api2.lifetimeTotalTokens);
5535
5607
  return {
5536
5608
  range,
5537
5609
  sessions: 0,
5538
5610
  messages: 0,
5539
- totalTokens: 0,
5611
+ totalTokens: api2.totalTokens,
5540
5612
  activeDays: 0,
5541
5613
  currentStreak: 0,
5542
5614
  longestStreak: 0,
5543
5615
  peakHour: null,
5544
5616
  favoriteModel: null,
5545
5617
  firstMessageAt: null,
5546
- lifetimeTotalTokens: 0,
5547
- milestone: { achieved: null, next: null, progressPct: null },
5618
+ lifetimeTotalTokens: api2.lifetimeTotalTokens,
5619
+ milestone: milestone2,
5548
5620
  activity: { granularityHours: 24, buckets: [] },
5549
5621
  dailyByModel: [],
5550
- byModel: []
5622
+ byModel: [],
5623
+ api: api2
5551
5624
  };
5552
5625
  }
5553
5626
  const dayBuckets = /* @__PURE__ */ new Map();
@@ -5681,26 +5754,95 @@ var ConversationStore = class {
5681
5754
  }
5682
5755
  activity = { granularityHours, buckets };
5683
5756
  }
5684
- let lifetimeTotalTokens = 0;
5685
- for (const b of dayBuckets.values()) lifetimeTotalTokens += b.promptTokens + b.genTokens;
5686
- const achieved = [...TOKEN_MILESTONES].reverse().find((m) => m <= lifetimeTotalTokens) ?? null;
5687
- const next = TOKEN_MILESTONES.find((m) => m > lifetimeTotalTokens) ?? null;
5688
- const progressPct = next !== null ? Math.round((lifetimeTotalTokens - (achieved ?? 0)) / (next - (achieved ?? 0)) * 1e3) / 10 : null;
5757
+ let chatLifetimeTotalTokens = 0;
5758
+ for (const b of dayBuckets.values()) chatLifetimeTotalTokens += b.promptTokens + b.genTokens;
5759
+ const api = this.apiUsageStats(range);
5760
+ const combinedTotalTokens = totalTokens + api.totalTokens;
5761
+ const combinedLifetimeTotalTokens = chatLifetimeTotalTokens + api.lifetimeTotalTokens;
5762
+ const milestone = milestoneFor(combinedLifetimeTotalTokens);
5689
5763
  return {
5690
5764
  range,
5691
5765
  sessions: convIds.size,
5692
5766
  messages: scopedRows.length,
5693
- totalTokens,
5767
+ totalTokens: combinedTotalTokens,
5694
5768
  activeDays,
5695
5769
  currentStreak,
5696
5770
  longestStreak,
5697
5771
  peakHour,
5698
5772
  favoriteModel: byModel[0]?.displayName ?? null,
5699
5773
  firstMessageAt,
5700
- lifetimeTotalTokens,
5701
- milestone: { achieved, next, progressPct },
5774
+ lifetimeTotalTokens: combinedLifetimeTotalTokens,
5775
+ milestone,
5702
5776
  activity,
5703
5777
  dailyByModel,
5778
+ byModel,
5779
+ api
5780
+ };
5781
+ }
5782
+ /** Insert one completed gateway request (GitHub #71) — called from the two gateway entry
5783
+ * points (gateway.ts) right where in-app chat already records into `messages`. Fully
5784
+ * additive/fail-safe: callers wrap this the same way `recordCompletion` is wrapped. */
5785
+ recordApiUsage(rec) {
5786
+ this.db.prepare(`
5787
+ INSERT INTO api_usage (id, created_at, source, model_key, prompt_tokens, gen_tokens)
5788
+ VALUES ($id, $createdAt, $source, $modelKey, $promptTokens, $genTokens)
5789
+ `).run({
5790
+ $id: randomUUID3(),
5791
+ $createdAt: (/* @__PURE__ */ new Date()).toISOString(),
5792
+ $source: rec.source,
5793
+ $modelKey: rec.modelKey,
5794
+ $promptTokens: Math.max(0, Math.floor(rec.promptTokens) || 0),
5795
+ $genTokens: Math.max(0, Math.floor(rec.genTokens) || 0)
5796
+ });
5797
+ }
5798
+ /** Gateway (external-client) token usage — see `ApiUsageStats`'s doc comment for why this
5799
+ * is a separate bucket from `tokenUsageStats`. Same local-day range scoping as the chat
5800
+ * stats above, but no streak/session concepts (a gateway request isn't a chat turn). */
5801
+ apiUsageStats(range = "all") {
5802
+ const rows = this.db.prepare(`
5803
+ SELECT created_at, source, model_key, prompt_tokens, gen_tokens FROM api_usage
5804
+ ORDER BY created_at ASC
5805
+ `).all();
5806
+ let lifetimeTotalTokens = 0;
5807
+ for (const r of rows) lifetimeTotalTokens += r.prompt_tokens + r.gen_tokens;
5808
+ if (rows.length === 0) {
5809
+ return { range, requests: 0, totalTokens: 0, lifetimeTotalTokens: 0, bySource: [], byModel: [] };
5810
+ }
5811
+ const todayKey = dayKey((/* @__PURE__ */ new Date()).toISOString());
5812
+ const heatStart = range === "all" ? null : addDays(todayKey, -(RANGE_WINDOW_DAYS[range] - 1));
5813
+ const scoped = heatStart === null ? rows : rows.filter((r) => dayKey(r.created_at) >= heatStart);
5814
+ const sourceTally = /* @__PURE__ */ new Map();
5815
+ const modelTally = /* @__PURE__ */ new Map();
5816
+ let totalTokens = 0;
5817
+ for (const r of scoped) {
5818
+ const tokens = r.prompt_tokens + r.gen_tokens;
5819
+ totalTokens += tokens;
5820
+ const st = sourceTally.get(r.source) ?? { requests: 0, totalTokens: 0 };
5821
+ st.requests++;
5822
+ st.totalTokens += tokens;
5823
+ sourceTally.set(r.source, st);
5824
+ if (r.model_key) {
5825
+ const mt = modelTally.get(r.model_key) ?? { requests: 0, promptTokens: 0, genTokens: 0 };
5826
+ mt.requests++;
5827
+ mt.promptTokens += r.prompt_tokens;
5828
+ mt.genTokens += r.gen_tokens;
5829
+ modelTally.set(r.model_key, mt);
5830
+ }
5831
+ }
5832
+ const byModel = [...modelTally.entries()].map(([modelKey, m]) => ({
5833
+ modelKey,
5834
+ displayName: titleCaseModelName(modelKey),
5835
+ requests: m.requests,
5836
+ promptTokens: m.promptTokens,
5837
+ genTokens: m.genTokens,
5838
+ totalTokens: m.promptTokens + m.genTokens
5839
+ })).sort((a, b) => b.totalTokens - a.totalTokens);
5840
+ return {
5841
+ range,
5842
+ requests: scoped.length,
5843
+ totalTokens,
5844
+ lifetimeTotalTokens,
5845
+ bySource: [...sourceTally.entries()].map(([source, s]) => ({ source, ...s })),
5704
5846
  byModel
5705
5847
  };
5706
5848
  }
@@ -6686,6 +6828,20 @@ var DownloadManager = class {
6686
6828
  this.pump();
6687
6829
  return true;
6688
6830
  }
6831
+ /** Resume a paused or errored job (spec 10 §5): re-queue it so pump() picks it back
6832
+ * up. run() already resumes from the .part file's byte offset via a Range request —
6833
+ * this just flips the status so the queue actually starts it again. A restart-
6834
+ * restored job comes back as 'paused' with no automatic way back to 'downloading'
6835
+ * until this is called. */
6836
+ resume(id) {
6837
+ const rec = this.records.get(id);
6838
+ if (!rec || rec.status !== "paused" && rec.status !== "error") return false;
6839
+ rec.status = "queued";
6840
+ rec.error = null;
6841
+ this.persist();
6842
+ this.pump();
6843
+ return true;
6844
+ }
6689
6845
  /** Remove a record entirely (and any lingering .part). */
6690
6846
  remove(id) {
6691
6847
  const rec = this.records.get(id);
@@ -10772,10 +10928,11 @@ function buildSnapshot(conv, cfg2, version2, exportedAt, format) {
10772
10928
  }
10773
10929
 
10774
10930
  // src/tools/tool-policy.ts
10775
- function resolveToolPolicy(name, globalPolicies, convOverrides) {
10931
+ function resolveToolPolicy(name, globalPolicies, convOverrides, autoAllowAll = false) {
10776
10932
  if (convOverrides[name]) return convOverrides[name];
10777
- if (globalPolicies[name]) return globalPolicies[name];
10778
- return "ask";
10933
+ const global = globalPolicies[name];
10934
+ if (global) return global === "ask" && autoAllowAll ? "allow" : global;
10935
+ return autoAllowAll ? "allow" : "ask";
10779
10936
  }
10780
10937
 
10781
10938
  // src/tools/approval-gate.ts
@@ -10813,8 +10970,8 @@ function resolveToolApproval(key, decision) {
10813
10970
 
10814
10971
  // src/tools/execute-with-approval.ts
10815
10972
  async function executeToolCallWithApproval(params) {
10816
- const { tools, sink, convId, id, name, args, globalPolicies, convOverrides, signal, interactive, agentAllowedTools } = params;
10817
- const policy = resolveToolPolicy(name, globalPolicies, convOverrides);
10973
+ const { tools, sink, convId, id, name, args, globalPolicies, convOverrides, signal, interactive, agentAllowedTools, autoAllowAll } = params;
10974
+ const policy = resolveToolPolicy(name, globalPolicies, convOverrides, autoAllowAll);
10818
10975
  if (policy === "deny") {
10819
10976
  const result2 = "Blocked: this tool is set to Deny in Tool Permissions settings.";
10820
10977
  await sink({ event: "tool_call", data: { id, name, args, status: "done", result: result2 } });
@@ -11927,6 +12084,7 @@ After reviewing the search results, include a confidence assessment on a line by
11927
12084
  // start) — otherwise a same-turn repeat tool call misses an "Allow for this
11928
12085
  // chat" decision the user just made via POST .../tool-calls/:id/approve.
11929
12086
  convOverrides: d.db.getToolOverrides(convId),
12087
+ autoAllowAll: d.store.snapshot().tools.autoAllowAll ?? false,
11930
12088
  signal: ac.signal,
11931
12089
  interactive: true
11932
12090
  });
@@ -12689,6 +12847,60 @@ var ALL = [
12689
12847
  hasPrebuilt: false
12690
12848
  }
12691
12849
  ]
12850
+ },
12851
+ {
12852
+ id: "prism",
12853
+ name: "Prism (llama.cpp fork)",
12854
+ kind: "llama-server",
12855
+ description: "A llama.cpp fork tuned for 1-2 bit ternary/Bonsai models. Ships llama-server but has no install endpoint here yet \u2014 build it, then add your own engine.",
12856
+ provision: "github-release",
12857
+ homepage: "https://github.com/PrismML-Eng/llama.cpp",
12858
+ repo: "PrismML-Eng/llama.cpp",
12859
+ // The fork itself publishes extensive per-commit prebuilt archives (CUDA/Vulkan/ROCm ×
12860
+ // win/linux/macOS, same naming convention as official llama.cpp) — but TurboLLM doesn't
12861
+ // resolve them automatically (deliberately deferred, see ADR-238). Build-from-source only.
12862
+ platforms: ["win32", "darwin", "linux"],
12863
+ support: "experimental",
12864
+ installEndpoint: "",
12865
+ note: "Upstream publishes prebuilt binaries, but TurboLLM builds from source here (guided walkthrough) rather than resolving a specific release asset. Proven on a ternary/Bonsai-class model at 200K context (Q2_0 + q8 KV).",
12866
+ variants: [
12867
+ {
12868
+ id: "prism-source",
12869
+ label: "Build from source",
12870
+ repo: "PrismML-Eng/llama.cpp",
12871
+ requires: {},
12872
+ stability: "experimental",
12873
+ speed: "fast",
12874
+ hasPrebuilt: false
12875
+ }
12876
+ ]
12877
+ },
12878
+ {
12879
+ id: "beellama",
12880
+ name: "BeeLlama.cpp",
12881
+ kind: "llama-server",
12882
+ description: "A llama.cpp fork adding variance-normalized KV-cache quantization (KVarN), a KV precision tail, and adaptive DFlash speculative decoding. Ships llama-server but has no install endpoint here yet \u2014 build it, then add your own engine.",
12883
+ provision: "github-release",
12884
+ homepage: "https://github.com/Anbeeld/beellama.cpp",
12885
+ repo: "Anbeeld/beellama.cpp",
12886
+ // Also publishes a full prebuilt matrix (CUDA/ROCm/Vulkan/SYCL/CPU) in one normal
12887
+ // release — but solo-maintainer, fast-churn fork (v0.4.0 dropped its own TurboQuant/TCQ
12888
+ // support), so one-click install is deliberately deferred; build-from-source only.
12889
+ platforms: ["win32", "darwin", "linux"],
12890
+ support: "experimental",
12891
+ installEndpoint: "",
12892
+ note: "Upstream publishes prebuilt binaries, but TurboLLM builds from source here. The KVarN cache types (kvarn2...kvarn8, set via --cache-type-k/-v) aren't in the auto-tune sweep yet \u2014 use the custom/raw flags field to set them manually.",
12893
+ variants: [
12894
+ {
12895
+ id: "beellama-source",
12896
+ label: "Build from source",
12897
+ repo: "Anbeeld/beellama.cpp",
12898
+ requires: {},
12899
+ stability: "experimental",
12900
+ speed: "fast",
12901
+ hasPrebuilt: false
12902
+ }
12903
+ ]
12692
12904
  }
12693
12905
  ];
12694
12906
  function catalogForPlatform(platform = process.platform) {
@@ -12998,6 +13210,8 @@ function registerApi(app2, d) {
12998
13210
  pid: ms.pid
12999
13211
  };
13000
13212
  if (ms.err) engine.error = ms.err;
13213
+ const launchCommand = d.manager.launchCommand();
13214
+ if (launchCommand) engine.launchCommand = launchCommand;
13001
13215
  const model = ms.model ? { key: ms.model.key, name: ms.model.name, quant: ms.model.quant, ctx: ms.model.ctx, vision: ms.model.vision, loadElapsedMs: ms.loadElapsedMs } : null;
13002
13216
  const engineStats = ms.state === "running" ? d.manager.sessionStats() : null;
13003
13217
  const liveGeneration = ms.state === "running" ? d.manager.liveGeneration() : null;
@@ -13852,7 +14066,8 @@ function registerApi(app2, d) {
13852
14066
  model: { key: entry.key, name: entry.name, quant: entry.quant, ctx: entry.nativeCtx, vision: entry.vision },
13853
14067
  modelPath: entry.path,
13854
14068
  extraArgs,
13855
- tensorParallelSize: savedProfile?.gpu?.tensorParallelSize
14069
+ tensorParallelSize: savedProfile?.gpu?.tensorParallelSize,
14070
+ preferredPort: savedProfile?.port
13856
14071
  };
13857
14072
  } else {
13858
14073
  const saved = getModelProfile(cfg2, entry.key, active.id);
@@ -13862,7 +14077,8 @@ function registerApi(app2, d) {
13862
14077
  engine: active,
13863
14078
  model: { key: entry.key, name: entry.name, quant: entry.quant, ctx: profile.ctx, vision: entry.vision },
13864
14079
  modelPath: entry.path,
13865
- extraArgs
14080
+ extraArgs,
14081
+ preferredPort: profile.port
13866
14082
  };
13867
14083
  }
13868
14084
  void d.manager.load(opts2, { beforeStart: () => d.comfy?.freeComfyUIBeforeLoad() ?? Promise.resolve() }).catch((e) => console.warn(`engine load failed: ${e}`));
@@ -14204,6 +14420,7 @@ function registerApi(app2, d) {
14204
14420
  Object.assign(cfg2.gateway, gwUpdates);
14205
14421
  if (toolchainDirs !== void 0) cfg2.build.toolchainDirs = toolchainDirs;
14206
14422
  if (toolPolicies !== void 0) cfg2.tools.toolPolicies = toolPolicies;
14423
+ if (b.autoAllowAll !== void 0) cfg2.tools.autoAllowAll = !!b.autoAllowAll;
14207
14424
  if (b.autoLoadOnStart !== void 0) cfg2.autoLoadOnStart = !!b.autoLoadOnStart;
14208
14425
  if (vramHeadroomMb !== void 0) cfg2.vramHeadroomMb = vramHeadroomMb;
14209
14426
  if (telemetryLevel !== void 0) cfg2.telemetry.level = telemetryLevel;
@@ -14433,6 +14650,11 @@ function registerApi(app2, d) {
14433
14650
  if (!ok) return err2(c, 404, "no_such_download", "No download with that id.");
14434
14651
  return c.json({ ok: true });
14435
14652
  });
14653
+ app2.post("/api/v1/downloads/:id/resume", (c) => {
14654
+ const ok = d.downloads.resume(c.req.param("id"));
14655
+ if (!ok) return err2(c, 404, "no_such_download", "No paused or errored download with that id.");
14656
+ return c.json({ ok: true });
14657
+ });
14436
14658
  app2.delete("/api/v1/downloads/:id", (c) => {
14437
14659
  const ok = d.downloads.remove(c.req.param("id"));
14438
14660
  if (!ok) return err2(c, 404, "no_such_download", "No download with that id.");
@@ -14580,7 +14802,8 @@ function settingsPayload(d) {
14580
14802
  experimental: cfg2.daemon.experimental,
14581
14803
  // Tool-call approval gate: global per-tool policy ('ask' | 'allow' | 'deny').
14582
14804
  // Not secret — echoed back directly so Settings can render Tool Permissions.
14583
- toolPolicies: cfg2.tools.toolPolicies ?? {}
14805
+ toolPolicies: cfg2.tools.toolPolicies ?? {},
14806
+ autoAllowAll: cfg2.tools.autoAllowAll ?? false
14584
14807
  };
14585
14808
  }
14586
14809
  function telemetryPreview(level, version2) {
@@ -17607,6 +17830,7 @@ function registerGateway(app2, d) {
17607
17830
  (u) => {
17608
17831
  try {
17609
17832
  d.manager.recordCompletion({ inputTokens: u.inputTokens, outputTokens: u.outputTokens });
17833
+ d.db.recordApiUsage({ source: "anthropic", modelKey: req.model ?? null, promptTokens: u.inputTokens, genTokens: u.outputTokens });
17610
17834
  } catch {
17611
17835
  }
17612
17836
  },
@@ -17633,7 +17857,7 @@ function registerGateway(app2, d) {
17633
17857
  }
17634
17858
  try {
17635
17859
  const oaiRes = await res.json();
17636
- recordOpenAiUsage(d, oaiRes);
17860
+ recordOpenAiUsage(d, oaiRes, "anthropic", req.model ?? null);
17637
17861
  return c.json(mapFromOpenAI(oaiRes, modelName));
17638
17862
  } finally {
17639
17863
  d.manager.generationEnd();
@@ -17710,6 +17934,9 @@ function registerGateway(app2, d) {
17710
17934
  const alias = engineModelAlias(d.registry.active()?.kind ?? "");
17711
17935
  if (alias) parsedBody.model = alias;
17712
17936
  }
17937
+ if (parsedBody?.stream === true && parsedBody.stream_options === void 0) {
17938
+ parsedBody.stream_options = { include_usage: true };
17939
+ }
17713
17940
  headers.delete("content-length");
17714
17941
  init.body = parsedBody ? JSON.stringify(parsedBody) : "";
17715
17942
  } else {
@@ -17722,7 +17949,8 @@ function registerGateway(app2, d) {
17722
17949
  try {
17723
17950
  const [a, b] = res.body.tee();
17724
17951
  d.manager.generationStart();
17725
- void recordOpenAiStreamUsage(d, b).finally(() => d.manager.generationEnd());
17952
+ const drain = parsedBody?.stream === true ? recordOpenAiStreamUsage(d, b, "openai", requestedModel || null) : recordOpenAiJsonUsage(d, b, "openai", requestedModel || null);
17953
+ void drain.finally(() => d.manager.generationEnd());
17726
17954
  return new Response(a, { status: res.status, headers: res.headers });
17727
17955
  } catch {
17728
17956
  return new Response(res.body, { status: res.status, headers: res.headers });
@@ -17731,7 +17959,7 @@ function registerGateway(app2, d) {
17731
17959
  return new Response(res.body, { status: res.status, headers: res.headers });
17732
17960
  });
17733
17961
  }
17734
- function recordOpenAiUsage(d, oai) {
17962
+ function recordOpenAiUsage(d, oai, source, modelKey) {
17735
17963
  try {
17736
17964
  const usage = oai.usage;
17737
17965
  const timings = oai.timings;
@@ -17741,10 +17969,19 @@ function recordOpenAiUsage(d, oai) {
17741
17969
  promptTps: timings?.prompt_per_second,
17742
17970
  genTps: timings?.predicted_per_second
17743
17971
  });
17972
+ d.db.recordApiUsage({ source, modelKey, promptTokens: usage?.prompt_tokens ?? 0, genTokens: usage?.completion_tokens ?? 0 });
17973
+ } catch {
17974
+ }
17975
+ }
17976
+ async function recordOpenAiJsonUsage(d, body6, source, modelKey) {
17977
+ try {
17978
+ const text = await new Response(body6).text();
17979
+ const oai = JSON.parse(text);
17980
+ recordOpenAiUsage(d, oai, source, modelKey);
17744
17981
  } catch {
17745
17982
  }
17746
17983
  }
17747
- async function recordOpenAiStreamUsage(d, body6) {
17984
+ async function recordOpenAiStreamUsage(d, body6, source, modelKey) {
17748
17985
  try {
17749
17986
  const reader = body6.getReader();
17750
17987
  const dec = new TextDecoder();
@@ -17790,6 +18027,7 @@ async function recordOpenAiStreamUsage(d, body6) {
17790
18027
  }
17791
18028
  }
17792
18029
  d.manager.recordCompletion({ inputTokens: promptTokens, outputTokens: completionTokens, promptTps, genTps });
18030
+ d.db.recordApiUsage({ source, modelKey, promptTokens, genTokens: completionTokens });
17793
18031
  } catch {
17794
18032
  }
17795
18033
  }
@@ -17800,13 +18038,21 @@ var WEB_ROOT = join29(dirname11(fileURLToPath(import.meta.url)), "webdist");
17800
18038
  function createApp(d) {
17801
18039
  const app2 = new Hono();
17802
18040
  app2.use(
17803
- "*",
18041
+ "/v1/*",
17804
18042
  cors({
17805
18043
  origin: "*",
17806
18044
  allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
17807
18045
  allowHeaders: ["Content-Type", "Authorization", "x-api-key", "X-TurboLLM-Auth"]
17808
18046
  })
17809
18047
  );
18048
+ app2.use(
18049
+ "/api/*",
18050
+ cors({
18051
+ origin: "https://turbollm.dev",
18052
+ allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
18053
+ allowHeaders: ["Content-Type", "Authorization", "x-api-key", "X-TurboLLM-Auth"]
18054
+ })
18055
+ );
17810
18056
  app2.use("*", async (c, next) => {
17811
18057
  c.header("Server", `TurboLLM/${d.version}`);
17812
18058
  await next();
@@ -1 +1 @@
1
- import{j as e}from"./radix-C-sz7VcM.js";import{u as M,b as W,r as b}from"./vendor-BFom-Mq6.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-rezSzJo3.js";import{f as U}from"./chat-api-CtbwBsSY.js";import{f as V,a as X}from"./agent-api-C48x10WZ.js";import{P as z}from"./personas-DGNsw4JH.js";import{C as Z}from"./chevron-left-DScqk7lr.js";import{R as ee}from"./rotate-ccw-L6hm18l-.js";import{T as te}from"./trash-2-CI-4vgjV.js";import{C as se}from"./chevron-right-DNc_yz-y.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-C-sz7VcM.js";import{u as M,b as W,r as b}from"./vendor-BFom-Mq6.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-D4oBHgyO.js";import{f as U}from"./chat-api-uAJNrEFg.js";import{f as V,a as X}from"./agent-api-BZQuCIAd.js";import{P as z}from"./personas-fIGMukzj.js";import{C as Z}from"./chevron-left-Dv-zay0l.js";import{R as ee}from"./rotate-ccw-D99lmEA-.js";import{T as te}from"./trash-2-CuUIi8kE.js";import{C as se}from"./chevron-right-DvvJS6ye.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};