@liguoshuai/pi-web-chat 1.8.9 → 1.9.0
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/docs/CHANGELOG.md +17 -0
- package/package.json +1 -1
- package/public/app.js +126 -19
- package/public/index.html +1 -0
- package/public/style.css +98 -6
- package/server.js +60 -1
package/docs/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.9.0] - 2026-08-29
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **历史对话删除功能 (Session Deletion & Auto Process Cleanup)**:
|
|
14
|
+
- 后端新增 `DELETE /api/session` 接口,支持通过文件路径删除历史 JSONL 会话记录,并带有严格的会话目录越界与路径穿越安全校验。
|
|
15
|
+
- 删除会话时自动清理后端内存元数据缓存,并自动停止与回收当前会话所绑定的常驻后台 Pi 代理子进程。
|
|
16
|
+
- 前端侧边栏会话列表新增垃圾桶删除按钮(桌面端 Hover 显示,移动端常驻展示),点击后带有确认拦截提示,防止误删。
|
|
17
|
+
- 删除当前正在查看/进行的会话时,自动重置为空白新会话并清理 URL 参数与流式状态;删除其他会话时无感刷新会话列表。
|
|
18
|
+
- **左侧栏拖拽缩放宽度特性 (Resizable Sidebar with Dragging)**:
|
|
19
|
+
- 左侧边栏新增右侧边缘拖拽把手(`#sidebarResizer`),支持鼠标及触控指针拖拽自由调节侧边栏宽度(180px - 自适应上限)。
|
|
20
|
+
- 基于 Pointer Capture API 与 `--sidebar-width` 动态 CSS 变量实现 60fps 丝滑拖拽缩放体验,并消除文本误选与拖动延迟。
|
|
21
|
+
- 侧边栏折叠与展开动画完全自适应动态设置的侧边栏宽度,避免折叠时发生截断或残留。
|
|
22
|
+
- 支持双击拖拽把手快速重置回默认宽度(260px),并通过 `localStorage` 自动持久化保存用户自定宽度偏好。
|
|
23
|
+
- 优化移动端响应式,移动端抽屉侧栏下自动禁用并隐藏桌面端拖拽把手。
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
10
27
|
## [1.8.9] - 2026-08-29
|
|
11
28
|
|
|
12
29
|
### Fixed
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -491,6 +491,17 @@ function renderSidebar(sessions) {
|
|
|
491
491
|
sessions.forEach((s) => {
|
|
492
492
|
const title = s.sessionName || s.firstUser || "新对话";
|
|
493
493
|
const when = s.timestamp ? new Date(s.timestamp).toLocaleString("zh-CN", { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" }) : "";
|
|
494
|
+
|
|
495
|
+
const btnDelete = el("button", {
|
|
496
|
+
class: "btn-delete-session",
|
|
497
|
+
title: "删除会话",
|
|
498
|
+
html: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>`,
|
|
499
|
+
onclick: (e) => {
|
|
500
|
+
e.stopPropagation();
|
|
501
|
+
deleteSession(s.file, title);
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
|
|
494
505
|
const item = el("div", {
|
|
495
506
|
class: "session-item" + (s.file === state.currentSessionFile ? " active" : ""),
|
|
496
507
|
dataset: { file: s.file },
|
|
@@ -498,14 +509,57 @@ function renderSidebar(sessions) {
|
|
|
498
509
|
onclick: () => loadSession(s.file),
|
|
499
510
|
}, [
|
|
500
511
|
el("div", { class: "title" }, [
|
|
501
|
-
el("div", { text: title }),
|
|
512
|
+
el("div", { class: "session-item-name", text: title }),
|
|
502
513
|
el("div", { class: "meta", text: `${when} · ${s.messageCount || 0} 条` }),
|
|
503
514
|
]),
|
|
515
|
+
btnDelete,
|
|
504
516
|
]);
|
|
505
517
|
list.appendChild(item);
|
|
506
518
|
});
|
|
507
519
|
}
|
|
508
520
|
|
|
521
|
+
function startNewSession(askConfirm = true) {
|
|
522
|
+
if (state.streaming) {
|
|
523
|
+
if (askConfirm && !confirm("正在生成中,新建会话会终止当前操作,确定吗?")) return;
|
|
524
|
+
abortGeneration();
|
|
525
|
+
}
|
|
526
|
+
clearChat();
|
|
527
|
+
showEmptyState(true);
|
|
528
|
+
state.currentSessionFile = null;
|
|
529
|
+
try {
|
|
530
|
+
window.history.replaceState({}, "", window.location.pathname);
|
|
531
|
+
} catch {}
|
|
532
|
+
connectWs({ explicitNewSession: true }); // no session -> pi creates a new one
|
|
533
|
+
$("#topSessionName").textContent = "新对话";
|
|
534
|
+
// Mobile: close sidebar on new session
|
|
535
|
+
if (window.innerWidth <= 768) closeSidebar();
|
|
536
|
+
refreshSessions();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async function deleteSession(file, title) {
|
|
540
|
+
if (!confirm(`确定要删除此会话记录吗?\n「${title || "新对话"}」\n删除后不可恢复。`)) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
try {
|
|
544
|
+
const res = await fetch(`${API}/api/session?file=${encodeURIComponent(file)}`, {
|
|
545
|
+
method: "DELETE",
|
|
546
|
+
});
|
|
547
|
+
const data = await res.json();
|
|
548
|
+
if (!res.ok || data.error) {
|
|
549
|
+
showToast(`删除失败: ${data.error || "未知错误"}`);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
showToast("会话已删除");
|
|
553
|
+
if (state.currentSessionFile === file) {
|
|
554
|
+
startNewSession(false);
|
|
555
|
+
} else {
|
|
556
|
+
await refreshSessions();
|
|
557
|
+
}
|
|
558
|
+
} catch (err) {
|
|
559
|
+
showToast(`删除失败: ${err.message || "网络错误"}`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
509
563
|
async function toggleSidebar() {
|
|
510
564
|
const app = $(".app");
|
|
511
565
|
const isOpen = app.classList.toggle("sidebar-open");
|
|
@@ -2224,6 +2278,70 @@ function autoResize() {
|
|
|
2224
2278
|
ta.style.height = Math.min(ta.scrollHeight, 220) + "px";
|
|
2225
2279
|
}
|
|
2226
2280
|
|
|
2281
|
+
function initSidebarResize() {
|
|
2282
|
+
const resizer = $("#sidebarResizer");
|
|
2283
|
+
if (!resizer) return;
|
|
2284
|
+
|
|
2285
|
+
// Restore saved width from localStorage
|
|
2286
|
+
const savedWidth = parseInt(localStorage.getItem("sidebarWidth"), 10);
|
|
2287
|
+
if (savedWidth && savedWidth >= 180 && savedWidth <= 800) {
|
|
2288
|
+
document.documentElement.style.setProperty("--sidebar-width", `${savedWidth}px`);
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
let isDragging = false;
|
|
2292
|
+
let startX = 0;
|
|
2293
|
+
let startWidth = 260;
|
|
2294
|
+
|
|
2295
|
+
resizer.addEventListener("pointerdown", (e) => {
|
|
2296
|
+
if (e.button !== 0) return; // Only primary button
|
|
2297
|
+
if (window.innerWidth <= 768) return; // Ignore on mobile
|
|
2298
|
+
|
|
2299
|
+
isDragging = true;
|
|
2300
|
+
startX = e.clientX;
|
|
2301
|
+
const currentWidth = parseInt(getComputedStyle(document.documentElement).getPropertyValue("--sidebar-width"), 10) || $(".sidebar")?.offsetWidth || 260;
|
|
2302
|
+
startWidth = currentWidth;
|
|
2303
|
+
|
|
2304
|
+
resizer.setPointerCapture(e.pointerId);
|
|
2305
|
+
document.body.classList.add("is-resizing");
|
|
2306
|
+
e.preventDefault();
|
|
2307
|
+
});
|
|
2308
|
+
|
|
2309
|
+
resizer.addEventListener("pointermove", (e) => {
|
|
2310
|
+
if (!isDragging) return;
|
|
2311
|
+
const deltaX = e.clientX - startX;
|
|
2312
|
+
let newWidth = startWidth + deltaX;
|
|
2313
|
+
|
|
2314
|
+
const minWidth = 180;
|
|
2315
|
+
const maxWidth = Math.min(650, Math.max(300, window.innerWidth - 250));
|
|
2316
|
+
newWidth = Math.max(minWidth, Math.min(newWidth, maxWidth));
|
|
2317
|
+
|
|
2318
|
+
document.documentElement.style.setProperty("--sidebar-width", `${newWidth}px`);
|
|
2319
|
+
});
|
|
2320
|
+
|
|
2321
|
+
const endDrag = (e) => {
|
|
2322
|
+
if (!isDragging) return;
|
|
2323
|
+
isDragging = false;
|
|
2324
|
+
try {
|
|
2325
|
+
resizer.releasePointerCapture(e.pointerId);
|
|
2326
|
+
} catch {}
|
|
2327
|
+
document.body.classList.remove("is-resizing");
|
|
2328
|
+
|
|
2329
|
+
const finalWidth = parseInt(getComputedStyle(document.documentElement).getPropertyValue("--sidebar-width"), 10);
|
|
2330
|
+
if (finalWidth) {
|
|
2331
|
+
localStorage.setItem("sidebarWidth", finalWidth);
|
|
2332
|
+
}
|
|
2333
|
+
};
|
|
2334
|
+
|
|
2335
|
+
resizer.addEventListener("pointerup", endDrag);
|
|
2336
|
+
resizer.addEventListener("pointercancel", endDrag);
|
|
2337
|
+
|
|
2338
|
+
// Double-click to reset width to default 260px
|
|
2339
|
+
resizer.addEventListener("dblclick", () => {
|
|
2340
|
+
document.documentElement.style.setProperty("--sidebar-width", "260px");
|
|
2341
|
+
localStorage.removeItem("sidebarWidth");
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2227
2345
|
// ---- Init ----
|
|
2228
2346
|
async function init() {
|
|
2229
2347
|
// Default cwd to home (server uses home default too).
|
|
@@ -2233,23 +2351,7 @@ async function init() {
|
|
|
2233
2351
|
await loadServerConfig();
|
|
2234
2352
|
|
|
2235
2353
|
// event listeners
|
|
2236
|
-
$("#btnNew").addEventListener("click", () =>
|
|
2237
|
-
if (state.streaming) {
|
|
2238
|
-
if (!confirm("正在生成中,新建会话会终止当前操作,确定吗?")) return;
|
|
2239
|
-
abortGeneration();
|
|
2240
|
-
}
|
|
2241
|
-
clearChat();
|
|
2242
|
-
showEmptyState(true);
|
|
2243
|
-
state.currentSessionFile = null;
|
|
2244
|
-
try {
|
|
2245
|
-
window.history.replaceState({}, "", window.location.pathname);
|
|
2246
|
-
} catch {}
|
|
2247
|
-
connectWs({ explicitNewSession: true }); // no session -> pi creates a new one
|
|
2248
|
-
$("#topSessionName").textContent = "新对话";
|
|
2249
|
-
// Mobile: close sidebar on new session
|
|
2250
|
-
if (window.innerWidth <= 768) closeSidebar();
|
|
2251
|
-
refreshSessions();
|
|
2252
|
-
});
|
|
2354
|
+
$("#btnNew").addEventListener("click", () => startNewSession(true));
|
|
2253
2355
|
|
|
2254
2356
|
$("#sendBtn").addEventListener("click", () => {
|
|
2255
2357
|
if (state.streaming) {
|
|
@@ -2348,7 +2450,9 @@ async function init() {
|
|
|
2348
2450
|
$("#sidebarSearch").addEventListener("input", (e) => {
|
|
2349
2451
|
const q = e.target.value.toLowerCase();
|
|
2350
2452
|
document.querySelectorAll(".session-item").forEach((it) => {
|
|
2351
|
-
|
|
2453
|
+
const titleEl = it.querySelector(".title");
|
|
2454
|
+
const text = titleEl ? titleEl.textContent.toLowerCase() : it.textContent.toLowerCase();
|
|
2455
|
+
it.style.display = text.includes(q) ? "" : "none";
|
|
2352
2456
|
});
|
|
2353
2457
|
});
|
|
2354
2458
|
|
|
@@ -2479,6 +2583,9 @@ async function init() {
|
|
|
2479
2583
|
// Mobile: floating button to jump back to the toolbar after long scrolls
|
|
2480
2584
|
initMobileToolbarFab();
|
|
2481
2585
|
|
|
2586
|
+
// Sidebar resizer
|
|
2587
|
+
initSidebarResize();
|
|
2588
|
+
|
|
2482
2589
|
refreshSessions();
|
|
2483
2590
|
// start in the disconnected state; connectWs will flip to green on open.
|
|
2484
2591
|
const initDot = $("#connDot");
|
package/public/index.html
CHANGED
package/public/style.css
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
:root {
|
|
2
|
+
--sidebar-width: 260px;
|
|
2
3
|
--bg: #212121;
|
|
3
4
|
--bg-sidebar: #171717;
|
|
4
5
|
--bg-hover: #2a2a2a;
|
|
@@ -42,7 +43,7 @@ body {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
.sidebar {
|
|
45
|
-
width:
|
|
46
|
+
width: var(--sidebar-width);
|
|
46
47
|
flex-shrink: 0;
|
|
47
48
|
background: var(--bg-sidebar);
|
|
48
49
|
border-right: 1px solid var(--border);
|
|
@@ -52,11 +53,12 @@ body {
|
|
|
52
53
|
transition: margin-left 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
53
54
|
z-index: 100;
|
|
54
55
|
overflow: hidden;
|
|
56
|
+
position: relative;
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
/* Sidebar collapsed state on desktop */
|
|
58
60
|
.app:not(.sidebar-open) .sidebar {
|
|
59
|
-
margin-left: -
|
|
61
|
+
margin-left: calc(-1 * var(--sidebar-width));
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
.main {
|
|
@@ -117,23 +119,103 @@ body {
|
|
|
117
119
|
padding: 4px 8px 16px;
|
|
118
120
|
}
|
|
119
121
|
.session-item {
|
|
120
|
-
padding:
|
|
122
|
+
padding: 8px 10px;
|
|
121
123
|
border-radius: 10px;
|
|
122
124
|
cursor: pointer;
|
|
123
125
|
font-size: 13px;
|
|
124
126
|
color: var(--text);
|
|
125
127
|
display: flex;
|
|
126
|
-
align-items:
|
|
128
|
+
align-items: center;
|
|
129
|
+
justify-content: space-between;
|
|
127
130
|
gap: 8px;
|
|
128
131
|
margin-bottom: 2px;
|
|
129
132
|
transition: background .12s;
|
|
130
133
|
word-break: break-word;
|
|
134
|
+
position: relative;
|
|
131
135
|
}
|
|
132
136
|
.session-item:hover { background: var(--bg-hover); }
|
|
133
137
|
.session-item.active { background: var(--bg-hover); }
|
|
134
|
-
.session-item .title {
|
|
138
|
+
.session-item .title {
|
|
139
|
+
flex: 1;
|
|
140
|
+
min-width: 0;
|
|
141
|
+
line-height: 1.35;
|
|
142
|
+
}
|
|
143
|
+
.session-item .session-item-name {
|
|
144
|
+
overflow: hidden;
|
|
145
|
+
text-overflow: ellipsis;
|
|
146
|
+
white-space: nowrap;
|
|
147
|
+
}
|
|
135
148
|
.session-item .meta {
|
|
136
|
-
font-size: 11px;
|
|
149
|
+
font-size: 11px;
|
|
150
|
+
color: var(--text-dim);
|
|
151
|
+
margin-top: 2px;
|
|
152
|
+
overflow: hidden;
|
|
153
|
+
text-overflow: ellipsis;
|
|
154
|
+
white-space: nowrap;
|
|
155
|
+
}
|
|
156
|
+
.btn-delete-session {
|
|
157
|
+
display: inline-flex;
|
|
158
|
+
align-items: center;
|
|
159
|
+
justify-content: center;
|
|
160
|
+
width: 26px;
|
|
161
|
+
height: 26px;
|
|
162
|
+
padding: 0;
|
|
163
|
+
border: none;
|
|
164
|
+
background: transparent;
|
|
165
|
+
color: var(--text-dim);
|
|
166
|
+
border-radius: 6px;
|
|
167
|
+
cursor: pointer;
|
|
168
|
+
opacity: 0;
|
|
169
|
+
flex-shrink: 0;
|
|
170
|
+
transition: opacity 0.15s ease, color 0.15s ease, background-color 0.15s ease;
|
|
171
|
+
}
|
|
172
|
+
.session-item:hover .btn-delete-session,
|
|
173
|
+
.session-item:focus-within .btn-delete-session {
|
|
174
|
+
opacity: 1;
|
|
175
|
+
}
|
|
176
|
+
.btn-delete-session:hover {
|
|
177
|
+
color: var(--danger, #ef4444);
|
|
178
|
+
background: rgba(239, 68, 68, 0.15);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* Sidebar Resizer */
|
|
182
|
+
.sidebar-resizer {
|
|
183
|
+
position: absolute;
|
|
184
|
+
top: 0;
|
|
185
|
+
right: 0;
|
|
186
|
+
bottom: 0;
|
|
187
|
+
width: 6px;
|
|
188
|
+
cursor: col-resize;
|
|
189
|
+
z-index: 102;
|
|
190
|
+
user-select: none;
|
|
191
|
+
transition: background-color 0.15s ease;
|
|
192
|
+
}
|
|
193
|
+
.sidebar-resizer::after {
|
|
194
|
+
content: "";
|
|
195
|
+
position: absolute;
|
|
196
|
+
top: 0;
|
|
197
|
+
right: 0;
|
|
198
|
+
width: 2px;
|
|
199
|
+
height: 100%;
|
|
200
|
+
background: transparent;
|
|
201
|
+
transition: background-color 0.15s ease;
|
|
202
|
+
}
|
|
203
|
+
.sidebar-resizer:hover::after,
|
|
204
|
+
.is-resizing .sidebar-resizer::after {
|
|
205
|
+
background: var(--accent);
|
|
206
|
+
}
|
|
207
|
+
.sidebar-resizer:hover,
|
|
208
|
+
.is-resizing .sidebar-resizer {
|
|
209
|
+
background: rgba(16, 163, 127, 0.12);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.is-resizing,
|
|
213
|
+
.is-resizing * {
|
|
214
|
+
user-select: none !important;
|
|
215
|
+
cursor: col-resize !important;
|
|
216
|
+
}
|
|
217
|
+
.is-resizing .sidebar {
|
|
218
|
+
transition: none !important;
|
|
137
219
|
}
|
|
138
220
|
.sidebar-empty { color: var(--text-dim); font-size: 13px; padding: 20px 12px; text-align: center; }
|
|
139
221
|
|
|
@@ -1217,6 +1299,16 @@ body {
|
|
|
1217
1299
|
z-index: 105;
|
|
1218
1300
|
}
|
|
1219
1301
|
|
|
1302
|
+
.sidebar-resizer {
|
|
1303
|
+
display: none !important;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
.btn-delete-session {
|
|
1307
|
+
opacity: 0.7;
|
|
1308
|
+
width: 30px;
|
|
1309
|
+
height: 30px;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1220
1312
|
.sidebar-bottom {
|
|
1221
1313
|
flex-shrink: 0;
|
|
1222
1314
|
padding: 12px 14px;
|
package/server.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// for listing sessions and reading session history from the JSONL store.
|
|
4
4
|
import { spawn } from "child_process";
|
|
5
5
|
import { randomUUID } from "crypto";
|
|
6
|
-
import { readFile, readdir, stat, writeFile, mkdir, realpath as fsRealpath } from "fs/promises";
|
|
6
|
+
import { readFile, readdir, stat, writeFile, mkdir, realpath as fsRealpath, unlink } from "fs/promises";
|
|
7
7
|
import { readFileSync, existsSync } from "fs";
|
|
8
8
|
import { StringDecoder } from "string_decoder";
|
|
9
9
|
import express from "express";
|
|
@@ -853,6 +853,65 @@ app.get("/api/session", async (req, res) => {
|
|
|
853
853
|
}
|
|
854
854
|
});
|
|
855
855
|
|
|
856
|
+
// Delete a session file from disk and clean up in-memory caches / active agents
|
|
857
|
+
app.delete("/api/session", async (req, res) => {
|
|
858
|
+
try {
|
|
859
|
+
const file = req.query.file || req.body?.file;
|
|
860
|
+
if (!file || !file.endsWith(".jsonl")) {
|
|
861
|
+
return res.status(400).json({ error: "bad file" });
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const resolvedSessionsDir = normalizePath(SESSIONS_DIR);
|
|
865
|
+
const requestedPath = normalizePath(file);
|
|
866
|
+
const relPath = path.relative(resolvedSessionsDir, requestedPath);
|
|
867
|
+
if (relPath.startsWith("..") || path.isAbsolute(relPath)) {
|
|
868
|
+
return res.status(403).json({ error: "Access denied" });
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
let resolvedFile;
|
|
872
|
+
try {
|
|
873
|
+
resolvedFile = await fsRealpath(requestedPath);
|
|
874
|
+
} catch (err) {
|
|
875
|
+
if (err.code === "ENOENT") {
|
|
876
|
+
return res.status(404).json({ error: "File not found" });
|
|
877
|
+
}
|
|
878
|
+
resolvedFile = requestedPath;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
let canonicalSessionsDir = resolvedSessionsDir;
|
|
882
|
+
try {
|
|
883
|
+
canonicalSessionsDir = await fsRealpath(resolvedSessionsDir);
|
|
884
|
+
} catch {}
|
|
885
|
+
|
|
886
|
+
const realRel = path.relative(canonicalSessionsDir, resolvedFile);
|
|
887
|
+
if (realRel.startsWith("..") || path.isAbsolute(realRel)) {
|
|
888
|
+
return res.status(403).json({ error: "Access denied" });
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Stop and unpool any active PiAgent managing this session
|
|
892
|
+
for (const [key, agent] of activeAgents.entries()) {
|
|
893
|
+
if (key.endsWith(`:${requestedPath}`) || key.endsWith(`:${resolvedFile}`)) {
|
|
894
|
+
activeAgents.delete(key);
|
|
895
|
+
try {
|
|
896
|
+
agent.stop();
|
|
897
|
+
} catch {}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Invalidate session cache
|
|
902
|
+
sessionMetadataCache.delete(requestedPath);
|
|
903
|
+
sessionMetadataCache.delete(resolvedFile);
|
|
904
|
+
|
|
905
|
+
// Delete the session JSONL file
|
|
906
|
+
await unlink(resolvedFile);
|
|
907
|
+
|
|
908
|
+
res.json({ success: true, file: requestedPath });
|
|
909
|
+
} catch (e) {
|
|
910
|
+
console.error("Delete session error:", e);
|
|
911
|
+
res.status(500).json({ error: String(e) });
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
|
|
856
915
|
// ---- WebSocket: 1 browser conn = 1 pi RPC conn (with process persistence) ----
|
|
857
916
|
const httpServer = app.listen(PORT, () => {
|
|
858
917
|
console.log(`pi-web-chat on http://localhost:${PORT}`);
|