# 任务板分页规格

> Canonical path: `specs/task/board-pages.md`
> Status: active-spec-2026-04-23
> Aligns with:
> - `SPECS.md` 第 2.8 节
> - `specs/task/board.md`
> - `specs/collaboration/collaboration.md` 第 8 节和第 11 节
> - `docs/architecture-spec.md` 第 7 节（`agentteam-task`）
> - `docs/tech_design.md` 第 4.7.5 节（当前 compat surface）
> 范围：workspace task hub 与 channel `List` / `Board` 的 browse/query/filter/sort/count/pagination 行为
> 非目标：重新定义任务状态、task detail、review process、task discussion，或 agent-native task execution 语义

## 1. 目标

本 spec 定义了 task surface 在规模超出“简单内存列表”之后应遵循的 canonical browse contract。

它正式确认了四个产品决策：

1. Task browse 默认采用增量式加载，而不是一次性全量加载。
2. Search、filter、sort、counts 与 pagination 属于同一套共享的 server-side browse contract。
3. `Workspace Task Hub`、channel `List` 和 channel `Board`，都是同一个概念性 browse model 上的不同投影。
4. Channel board 的 pagination 以列为单位，而 workspace hub 与 channel list 的 pagination 以行为单位。

## 2. 在 Canonical Specs 中的位置

本 spec 负责：

- workspace hub 和 channel task surface 的 browse query 语义
- pagination、cursor 和 count 语义
- global query state 与各 surface projection 之间的 contract
- first paint 限制与 incremental loading 规则

本 spec 不替代：

- `specs/task/board.md` 中关于 board columns、task detail、review 与 task-native lifecycle 的语义
- `specs/collaboration/collaboration.md` 中关于 task handoff、notify scope 与 collaboration boundaries 的定义
- `specs/identity/identity.md` 中关于 identity-sensitive presentation 与 targeting rules 的定义
- `docs/tech_design.md` 中关于当前已落地 handler/layout 细节的描述

## 3. 核心产品原则

### 3.1 增量式 browse 是默认路径

无论是 `Workspace Task Hub`、channel `List` 还是 channel `Board`，都不能要求在用户开始 browse、search、sort 或 filter 之前，先一次性加载完整的可见 task 集合。

规范性含义：

- pagination 是产品 contract 的一部分，不是后续才补的优化

### 3.2 Query 语义是全局的，不是 client-local 的

Search、sort 与 filters 必须先作用于完整的可见 task 集合，再进行 pagination。

规范性含义：

- client 不能只拉取任意一个 task 前缀，再把对这个前缀做出来的本地过滤结果当成权威 browse 结果

### 3.3 Root-task-first 仍然是默认行为

Workspace hub、channel list 和 channel board 的默认 browse 集合，仍然遵循 root-task-first。

规范性含义：

- subtasks 不会被摊平进入默认的 top-level browse 集合
- subtask inclusion 仍然必须显式启用、按需选择
- task detail 仍然是 subtask 展开与管理的主要 surface

## 4. 共享 Query Model

Workspace hub、channel `List` 与 channel `Board` 共享同一个概念性的 `TaskBrowseQuery`，即使它们的 response shape 不同。

概念上，canonical query fields 包括：

- `limit`
- 搜索词
- 排序模式
- 状态过滤
- 快捷过滤
- 结构化过滤，例如 assignee、Review Request reviewer、kind 和 priority
- `includeSubtasks`
- 行级 surface 使用的 opaque `cursor`
- board-aware surface 使用的 `columnCursors`，按当前 board status columns 的 column key 携带各列自己的 opaque cursor；系统默认 board 的 column key 是 `todo`、`in_progress`、`in_review`、`done`

规则：

- 默认 browse page size 是 `20`
- 对 row-based surface，`limit` 表示当前页的行数上限；对 board-aware surface，`limit` 表示当前请求中每个被请求列的卡片数上限
- implementation 可以允许请求更大的 limit，但 service 应把它 clamp 到一个有界上限，而不是把无界 fetch 当成合法行为
- 任何 search、sort 或 filter 的变化，都会使之前的 pagination state 失效，并从第一页重新开始 browse
- `cursor` 与 `columnCursors[*]` 都是 opaque server token，不能要求 client 对其进行解析
- row-based surface 使用单个 `cursor`
- board-aware surface 不使用 board-global cursor；client 必须按列携带 pagination state
- 对 board-aware surface，缺失的 `columnCursors[columnKey]` 表示该列从第一页开始

交互模型：

- automatic infinite scroll 和显式 `Load more` 都是合法交互，只要它们复用同一套 cursor 语义
- Browser 的 workspace 与 channel Task `List / Board` 使用 automatic infinite scroll：List 在共享滚动视口接近末尾时续取 row cursor，Board 在各列接近末尾时只续取该列 cursor，不常驻显示 `Load more` 按钮
- 已加载的 List rows 与 Board cards 使用共享滚动视口上的窗口化渲染，只保留可见区附近的 DOM；虚拟化不得改变服务端 ordering、count、cursor、selection、keyboard navigation 或 board drag/drop 语义
- 用户感知到的 search、sort 和 filtering，必须是针对完整结果集的属性，而不是对“当前恰好已经加载出来的那些 rows”做变换

