diff --git a/docs/superpowers/plans/2026-09-12-sidebar-project-tree.md b/docs/superpowers/plans/2026-09-12-sidebar-project-tree.md new file mode 100644 index 0000000..d12f839 --- /dev/null +++ b/docs/superpowers/plans/2026-09-12-sidebar-project-tree.md @@ -0,0 +1,99 @@ +# 侧边栏项目树 + 消息区对齐 + 空态清理 — 实施计划 + +配套需求:用户 7 条前端优化(侧边栏信息架构重构 + 气泡对齐 + 空态清理) +目标文件:`src/genesis/server/static/{chat.html,chat.css,chat.js,chat_state.js}` +前端无框架、无构建;后端零改动。 + +## 已确认决策 +- D1 气泡对齐到**文本输入框**:左 16px;右留发送按钮宽度(40+10=50px);**删除 assistant 头像点**。 +- D2 点击侧边栏项目名 = **选中该项目(设为当前项目)+ 展开/收起**。 +- D3 抽屉左侧项目列表**保留**(仅删除「+新建项目」项)。 +- D4 侧边栏折叠态**隐藏「项目/其他会话」两段**。 +- D5 req2 的「提示语及提示词」= 空态主标题 + 建议卡,发送后一并清除。 + +## 目标侧边栏 +``` +[G Genesis 概要设计书做成Agent] +[+ 新会话] +项目 + ← 仅"+",hover「追加项目」 + ▾ 股票系统 ⚙ ← 展开显示其会话;⚙ hover「配置」 + · 需求A + · 需求B + ▾ 人事系统 ⚙ + · 需求C +其他会话 + · 需求D(project="") +``` + +--- + +## Task 1:chat_state.js 分组纯函数 + node 测试 +- 新增 `groupSessionsByProject(sessions)` → `{ byProject: { "": [s...] }, other: [s...] }`(`project` 为空者入 `other`)。 +- `tests/test_chat_state.js` 增加断言:空项目归 other;两项目分别归组。 +- 运行 `node --test tests/test_chat_state.js`。 + +## Task 2:chat.html 侧边栏结构 + 移除底部新建项目按钮 +- 删除 `

会话历史

