@symerian/symi 3.5.0 → 3.5.1
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/build-info.json +3 -3
- package/dist/bundled/boot-md/handler.js +4 -4
- package/dist/bundled/session-memory/handler.js +4 -4
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/{chrome-C_I81hbq.js → chrome-B7-rO4i9.js} +4 -4
- package/dist/{chrome-BKUACyeO.js → chrome-DPjznJQ-.js} +4 -4
- package/dist/control-ui/css/revert-red-theme.md +141 -0
- package/dist/control-ui/css/style.css +5843 -0
- package/dist/control-ui/css/style.css.backup-2026-03-03-162525 +3546 -0
- package/dist/control-ui/css/style.css.backup-before-red-2026-03-03-162525 +3546 -0
- package/dist/control-ui/css/style.css.backup-before-red-theme-2026-03-03-162530 +3546 -0
- package/dist/control-ui/css/style.css.pre-2row +2165 -0
- package/dist/control-ui/css/style.css.pre-brand +1776 -0
- package/dist/control-ui/css/style.css.pre-history +1974 -0
- package/dist/control-ui/css/style.css.pre-nav +2264 -0
- package/dist/control-ui/css/style.css.pre-newsession +1898 -0
- package/dist/control-ui/css/style.css.pre-queue +2195 -0
- package/dist/control-ui/css/style.css.pre-red-prompt +2524 -0
- package/dist/control-ui/css/style.css.pre-stop +2239 -0
- package/dist/control-ui/css/style.css.pre-textarea +2184 -0
- package/dist/control-ui/css/style.css.pre-watchdog +1848 -0
- package/dist/control-ui/css/style.css.red-theme +2999 -0
- package/dist/control-ui/index.html +1049 -0
- package/dist/control-ui/js/app.js +1304 -0
- package/dist/control-ui/js/app.js.pre-2row +463 -0
- package/dist/control-ui/js/app.js.pre-heartbeat-filter +595 -0
- package/dist/control-ui/js/app.js.pre-newsession +408 -0
- package/dist/control-ui/js/app.js.pre-queue +476 -0
- package/dist/control-ui/js/app.js.pre-stop +564 -0
- package/dist/control-ui/js/app.js.pre-textarea +467 -0
- package/dist/control-ui/js/app.js.pre-watchdog +293 -0
- package/dist/control-ui/js/connections.js +438 -0
- package/dist/control-ui/js/gateway.js +233 -0
- package/dist/control-ui/js/gateway.js.pre-stop +110 -0
- package/dist/control-ui/js/history.js +732 -0
- package/dist/control-ui/js/logs.js +238 -0
- package/dist/control-ui/js/menu.js +232 -0
- package/dist/control-ui/js/menu.js.pre-nav +66 -0
- package/dist/control-ui/js/metrics.js +53 -0
- package/dist/control-ui/js/models.js +138 -0
- package/dist/control-ui/js/render.js +882 -0
- package/dist/control-ui/js/render.test.js +112 -0
- package/dist/control-ui/js/scheduling.js +461 -0
- package/dist/control-ui/js/settings.js +910 -0
- package/dist/control-ui/js/slash-autocomplete.js +168 -0
- package/dist/control-ui/js/subagents.js +560 -0
- package/dist/control-ui/js/utils.js +29 -0
- package/dist/control-ui/vendor/highlight.min.js +2518 -0
- package/dist/control-ui/vendor/marked.min.js +69 -0
- package/dist/{deliver-DyO3QD8O.js → deliver-DTRkeYm3.js} +4 -4
- package/dist/{deliver-Cjyb6h4g.js → deliver-oWGJwzFf.js} +4 -4
- package/dist/extensionAPI.js +4 -4
- package/dist/llm-slug-generator.js +4 -4
- package/dist/{manager-rvtFoeFT.js → manager-CFenq_aO.js} +1 -1
- package/dist/{manager-PTSjHNVq.js → manager-CsxTf96V.js} +1 -1
- package/dist/{pi-embedded-BPuUM-gD.js → pi-embedded-Cdub5Vs9.js} +10 -10
- package/dist/{pw-ai-BFS9ezWe.js → pw-ai-BOOB8qoi.js} +1 -1
- package/dist/{pw-ai-Cx-Ko_FL.js → pw-ai-D2pEVS5n.js} +1 -1
- package/dist/{synthesis-7UL3pCpj.js → synthesis-Be9nYyDd.js} +4 -4
- package/dist/{synthesis-fD8J2vag.js → synthesis-CBIT6Vnk.js} +4 -4
- package/dist/{unified-runner-BIiKFnNF.js → unified-runner-BVvvnjXW.js} +10 -10
- package/package.json +1 -1
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
// ── Session History Browser ────────────────────────────────────────────
|
|
2
|
+
// Provides the history drawer: list past sessions, browse archived
|
|
3
|
+
// transcripts, favourite/delete/load. Exactly one row is ever CURRENT;
|
|
4
|
+
// all others are loadable past sessions.
|
|
5
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
(function () {
|
|
10
|
+
const drawer = document.getElementById("history-drawer");
|
|
11
|
+
const overlay = document.getElementById("history-overlay");
|
|
12
|
+
const drawerBody = document.getElementById("history-drawer-body");
|
|
13
|
+
const closeBtn = document.getElementById("history-close-btn");
|
|
14
|
+
const historyBtn = document.getElementById("history-btn");
|
|
15
|
+
const archiveToast = document.getElementById("session-archive-toast");
|
|
16
|
+
const archiveToastLink = document.getElementById("session-archive-link");
|
|
17
|
+
const searchInput = document.getElementById("history-search-input");
|
|
18
|
+
const searchClear = document.getElementById("history-search-clear");
|
|
19
|
+
|
|
20
|
+
let isOpen = false;
|
|
21
|
+
let toastTimer = null;
|
|
22
|
+
let __viewingFile = null;
|
|
23
|
+
const DELETE_CONFIRM_MS = 5000;
|
|
24
|
+
const SEARCH_DEBOUNCE_MS = 250;
|
|
25
|
+
let searchDebounceTimer = null;
|
|
26
|
+
let activeSearchToken = 0;
|
|
27
|
+
|
|
28
|
+
// ── Open / close ──────────────────────────────────────────────────────
|
|
29
|
+
function openDrawer() {
|
|
30
|
+
isOpen = true;
|
|
31
|
+
drawer.classList.add("open");
|
|
32
|
+
overlay.classList.add("open");
|
|
33
|
+
drawer.setAttribute("aria-hidden", "false");
|
|
34
|
+
void loadSessionList();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function closeDrawer() {
|
|
38
|
+
isOpen = false;
|
|
39
|
+
drawer.classList.remove("open");
|
|
40
|
+
overlay.classList.remove("open");
|
|
41
|
+
drawer.setAttribute("aria-hidden", "true");
|
|
42
|
+
__viewingFile = null;
|
|
43
|
+
showToolCalls = false;
|
|
44
|
+
if (searchInput && searchInput.value) {
|
|
45
|
+
searchInput.value = "";
|
|
46
|
+
if (searchClear) {
|
|
47
|
+
searchClear.hidden = true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
historyBtn.addEventListener("click", () => (isOpen ? closeDrawer() : openDrawer()));
|
|
53
|
+
closeBtn.addEventListener("click", closeDrawer);
|
|
54
|
+
overlay.addEventListener("click", closeDrawer);
|
|
55
|
+
document.addEventListener("keydown", (e) => {
|
|
56
|
+
if (e.key === "Escape" && isOpen) {
|
|
57
|
+
closeDrawer();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ── Cross-session search ─────────────────────────────────────────────
|
|
62
|
+
if (searchInput) {
|
|
63
|
+
searchInput.addEventListener("input", () => {
|
|
64
|
+
const value = searchInput.value;
|
|
65
|
+
if (searchClear) {
|
|
66
|
+
searchClear.hidden = !value;
|
|
67
|
+
}
|
|
68
|
+
if (searchDebounceTimer) {
|
|
69
|
+
clearTimeout(searchDebounceTimer);
|
|
70
|
+
}
|
|
71
|
+
searchDebounceTimer = setTimeout(() => {
|
|
72
|
+
const trimmed = value.trim();
|
|
73
|
+
if (!trimmed) {
|
|
74
|
+
void loadSessionList();
|
|
75
|
+
} else {
|
|
76
|
+
void runSessionSearch(trimmed);
|
|
77
|
+
}
|
|
78
|
+
}, SEARCH_DEBOUNCE_MS);
|
|
79
|
+
});
|
|
80
|
+
searchInput.addEventListener("keydown", (e) => {
|
|
81
|
+
if (e.key === "Escape" && searchInput.value) {
|
|
82
|
+
e.stopPropagation();
|
|
83
|
+
searchInput.value = "";
|
|
84
|
+
if (searchClear) {
|
|
85
|
+
searchClear.hidden = true;
|
|
86
|
+
}
|
|
87
|
+
void loadSessionList();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (searchClear) {
|
|
92
|
+
searchClear.addEventListener("click", () => {
|
|
93
|
+
if (!searchInput) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
searchInput.value = "";
|
|
97
|
+
searchClear.hidden = true;
|
|
98
|
+
searchInput.focus();
|
|
99
|
+
void loadSessionList();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Session list ──────────────────────────────────────────────────────
|
|
104
|
+
async function loadSessionList() {
|
|
105
|
+
drawerBody.innerHTML = '<div class="history-loading">Loading sessions…</div>';
|
|
106
|
+
__viewingFile = null;
|
|
107
|
+
|
|
108
|
+
const currentKey = window.SESSION_KEY ?? "agent:main:main";
|
|
109
|
+
const url = "/api/sessions?currentKey=" + encodeURIComponent(currentKey);
|
|
110
|
+
|
|
111
|
+
let sessions;
|
|
112
|
+
try {
|
|
113
|
+
const res = await fetch(url);
|
|
114
|
+
const payload = await res.json();
|
|
115
|
+
// Response shape: { sessions: [...] } (2.7.1+)
|
|
116
|
+
sessions = Array.isArray(payload?.sessions) ? payload.sessions : [];
|
|
117
|
+
} catch {
|
|
118
|
+
drawerBody.innerHTML = '<div class="history-empty">Failed to load sessions.</div>';
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!sessions.length) {
|
|
123
|
+
drawerBody.innerHTML = '<div class="history-empty">No session history found.</div>';
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Group rows: CURRENT / FAVOURITES / RECENT
|
|
128
|
+
const current = sessions.filter((s) => s.kind === "current");
|
|
129
|
+
const favourites = sessions.filter((s) => s.kind !== "current" && s.favorited);
|
|
130
|
+
const recent = sessions.filter((s) => s.kind !== "current" && !s.favorited);
|
|
131
|
+
|
|
132
|
+
// Newest `.reset.*` archive with content — this becomes the target of
|
|
133
|
+
// the Load Session button on the CURRENT row, giving the user a
|
|
134
|
+
// one-click path back to the session they just left.
|
|
135
|
+
const returnCandidate = findReturnCandidate(sessions);
|
|
136
|
+
|
|
137
|
+
drawerBody.innerHTML = "";
|
|
138
|
+
|
|
139
|
+
if (current.length > 0) {
|
|
140
|
+
drawerBody.appendChild(renderSection("CURRENT", current, returnCandidate));
|
|
141
|
+
}
|
|
142
|
+
if (favourites.length > 0) {
|
|
143
|
+
drawerBody.appendChild(renderSection("★ FAVOURITES", favourites, null));
|
|
144
|
+
}
|
|
145
|
+
if (recent.length > 0) {
|
|
146
|
+
drawerBody.appendChild(renderSection("RECENT", recent, null));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function findReturnCandidate(sessions) {
|
|
151
|
+
const now = Date.now();
|
|
152
|
+
const windowMs = 24 * 60 * 60 * 1000; // 24 hours
|
|
153
|
+
const candidates = sessions.filter((s) => {
|
|
154
|
+
if (s.kind !== "archived") {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
if (s.archivedReason !== "reset") {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
if (!s.msgCount || s.msgCount <= 0) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
if (!s.archivedAt) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
const archivedMs = new Date(s.archivedAt).getTime();
|
|
167
|
+
if (Number.isNaN(archivedMs)) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
return now - archivedMs < windowMs;
|
|
171
|
+
});
|
|
172
|
+
if (!candidates.length) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
// Newest first
|
|
176
|
+
candidates.sort((a, b) => (b.archivedAt ?? "").localeCompare(a.archivedAt ?? ""));
|
|
177
|
+
return candidates[0];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function renderSection(title, rows, returnCandidate) {
|
|
181
|
+
const section = document.createElement("section");
|
|
182
|
+
section.className = "history-section";
|
|
183
|
+
const heading = document.createElement("div");
|
|
184
|
+
heading.className = "history-section-heading";
|
|
185
|
+
heading.textContent = title;
|
|
186
|
+
section.appendChild(heading);
|
|
187
|
+
for (const row of rows) {
|
|
188
|
+
// Pass the return candidate through only for the CURRENT row — it
|
|
189
|
+
// drives the row's Load Session button (which returns the user to
|
|
190
|
+
// their previous session). Other rows use their own row data.
|
|
191
|
+
section.appendChild(renderRow(row, row.kind === "current" ? returnCandidate : null));
|
|
192
|
+
}
|
|
193
|
+
return section;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function renderRow(row, returnCandidate) {
|
|
197
|
+
const item = document.createElement("div");
|
|
198
|
+
item.className = "history-session-item history-session-" + row.kind;
|
|
199
|
+
|
|
200
|
+
const badge = BADGE[row.kind] ?? BADGE.past;
|
|
201
|
+
const when = formatTimestamp(row.lastActivity);
|
|
202
|
+
const preview = row.preview
|
|
203
|
+
? escHtml(row.preview) + (row.preview.length >= 80 ? "…" : "")
|
|
204
|
+
: '<em style="opacity:0.5">No messages</em>';
|
|
205
|
+
|
|
206
|
+
// CURRENT row's Load Session button targets the most-recent .reset.*
|
|
207
|
+
// archive (returnCandidate). Non-current rows use their own file.
|
|
208
|
+
let footerHtml;
|
|
209
|
+
if (row.kind === "current") {
|
|
210
|
+
footerHtml = returnCandidate
|
|
211
|
+
? `<footer class="history-session-actions">
|
|
212
|
+
<button class="history-load-btn" data-return="1">Load Session</button>
|
|
213
|
+
</footer>`
|
|
214
|
+
: "";
|
|
215
|
+
} else {
|
|
216
|
+
footerHtml = `<footer class="history-session-actions">
|
|
217
|
+
${row.canLoad ? `<button class="history-load-btn">Load Session</button>` : ""}
|
|
218
|
+
${row.canDelete ? `<button class="history-delete-btn">Delete</button>` : ""}
|
|
219
|
+
</footer>`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
item.innerHTML = `
|
|
223
|
+
<header class="history-session-header">
|
|
224
|
+
<button class="history-fav-btn ${row.favorited ? "active" : ""}"
|
|
225
|
+
aria-pressed="${row.favorited ? "true" : "false"}"
|
|
226
|
+
aria-label="${row.favorited ? "Unfavourite" : "Favourite"} session"
|
|
227
|
+
title="${row.favorited ? "Unfavourite" : "Favourite"}">★</button>
|
|
228
|
+
<span class="history-session-badge ${badge.className}">${badge.label}</span>
|
|
229
|
+
<span class="history-session-date">${escHtml(when)}</span>
|
|
230
|
+
</header>
|
|
231
|
+
<div class="history-session-meta">
|
|
232
|
+
<span class="history-session-count">${row.msgCount} msgs · ${fmtSize(row.size)}</span>
|
|
233
|
+
</div>
|
|
234
|
+
<div class="history-session-preview">${preview}</div>
|
|
235
|
+
${footerHtml}
|
|
236
|
+
`;
|
|
237
|
+
|
|
238
|
+
// Body-click behaviour varies by kind:
|
|
239
|
+
// - CURRENT: close the drawer (user is already viewing this session
|
|
240
|
+
// in the main feed — no point opening a transcript preview).
|
|
241
|
+
// - PAST / ARCHIVED: open the transcript viewer.
|
|
242
|
+
item.addEventListener("click", (e) => {
|
|
243
|
+
const target = e.target;
|
|
244
|
+
if (target instanceof Element && target.closest("button")) {
|
|
245
|
+
return; // button click handled below
|
|
246
|
+
}
|
|
247
|
+
if (row.kind === "current") {
|
|
248
|
+
closeDrawer();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
void loadTranscript(row.file, when);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const favBtn = item.querySelector(".history-fav-btn");
|
|
255
|
+
favBtn?.addEventListener("click", async (e) => {
|
|
256
|
+
e.stopPropagation();
|
|
257
|
+
await toggleFavorite(row, favBtn);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const loadBtn = item.querySelector(".history-load-btn");
|
|
261
|
+
loadBtn?.addEventListener("click", async (e) => {
|
|
262
|
+
e.stopPropagation();
|
|
263
|
+
// On the CURRENT row, Load Session loads the most-recent archive
|
|
264
|
+
// (returnCandidate). On PAST/ARCHIVED rows, it loads that row.
|
|
265
|
+
const target = loadBtn.getAttribute("data-return") === "1" ? returnCandidate : row;
|
|
266
|
+
if (target) {
|
|
267
|
+
await loadRow(target);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const deleteBtn = item.querySelector(".history-delete-btn");
|
|
272
|
+
if (deleteBtn instanceof HTMLButtonElement) {
|
|
273
|
+
wireTwoStageDelete(deleteBtn, () => deleteRow(row));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return item;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const BADGE = {
|
|
280
|
+
current: { label: "CURRENT", className: "badge-current" },
|
|
281
|
+
past: { label: "PAST", className: "badge-past" },
|
|
282
|
+
archived: { label: "ARCHIVED", className: "badge-archived" },
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// ── Cross-session search ─────────────────────────────────────────────
|
|
286
|
+
async function runSessionSearch(query) {
|
|
287
|
+
if (!window.gateway?.connected) {
|
|
288
|
+
drawerBody.innerHTML = '<div class="history-empty">Gateway not connected.</div>';
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const token = ++activeSearchToken;
|
|
292
|
+
drawerBody.innerHTML = '<div class="history-loading">Searching…</div>';
|
|
293
|
+
|
|
294
|
+
let result;
|
|
295
|
+
try {
|
|
296
|
+
result = await window.gateway.rpc("sessions.search", { query, limit: 24 });
|
|
297
|
+
} catch (err) {
|
|
298
|
+
if (token !== activeSearchToken) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
drawerBody.innerHTML =
|
|
302
|
+
'<div class="history-empty">Search failed: ' +
|
|
303
|
+
escHtml(err?.message ?? String(err)) +
|
|
304
|
+
"</div>";
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Drop stale responses if a newer search was kicked off mid-flight.
|
|
309
|
+
if (token !== activeSearchToken) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const matches = Array.isArray(result?.matches) ? result.matches : [];
|
|
314
|
+
if (!matches.length) {
|
|
315
|
+
drawerBody.innerHTML = `<div class="history-search-status">No matches for ${escHtml(
|
|
316
|
+
query,
|
|
317
|
+
)}.</div>`;
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
drawerBody.innerHTML = "";
|
|
322
|
+
const status = document.createElement("div");
|
|
323
|
+
status.className = "history-search-status";
|
|
324
|
+
status.textContent = `${matches.length} match${matches.length === 1 ? "" : "es"} for "${query}"`;
|
|
325
|
+
drawerBody.appendChild(status);
|
|
326
|
+
|
|
327
|
+
const section = document.createElement("section");
|
|
328
|
+
section.className = "history-section";
|
|
329
|
+
for (const match of matches) {
|
|
330
|
+
section.appendChild(renderSearchMatch(match, query));
|
|
331
|
+
}
|
|
332
|
+
drawerBody.appendChild(section);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function renderSearchMatch(match, query) {
|
|
336
|
+
const item = document.createElement("div");
|
|
337
|
+
item.className = "history-session-item history-session-search-match";
|
|
338
|
+
|
|
339
|
+
const title =
|
|
340
|
+
match.label ||
|
|
341
|
+
match.displayName ||
|
|
342
|
+
match.sessionKey ||
|
|
343
|
+
`session ${match.sessionId.slice(0, 8)}`;
|
|
344
|
+
const when = match.updatedAt ? formatTimestamp(new Date(match.updatedAt).toISOString()) : "—";
|
|
345
|
+
const lineRange =
|
|
346
|
+
match.lineStart === match.lineEnd
|
|
347
|
+
? `line ${match.lineStart}`
|
|
348
|
+
: `lines ${match.lineStart}–${match.lineEnd}`;
|
|
349
|
+
const score = typeof match.score === "number" ? match.score.toFixed(2) : "—";
|
|
350
|
+
const snippetHtml = highlightSnippet(match.snippet ?? "", query);
|
|
351
|
+
|
|
352
|
+
const canLoad = Boolean(match.sessionKey);
|
|
353
|
+
const footerHtml = canLoad
|
|
354
|
+
? `<footer class="history-session-actions">
|
|
355
|
+
<button class="history-load-btn">Load Session</button>
|
|
356
|
+
</footer>`
|
|
357
|
+
: "";
|
|
358
|
+
|
|
359
|
+
item.innerHTML = `
|
|
360
|
+
<header class="history-session-header">
|
|
361
|
+
<span class="history-session-badge badge-past">MATCH</span>
|
|
362
|
+
<span class="history-session-date">${escHtml(title)}</span>
|
|
363
|
+
</header>
|
|
364
|
+
<div class="history-match-snippet">${snippetHtml}</div>
|
|
365
|
+
<div class="history-match-meta">
|
|
366
|
+
<span>${escHtml(when)}</span>
|
|
367
|
+
<span>${escHtml(lineRange)}</span>
|
|
368
|
+
<span>score ${escHtml(score)}</span>
|
|
369
|
+
</div>
|
|
370
|
+
${footerHtml}
|
|
371
|
+
`;
|
|
372
|
+
|
|
373
|
+
const loadBtn = item.querySelector(".history-load-btn");
|
|
374
|
+
loadBtn?.addEventListener("click", async (e) => {
|
|
375
|
+
e.stopPropagation();
|
|
376
|
+
if (!match.sessionKey) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
// Adopt-by-file using the session's current transcript file (basename).
|
|
380
|
+
await loadRow({ file: match.sessionId + ".jsonl" });
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
item.addEventListener("click", (e) => {
|
|
384
|
+
const target = e.target;
|
|
385
|
+
if (target instanceof Element && target.closest("button")) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
void loadTranscript(match.sessionId + ".jsonl", title);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
return item;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function highlightSnippet(text, query) {
|
|
395
|
+
if (!text) {
|
|
396
|
+
return "";
|
|
397
|
+
}
|
|
398
|
+
if (!query) {
|
|
399
|
+
return escHtml(text);
|
|
400
|
+
}
|
|
401
|
+
const safeText = escHtml(text);
|
|
402
|
+
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
403
|
+
try {
|
|
404
|
+
const re = new RegExp(escapedQuery, "gi");
|
|
405
|
+
return safeText.replace(re, (m) => `<mark>${m}</mark>`);
|
|
406
|
+
} catch {
|
|
407
|
+
return safeText;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ── Actions ───────────────────────────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
async function toggleFavorite(row, favBtn) {
|
|
414
|
+
if (!window.gateway?.connected) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const next = !row.favorited;
|
|
418
|
+
try {
|
|
419
|
+
await window.gateway.rpc("sessions.favoriteFile", {
|
|
420
|
+
file: row.file,
|
|
421
|
+
favorited: next,
|
|
422
|
+
});
|
|
423
|
+
row.favorited = next;
|
|
424
|
+
favBtn.classList.toggle("active", next);
|
|
425
|
+
favBtn.setAttribute("aria-pressed", next ? "true" : "false");
|
|
426
|
+
// Re-render to move the row between FAVOURITES and RECENT sections.
|
|
427
|
+
void loadSessionList();
|
|
428
|
+
} catch (err) {
|
|
429
|
+
console.warn("[history] favorite toggle failed", err);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async function loadRow(row) {
|
|
434
|
+
if (!window.gateway?.connected) {
|
|
435
|
+
alert("Gateway not connected.");
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
drawerBody.innerHTML = '<div class="history-loading">Loading session…</div>';
|
|
439
|
+
try {
|
|
440
|
+
await window.gateway.rpc("sessions.adoptFile", {
|
|
441
|
+
key: window.SESSION_KEY,
|
|
442
|
+
targetFile: row.file,
|
|
443
|
+
});
|
|
444
|
+
closeDrawer();
|
|
445
|
+
if (typeof window.reloadSession === "function") {
|
|
446
|
+
window.reloadSession();
|
|
447
|
+
}
|
|
448
|
+
} catch (err) {
|
|
449
|
+
drawerBody.innerHTML =
|
|
450
|
+
'<div class="history-empty">Load failed: ' +
|
|
451
|
+
escHtml(err?.message ?? String(err)) +
|
|
452
|
+
"</div>";
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function deleteRow(row) {
|
|
457
|
+
if (!window.gateway?.connected) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
if (row.kind === "past" && row.sessionKey) {
|
|
462
|
+
// Archives the transcript via `sessions.delete` — the file becomes
|
|
463
|
+
// an ARCHIVED row in the next list refresh. Reversible.
|
|
464
|
+
await window.gateway.rpc("sessions.delete", {
|
|
465
|
+
key: row.sessionKey,
|
|
466
|
+
deleteTranscript: true,
|
|
467
|
+
});
|
|
468
|
+
} else {
|
|
469
|
+
// Archived files (or orphan live files): permanent deletion.
|
|
470
|
+
await window.gateway.rpc("sessions.deleteFile", { file: row.file });
|
|
471
|
+
}
|
|
472
|
+
await loadSessionList();
|
|
473
|
+
} catch (err) {
|
|
474
|
+
console.warn("[history] delete failed", err);
|
|
475
|
+
alert("Delete failed: " + (err?.message ?? String(err)));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Wire a Delete button as a two-stage confirm. First click arms it with
|
|
481
|
+
* "Really delete?" for DELETE_CONFIRM_MS; second click within the window
|
|
482
|
+
* runs the action. Auto-reverts if the user walks away.
|
|
483
|
+
*/
|
|
484
|
+
function wireTwoStageDelete(btn, run) {
|
|
485
|
+
let armed = false;
|
|
486
|
+
let armTimer = null;
|
|
487
|
+
const originalLabel = btn.textContent;
|
|
488
|
+
btn.addEventListener("click", (e) => {
|
|
489
|
+
e.stopPropagation();
|
|
490
|
+
if (!armed) {
|
|
491
|
+
armed = true;
|
|
492
|
+
btn.textContent = "Really delete?";
|
|
493
|
+
btn.classList.add("armed");
|
|
494
|
+
if (armTimer) {
|
|
495
|
+
clearTimeout(armTimer);
|
|
496
|
+
}
|
|
497
|
+
armTimer = setTimeout(() => {
|
|
498
|
+
armed = false;
|
|
499
|
+
btn.textContent = originalLabel;
|
|
500
|
+
btn.classList.remove("armed");
|
|
501
|
+
}, DELETE_CONFIRM_MS);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
clearTimeout(armTimer);
|
|
505
|
+
armed = false;
|
|
506
|
+
btn.disabled = true;
|
|
507
|
+
btn.textContent = "Deleting…";
|
|
508
|
+
btn.classList.remove("armed");
|
|
509
|
+
void run();
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ── Transcript viewer ─────────────────────────────────────────────────
|
|
514
|
+
// Per-drawer-session preference: whether the transcript modal includes
|
|
515
|
+
// tool_use / tool_result content blocks alongside chat text. Defaults
|
|
516
|
+
// off (matches the historical view); the toggle button in the banner
|
|
517
|
+
// flips it for the current viewing session, then resets on close.
|
|
518
|
+
let showToolCalls = false;
|
|
519
|
+
|
|
520
|
+
async function loadTranscript(file, label) {
|
|
521
|
+
__viewingFile = file;
|
|
522
|
+
drawerBody.innerHTML = '<div class="history-loading">Loading transcript…</div>';
|
|
523
|
+
|
|
524
|
+
let messages;
|
|
525
|
+
try {
|
|
526
|
+
const res = await fetch("/api/transcript?" + new URLSearchParams({ file }));
|
|
527
|
+
const data = await res.json();
|
|
528
|
+
messages = data.messages ?? [];
|
|
529
|
+
} catch {
|
|
530
|
+
drawerBody.innerHTML = '<div class="history-empty">Failed to load transcript.</div>';
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
renderTranscript(messages, label);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function renderTranscript(messages, label) {
|
|
538
|
+
drawerBody.innerHTML = "";
|
|
539
|
+
|
|
540
|
+
const banner = document.createElement("div");
|
|
541
|
+
banner.className = "archive-banner";
|
|
542
|
+
banner.innerHTML = `
|
|
543
|
+
<span class="archive-banner-label">◈ ${escHtml(label)}</span>
|
|
544
|
+
<div class="archive-banner-actions">
|
|
545
|
+
<button class="archive-banner-toggle ${showToolCalls ? "active" : ""}"
|
|
546
|
+
id="archive-tools-toggle"
|
|
547
|
+
aria-pressed="${showToolCalls ? "true" : "false"}">
|
|
548
|
+
${showToolCalls ? "Hide tool calls" : "Show tool calls"}
|
|
549
|
+
</button>
|
|
550
|
+
<button class="archive-banner-back" id="archive-back-btn">← All Sessions</button>
|
|
551
|
+
</div>
|
|
552
|
+
`;
|
|
553
|
+
drawerBody.appendChild(banner);
|
|
554
|
+
drawerBody.querySelector("#archive-back-btn").addEventListener("click", loadSessionList);
|
|
555
|
+
drawerBody.querySelector("#archive-tools-toggle").addEventListener("click", () => {
|
|
556
|
+
showToolCalls = !showToolCalls;
|
|
557
|
+
renderTranscript(messages, label);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
const container = document.createElement("div");
|
|
561
|
+
container.className = "history-transcript";
|
|
562
|
+
|
|
563
|
+
const relevant = messages.filter((m) => m.role === "user" || m.role === "assistant");
|
|
564
|
+
if (!relevant.length) {
|
|
565
|
+
container.innerHTML = '<div class="history-empty">No chat messages in this session.</div>';
|
|
566
|
+
} else {
|
|
567
|
+
let renderedAny = false;
|
|
568
|
+
for (const msg of relevant) {
|
|
569
|
+
const text = extractMsgText(msg);
|
|
570
|
+
const tools = showToolCalls ? extractToolBlocks(msg) : [];
|
|
571
|
+
if (!text.trim() && tools.length === 0) {
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
renderedAny = true;
|
|
575
|
+
const el = document.createElement("div");
|
|
576
|
+
el.className = `history-msg ${msg.role}`;
|
|
577
|
+
const roleLabel = msg.role === "user" ? "YOU" : "SYMI";
|
|
578
|
+
|
|
579
|
+
let html = `<div class="history-msg-role ${msg.role}">${roleLabel}</div>`;
|
|
580
|
+
if (text.trim()) {
|
|
581
|
+
html += `<div class="history-msg-text">${escHtml(text)}</div>`;
|
|
582
|
+
}
|
|
583
|
+
for (const block of tools) {
|
|
584
|
+
html += renderToolBlock(block);
|
|
585
|
+
}
|
|
586
|
+
el.innerHTML = html;
|
|
587
|
+
container.appendChild(el);
|
|
588
|
+
}
|
|
589
|
+
if (!renderedAny) {
|
|
590
|
+
container.innerHTML = '<div class="history-empty">No chat messages in this session.</div>';
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
drawerBody.appendChild(container);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function extractToolBlocks(msg) {
|
|
598
|
+
if (!Array.isArray(msg.content)) {
|
|
599
|
+
return [];
|
|
600
|
+
}
|
|
601
|
+
const out = [];
|
|
602
|
+
for (const block of msg.content) {
|
|
603
|
+
if (!block || typeof block !== "object") {
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
if (block.type === "tool_use") {
|
|
607
|
+
out.push({
|
|
608
|
+
kind: "use",
|
|
609
|
+
name: typeof block.name === "string" ? block.name : "tool",
|
|
610
|
+
input: block.input ?? null,
|
|
611
|
+
});
|
|
612
|
+
} else if (block.type === "tool_result") {
|
|
613
|
+
let resultText = "";
|
|
614
|
+
if (typeof block.content === "string") {
|
|
615
|
+
resultText = block.content;
|
|
616
|
+
} else if (Array.isArray(block.content)) {
|
|
617
|
+
resultText = block.content
|
|
618
|
+
.filter((c) => c && (c.type === "text" || typeof c === "string"))
|
|
619
|
+
.map((c) => (typeof c === "string" ? c : (c.text ?? "")))
|
|
620
|
+
.join("\n");
|
|
621
|
+
}
|
|
622
|
+
out.push({
|
|
623
|
+
kind: "result",
|
|
624
|
+
isError: block.is_error === true,
|
|
625
|
+
text: resultText,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return out;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function renderToolBlock(block) {
|
|
633
|
+
const MAX_INPUT_CHARS = 400;
|
|
634
|
+
const MAX_OUTPUT_CHARS = 600;
|
|
635
|
+
if (block.kind === "use") {
|
|
636
|
+
let body = "";
|
|
637
|
+
try {
|
|
638
|
+
body = block.input == null ? "" : JSON.stringify(block.input, null, 2);
|
|
639
|
+
} catch {
|
|
640
|
+
body = String(block.input);
|
|
641
|
+
}
|
|
642
|
+
if (body.length > MAX_INPUT_CHARS) {
|
|
643
|
+
body = body.slice(0, MAX_INPUT_CHARS) + "\n… [truncated]";
|
|
644
|
+
}
|
|
645
|
+
return `
|
|
646
|
+
<div class="history-tool-block history-tool-use">
|
|
647
|
+
<div class="history-tool-head">⚙ ${escHtml(block.name)}</div>
|
|
648
|
+
${body ? `<pre class="history-tool-body">${escHtml(body)}</pre>` : ""}
|
|
649
|
+
</div>
|
|
650
|
+
`;
|
|
651
|
+
}
|
|
652
|
+
let text = block.text ?? "";
|
|
653
|
+
if (text.length > MAX_OUTPUT_CHARS) {
|
|
654
|
+
text = text.slice(0, MAX_OUTPUT_CHARS) + "\n… [truncated]";
|
|
655
|
+
}
|
|
656
|
+
const cls = block.isError ? "history-tool-result-error" : "history-tool-result";
|
|
657
|
+
return `
|
|
658
|
+
<div class="history-tool-block ${cls}">
|
|
659
|
+
<div class="history-tool-head">${block.isError ? "✗ tool error" : "✓ tool result"}</div>
|
|
660
|
+
${text ? `<pre class="history-tool-body">${escHtml(text)}</pre>` : ""}
|
|
661
|
+
</div>
|
|
662
|
+
`;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ── Archive toast (shown after /new) ──────────────────────────────────
|
|
666
|
+
window.showArchiveToast = function () {
|
|
667
|
+
if (toastTimer) {
|
|
668
|
+
clearTimeout(toastTimer);
|
|
669
|
+
}
|
|
670
|
+
archiveToast.classList.add("show");
|
|
671
|
+
toastTimer = setTimeout(() => archiveToast.classList.remove("show"), 8000);
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
archiveToastLink.addEventListener("click", () => {
|
|
675
|
+
archiveToast.classList.remove("show");
|
|
676
|
+
openDrawer();
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
// ── Helpers ───────────────────────────────────────────────────────────
|
|
680
|
+
function extractMsgText(msg) {
|
|
681
|
+
if (typeof msg.content === "string") {
|
|
682
|
+
return msg.content;
|
|
683
|
+
}
|
|
684
|
+
if (!Array.isArray(msg.content)) {
|
|
685
|
+
return "";
|
|
686
|
+
}
|
|
687
|
+
return msg.content
|
|
688
|
+
.filter((b) => b.type === "text" || b.type === "thinking")
|
|
689
|
+
.map((b) => b.text ?? b.thinking ?? "")
|
|
690
|
+
.join("\n")
|
|
691
|
+
.trim();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function escHtml(str) {
|
|
695
|
+
return String(str)
|
|
696
|
+
.replace(/&/g, "&")
|
|
697
|
+
.replace(/</g, "<")
|
|
698
|
+
.replace(/>/g, ">")
|
|
699
|
+
.replace(/"/g, """);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function formatTimestamp(iso) {
|
|
703
|
+
if (!iso) {
|
|
704
|
+
return "Unknown";
|
|
705
|
+
}
|
|
706
|
+
const d = new Date(iso);
|
|
707
|
+
if (Number.isNaN(d.getTime())) {
|
|
708
|
+
return "Unknown";
|
|
709
|
+
}
|
|
710
|
+
return d.toLocaleString("en-US", {
|
|
711
|
+
month: "short",
|
|
712
|
+
day: "numeric",
|
|
713
|
+
year: "numeric",
|
|
714
|
+
hour: "2-digit",
|
|
715
|
+
minute: "2-digit",
|
|
716
|
+
hour12: true,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function fmtSize(bytes) {
|
|
721
|
+
if (typeof bytes !== "number" || !Number.isFinite(bytes) || bytes < 0) {
|
|
722
|
+
return "0B";
|
|
723
|
+
}
|
|
724
|
+
if (bytes < 1024) {
|
|
725
|
+
return `${bytes}B`;
|
|
726
|
+
}
|
|
727
|
+
if (bytes < 1024 * 1024) {
|
|
728
|
+
return `${(bytes / 1024).toFixed(0)}KB`;
|
|
729
|
+
}
|
|
730
|
+
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
731
|
+
}
|
|
732
|
+
})();
|