Skip to content

feat(adopt): /adopt 会话选择卡片改用 relay 同款 V2 展示(可搜索/分页/信息全) - #703

Open
xiaoxueSunn wants to merge 3 commits into
deepcoldy:masterfrom
xiaoxueSunn:feat/adopt-v2-picker
Open

feat(adopt): /adopt 会话选择卡片改用 relay 同款 V2 展示(可搜索/分页/信息全)#703
xiaoxueSunn wants to merge 3 commits into
deepcoldy:masterfrom
xiaoxueSunn:feat/adopt-v2-picker

Conversation

@xiaoxueSunn

@xiaoxueSunn xiaoxueSunn commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

📄 带截图的详细说明(问题现象 / 前后对比 / 真机效果)见内部文档(需飞书登录):https://bytedance.larkoffice.com/docx/JsXudz9IeoRpqnxbQSccJXtwn9d

如何发现

用户在新群 /adopt 时反馈「根本认不出该 load 哪一个」。当前卡片是两个 select_static 下拉,历史会话选项只拼「首句 prompt · 项目 · 时间」,而 codex 的 rollout 首句都以「你是一个只读诊断子 agent…」开头且被截到 40 字 → 前缀几乎一样,完全无法区分;也看不到 CLI 类型 / cwd / 会话 ID。用户要求做成接力(relay)那种 V2 卡片,信息多一点。

用户看到什么

触发方式 旧表现 修复后表现
/adopt 打开选择卡 两个下拉,单行纯文本,首句截断后几条长得一样,认不出 可搜索卡片列表,每条展示 来源/CLI/路径/会话 ID/时间
想按已知 session ID 选 下拉里没有 ID,选不了 搜索框输入 id / 项目 / cwd 直接定位;卡片显式展示会话 ID
历史会话较多 静默截断到 20,不提示 仍是 20(纯展示改造),命中上限时卡片提示「用搜索缩小范围」

改了什么

  • buildAdoptSelectCard 从两个 select_static 下拉重写为 relay 同款 V2 卡片:搜索框 + interactive_container 卡片列表(≤5/页)+ 分页 + 选中高亮 + 确认按钮。
  • 「运行中(tmux/zellij/herdr 活进程)」与「历史(磁盘 resume)」两类源合并进同一个可搜索列表,用「来源」标签区分;选中/确认按来源分派 startAdoptSessionstartResumeImportSession
  • 合成选择键 adoptLiveKeylive:tmux:<target>:<pid> / live:zellij:… / resume:<cliSessionId>),确定性可复现,re-render 重新发现也能对回。
  • 新增 services/adopt-picker.ts:首次渲染快照候选并按 root 消息缓存(5min TTL),搜索/翻页复用缓存、不再每次 re-shell-out 到 tmux;确认时才重新发现校验活进程(走 discoverAdoptableSessionByTarget 快路径,守住飞书 3s 回调预算)。
  • resume 上限保持 20(纯展示改造,非扩容)。
  • 卡片动作块移到 card-handler 通用 if (value?.action) 兜底之前,否则 adopt_confirm 会被兜底吞掉。

观察到的修复结果(测试)

  • test/card-builder.test.ts:重写 buildAdoptSelectCard 用例(live/resume 卡片、确认按钮门控、搜索过滤、分页、截断提示)——146 通过。
  • test/session-adopt.test.tsadopt_select 下拉用例改为 adopt_confirm——11 通过。
  • 新增 test/adopt-picker-cache.test.ts:缓存 store / TTL / clear——4 通过。
  • 回归:adopt-route / relay-picker / card-handler-relay-pickup / card-integration 通过。

仍未验证什么

剩余检查 需要的环境 / 原因 影响范围
真机飞书渲染 & 点击接入/恢复 需连真 bot 发 /adopt 交互 阻塞发布,不阻塞 PR 审阅
pnpm build 全量构建 本地用共享 store 离线安装,未跑完整 build 阻塞发布
全量 unit 中 11 个失败 均为 pre-existing:*-cli/workflow-c0 需构建 dist;card-handler-grant-partial 在 pristine master 同样失败 与本改动无关

🤖 Generated with Claude Code

