# 消息操作规范

> Status: canonical-spec
> Canonical path: `specs/message/actions.md`
> Related specs: [../collaboration/collaboration.md](../collaboration/collaboration.md), [rewind.md](rewind.md), [delivery-runtime.md](delivery-runtime.md)
> Scope: chat / reply thread 视图中的消息行操作层、线程入口、桌面端 hover 操作区与移动端长按操作菜单
> Non-goals: 不在本版规格中重做 thread panel、本体 composer、delivery diagnostics 语义或 rewind 授权规则

## 1. 目标

把 AgentTeam 的消息行操作收敛成类似 Slack 的交互模型，满足以下目标：

- 桌面端的 grouped message 绝不能再出现按钮覆盖正文
- 桌面端消息操作应是 row-level affordance，而不是正文里的 inline chrome
- 移动端不再在消息气泡上堆一排操作图标，而是改走 long-press action sheet
- thread 入口保持清晰可发现，但消息本体不演化成工具栏

本规范只约束消息行操作层的产品行为，不重写 reply thread 的整体导航模型。

## 2. 问题定义

当前桌面端在 `web/src/agentteam/workspace/MessageRow.tsx` 上有一个明确的布局问题：

- grouped continuation row 目前用 absolute 定位渲染桌面端操作按钮
- 这些按钮直接压在消息文本盒子的右上区域
- hover 后出现 `Reply in thread`、`Copy markdown`、`Rewind conversation`、`Inspect delivery` 时，会和正文发生视觉重叠

这背后还有一个更广义的交互问题：

- 消息操作被当成气泡上的 inline 装饰，而不是独立的交互层
- grouped row 和 first row 的按钮模型不一致
- 移动端当前仍保留 inline action icon，和 Slack 的移动端交互方向相反

## 3. Slack 对齐原则

本规范按 Slack 现有产品形态收敛消息操作层，但只采纳对 AgentTeam 有意义的交互原则，不要求像素级复刻。

### 3.1 桌面端

桌面端遵循以下原则：

- 消息操作属于整条 message row，而不是 markdown 内容流
- thread 入口是显式动作，不是覆盖正文后顺带得到的点击区
- 低频动作进入 overflow menu，不把所有能力都做成一排 hover icon

### 3.2 移动端

移动端遵循以下原则：

- 没有 hover toolbar
- 没有附着在消息气泡上的 inline action rail
- 低频操作从 long-press action sheet 暴露
- thread 入口通过显式 affordance 提供，而不是在气泡上继续叠图标

## 4. 不变约束

以下产品约束在本规范中保持不变：

- reply thread 仍然是独立的二级会话面，不在消息行内展开成子树
- `Rewind conversation` 只在当前产品本来允许的地方出现
- `Inspect delivery` 只在当前产品本来支持的地方出现
- grouped-message compaction 继续保留，消息是否合并展示不因本规范而改变
- 已有 thread summary 的消息，reply count / 最近回复作者摘要仍应显示在正文下方；头像只表示最近实际回复过的作者，不表示订阅者集合；工作流自动发送消息保留冻结真人作者用于鉴权和审计，但该真人不计入最近回复作者

## 5. 桌面端规范

### 5.1 行结构

桌面端消息行必须采用“内容层 + 悬浮操作层”的结构，而不是固定右侧 gutter。

- 左侧 gutter
  - author group 首条消息显示 avatar
  - grouped continuation row 显示 hover timestamp
- 中间内容层
  - author header
  - message body
  - attachments
  - reaction strip
  - thread summary footer
  - delivery summary footer
- 悬浮操作层
  - anchored 到 message row
  - 视觉上位于消息上沿右侧或右上外缘附近
  - 默认安静，hover / focus-within 时显式显示

强制要求：

- 悬浮操作层不能进入 markdown 文本的排版流
- hover 不得改变正文宽度
- hover 不得引发正文重新换行
- 悬浮操作层虽然视觉上贴近消息上沿，但不能遮挡正文、author header 或 footer
- first row 与 grouped continuation row 使用同一套桌面端悬浮操作模型

实现含义：

- 允许 toolbar 看起来“浮在消息上方”
- 但它必须落在 message row 的安全交互区里，而不是直接压在正文盒子上
- 这层 affordance 应被理解为 row-anchored floating toolbar，不是正文右侧的 inline chrome

### 5.2 顶层操作

为对齐 Slack 的密度控制，桌面端顶层操作应尽量少。

桌面端顶层操作只保留：

- `Reply in thread`
- `More actions`

