diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8bf81a46..9bfc112a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,7 +8,7 @@
## Unreleased
-- 项目管理集中到左侧:支持直接创建、重命名、查看文件和安全移除空项目;右侧入口改为“当前项目 / 全部文件”,专注项目内及跨项目文件浏览。
+- 项目管理集中到左侧:支持直接创建、重命名、查看文件和安全移除空项目;最近 3 条对话保持可见,项目树独立滚动,右侧入口改为“当前项目 / 全部文件”。
## 0.72.1 — 2026-09-04
diff --git a/tests/frontend_files.test.mjs b/tests/frontend_files.test.mjs
index 8cc88d32..6f36add4 100644
--- a/tests/frontend_files.test.mjs
+++ b/tests/frontend_files.test.mjs
@@ -40,6 +40,10 @@ test("task navigation groups chats by project with restrained icon sizes", () =>
assert.match(chatJs, /working_dir: projectName/);
assert.match(chatJs, /state\.taskHasMore = filtered &&/);
assert.match(chatJs, /task-project-more/);
+ assert.match(chatJs, /Math\.min\(3, tasks\.length\)/);
+ assert.match(chatJs, /task-recent-toggle/);
+ assert.match(chatJs, /revealTaskInProject/);
+ assert.match(chatJs, /scroller\.onscroll = maybeLoadMore/);
assert.match(chatJs, /task-project-create/);
assert.doesNotMatch(chatJs, /if \(!folder\.n_tasks\) continue/);
assert.match(chatJs, /renameProjectFolder/);
@@ -53,6 +57,8 @@ test("task navigation groups chats by project with restrained icon sizes", () =>
assert.match(filesJs, /recursive: entries\.length > 0/);
assert.match(pageHtml, /\.task-nav-icon \{ width: 15px; height: 15px;/);
assert.match(pageHtml, /\.task-project-head \.task-nav-icon \{ width: 16px; height: 16px;/);
+ assert.match(pageHtml, /\.task-projects \{ flex: 1; min-height: 0; overflow-y: auto;/);
+ assert.match(pageHtml, /\.task-nav-recent \{ flex: none;/);
});
test("file rows render restrained type-specific svg icons", () => {
diff --git a/web/static/dev.html b/web/static/dev.html
index 4ca460ab..e2d4a6fa 100644
--- a/web/static/dev.html
+++ b/web/static/dev.html
@@ -561,10 +561,12 @@
#hd-admin:hover { background: var(--accent-soft); }
.pane { border-right: 1px solid var(--border); background: var(--panel); overflow: auto; min-height: 0; }
- /* 左 pane:flex column,顶部多行 pane-head 固定,只让 #task-scroll 滚 — 滚动条不再覆盖顶栏 */
+ /* 左 pane:flex column;最近对话固定,只有项目树或筛选结果拥有滚动条。 */
#pane-left { grid-area: left; display: flex; flex-direction: column; overflow: hidden; }
#pane-left > .pane-head { flex-shrink: 0; }
- #task-scroll { flex: 1; min-height: 0; overflow: auto; }
+ #task-scroll { flex: 1; min-height: 0; overflow: hidden; }
+ #task-list { height: 100%; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
+ #task-sentinel { display: none; }
/* min-height: 0 + overflow: hidden 让内部 flex 子项的 overflow: auto 真正生效(否则被默认 min-height: auto 顶出) */
#pane-mid { grid-area: mid; display: flex; flex-direction: column; border-right: 1px solid var(--border); background: var(--panel); min-height: 0; min-width: 0; overflow: hidden; position: relative; }
/* 右侧工作区:页签头固定,文件 / 作业面板各自管理内部滚动 */
@@ -682,14 +684,20 @@
.task-row-compact .meta.stats { justify-content: flex-end; margin-top: 1px; }
.task-row-compact .task-menu { width: 28px; height: 28px; padding: 0; align-items: center; justify-content: center; }
.task-row-nested { margin-left: 22px; }
- .task-nav-recent { padding: 8px 0 6px; border-bottom: 1px solid var(--border-soft); }
+ .task-nav-recent { flex: none; padding: 8px 0 6px; border-bottom: 1px solid var(--border-soft); }
.task-nav-section-title { min-height: 28px; padding: 4px 12px; display: flex; align-items: center; gap: 7px;
color: var(--muted); font-size: 11px; font-weight: 600; letter-spacing: .02em; }
+ .task-recent-toggle { width: 100%; border: 0; background: transparent; text-align: left; cursor: pointer; }
+ .task-recent-toggle:hover { color: var(--text); }
+ .task-recent-chevron { font-size: 14px; transition: transform .14s ease; }
+ .task-nav-recent.collapsed .task-recent-chevron { transform: rotate(-90deg); }
+ .task-nav-recent.collapsed .task-recent-items { display: none; }
.task-nav-section-title .spacer { flex: 1; }
.task-project-create { width: 24px; height: 24px; padding: 0; border: 0; border-radius: 5px;
background: transparent; color: var(--muted); font-size: 17px; line-height: 1; cursor: pointer; }
.task-project-create:hover { background: var(--hover); color: var(--text); }
- .task-projects { padding: 6px 0 10px; }
+ .task-project-section-title { flex: none; }
+ .task-projects { flex: 1; min-height: 0; overflow-y: auto; padding: 6px 0 10px; scrollbar-gutter: stable; }
.task-project { margin: 1px 0; }
.task-project-row { display: flex; align-items: center; }
.task-project-head { flex: 1; min-width: 0; min-height: 34px; padding: 4px 4px 4px 8px; border: 0; border-radius: 0;
@@ -715,7 +723,7 @@
text-align: left; cursor: pointer; border-radius: 4px; }
.task-project-more:hover, .task-project-retry:hover { background: var(--hover); color: var(--text); }
.task-project-retry { color: var(--danger); }
- .task-filter-results { padding: 5px 0 10px; }
+ .task-filter-results { flex: 1; min-height: 0; overflow-y: auto; padding: 5px 0 10px; scrollbar-gutter: stable; }
.task-row .desc { font-weight: 500; color: var(--text); margin-bottom: 2px;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* meta 行:flex nowrap + 每个子项 nowrap,防 CJK 字符在窄 pane(320px)被 shrink 后断行 */
diff --git a/web/static/js/chat.js b/web/static/js/chat.js
index 8052c79a..4b5fcf61 100644
--- a/web/static/js/chat.js
+++ b/web/static/js/chat.js
@@ -224,6 +224,8 @@ function syncTaskRowRunIndicator(tid) {
}
const TASK_PROJECTS_EXPANDED_KEY = "zcbot-task-projects-expanded-v2";
+const TASK_RECENT_COLLAPSED_KEY = "zcbot-task-recent-collapsed-v1";
+const _compactRecentMedia = window.matchMedia("(max-height: 700px)");
function taskProject(t) {
const path = t.working_dir || "";
@@ -289,6 +291,11 @@ function expandedTaskProjects() {
}
}
+function recentTasksCollapsed() {
+ const saved = localStorage.getItem(TASK_RECENT_COLLAPSED_KEY);
+ return saved === null ? _compactRecentMedia.matches : saved === "1";
+}
+
function taskProjectGroups(tasks) {
const loadedGroups = new Map();
for (const task of tasks) {
@@ -338,11 +345,15 @@ function groupedTaskListHtml(tasks) {
const expanded = expandedTaskProjects();
const activeProject = state.taskId && state.tasksById[state.taskId]
? taskProject(state.tasksById[state.taskId]).key : "";
- const recent = tasks.slice(0, Math.min(4, tasks.length));
+ const recent = tasks.slice(0, Math.min(3, tasks.length));
+ const recentCollapsed = recentTasksCollapsed();
const recentHtml = recent.length ? `
-