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.
- package/README.md +7 -0
- package/dist/cli.js +362 -2
- package/dist/webdist/assets/{AgentEditPage-CfiJQeRy.js → AgentEditPage-Biq01vi2.js} +1 -1
- package/dist/webdist/assets/ChatScreen-Dmt_qvDr.js +65 -0
- package/dist/webdist/assets/{CustomizeScreen-BQMntB-C.js → CustomizeScreen-Dy4Cn3bT.js} +1 -1
- package/dist/webdist/assets/DeveloperScreen-DFs3rx7C.js +1 -0
- package/dist/webdist/assets/{EnginesScreen-BGc09u0i.js → EnginesScreen-RkTfEghZ.js} +6 -6
- package/dist/webdist/assets/FsBrowser-Dz4gWOWq.js +1 -0
- package/dist/webdist/assets/{MessageBubble-CxHvNrgj.js → MessageBubble-BxDHn4od.js} +16 -16
- package/dist/webdist/assets/{ModelsScreen-RRK-uCW8.js → ModelsScreen-DuUqlRGS.js} +2 -2
- package/dist/webdist/assets/SettingsScreen-BfP75nuS.js +1 -0
- package/dist/webdist/assets/{SkillEditPage-CHh9An9Z.js → SkillEditPage-D6OKcHNG.js} +1 -1
- package/dist/webdist/assets/TokensScreen-DblmY7_u.js +1 -0
- package/dist/webdist/assets/WorkspaceScreen-DLfMYwYA.js +1 -0
- package/dist/webdist/assets/{agent-api-B0qitSjr.js → agent-api-DZqYdw7O.js} +1 -1
- package/dist/webdist/assets/{arc-Cdcmyops.js → arc-DV0DkONk.js} +1 -1
- package/dist/webdist/assets/{architectureDiagram-3BPJPVTR-BQlFr2uc.js → architectureDiagram-3BPJPVTR-CCklxbKw.js} +1 -1
- package/dist/webdist/assets/{arrow-left-BgPNNAIi.js → arrow-left-CGv_OXEs.js} +1 -1
- package/dist/webdist/assets/badge-K_-MWGgP.js +1 -0
- package/dist/webdist/assets/{blockDiagram-GPEHLZMM-CMRPD5BN.js → blockDiagram-GPEHLZMM-DxjGvSV7.js} +1 -1
- package/dist/webdist/assets/{bot-CVbPHmTQ.js → bot-CD_BRtw9.js} +1 -1
- package/dist/webdist/assets/{c4Diagram-AAUBKEIU-BYXwwlnp.js → c4Diagram-AAUBKEIU-C9AuWiX4.js} +1 -1
- package/dist/webdist/assets/channel-D8wWQ4tZ.js +1 -0
- package/dist/webdist/assets/{chat-api-x93Fzf3q.js → chat-api-B6AobDYd.js} +3 -3
- package/dist/webdist/assets/check-Dwz6EU4N.js +1 -0
- package/dist/webdist/assets/{chevron-left-D03iirWA.js → chevron-left-B-1Bh1dK.js} +1 -1
- package/dist/webdist/assets/{chevron-right-B7Mj5eje.js → chevron-right-By1T2-Hr.js} +1 -1
- package/dist/webdist/assets/{chunk-2J33WTMH-BTHWkgj0.js → chunk-2J33WTMH-jxkAgaR9.js} +1 -1
- package/dist/webdist/assets/{chunk-4BX2VUAB-C0OdA31w.js → chunk-4BX2VUAB-BNFzh5g5.js} +1 -1
- package/dist/webdist/assets/{chunk-55IACEB6-V6hn9Bpx.js → chunk-55IACEB6-DX5wR3Qq.js} +1 -1
- package/dist/webdist/assets/{chunk-727SXJPM-DCntVbqt.js → chunk-727SXJPM-CbrKMz1R.js} +1 -1
- package/dist/webdist/assets/{chunk-AQP2D5EJ-D4PhS_5c.js → chunk-AQP2D5EJ-CNb3wWkL.js} +1 -1
- package/dist/webdist/assets/{chunk-FMBD7UC4-qDBSNTHO.js → chunk-FMBD7UC4-DEhDwjoQ.js} +1 -1
- package/dist/webdist/assets/{chunk-ND2GUHAM-D7KguwAx.js → chunk-ND2GUHAM-BHmlzliw.js} +1 -1
- package/dist/webdist/assets/{chunk-QZHKN3VN-3zsu8TEd.js → chunk-QZHKN3VN-uI16s_C7.js} +1 -1
- package/dist/webdist/assets/{circle-x-Bm98m9k6.js → circle-x-yVtiCsS-.js} +1 -1
- package/dist/webdist/assets/classDiagram-4FO5ZUOK-BCecvXTJ.js +1 -0
- package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-BCecvXTJ.js +1 -0
- package/dist/webdist/assets/{collapsible-CZm9yoBQ.js → collapsible-D0c6B4Ip.js} +1 -1
- package/dist/webdist/assets/{common-DhVKZnxd.js → common-D_CzNUTa.js} +1 -1
- package/dist/webdist/assets/{cose-bilkent-S5V4N54A-CdJ22dvm.js → cose-bilkent-S5V4N54A-B1nfQjrr.js} +1 -1
- package/dist/webdist/assets/{dagre-BM42HDAG-C0tsLnJj.js → dagre-BM42HDAG-B6nBvZED.js} +2 -2
- package/dist/webdist/assets/{diagram-2AECGRRQ-tReRMIGZ.js → diagram-2AECGRRQ-4HWbpgy8.js} +1 -1
- package/dist/webdist/assets/{diagram-5GNKFQAL-BPDwtKWf.js → diagram-5GNKFQAL-DYfcLJfD.js} +1 -1
- package/dist/webdist/assets/{diagram-KO2AKTUF-dxXvPo_e.js → diagram-KO2AKTUF-BIqc86GH.js} +1 -1
- package/dist/webdist/assets/{diagram-LMA3HP47-BblbbKep.js → diagram-LMA3HP47-CfsmrpTV.js} +1 -1
- package/dist/webdist/assets/{diagram-OG6HWLK6-Bm5kmmiK.js → diagram-OG6HWLK6-DdZxRjl2.js} +1 -1
- package/dist/webdist/assets/{dialog-BS2ga4Li.js → dialog-C5rebm8b.js} +1 -1
- package/dist/webdist/assets/{erDiagram-TEJ5UH35-DtBl4psP.js → erDiagram-TEJ5UH35-BINxvowu.js} +1 -1
- package/dist/webdist/assets/{flowDiagram-I6XJVG4X-inuE1WTM.js → flowDiagram-I6XJVG4X-BphwX6XS.js} +1 -1
- package/dist/webdist/assets/{folder-input-DPrdjjXF.js → folder-input-DsjMa7S5.js} +1 -1
- package/dist/webdist/assets/{ganttDiagram-6RSMTGT7-C1x80EJK.js → ganttDiagram-6RSMTGT7-C63SidXF.js} +1 -1
- package/dist/webdist/assets/{gitGraphDiagram-PVQCEYII-BHFBSO22.js → gitGraphDiagram-PVQCEYII-D9FpCno0.js} +1 -1
- package/dist/webdist/assets/index-BAK-ZQCj.css +1 -0
- package/dist/webdist/assets/{index-DT-1Skfu.js → index-Cl88q0Ki.js} +7 -7
- package/dist/webdist/assets/{index-BPqqiChM.js → index-g6XC4RLK.js} +1 -1
- package/dist/webdist/assets/{infoDiagram-5YYISTIA-DErEOAbT.js → infoDiagram-5YYISTIA-BTrZQ4Wc.js} +1 -1
- package/dist/webdist/assets/{ishikawaDiagram-YF4QCWOH-CFYAcSCu.js → ishikawaDiagram-YF4QCWOH-CtpkiMI3.js} +1 -1
- package/dist/webdist/assets/{journeyDiagram-JHISSGLW-BRF8x-vW.js → journeyDiagram-JHISSGLW-Cs5Ssisp.js} +1 -1
- package/dist/webdist/assets/{kanban-definition-UN3LZRKU-Bpxu1hZh.js → kanban-definition-UN3LZRKU-Bd2W5L9B.js} +7 -7
- package/dist/webdist/assets/{linear-SOR4O2dK.js → linear-GlXwNR5R.js} +1 -1
- package/dist/webdist/assets/{mermaid.core-Gi-pYgdj.js → mermaid.core-BmvPXnHB.js} +5 -5
- package/dist/webdist/assets/{mindmap-definition-RKZ34NQL-DtatIfBM.js → mindmap-definition-RKZ34NQL-CuO5FwWr.js} +1 -1
- package/dist/webdist/assets/{pencil-Bi7z3_2g.js → pencil-C4EXnT_a.js} +1 -1
- package/dist/webdist/assets/{personas-CsvqnRpC.js → personas-C_VMMyvv.js} +15 -8
- package/dist/webdist/assets/{pieDiagram-4H26LBE5-DQYaIU8r.js → pieDiagram-4H26LBE5-CLty44d5.js} +1 -1
- package/dist/webdist/assets/{quadrantDiagram-W4KKPZXB-CUu3EWq8.js → quadrantDiagram-W4KKPZXB-BK9u_S8e.js} +1 -1
- package/dist/webdist/assets/{refresh-cw-CkhIL2Hz.js → refresh-cw-ChBdLwHh.js} +1 -1
- package/dist/webdist/assets/{requirementDiagram-4Y6WPE33-D54OOdDO.js → requirementDiagram-4Y6WPE33-DaldyJn3.js} +1 -1
- package/dist/webdist/assets/{rotate-ccw-BOQNX9cU.js → rotate-ccw-xceH34T7.js} +1 -1
- package/dist/webdist/assets/{sankeyDiagram-5OEKKPKP-yykV1xV0.js → sankeyDiagram-5OEKKPKP-ECm1YkbT.js} +1 -1
- package/dist/webdist/assets/{sequenceDiagram-3UESZ5HK-CD2n3CVh.js → sequenceDiagram-3UESZ5HK-D_WQr7Ox.js} +1 -1
- package/dist/webdist/assets/skeleton-DK4wQNYn.js +1 -0
- package/dist/webdist/assets/{star-geKnFl6r.js → star-uW87WNRR.js} +1 -1
- package/dist/webdist/assets/{stateDiagram-AJRCARHV-C7hTc-QA.js → stateDiagram-AJRCARHV-Dc4nVZrV.js} +1 -1
- package/dist/webdist/assets/stateDiagram-v2-BHNVJYJU-b4uEiOn_.js +1 -0
- package/dist/webdist/assets/{switch-BIJmJ4CL.js → switch-4XvTVvk1.js} +1 -1
- package/dist/webdist/assets/{timeline-definition-PNZ67QCA-6kJpBXAe.js → timeline-definition-PNZ67QCA-BLAoVxaR.js} +1 -1
- package/dist/webdist/assets/{trash-2-CT_92vaT.js → trash-2-BazpB6Tv.js} +1 -1
- package/dist/webdist/assets/{useIsDesktop-Dxs_CPEE.js → useIsDesktop-BRKlTjOF.js} +1 -1
- package/dist/webdist/assets/{vennDiagram-CIIHVFJN-B7h2KOpV.js → vennDiagram-CIIHVFJN-Dxiec1ZZ.js} +1 -1
- package/dist/webdist/assets/{wardley-L42UT6IY-CvmW62QW.js → wardley-L42UT6IY-B6eLCEJg.js} +1 -1
- package/dist/webdist/assets/{wardleyDiagram-YWT4CUSO-BNvpRPx5.js → wardleyDiagram-YWT4CUSO-CZda0vR7.js} +1 -1
- package/dist/webdist/assets/{wrench-DNrLSGCv.js → wrench-DffxK2pt.js} +1 -1
- package/dist/webdist/assets/{xychartDiagram-2RQKCTM6-BQI4t1tF.js → xychartDiagram-2RQKCTM6-B8xutv1Z.js} +1 -1
- package/dist/webdist/index.html +2 -2
- package/package.json +1 -1
- package/dist/webdist/assets/ChatScreen-CyzSdlm7.js +0 -65
- package/dist/webdist/assets/DeveloperScreen-DKDEoZ_0.js +0 -1
- package/dist/webdist/assets/FsBrowser-CxCRvnU8.js +0 -1
- package/dist/webdist/assets/SettingsScreen-CDUlGnqu.js +0 -1
- package/dist/webdist/assets/WorkspaceScreen-BbjiYb6f.js +0 -1
- package/dist/webdist/assets/channel-B3OIXexj.js +0 -1
- package/dist/webdist/assets/check-Cx4ep7jU.js +0 -1
- package/dist/webdist/assets/classDiagram-4FO5ZUOK-CIneh01q.js +0 -1
- package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-CIneh01q.js +0 -1
- package/dist/webdist/assets/index-CuJf6kYG.css +0 -1
- 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-
|
|
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};
|