diff --git a/PROGRESS.md b/PROGRESS.md index aa72c4d..78bd55e 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -2,7 +2,7 @@ > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 2-4 句:做了啥 + 关键判断 + 没动什么;细节查 `git log` / `git diff`。 -最后更新:2026-05-20(dev SPA 任务列表换滚动加载 — 删 pager bar / 加 IntersectionObserver sentinel / 共 N 个 count badge) +最后更新:2026-05-20(dev SPA 左 pane 280→320px + header 折叠 toggle + 任务列表行去 id8 / meta nowrap 防 CJK 断行) --- @@ -23,6 +23,7 @@ ### 2026-05-20 +- **dev SPA 左 pane 调宽 280→320px + header 折叠 toggle + 任务行精简 meta**:用户报 280px 下底行(badge/skill/N条/Ntok/time/id8)被 flex shrink 后 CJK 字符断行(像"10 小时前"裂成两行)。三件套修:① `#app.ready grid-template-columns` `280px → 320px`(右 pane / chat 不动,从 chat 借 40px,任务名 / 描述 / wd 都更舒展);② header 最左插 ``,点击 toggle `body.left-collapsed` → CSS `grid-template-columns: 0 1fr 320px` + `#pane-left { display: none }`(列归零腾给 chat,折叠态 chevron 翻 `›`);state 存 `localStorage zcbot.left-collapsed`,boot 即应用,刷新保持。IntersectionObserver 留着不重建(display:none 期间 sentinel 0 高度自然不触发,展开后重算 layout 若 sentinel 在视口自然续传);③ 任务行删 `id8` span(8 位 hex 调试时才用),挪到 row `title=` hover 出 `${name}\n${task_id}` 完整 id 仍可查;`.task-row .meta > *` 全加 `white-space: nowrap; overflow: hidden; text-overflow: ellipsis` 防内部 CJK 字符破断;badge + time-ago 加 `flex-shrink: 0` 保两端不缩;wd / desc 副行恢复 inline 三件套 `overflow:hidden;text-overflow:ellipsis;white-space:nowrap`(它们是单文本带不是 flex 子元素行,`> *` CSS 不命中文本节点)。**没动**:右 pane 320px 不变(文件预览常用)、chat 中列 1fr(自适应剩余);折叠按钮没做右 pane 对应版(用户没要)。 - **dev SPA 左侧任务列表 pager bar → 滚动加载(ChatGPT/DeepSeek 范式)**:用户嫌底部分页 chrome 别扭。删 `#task-pager`(prev/next/info bar)+ `renderPager` + `resetPageAndReload`,改 `IntersectionObserver` on `#task-sentinel`(`#task-list` 后兄弟,`min-height:1px`),root = `#pane-left`(整 pane 是 scroll 容器,`.pane{overflow:auto}`)+ `rootMargin: 200px 0px` 提前 200px 触发体感更顺。`loadTaskList({append=false})` 双语义:reset 抢占式(filters / refresh / 写操作后,page=1 替换);`append=true` 仅 sentinel 触发,page+1 拼到底,受 `taskLoading || !taskHasMore` 互斥。**并发模型**:用 `_taskLoadSeq` token 让 reset 永远抢占 — 收到响应时若 `mySeq !== _taskLoadSeq` 整段 short-circuit return(也含 finally 的 `taskLoading=false`,避免 reset 在途时被 stale append 错误解锁),解决"append 在途时改筛选被丢"的旧 bug。**新增**:① 首 pane-head 加 `共 N 个` muted 小字补偿总数显示;② sentinel 文案三态(加载中… / — 已加载全部 — / 空字符串);③ `renderTaskList(tasks, append)` append 走 `
.innerHTML` 临时容器 + `appendChild` 不 clobber 已渲染行,事件 handler 只挂新行。**没动**:`/v1/tasks` 后端(本来就是标准分页 `{page,page_size,count,results}`)、page_size=20 默认、所有 7 处 `loadTaskList()` 旧调用点(默认 reset 语义与原行为等价)。**Tradeoff**:失"跳到第 N 页"但筛选 / 搜索 / 排序 + 滚动覆盖所有导航场景;失"当前页位置"但写操作后跳回顶端在 zcbot 任务规模(几十~几百)体感自然。 - **dev SPA 左侧任务列表行加「最近操作时间」**:用户要"显示最新操作时间"。`renderTaskList` 行 meta 区(badge / skill / N 条 / N tok / id-slice)在 id-slice 之前插一个 ``,文案用新加的 `fmtTimeAgo(iso)` 相对时间 helper:`<60s`→刚刚 / `<1h`→N 分钟前 / 同日→N 小时前 / 昨日→昨天 HH:MM / 同年→MM-DD HH:MM / 跨年→YYYY-MM-DD,`title=` hover 出完整 `fmtTime` locale 串。`margin-left:auto` 从 id-slice 挪到时间 span(让两者一起靠右,中间 8px `.meta gap` 自然分隔)。字段用 `updated_at`(任务任何写操作 — 改名 / 新消息 / 状态切 — 都会更新,贴合"最新操作"语义),`/v1/tasks` payload 早已包含,后端零改。**没动**:左 pane 列表默认排序仍 `-created_at`(用户改排序顺序时另说);id-slice 保留(调试参考)。 - **dev SPA 新建任务弹框「工作目录」从 input + datalist 改 `` autocomplete 改 `` 输入新目录名 + autofocus,提交时 `working_dir = sel === "__new__" ? nt-wd-new.value : sel`。hint 区改 `updateWdHint()` 三分支(新建 / 留空 / 复用),change + new-input + name-input 三事件触发。`` 留在 modal 内但不再被它消费,**只供左 pane 顶部 `#filter-wd` 筛选 autocomplete**(datalist 按 id 引用,DOM 位置无关);`loadFolderSuggestions()` 同次拉取灌两边。**没动**:`/v1/folders` API、提交 body 形态(仍 `working_dir: string`,空串语义不变 → 后端 fallback 用任务名)、左 pane filter-wd 仍用 input + datalist(用户只点名"任务弹框")、DESIGN / RUN。**Tradeoff**:纯 select 实现最直接但会失"新名则新建",改两段式(select 含 `+ 新建…`,触发后展开 text input)保留所有原能力。 diff --git a/web/static/dev.html b/web/static/dev.html index da39285..4c01125 100644 --- a/web/static/dev.html +++ b/web/static/dev.html @@ -140,13 +140,23 @@ /* ───── 3-pane layout ───── */ #app { display: none; height: 100vh; } - #app.ready { display: grid; grid-template-columns: 280px 1fr 320px; grid-template-rows: auto 1fr; grid-template-areas: "head head head" "left mid right"; } + #app.ready { display: grid; grid-template-columns: 320px 1fr 320px; grid-template-rows: auto 1fr; grid-template-areas: "head head head" "left mid right"; } + /* 折叠左 pane:整列归零 + pane 隐藏(配合 header toggle 按钮 + localStorage 持久化) */ + body.left-collapsed #app.ready { grid-template-columns: 0 1fr 320px; } + body.left-collapsed #pane-left { display: none; } header { grid-area: head; background: #fff; border-bottom: 1px solid var(--border); padding: 8px 14px; display: flex; align-items: center; gap: 12px; box-shadow: 0 1px 2px rgba(0,0,0,.03); } + /* header 小图标按钮(折叠 toggle 等):透明常态 + 悬浮微底 */ + header .icon-btn { + background: transparent; border: 1px solid transparent; + color: var(--muted); padding: 2px 8px; font-size: 16px; line-height: 1.2; + border-radius: 4px; cursor: pointer; + } + header .icon-btn:hover { background: var(--hover); color: var(--text); border-color: var(--border); } header .brand { display: flex; align-items: center; gap: 8px; } header .brand .logo { width: 24px; height: 24px; border-radius: 6px; @@ -224,7 +234,10 @@ .task-row.active { background: var(--accent-soft); border-left: 3px solid var(--accent); padding-left: 9px; } .task-row .desc { font-weight: 500; color: var(--text); margin-bottom: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } - .task-row .meta { font-size: 11px; color: var(--muted); display: flex; gap: 8px; } + /* meta 行:flex nowrap + 每个子项 nowrap,防 CJK 字符在窄 pane(280px)被 shrink 后断行 */ + .task-row .meta { font-size: 11px; color: var(--muted); display: flex; gap: 8px; min-width: 0; } + .task-row .meta > * { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; } + .task-row .meta .badge, .task-row .meta .time-ago { flex-shrink: 0; } /* 状态徽章 + 相对时间不缩 */ .task-row .badge { display: inline-block; padding: 0 6px; border-radius: 8px; font-size: 11px; background: #eef; color: #336; @@ -536,6 +549,7 @@
+
zcbot
@@ -690,6 +704,7 @@ const LS_TOKEN = "zcbot.token"; const LS_UID = "zcbot.user_id"; const LS_NAME = "zcbot.name"; +const LS_LEFT_COLLAPSED = "zcbot.left-collapsed"; const state = { token: localStorage.getItem(LS_TOKEN) || "", @@ -939,6 +954,20 @@ function logout() { } $("hd-logout").onclick = logout; +// ───── 左 pane 折叠 toggle(localStorage 持久化) ───── +function applyLeftCollapsed(collapsed) { + document.body.classList.toggle("left-collapsed", collapsed); + const btn = $("hd-toggle-left"); + btn.textContent = collapsed ? "›" : "‹"; + btn.title = collapsed ? "展开任务列表" : "折叠任务列表"; +} +$("hd-toggle-left").onclick = () => { + const next = !document.body.classList.contains("left-collapsed"); + localStorage.setItem(LS_LEFT_COLLAPSED, next ? "1" : ""); + applyLeftCollapsed(next); +}; +applyLeftCollapsed(localStorage.getItem(LS_LEFT_COLLAPSED) === "1"); + // ───── enter app ───── function enterApp() { $("login").style.display = "none"; @@ -1033,19 +1062,19 @@ function renderTaskList(tasks, append = false) { const wdName = t.working_dir ? t.working_dir.split("/").filter(Boolean).pop() : ""; const desc = t.description || ""; const statusLabel = statusLabels[t.status] || t.status; + const rowTitle = `${taskName}\n${t.task_id}`; // hover 出全名 + 完整 id(替代 meta 里被去掉的 id8) return ` -
+
-
${escapeHtml(taskName)}
- ${wdName ? `
📁 ${escapeHtml(wdName)}
` : ""} - ${desc ? `
${escapeHtml(desc)}
` : ""} +
${escapeHtml(taskName)}
+ ${wdName ? `
📁 ${escapeHtml(wdName)}
` : ""} + ${desc ? `
${escapeHtml(desc)}
` : ""}
${statusLabel} - ${t.skill ? `${escapeHtml(t.skill)}` : ""} + ${t.skill ? `${escapeHtml(t.skill)}` : ""} ${t.n_messages || 0} 条 ${t.tokens || 0} tok - ${escapeHtml(fmtTimeAgo(t.updated_at))} - ${t.task_id.slice(0, 8)} + ${escapeHtml(fmtTimeAgo(t.updated_at))}