diff --git a/PROGRESS.md b/PROGRESS.md index 78bd55e..55e78f9 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 左 pane 280→320px + header 折叠 toggle + 任务列表行去 id8 / meta nowrap 防 CJK 断行) +最后更新:2026-05-20(dev SPA 左 pane 折叠改 40px rail 模式 + time-ago 锁宽让 N条/Ntok 跨行对齐 + 删 header 冗余按钮) --- @@ -23,6 +23,8 @@ ### 2026-05-20 +- **dev SPA 左 pane 折叠改 rail 模式 + 删 header 冗余按钮 + time-ago 锁宽完成跨行对齐**:用户反馈 ① "原来 zcbot 旁的折叠按钮不要了,没用处" + ② "数字对齐那块现在是不是每块内容左侧对齐?"(实际是右对齐但因 time-ago 宽度变化导致 N 条/N tok 右边界也跟着抖,跨行没真对齐)。两件套:① 折叠模式从「pane display:none」改 VS Code 范式 rail —— `body.left-collapsed #app.ready { grid-template-columns: 40px 1fr 320px }` + `#pane-left > * { display: none }`(藏全部直接子) + override 第一行 pane-head 重显且只留 `#pane-toggle-left`(`> *:not(#pane-toggle-left) { display: none }`,选择器特异性 2 ids 压 1 id);pane-head 第一行用 `position: static` 取消 sticky / `border-bottom: none` / `background: transparent` 看起来更像 rail 非"卡片"。按钮符号根据 `body.left-collapsed` 在 `applyLeftCollapsed` 里翻向(展开态 `‹` 折叠态 `›`)。彻底删 `#hd-toggle-left` + `header .icon-btn` CSS 块,header 不再背 expand 入口的债。② time-ago 加 `flex-shrink: 0; text-align: right; min-width: 64px` 锁宽,**这才是真正解决跨行对齐的关键**:此前 `.num.right-group` 用 `margin-left: auto` 把 [N 条][N tok][time] 整组推右,但 time 自身宽度浮动 30~70px(刚刚 / 10 小时前 / 2025-12-05)→ time 左边界抖 → N tok 右边界抖 → N 条 右边界抖,逐级传染。锁 time 宽后整组位置稳定,槽内 `text-align: right` 才能让"条/tok"后缀跨行真正垂直对齐。删 `.badge .time-ago { flex-shrink: 0 }` 合并里的 time-ago(已独立给规则)。**没动**:fmtTokens / 桶分级 / tabular-nums / `.num min-width: 44px`(上一轮已正确)、右 pane / chat 中列。 +- **dev SPA 任务行 meta 数字槽位跨行对齐 + 折叠按钮位置调整**:用户报"N 条 / N tok 数字宽窄不一,看着不齐";又说"折叠按钮应该贴刷新按钮"。两件套:① meta CSS 加 `font-variant-numeric: tabular-nums` + `align-items: baseline`,新 `.num` 子选择器 `flex-shrink: 0; text-align: right; min-width: 44px`(右对齐让 `条` / `tok` 后缀跨行垂直对齐);N 条 span 戴 `right-group` 类拿 `margin-left: auto`,把 [N 条][N tok][time-ago] 整组挤右侧,左侧只剩 badge + skill;原 time-ago 上的 inline `margin-left:auto` 移除避免双 push 失效。新 `fmtTokens(n)` helper:<1k 原数 / <10k `1.2k` / <1M `123k` / >=1M `1.2M`,bound 槽位宽度;`title=` hover 出 `123,456 tokens` 完整值(`Number.toLocaleString()`)。② 折叠按钮拆双入口 — `#pane-toggle-left` 放第一行 pane-head 紧贴刷新按钮(展开态用,点击折叠);`#hd-toggle-left` 留 header 但 `style="display:none"` 默认隐藏,仅折叠态显示(用户路径:折叠后 pane display:none → 无法在 pane 内点展开 → 必须 header 保留 expand 入口)。`applyLeftCollapsed(collapsed)` 控制 hd 按钮 display,两按钮共享 `toggleLeftCollapsed()` 实现;每按钮符号固定(pane 内 `‹` 一直是折叠方向,header 内 `›` 一直是展开方向),不再翻向(语义更清)。**没动**:右 pane / chat 列宽、`/v1/tasks` 后端、id8 仍在 row title hover(上次改的不动)、CSS `.small` 等。 - **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 保留(调试参考)。 diff --git a/web/static/dev.html b/web/static/dev.html index 4c01125..69266bb 100644 --- a/web/static/dev.html +++ b/web/static/dev.html @@ -141,22 +141,22 @@ /* ───── 3-pane layout ───── */ #app { display: none; height: 100vh; } #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; } + /* 折叠左 pane:rail 模式,列收成 40px,pane 内只留一个展开按钮(类 VS Code 范式) */ + body.left-collapsed #app.ready { grid-template-columns: 40px 1fr 320px; } + body.left-collapsed #pane-left > * { display: none; } + body.left-collapsed #pane-left > .pane-head:first-child { + display: flex; justify-content: center; align-items: center; + padding: 6px 4px; border-bottom: none; background: transparent; + position: static; /* 取消 sticky,rail 太窄不需要滚 */ + } + body.left-collapsed #pane-left > .pane-head:first-child > * { display: none; } + body.left-collapsed #pane-left > .pane-head:first-child > #pane-toggle-left { display: inline-block; } 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; @@ -234,10 +234,16 @@ .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; } - /* meta 行:flex nowrap + 每个子项 nowrap,防 CJK 字符在窄 pane(280px)被 shrink 后断行 */ - .task-row .meta { font-size: 11px; color: var(--muted); display: flex; gap: 8px; min-width: 0; } + /* meta 行:flex nowrap + 每个子项 nowrap,防 CJK 字符在窄 pane(320px)被 shrink 后断行 */ + /* tabular-nums 让数字等宽(条 / tok 计数跨行对齐) */ + .task-row .meta { font-size: 11px; color: var(--muted); display: flex; gap: 8px; min-width: 0; + align-items: baseline; font-variant-numeric: tabular-nums; } .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 .meta .badge { flex-shrink: 0; } + /* 数字槽位:固定 min-width + 右对齐;time-ago 也锁宽 → 整个右侧组位置稳定,跨行"条/tok"才能对齐 */ + .task-row .meta .num { flex-shrink: 0; text-align: right; min-width: 44px; } + .task-row .meta .num.right-group { margin-left: auto; } /* 把数字+时间整组挤到右侧 */ + .task-row .meta .time-ago { flex-shrink: 0; text-align: right; min-width: 64px; } .task-row .badge { display: inline-block; padding: 0 6px; border-radius: 8px; font-size: 11px; background: #eef; color: #336; @@ -549,7 +555,6 @@
-
zcbot
@@ -573,6 +578,7 @@ +
@@ -813,6 +819,16 @@ function fmtTime(iso) { try { return new Date(iso).toLocaleString(); } catch (e) { return iso; } } +// 紧凑 token 显示:<1k 原数,<10k 一位小数 k,>=10k 整数 k,>=1M 一位小数 M +// 目的:让列表行 "N tok" 槽位宽度有上限,跨行对齐 +function fmtTokens(n) { + n = n || 0; + if (n < 1000) return String(n); + if (n < 10000) return (n / 1000).toFixed(1) + "k"; + if (n < 1000000) return Math.round(n / 1000) + "k"; + return (n / 1000000).toFixed(1) + "M"; +} + // 相对时间(任务列表用):刚刚 / N 分钟前 / N 小时前 / 昨天 HH:MM / MM-DD / YYYY-MM-DD function fmtTimeAgo(iso) { if (!iso) return ""; @@ -954,14 +970,15 @@ function logout() { } $("hd-logout").onclick = logout; -// ───── 左 pane 折叠 toggle(localStorage 持久化) ───── +// ───── 左 pane 折叠 toggle(rail 模式 + localStorage 持久化) ───── +// 折叠 = pane 收成 40px rail,只留 #pane-toggle-left 一直可点;按钮符号根据状态翻向 function applyLeftCollapsed(collapsed) { document.body.classList.toggle("left-collapsed", collapsed); - const btn = $("hd-toggle-left"); + const btn = $("pane-toggle-left"); btn.textContent = collapsed ? "›" : "‹"; btn.title = collapsed ? "展开任务列表" : "折叠任务列表"; } -$("hd-toggle-left").onclick = () => { +$("pane-toggle-left").onclick = () => { const next = !document.body.classList.contains("left-collapsed"); localStorage.setItem(LS_LEFT_COLLAPSED, next ? "1" : ""); applyLeftCollapsed(next); @@ -1072,9 +1089,9 @@ function renderTaskList(tasks, append = false) {
${statusLabel} ${t.skill ? `${escapeHtml(t.skill)}` : ""} - ${t.n_messages || 0} 条 - ${t.tokens || 0} tok - ${escapeHtml(fmtTimeAgo(t.updated_at))} + ${t.n_messages || 0} 条 + ${fmtTokens(t.tokens)} tok + ${escapeHtml(fmtTimeAgo(t.updated_at))}