## 5. 共享 Response Model

Paginated task browse response 必须携带足够的信息，让 client 能渲染当前 rows/cards，并理解剩余结果集的大致形状。

概念上，canonical page metadata 包括：

- `queryEcho`
- `totalCount`
- row-based surface 顶层的 `hasMore`
- row-based surface 顶层的 `nextCursor`
- board-aware surface 各列自己的 `hasMore`
- board-aware surface 各列自己的 `nextCursor`

`queryEcho` 必须反映服务端实际执行的 search、sort、quick filters、structured filters 与 subtask inclusion。Client 在请求这些条件后如果收到不一致的 `queryEcho`，必须把响应视为不兼容或失败，不能静默展示未过滤的全量结果。

Count 语义：

- 每个 paginated browse surface 都必须暴露当前 filtered query 对应的 `totalCount`
- 具备 board 语义的 browse surface，必须暴露当前 board column counts。系统默认 board columns 的 counts 是：
  - `todo`
  - `in_progress`
  - `in_review`
  - `done`
- count 语义针对的是当前 query 的全局结果，而不是从当前已加载 page 推断出来的

## 6. Workspace Task Hub

`Workspace Task Hub` 在同一份 query state 上支持 `Board / List`。`List` 在展示层按 channel 分组；`Board` 按 lifecycle status 分列，并在 card 上保留 channel identity。

规则：

- workspace hub 是跨 channel 的聚合入口，不改变 Task 的 channel ownership 或 ACL
- workspace `Board` 使用共享的 `todo / in_progress / in_review / done` status columns；改变 status 仍然只更新原 Task，不改变所属 channel
- workspace `List` 按 channel 分组；两种 presentation 都必须显示足够的 channel identity 以支持标注和导航
- 需要在 channel 上下文操作 task 时，用户进入对应 channel board；该 board
  使用 status-driven 列（todo / in_progress / in_review / done），并通过
  task lifecycle action（claim、unclaim、block、unblock、review）改变 task
  状态
- `List` pagination 作用于 task rows，而不是 channel blocks；`Board` pagination 遵循列级 cursor 语义
- search、sort 和 filters 必须先作用于用户完整可见的 task 集合，再进行 pagination
- 当只加载了第一页时，workspace hub 不能把自己表现成“结果已经完整”
- workspace Task 一级入口不使用常驻二级导航；页面首帧不得恢复或短暂渲染其他入口的二级导航。仅 Task 页面 Header 的 identity 区复用二级 sidebar 的 icon + title、description 两行样式，其他页面 Header 不受影响；`Board / List` 位于其最右侧 action 区。system/saved view preset 位于 toolbar 起始位置并排在 search 前，与 status、channel、assignee、其他 filter、sort 和 `Board / List` 控件共享同一份 view state；display 字段使用独立的 browser-local workspace/user preference
- workspace Task 的 Header、View、search、filter、sort、display 与 presentation controls 构成稳定 shell，不能等待 browse 数据或整页 lazy route 完成后才挂载；下面的 List/Board 数据区独立显示 loading，并按 row/column pagination 渐进填充
- toolbar 入口命名为 View / 视图；system view 固定按 All tasks、Created by me、Related to me、Blocked 排列，个人 saved view 在其下方独立分组。system view 不可修改；当前 state 偏离 system view preset 后，View 入口显示 Custom / 自定义，并仍以原 preset 作为 restore baseline。个人 saved view 被修改时保留原名并标记未保存状态。创建 saved view 时，对话框必须从当前 state 初始化，并允许在保存前配置名称、Board/List、search、sort、channel、status、assignee、kind 与 priority；saved view 支持 update、restore 与 delete
- workspace toolbar 在 search 后按 channel、status、assignee 排列，kind 与 priority 收入其他 filter；与 system view 重复的 Related to me、Created by me、Blocked 不得再作为普通 filter 重复出现。filter option 只显示名称、不显示任务数量；支持 `None` 的维度中，`None` 与具体值互斥

### 6.1 Filter option discovery

Workspace toolbar 的动态 filter candidates 通过独立于 browse page 的聚合 contract 获取：