以下动作必须进入 overflow menu：

- `Copy markdown`
- `Rewind conversation`（若当前消息允许）
- `Inspect delivery`（若当前消息支持）

理由：

- `Reply in thread` 是唯一需要常驻顶层优先级的高频动作
- `Copy`、`Rewind`、`Inspect delivery` 都是低频动作
- 收进 overflow 后能显著降低遮挡风险，并让消息行更接近 Slack 的信息密度

补充说明：

- reaction pills 仍挂在消息正文下方，作为该消息的附属状态行存在
- 桌面端 `Add reaction` 允许出现在悬浮 toolbar 中，但它应使用 emoji-style add-reaction 图标，而不是通用 `+`
- 桌面端 `Add reaction` 是一个 **picker trigger**，不是与 `Reply in thread`、`Copy markdown`、`Rewind` 同类的消息语义操作
- 点击桌面端 `Add reaction` 后，emoji picker 应以 row-anchored dropdown / popover 形式展开，而不是把候选 emoji 铺在消息正文下方
- picker 应在最上方用 emoji-only 分类 tabs，搜索框放在 tabs 下方；点击 tab 需要滚动到对应分类，滚动 emoji 列表时也要反向更新当前高亮 tab
- picker 默认建议集不应只是少量固定 emoji，而应提供更接近通用聊天产品的完整开源 emoji dataset
- 底部 composer 输入框旁可以复用同一套 emoji symbol trigger / catalog / search / `Add emoji` 选择能力，但不要把额外 trigger 塞到 reaction picker 的搜索输入框右侧
- 桌面端 picker 打开后，点击其外部区域应直接收起；如果因此不再 hover/focus 当前消息行，floating toolbar 也应一起消失
- 桌面端 message row picker 与底部 composer / thread composer picker 都应给足宽高，避免完整 emoji dataset 被裁切或显得过于局促
- 移动端 long-press action sheet 顶部应先显示左上角的小号 `Message actions` 提示，再展示一排常用 reaction quick picks；这一排最右侧提供 `more` 图标，点击后再以上滑 bottom sheet / drawer 打开完整 reaction picker
- 如果搜索内容不是当前建议集中的 emoji，也应提供显式 `Add emoji` 入口来提交 Unicode emoji 或 shortcode
- 现有 reaction pill 本身需要承担“查看是谁 reacted”的职责：桌面端 hover pill 时显示 reactor 名单浮层，移动端对 pill 长按后显示同类名单；直接点 pill 仍然是 add/remove 自己的 reaction，不改成打开名单

### 5.3 Thread 入口

桌面端 thread 入口必须显式。

允许的 thread 入口只有：

- 消息正文下方的 thread summary footer
- 悬浮操作层中的 `Reply in thread`

禁止行为：

- 不能因为消息存在 thread，就让整行 message row 变成可点击
- 不能让 markdown 正文暗含“点正文就开 thread”的行为

理由：

- row-wide click 会和文本选择、链接点击、内容扫描冲突
- 显式 thread 入口更容易解释，也更接近 Slack 的交互模型

### 5.4 Grouped Continuation Row

grouped continuation row 必须沿用与首条消息相同的桌面端操作模型。

要求：

- grouped row 的左侧 gutter 仍负责显示 hover timestamp
- grouped row 使用与首条消息一致的悬浮操作层，而不是压在正文盒子上的 absolute overlay icon
- grouped row 不显示 avatar
- grouped row 不显示 author header
- grouped row 仍支持和首条消息相同的桌面端顶层操作

非目标：

- 不为 grouped row 再额外发明一套独立的操作系统

### 5.5 Thread Summary Footer

thread summary footer 继续作为正文下方的独立内容行存在。

要求：

- reply count、participant avatar、last-reply metadata 继续位于正文下方
- 对已有 thread 的消息，footer 仍是最主要、最显式的 thread affordance
- footer 保持可点击
- 悬浮操作层不能压到 footer 文案或 avatar summary 上

## 6. 移动端规范

### 6.1 消息行模型

移动端消息行不再展示 inline action icon。

要求：

- 移除消息行中的 inline `Reply in thread`、`Copy markdown`、`Rewind conversation`、`Inspect delivery` 图标簇
- 消息行视觉上只承担内容展示，不承担操作栏
- reaction pills 与 add-reaction 控件继续贴近消息内容展示，不并入 long-press action sheet
- 对已有 replies 的消息，thread summary footer 继续显示

### 6.2 操作入口

移动端低频操作统一从 long-press action sheet 进入。

long-press menu 项目：