## 如何发现
用户在新群 /adopt 时反馈「根本认不出该 load 哪一个」:卡片是两个
select_static 下拉,历史会话选项只拼「首句 prompt · 项目 · 时间」,而
codex 那几条 rollout 首句都以「你是一个只读诊断子 agent…」开头且被截到
40 字,前缀几乎一样 → 完全无法区分;也看不到 CLI 类型 / cwd / 会话 ID。

## 改了什么
- buildAdoptSelectCard 从两个 select_static 下拉重写为 relay 同款 V2 卡片:
  搜索框 + interactive_container 卡片列表(≤5/页)+ 分页 + 选中高亮 + 确认按钮。
- 运行中 / 历史两类源合并进同一个可搜索列表,用「来源」标签区分,选中/
  确认按来源分派 startAdoptSession(活进程)或 startResumeImportSession(磁盘)。
- 每条卡片展示:来源 / CLI / 路径(cwd) / 会话 ID / 时间(运行中=已运行,
  历史=最近活动)。搜索覆盖标题/项目/cwd/CLI/会话 ID,知道 id 可直接搜到。
- 合成选择键 adoptLiveKey(live:tmux:<target>:<pid> / live:zellij:… /
  resume:<cliSessionId>),确定性可复现,re-render 重新发现也能对回。
- 新增 services/adopt-picker.ts:首次渲染快照候选并按 root 消息缓存(5min TTL),
  搜索/翻页复用缓存、不再每次 re-shell-out 到 tmux;确认时才重新发现校验活进程
  (走 discoverAdoptableSessionByTarget 快路径,守住飞书 3s 回调预算)。
- resume 上限保持 20(纯展示改造,非扩容);命中上限时卡片给出提示指向搜索。
- 卡片动作块移到 card-handler 的通用 `if (value?.action)` 兜底之前,否则
  adopt_confirm 会被兜底吞掉。

## 测试
- test/card-builder.test.ts:重写 buildAdoptSelectCard 用例(live/resume 卡片、
  确认按钮门控、搜索过滤、分页、截断提示),+ 全 146 通过。
- test/session-adopt.test.ts:adopt_select 下拉用例改为 adopt_confirm,11 通过。
- 新增 test/adopt-picker-cache.test.ts:缓存 store/TTL/clear,4 通过。
- adopt-route / relay-picker / card-handler-relay-pickup / card-integration 回归通过。
- 全量 unit:仅 11 个失败,均为 pre-existing(*-cli 需构建 dist、grant-partial
  在 pristine master 同样失败),与本改动无关。

Co-Authored-By: Claude <noreply@anthropic.com>
@xiaoxueSunn
xiaoxueSunn requested a review from deepcoldy as a code owner August 2, 2026 08:52
真机渲染发现:ResumableSession 不带 cliId,历史条目 CLI 列显示"—"。
resume 只会提供 bot 自己那种 CLI,调用方本就知道,于是把 bot 的 cliId
透传进 buildAdoptEntries / buildAdoptSelectCard,历史行显示「CLI: Codex」。

Co-Authored-By: Claude <noreply@anthropic.com>
@xiaoxueSunn
xiaoxueSunn force-pushed the feat/adopt-v2-picker branch from 6fb49a0 to 4124e3e Compare August 2, 2026 09:58
@deepcoldy

Copy link
Copy Markdown
Owner

首审 (Claude) — 🔴 1 P1 blocker

整体质量高:V2 卡片镜像 /relay 很到位,pnpm build + 隔离单测(161)+ 回归套(76)全绿,与当前 master 无冲突。发现 1 个 zellij 回退型 P1,建议修后再合。

🔴 P1 — zellij 确认路径把 cliPid 塞回匹配 key,回退了 57dcbebbb 的专门修复

adoptLiveKey 对 zellij 生成 live:zellij:<session>/<paneId>:<cliPid>(card-builder.ts:2133),确认时只有 find(s => adoptLiveKey(s) === entryKey)(card-handler.ts:1874),无任何忽略 pid 的兜底

但 merge-base 老代码对 zellij 是「只按 (session, paneId) 匹配」,且有明确注释:

the resolved CLI pid can legitimately differ from the card's snapshot (wrapper⇄native pid shift), so requiring an exact pid match would spuriously report 已退出.