- `GET /api/v1/workspaces/:workspaceId/tasks/filter-options` 一次返回 `channel`、`assignee`、`kind` 与 `priority` 四个维度，首屏不能按维度拆成四个请求
- candidates 来自当前用户完整可见的 root Task 集合，并复用 workspace browse 的 scope membership ACL；不能从当前 page、当前 presentation column 或当前 active filter 的结果子集推断完整候选集合
- `status` 是固定 lifecycle vocabulary，由 client 直接提供，不需要动态 discovery
- 每个动态维度返回有界的 `options`、`hasMore` 与可选 opaque `nextCursor`；option 包含稳定 `value`、展示 `label` 与 `standard / inactive / legacy / none` tone，不返回 Task count
- 大型维度的后续 discovery 搜索或分页继续复用同一个 URL，通过 `dimension`、`q`、`cursor`、`limit` 与重复的 `include` query 参数表达；Channel Task surface 通过可选 `channelId` 把候选集约束到固定 Channel，同时仍保留 workspace membership ACL；`include` 只能补回当前用户仍可见的选中值，不能绕过 ACL
- assignee popup 必须支持按名字搜索 Human 与 Agent。显式搜索继续使用同一个 Task filter-option URL，并通过 `dimension=assignee`、`q` 与当前可选 `channelId` 表达；这些请求只在用户输入后发生，不属于页面首屏请求。client 不能在 Task domain 中复制或并发调用第二套 Human/Agent directory search contract
- discovery candidates 不随当前 browse query 自收窄。改变 channel、assignee、kind 或 priority filter 只刷新 browse results，不应清空或重新挂载 filter popup

在 client 与 server 独立升级的兼容窗口内，client 可以在 discovery route 不存在、失败或某个维度为空时，从已缓存的 browse page 补出可用 candidates。该 fallback 只是交互可用性保障，不是完整候选集合的权威来源；discovery response 可用时必须优先采用，并允许 cached browse values 仅补齐当前已显示或已选中的缺项。无论 discovery 与 browse 处于 loading、error 还是 empty 状态，filter popup 都必须显示稳定选项或明确的空状态，不能渲染成空白容器。

## 7. Channel `List`

Channel `List` 复用与 workspace hub 相同的 browse/query model，只是 scope 固定为单个 channel。

规则：

- pagination 作用于 task rows
- 默认 browse 仍然遵循 root-task-first
- search、sort 和 filters 继续沿用与 workspace hub 相同的 server-side 语义
- toolbar 复用 workspace Task 的 filter component 与维度配置；固定 Channel 不重复显示 Channel filter，inline 维度是 status、assignee，kind 与 priority 位于附加 Filter menu，semantic quick filters 不在此重复出现
- Task card field visibility 与 workspace Task 入口共享同一个 browser-local workspace/user preference，不属于 Channel view state

## 8. Workspace / Channel `Board`

Workspace 与 Channel `Board` 都不是一个单一的 board-global page stream。

规则：

- board pagination 以列为单位，而不是共享一个 board-global cursor
- board 请求必须复用同一套 shared query state，并显式按 `columnCursors` 携带列级 pagination state
- board 请求可以一次请求一列，也可以一次请求多列；无论 transport shape 如何，pagination 语义都必须保持 column-local
- 当请求携带一个或多个 `columnCursors` 时，服务端可以只为这些列返回 card body，
  同时仍为所有列返回 count / `hasMore` 等轻量 metadata；客户端必须把这种响应作为
  column-local continuation，而不是全板首屏刷新。
- board 的 search、sort 和 filters 仍然必须先全局作用于当前 board 的 task 集合，再把结果投影到各列
- 各列不能发明与 shared browse query 不兼容的本地 filter 语义
- 每一列都必须能够报告：
  - `count`
  - loaded items
  - `nextCursor`
  - `hasMore`
- `done` 默认采用 count-first：折叠状态下的 `done` 可以只返回 count，而不必急切加载 card body，直到用户展开或显式请求更多
- first paint 必须保持有界；当把默认 browse page 盲目乘到每一列会造成不必要的 payload 和渲染膨胀时，产品不应这么做

推荐的 first-paint 行为：

- active columns 可以先加载一个有界的首屏切片，典型范围是 `10-20`
- `done` 在 first paint 时可以加载 `0` 个 items，同时仍然报告自己的 count

## 9. Cursor、Sort 与 Realtime 规则

Cursor 语义在 live task churn 下必须保持稳定。

规则：

- offset-based pagination 不是这些 task surface 的 canonical model
- cursor 的有效性绑定在当前 active 的 search/sort/filter state 上
- 每种 sort mode 都必须有确定性的 tie-breaker，避免在时间戳或 rank 相同的情况下 cursor 漂移
- task create、claim、unclaim、status change 和 review change 都可能使当前显示的结果形状失效
- realtime update 之后，page contents 和 counts 的 source of truth 仍然是 service

V1 预期：

- 当 live task changes 让本地结果变旧时，client 可以刷新受影响的 page 或 columns，而不是实现复杂的本地 rebase 逻辑

## 10. Compatibility 与 Rollout

这个 spec 设计为可渐进落地。

推荐顺序：

1. 先定义 shared browse/query contract
2. 落地 workspace hub pagination
3. 落地 channel `List` pagination
4. 落地按列分页的 channel `Board` pagination
5. 当所有 first-party task browse surface 都用上 canonical contract 后，退役 full-fetch browse path

兼容性规则：

- rollout 期间当前 compat APIs 可以继续存在，但它们不定义 task browsing 的长期产品 contract