- `Reply in thread`
- `Copy markdown`
- `Rewind conversation`（若当前消息允许）
- `Inspect delivery`（若当前消息支持）

reaction 说明：

- reaction add/remove 不是 long-press menu 项目
- reaction 保持为消息行内的轻量状态控件，避免把“轻量反馈”与“低频操作”混成同一类菜单项
- 移动端 long-press 打开的 action sheet 最上方应提供一排常用 reaction 作为 quick picks；这一排位于 action list 之上，但不算作 long-press menu 项目本身
- 在移动端，如果需要 picker，应以 bottom sheet / drawer 形式从底部弹出，而不是以内联方式把候选 emoji 铺在消息正文下方，也不要把候选 emoji 混入 long-press menu
- 移动端 reaction picker 打开后，向下拖拽 sheet 或使用浏览器返回手势，都应先关闭 picker，而不是直接离开当前消息上下文

规则：

- 不支持的动作直接省略，不显示 disabled item
- 默认排序按使用频率优先：
  - `Reply in thread`
  - `Copy markdown`
  - `Rewind conversation`
  - `Inspect delivery`

### 6.3 Thread 入口

移动端 thread 入口必须清晰且不干扰内容阅读。

允许的 thread 入口：

- 点击消息行本体，直接打开该消息的 reply thread
- 点击 thread summary footer
- long-press 后选择 `Reply in thread`

禁止行为：

- 不再提供移动端 inline action rail
- 不把 thread 打开入口藏成一排 inline icon 覆盖在消息气泡本体上

说明：

- channel 主聊天流中，单击消息行本体是移动端进入 thread 的主入口
- long-press 继续承担“打开消息操作菜单”的职责，不取代单击进入 thread 的默认路径
- 移动端通过消息行、thread summary footer 或 long-press 菜单进入 thread 时，只做导航，不自动聚焦 composer，也不应触发系统输入法弹出

### 6.4 Grouped Message

移动端 grouped message 保持纯内容导向。

要求：

- grouped continuation row 不显示 inline action
- grouped message 之间不额外插入操作条
- grouped message 的长按菜单行为与普通消息一致

## 7. 桌面端 Overflow 与移动端 Long-Press Contract

桌面端 overflow menu 与移动端 long-press action sheet，在语义上应尽量对齐。

公共动作集合：

- `Reply in thread`
- `Copy markdown`
- `Rewind conversation`（若允许）
- `Inspect delivery`（若支持）

命名规则：

- desktop 和 mobile 的同一动作，文案语义应保持一致
- 除非交互语义确实不同，否则不要为了平台差异改写动作名称

## 8. 可访问性

桌面端要求：

- 悬浮操作层不能只在 hover 时出现，也要在 keyboard focus-within 时可见
- overflow menu 必须可通过键盘进入
- thread summary footer 必须可聚焦
- toolbar 的浮动定位不能导致 tab 顺序混乱或 focus ring 脱离所属 message row

移动端要求：

- long-press 对应的关键动作，也应能通过平台已有的可访问性 action 或上下文菜单暴露
- 不能把关键操作只藏在过于隐蔽的视觉线索之后

## 9. 实现落点（非规范性说明）

主要改动面预计在：

- `web/src/agentteam/workspace/MessageRow.tsx`

可能关联的实现面：

- `web/src/agentteam/workspace/ThreadMessages.tsx`
- workspace UI 中已经存在的 shared menu / popover primitive

建议的桌面端重构方向：

- 把当前“压在正文盒子上的 absolute 按钮”改成 row-anchored floating toolbar
- toolbar 定位在消息上沿右侧或右上外缘附近，而不是正文内部
- 中间内容列保持 `min-w-0`
- 用 `opacity` 控制显隐，而不是用插入/删除节点改变布局
- 如果需要 absolute 定位，它的 containing block 也应是 message row 外层，而不是正文文本容器

建议的移动端重构方向：

- 去掉 `md:hidden` 这类 inline action group
- 增加消息 long-press 后的 action sheet
- thread 打开入口只保留在 footer 与 action sheet 中

## 10. 验收标准

当且仅当以下条件同时成立时，本规范算实现完成：

1. 桌面端 grouped message 不再出现按钮覆盖正文
2. 桌面端顶层动作收敛为 Slack 风格的精简主动作
3. 桌面端 thread 入口改为显式入口，而不是整行可点
4. 移动端不再展示 inline action rail
5. 移动端低频动作可通过 long-press 获得
6. thread summary 继续作为独立、可见的 footer-level affordance 存在