` 与 `
`。 +- 新增 `#sec-projects`(`side-head`:标题「项目」+ `#side-add-project`「+」)与 `#project-tree`。 +- 新增 `#sec-others`(标题「其他会话」)与 `#other-session-list`。 +- 删除 `#tb-newproject` 按钮。 + +## Task 3:chat.css 侧边栏树 + 消息区对齐 + 折叠态 +- 新增 `.side-section/.side-head/.side-title/.side-add/.proj-group/.proj-row/.proj-caret/.proj-name/.proj-config/.proj-sessions` 样式;嵌套 `.sess` 缩进。 +- 折叠态:`#sidebar.collapsed .side-section { display: none; }`。 +- 消息区:`#chat { max-width: none; margin: 0; padding: var(--sp-6) var(--sp-4); }`;`.msg { padding-right: 50px; }`;`.msg .bubble { max-width: 100%; }`;**删除 `.msg.assistant::before`**。 + +## Task 4:chat.js 分组渲染 + 绑定调整 + 空态清理 +- `refreshSessions()` 改拉全量并按 `groupSessionsByProject` 分组 → `renderProjectTree()` + `renderOtherSessions()`。 +- 抽 `renderSessItem(s)`(复用现有会话行逻辑)。 +- `renderProjectTree()`:项目行点击选中+展开/收起(`collapsedProjects` Set,localStorage 持久化);`⚙` → `openProjectDrawer(name)`。 +- `#side-add-project` → `openProjectDrawer(null)`(带脏守卫)。 +- `renderProjectSwitcher()`:删除 `ps-manage`「项目管理(打开抽屉)」项及绑定。 +- `renderDrawerList()`:删除 `di-create`「+新建项目」项。 +- `send()`:发消息前 `chatEl.querySelector('.empty-state')?.remove()`。 +- 删除 `#tb-newproject` 绑定。 + +## Task 5:验证与提交 +- `node --check`、`node --test`、`python -m pytest`(≥99%)。 +- 手动核对:气泡对齐、空态清理、项目树/⚙/+、底部无新建项目、未选项目时新会话入「其他会话」。 +- 更新 `_AI_USAGE_LOG.md` 并提交。 + +## 风险 +- `#sidebar.collapsed` 下 `.sess` 旧样式成为死代码(无害)。 +- 分组渲染依赖 `GET /api/sessions` 全量返回(已支持 `project` 可选)。 +- 若某会话的 project 已从项目列表删除,仍会以其 project 名建组渲染(保证不丢会话)。 + +--- + +## 评审结论与计划调整(plan-eng-review) + +### 采纳的修正 +- **F1(正确性,req 5)显式「不选择项目」持久化** + - `chat_state.js`:`autoBindProject(draftProject, projects, explicitNone = false)` —— `explicitNone` 为真时原样返回 `draftProject`(不移除用户选择);否则维持原自动绑定逻辑。新增 node 断言。 + - `chat.js`:读写 `localStorage['genesis_project_explicit_none']`;`loadProjects()` 传入该标记;选任意项目 → 清除标记;选「不选择项目」→ 设标记。保证「无项目选择 → 新会话进『其他会话』」成立。 +- **F2(UX)打开会话所在分组高亮** + - `renderProjectTree()`:组内任一会话 `session_id === sid` 时给 `.proj-group` 加 `.has-active`(与 `.current` = 选中项目 区分)。 +- **F3(DRY)对齐余量单一来源** + - 新增 CSS 变量:`--send-size: 40px`、`--composer-gap: 10px`、`--composer-gutter: calc(var(--send-size) + var(--composer-gap))`。 + - `#send { width/height: var(--send-size) }`、`#composer-inner { gap: var(--composer-gap) }`、`.msg { padding-right: var(--composer-gutter) }`。 +- **F6(清理)删除折叠态死样式** + - 移除 `#sidebar.collapsed .sess*` 与 `#sidebar h2` 相关规则(折叠态整段隐藏、h2 已不存在)。 + +### 接受现状(标注,不改) +- F4 project 已删的会话以原 project 名成组渲染(不丢会话)。 +- F5 DOM 渲染层无自动化测试;靠纯函数 `groupSessionsByProject` 断言 + `node --check` + 手动。 +- F7 项目行沿用 `div.onclick`(非键盘可达),与会话行一致。 +- F8 `refreshSessions` 每次拉全量;规模增大时再考虑限制。 + +### 受影响任务更新 +- Task 1 追加:`autoBindProject` 第三参数 + `groupSessionsByProject` + node 断言。 +- Task 3 追加:`--send-size/--composer-gap/--composer-gutter` 变量;删除折叠死样式。 +- Task 4 追加:`explicit_none` 读写与联动;`.has-active` 渲染。 + +## GSTACK REVIEW REPORT +- STATUS: DONE_WITH_CONCERNS +- REVIEWED: docs/superpowers/plans/2026-09-12-sidebar-project-tree.md +- FINDINGS: F1 正确性(已采纳修复) / F2 UX(已采纳) / F3 DRY(已采纳) / F6 清理(已采纳);F4/F5/F7/F8 接受 +- KEY DECISION: 显式「不选择项目」持久化修复(否则 req 5 不成立) +- CONCERN: 渲染层无自动化测试(无 DOM harness);折叠态样式清理后需回归核对 +- NEXT: 待用户批准后实施 diff --git a/src/genesis/server/static/chat.css b/src/genesis/server/static/chat.css index cd5b219..6121e46 100644 --- a/src/genesis/server/static/chat.css +++ b/src/genesis/server/static/chat.css @@ -37,6 +37,11 @@ /* === 动效 (spec §一) === */ --ease: cubic-bezier(.2, 0, 0, 1); --dur: .18s; + + /* 输入区几何(单一来源,供消息区对齐) */ + --send-size: 40px; + --composer-gap: 10px; + --composer-gutter: calc(var(--send-size) + var(--composer-gap)); } /* === 全局 === */ @@ -64,7 +69,6 @@ body { } #sidebar.collapsed { width: 56px; } #sidebar.collapsed #sidebar-toggle { /* 折叠态位置不再切换,按钮统一在侧边栏内下边 */ } -#sidebar.collapsed #session-list { padding-bottom: 52px; } .brand { display: flex; align-items: center; gap: 12px; padding: 16px 12px 12px 16px; margin: 0 8px 8px; @@ -104,13 +108,47 @@ body { #sidebar.collapsed #new-chat { margin: var(--sp-2) 10px var(--sp-3); padding: 10px 0; } #new-chat .ic-plus { display: inline-block; font-weight: 900; } #sidebar.collapsed #new-chat .nc-text { display: none; } -#sidebar h2 { - font-family: var(--font-mono); - font-size: 11px; font-weight: 500; letter-spacing: .12em; text-transform: uppercase; - color: var(--text-muted); margin: var(--sp-3) var(--sp-4) var(--sp-2); +/* 侧边栏分区(项目 / 其他会话) */ +.side-section { display: flex; flex-direction: column; min-height: 0; } +#sidebar.collapsed .side-section { display: none; } +.side-head { + display: flex; align-items: center; justify-content: space-between; + margin: var(--sp-3) var(--sp-4) var(--sp-2); } -#sidebar.collapsed h2 { display: none; } -#session-list { flex: 1; overflow-y: auto; padding: 0 8px 8px; } +.side-title { + font-family: var(--font-mono); font-size: 11px; font-weight: 500; + letter-spacing: .12em; text-transform: uppercase; color: var(--text-muted); +} +.side-add { + background: transparent; border: 1px solid var(--border-strong); border-radius: var(--r-sm); + color: var(--text-secondary); cursor: pointer; width: 22px; height: 22px; line-height: 1; + font-size: 15px; display: flex; align-items: center; justify-content: center; + transition: all var(--dur) var(--ease); +} +.side-add:hover { border-color: var(--accent); color: var(--accent); } +#project-tree, #other-session-list { padding: 0 8px 8px; overflow-y: auto; } +#project-tree { flex: 0 1 auto; } +#sec-others { flex: 1; min-height: 0; } +/* 项目分组 */ +.proj-group { margin: 2px 0; } +.proj-row { + display: flex; align-items: center; gap: 6px; padding: 7px 8px; border-radius: var(--r-md); + cursor: pointer; transition: all var(--dur) var(--ease); border: 1px solid transparent; +} +.proj-row:hover { background: var(--bg-elevated); } +.proj-group.current .proj-row { background: var(--bg-elevated); border-color: var(--accent); } +.proj-group.has-active .proj-row .proj-name { color: var(--accent); font-weight: 700; } +.proj-caret { font-size: 10px; color: var(--text-muted); width: 12px; flex-shrink: 0; } +.proj-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + font-size: 13px; color: var(--text-primary); } +.proj-config { + flex-shrink: 0; opacity: 0; transition: opacity .15s ease; background: transparent; + border: none; color: var(--text-muted); cursor: pointer; width: 22px; height: 22px; border-radius: 6px; + font-size: 13px; display: flex; align-items: center; justify-content: center; +} +.proj-row:hover .proj-config { opacity: .8; } +.proj-config:hover { opacity: 1 !important; background: var(--bg-base); color: var(--accent); } +.proj-sessions { padding-left: 14px; } .sess { padding: 9px 12px; border-radius: var(--r-md); margin: 2px 0; cursor: pointer; position: relative; border: 1px solid transparent; @@ -139,16 +177,7 @@ body { } .sess:hover .del { opacity: .7; } .sess .del:hover { opacity: 1 !important; background: var(--danger); color: var(--bg-base); } -#sidebar.collapsed .sess { justify-content: center; padding: 9px 6px; } -#sidebar.collapsed .sess .info, #sidebar.collapsed .sess .del { display: none; } -#sidebar.collapsed .sess .avatar { - display: flex; align-items: center; justify-content: center; - width: 8px; height: 8px; border-radius: 50%; - background: var(--text-muted); -} .sess .avatar { display: none; } -#sidebar.collapsed .sess .avatar { display: flex; } -#sidebar.collapsed .sess.active .avatar { background: var(--accent); box-shadow: 0 0 8px var(--accent-soft); } /* === 主区 + 顶栏 (spec §2.2) === */ #main { flex: 1; display: flex; flex-direction: column; min-width: 0; background: var(--bg-base); } @@ -226,10 +255,10 @@ header h1 { font-size: 16px; font-weight: 700; letter-spacing: -.02em; margin: 0 #rag-bar button:hover:not(:disabled) { box-shadow: 0 0 16px var(--accent-soft); } /* === 聊天区 (spec §2.3) === */ -#chat { flex: 1; overflow-y: auto; padding: var(--sp-6) var(--sp-6) var(--sp-6); max-width: 880px; width: 100%; margin: 0 auto; } -.msg { display: flex; margin: var(--sp-3) 0; animation: zoomIn .25s var(--ease); position: relative; } +#chat { flex: 1; overflow-y: auto; padding: var(--sp-6) var(--sp-4); max-width: none; width: 100%; margin: 0; } +.msg { display: flex; margin: var(--sp-3) 0; padding-right: var(--composer-gutter); animation: zoomIn .25s var(--ease); position: relative; } .msg .bubble { - max-width: 74%; padding: 11px 16px; border-radius: var(--r-lg); + max-width: 100%; padding: 11px 16px; border-radius: var(--r-lg); white-space: pre-wrap; line-height: 1.65; font-size: 14px; letter-spacing: .01em; } .msg.user { justify-content: flex-end; } @@ -247,11 +276,7 @@ header h1 { font-size: 16px; font-weight: 700; letter-spacing: -.02em; margin: 0 border-bottom-left-radius: 6px; color: var(--text-primary); } -.msg.assistant::before { - content: ""; width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; - margin: 18px 10px 0 0; background: var(--accent); - box-shadow: 0 0 8px var(--accent-soft); -} + .msg.progress .bubble { background: var(--bg-input); border: 1px dashed var(--accent-soft); font-size: 13px; color: var(--text-secondary); @@ -303,7 +328,7 @@ label.vh { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px /* === 输入区 (spec §2.5) === */ #composer { border-top: none; background: transparent; padding: var(--sp-3) var(--sp-4) var(--sp-4); } -#composer-inner { max-width: none; margin: 0; display: flex; gap: 10px; align-items: flex-end; } +#composer-inner { max-width: none; margin: 0; display: flex; gap: var(--composer-gap); align-items: flex-end; } #input { flex: 1; border: 1px solid var(--border-strong); border-radius: var(--r-md); padding: 11px 14px; font-size: 14px; outline: none; background: var(--bg-input); @@ -315,7 +340,7 @@ label.vh { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px #input::placeholder { color: var(--text-muted); } #send { background: var(--accent); color: var(--bg-base); border: none; border-radius: 50%; - width: 40px; height: 40px; padding: 0; cursor: pointer; flex-shrink: 0; + width: var(--send-size); height: var(--send-size); padding: 0; cursor: pointer; flex-shrink: 0; display: flex; align-items: center; justify-content: center; transition: all var(--dur) var(--ease); } @@ -464,5 +489,4 @@ label.vh { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px font-size: 13px; font-family: var(--font-ui); cursor: pointer; text-align: left; } .sess-menu .sm-item:hover { background: var(--bg-elevated); color: var(--accent); } .sess-menu .sm-ic { color: var(--accent); } -#sidebar.collapsed .sess .kebab { display: none; } @media (max-width: 640px) { .empty-state .suggests { grid-template-columns: 1fr; } } diff --git a/src/genesis/server/static/chat.html b/src/genesis/server/static/chat.html index 6c7ce44..dbc6359 100644 --- a/src/genesis/server/static/chat.html +++ b/src/genesis/server/static/chat.html @@ -14,8 +14,17 @@
Genesis概要设计书做成Agent
-

会话历史

-
+
+
+ 项目 + +
+
+
+
+
其他会话
+
+
@@ -40,7 +49,6 @@
-