turbollm 1.7.5 → 1.7.6

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 (98) hide show
  1. package/README.md +7 -0
  2. package/dist/cli.js +362 -2
  3. package/dist/webdist/assets/{AgentEditPage-CfiJQeRy.js → AgentEditPage-Biq01vi2.js} +1 -1
  4. package/dist/webdist/assets/ChatScreen-Dmt_qvDr.js +65 -0
  5. package/dist/webdist/assets/{CustomizeScreen-BQMntB-C.js → CustomizeScreen-Dy4Cn3bT.js} +1 -1
  6. package/dist/webdist/assets/DeveloperScreen-DFs3rx7C.js +1 -0
  7. package/dist/webdist/assets/{EnginesScreen-BGc09u0i.js → EnginesScreen-RkTfEghZ.js} +6 -6
  8. package/dist/webdist/assets/FsBrowser-Dz4gWOWq.js +1 -0
  9. package/dist/webdist/assets/{MessageBubble-CxHvNrgj.js → MessageBubble-BxDHn4od.js} +16 -16
  10. package/dist/webdist/assets/{ModelsScreen-RRK-uCW8.js → ModelsScreen-DuUqlRGS.js} +2 -2
  11. package/dist/webdist/assets/SettingsScreen-BfP75nuS.js +1 -0
  12. package/dist/webdist/assets/{SkillEditPage-CHh9An9Z.js → SkillEditPage-D6OKcHNG.js} +1 -1
  13. package/dist/webdist/assets/TokensScreen-DblmY7_u.js +1 -0
  14. package/dist/webdist/assets/WorkspaceScreen-DLfMYwYA.js +1 -0
  15. package/dist/webdist/assets/{agent-api-B0qitSjr.js → agent-api-DZqYdw7O.js} +1 -1
  16. package/dist/webdist/assets/{arc-Cdcmyops.js → arc-DV0DkONk.js} +1 -1
  17. package/dist/webdist/assets/{architectureDiagram-3BPJPVTR-BQlFr2uc.js → architectureDiagram-3BPJPVTR-CCklxbKw.js} +1 -1
  18. package/dist/webdist/assets/{arrow-left-BgPNNAIi.js → arrow-left-CGv_OXEs.js} +1 -1
  19. package/dist/webdist/assets/badge-K_-MWGgP.js +1 -0
  20. package/dist/webdist/assets/{blockDiagram-GPEHLZMM-CMRPD5BN.js → blockDiagram-GPEHLZMM-DxjGvSV7.js} +1 -1
  21. package/dist/webdist/assets/{bot-CVbPHmTQ.js → bot-CD_BRtw9.js} +1 -1
  22. package/dist/webdist/assets/{c4Diagram-AAUBKEIU-BYXwwlnp.js → c4Diagram-AAUBKEIU-C9AuWiX4.js} +1 -1
  23. package/dist/webdist/assets/channel-D8wWQ4tZ.js +1 -0
  24. package/dist/webdist/assets/{chat-api-x93Fzf3q.js → chat-api-B6AobDYd.js} +3 -3
  25. package/dist/webdist/assets/check-Dwz6EU4N.js +1 -0
  26. package/dist/webdist/assets/{chevron-left-D03iirWA.js → chevron-left-B-1Bh1dK.js} +1 -1
  27. package/dist/webdist/assets/{chevron-right-B7Mj5eje.js → chevron-right-By1T2-Hr.js} +1 -1
  28. package/dist/webdist/assets/{chunk-2J33WTMH-BTHWkgj0.js → chunk-2J33WTMH-jxkAgaR9.js} +1 -1
  29. package/dist/webdist/assets/{chunk-4BX2VUAB-C0OdA31w.js → chunk-4BX2VUAB-BNFzh5g5.js} +1 -1
  30. package/dist/webdist/assets/{chunk-55IACEB6-V6hn9Bpx.js → chunk-55IACEB6-DX5wR3Qq.js} +1 -1
  31. package/dist/webdist/assets/{chunk-727SXJPM-DCntVbqt.js → chunk-727SXJPM-CbrKMz1R.js} +1 -1
  32. package/dist/webdist/assets/{chunk-AQP2D5EJ-D4PhS_5c.js → chunk-AQP2D5EJ-CNb3wWkL.js} +1 -1
  33. package/dist/webdist/assets/{chunk-FMBD7UC4-qDBSNTHO.js → chunk-FMBD7UC4-DEhDwjoQ.js} +1 -1
  34. package/dist/webdist/assets/{chunk-ND2GUHAM-D7KguwAx.js → chunk-ND2GUHAM-BHmlzliw.js} +1 -1
  35. package/dist/webdist/assets/{chunk-QZHKN3VN-3zsu8TEd.js → chunk-QZHKN3VN-uI16s_C7.js} +1 -1
  36. package/dist/webdist/assets/{circle-x-Bm98m9k6.js → circle-x-yVtiCsS-.js} +1 -1
  37. package/dist/webdist/assets/classDiagram-4FO5ZUOK-BCecvXTJ.js +1 -0
  38. package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-BCecvXTJ.js +1 -0
  39. package/dist/webdist/assets/{collapsible-CZm9yoBQ.js → collapsible-D0c6B4Ip.js} +1 -1
  40. package/dist/webdist/assets/{common-DhVKZnxd.js → common-D_CzNUTa.js} +1 -1
  41. package/dist/webdist/assets/{cose-bilkent-S5V4N54A-CdJ22dvm.js → cose-bilkent-S5V4N54A-B1nfQjrr.js} +1 -1
  42. package/dist/webdist/assets/{dagre-BM42HDAG-C0tsLnJj.js → dagre-BM42HDAG-B6nBvZED.js} +2 -2
  43. package/dist/webdist/assets/{diagram-2AECGRRQ-tReRMIGZ.js → diagram-2AECGRRQ-4HWbpgy8.js} +1 -1
  44. package/dist/webdist/assets/{diagram-5GNKFQAL-BPDwtKWf.js → diagram-5GNKFQAL-DYfcLJfD.js} +1 -1
  45. package/dist/webdist/assets/{diagram-KO2AKTUF-dxXvPo_e.js → diagram-KO2AKTUF-BIqc86GH.js} +1 -1
  46. package/dist/webdist/assets/{diagram-LMA3HP47-BblbbKep.js → diagram-LMA3HP47-CfsmrpTV.js} +1 -1
  47. package/dist/webdist/assets/{diagram-OG6HWLK6-Bm5kmmiK.js → diagram-OG6HWLK6-DdZxRjl2.js} +1 -1
  48. package/dist/webdist/assets/{dialog-BS2ga4Li.js → dialog-C5rebm8b.js} +1 -1
  49. package/dist/webdist/assets/{erDiagram-TEJ5UH35-DtBl4psP.js → erDiagram-TEJ5UH35-BINxvowu.js} +1 -1
  50. package/dist/webdist/assets/{flowDiagram-I6XJVG4X-inuE1WTM.js → flowDiagram-I6XJVG4X-BphwX6XS.js} +1 -1
  51. package/dist/webdist/assets/{folder-input-DPrdjjXF.js → folder-input-DsjMa7S5.js} +1 -1
  52. package/dist/webdist/assets/{ganttDiagram-6RSMTGT7-C1x80EJK.js → ganttDiagram-6RSMTGT7-C63SidXF.js} +1 -1
  53. package/dist/webdist/assets/{gitGraphDiagram-PVQCEYII-BHFBSO22.js → gitGraphDiagram-PVQCEYII-D9FpCno0.js} +1 -1
  54. package/dist/webdist/assets/index-BAK-ZQCj.css +1 -0
  55. package/dist/webdist/assets/{index-DT-1Skfu.js → index-Cl88q0Ki.js} +7 -7
  56. package/dist/webdist/assets/{index-BPqqiChM.js → index-g6XC4RLK.js} +1 -1
  57. package/dist/webdist/assets/{infoDiagram-5YYISTIA-DErEOAbT.js → infoDiagram-5YYISTIA-BTrZQ4Wc.js} +1 -1
  58. package/dist/webdist/assets/{ishikawaDiagram-YF4QCWOH-CFYAcSCu.js → ishikawaDiagram-YF4QCWOH-CtpkiMI3.js} +1 -1
  59. package/dist/webdist/assets/{journeyDiagram-JHISSGLW-BRF8x-vW.js → journeyDiagram-JHISSGLW-Cs5Ssisp.js} +1 -1
  60. package/dist/webdist/assets/{kanban-definition-UN3LZRKU-Bpxu1hZh.js → kanban-definition-UN3LZRKU-Bd2W5L9B.js} +7 -7
  61. package/dist/webdist/assets/{linear-SOR4O2dK.js → linear-GlXwNR5R.js} +1 -1
  62. package/dist/webdist/assets/{mermaid.core-Gi-pYgdj.js → mermaid.core-BmvPXnHB.js} +5 -5
  63. package/dist/webdist/assets/{mindmap-definition-RKZ34NQL-DtatIfBM.js → mindmap-definition-RKZ34NQL-CuO5FwWr.js} +1 -1
  64. package/dist/webdist/assets/{pencil-Bi7z3_2g.js → pencil-C4EXnT_a.js} +1 -1
  65. package/dist/webdist/assets/{personas-CsvqnRpC.js → personas-C_VMMyvv.js} +15 -8
  66. package/dist/webdist/assets/{pieDiagram-4H26LBE5-DQYaIU8r.js → pieDiagram-4H26LBE5-CLty44d5.js} +1 -1
  67. package/dist/webdist/assets/{quadrantDiagram-W4KKPZXB-CUu3EWq8.js → quadrantDiagram-W4KKPZXB-BK9u_S8e.js} +1 -1
  68. package/dist/webdist/assets/{refresh-cw-CkhIL2Hz.js → refresh-cw-ChBdLwHh.js} +1 -1
  69. package/dist/webdist/assets/{requirementDiagram-4Y6WPE33-D54OOdDO.js → requirementDiagram-4Y6WPE33-DaldyJn3.js} +1 -1
  70. package/dist/webdist/assets/{rotate-ccw-BOQNX9cU.js → rotate-ccw-xceH34T7.js} +1 -1
  71. package/dist/webdist/assets/{sankeyDiagram-5OEKKPKP-yykV1xV0.js → sankeyDiagram-5OEKKPKP-ECm1YkbT.js} +1 -1
  72. package/dist/webdist/assets/{sequenceDiagram-3UESZ5HK-CD2n3CVh.js → sequenceDiagram-3UESZ5HK-D_WQr7Ox.js} +1 -1
  73. package/dist/webdist/assets/skeleton-DK4wQNYn.js +1 -0
  74. package/dist/webdist/assets/{star-geKnFl6r.js → star-uW87WNRR.js} +1 -1
  75. package/dist/webdist/assets/{stateDiagram-AJRCARHV-C7hTc-QA.js → stateDiagram-AJRCARHV-Dc4nVZrV.js} +1 -1
  76. package/dist/webdist/assets/stateDiagram-v2-BHNVJYJU-b4uEiOn_.js +1 -0
  77. package/dist/webdist/assets/{switch-BIJmJ4CL.js → switch-4XvTVvk1.js} +1 -1
  78. package/dist/webdist/assets/{timeline-definition-PNZ67QCA-6kJpBXAe.js → timeline-definition-PNZ67QCA-BLAoVxaR.js} +1 -1
  79. package/dist/webdist/assets/{trash-2-CT_92vaT.js → trash-2-BazpB6Tv.js} +1 -1
  80. package/dist/webdist/assets/{useIsDesktop-Dxs_CPEE.js → useIsDesktop-BRKlTjOF.js} +1 -1
  81. package/dist/webdist/assets/{vennDiagram-CIIHVFJN-B7h2KOpV.js → vennDiagram-CIIHVFJN-Dxiec1ZZ.js} +1 -1
  82. package/dist/webdist/assets/{wardley-L42UT6IY-CvmW62QW.js → wardley-L42UT6IY-B6eLCEJg.js} +1 -1
  83. package/dist/webdist/assets/{wardleyDiagram-YWT4CUSO-BNvpRPx5.js → wardleyDiagram-YWT4CUSO-CZda0vR7.js} +1 -1
  84. package/dist/webdist/assets/{wrench-DNrLSGCv.js → wrench-DffxK2pt.js} +1 -1
  85. package/dist/webdist/assets/{xychartDiagram-2RQKCTM6-BQI4t1tF.js → xychartDiagram-2RQKCTM6-B8xutv1Z.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/FsBrowser-CxCRvnU8.js +0 -1
  91. package/dist/webdist/assets/SettingsScreen-CDUlGnqu.js +0 -1
  92. package/dist/webdist/assets/WorkspaceScreen-BbjiYb6f.js +0 -1
  93. package/dist/webdist/assets/channel-B3OIXexj.js +0 -1
  94. package/dist/webdist/assets/check-Cx4ep7jU.js +0 -1
  95. package/dist/webdist/assets/classDiagram-4FO5ZUOK-CIneh01q.js +0 -1
  96. package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-CIneh01q.js +0 -1
  97. package/dist/webdist/assets/index-CuJf6kYG.css +0 -1
  98. package/dist/webdist/assets/stateDiagram-v2-BHNVJYJU-BEt8Mhcv.js +0 -1
package/README.md CHANGED
@@ -225,6 +225,13 @@ the detail:
225
225
  - **Tool-call approval gate** — every tool call asks for your approval by default before it runs,
226
226
  with **Deny**, **Allow**, **Allow for this chat**, or **Always Allow** on an inline bar above the
227
227
  composer. Set per-tool defaults globally from Settings → Tools & safety.
228
+ - **Usage dashboard** — a GitHub-style activity heatmap of your local generation history (adaptive
229
+ 1h/12h/24h boxes for the 7-day/30-day/all-time views), streaks, peak hour, a per-model
230
+ breakdown, and a lifetime token-milestone tracker.
231
+ - **Auto-memory** *(experimental, off by default)* — silently extracts durable facts you mention
232
+ in chat (name, preferences, hardware) using your own loaded model, and carries them into future
233
+ new conversations. Nothing leaves your device; the full fact list is reviewable and deletable
234
+ from Settings → Memory, and turning the toggle off stops new chats from seeing it immediately.
228
235
 
229
236
  </details>
230
237
 
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: [],
@@ -4106,6 +4107,24 @@ function vllmProfileToArgs(p) {
4106
4107
  import { DatabaseSync } from "node:sqlite";
4107
4108
  import { join as join14 } from "path";
4108
4109
  import { randomUUID as randomUUID3 } from "crypto";
4110
+ var RANGE_WINDOW_DAYS = { "30d": 30, "7d": 7 };
4111
+ var TOKEN_MILESTONES = [
4112
+ 1e3,
4113
+ 1e4,
4114
+ 1e5,
4115
+ 5e5,
4116
+ 1e6,
4117
+ 5e6,
4118
+ 1e7,
4119
+ 5e7,
4120
+ 1e8,
4121
+ 5e8,
4122
+ 1e9
4123
+ ];
4124
+ function titleCaseModelName(modelKey) {
4125
+ const raw = modelKey.split("|")[0] ?? modelKey;
4126
+ return raw.replace(/\b\w/g, (c) => c.toUpperCase());
4127
+ }
4109
4128
  function safeJson(s) {
4110
4129
  try {
4111
4130
  return JSON.parse(s);
@@ -4370,6 +4389,18 @@ var ConversationStore = class {
4370
4389
  if (!this.hasColumn("messages", "edited")) this.db.exec(`ALTER TABLE messages ADD COLUMN edited INTEGER NOT NULL DEFAULT 0;`);
4371
4390
  this.db.exec(`PRAGMA user_version = 26;`);
4372
4391
  }
4392
+ if (v < 27) {
4393
+ this.db.exec(`
4394
+ CREATE TABLE IF NOT EXISTS memory_facts (
4395
+ id TEXT PRIMARY KEY,
4396
+ fact_text TEXT NOT NULL,
4397
+ source_conv_id TEXT,
4398
+ created_at TEXT NOT NULL
4399
+ );
4400
+ CREATE INDEX IF NOT EXISTS idx_memory_facts_created ON memory_facts(created_at);
4401
+ PRAGMA user_version = 27;
4402
+ `);
4403
+ }
4373
4404
  }
4374
4405
  listConversations(q, kind = "all") {
4375
4406
  if (q) {
@@ -4526,6 +4557,200 @@ var ConversationStore = class {
4526
4557
  }
4527
4558
  return out;
4528
4559
  }
4560
+ /** Token usage dashboard (Release 3): sessions/messages/tokens/active-days scoped to
4561
+ * `range`, lifetime streaks (a streak isn't a "last N days" concept), the local hour
4562
+ * with the most activity, a per-model breakdown, and the heatmap day buckets — all from
4563
+ * one pass over `messages`. Days are bucketed by LOCAL calendar date (this daemon and its
4564
+ * user are always the same machine), not UTC, so a late-night session lands on the day
4565
+ * the user actually thinks of as "today". */
4566
+ tokenUsageStats(range = "all") {
4567
+ const rows = this.db.prepare(`
4568
+ SELECT conv_id, created_at, stats, model_key FROM messages
4569
+ WHERE role = 'assistant'
4570
+ ORDER BY created_at ASC
4571
+ `).all();
4572
+ if (rows.length === 0) {
4573
+ return {
4574
+ range,
4575
+ sessions: 0,
4576
+ messages: 0,
4577
+ totalTokens: 0,
4578
+ activeDays: 0,
4579
+ currentStreak: 0,
4580
+ longestStreak: 0,
4581
+ peakHour: null,
4582
+ favoriteModel: null,
4583
+ firstMessageAt: null,
4584
+ lifetimeTotalTokens: 0,
4585
+ milestone: { achieved: null, next: null, progressPct: null },
4586
+ activity: { granularityHours: 24, buckets: [] },
4587
+ dailyByModel: [],
4588
+ byModel: []
4589
+ };
4590
+ }
4591
+ const dayKey = (iso) => {
4592
+ const d = new Date(iso);
4593
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
4594
+ };
4595
+ const addDays = (key, n) => {
4596
+ const [y, m, d] = key.split("-").map(Number);
4597
+ const dt = new Date(y, m - 1, d + n);
4598
+ return `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, "0")}-${String(dt.getDate()).padStart(2, "0")}`;
4599
+ };
4600
+ const dayBuckets = /* @__PURE__ */ new Map();
4601
+ const firstMessageAt = rows[0].created_at;
4602
+ for (const r of rows) {
4603
+ const key = dayKey(r.created_at);
4604
+ const b = dayBuckets.get(key) ?? { promptTokens: 0, genTokens: 0, messageCount: 0 };
4605
+ const s = safeJson(r.stats);
4606
+ b.promptTokens += typeof s.promptTokens === "number" ? s.promptTokens : 0;
4607
+ b.genTokens += typeof s.genTokens === "number" ? s.genTokens : 0;
4608
+ b.messageCount++;
4609
+ dayBuckets.set(key, b);
4610
+ }
4611
+ const todayKey = dayKey((/* @__PURE__ */ new Date()).toISOString());
4612
+ const firstKey = dayKey(firstMessageAt);
4613
+ let longestStreak = 0, run = 0;
4614
+ for (let key = firstKey; key <= todayKey; key = addDays(key, 1)) {
4615
+ if (dayBuckets.has(key)) {
4616
+ run++;
4617
+ longestStreak = Math.max(longestStreak, run);
4618
+ } else {
4619
+ run = 0;
4620
+ }
4621
+ }
4622
+ let currentStreak = 0;
4623
+ {
4624
+ let key = dayBuckets.has(todayKey) ? todayKey : addDays(todayKey, -1);
4625
+ while (key >= firstKey && dayBuckets.has(key)) {
4626
+ currentStreak++;
4627
+ key = addDays(key, -1);
4628
+ }
4629
+ }
4630
+ const minAllStart = addDays(todayKey, -179);
4631
+ const heatStart = range === "all" ? firstKey < minAllStart ? firstKey : minAllStart : addDays(todayKey, -(RANGE_WINDOW_DAYS[range] - 1));
4632
+ const scopedRows = range === "all" ? rows : rows.filter((r) => dayKey(r.created_at) >= heatStart);
4633
+ const convIds = /* @__PURE__ */ new Set();
4634
+ const hourTally = /* @__PURE__ */ new Map();
4635
+ const modelTally = /* @__PURE__ */ new Map();
4636
+ const dayModelTally = /* @__PURE__ */ new Map();
4637
+ let totalTokens = 0;
4638
+ for (const r of scopedRows) {
4639
+ convIds.add(r.conv_id);
4640
+ const s = safeJson(r.stats);
4641
+ const pt = typeof s.promptTokens === "number" ? s.promptTokens : 0;
4642
+ const gt = typeof s.genTokens === "number" ? s.genTokens : 0;
4643
+ totalTokens += pt + gt;
4644
+ const hour = new Date(r.created_at).getHours();
4645
+ hourTally.set(hour, (hourTally.get(hour) ?? 0) + 1);
4646
+ if (r.model_key) {
4647
+ const m = modelTally.get(r.model_key) ?? { messageCount: 0, promptTokens: 0, genTokens: 0 };
4648
+ m.messageCount++;
4649
+ m.promptTokens += pt;
4650
+ m.genTokens += gt;
4651
+ modelTally.set(r.model_key, m);
4652
+ const day = dayKey(r.created_at);
4653
+ const dm = dayModelTally.get(day) ?? /* @__PURE__ */ new Map();
4654
+ dm.set(r.model_key, (dm.get(r.model_key) ?? 0) + pt + gt);
4655
+ dayModelTally.set(day, dm);
4656
+ }
4657
+ }
4658
+ let peakHour = null, peakHourCount = -1;
4659
+ for (const [hour, count] of hourTally) if (count > peakHourCount) {
4660
+ peakHour = hour;
4661
+ peakHourCount = count;
4662
+ }
4663
+ const byModel = [...modelTally.entries()].map(([modelKey, m]) => ({
4664
+ modelKey,
4665
+ displayName: titleCaseModelName(modelKey),
4666
+ messageCount: m.messageCount,
4667
+ promptTokens: m.promptTokens,
4668
+ genTokens: m.genTokens,
4669
+ totalTokens: m.promptTokens + m.genTokens
4670
+ })).sort((a, b) => b.totalTokens - a.totalTokens);
4671
+ const days = [];
4672
+ const dailyByModel = [];
4673
+ let activeDays = 0;
4674
+ for (let key = heatStart; key <= todayKey; key = addDays(key, 1)) {
4675
+ const b = dayBuckets.get(key);
4676
+ if (b && b.messageCount > 0) activeDays++;
4677
+ const dayTotalTokens = (b?.promptTokens ?? 0) + (b?.genTokens ?? 0);
4678
+ days.push({
4679
+ date: key,
4680
+ promptTokens: b?.promptTokens ?? 0,
4681
+ genTokens: b?.genTokens ?? 0,
4682
+ totalTokens: dayTotalTokens,
4683
+ messageCount: b?.messageCount ?? 0
4684
+ });
4685
+ const dm = dayModelTally.get(key);
4686
+ dailyByModel.push({
4687
+ date: key,
4688
+ totalTokens: dayTotalTokens,
4689
+ byModel: dm ? [...dm.entries()].map(([modelKey, tokens]) => ({ modelKey, tokens })) : []
4690
+ });
4691
+ }
4692
+ let activity;
4693
+ if (range === "all") {
4694
+ activity = {
4695
+ granularityHours: 24,
4696
+ buckets: days.map((d) => ({ start: d.date, totalTokens: d.totalTokens, messageCount: d.messageCount }))
4697
+ };
4698
+ } else {
4699
+ const granularityHours = range === "7d" ? 1 : 12;
4700
+ const subTally = /* @__PURE__ */ new Map();
4701
+ for (const r of scopedRows) {
4702
+ const d = new Date(r.created_at);
4703
+ const day = dayKey(r.created_at);
4704
+ const bucketKey = granularityHours === 1 ? `${day}T${String(d.getHours()).padStart(2, "0")}` : `${day}-${d.getHours() < 12 ? "AM" : "PM"}`;
4705
+ const s = safeJson(r.stats);
4706
+ const pt = typeof s.promptTokens === "number" ? s.promptTokens : 0;
4707
+ const gt = typeof s.genTokens === "number" ? s.genTokens : 0;
4708
+ const b = subTally.get(bucketKey) ?? { totalTokens: 0, messageCount: 0 };
4709
+ b.totalTokens += pt + gt;
4710
+ b.messageCount++;
4711
+ subTally.set(bucketKey, b);
4712
+ }
4713
+ const buckets = [];
4714
+ for (let key = heatStart; key <= todayKey; key = addDays(key, 1)) {
4715
+ if (granularityHours === 1) {
4716
+ for (let h = 0; h < 24; h++) {
4717
+ const bucketKey = `${key}T${String(h).padStart(2, "0")}`;
4718
+ const b = subTally.get(bucketKey);
4719
+ buckets.push({ start: bucketKey, totalTokens: b?.totalTokens ?? 0, messageCount: b?.messageCount ?? 0 });
4720
+ }
4721
+ } else {
4722
+ for (const half of ["AM", "PM"]) {
4723
+ const bucketKey = `${key}-${half}`;
4724
+ const b = subTally.get(bucketKey);
4725
+ buckets.push({ start: bucketKey, totalTokens: b?.totalTokens ?? 0, messageCount: b?.messageCount ?? 0 });
4726
+ }
4727
+ }
4728
+ }
4729
+ activity = { granularityHours, buckets };
4730
+ }
4731
+ let lifetimeTotalTokens = 0;
4732
+ for (const b of dayBuckets.values()) lifetimeTotalTokens += b.promptTokens + b.genTokens;
4733
+ const achieved = [...TOKEN_MILESTONES].reverse().find((m) => m <= lifetimeTotalTokens) ?? null;
4734
+ const next = TOKEN_MILESTONES.find((m) => m > lifetimeTotalTokens) ?? null;
4735
+ const progressPct = next !== null ? Math.round((lifetimeTotalTokens - (achieved ?? 0)) / (next - (achieved ?? 0)) * 1e3) / 10 : null;
4736
+ return {
4737
+ range,
4738
+ sessions: convIds.size,
4739
+ messages: scopedRows.length,
4740
+ totalTokens,
4741
+ activeDays,
4742
+ currentStreak,
4743
+ longestStreak,
4744
+ peakHour,
4745
+ favoriteModel: byModel[0]?.displayName ?? null,
4746
+ firstMessageAt,
4747
+ lifetimeTotalTokens,
4748
+ milestone: { achieved, next, progressPct },
4749
+ activity,
4750
+ dailyByModel,
4751
+ byModel
4752
+ };
4753
+ }
4529
4754
  /** Active branch only — matches getMessages(). Used to find "the last thing the user
4530
4755
  * currently sees" (e.g. to decide whether a fresh assistant placeholder follows it). */
4531
4756
  getLastMessage(convId) {
@@ -4749,6 +4974,21 @@ var ConversationStore = class {
4749
4974
  pruneAgentLessons(agentId) {
4750
4975
  this.db.prepare(`DELETE FROM agent_lessons WHERE agent_id = $a`).run({ $a: agentId });
4751
4976
  }
4977
+ // ── Auto-memory (Release 3) ───────────────────────────────────────────────────
4978
+ // Insert/list/delete only — no update. Facts are reviewed and removed, not hand-edited.
4979
+ addMemoryFact(row) {
4980
+ const id = randomUUID3();
4981
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4982
+ 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 });
4983
+ return { id, factText: row.factText, sourceConvId: row.sourceConvId, createdAt: now };
4984
+ }
4985
+ listMemoryFacts() {
4986
+ const rows = this.db.prepare(`SELECT * FROM memory_facts ORDER BY created_at DESC`).all();
4987
+ return rows.map((r) => ({ id: r.id, factText: r.fact_text, sourceConvId: r.source_conv_id ?? void 0, createdAt: r.created_at }));
4988
+ }
4989
+ deleteMemoryFact(id) {
4990
+ return this.db.prepare(`DELETE FROM memory_facts WHERE id = $id`).run({ $id: id }).changes > 0;
4991
+ }
4752
4992
  // ── Skills grown from experience (redesign §3.3, Voyager) ─────────────────────
4753
4993
  addAgentSkill(row) {
4754
4994
  const id = randomUUID3();
@@ -8987,6 +9227,108 @@ function saveSkillFromConversation(d, convId) {
8987
9227
  return taskId;
8988
9228
  }
8989
9229
 
9230
+ // src/chat/memory.ts
9231
+ var MIN_LENGTH = 12;
9232
+ var MAX_FACTS_PER_TURN = 5;
9233
+ var MAX_FACT_LENGTH = 200;
9234
+ function extractionPrompt(userText) {
9235
+ 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).
9236
+
9237
+ Rules:
9238
+ - Only use what the user explicitly said about themselves in THIS message. Never infer, guess, or invent.
9239
+ - 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).
9240
+ - Each fact must be a short, self-contained sentence (max ~15 words) that makes sense with no other context.
9241
+ - If there are no durable personal facts, reply with exactly: NONE
9242
+ - Otherwise reply with ONLY a list, one fact per line, each line starting with "- ". No preamble, no explanation, nothing else.
9243
+
9244
+ User's message:
9245
+ """
9246
+ ${userText.slice(0, 2e3)}
9247
+ """`;
9248
+ }
9249
+ var FACT_STOPWORDS = /* @__PURE__ */ new Set([
9250
+ "i",
9251
+ "a",
9252
+ "an",
9253
+ "the",
9254
+ "is",
9255
+ "am",
9256
+ "are",
9257
+ "was",
9258
+ "were",
9259
+ "in",
9260
+ "on",
9261
+ "at",
9262
+ "as",
9263
+ "my",
9264
+ "of",
9265
+ "to",
9266
+ "for",
9267
+ "and",
9268
+ "or",
9269
+ "with"
9270
+ ]);
9271
+ function isDuplicateFact(existing, candidate) {
9272
+ const norm = (s) => s.toLowerCase().trim().replace(/[.!?]+$/, "");
9273
+ const contentWords = (s) => new Set(s.split(/\s+/).filter((w) => w && !FACT_STOPWORDS.has(w)));
9274
+ const c = norm(candidate);
9275
+ if (!c) return true;
9276
+ for (const e of existing) {
9277
+ const ne = norm(e);
9278
+ if (!ne) continue;
9279
+ if (ne.includes(c) || c.includes(ne)) return true;
9280
+ const ws1 = contentWords(ne);
9281
+ const ws2 = contentWords(c);
9282
+ const intersection = [...ws1].filter((w) => ws2.has(w)).length;
9283
+ const union = (/* @__PURE__ */ new Set([...ws1, ...ws2])).size;
9284
+ if (union > 0 && intersection / union >= 0.6) return true;
9285
+ }
9286
+ return false;
9287
+ }
9288
+ function parseFacts(raw) {
9289
+ const cleaned = raw.replace(/<think>[\s\S]*?<\/think>/gi, "").trim();
9290
+ if (!cleaned || /^none$/i.test(cleaned)) return [];
9291
+ 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);
9292
+ }
9293
+ async function extractMemoryFacts(d, convId, userText, target) {
9294
+ try {
9295
+ if (userText.trim().length < MIN_LENGTH) return;
9296
+ const ms = d.manager.status();
9297
+ if (ms.state !== "running") return;
9298
+ const release = d.gate ? await d.gate.acquire("bg") : null;
9299
+ let res;
9300
+ try {
9301
+ res = await fetch(`${target}/v1/chat/completions`, {
9302
+ method: "POST",
9303
+ headers: { "Content-Type": "application/json" },
9304
+ body: JSON.stringify({
9305
+ model: engineModelAlias(d.registry.active()?.kind ?? "") ?? ms.model?.key,
9306
+ messages: [{ role: "user", content: extractionPrompt(userText) }],
9307
+ stream: false,
9308
+ temperature: 0.2,
9309
+ max_tokens: 200,
9310
+ reasoning_budget: 0,
9311
+ chat_template_kwargs: { enable_thinking: false }
9312
+ }),
9313
+ signal: AbortSignal.timeout(2e4)
9314
+ });
9315
+ } finally {
9316
+ release?.();
9317
+ }
9318
+ if (!res.ok) return;
9319
+ const data = await res.json();
9320
+ const facts = parseFacts(data.choices?.[0]?.message?.content ?? "");
9321
+ if (!facts.length) return;
9322
+ const existing = d.db.listMemoryFacts().map((f) => f.factText);
9323
+ for (const fact of facts) {
9324
+ if (isDuplicateFact(existing, fact)) continue;
9325
+ d.db.addMemoryFact({ factText: fact, sourceConvId: convId });
9326
+ existing.push(fact);
9327
+ }
9328
+ } catch {
9329
+ }
9330
+ }
9331
+
8990
9332
  // src/chat/chat-routes.ts
8991
9333
  var inflight = /* @__PURE__ */ new Map();
8992
9334
  function abortAllInFlightChats() {
@@ -9029,6 +9371,17 @@ function registerChatRoutes(app2, d) {
9029
9371
  if (!ok) return err(c, 404, "not_found", "Folder not found.");
9030
9372
  return c.json({ ok: true });
9031
9373
  });
9374
+ app2.get("/api/v1/memory", (c) => c.json({ facts: db2.listMemoryFacts() }));
9375
+ app2.delete("/api/v1/memory/:id", (c) => {
9376
+ const ok = db2.deleteMemoryFact(c.req.param("id"));
9377
+ if (!ok) return err(c, 404, "not_found", "Fact not found.");
9378
+ return c.json({ ok: true });
9379
+ });
9380
+ app2.get("/api/v1/tokens/usage", (c) => {
9381
+ const q = c.req.query("range");
9382
+ const range = q === "30d" || q === "7d" ? q : "all";
9383
+ return c.json(db2.tokenUsageStats(range));
9384
+ });
9032
9385
  app2.get("/api/v1/conversations", (c) => {
9033
9386
  const q = c.req.query("q");
9034
9387
  return c.json({ conversations: db2.listConversations(q, "chat") });
@@ -9158,7 +9511,7 @@ ${content}` : b.docContext : content;
9158
9511
  ...images.map((url) => ({ type: "image_url", image_url: { url } }))
9159
9512
  ] : fullContent;
9160
9513
  engineMessages.push({ role: "user", content: userContent });
9161
- await runGeneration(d, stream, { convId, conv, engineMessages, assistantMsg, ms, target, ac, disableThinking: b.disableThinking ?? false });
9514
+ await runGeneration(d, stream, { convId, conv, engineMessages, assistantMsg, ms, target, ac, disableThinking: b.disableThinking ?? false, userText: content });
9162
9515
  });
9163
9516
  });
