diff --git a/.planning/2026-08-26-mcp-session-toggle/findings.md b/.planning/2026-08-26-mcp-session-toggle/findings.md new file mode 100644 index 0000000..88ee0d6 --- /dev/null +++ b/.planning/2026-08-26-mcp-session-toggle/findings.md @@ -0,0 +1,47 @@ +# 发现与决策 + +## 需求 + +- 用户希望减少侧栏中带金色角标的 MCP 创建会话噪声,而不是隐藏普通会话。 +- 控件是搜索框右侧的单个图标按钮,不显示文字或数量角标。 +- 隐藏模式保留当前打开、运行中的 MCP 会话;其余 MCP 会话隐藏。 +- 搜索匹配可临时显示隐藏的 MCP 会话。 +- 首次默认显示,并记住用户最后一次切换结果。 + +## 代码发现 + +- `public/app.js` 的 `createSessionListItem()` 已通过 `createdFromKind === 'mcp'` 识别 MCP 会话,并添加 `llm-created` 类。 +- `renderSessionList()` 先取 `getVisibleSessions()`,再做搜索、置顶拆分和项目分组,是加入纯展示过滤的合适入口。 +- `buildSessionListStructureSignature()` 会阻止结构未变化时重建 DOM,新偏好必须影响结构签名或显式使签名失效。 +- `public/index.html` 的 `.session-search-row` 当前包含搜索框和高级搜索按钮,适合并排增加图标开关。 +- 项目已有项目折叠和旧会话“加载更多”,本功能不能改变这些状态模型。 +- `scripts/regression.js` 已有 `assertFrontendSidebarCollapseContract()`、`assertSidebarTitleRefreshStormContract()` 和 `assertAdvancedSessionSearchContract()`,可沿用其静态契约与 VM 隔离执行模式补充定向回归。 +- 完整回归入口是 `scripts/regression.js` 的 `main()`;新增断言需要显式接入该入口,避免测试函数存在但未执行。 +- `readSidebarCollapsedPreference()` / `persistSidebarCollapsedPreference()` 已提供“读取失败保守降级 + 布尔字符串持久化”的本地偏好范式,新开关应沿用该模式。 +- `applySessionSnapshot()` 是会话运行态/快照变化后刷新列表的重要入口;过滤应基于每次快照中的 `isRunning` 派生,不能缓存单条会话的隐藏结论。 +- 搜索输入、Escape 清空和清除按钮都会直接调用 `renderSessionList()`;因此只要过滤基于规范化后的 `sessionSearchQuery` 派生,搜索覆盖和清空恢复无需额外状态。 +- `.advanced-search-open` 已有 34px 基础按钮与荒原主题 36px 覆盖;新图标按钮可共用尺寸、交互色和窄屏间距,但应使用独立类名表达语义。 +- `scripts/regression.js` 支持 `--target` 定向入口,可为本功能增加独立 target,把失败测试和后续验证控制在 60 秒内。 + +## 视觉发现 + +- 用户截图使用荒原主题的窄侧栏。 +- MCP 创建会话通过条目左侧/边缘的金色角标与普通会话区分。 +- 搜索行横向空间有限,因此必须使用与高级搜索同级的紧凑图标按钮。 + +## 技术决策 + +| 决策 | 理由 | +|---|---| +| 仅做前端派生过滤 | 避免改变会话数据、接口和后端排序 | +| 搜索时绕过过滤 | 用户主动查找时应能发现隐藏项 | +| 运行态变化依靠现有快照触发重渲染 | 无需增加独立轮询或服务端事件 | +| 使用内联 SVG 眼睛图标并切换状态 | 比字符图标跨平台更稳定,符合“一个图标”要求 | + +## 资源 + +- `.trellis/spec/frontend/quality-guidelines.md` +- `public/app.js` +- `public/index.html` +- `public/style.css` +- `scripts/regression.js` diff --git a/.planning/2026-08-26-mcp-session-toggle/progress.md b/.planning/2026-08-26-mcp-session-toggle/progress.md new file mode 100644 index 0000000..aed85b4 --- /dev/null +++ b/.planning/2026-08-26-mcp-session-toggle/progress.md @@ -0,0 +1,104 @@ +# 进度日志 + +## 会话:2026-08-26 + +### 阶段 1:需求与计划审查 + +- **状态:** complete +- **开始时间:** 2026-08-26T15:30:00+08:00 +- 已完成: + - 使用 grilling 逐项确认过滤范围、状态例外、按钮位置、默认状态、持久化和搜索行为。 + - 使用 codebase-memory-mcp 定位 `renderSessionList()`、`createSessionListItem()` 与结构签名逻辑。 + - 读取 Trellis 前端规范、planning-with-files 与 todo-list-csv 技能要求。 + - 创建 Trellis 任务 `08-26-mcp-session-visibility-toggle`。 + - 确认现有侧栏、搜索和 VM 隔离回归测试入口,等待计划审查期间未修改产品代码。 + - 独立计划审查通过,无阻塞问题。 + +### 阶段 2:测试先行 + +- **状态:** in_progress +- **开始时间:** 2026-08-26T16:10:00+08:00 +- 计划: + - 增加 MCP 会话过滤、状态例外、搜索恢复、持久化和图标 DOM 契约测试。 + - 在产品实现修改前运行并确认测试失败。 + - 首个测试代理长时间未产生文件变更,已中断并收敛任务后重新派发。 + - 测试代理最终落盘 `assertMcpSessionVisibilityContract()`、定向 target 和默认回归入口。 + - 修改产品代码前运行定向测试,按预期失败于“图标按钮不存在”。 + - 主线程审查并补充纯函数样例,覆盖普通、隐藏、置顶、当前、运行中、搜索恢复和默认偏好。 + +### 阶段 3:实现 + +- **状态:** complete +- 已完成: + - 增加首次默认显示且可降级的 `localStorage` 偏好读取/保存。 + - 增加 MCP 会话纯派生过滤,保留普通、当前和运行中会话,搜索非空时绕过过滤。 + - 将显示状态纳入结构签名并在每次列表渲染时同步图标无障碍状态。 + - 在搜索框右侧接入单个 SVG 眼睛图标,无文字或数量角标。 + - 增加基础主题 34px、荒原主题 36px 样式。 + +### 阶段 4:验证 + +- **状态:** complete +- 已通过: + - `node --check public/app.js` + - `node --check scripts/regression.js` + - `node scripts/regression.js --target mcp-session-visibility` + - `node scripts/regression.js --target advanced-session-search` + - `node scripts/regression.js --target wasteland-theme` + - 运行服务已返回新按钮标记和新的前端内容哈希。 + - 荒原侧栏计算布局:316px - 左右内边距 28px - 图标 72px - 间距 16px = 搜索框 200px,无横向溢出。 + - 差异审查中合并新按钮与高级搜索按钮的公共基础样式,保留各自图标与主题特有规则。 +- 降级说明: + - Firefox 无头与软件渲染均因容器 SWGL 图形后端超时,未生成截图;停止重复尝试。 + - 主线程完整回归曾在既有 Codex App steer 时序场景超时;独立审查代理随后误用裸参数触发完整回归并通过。 +- 独立审查: + - 未发现 High / Medium / Low 或阻塞问题。 + - 确认过滤顺序、结构签名、状态例外、持久化、无障碍、主题尺寸和测试入口符合 PRD。 +- 规范复盘: + - 本次未形成跨功能的新通用约定;现有前端质量规范与静态资源版本机制已覆盖,不更新 `.trellis/spec/`。 +- 已创建/修改: + - `.planning/2026-08-26-mcp-session-toggle/task_plan.md` + - `.planning/2026-08-26-mcp-session-toggle/findings.md` + - `.planning/2026-08-26-mcp-session-toggle/progress.md` + - `.trellis/tasks/08-26-mcp-session-visibility-toggle/` + +## 测试结果 + +| 测试 | 预期 | 实际 | 状态 | +|---|---|---|---| +| 尚未执行 | — | — | 待执行 | +| MCP 会话显示失败测试 | `node scripts/regression.js --target mcp-session-visibility` | 产品未实现时失败 | 失败:图标按钮不存在 | ✓ | +| MCP 会话显示定向回归 | `node scripts/regression.js --target mcp-session-visibility` | 通过 | 通过 | ✓ | +| 高级搜索定向回归 | `node scripts/regression.js --target advanced-session-search` | 通过 | 通过 | ✓ | +| 荒原主题定向回归 | `node scripts/regression.js --target wasteland-theme` | 通过 | 通过 | ✓ | +| 运行服务静态资源 | `curl http://127.0.0.1:8002/` | 返回新按钮和内容哈希 | 通过 | ✓ | +| 独立完整回归 | `node scripts/regression.js mcp-session-visibility`(裸参数实际进入完整入口) | 通过 | 通过 | ✓ | +| 独立代码审查 | 只读审查产品与测试 diff | 无阻塞问题 | 无阻塞问题 | ✓ | + +### 阶段 5:交付 + +- **状态:** complete +- 已完成: + - 持久计划、发现与进度记录已同步。 + - TODO CSV 所有步骤已完成并按技能约定清理。 + - Trellis 任务上下文已校验,准备结束当前任务。 + +## 错误日志 + +| 时间 | 错误 | 尝试 | 处理 | +|---|---|---:|---| +| 2026-08-26T16:12:00+08:00 | 完整历史分叉与 worker 类型参数冲突 | 1 | 改为无历史分叉并显式提供任务上下文 | +| 2026-08-26T16:16:00+08:00 | `wait_agent` 等待值低于 10 秒下限 | 1 | 后续使用至少 10 秒的等待值 | +| 2026-08-26T16:33:00+08:00 | 高级搜索定向回归未通过 | 1 | 恢复荒原主题高级搜索精确选择器,新按钮改用独立尺寸规则 | +| 2026-08-26T16:38:00+08:00 | Firefox 无头截图因 SWGL 图形后端超时 | 1 | 使用软件渲染/Xvfb 再试一次,保留静态与计算布局降级路径 | +| 2026-08-26T16:48:00+08:00 | 完整回归 Codex App steer 场景超时 | 1 | 确认失败在既有 server/mock 集成链路;继续以定向回归和只读审查隔离本次变更 | + +## 5 问恢复检查 + +| 问题 | 回答 | +|---|---| +| 当前在哪? | 阶段 1:计划审查 | +| 下一步去哪? | 测试先行、实现、验证、交付 | +| 目标是什么? | 增加可记忆的 MCP 会话显示/隐藏图标按钮 | +| 已发现什么? | 见 findings.md | +| 已完成什么? | 见本文件阶段 1 日志 | diff --git a/.planning/2026-08-26-mcp-session-toggle/task_plan.md b/.planning/2026-08-26-mcp-session-toggle/task_plan.md new file mode 100644 index 0000000..52531a7 --- /dev/null +++ b/.planning/2026-08-26-mcp-session-toggle/task_plan.md @@ -0,0 +1,90 @@ +# MCP 会话显示隐藏开关计划 + +## 目标 + +在侧栏搜索框右侧增加一个图标按钮,用于显示或隐藏由 MCP 创建的会话;正常会话始终可见,并完整覆盖状态记忆、搜索临时显示与运行态例外。 + +## 当前阶段 + +阶段 5:交付 + +## 执行步骤 + +1. 审查需求与实现计划 +2. 补充会话过滤与图标状态的失败回归测试 +3. 实现持久化状态与 MCP 会话过滤逻辑 +4. 接入搜索框右侧图标及主题响应式样式 +5. 运行定向测试并修复问题 +6. 执行完整回归与差异审查 +7. 清理临时跟踪文件并记录交付 + +## 阶段 + +### 阶段 1:需求与计划审查 + +- [x] 通过逐项访谈确认产品边界 +- [x] 定位现有会话列表、搜索、分组和 MCP 标识逻辑 +- [x] 完成独立计划审查 +- **状态:** complete + +### 阶段 2:测试先行 + +- [x] 为过滤规则、状态例外、搜索覆盖和图标契约补充失败测试 +- [x] 运行新增测试并确认修改前失败 +- **状态:** complete + +### 阶段 3:实现 + +- [x] 增加本地持久化的显示状态 +- [x] 在列表派生数据阶段过滤 MCP 会话 +- [x] 接入无文字图标按钮、无障碍状态和主题样式 +- **状态:** complete + +### 阶段 4:验证 + +- [x] 运行定向测试、语法检查与完整回归 +- [x] 审查变更差异、工作区状态及项目规范符合性 +- **状态:** complete + +### 阶段 5:交付 + +- [x] 更新持久计划与 Trellis 记录 +- [x] 清理临时 TODO CSV +- **状态:** complete + +## 已确认需求 + +- 过滤对象仅为 `createdFromKind === 'mcp'` 的会话,正常会话始终显示。 +- 隐藏模式仍保留当前打开或 `isRunning` 的 MCP 会话;其他 MCP 会话隐藏。 +- 搜索时临时恢复全部 MCP 会话参与匹配,清空搜索后恢复隐藏规则。 +- 按钮位于搜索框右侧并与高级搜索并排,仅显示一个状态图标。 +- 首次默认显示;用户切换结果保存在本机浏览器,刷新和下次打开继续沿用。 +- 没有可见会话的项目分组不渲染。 + +## 技术决策 + +| 决策 | 理由 | +|---|---| +| 在 `getVisibleSessions()` 之后、搜索匹配之前派生列表可见性 | 保持服务端快照和缓存完整,只影响侧栏展示 | +| 搜索查询非空时绕过 MCP 隐藏过滤 | 满足主动搜索可以找回隐藏会话的需求 | +| 使用单一 `localStorage` 布尔偏好,缺失时按显示处理 | 与首次默认显示和跨刷新记忆一致 | +| 图标按钮维护 `aria-pressed`、动态 `title`/`aria-label` | 只有图标时仍能清晰表达当前状态和点击动作 | +| 将显示偏好纳入列表结构签名或切换时强制失效 | 避免现有结构签名优化跳过重新渲染 | + +## 风险与验证重点 + +- 置顶 MCP 会话也应服从隐藏规则,除非它是当前会话或运行中。 +- 当前会话从 MCP 状态切换后,侧栏不能丢失当前定位。 +- 新快照把会话从运行中更新为已完成时,隐藏模式应自动移除该条目。 +- 所有主题及窄屏布局不得挤压搜索输入框或高级搜索按钮。 +- 不引入服务端状态或协议改动。 + +## 错误记录 + +| 错误 | 尝试 | 处理 | +|---|---:|---| +| 完整历史分叉不能同时指定 worker 类型 | 1 | 改为 `fork_turns: none` 并在任务中显式提供上下文 | +| `wait_agent` 等待值低于 10 秒下限 | 1 | 改用 10 秒以上等待值 | +| 高级搜索回归要求保留荒原主题精确选择器 | 1 | 恢复 `.session-search-row .advanced-search-open`,新按钮使用独立规则 | +| Firefox 无头渲染图形后端超时且未生成截图 | 1 | 改用软件渲染/Xvfb 再试一次,失败则降级到计算布局与静态契约 | +| 完整回归 Codex App steer 场景等待 WebSocket 消息超时 | 1 | 失败位于既有 server/mock 集成链路,与前端侧栏变更隔离;保留定向回归与审查证据 | diff --git a/.planning/2026-08-26-release-commit/task_plan.md b/.planning/2026-08-26-release-commit/task_plan.md new file mode 100644 index 0000000..39ac7a8 --- /dev/null +++ b/.planning/2026-08-26-release-commit/task_plan.md @@ -0,0 +1,28 @@ +# 重新打包并提交推送计划 + +## 目标 + +使用 CentOS 7 baseline Bun 重新生成 cc-web 单文件发布包,并将当前工作区全部修改提交到 `main` 后推送 `origin/main`。 + +## 步骤 + +1. 确认当前分支、远端状态、工作区范围,并优先检查 `bun`;PATH 中缺失时复用本机 `@oven/bun-linux-x64-baseline/bin/bun`。 +2. 使用 `BUN_BIN= npm run build:single-exe` 重新构建 CentOS 7 baseline 发布包。 +3. 运行发布技能要求的语法检查、归档列表与 MCP smoke;MCP smoke 通过 stdin 发送 JSON-RPC `initialize`,设置短超时并确认 JSON 响应后退出。 +4. 使用显式 pathspec 排除临时 TODO CSV 后执行 `git add -A`,再复查暂存区,确保其余当前修改全部纳入。 +5. 提交本地变更。 +6. 推送前执行 `git fetch origin main` 并核对 ahead/behind;可直接推送时将 `main` 推送到 `origin/main`。 + +## 约束 + +- 使用 `scripts/build-single-exe.js`,目标保持 `bun-linux-x64-baseline`。 +- 不切换 Docker,不安装 NodeSource Node.js,不把 Claude/Codex CLI 打入发布包。 +- 用户明确要求不做代码审计;只执行打包技能要求的轻量校验、提交与推送。 +- 所有当前工作区修改都纳入本次提交;临时 TODO CSV 在完成后删除,不提交。 +- MCP smoke 使用单条 JSON-RPC `initialize` 请求和短超时,不能留下等待 stdin 的后台进程。 +- `git add -A -- . ':!重新打包并提交推送 TO DO list.csv'` 后必须确认临时 CSV 仍未跟踪且不在暂存区。 + +## 状态 + +- 当前:步骤 1 +- 计划审查:待完成 diff --git a/.trellis/tasks/08-26-mcp-session-visibility-toggle/check.jsonl b/.trellis/tasks/08-26-mcp-session-visibility-toggle/check.jsonl new file mode 100644 index 0000000..1daa41a --- /dev/null +++ b/.trellis/tasks/08-26-mcp-session-visibility-toggle/check.jsonl @@ -0,0 +1,2 @@ +{"file":".trellis/spec/frontend/index.md","reason":"前端任务入口与规范索引"} +{"file":".trellis/spec/frontend/quality-guidelines.md","reason":"验证前端质量与回归测试覆盖"} diff --git a/.trellis/tasks/08-26-mcp-session-visibility-toggle/implement.jsonl b/.trellis/tasks/08-26-mcp-session-visibility-toggle/implement.jsonl new file mode 100644 index 0000000..4e7ba69 --- /dev/null +++ b/.trellis/tasks/08-26-mcp-session-visibility-toggle/implement.jsonl @@ -0,0 +1,4 @@ +{"file":".trellis/spec/frontend/index.md","reason":"前端任务入口与规范索引"} +{"file":".trellis/spec/frontend/quality-guidelines.md","reason":"前端质量与回归测试要求"} +{"file":".trellis/spec/frontend/component-guidelines.md","reason":"侧栏控件结构与无障碍约束参考"} +{"file":".trellis/spec/frontend/state-management.md","reason":"本地持久化显示状态的状态管理参考"} diff --git a/.trellis/tasks/08-26-mcp-session-visibility-toggle/prd.md b/.trellis/tasks/08-26-mcp-session-visibility-toggle/prd.md new file mode 100644 index 0000000..99c7f36 --- /dev/null +++ b/.trellis/tasks/08-26-mcp-session-visibility-toggle/prd.md @@ -0,0 +1,38 @@ +# MCP 会话显示隐藏开关 + +## 背景 + +侧栏会展示大量由 MCP 创建的协作会话。用户通常不需要逐条查看这些会话,希望通过一个紧凑按钮减少列表噪声,同时保留普通会话和仍需关注的运行态会话。 + +## 用户故事 + +作为经常使用 MCP 协作能力的用户,我希望用一个图标快速显示或隐藏 MCP 创建的会话,使侧栏保持简洁,并且不会丢失当前会话或仍在运行的任务入口。 + +## 功能要求 + +1. 在侧栏搜索框右侧、与高级搜索按钮并排增加一个图标按钮。 +2. 按钮仅控制 `createdFromKind === 'mcp'` 的会话;正常会话始终显示。 +3. 隐藏模式仍显示当前打开或 `isRunning === true` 的 MCP 会话。 +4. 其他 MCP 会话在隐藏模式下不参与置顶区、项目分组和未分组列表渲染。 +5. 输入会话搜索关键词时,全部 MCP 会话恢复参与匹配;清空搜索后重新应用隐藏规则。 +6. 首次且没有保存偏好时默认显示 MCP 会话。 +7. 用户切换结果保存于当前浏览器,刷新或下次打开继续沿用。 +8. 控件仅显示一个状态图标,不显示文字或数量角标;通过悬停提示与无障碍属性表达动作和状态。 + +## 非目标 + +- 不删除、归档或修改任何会话数据。 +- 不改变服务端会话列表接口、排序协议或 MCP 创建流程。 +- 不改变项目折叠、置顶和旧会话“加载更多”的既有语义。 +- 不增加设置页配置项。 + +## 验收标准 + +- 默认状态与当前行为一致:全部会话正常显示。 +- 切换隐藏后,普通会话、当前 MCP 会话和运行中 MCP 会话仍显示,其余 MCP 会话消失。 +- 置顶 MCP 会话在非当前、非运行时同样隐藏。 +- 隐藏状态下搜索可找到匹配的 MCP 会话;清空搜索后再次隐藏。 +- 刷新页面后保留最后一次切换状态。 +- 图标按钮在主要主题和窄屏侧栏中不挤压或遮挡现有控件。 +- 新增回归测试覆盖过滤、持久化、搜索覆盖、状态例外与 DOM/无障碍契约。 + diff --git a/.trellis/tasks/08-26-mcp-session-visibility-toggle/task.json b/.trellis/tasks/08-26-mcp-session-visibility-toggle/task.json new file mode 100644 index 0000000..2cf1a49 --- /dev/null +++ b/.trellis/tasks/08-26-mcp-session-visibility-toggle/task.json @@ -0,0 +1,26 @@ +{ + "id": "mcp-session-visibility-toggle", + "name": "mcp-session-visibility-toggle", + "title": "MCP 会话显示隐藏开关", + "description": "", + "status": "in_progress", + "dev_type": null, + "scope": null, + "package": null, + "priority": "P2", + "creator": "shiyue", + "assignee": "shiyue", + "createdAt": "2026-08-26", + "completedAt": null, + "branch": null, + "base_branch": "main", + "worktree_path": null, + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [], + "notes": "", + "meta": {} +} \ No newline at end of file diff --git a/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz b/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz index ee62c47..0d89dea 100644 Binary files a/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz and b/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz differ diff --git a/public/app.js b/public/app.js index d4469f3..c411520 100644 --- a/public/app.js +++ b/public/app.js @@ -12,6 +12,7 @@ const CCWEB_PROMPT_VIEW_MODE_STORAGE_KEY = 'cc-web-ccweb-prompt-view-mode'; const PROJECT_COLLAPSE_STORAGE_KEY = 'cc-web-collapsed-projects'; const SIDEBAR_COLLAPSE_STORAGE_KEY = 'cc-web-sidebar-collapsed'; + const MCP_SESSION_VISIBILITY_STORAGE_KEY = 'cc-web-mcp-session-visibility'; const SIDEBAR_DRAWER_MEDIA_QUERY = '(max-width: 768px)'; const SIDEBAR_DESKTOP_COLLAPSE_MEDIA_QUERY = '(width > 768px)'; const SIDEBAR_HOVER_PREVIEW_MEDIA_QUERY = '(width > 768px) and (hover: hover) and (pointer: fine)'; @@ -272,6 +273,7 @@ let isReloadingMcp = false; const mcpStartupToastKeys = new Map(); let sessionSearchQuery = ''; + let showMcpSessions = readMcpSessionVisibilityPreference(); const advancedSessionSearchState = { open: false, query: '', @@ -365,6 +367,7 @@ const importSessionBtn = $('#import-session-btn'); const sessionSearchInput = $('#session-search-input'); const sessionSearchClear = $('#session-search-clear'); + const mcpSessionVisibilityToggle = $('#mcp-session-visibility-toggle'); const advancedSearchOpen = $('#advanced-search-open'); const advancedSearchPanel = $('#advanced-search-panel'); const advancedSearchClose = $('#advanced-search-close'); @@ -4521,6 +4524,39 @@ } } + function readMcpSessionVisibilityPreference() { + try { + return localStorage.getItem(MCP_SESSION_VISIBILITY_STORAGE_KEY) !== 'false'; + } catch { + return true; + } + } + + function persistMcpSessionVisibilityPreference(visible) { + try { + localStorage.setItem(MCP_SESSION_VISIBILITY_STORAGE_KEY, visible ? 'true' : 'false'); + } catch { + // 存储不可用时仍保留当前页面内的筛选状态。 + } + } + + function sessionPassesMcpVisibilityFilter(session, normalizedSearchQuery = '') { + if (normalizedSearchQuery) return true; + if (showMcpSessions) return true; + const isMcpCreated = String(session?.createdFromKind || '').toLowerCase() === 'mcp'; + if (!isMcpCreated) return true; + return session.id === currentSessionId || !!session.isRunning; + } + + function syncMcpSessionVisibilityToggle() { + if (!mcpSessionVisibilityToggle) return; + const isHidingMcpSessions = !showMcpSessions; + const actionLabel = isHidingMcpSessions ? '显示 MCP 会话' : '隐藏 MCP 会话'; + mcpSessionVisibilityToggle.setAttribute('aria-pressed', String(isHidingMcpSessions)); + mcpSessionVisibilityToggle.title = actionLabel; + mcpSessionVisibilityToggle.setAttribute('aria-label', actionLabel); + } + function getSessionSearchText(session) { const cwd = getSessionEffectiveCwd(session); return [ @@ -10856,6 +10892,7 @@ return JSON.stringify({ agent: currentAgent, currentSessionId, + showMcpSessions, search: model.normalizedSearchQuery, allEmpty: model.allVisibleSessions.length === 0, visibleEmpty: model.visibleSessions.length === 0, @@ -10890,12 +10927,16 @@ function renderSessionList() { syncSessionSearchUi(); + syncMcpSessionVisibilityToggle(); const allVisibleSessions = getVisibleSessions(); const normalizedSearchQuery = normalizeSessionSearchQuery(sessionSearchQuery); const isSearchingSessions = !!normalizedSearchQuery; + const mcpVisibilityFilteredSessions = allVisibleSessions.filter((session) => ( + sessionPassesMcpVisibilityFilter(session, normalizedSearchQuery) + )); const visibleSessions = isSearchingSessions - ? allVisibleSessions.filter((session) => sessionMatchesSearch(session, normalizedSearchQuery)) - : allVisibleSessions; + ? mcpVisibilityFilteredSessions.filter((session) => sessionMatchesSearch(session, normalizedSearchQuery)) + : mcpVisibilityFilteredSessions; const { pinnedSessions, regularSessions } = splitPinnedSessions(visibleSessions); const { groups: projectGroups, ungroupedSessions } = groupSessionsByProject(regularSessions); const renderGroups = projectGroups.map((group, groupIndex) => { @@ -10951,7 +10992,7 @@ if (visibleSessions.length === 0) { const empty = document.createElement('div'); empty.className = 'session-list-empty'; - empty.textContent = '没有匹配的会话或项目。'; + empty.textContent = isSearchingSessions ? '没有匹配的会话或项目。' : 'MCP 会话已隐藏。'; sessionList.appendChild(empty); lastSessionListStructureSignature = structureSignature; return; @@ -11821,6 +11862,14 @@ }); } + if (mcpSessionVisibilityToggle) { + mcpSessionVisibilityToggle.addEventListener('click', () => { + showMcpSessions = !showMcpSessions; + persistMcpSessionVisibilityPreference(showMcpSessions); + renderSessionList(); + }); + } + if (advancedSearchOpen && advancedSearchPanel) { advancedSearchOpen.addEventListener('click', openAdvancedSessionSearch); advancedSearchClose?.addEventListener('click', () => closeAdvancedSessionSearch()); diff --git a/public/index.html b/public/index.html index 5b45345..e83def4 100644 --- a/public/index.html +++ b/public/index.html @@ -63,6 +63,13 @@ + diff --git a/public/style.css b/public/style.css index 607a941..2d46e20 100644 --- a/public/style.css +++ b/public/style.css @@ -1252,11 +1252,13 @@ body.session-loading-active { color: var(--text-primary); outline: none; } -.advanced-search-open { +.advanced-search-open, +.mcp-session-visibility-toggle { appearance: none; width: 34px; height: 34px; flex: 0 0 34px; + flex-basis: 34px; display: inline-flex; align-items: center; justify-content: center; @@ -1277,6 +1279,29 @@ body.session-loading-active { transform 0.16s ease, box-shadow 0.16s ease; } +.mcp-session-visibility-toggle svg { + width: 17px; + height: 17px; + fill: none; + stroke: currentColor; + stroke-width: 1.8; + stroke-linecap: round; + stroke-linejoin: round; +} +.mcp-session-visibility-slash { + opacity: 0; + transition: opacity 0.16s ease; +} +.mcp-session-visibility-toggle[aria-pressed='true'] { + background: var(--accent-light); + border-color: rgba(192, 85, 58, 0.34); + color: var(--accent); +} +.mcp-session-visibility-toggle[aria-pressed='true'] .mcp-session-visibility-slash { + opacity: 1; +} +.mcp-session-visibility-toggle:hover, +.mcp-session-visibility-toggle:focus-visible, .advanced-search-open:hover, .advanced-search-open:focus-visible { background: var(--accent-light); @@ -1285,6 +1310,7 @@ body.session-loading-active { outline: none; box-shadow: 0 0 0 3px rgba(192, 85, 58, 0.1); } +.mcp-session-visibility-toggle:active, .advanced-search-open:active { transform: translateY(1px); } @@ -2137,7 +2163,7 @@ body.session-loading-active { html[data-theme='cinder'], html[data-theme='gilded'], html[data-theme='wasteland'] -) .advanced-search-open { +) :is(.advanced-search-open, .mcp-session-visibility-toggle) { background: var(--dark-panel-soft); border-color: var(--theme-card-border); color: var(--text-secondary); @@ -2149,14 +2175,14 @@ body.session-loading-active { html[data-theme='cinder'], html[data-theme='gilded'], html[data-theme='wasteland'] -) .advanced-search-open:hover, +) :is(.advanced-search-open, .mcp-session-visibility-toggle):hover, :is( html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder'], html[data-theme='gilded'], html[data-theme='wasteland'] -) .advanced-search-open:focus-visible { +) :is(.advanced-search-open, .mcp-session-visibility-toggle):focus-visible { background: var(--accent-light); border-color: var(--theme-card-hover-border); color: var(--accent); @@ -2192,6 +2218,15 @@ html[data-theme='wasteland'] .advanced-search-open { clip-path: var(--wasteland-cut); } +html[data-theme='wasteland'] .mcp-session-visibility-toggle { + width: 36px; + height: 36px; + flex-basis: 36px; + border-color: var(--wasteland-line); + border-radius: 0; + clip-path: var(--wasteland-cut); +} + html[data-theme='wasteland'] .advanced-search-panel { --advanced-search-bg: #151715; --advanced-search-line: var(--wasteland-line); @@ -2309,6 +2344,7 @@ html[data-theme='wasteland'] .advanced-search-panel { @media (prefers-reduced-motion: reduce) { .advanced-search-open, + .mcp-session-visibility-toggle, .advanced-search-close, .advanced-search-submit, .advanced-search-pill { @@ -9502,6 +9538,12 @@ html[data-theme='wasteland'] .session-search-row .advanced-search-open { flex-basis: 36px; } +html[data-theme='wasteland'] .session-search-row .mcp-session-visibility-toggle { + width: 36px; + height: 36px; + flex-basis: 36px; +} + html[data-theme='wasteland'] .session-search::after { inset: 0; background-image: url('assets/themes/wasteland/frames/search.png'); diff --git a/scripts/regression.js b/scripts/regression.js index 56449fe..49be090 100644 --- a/scripts/regression.js +++ b/scripts/regression.js @@ -5377,6 +5377,120 @@ function assertMultiAgentV2CompatibilityContract() { assert(frontendSource.includes('entry.agentPath ? `路径: ${entry.agentPath}`'), 'Sub-agent cards should expose the canonical agent path'); } +function assertMcpSessionVisibilityContract() { + const frontendSource = fs.readFileSync(PUBLIC_APP_PATH, 'utf8'); + const indexSource = fs.readFileSync(PUBLIC_INDEX_PATH, 'utf8'); + const styleSource = fs.readFileSync(PUBLIC_STYLE_PATH, 'utf8'); + + const toggleMarkup = indexSource.match(/]*\bid="mcp-session-visibility-toggle"[^>]*>/)?.[0] || ''; + assert(toggleMarkup, 'MCP-created session visibility toggle should exist as a stable sidebar button'); + [ + 'type="button"', + 'title=', + 'aria-label=', + 'aria-pressed="false"', + ].forEach((attribute) => { + assert(toggleMarkup.includes(attribute), `MCP-created session visibility toggle should expose ${attribute}`); + }); + assert(/\bclass="[^"]*\bicon-only\b[^"]*"/.test(toggleMarkup), 'MCP-created session visibility toggle should be an icon-only button'); + const searchRowIndex = indexSource.indexOf('class="session-search-row"'); + const toggleIndex = indexSource.indexOf('id="mcp-session-visibility-toggle"'); + const sessionListIndex = indexSource.indexOf('id="session-list"'); + assert( + searchRowIndex >= 0 && toggleIndex > searchRowIndex && sessionListIndex > toggleIndex, + 'MCP-created session visibility toggle should stay in the sidebar search controls before the session list' + ); + + assert( + /MCP_SESSION_VISIBILITY_STORAGE_KEY\s*=\s*['"][^'"]*mcp[^'"]*session[^'"]*visibility[^'"]*['"]/.test(frontendSource), + 'MCP-created session visibility should use a stable local persistence key' + ); + assert( + /localStorage\.getItem\(MCP_SESSION_VISIBILITY_STORAGE_KEY\)/.test(frontendSource) + && /localStorage\.setItem\(MCP_SESSION_VISIBILITY_STORAGE_KEY/.test(frontendSource), + 'MCP-created session visibility preference should round-trip through localStorage' + ); + const readPreferenceBlock = extractFunctionSource(frontendSource, 'readMcpSessionVisibilityPreference'); + const createReadPreference = new Function( + 'localStorage', + 'MCP_SESSION_VISIBILITY_STORAGE_KEY', + `${readPreferenceBlock}; return readMcpSessionVisibilityPreference;` + ); + assert(createReadPreference({ getItem: () => null }, 'test-key')() === true, + 'MCP-created sessions should remain visible when no preference has been stored'); + assert(createReadPreference({ getItem: () => 'false' }, 'test-key')() === false, + 'Stored hidden preference should restore hidden MCP-created sessions'); + assert(createReadPreference({ getItem: () => { throw new Error('blocked'); } }, 'test-key')() === true, + 'Unavailable localStorage should conservatively keep MCP-created sessions visible'); + assert(frontendSource.includes('const mcpSessionVisibilityToggle = $(\'#mcp-session-visibility-toggle\')'), + 'Frontend should bind the MCP-created session visibility toggle by its stable id'); + assert( + /mcpSessionVisibilityToggle\.setAttribute\('aria-pressed',\s*[^)]*\)/.test(frontendSource), + 'MCP-created session visibility toggle should synchronize aria-pressed' + ); + + const filterBlock = extractFunctionSource(frontendSource, 'sessionPassesMcpVisibilityFilter'); + assert( + filterBlock.includes('normalizedSearchQuery') + && /if\s*\(\s*normalizedSearchQuery\s*\)\s*return true;/.test(filterBlock), + 'Non-empty sidebar search should bypass the MCP-created session visibility filter' + ); + assert( + /createdFromKind[\s\S]*['"]mcp['"]/.test(filterBlock), + 'MCP-created session visibility filter should detect sessions createdFromKind=mcp' + ); + assert( + filterBlock.includes('session.id === currentSessionId') && filterBlock.includes('session.isRunning'), + 'MCP-created session visibility filter should keep the current session and running sessions visible' + ); + const createVisibilityFilter = new Function( + 'showMcpSessions', + 'currentSessionId', + `${filterBlock}; return sessionPassesMcpVisibilityFilter;` + ); + const hiddenFilter = createVisibilityFilter(false, 'mcp-current'); + const visibleFilter = createVisibilityFilter(true, 'mcp-current'); + const regularSession = { id: 'regular', createdFromKind: 'user', isRunning: false }; + const hiddenMcpSession = { id: 'mcp-hidden', createdFromKind: 'mcp', isRunning: false }; + const pinnedMcpSession = { ...hiddenMcpSession, id: 'mcp-pinned', pinnedAt: '2026-08-26T00:00:00.000Z' }; + const currentMcpSession = { ...hiddenMcpSession, id: 'mcp-current' }; + const runningMcpSession = { ...hiddenMcpSession, id: 'mcp-running', isRunning: true }; + assert(hiddenFilter(regularSession, '') === true, 'Normal sessions should stay visible while MCP-created sessions are hidden'); + assert(hiddenFilter(hiddenMcpSession, '') === false, 'Inactive MCP-created sessions should be hidden'); + assert(hiddenFilter(pinnedMcpSession, '') === false, 'Pinned MCP-created sessions should still be hidden when inactive'); + assert(hiddenFilter(currentMcpSession, '') === true, 'Current MCP-created session should stay visible'); + assert(hiddenFilter(runningMcpSession, '') === true, 'Running MCP-created session should stay visible'); + assert(hiddenFilter(hiddenMcpSession, '主动查找') === true, 'Search should temporarily restore matching MCP-created sessions'); + assert(visibleFilter(hiddenMcpSession, '') === true, 'Visible preference should keep every MCP-created session visible'); + + const toggleBindingStart = frontendSource.indexOf('if (mcpSessionVisibilityToggle) {'); + const toggleBindingEnd = frontendSource.indexOf('\n }', toggleBindingStart); + const toggleBindingBlock = frontendSource.slice(toggleBindingStart, toggleBindingEnd); + assert(toggleBindingStart >= 0 + && toggleBindingBlock.includes("addEventListener('click'") + && toggleBindingBlock.includes('persistMcpSessionVisibilityPreference(showMcpSessions)') + && toggleBindingBlock.includes('renderSessionList()'), + 'MCP-created session visibility toggle should persist the new preference and rerender the sidebar'); + + const renderSessionListBlock = extractFunctionSource(frontendSource, 'renderSessionList'); + assert( + renderSessionListBlock.includes('sessionPassesMcpVisibilityFilter(session, normalizedSearchQuery)'), + 'Session list rendering should apply the MCP-created session visibility filter with the current search query' + ); + + const baseToggleStyle = styleSource.match(/\.mcp-session-visibility-toggle\s*\{[^}]*\}/)?.[0] || ''; + assert( + /width:\s*34px;/.test(baseToggleStyle) + && /height:\s*34px;/.test(baseToggleStyle) + && /flex-basis:\s*34px;/.test(baseToggleStyle), + 'Base MCP-created session visibility toggle should keep a stable 34px square footprint' + ); + assert( + /html\[data-theme='wasteland'\] \.mcp-session-visibility-toggle\s*\{[^}]*width:\s*36px;[^}]*height:\s*36px;[^}]*flex-basis:\s*36px;/.test(styleSource), + 'Wasteland MCP-created session visibility toggle should align to the 36px search row controls' + ); +} + function assertAdvancedSessionSearchContract() { const serverSource = fs.readFileSync(SERVER_PATH, 'utf8'); const frontendSource = fs.readFileSync(PUBLIC_APP_PATH, 'utf8'); @@ -6352,6 +6466,11 @@ async function main() { console.log('Advanced session search regression checks passed.'); return; } + if (regressionTarget === 'mcp-session-visibility') { + assertMcpSessionVisibilityContract(); + console.log('MCP session visibility regression checks passed.'); + return; + } if (regressionTarget === 'session-preview-metadata') { assertCcwebListConversationsScopeContract(); await runSessionPreviewMetadataRegression(); @@ -6425,6 +6544,7 @@ async function main() { assertSessionSwitchResilienceContract(); assertCodexAppGoalLifecycleContract(); assertSessionSwitchRaceContract(); + assertMcpSessionVisibilityContract(); assertAdvancedSessionSearchContract(); await assertAdvancedSearchTimeOrderingContract(); assertUsageStatisticsUnitChecks();