@liguoshuai/pi-web-chat 1.8.0 → 1.8.3
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/ARCHITECTURE.md +1 -1
- package/docs/CHANGELOG.md +32 -0
- package/docs/ISSUES.md +66 -0
- package/package.json +1 -1
- package/public/app.js +28 -5
- package/public/style.css +116 -17
- package/server.js +45 -15
package/docs/ARCHITECTURE.md
CHANGED
package/docs/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.8.3] - 2026-07-26
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **移动端工作目录切换弹窗按钮溢出修复 (CWD Modal Mobile Overflow & Input Sizing)**:
|
|
14
|
+
- 修复手机端切换工作目录弹窗中“确定切换”按钮超出卡片右侧边框的问题。
|
|
15
|
+
- 为 `.input-group input` 添加 `min-width: 0` 和 `box-sizing: border-box`,消除原生 input 元素的内在最小宽度导致的 Flexbox 溢出。
|
|
16
|
+
- 适配移动端弹窗内边距与间距(padding 降至 16px),并在 `<= 380px` 窄屏设备上自适应将按钮折行全宽呈现,防止水平溢出。
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## [1.8.2] - 2026-07-26
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- **移动端顶栏空间挤压与思考胶囊截断修复 (Mobile Topbar Layout & Thinking Pill Overflow)**:
|
|
24
|
+
- 移除移动端 `.model-pill-container` 的固定 `max-width: 130px` 宽度限制,修复深度思考胶囊被挤压截断、只显示一半图标的问题。
|
|
25
|
+
- 优化移动端顶栏各元素间距与自适应弹性缩放:侧边栏菜单按钮调整为 36px,工作目录胶囊最大宽度限制为 75px,模型胶囊适度收敛文字并隐藏默认徽章,思考胶囊保留完整图标与级别文本。
|
|
26
|
+
- 增加对 `<= 360px` 极窄小屏设备的精细化响应式适配规则,保障全尺寸移动端设备顶部操作栏整齐美观且不换行、不溢出。
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## [1.8.1] - 2026-07-26
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- **模型切换系统通知未定义函数报错修复 (Missing appendSystemNotice ReferenceError)**:补充前端 `appendSystemNotice` 函数实现,使用 `.system-notice-divider` 与 `.system-notice-text` 渲染模型变更系统提示,解决切换模型时触发 `ReferenceError` 的问题。
|
|
34
|
+
- **思考深度循环切换响应数据字段适配 (cycle_thinking_level Response Handling)**:兼容 Pi RPC 响应中返回的 `data.level` 字段,避免因字段名不匹配导致思考深度未能及时更新。
|
|
35
|
+
- **会话自定义命名解析与侧边栏即时呈现 (Session Name & session_info Extraction)**:服务端 `/api/sessions` 与 `/api/session` 增加对 `session_info` 记录的解析,支持读取用户通过 `set_session_name` 设置的会话名称并在侧边栏和顶栏直观显示。
|
|
36
|
+
- **默认模型设为已启用模型列表防回退保护 (enabledModels Sync on Set Default)**:在写入 `defaultModel` 时同步检查并加入 `enabledModels` 列表,防止 Pi core 启动时回退至 `enabledModels[0]`。
|
|
37
|
+
- **会话历史加载异常容错与提示 (Session Load Error Handling)**:`loadSession` 增加网络及文件读取异常判断与 Toast 友好提示,避免读取异常会话文件时界面卡死。
|
|
38
|
+
- **Systemd User 单元文件权限沙箱配置优化 (Systemd Unit Workspace Access)**:移除 `ProtectHome=read-only`,确保作为用户级服务启动时 Pi Agent 可以正常读写和编辑工作区项目文件。
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
10
42
|
## [1.8.0] - 2026-07-26
|
|
11
43
|
|
|
12
44
|
### Added
|
package/docs/ISSUES.md
CHANGED
|
@@ -146,5 +146,71 @@
|
|
|
146
146
|
- 修改 `detachWs`,当所有连接断开但 Agent 仍在生成中时,保留 `eventBuffer` 保证继续录制。
|
|
147
147
|
- 修改 `replayBuffered`,在 Agent 仍处于 busy 状态时消费后不清空缓存,使多个设备/多个 Tab 可多次或同时连入并安全回溯追平全部生成细节,并在流式终止时通过 `agent_settled` 进行最终的统一清理。
|
|
148
148
|
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 14. 切换模型时触发 `appendSystemNotice is not defined` 报错
|
|
152
|
+
|
|
153
|
+
**症状**:在顶部模型下拉菜单中切换模型后,控制台报错 `ReferenceError: appendSystemNotice is not defined`,并在 `/api/log-error` 中记录错误日志。
|
|
154
|
+
|
|
155
|
+
**根因**:前端 `app.js` 在处理模型切换和接收 `set_model` 响应时调用了 `appendSystemNotice` 函数在消息流中插入切换提醒,但在重构时该辅助函数未被声明。
|
|
156
|
+
|
|
157
|
+
**修复**:在 `app.js` 中补充 `appendSystemNotice(text)` 实现,利用 CSS 中已定义好的 `.system-notice-divider` 与 `.system-notice-text` 类在聊天区尾部安全插入居中分割标签。
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## 15. `cycle_thinking_level` 响应字段未正确更新思考深度
|
|
162
|
+
|
|
163
|
+
**症状**:通过 RPC 调用循环切换思考级别后,前端状态与顶栏思考胶囊未实时更新。
|
|
164
|
+
|
|
165
|
+
**根因**:Pi RPC 模式下 `cycle_thinking_level` 返回的响应数据为 `{ level: ThinkingLevel }`,而前端代码按 `obj.data?.thinkingLevel` 读取导致取得 `undefined`。
|
|
166
|
+
|
|
167
|
+
**修复**:在 `app.js` 中改为 `const newLevel = obj.data?.level || obj.data?.thinkingLevel;`,兼容标准 RPC 返回格式。
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## 16. 自定义会话名称 (`session_info`) 无法在侧边栏与顶栏显示
|
|
172
|
+
|
|
173
|
+
**症状**:通过 `set_session_name` 重命名会话后,侧边栏列表中依然只展示第一条用户 prompt 提取出的简略标题。
|
|
174
|
+
|
|
175
|
+
**根因**:服务端 `getSessionMetadata` 和 `/api/session` 在逐行扫描 `.jsonl` 时只查找了 `type === "session"` 和 `type === "message"`,忽略了 `type === "session_info"` 记录。
|
|
176
|
+
|
|
177
|
+
**修复**:
|
|
178
|
+
- 服务端解析 `.jsonl` 时提取最新的 `session_info.name` 作为 `sessionName` 返回。
|
|
179
|
+
- 前端 `renderSidebar` 优先使用 `s.sessionName || s.firstUser || "新对话"` 进行渲染。
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## 17. Systemd 用户服务中 `ProtectHome=read-only` 导致 Agent 读写工作区报错
|
|
184
|
+
|
|
185
|
+
**症状**:通过 `install-service.sh` 将服务安装至 systemd user 后,Pi 编程代理在用户项目目录中执行 `write` 或 `edit` 工具时触发 `EACCES` 权限拒绝错误。
|
|
186
|
+
|
|
187
|
+
**根因**:服务单元模板设置了 `ProtectHome=read-only` 并仅将 `~/.pi` 和 `~/.npm-global` 列入白名单,导致 Agent 无法在 `$HOME/projects` 等工作区目录下创建或修改文件。
|
|
188
|
+
|
|
189
|
+
**修复**:移除该限制,让 User-level 服务在常规用户权限上下文下直接操作工作区。
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## 19. 移动端切换工作目录弹窗按钮超出边框
|
|
194
|
+
|
|
195
|
+
**症状**:在手机端点击顶栏切换工作目录(CWD Modal)时,右侧的“确定切换”按钮超出了弹窗白色/暗色卡片右边框。
|
|
196
|
+
|
|
197
|
+
**根因**:
|
|
198
|
+
1. 原生 `<input>` 元素具有默认的内在 `size`(`min-width: auto` 相当于约 180px~200px)。在小屏手机上,input 的内在最小宽度加上“确定切换”按钮宽度和弹窗内边距超出了视口总可用宽度。
|
|
199
|
+
2. 移动端没有针对 `.modal` 和 `.input-group` 进行专门的响应式约束和内边距收敛。
|
|
200
|
+
|
|
201
|
+
**修复**:
|
|
202
|
+
- 给 `.input-group input` 添加 `min-width: 0; box-sizing: border-box;`,确保其在 flex 容器中可以自由收缩。
|
|
203
|
+
- 在移动端媒体查询中调整弹窗内边距为 16px,优化输入框与按钮的 padding。
|
|
204
|
+
- 在 `<= 380px` 超窄屏设备上自适应将输入框与操作按钮上下垂直排列(`flex-direction: column`),确保彻底杜绝横向溢出。
|
|
205
|
+
|
|
206
|
+
**症状**:在手机端竖屏打开页面时,顶栏右侧的“深度思考 / 推理级别”(`🧠 High`)胶囊被挤压,只能看到一半的脑花图标,文本完全不可见。
|
|
207
|
+
|
|
208
|
+
**根因**:移动端媒体查询中为 `.model-pill-container` 设置了 `max-width: 130px` 的硬限制。由于模型胶囊占据了大部分宽度,容器剩余宽度不足以完整容纳思考胶囊,导致其被裁剪。
|
|
209
|
+
|
|
210
|
+
**修复**:
|
|
211
|
+
- 移除移动端 `.model-pill-container` 的 `max-width: 130px` 限制,改为弹性无约束容器。
|
|
212
|
+
- 优化顶栏各按钮尺寸与间距:缩小菜单按钮为 36px,工作目录胶囊限制最大 75px,模型胶囊适度缩略模型名并隐藏手机端冗余的“默认”徽章,为思考胶囊留出充足显示空间。
|
|
213
|
+
- 追加 `<= 360px` 极窄屏响应式规则,确保在 iPhone SE 等小屏设备上也能完整显示。
|
|
214
|
+
|
|
149
215
|
|
|
150
216
|
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -465,7 +465,7 @@ function renderSidebar(sessions) {
|
|
|
465
465
|
return;
|
|
466
466
|
}
|
|
467
467
|
sessions.forEach((s) => {
|
|
468
|
-
const title = s.firstUser || "新对话";
|
|
468
|
+
const title = s.sessionName || s.firstUser || "新对话";
|
|
469
469
|
const when = s.timestamp ? new Date(s.timestamp).toLocaleString("zh-CN", { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" }) : "";
|
|
470
470
|
const item = el("div", {
|
|
471
471
|
class: "session-item" + (s.file === state.currentSessionFile ? " active" : ""),
|
|
@@ -533,11 +533,21 @@ async function loadSession(file) {
|
|
|
533
533
|
const res = await fetch(`${API}/api/session?file=${encodeURIComponent(file)}`);
|
|
534
534
|
const data = await res.json();
|
|
535
535
|
|
|
536
|
+
if (!res.ok || data.error) {
|
|
537
|
+
showToast(`加载会话失败: ${data.error || "无法读取会话文件"}`);
|
|
538
|
+
showEmptyState(true);
|
|
539
|
+
state.currentSessionFile = null;
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
536
543
|
if (data.model) {
|
|
537
544
|
state.currentModel = data.model;
|
|
538
545
|
renderModelPill();
|
|
539
546
|
}
|
|
540
547
|
|
|
548
|
+
const topName = data.sessionName || (data.header?.id ? baseName(file) : "新对话");
|
|
549
|
+
$("#topSessionName").textContent = topName;
|
|
550
|
+
|
|
541
551
|
clearChat();
|
|
542
552
|
document.querySelector("#emptyState").style.display = "none";
|
|
543
553
|
const chat = $("#chat-inner");
|
|
@@ -601,11 +611,22 @@ function extractContentText(content) {
|
|
|
601
611
|
if (typeof content === "string") return content;
|
|
602
612
|
if (!Array.isArray(content)) return "";
|
|
603
613
|
return content
|
|
604
|
-
.filter(c => c.type === "text" || typeof c === "string")
|
|
605
|
-
.map(c => typeof c === "string" ? c : c.text)
|
|
614
|
+
.filter(c => c && (c.type === "text" || typeof c === "string"))
|
|
615
|
+
.map(c => typeof c === "string" ? c : (c.text || ""))
|
|
606
616
|
.join("");
|
|
607
617
|
}
|
|
608
618
|
|
|
619
|
+
function appendSystemNotice(text) {
|
|
620
|
+
if (!text) return;
|
|
621
|
+
const chatInner = $("#chat-inner");
|
|
622
|
+
if (!chatInner) return;
|
|
623
|
+
const node = el("div", { class: "system-notice-divider" }, [
|
|
624
|
+
el("span", { class: "system-notice-text", text })
|
|
625
|
+
]);
|
|
626
|
+
chatInner.appendChild(node);
|
|
627
|
+
scrollBottom();
|
|
628
|
+
}
|
|
629
|
+
|
|
609
630
|
// ---- Chat rendering ----
|
|
610
631
|
function clearChat() {
|
|
611
632
|
const chatInner = $("#chat-inner");
|
|
@@ -1213,8 +1234,9 @@ function handlePiMessage(obj) {
|
|
|
1213
1234
|
}
|
|
1214
1235
|
}
|
|
1215
1236
|
else if (obj.command === "cycle_thinking_level" && obj.success) {
|
|
1216
|
-
|
|
1217
|
-
|
|
1237
|
+
const newLevel = obj.data?.level || obj.data?.thinkingLevel;
|
|
1238
|
+
if (newLevel) {
|
|
1239
|
+
state.thinkingLevel = newLevel;
|
|
1218
1240
|
renderThinkingPill();
|
|
1219
1241
|
updateEmptyStateModelInfo();
|
|
1220
1242
|
}
|
|
@@ -1264,6 +1286,7 @@ function handlePiMessage(obj) {
|
|
|
1264
1286
|
finalizeStreamingMsg();
|
|
1265
1287
|
state.streaming = false;
|
|
1266
1288
|
setComposerAborting(false);
|
|
1289
|
+
sendWs({ type: "get_state" });
|
|
1267
1290
|
refreshSessions(); // titles may have changed
|
|
1268
1291
|
break;
|
|
1269
1292
|
case "message_start": {
|
package/public/style.css
CHANGED
|
@@ -811,9 +811,12 @@ body {
|
|
|
811
811
|
.input-group {
|
|
812
812
|
display: flex;
|
|
813
813
|
gap: 8px;
|
|
814
|
+
width: 100%;
|
|
815
|
+
box-sizing: border-box;
|
|
814
816
|
}
|
|
815
817
|
.input-group input {
|
|
816
818
|
flex: 1;
|
|
819
|
+
min-width: 0;
|
|
817
820
|
background: var(--bg-input);
|
|
818
821
|
border: 1px solid var(--border);
|
|
819
822
|
border-radius: 10px;
|
|
@@ -822,6 +825,7 @@ body {
|
|
|
822
825
|
font-size: 13px;
|
|
823
826
|
font-family: "SF Mono", Menlo, Consolas, monospace;
|
|
824
827
|
outline: none;
|
|
828
|
+
box-sizing: border-box;
|
|
825
829
|
}
|
|
826
830
|
.input-group input:focus {
|
|
827
831
|
border-color: var(--text-dim);
|
|
@@ -837,6 +841,8 @@ body {
|
|
|
837
841
|
cursor: pointer;
|
|
838
842
|
white-space: nowrap;
|
|
839
843
|
transition: background 0.15s;
|
|
844
|
+
flex-shrink: 0;
|
|
845
|
+
box-sizing: border-box;
|
|
840
846
|
}
|
|
841
847
|
.btn-primary:hover { background: var(--accent-hover); }
|
|
842
848
|
|
|
@@ -1205,9 +1211,9 @@ body {
|
|
|
1205
1211
|
|
|
1206
1212
|
/* Topbar Mobile Adjustments — fixed, taller, shadowed so it is always findable */
|
|
1207
1213
|
.topbar {
|
|
1208
|
-
height:
|
|
1209
|
-
padding: 0
|
|
1210
|
-
gap:
|
|
1214
|
+
height: 52px;
|
|
1215
|
+
padding: 0 8px;
|
|
1216
|
+
gap: 6px;
|
|
1211
1217
|
position: fixed;
|
|
1212
1218
|
top: 0;
|
|
1213
1219
|
left: 0;
|
|
@@ -1219,36 +1225,37 @@ body {
|
|
|
1219
1225
|
}
|
|
1220
1226
|
|
|
1221
1227
|
.main {
|
|
1222
|
-
padding-top:
|
|
1228
|
+
padding-top: 52px;
|
|
1223
1229
|
}
|
|
1224
1230
|
|
|
1225
1231
|
.topbar .session-name {
|
|
1226
|
-
font-size:
|
|
1232
|
+
font-size: 13px;
|
|
1227
1233
|
font-weight: 500;
|
|
1228
1234
|
min-width: 0;
|
|
1229
1235
|
flex: 1;
|
|
1230
1236
|
overflow: hidden;
|
|
1231
1237
|
text-overflow: ellipsis;
|
|
1232
1238
|
white-space: nowrap;
|
|
1233
|
-
padding: 0
|
|
1239
|
+
padding: 0 2px;
|
|
1234
1240
|
}
|
|
1235
1241
|
|
|
1236
1242
|
.btn-toggle-sidebar {
|
|
1237
|
-
width:
|
|
1238
|
-
height:
|
|
1243
|
+
width: 36px;
|
|
1244
|
+
height: 36px;
|
|
1239
1245
|
padding: 0;
|
|
1240
1246
|
color: var(--text);
|
|
1241
1247
|
background: var(--bg-hover);
|
|
1248
|
+
flex-shrink: 0;
|
|
1242
1249
|
}
|
|
1243
1250
|
.btn-toggle-sidebar svg {
|
|
1244
|
-
width:
|
|
1245
|
-
height:
|
|
1251
|
+
width: 20px;
|
|
1252
|
+
height: 20px;
|
|
1246
1253
|
}
|
|
1247
1254
|
|
|
1248
1255
|
#cwdPillWrap {
|
|
1249
|
-
padding:
|
|
1256
|
+
padding: 4px 7px;
|
|
1250
1257
|
font-size: 11px;
|
|
1251
|
-
max-width:
|
|
1258
|
+
max-width: 75px;
|
|
1252
1259
|
overflow: hidden;
|
|
1253
1260
|
text-overflow: ellipsis;
|
|
1254
1261
|
white-space: nowrap;
|
|
@@ -1256,25 +1263,65 @@ body {
|
|
|
1256
1263
|
}
|
|
1257
1264
|
|
|
1258
1265
|
.model-pill-container {
|
|
1259
|
-
|
|
1266
|
+
display: flex;
|
|
1267
|
+
align-items: center;
|
|
1268
|
+
gap: 4px;
|
|
1269
|
+
flex-shrink: 0;
|
|
1270
|
+
max-width: none;
|
|
1260
1271
|
}
|
|
1261
1272
|
|
|
1262
1273
|
.model-pill {
|
|
1263
|
-
padding:
|
|
1274
|
+
padding: 4px 8px;
|
|
1264
1275
|
font-size: 11px;
|
|
1265
|
-
max-width:
|
|
1276
|
+
max-width: 105px;
|
|
1266
1277
|
overflow: hidden;
|
|
1267
1278
|
text-overflow: ellipsis;
|
|
1268
1279
|
white-space: nowrap;
|
|
1269
1280
|
flex-shrink: 0;
|
|
1281
|
+
gap: 4px;
|
|
1270
1282
|
}
|
|
1271
1283
|
.model-pill-name {
|
|
1272
|
-
max-width:
|
|
1284
|
+
max-width: 65px;
|
|
1285
|
+
}
|
|
1286
|
+
.model-pill-badge {
|
|
1287
|
+
display: none !important;
|
|
1273
1288
|
}
|
|
1274
1289
|
|
|
1275
1290
|
.thinking-pill {
|
|
1276
|
-
padding:
|
|
1291
|
+
padding: 4px 7px;
|
|
1277
1292
|
font-size: 11px;
|
|
1293
|
+
flex-shrink: 0;
|
|
1294
|
+
white-space: nowrap;
|
|
1295
|
+
gap: 3px;
|
|
1296
|
+
display: inline-flex;
|
|
1297
|
+
align-items: center;
|
|
1298
|
+
}
|
|
1299
|
+
.thinking-pill .thinking-icon {
|
|
1300
|
+
font-size: 12px;
|
|
1301
|
+
}
|
|
1302
|
+
.thinking-pill .thinking-label {
|
|
1303
|
+
font-size: 11px;
|
|
1304
|
+
font-weight: 500;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
@media (max-width: 360px) {
|
|
1308
|
+
.topbar {
|
|
1309
|
+
padding: 0 4px;
|
|
1310
|
+
gap: 4px;
|
|
1311
|
+
}
|
|
1312
|
+
#cwdPillWrap {
|
|
1313
|
+
max-width: 50px;
|
|
1314
|
+
padding: 4px 5px;
|
|
1315
|
+
}
|
|
1316
|
+
.model-pill {
|
|
1317
|
+
max-width: 90px;
|
|
1318
|
+
}
|
|
1319
|
+
.model-pill-name {
|
|
1320
|
+
max-width: 50px;
|
|
1321
|
+
}
|
|
1322
|
+
.thinking-pill {
|
|
1323
|
+
padding: 4px 5px;
|
|
1324
|
+
}
|
|
1278
1325
|
}
|
|
1279
1326
|
|
|
1280
1327
|
/* Model & Thinking Dropdown Menu Mobile Adjustments */
|
|
@@ -1395,6 +1442,58 @@ body {
|
|
|
1395
1442
|
height: 32px;
|
|
1396
1443
|
}
|
|
1397
1444
|
|
|
1445
|
+
/* CWD Modal Mobile Adjustments */
|
|
1446
|
+
.modal {
|
|
1447
|
+
width: 92%;
|
|
1448
|
+
max-width: 440px;
|
|
1449
|
+
padding: 16px;
|
|
1450
|
+
border-radius: 14px;
|
|
1451
|
+
gap: 12px;
|
|
1452
|
+
box-sizing: border-box;
|
|
1453
|
+
}
|
|
1454
|
+
.modal-header h3 {
|
|
1455
|
+
font-size: 15px;
|
|
1456
|
+
}
|
|
1457
|
+
.modal-desc {
|
|
1458
|
+
font-size: 12px;
|
|
1459
|
+
}
|
|
1460
|
+
.input-group {
|
|
1461
|
+
display: flex;
|
|
1462
|
+
flex-direction: row;
|
|
1463
|
+
gap: 6px;
|
|
1464
|
+
width: 100%;
|
|
1465
|
+
box-sizing: border-box;
|
|
1466
|
+
}
|
|
1467
|
+
.input-group input {
|
|
1468
|
+
font-size: 13px;
|
|
1469
|
+
padding: 9px 10px;
|
|
1470
|
+
min-width: 0;
|
|
1471
|
+
}
|
|
1472
|
+
.btn-primary {
|
|
1473
|
+
padding: 0 14px;
|
|
1474
|
+
font-size: 12px;
|
|
1475
|
+
height: 38px;
|
|
1476
|
+
display: inline-flex;
|
|
1477
|
+
align-items: center;
|
|
1478
|
+
justify-content: center;
|
|
1479
|
+
flex-shrink: 0;
|
|
1480
|
+
}
|
|
1481
|
+
.quick-dirs .chips .chip {
|
|
1482
|
+
font-size: 11px;
|
|
1483
|
+
padding: 5px 8px;
|
|
1484
|
+
word-break: break-all;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
@media (max-width: 380px) {
|
|
1488
|
+
.input-group {
|
|
1489
|
+
flex-direction: column;
|
|
1490
|
+
gap: 8px;
|
|
1491
|
+
}
|
|
1492
|
+
.btn-primary {
|
|
1493
|
+
width: 100%;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1398
1497
|
.composer-hint {
|
|
1399
1498
|
font-size: 10px;
|
|
1400
1499
|
margin-top: 6px;
|
package/server.js
CHANGED
|
@@ -551,6 +551,19 @@ app.post("/api/set-default-model", async (req, res) => {
|
|
|
551
551
|
current.defaultThinkingLevel = thinkingLevel;
|
|
552
552
|
}
|
|
553
553
|
|
|
554
|
+
// If enabledModels is configured in settings.json, ensure the new default model
|
|
555
|
+
// is in the enabledModels list, otherwise Pi core startup logic will fall back
|
|
556
|
+
// to enabledModels[0] instead of using defaultModel.
|
|
557
|
+
if (Array.isArray(current.enabledModels) && current.enabledModels.length > 0) {
|
|
558
|
+
const fullId = `${provider}/${modelId}`;
|
|
559
|
+
const inList = current.enabledModels.some(item =>
|
|
560
|
+
item === fullId || item === modelId || item === `${provider}/*` || item === "*"
|
|
561
|
+
);
|
|
562
|
+
if (!inList) {
|
|
563
|
+
current.enabledModels.unshift(fullId);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
554
567
|
await writeFile(settingsPath, JSON.stringify(current, null, 2), "utf8");
|
|
555
568
|
|
|
556
569
|
res.json({
|
|
@@ -604,11 +617,13 @@ async function listAllSessionFiles() {
|
|
|
604
617
|
// Root-level .jsonl files (created when we pass --session-dir to pi).
|
|
605
618
|
const top = await readdir(SESSIONS_DIR, { withFileTypes: true }).catch(() => []);
|
|
606
619
|
for (const e of top) {
|
|
607
|
-
if (e.isFile() && e.name.endsWith(".jsonl"))
|
|
620
|
+
if ((e.isFile() || e.isSymbolicLink()) && e.name.endsWith(".jsonl")) {
|
|
621
|
+
files.push(path.join(SESSIONS_DIR, e.name));
|
|
622
|
+
}
|
|
608
623
|
}
|
|
609
624
|
|
|
610
625
|
// Subdirectory .jsonl files (created by pi itself when cwd is encoded).
|
|
611
|
-
const subdirs = top.filter(d => d.isDirectory());
|
|
626
|
+
const subdirs = top.filter(d => d.isDirectory() || d.isSymbolicLink());
|
|
612
627
|
for (const d of subdirs) {
|
|
613
628
|
const dp = path.join(SESSIONS_DIR, d.name);
|
|
614
629
|
const names = (await readdir(dp).catch(() => [])).filter(f => f.endsWith(".jsonl"));
|
|
@@ -629,11 +644,14 @@ async function getSessionMetadata(file) {
|
|
|
629
644
|
|
|
630
645
|
const content = await readFile(file, "utf8");
|
|
631
646
|
const lines = content.split("\n").filter(Boolean);
|
|
632
|
-
let header = null, title = null, msgCount = 0;
|
|
647
|
+
let header = null, title = null, sessionName = null, msgCount = 0;
|
|
633
648
|
for (const line of lines) {
|
|
634
649
|
let o;
|
|
635
650
|
try { o = JSON.parse(line); } catch { continue; }
|
|
636
651
|
if (o.type === "session") header = o;
|
|
652
|
+
if (o.type === "session_info" && o.name) {
|
|
653
|
+
sessionName = o.name.trim();
|
|
654
|
+
}
|
|
637
655
|
if (o.type === "message" && o.message && o.message.role === "user" && !title) {
|
|
638
656
|
title = extractText(o.message.content).slice(0, 80);
|
|
639
657
|
}
|
|
@@ -649,6 +667,7 @@ async function getSessionMetadata(file) {
|
|
|
649
667
|
file,
|
|
650
668
|
name: path.basename(file),
|
|
651
669
|
id: header.id,
|
|
670
|
+
sessionName: sessionName || null,
|
|
652
671
|
timestamp: header.timestamp,
|
|
653
672
|
firstUser: title,
|
|
654
673
|
messageCount: msgCount,
|
|
@@ -662,15 +681,18 @@ app.get("/api/sessions", async (req, res) => {
|
|
|
662
681
|
try {
|
|
663
682
|
const cwd = normalizeCwd(req.query.cwd);
|
|
664
683
|
const all = await listAllSessionFiles();
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
684
|
+
const results = await Promise.all(
|
|
685
|
+
all.map(async (full) => {
|
|
686
|
+
try {
|
|
687
|
+
const meta = await getSessionMetadata(full);
|
|
688
|
+
if (meta && meta.cwd === cwd) {
|
|
689
|
+
return meta.sessionInfo;
|
|
690
|
+
}
|
|
691
|
+
} catch {}
|
|
692
|
+
return null;
|
|
693
|
+
})
|
|
694
|
+
);
|
|
695
|
+
const sessions = results.filter(Boolean);
|
|
674
696
|
sessions.sort((a, b) => (b.timestamp || "").localeCompare(a.timestamp || ""));
|
|
675
697
|
res.json({ cwd, sessions });
|
|
676
698
|
} catch (e) {
|
|
@@ -684,7 +706,7 @@ function extractText(content) {
|
|
|
684
706
|
if (!Array.isArray(content)) return "";
|
|
685
707
|
return content
|
|
686
708
|
.filter(c => c && (c.type === "text" || typeof c === "string"))
|
|
687
|
-
.map(c => typeof c === "string" ? c : c.text)
|
|
709
|
+
.map(c => typeof c === "string" ? c : (c.text || ""))
|
|
688
710
|
.join("");
|
|
689
711
|
}
|
|
690
712
|
|
|
@@ -695,7 +717,11 @@ app.get("/api/session", async (req, res) => {
|
|
|
695
717
|
if (!file || !file.endsWith(".jsonl")) return res.status(400).json({ error: "bad file" });
|
|
696
718
|
|
|
697
719
|
// Security check: ensure the file path is within SESSIONS_DIR
|
|
698
|
-
|
|
720
|
+
let resolvedFile = file;
|
|
721
|
+
if (resolvedFile.startsWith("~")) {
|
|
722
|
+
resolvedFile = path.join(home(), resolvedFile.slice(1));
|
|
723
|
+
}
|
|
724
|
+
resolvedFile = path.resolve(resolvedFile);
|
|
699
725
|
const resolvedSessionsDir = path.resolve(SESSIONS_DIR);
|
|
700
726
|
const relPath = path.relative(resolvedSessionsDir, resolvedFile);
|
|
701
727
|
if (relPath.startsWith("..") || path.isAbsolute(relPath)) {
|
|
@@ -706,9 +732,13 @@ app.get("/api/session", async (req, res) => {
|
|
|
706
732
|
const lines = content.split("\n").filter(Boolean);
|
|
707
733
|
const entries = [];
|
|
708
734
|
let header = null;
|
|
735
|
+
let sessionName = null;
|
|
709
736
|
for (const line of lines) {
|
|
710
737
|
let o; try { o = JSON.parse(line); } catch { continue; }
|
|
711
738
|
if (o.type === "session") header = o;
|
|
739
|
+
if (o.type === "session_info" && o.name) {
|
|
740
|
+
sessionName = o.name.trim();
|
|
741
|
+
}
|
|
712
742
|
entries.push(o);
|
|
713
743
|
}
|
|
714
744
|
// Build a map and reconstruct the active path from root -> leaf.
|
|
@@ -762,7 +792,7 @@ app.get("/api/session", async (req, res) => {
|
|
|
762
792
|
}
|
|
763
793
|
}
|
|
764
794
|
|
|
765
|
-
res.json({ header, entries: entryChain, model: sessionModel });
|
|
795
|
+
res.json({ header, entries: entryChain, model: sessionModel, sessionName });
|
|
766
796
|
} catch (e) {
|
|
767
797
|
console.error(e);
|
|
768
798
|
res.status(500).json({ error: String(e) });
|