teapot-coding-agent 0.37.75 → 0.37.77
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/dist/master.js +77 -8
- package/dist/migrations/session-titles.js +38 -9
- package/package.json +1 -1
- package/public/assets/{index-B4K5fPmy.js → index-Betlb5cN.js} +10 -10
- package/public/assets/index-Betlb5cN.js.map +1 -0
- package/public/assets/{shiki-C-luiPAq.js → shiki-nwPgnhe1.js} +2 -2
- package/public/assets/{shiki-C-luiPAq.js.map → shiki-nwPgnhe1.js.map} +1 -1
- package/public/index.html +1 -1
- package/public/assets/index-B4K5fPmy.js.map +0 -1
package/dist/master.js
CHANGED
|
@@ -287,6 +287,10 @@ export class Master {
|
|
|
287
287
|
this.raw.defaultProvider = this.config.defaultProvider;
|
|
288
288
|
this.raw.tasks = this.config.tasks;
|
|
289
289
|
this.raw.mcpServers = this.config.mcpServers;
|
|
290
|
+
if (this.config.sessionTitles !== undefined)
|
|
291
|
+
this.raw.sessionTitles = this.config.sessionTitles;
|
|
292
|
+
else
|
|
293
|
+
delete this.raw.sessionTitles;
|
|
290
294
|
if (this.config.customThemes !== undefined)
|
|
291
295
|
this.raw.customThemes = this.config.customThemes;
|
|
292
296
|
if (this.config.password !== undefined)
|
|
@@ -714,6 +718,28 @@ export class Master {
|
|
|
714
718
|
agent.toolCtx.subAgents = hooks;
|
|
715
719
|
}
|
|
716
720
|
await agent.init();
|
|
721
|
+
/**
|
|
722
|
+
* Seed the title from the SESSION, so a restart shows the name the operator gave it.
|
|
723
|
+
*
|
|
724
|
+
* `snapshot().sessionTitle` comes from the agent's own field, and that was only ever set by
|
|
725
|
+
* `renameSession` — so after a restart the UI fell back to the auto-generated name even though the
|
|
726
|
+
* title was on disk. The title comes from `config.sessionTitles`, which is the one copy.
|
|
727
|
+
*/
|
|
728
|
+
const seedTitle = this.readSessionTitle(path.basename(sessionDir));
|
|
729
|
+
if (seedTitle)
|
|
730
|
+
agent.setSessionTitle(seedTitle);
|
|
731
|
+
else {
|
|
732
|
+
/**
|
|
733
|
+
* Derive a missing title at BOOT, once, and write it down.
|
|
734
|
+
*
|
|
735
|
+
* Waiting for the session's next user prompt means an existing session shows no name until the
|
|
736
|
+
* operator types into it, and every left-panel read has to fall back. Doing it here keeps the
|
|
737
|
+
* derivation to one pass per session and off the read path.
|
|
738
|
+
*/
|
|
739
|
+
const id = path.basename(sessionDir);
|
|
740
|
+
this.writeSessionTitle(id, this.sessionTitleOf(sessionDir) ?? id);
|
|
741
|
+
agent.setSessionTitle(this.readSessionTitle(id) ?? id);
|
|
742
|
+
}
|
|
717
743
|
this.agents.set(ac.id, agent);
|
|
718
744
|
if (opts.persist) {
|
|
719
745
|
this.config.agents.push(ac);
|
|
@@ -730,6 +756,9 @@ export class Master {
|
|
|
730
756
|
if (!entry)
|
|
731
757
|
return;
|
|
732
758
|
entry.session = sessionId;
|
|
759
|
+
// The title is NOT deleted here: it now lives in the session's own manifest, so moving to another
|
|
760
|
+
// session simply means reading a different one. (Deleting it was how a rename reverted to the
|
|
761
|
+
// auto-generated name on the next switch.)
|
|
733
762
|
delete entry.sessionTitle;
|
|
734
763
|
delete ac.sessionTitle;
|
|
735
764
|
if (this.sessionsReadOnly)
|
|
@@ -1029,18 +1058,49 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
|
|
|
1029
1058
|
const a = this.config.agents.find((x) => x.id === agentId);
|
|
1030
1059
|
if (!a || a.session !== sessionId)
|
|
1031
1060
|
return undefined;
|
|
1032
|
-
|
|
1033
|
-
return t ? t : undefined;
|
|
1061
|
+
return this.readSessionTitle(sessionId) ?? undefined;
|
|
1034
1062
|
}
|
|
1063
|
+
/** The session's own name, from its manifest. One place, so it cannot go stale in another. */
|
|
1064
|
+
readSessionTitle(sessionId) {
|
|
1065
|
+
if (!sessionId)
|
|
1066
|
+
return null;
|
|
1067
|
+
const t = this.config.sessionTitles?.[sessionId];
|
|
1068
|
+
return typeof t === "string" && t.trim() ? t.trim() : null;
|
|
1069
|
+
}
|
|
1070
|
+
writeSessionTitle(sessionId, title) {
|
|
1071
|
+
if (!sessionId)
|
|
1072
|
+
return;
|
|
1073
|
+
/**
|
|
1074
|
+
* Never write when the data dir was overridden by `TEAPOT_DATA_DIR`.
|
|
1075
|
+
*
|
|
1076
|
+
* In that mode the sessions being resolved are NOT the ones this config describes, so recording
|
|
1077
|
+
* anything about them — a pointer, or now a title — corrupts a config the operator did not ask to
|
|
1078
|
+
* change (#143). The title is still derived in memory so the UI shows something.
|
|
1079
|
+
*/
|
|
1080
|
+
if (this.sessionsReadOnly) {
|
|
1081
|
+
this.config.sessionTitles ??= {};
|
|
1082
|
+
this.config.sessionTitles[sessionId] = title;
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
this.config.sessionTitles ??= {};
|
|
1086
|
+
this.config.sessionTitles[sessionId] = title;
|
|
1087
|
+
this.saveConfig();
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Derive a session's title ONCE, into the session's own manifest.
|
|
1091
|
+
*
|
|
1092
|
+
* Deriving it at read time would be worse: `sessionsOf`/`archivedAgents` would scan a `chat.jsonl`
|
|
1093
|
+
* per session on every call (MEASURED earlier: 0.87s for one archive toggle). Writing it down once,
|
|
1094
|
+
* where the session lives, keeps that scan off the hot path and keeps the answer in ONE place.
|
|
1095
|
+
*/
|
|
1035
1096
|
async ensureSessionTitle(agentId, sessionId) {
|
|
1036
1097
|
const a = this.config.agents.find((x) => x.id === agentId);
|
|
1037
1098
|
if (!a || a.session !== sessionId)
|
|
1038
1099
|
return;
|
|
1039
|
-
if (
|
|
1100
|
+
if (this.readSessionTitle(sessionId))
|
|
1040
1101
|
return;
|
|
1041
1102
|
const dir = path.join(this.sessionsRoot(), sessionId);
|
|
1042
|
-
|
|
1043
|
-
this.saveConfig();
|
|
1103
|
+
this.writeSessionTitle(sessionId, this.sessionTitleOf(dir) ?? sessionId);
|
|
1044
1104
|
}
|
|
1045
1105
|
sessionTitleOf(dirPath) {
|
|
1046
1106
|
let fd;
|
|
@@ -1215,7 +1275,8 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
|
|
|
1215
1275
|
size = st.size;
|
|
1216
1276
|
}
|
|
1217
1277
|
catch { }
|
|
1218
|
-
|
|
1278
|
+
// from the config map — no second per-directory read
|
|
1279
|
+
const title = owner ? this.readSessionTitle(d) ?? undefined : undefined;
|
|
1219
1280
|
out.push({ id: d, agentId: owner, mtimeMs: m, sizeBytes: size, ...(title ? { title } : {}) });
|
|
1220
1281
|
}
|
|
1221
1282
|
return out.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
@@ -1263,8 +1324,16 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
|
|
|
1263
1324
|
const clean = title.replace(/\s+/g, " ").trim().slice(0, 80);
|
|
1264
1325
|
if (!clean)
|
|
1265
1326
|
throw new Error("a session title cannot be empty");
|
|
1266
|
-
|
|
1267
|
-
|
|
1327
|
+
/**
|
|
1328
|
+
* The title is keyed by SESSION, not by agent.
|
|
1329
|
+
*
|
|
1330
|
+
* As `agents[].sessionTitle` it was per AGENT, so `recordSession()` had to delete it whenever the
|
|
1331
|
+
* agent moved session — which is what reverted a rename — and a session no agent points at could not
|
|
1332
|
+
* keep a name at all. The map fixes both AND keeps the left panel to one config read: storing it in
|
|
1333
|
+
* `<session>/session.json` instead was MEASURED at 13.2ms for 838 sessions versus 1.2ms here.
|
|
1334
|
+
*/
|
|
1335
|
+
this.writeSessionTitle(sessionId, clean);
|
|
1336
|
+
delete a.sessionTitle;
|
|
1268
1337
|
this.agents.get(agentId)?.setSessionTitle(clean);
|
|
1269
1338
|
bus.emit("update", { kind: "agent-update", agentId });
|
|
1270
1339
|
return { title: clean };
|
|
@@ -7,19 +7,48 @@ export const sessionTitles = {
|
|
|
7
7
|
run(ctx) {
|
|
8
8
|
const raw = ctx.raw();
|
|
9
9
|
const agents = Array.isArray(raw.agents) ? raw.agents : [];
|
|
10
|
+
const isRecord = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
10
11
|
let changed = false;
|
|
12
|
+
/**
|
|
13
|
+
* Move the title out of `config.json` and into the session's own `session.json`.
|
|
14
|
+
*
|
|
15
|
+
* It used to be stored in `config.json` — a second copy of something the session directory owns,
|
|
16
|
+
* and the copy that went stale. A title already present wins over a derived one, so an operator's
|
|
17
|
+
* rename is never replaced by the auto-generated name.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Move `agents[].sessionTitle` into the `sessionTitles` MAP, keyed by session id.
|
|
21
|
+
*
|
|
22
|
+
* Per-agent was the wrong key: an agent moves between sessions, so the field had to be cleared on
|
|
23
|
+
* every switch (which reverted a rename), and a session no agent points at could not keep a name.
|
|
24
|
+
* A map also keeps the left panel to ONE config read instead of a `session.json` per directory —
|
|
25
|
+
* MEASURED: 838 sessions = 13.2ms of directory reads vs 1.2ms for the config file.
|
|
26
|
+
*/
|
|
27
|
+
const titles = (raw.sessionTitles = isRecord(raw.sessionTitles) ? raw.sessionTitles : {});
|
|
11
28
|
for (const a of agents) {
|
|
12
29
|
const session = typeof a.session === "string" ? a.session : "";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
const legacy = typeof a.sessionTitle === "string" ? a.sessionTitle.trim() : "";
|
|
31
|
+
if (session && !session.includes("/") && !session.includes("..")) {
|
|
32
|
+
const already = typeof titles[session] === "string" ? String(titles[session]).trim() : "";
|
|
33
|
+
if (!already) {
|
|
34
|
+
const derived = legacy ||
|
|
35
|
+
firstUserPromptText(path.join(ctx.dataDir, "sessions", session, "chat.jsonl")) ||
|
|
36
|
+
session;
|
|
37
|
+
titles[session] = derived;
|
|
38
|
+
changed = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Only report a change when one happened: a migration that claims success every boot rewrites
|
|
43
|
+
* the config file forever, and re-running must be a genuine no-op (the suite asserts it).
|
|
44
|
+
*/
|
|
45
|
+
if (a.sessionTitle !== undefined) {
|
|
46
|
+
delete a.sessionTitle;
|
|
47
|
+
changed = true;
|
|
48
|
+
}
|
|
22
49
|
}
|
|
50
|
+
if (Object.keys(titles).length === 0 && raw.sessionTitles !== undefined)
|
|
51
|
+
delete raw.sessionTitles;
|
|
23
52
|
if (changed)
|
|
24
53
|
ctx.touch();
|
|
25
54
|
return changed;
|