docs(tasks): 会话列表单改为常驻外壳 layout 方案

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
This commit is contained in:
Jesse_Chen
2026-09-17 12:03:47 +00:00
co-authored by Claude Fable 5.1
parent 8eb1de1fa1
commit f6db3c7709
2 changed files with 25 additions and 16 deletions
+1 -1
View File
@@ -131,7 +131,7 @@
| `TASK-rectification-p0-fix-20260915.md` | `PROGRESS-rectification-p0-fix-20260915.md` | **验收修复单**`f51e494c` 六条缺陷全部实现且方式正确,但 `page.tsx` 从 1951 涨到 1964 行,撞了 `chart-view-route.test.ts``<= 1951` 上限(AGENTS.md §6 增长冻结)。全量 fail 32→33,就这一条。门禁红很可能是 staging 停在 `2d7698ea`、6 个提交未部署的原因。修法是把 BUG-705 的十来行接线搬出 page.tsx,不放宽上限 | 待验收 | `codex/rectification-p0-fix-20260915` |
| `TASK-settings-dialog-size-and-nav-20260915.md` | — | **复发单**:设置弹窗四个分区尺寸仍随内容跳变(BUG-698,复发自 BUG-554——旧防复发只查「有没有写 height」,查不到「写了没生效」);首要嫌疑是 `.settings-modal``dvh` 没有 `vh` 回退,不支持时整条 `height` 作废退化成内容高度,需先复现确认。另按产品要求去掉分区菜单左侧强调条,并拆开与悬停共用的选中态 | 待领取 | `codex/settings-dialog-size-and-nav-20260915` |
| `TASK-consult-followup-tool-contract-20260917.md` | `PROGRESS-consult-followup-tool-contract-20260917.md` | 真机:申报时段会话连发「?」「你在说什么鬼」都 `run.failed runtime_contract_incomplete`,回执无任何 `tool` 步骤。根因是 Agent 系统指令写明「简单追问可复用已有 packet / context、不调工具」,而 `contractReady()` 要求每次请求恰好一次成功排盘调用;「已有 packet」跨请求并不存在(缓存只在单次请求内)。本命与窗口两个 Agent 同构。**产品拍板方案 1**:每轮必调工具(BUG-922 删例外句 + BUG-923 第 0 步 `toolChoice: required`);否决「没调工具就走不扣点纯对话」。第一轮正经问题为何失败留 T4 取证(回执只在 web 容器日志) | 待验收 | `codex/consult-followup-tool-contract-20260917` |
| `TASK-session-list-single-source-20260917.md` | — | 会话列表一处数据源:本地 PG 兼容层 `order()` 只保留最后一键,`/api/sessions` 实际按 `id` 排、与游标不一致;`/` 与次级页两份数据源、`/` 每次回来重启动;空「新对话」落库堆积(首页 50 条里 28 条);标题类别在后、同名靠墙钟 HH:MM。串行在 composer-guard 单之后。BUG 段 926 起 | 待领取 | — |
| `TASK-session-list-single-source-20260917.md` | — | 会话列表一处数据源:本地 PG 兼容层 `order()` 只保留最后一键,`/api/sessions` 实际按 `id` 排、与游标不一致;`/` 与次级页两份数据源、`/` 每次回来重启动(产品拍板:首页与三个次级页进同一路由组,侧栏外壳与列表 provider 常驻 layout,不做 store;空「新对话」落库堆积(首页 50 条里 28 条);标题类别在后、同名靠墙钟 HH:MM。串行在 composer-guard 单之后。BUG 段 926 起 | 待领取 | — |
### 个人报告
@@ -4,7 +4,7 @@
- 基线 commit`eea90926``origin/staging` head)。**串行在 `TASK-rectification-session-composer-guard-20260917.md` 之后**:那一单改 `use-session-management.ts` 的回退路径,本单从它合入后的 staging 起分支,开工时以当时的 `origin/staging` 为准。
- 分支:`codex/session-list-single-source-20260917``git worktree add -b codex/session-list-single-source-20260917 .worktrees/session-list-single-source-20260917 origin/staging`
- 范围:`frontend/src/lib/db/local-postgres-client-core.ts`(排序)、`api/sessions` 两个路由、`lib/sidebar-data-cache.ts` / `hooks/use-sidebar-data.ts` / `hooks/use-session-management.ts` / `app/(secondary)/layout.tsx` / `components/app-sidebar.tsx` / `components/sidebar-session-row.tsx``lib/agent-reply.ts` 的标题函数、`lib/home-profile.ts` 的侧栏标题/副标题、`lib/session-groups.ts`,以及 `page.tsx`(只允许减行)。不动 Python、不动 Skill。**T3 若选服务端过滤方案不动表结构**;若产品另批清理迁移再动表并真跑 `npm run test:db`
- 范围:`frontend/src/lib/db/local-postgres-client-core.ts`(排序)、`api/sessions` 两个路由、`frontend/src/app/` 路由组重排(`page.tsx``(secondary)/*` 移入 `(app)/`)、新建 `lib/session-list-context.tsx` / `hooks/use-session-list.ts``hooks/use-session-management.ts`、删除 `lib/sidebar-data-cache.ts` / `hooks/use-sidebar-data.ts``components/app-sidebar.tsx` / `components/sidebar-session-row.tsx``lib/agent-reply.ts` 的标题函数、`lib/home-profile.ts` 的侧栏标题/副标题、`lib/session-groups.ts`,以及 `page.tsx`(只允许减行)。不动 Python、不动 Skill。**T3 若选服务端过滤方案不动表结构**;若产品另批清理迁移再动表并真跑 `npm run test:db`
- BUG 段:**BUG-926 起**(上一单占 924925;开工时核对 `docs/BUG_HISTORY.md` 最大号)。
## 1. 事故实证
@@ -46,15 +46,15 @@ Bug 历史检索:BUG-553(列表只按置顶排)、BUG-557(标题守卫 0
- **标题**:普通会话 = 模型标题,否则第一问前 14 字;第一次回答落库前显示「新对话」但不入列。校正会话 = 「生时校正 · M月D日」;今日节奏 = 「今日节奏 · M月D日」——**类别在前,日期在后**,扫一眼能分类。同日多条不再加墙钟「HH:MM」,改为副标题区分。
- **副标题**(两处侧栏都显示一行小字):`M月D日 HH:MM`(会话创建时间,Asia/Shanghai+「 · <盘主名>」(仅他人盘)。校正会话再补状态词:进行中 / 已交付 / 已结束(从 entry-summary 或列表字段取;取不到就不显示,不得猜)。
- **存量数据**:旧标题不批量改(BUG-699 已做过一次校正会话标题修复,且手改标题不能动);只对未来新建生效。空会话**不删**,只是不入列;若产品负责人另批清理,另开迁移单。
- 不推翻 BUG-553 / BUG-699 / BUG-745 的任何防复发措施
- `page.tsx` 不得增长;`Home()` 拥有的乐观更新与回滚层保留,只是数据源换成共享 store
- 产品负责人 2026-09-17 拍板:**侧栏外壳提到路由组 layout,首页也进去,侧栏在四个页面之间不卸载;不做模块级 store**。这推翻 BUG-745 防复发里「layout 不传 `controls`」与 `TASK-sidebar-unify-20260916` D1「次级页侧栏只读、管理操作留在首页」两条:管理操作里只依赖列表的部分(改名、置顶、归档、删除、翻页)上移到 provider,四页都能用;依赖首页状态的(新建、选会话、校正打开)仍由首页注册。BUG-553 / BUG-699 的防复发不动
- `page.tsx` 不得增长(本单应明显减少)`Home()` 的乐观更新与回滚层保留,只是列表 state 的所有者换成 provider
## 4. 硬红线
1. 兼容层 `order()` 改多键后,所有既有单键调用行为不变;有 SQL 文本级测试。
2. 服务端列表排序键与 `sessionCursorFilter` 的键必须一致(`updated_at desc, id desc`),有测试锁住。
3. 元数据 PATCH 仍不写 `updated_at`BUG-553);打开已有会话不改 `title` / `updatedAt`BUG-699`session-open-preserves-identity.test.ts` 必须保持绿)。
4. 次级页不得再挂第二份 `SidebarProvider`BUG-745);侧栏源码不得出现 `window.location`
4. `app/` `SidebarProvider` 只在 `(app)/layout.tsx` 出现一次BUG-745 口径升级);侧栏源码不得出现 `window.location``/login``/admin*` 不得带侧栏
5. 既有测试断言不得静默弱化;改断言写「原值 / 新值 / 原因」三栏。测试总数不低于开工时。
6. 无 Docker 时 `npm run test:db` 失败照基线记录,不得写成通过。
@@ -69,22 +69,31 @@ Bug 历史检索:BUG-553(列表只按置顶排)、BUG-557(标题守卫 0
- `frontend/tests/session-cursor.test.ts``chat-session-authority.test.ts` 加断言:列表路由源码的排序键序列与 `sessionCursorFilter` 的键一致。
- 有 Docker 时 `npm run test:db` 加一条:插入三条不同 `updated_at` 的会话,`GET /api/sessions?limit=2` 返回最新两条且 `nextCursor` 翻页拿到第三条、无重叠。无 Docker 写 `BLOCKED.md`
### T2 一份会话列表 store,`/` 与次级页共用BUG-927
### T2 应用外壳提到路由组 layout,首页也进去,侧栏在四个页面之间不卸载BUG-927
- 新建 `frontend/src/lib/session-list-store.ts`:模块级 store`useSyncExternalStore`),按账户 id 分区,保存列表行、游标、账户摘要、`fetchedAt`;提供 `readSessionList()``subscribe``hydrateFromServer()``applyLocalWrite(op)`create / rename / pin / archive / delete / activityBump)与 `revalidate()`(后台重拉,先渲染现有行)。`sidebar-data-cache.ts` 并入或删除,不留两份
- `Home()` 启动:若 store 已有本账户列表,**先用它**渲染侧栏与落点选择,再后台 `revalidate()`;没有才拉。`Home()` 现有的 `sessions` state 继续作为详情(消息)的所有者,但列表行的来源与写操作统一通过 store;`updateSession` / `persistSession` 成功后同步 `applyLocalWrite`
- `(secondary)/layout.tsx` `useSidebarData()` 改读同一 store;不再自己 `/api/sessions`,除非 store 为空或过期(60 s 保留)。
- `/` ↔ 次级页往返不得再触发 `/api/sessions` `/api/account`60 s 内);`invalidateSidebarCache()` 改为 `applyLocalWrite`,不清空整份
产品负责人 2026-09-17 拍板:**不做 store,做常驻外壳**。侧栏、会话列表、账户摘要只有一个所有者,就是 layout;四个页面切换只换中间内容
- **路由组**:新建 `frontend/src/app/(app)/``app/page.tsx` 移到 `(app)/page.tsx``(secondary)/chart|ephemeris|reports|reports/[reportId]` 移到 `(app)/` 下,`(secondary)` 目录删除。URL 不变;`/login``/admin*``error` / `not-found` 留在外面,不得带侧栏。每页自己 `metadata` / `dynamic` 声明原样保留(`reports``force-dynamic` 是页级的,不影响 `/` 的 Static)。
- **外壳 layout**`(app)/layout.tsx`client= `SessionListProvider` + `SidebarProvider` + `AppSidebar` + `SidebarInset`。首页现在 `page.tsx` 里的 `<SidebarProvider escapeBlocked=…><main className="chat-app">…<AppSidebar …/>` 整段搬到这里;首页只剩 `SidebarInset` 里的内容
- **`SessionListProvider`**(新文件 `frontend/src/lib/session-list-context.tsx` + `frontend/src/hooks/use-session-list.ts`)拥有:`sessions: ChatSession[]``setSessions`、分页游标、归档视图开关、账户摘要(侧栏页脚用)、`signedOut`。签入后每个标签页只拉一次列表;暴露一个 `ready` Promise 给首页启动用。只依赖 `sessions` + fetch 的元数据操作(`renameSession``togglePinnedSession``toggleArchivedSession``deleteSession``loadMoreSessions``toggleArchivedView`)从 `use-session-management.ts` 搬到 provider401 时清空并标 `signedOut`
- **首页改造**`const [sessions, setSessions] = useState<ChatSession[]>([])` 改为从 provider 取;启动 effect 不再自己 `fetchSessions()`,改 `await ready`,其余(账户、目录、详情、落点选择、entry-summary、今日星语)不变。`use-session-management.ts` 保留选会话、新建、换模型、校正会话延迟切换这些需要首页状态的部分,参数表相应缩短。
- **侧栏控制注册**:首页独有的控制(新建对话、选会话、正在打开 / 打开失败的行状态、账户菜单开关、`escapeBlocked`)通过 provider 的 `registerShellControls()` 在首页挂载时注册、卸载时清空;layout 把注册到的 `controls` 传给 `AppSidebar`。另外三页没注册:会话行仍是 `Link``/?c=<id>`,但改名、置顶、归档、删除、翻页这些 provider 自带的操作在四页都可用;新建对话在三页上是 `Link``/`。高亮只在 `/` 上有。
- **删除**`hooks/use-sidebar-data.ts``lib/sidebar-data-cache.ts``tests/sidebar-data-cache.test.ts`(改写成 provider 测试,不是删掉测试数量)。
- **合同测试改写**(每条写三栏「原值 / 新值 / 原因」):
- `sidebar-contract.test.ts` 「the same component renders read-only when `/` is not the one mounting it」:原值断言次级 layout 不传 `controls`;新值断言 layout 只传 `registerShellControls` 注册到的 `controls`,未注册即只读。
- `sidebar-contract.test.ts` 第 313 行附近对 `page.tsx``<SidebarProvider escapeBlocked={accountMenuOpen || modalOpen}>` 正则:搬到对 `(app)/layout.tsx` 的断言,`escapeBlocked` 来自注册值。
- BUG-745 的防复发「layout 里 `<SidebarProvider>` 恰好一次、且不传 `controls`」改为「`(app)/layout.tsx` 里恰好一次、`app/` 其它地方零次;`controls` 只来自注册」。`chart-page-view``ephemeris-page``personal-report-view` 三个测试里对 `(secondary)` 路径的引用改到 `(app)`
- 验收:
- `sidebar-data-cache.test.ts` 改为 store 测试:写入后两处读同一份;rename 后次级页立刻是新标题;60 s 内 `revalidate()` 不发请求;账户切换隔离
- `sidebar-contract.test.ts` / `home-bootstrap-reveal.test.ts``Home()` 有缓存时启动不 `/api/sessions`mock fetch 计数为 0),有缓存也不阻塞揭幕
- 真机清单(`docs/testing/`):DevTools Network 观察 `/``/chart``/ephemeris``/reports``/` 全程 `/api/sessions` 只出现一次;改名后各页标题一致
- `next build``/``○ Static``/chart``/ephemeris``/reports` 的渲染方式与基线一致
- 新测试:provider 挂载后 `/api/sessions` 恰好一次;首页有 provider 列表时启动不再请求 `/api/sessions`mock fetch 计数);在 `/` 改名后 provider 里的行立即更新;401 清空
- `page.tsx` 行数明显下降(外壳段 + 列表 state + 六个元数据操作的调用点都走了),进度记录写出前后行数
- 真机清单(`docs/testing/`):DevTools Network 观察 `/``/chart``/ephemeris``/reports``/``/api/sessions``/api/account` 全程各一次;侧栏 DOM 节点在四页之间不重建(Elements 面板选中侧栏节点,切页后仍是同一节点);改名、置顶、删除后另外三页立即一致;`/login``/admin` 无侧栏。
### T3 空会话不入列,且延迟落库(BUG-928)
- 服务端:`api/sessions` `GET` 排除 `session_type = 'consultation' AND messages = '[]'`(保留校正会话);置顶查询同样处理。`GET /api/sessions/<id>` 不变(详情仍可读)。
- 客户端:`startNewChat` 与启动时的落点会话改为**本地创建、不立即落库**,`ChatSession``persisted: boolean``send()` 在撤回窗口结束、`POST /api/consult` 之前若 `!persisted``writeChatSession(create)`(失败则回滚并提示,沿用 `startNewChat` 现有回滚文案)。校正会话仍由 `/cases/open` 服务端创建,不受影响。
- `visibleSessions` 的过滤保留(当前空会话只在 `/` 本地可见),次级页因服务端已过滤而自然一致
- `visibleSessions` 的过滤搬进 provider 的派生值(当前空会话只在 `/` 激活时可见),四页读同一个派生结果
- 让步:若延迟落库牵动 `?c=` 深链、刷新恢复(`pendingConsultationStorageKey`)过多,可只做服务端过滤 + 启动时**复用**已有空会话而不再新建,把延迟落库写进 `BLOCKED.md`
- 验收:
- `chat-session-authority.test.ts`:列表路由过滤条件存在;详情路由不过滤。
@@ -105,7 +114,7 @@ Bug 历史检索:BUG-553(列表只按置顶排)、BUG-557(标题守卫 0
## 6. 让步顺序
T1 > T2 > T3(服务端过滤部分)> T4 > T3(延迟落库部分)。T1 一项独立可发,若其余来不及,T1 单独推 staging 也值得
T1 > T2 > T3(服务端过滤部分)> T4 > T3(延迟落库部分)。T1 一项独立可发。T2 内部若元数据操作上移牵动过大,允许第一步只做「路由组 + layout 常驻 + provider 拥有列表 + 首页注册全部控制」,元数据操作上移写进 `BLOCKED.md` 作第二步;但外壳常驻与列表单一所有者不得让步
## 7. 开工前置命令
@@ -120,4 +129,4 @@ grep -n "^## BUG-" ../docs/BUG_HISTORY.md | tail -1
## 8. 验收口径
`tsc --noEmit` 0 错;`npm run lint` 0 error`npm test` 失败清单与基线逐条一致、新增测试全绿、总数不降;`next build``/` 仍 Static;首屏 gzip ±2%`page.tsx` 行数 ≤ 开工时;有 Docker 则 `npm run test:db` 通过,无 Docker 写 `BLOCKED.md`。部署后用 DevTools 复核 T2 的「一次 `/api/sessions`」与 `GET /api/sessions``updated_at` 单调递减。
`tsc --noEmit` 0 错;`npm run lint` 0 error`npm test` 失败清单与基线逐条一致、新增测试全绿、总数不降;`next build``/` 仍 Static;首屏 gzip ±2%`page.tsx` 行数 ≤ 开工时;有 Docker 则 `npm run test:db` 通过,无 Docker 写 `BLOCKED.md`。部署后用 DevTools 复核 T2 的「一次 `/api/sessions`、侧栏节点不重建,`GET /api/sessions``updated_at` 单调递减。