From f6db3c77095a160ec2af1d78b4b458e41b753fde Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Thu, 17 Sep 2026 12:03:47 +0000 Subject: [PATCH] =?UTF-8?q?docs(tasks):=20=E4=BC=9A=E8=AF=9D=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E5=8D=95=E6=94=B9=E4=B8=BA=E5=B8=B8=E9=A9=BB=E5=A4=96?= =?UTF-8?q?=E5=A3=B3=20layout=20=E6=96=B9=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P --- docs/tasks/README.md | 2 +- ...ASK-session-list-single-source-20260917.md | 39 ++++++++++++------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 21cd507f..c8c9d6b0 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -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 起 | 待领取 | — | ### 个人报告 diff --git a/docs/tasks/TASK-session-list-single-source-20260917.md b/docs/tasks/TASK-session-list-single-source-20260917.md index ddab18cd..0faf6495 100644 --- a/docs/tasks/TASK-session-list-single-source-20260917.md +++ b/docs/tasks/TASK-session-list-single-source-20260917.md @@ -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 起**(上一单占 924–925;开工时核对 `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` 里的 `
` 整段搬到这里;首页只剩 `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` 搬到 provider;401 时清空并标 `signedOut`。 +- **首页改造**:`const [sessions, setSessions] = useState([])` 改为从 provider 取;启动 effect 不再自己 `fetchSessions()`,改 `await ready`,其余(账户、目录、详情、落点选择、entry-summary、今日星语)不变。`use-session-management.ts` 保留选会话、新建、换模型、校正会话延迟切换这些需要首页状态的部分,参数表相应缩短。 +- **侧栏控制注册**:首页独有的控制(新建对话、选会话、正在打开 / 打开失败的行状态、账户菜单开关、`escapeBlocked`)通过 provider 的 `registerShellControls()` 在首页挂载时注册、卸载时清空;layout 把注册到的 `controls` 传给 `AppSidebar`。另外三页没注册:会话行仍是 `Link` 到 `/?c=`,但改名、置顶、归档、删除、翻页这些 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` 的 `` 正则:搬到对 `(app)/layout.tsx` 的断言,`escapeBlocked` 来自注册值。 + - BUG-745 的防复发「layout 里 `` 恰好一次、且不传 `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/` 不变(详情仍可读)。 - 客户端:`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` 单调递减。