这正是 57dcbebbbfix(adopt): 点击候选改按 (session,paneId) 匹配…彻底消除误报已退出」修掉的 bug(原文:「cliPid 可能在 wrapper⇄native 间漂移…find 落空→误报『目标 CLI 会话已退出』」)。

后果:zellij 会话在「渲染→确认」窗口内 pid 漂移(wrapper 链塌缩 / re-fork),重新发现算出的 key ≠ 卡片 entryKeyfind 落空 → 点确认后误报「目标 CLI 会话已退出」。那个最多 3 次的重试救不了——重试只在 !target 时重跑,而重新发现每次都稳定算出同一个「pid 不同」的 key,重试只是重复 miss。

范围:仅 zellij。tmux 老代码本就按含 pid 的 adoptTargetKey 匹配(行为不变,非回归);herdr key 不含 pid(稳定)。

修法(小而干净):adoptLiveKey 的 zellij 分支去掉 pid → live:zellij:${session}/${paneId},恢复 (session, paneId) 语义。key 仍确定性,顺带修复 pid 漂移时「选中高亮跨 re-render 丢失」的连带问题。

🟡 P3(非阻塞)

  • 无匹配分支未转义 query:empty_filtered 把用户原始 searchQuery 未转义插进 markdown(t()interpolate 不转义),理论上 ![](http://x) 会渲染成图片(SSRF/追踪,见 fix(dashboard): 会话预览图标居中 + 浮层渲染 markdown 并保留换行 + 去掉 ••• 省略号 #697 教训)。但 /relayempty_filtered 完全同样处理(card-builder.ts:1905)——既有模式,非本 PR 引入;且仅 operator 触发、回显给自己。会话卡字段都走了 escapeMd,无问题。
  • 测试覆盖缺口:新增 handler 路径(adopt_pick/adopt_page/adopt_search re-render + zellij confirm)无 handler 级测试。上面的 zellij P1 恰落在未覆盖处——一个「渲染→pid 漂移→确认」的 handler 测试本可抓到。

✅ 已核实:build 通过(作者未跑全量,本机装依赖后跑通);command-handler 删除的 import 无残留、保留的仍在用;tmux/herdr key 往返正确;复用的 i18n key 全存在;adopt 卡走公开投递(非 ephemeral)→ 就地 patch 正确;动作块顺序在通用兜底之前、前面无更早 value.action 截胡。

@codex 复审 + 申晗确认;未获确认前不合码。

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

接首审 review。修两处:

1) 🔴 P1 — adoptLiveKey 对 zellij 生成的 key 含 cliPid,确认路径只按
   `adoptLiveKey(fresh) === entryKey` 匹配、无忽略 pid 的兜底,回退了
   57dcbeb「点击候选改按 (session,paneId) 匹配」的专门修复:zellij pane
   的 CLI pid 会在「渲染→确认」窗口内漂移(wrapper⇄native 塌缩 / re-fork),
   pid 进 key 导致确认时重新发现算出的 key 与卡片 entryKey 不一致 → find
   落空 → 误报「目标 CLI 会话已退出」。那个 3 次重试救不了(每次稳定算出同
   一个 pid 不同的坏 key,只是重复 miss)。
   修:zellij 分支去掉 pid → `live:zellij:${session}/${paneId}`,(session,
   paneId) 已唯一标识 pane。tmux/herdr key 不变(tmux 本就含 pid,确认快路径
   要解析尾部 pid;herdr key 无 pid)。加显式注释防再次塞回。

2) 🟡 P3 — 无匹配分支把用户原始 searchQuery 未转义插进 markdown 元素,
   `![](http://x)` 会渲染成图片(外链请求=追踪信标 / SSRF)。改走 escapeMd
   中和 [ ] 等。(buildRelayPickerCard 同样回显 query 有同样隐患,单独跟踪。)

测试:
- card-builder.test:新增 adoptLiveKey 不变量(zellij 同 (session,paneId)
  异 pid → 同 key;tmux 保持 pid 敏感)+ 无匹配 query 转义用例。
  变异验证:临时把 pid 塞回 zellij key,zellij 不变量用例即红。
- session-adopt.test:修正 adoptLiveKey mock 的 zellij 分支去 pid,与真实
  实现一致。
- 隔离单跑 164 全绿;adopt/relay/card/cli-send-dispatch 全量回归 278 全绿;
  pnpm build 通过。
@deepcoldy

