@liguoshuai/pi-web-chat 1.8.8 → 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 CHANGED
@@ -7,6 +7,37 @@ 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
+
27
+ ## [1.8.9] - 2026-08-29
28
+
29
+ ### Fixed
30
+ - **Markdown 下划线变量与标识符渲染保护 (Markdown Identifier & Underscore Parsing Protection)**:
31
+ - 优化行内 Markdown 解析器,限定下划线 `_` 仅在非单词边界(空格/标点包裹)时触发斜体,防止编程标识符与文件名(如 `user_id_list`、`process.env.NODE_ENV`)被误切分解析为斜体。
32
+ - 优化加粗与嵌套斜体(`**bold and *italic***`)的非贪婪匹配规则。
33
+ - **移动端模型选择菜单支持“设为默认” (Mobile Model Selector 'Set Default' Accessibility)**:
34
+ - 在移动端媒体查询下保持 `.btn-set-default` 可见(`opacity: 1`),修复手机端因缺少 hover 无法将模型设为全局默认的问题。
35
+ - **工具调用头部与顶栏弹性布局溢出保护 (Tool Block & Topbar Flex Overflow Protection)**:
36
+ - 给 `.tool-head .args` 增加 `min-width: 0`,防止长命令/长文件路径将右侧“复制”按钮与“执行状态”标签挤出卡片。
37
+ - 给桌面端 `.topbar .session-name` 增加 `min-width: 0`,防止窄屏下超长会话标题破坏顶栏胶囊布局。
38
+
39
+ ---
40
+
10
41
  ## [1.8.8] - 2026-08-29
11
42
 
12
43
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liguoshuai/pi-web-chat",
3
- "version": "1.8.8",
3
+ "version": "1.9.0",
4
4
  "description": "A ChatGPT/Gemini-style web UI for the pi coding agent, powered by pi's RPC mode.",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/public/app.js CHANGED
@@ -422,16 +422,16 @@ function renderInlineMd(text) {
422
422
 
423
423
  // helper state bag attached to the function during line scan
424
424
  function mdInlineBlock(text) {
425
- function esc(s) { return escapeHtml(s); }
426
425
  let s = escapeHtml(text);
427
426
  // bold
428
- s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
429
- s = s.replace(/__([^_]+)__/g, "<strong>$1</strong>");
427
+ s = s.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
428
+ s = s.replace(/__(.+?)__/g, "<strong>$1</strong>");
430
429
  // strikethrough
431
- s = s.replace(/~~([^~]+)~~/g, "<del>$1</del>");
432
- // italic
433
- s = s.replace(/(^|[^*])\*([^*]+)\*/g, "$1<em>$2</em>");
434
- s = s.replace(/(^|[^_])_([^_]+)_/g, "$1<em>$2</em>");
430
+ s = s.replace(/~~(.+?)~~/g, "<del>$1</del>");
431
+ // italic (asterisk)
432
+ s = s.replace(/(^|[^*])\*([^*\s](?:[^*]*?[^*\s])?)\*(?!\*)/g, "$1<em>$2</em>");
433
+ // italic (underscore): only match boundary/space so identifier_names are preserved
434
+ s = s.replace(/(^|[\s(\[<,;:])_([^_\s](?:[^_]*?[^_\s])?)_(?=[\s)\]>,;:!?.]|$)/g, "$1<em>$2</em>");
435
435
  // links [txt](url)
436
436
  s = s.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
437
437
  return s;
@@ -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
- it.style.display = it.textContent.toLowerCase().includes(q) ? "" : "none";
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
@@ -28,6 +28,7 @@
28
28
  <span id="appVersion" class="app-version" title="pi-web-chat 版本"></span>
29
29
  </div>
30
30
  </div>
31
+ <div class="sidebar-resizer" id="sidebarResizer" title="拖拽调整侧边栏宽度,双击恢复默认"></div>
31
32
  </aside>
32
33
 
33
34
  <!-- Sidebar Overlay -->
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: 260px;
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: -260px;
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: 9px 10px;
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: flex-start;
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 { flex: 1; line-height: 1.35; }
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; color: var(--text-dim); margin-top: 3px;
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
 
@@ -202,6 +284,7 @@ body {
202
284
  color: var(--text);
203
285
  font-weight: 500;
204
286
  flex: 1;
287
+ min-width: 0;
205
288
  white-space: nowrap;
206
289
  overflow: hidden;
207
290
  text-overflow: ellipsis;
@@ -496,7 +579,7 @@ body {
496
579
  }
497
580
  .tool-head .ic { color: var(--accent); font-size: 12px; }
498
581
  .tool-head .name { font-weight: 600; color: var(--text); }
499
- .tool-head .args { color: var(--text-muted); font-family: monospace; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
582
+ .tool-head .args { color: var(--text-muted); font-family: monospace; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
500
583
  .tool-head .state { font-size: 11px; color: var(--text-dim); padding: 2px 8px; background: #2a2a2a; border-radius: 999px; }
501
584
  .tool-head .state.error { color: var(--danger); background: #3a1a1a; }
502
585
  .tool-body {
@@ -1216,6 +1299,16 @@ body {
1216
1299
  z-index: 105;
1217
1300
  }
1218
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
+
1219
1312
  .sidebar-bottom {
1220
1313
  flex-shrink: 0;
1221
1314
  padding: 12px 14px;
@@ -1365,6 +1458,11 @@ body {
1365
1458
  .model-menu-list {
1366
1459
  max-height: calc(100dvh - 130px);
1367
1460
  }
1461
+ .btn-set-default {
1462
+ opacity: 1;
1463
+ font-size: 10px;
1464
+ padding: 2px 6px;
1465
+ }
1368
1466
 
1369
1467
  .empty-model-banner {
1370
1468
  padding: 10px 12px;
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}`);