9164
9517
  app2.post("/api/v1/conversations/:id/continue", async (c) => {
@@ -9874,6 +10227,11 @@ ${existing}`.trim() };
9874
10227
  void autoTitle(d, convId, ctx.engineMessages, fullContent, target);
9875
10228
  }, 1e3);
9876
10229
  }
10230
+ if (!aborted && ctx.userText && d.store.snapshot().daemon.autoMemoryEnabled) {
10231
+ setTimeout(() => {
10232
+ void extractMemoryFacts(d, convId, ctx.userText, target);
10233
+ }, 1500);
10234
+ }
9877
10235
  }
9878
10236
  async function autoTitle(d, convId, prevMessages, assistantReply, target) {
9879
10237
  try {
@@ -12213,6 +12571,7 @@ function registerApi(app2, d) {
12213
12571
  updates2.theme = b.theme;
12214
12572
  }
12215
12573
  if (b.autoGenerateTitles !== void 0) updates2.autoGenerateTitles = !!b.autoGenerateTitles;
12574
+ if (b.autoMemoryEnabled !== void 0) updates2.autoMemoryEnabled = !!b.autoMemoryEnabled;
12216
12575
  if (b.openBrowserOnStart !== void 0) updates2.openBrowserOnStart = !!b.openBrowserOnStart;
12217
12576
  let vramHeadroomMb;
12218
12577
  if (b.vramHeadroomMb !== void 0) {
@@ -12643,6 +13002,7 @@ function settingsPayload(d) {
12643
13002
  port: cfg2.daemon.port,
12644
13003
  theme: cfg2.daemon.theme,
12645
13004
  autoGenerateTitles: cfg2.daemon.autoGenerateTitles,
13005
+ autoMemoryEnabled: cfg2.daemon.autoMemoryEnabled,
12646
13006
  openBrowserOnStart: cfg2.daemon.openBrowserOnStart,
12647
13007
  autoLoadOnStart: cfg2.autoLoadOnStart,
12648
13008
  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-Cl88q0Ki.js";import{f as U}from"./chat-api-B6AobDYd.js";import{f as V,a as X}from"./agent-api-DZqYdw7O.js";import{P as z}from"./personas-C_VMMyvv.js";import{C as Z}from"./chevron-left-B-1Bh1dK.js";import{R as ee}from"./rotate-ccw-xceH34T7.js";import{T as te}from"./trash-2-BazpB6Tv.js";import{C as se}from"./chevron-right-By1T2-Hr.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};