@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 +31 -0
- package/package.json +1 -1
- package/public/app.js +133 -26
- package/public/index.html +1 -0
- package/public/style.css +105 -7
- package/server.js +60 -1
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
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(/\*\*(
|
|
429
|
-
s = s.replace(/__(
|
|
427
|
+
s = s.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
|
428
|
+
s = s.replace(/__(.+?)__/g, "<strong>$1</strong>");
|
|
430
429
|
// strikethrough
|
|
431
|
-
s = s.replace(/~~(
|
|
432
|
-
// italic
|
|
433
|
-
s = s.replace(/(^|[^*])\*([^*]
|
|
434
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -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}`);
|