Copy link
Copy Markdown
Owner

代作者修复(申晗授权)— head 611c78da7

首审的 1 P1 + 1 P3 已修,fast-forward 在原 head 上。

🔴 P1(已修)zellij 选择键去掉 cliPid

adoptLiveKey 的 zellij 分支 live:zellij:${s}/${pane}:${pid}live:zellij:${s}/${pane},恢复 (session, paneId) 语义,回滚了对 57dcbebbb 的回退。加了显式 ⚠️ 注释防再塞回。tmux/herdr 一字未改(tmux 本就含 pid、确认快路径要解析尾部 pid;herdr key 无 pid)。确认分支 find(s => adoptLiveKey(s) === entryKey) 无需改——两侧都 pid-less 即对齐。

🟡 P3(已修)无匹配 query 转义

empty_filtered{ query: searchQuery }{ query: escapeMd(searchQuery) },中和 ![](url) 图片注入(外链请求 = 追踪信标 / SSRF)。注释标注 buildRelayPickerCard 有同样隐患、单独跟踪(本 PR 不扩面)。

测试

  • card-builder.test:新增 adoptLiveKey 不变量——zellij 同 (session,paneId) 异 pid → 同 key;tmux 保持 pid 敏感。变异验证有牙:临时把 pid 塞回 zellij key,该用例立即红。
  • 新增无匹配 query 转义用例(空列表会先短路到 card.adopt.empty,须放一条不匹配 entry 才走到 empty_filtered)。
  • session-adopt.test:同步把 adoptLiveKey mock 的 zellij 分支去 pid(避免 mock 与真实实现漂移)。
  • 隔离单跑 164 全绿;adopt-route/relay-picker/card-handler-relay-pickup/card-integration/cli-send-dispatch 全量回归 278 全绿;pnpm build 通过。

待 codex 复审 + 申晗确认;未获确认前不合码。

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex 复审 — ✅ 通过(head 611c78da7

本轮未发现新的阻塞项,首审的 P1 / P3 均已正确收口。

  • P1 zellij key:独立回看了 57dcbebbb 及当前完整链路。卡片确认严格执行 adoptLiveKey(fresh) === entryKey;zellij 发现会从当前进程树重新解析 cliPid,因此 snapshot PID 与 fresh PID 不同是合法窗口。旧写法会机械地产生不同 key,新写法只取 (zellijSession, zellijPaneId),两侧稳定对齐;tmux 仍保留含 PID 的既有 key 与单 pane 快路径,herdr 语义未变。本机目前没有活跃可接管的 zellij session,因此未人为创建/扰动 live pane,但代码路径、历史修复和 PID 变异用例三者证据一致。
  • P3 query 转义empty_filtered 现经 escapeMd 转义 []、反斜杠、反引号和尖括号,![](url) 不再能形成图片/链接;测试确实进入“有候选但过滤为空”分支,不是空列表短路假绿。
  • 覆盖取舍:接受当前的 adoptLiveKey 纯函数不变量作为本次 P1 的回归守卫;把 PID 加回会直接使该用例失败。为补一个 zellij confirm handler 用例而引入 resetModules/静态依赖重构,收益不足以要求本 PR 扩面。现有 confirm 失效路径与 adopt/relay/card 回归也已通过。
  • relay 同类隐患:是既有路径,建议单独跟踪并用小 PR 修复,不要求 #703 顺手扩面。

本地实际验证:

  • pnpm build
  • pnpm vitest run test/card-builder.test.ts test/adopt-picker-cache.test.ts test/session-adopt.test.ts test/adopt-route.test.ts test/relay-picker.test.ts test/card-handler-relay-pickup.test.ts test/card-integration.test.ts test/cli-send-dispatch.test.ts8 files / 278 tests 全绿
  • git diff --check
  • 与当前 upstream/master merge-tree 无冲突;GitHub 当前未配置/未上报 CI checks。

影响面核对:改动限于飞书 /adopt picker 的候选采集、V2 卡片与 callback;覆盖 tmux / zellij / herdr、live / resume 和所有 bot CLI 的共同入口,未改 worker、PTY/Tmux backend 或其它 IM。保留“需真机飞书交互后再发布”的既有待验证项。

批准仅代表复审通过;按约定不执行合并,等待申晗最终确认。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants