Skip to content

feat(desktop): 分支下拉加搜索框,支持按分支名过滤 - #3844

Open
wpr317694-del wants to merge 4 commits into
makecindy:mainfrom
wpr317694-del:feat/branch-picker-search
Open

feat(desktop): 分支下拉加搜索框,支持按分支名过滤#3844
wpr317694-del wants to merge 4 commits into
makecindy:mainfrom
wpr317694-del:feat/branch-picker-search

Conversation

@wpr317694-del

Copy link
Copy Markdown
Contributor

这次改了什么

摘要

新建会话页的 [⎇ 分支 │ ☑ worktree] 联合控件,左半分支区点开后是个纯列表,分支多了只能逐条肉眼找。这里在下拉顶部加了一个搜索输入框:打开即聚焦,按分支名做大小写不敏感的子串过滤,Enter 选第一个匹配项,↑↓ 在列表内移动,关闭时清空搜索词。

顺带把这个下拉的容器从 Radix DropdownMenu 换成了 Popover。原因是 menu 自带两条跟「面板里有个输入框」直接冲突的行为:

  1. typeahead — @radix-ui/react-menuContent.onKeyDown 对任意单字符按键调 handleTypeaheadSearch,打字会被拿去做菜单项跳转。这条能用 stopPropagation 挡掉。
  2. Item.onPointerMove 会把焦点抢到鼠标掠过的那一项 —— 打字时鼠标从列表上方经过就丢焦点,后续按键回落 typeahead。这条挡不掉。

仓内另两处「下拉里带搜索」的实现(OneshotModelPinPickerScriptCapabilityMultiSelect)同样是 Popover + onOpenAutoFocus,本次与它们对齐。

变更类型

  • feat 新功能

范围

  • 关联 Issue / 需求:无(用户口头反馈:分支多时下拉搜不到)
  • 本 PR 包含:分支下拉搜索框(含过滤、空匹配态、键盘导航)、5 语言文案、design-inventory 台账重新生成
  • 明确不包含:远程分支(origin/*)不纳入列表 —— 要改 main 侧 listBranches、IPC 契约、device-link 隧道,还得回答「选了远程分支怎么建 worktree」,不在本次范围;分支排序不动;worktree 勾选半区完全不动
  • 用户可见变化:分支下拉多一个搜索框;下拉面板圆角从 rounded-xl 保持不变,宽度上限 320px(原来只有 min-w-[200px] 无上限)
  • 是否存在 breaking change:无

UI 变化

未附截图,原因见「未执行的验证」。

  • 引用的设计规范:
    • docs/design-rules/DESIGN.md §4「Inputs & Forms」 — 搜索行按单行输入规范:pill 圆角(rounded-full)、--surface-elevated 填充、--border-default 1px 描边、placeholder 走 --text-placeholder(该节明确禁止用 Silver #a3a3a3,本次未引入任何写死色值)。
    • docs/design-rules/DESIGN.md §4「Select & Dropdown」 — 面板 12px 圆角(rounded-xl)+ Card bg,选项行用 8px 内层圆角(rounded-[8px]),内层半径小于容器半径。该节还要求面板宽度绑定 trigger 宽度;这个菜单改动前用的就是 min-w-[200px] 而非绑定值,本次只在面板内部加了一行控件、没有改这条既有形态,未新增违规,也没顺手去改(会牵动 compact / 非 compact 两套 chip 宽度,超出本 PR 范围)。
    • docs/design-rules/DESIGN.md §10「Light / Dark Dual-Mode Delivery Gate」 — 新增的搜索行、空匹配提示、列表行 hover / focus-visible 态全部走语义 token(--surface-elevated / --text-placeholder / border-border / bg-sidebar-item-hover / text-muted-foreground),无单模式硬编码、无条件补丁,两种模式由 token 系统覆盖。按该节最后一条:目检未做,不把「复用了 themed 样式」当成「双模式已验证」

怎么验证的

自动验证

pnpm --filter desktop run --if-present typecheck
结果:通过(无输出)

pnpm test:unit:related
结果:# tests 503 / # pass 497 / # fail 5
      5 个失败全部是本机环境导致的既有失败,与本 PR 无关,基线已证明(见下)

node --test scripts/__tests__/design-inventory.test.mjs
结果:# tests 47 / # pass 47 / # fail 0
      (首轮曾失败:radii 计数 411→412;已跑 pnpm design:inventory 重新生成台账)

pnpm check:i18n
结果:✅ zh-CN / zh-TW / en / ja / ko 共 8644 个 key 全部一致

pnpm check:i18n-glossary
结果:✅ 术语表 33 条已裁决 / 60 条待讨论,五语言无新增违规

pnpm check:dco
结果:DCO check passed: 1 commit signed off — range ec4bd863..96c57d90

npx eslint src/renderer/components/new-chat/WorktreeChipsRow.tsx
结果:No issues found

失败基线(在干净的 origin/mainec4bd8633)上 stash 掉本 PR 全部改动后跑同一批测试):

node --test scripts/__tests__/check-dco.test.mjs scripts/__tests__/migration-freeze.test.mjs \
             scripts/__tests__/test-workspaces.test.mjs scripts/__tests__/design-inventory.test.mjs
结果:# tests 161 / # pass 158 / # fail 3
  not ok - installer refuses to clobber an edited hook without --force
  not ok - new repository main baseline freezes companion runtime scripts
  not ok - symlink tests never skip solely because the host is Windows
  (design-inventory 在基线上全绿 → 它的失败确实由本 PR 引入,已修)

三个基线失败的本机原因,逐条有直接证据:

失败 根因 证据
check-dco.test.mjs installer 用例 本机全局 core.hooksPath 被指向别处,测试读到全局值 断言实际值 /Users/ccsa/.claude/ocr-local/githooksgit config --global core.hooksPath 输出逐字符一致
migration-freeze.test.mjs 用例自己的 cleanup()rmSync 临时 git 仓时 ENOTEMPTY 三条失败的 error 都是 ENOTEMPTY: directory not empty, rmdir '/var/folders/.../cindy-migration-freeze-*/.git';该文件只读 drizzle migration 与 scripts/,与 renderer 无依赖路径。并发下失败条数会从 1 条涨到 3 条
test-workspaces.test.mjs symlink 用例 工作区里存在两个 .cindy-worktrees/ 目录被扫进来 断言 diff 里的多出项全部以 .cindy-worktrees/auto-k6cgvq/.cindy-worktrees/auto-nv69fk/ 开头,ls -d .cindy-worktrees/* 正是这两个

手工验证

未执行。

未执行的验证

  • 实机目检(Light / Dark 两种模式)与截图:未做。pnpm restart:desktop:remote 会先关掉当前运行的 Desktop 实例,而提交这个 PR 的会话本身就跑在同一台机器的 Cindy Desktop 里,起 dev 实例有中断当前会话的风险,没有在未确认的情况下执行。因此双模式实际观感未经验证,只能说明新增样式全部走语义 token、未引入单模式硬编码。合并前建议由 reviewer 或作者补一次两模式目检 + 截图。
  • 未跑 pnpm test:all:本次改动只在 renderer 单文件 + i18n json + 一份生成的文档台账,无跨模块、无基础设施改动。CI 的完整 pnpm test:unit 仍会跑。

风险

风险分类

  • 无已知风险

影响与回滚

  • 影响范围:只影响新建会话页分支下拉的打开形态。useBranches 数据链、resolveBranchPickonSourceBranchChange、worktree 勾选、发送门禁、device-link 远程分支拉取一律未碰;main 进程 / IPC 契约 / preload / device-link allowlist 零改动,无跨端 wire protocol 变化;改动全在 apps/desktop,不进 mobile runtime fingerprint。
  • 回滚 / 降级方式:revert 本 commit 即可,无数据迁移、无配置开关、无持久化状态。

分支多的时候,分支 chip 点开是个纯列表,只能逐条肉眼找。这里在下拉顶部加了
一个搜索输入框:打开即聚焦,按分支名做大小写不敏感的子串过滤,Enter 选第一个
匹配项,↑↓ 在列表内移动,关闭时清空搜索词。

下拉容器从 Radix DropdownMenu 换成 Popover。菜单自己会把单字符按键拿去做
菜单项跳转(react-menu 的 Content onKeyDown → handleTypeaheadSearch),Item 的
onPointerMove 还会把焦点抢到鼠标掠过的那一项 —— 两条都跟"面板里有个输入框"
冲突,后者靠 stopPropagation 也绕不掉。仓内另两处带搜索的下拉
(OneshotModelPinPicker / ScriptCapabilityMultiSelect)同样是 Popover。

只动 renderer:main 侧 listBranches 已经返回全量本地分支,过滤放在渲染层做,
IPC 契约与 device-link 隧道零改动。远程分支(origin/*)不在本次范围。

design-inventory 台账随 radii 计数变化重新生成。

Signed-off-by: luojiajun <luo.jj@trip.com>
@wpr317694-del
wpr317694-del requested a review from a team as a code owner September 3, 2026 07:11
@greptile-apps

greptile-apps Bot commented Sep 3, 2026

Copy link
Copy Markdown

Greptile Summary

本 PR 将新建会话页的分支菜单改为带搜索框的 Popover,并补充五种语言的搜索与空结果文案。

  • 支持大小写不敏感的分支名过滤、Enter 选择首项及上下方向键导航
  • 关闭面板时清空搜索词,并保留加载失败重试入口
  • 重新生成设计规范台账中的圆角计数与快照日期
  • 自定义 listbox 的 Tab 顺序及 Home/End 导航仍需完善

Confidence Score: 4/5

该 PR 看起来可以合并,但建议先完善分支列表的 Tab 顺序和 Home/End 键盘导航。

搜索、过滤、选择及状态清理流程未发现阻塞性故障;剩余问题集中在自定义 listbox 相较仓内现有选择器缺少完整的键盘与辅助技术焦点管理。

Files Needing Attention: apps/desktop/src/renderer/components/new-chat/WorktreeChipsRow.tsx

Important Files Changed

Filename Overview
apps/desktop/src/renderer/components/new-chat/WorktreeChipsRow.tsx 分支下拉已改为可搜索 Popover,核心过滤和选择流程清晰,但自定义 listbox 没有采用仓内既有的 roving tabIndex 与完整键盘导航模式。
apps/desktop/src/renderer/i18n/locales/zh-CN/common.json 新增搜索占位符与无匹配结果文案,并已在五种语言中保持键一致。
docs/design-rules/design-inventory.md 生成台账更新了快照日期,并将相关界面的裸圆角计数增加一项。
Prompt To Fix All With AI
### Issue 1
apps/desktop/src/renderer/components/new-chat/WorktreeChipsRow.tsx:617-632
**列表框焦点管理不完整**

每个 `role="option"` 按钮都会进入默认 Tab 顺序,而且列表只处理上下方向键;分支较多时,键盘用户需要逐项 Tab 穿过列表,Home/End 也无法跳到首尾。请沿用仓内其他选择器的 roving `tabIndex` 与完整列表导航模式。

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "feat(desktop): 分支下拉加搜索框,支持按分支名过滤" | Re-trigger Greptile

Comment thread apps/desktop/src/renderer/components/new-chat/WorktreeChipsRow.tsx Outdated
自动 review 指出:每个 role="option" 都在默认 Tab 序里,分支上百条时键盘
用户要逐项 Tab 穿过整个列表 —— 而"分支很多"恰好就是这个搜索框要解决的场景。

改成仓内既有的 roving tabIndex 形态(见 HookConnectionsSection 的单选组):
首项 tabIndex=0、其余 -1,Tab 一次进列表、再 Tab 就走出去,组内换项靠 ↑↓ 与
Home/End。Home/End 只挂在列表容器上,搜索框里这两个键留给文本光标。

review 原话说"沿用仓内其他选择器的 roving tabIndex 模式",但实际对齐的那个
先例(OneshotModelPinPicker)本身没有 tabIndex 管理;这里按 HookConnectionsSection
的写法补,不回头改那个先例。

Signed-off-by: luojiajun <luo.jj@trip.com>
@wpr317694-del

Copy link
Copy Markdown
Contributor Author

已按 Greptile review 补上 8a31654e5:分支列表改成 roving tabIndex(首项 tabIndex=0、其余 -1),Tab 一次进列表、再 Tab 就走出去,组内换项靠 ↑↓ 与新加的 Home/End。Home/End 只挂在列表容器上,搜索框里这两个键留给文本光标。

一点更正:review 说「沿用仓内其他选择器的 roving tabIndex 模式」,但本 PR 原本对齐的那个先例 cindy-brain/OneshotModelPinPicker.tsx:239-399role="option" 按钮本身并没有 tabIndex 管理,ScriptCapabilityMultiSelectrole="option" 都没有。仓内真正有 roving 形态的是 components/settings/HookConnectionsSection.tsx:385tabIndex={selected ? 0 : -1} + 容器方向键,注释在 :322-354),本次按它的写法补,没有回头去改那两个先例。

Tab 穿越本身确实是真问题,而且「分支很多」恰好就是这个搜索框要解决的场景,所以这条采纳了。

补充验证:pnpm --filter desktop typecheck 通过;eslint No issues found;pnpm check:design-inventory 台账仍最新(只加了 tabIndex 与注释,radii 计数未变);pnpm test:unit:related 503 tests / 498 pass / 4 fail,4 个失败仍全部落在 PR 描述里已用 origin/main 基线证明的那三个本机环境文件内(check-dco 的全局 hooksPath、migration-freeze 的 ENOTEMPTY 临时目录、test-workspaces 的 .cindy-worktrees 扫描)。pnpm check:dco 2 commits signed off。

实机双模式目检仍未做,原因同 PR 描述「未执行的验证」段,未变。

@MagicLizi MagicLizi added awaiting-discussion 等待维护者讨论(review-pr) touches:product-ui 改动碰到产品 / UI 面(review-pr 自动维护,仅展示) touches:rules 改动碰到规则 / 规范文档(review-pr 自动维护,仅展示) labels Sep 3, 2026
@MagicLizi

Copy link
Copy Markdown
Contributor

@wpr317694-del 这条 PR 改了用户看得见的分支选择交互(下拉加搜索框),需要维护者确认后再继续合并流程。

请维护者直接在本 PR 上 Approve;如果要改,请 Request Changes。讨论 issue:#3845

单测意外抓到真 bug:pick() 里直接 setOpen(false),而受控 Popover 的
onOpenChange 只在 Radix 自己发起关闭时才触发 —— 程序化关闭不走那条回调,于是
"关闭即清空搜索词"被绕过。表现:搜 feishu、点中结果、再点开下拉,搜索框里还是
feishu、列表只剩一条。改成把关闭与清空收在一个 closePanel() 出口,选中项 /
Esc / 点面板外三条路径都走它。

两份测试:
- branchFilter.test.ts —— 把过滤逻辑抽成纯函数 filterBranches 后单测,覆盖
  大小写、首尾空格、`/` 中间段、无匹配、不改入参、空搜索词返回同一引用。
  抽文件而不是塞进 branchPick.ts:那边管"点中一项产生什么 effect",这边管
  "哪些项该显示",出于不同原因变化。
- branchPickerSearch.test.tsx —— jsdom + 真 Radix Popover(不 mock 容器)覆盖
  打开即聚焦、实时过滤、无匹配态、Enter 选首项、无匹配时 Enter 不动、点击选中、
  关闭清空、roving tabIndex、↓ 进列表、Home/End、loading/failed 两态、
  不可交互时不出下拉。为此把 BranchWorktreeChip 导出(仅供测试 render,不是对外 API)。

关于本地门禁的一处更正:pnpm test:unit:related 在本机跑不到 desktop 的 vitest。
test-workspaces.mjs:1077-1082 在 root test runner 非零退出时直接 return,而本机
root runner 因三个环境相关用例必然失败(全局 core.hooksPath 指向别处、
.cindy-worktrees 被扫描、临时目录 ENOTEMPTY),于是 workspace 测试一个都不执行。
本次改为手动补跑等价命令:vitest related --run 那 9 个改动文件(23 passed),外加
related 模式算不到的源码字符串断言型测试(newMakerProjectPicker /
newMakerCreateAgentVisualContract / newMakerWorktreeSend / branchPick /
pickBranchLabel,132 passed)——它们用 readFileSync 读源码、不 import 组件,
vitest 的模块图看不到这层依赖。

Signed-off-by: luojiajun <luo.jj@trip.com>
@wpr317694-del

wpr317694-del commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

补了单测(5a3673bf1),顺带被单测抓出一个真 bug。

抓到的 bug

pick() 里是 setOpen(false),而受控 Radix Popover 的 onOpenChange 只在 Radix 自己发起关闭时触发 —— 程序化关闭不走那条回调,于是「关闭即清空搜索词」被绕过。

复现:搜 feishu → 点中结果 → 再点开下拉,搜索框里还是 feishu、列表只剩一条。

改成把关闭与清空收在一个 closePanel() 出口,选中项 / Esc / 点面板外三条路径都走它。

两份测试

branchFilter.test.ts(9 例):先把过滤逻辑抽成纯函数 filterBranches,再测大小写不敏感、首尾空格忽略、/ 中间段匹配、无匹配、不改入参、空搜索词返回同一引用、不按相关度重排。抽成独立文件而不是塞进 branchPick.ts:那边管「点中一项产生什么 effect」,这边管「哪些项该显示」,出于不同原因变化。

branchPickerSearch.test.tsx(14 例,jsdom + 真 Radix Popover,不 mock 容器):打开即聚焦、实时过滤、无匹配态、Enter 选首项、无匹配时 Enter 不动也不关面板、点击选中、关闭清空、roving tabIndex(含过滤后)、↓ 进列表、列表内 ↓↑ 与 Home/End、loading / failed 两态、重试不关面板、不可交互时不出下拉。为此把 BranchWorktreeChip 导出,注释里写明仅供测试 render、不是对外 API。

一处需要更正前面的验证说明

前面 PR 描述里写的 pnpm test:unit:related 结果(503 tests / 498 pass),实际覆盖范围被我高估了:它在本机跑不到 desktop 的 vitest

根因在 scripts/test-workspaces.mjs:1077-1082

if (willRunTestRunner) {
  const runnerResult = await runRootTestRunner();
  if (runnerResult.exitCode !== 0) {
    process.exitCode = 1;
    return;        // ← workspace 测试一个都不跑
  }
}

root test runner 非零退出就直接 return。而本机 root runner 因那三个环境相关用例必然失败(全局 core.hooksPath 指向别处、.cindy-worktrees/ 被扫描、临时目录 ENOTEMPTY),于是 workspace 测试从来没执行过 —— 对应证据是四轮 test:unit:related 的日志里 vitest 输出为零、总数恒为 503。这不是本 PR 引入的,但我先前把它的结果当成了全量覆盖,这里更正。

补跑的等价命令与结果:

# 门禁在 related 模式下对 desktop 实际会执行的命令(buildPnpmArgs → vitestRelatedArgs)
cd apps/desktop && npx vitest related --run --passWithNoTests <本次 9 个改动文件>
结果:23 passed / 0 failed

# related 模式算不到的源码字符串断言型测试(用 readFileSync 读源码、不 import 组件,
# vitest 的模块图看不到这层依赖,必须点名跑)
npx vitest run --root apps/desktop \
  src/renderer/__tests__/newMakerProjectPicker.test.ts \
  src/renderer/__tests__/newMakerCreateAgentVisualContract.test.ts \
  src/renderer/__tests__/newMakerWorktreeSend.test.ts \
  src/renderer/__tests__/branchPick.test.ts \
  src/renderer/__tests__/pickBranchLabel.test.ts
结果:132 passed / 0 failed

pnpm --filter desktop typecheck        通过
eslint(4 个改动/新增文件)              No issues found
pnpm check:design-inventory            台账最新(新增 branchFilter.ts → files 计数 +1,已重新生成)
pnpm check:dco                         3 commits signed off

CI 上的完整 pnpm test:unit 不受本机这条 fail-fast 影响,仍是权威门禁。

实机双模式目检仍未做,原因同 PR 描述「未执行的验证」段,未变。

@MagicLizi

Copy link
Copy Markdown
Contributor

@wpr317694-del 👋 这个 PR 还有 1 条 review conversation 没 resolve(apps/desktop/src/renderer/components/new-chat/WorktreeChipsRow.tsx),auto-review 因此暂时跳过、没法继续审查 / 合并。

如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。

@MagicLizi

Copy link
Copy Markdown
Contributor

命中 UI 路径(apps/desktop/src/renderer/components/new-chat/WorktreeChipsRow.tsx / apps/desktop/src/renderer/components/new-chat/branchFilter.ts)但 description 未附界面效果证据——建议补充改动后效果:截图/录屏,或改动后界面的 HTML 页面(```html 代码块、.html 附件或在线预览链接),便于确认界面符合 DESIGN.md 设计规范

@MagicLizi

Copy link
Copy Markdown
Contributor

@wpr317694-del 👋 这个 PR 目前与 main 有合并冲突,auto-review 因此暂时跳过、没法继续审查 / 合并。

请在本地 merge 最新的 origin/main 解决冲突后推送;冲突解除后,下一轮 auto-review 会自动重新处理这个 PR。

…arch

Signed-off-by: luojiajun <luo.jj@trip.com>

# Conflicts:
#	docs/design-rules/design-inventory.md
@wpr317694-del

Copy link
Copy Markdown
Contributor Author

冲突已解决 + 补上界面效果证据

1. 合并冲突

已在本地 merge 最新 origin/mainc4033acd0)并推送,63d86d6ed

冲突只有一个文件:docs/design-rules/design-inventory.md。它是 scripts/design-inventory.mjs 的生成产物,所以没有手工合并 —— 取 main 侧版本后重新跑 pnpm design:inventory 生成,人工区由脚本自身保留。pnpm check:design-inventory 已确认 GENERATED 区块最新(34 个 surface),本次新增的 branchFilter.ts 也已计入。

merge 后重跑:

pnpm --filter desktop typecheck                通过
vitest run(branchFilter / branchPickerSearch / newMakerProjectPicker /
           newMakerCreateAgentVisualContract / newMakerWorktreeSend / branchPick)
                                               147 passed / 0 failed
pnpm check:design-inventory                    GENERATED 区块最新
pnpm check:dco                                 3 commits signed off, 1 exempt (merge)

2. 界面效果证据

按建议做了改动后界面的 HTML 页面(自包含单文件,无外部资源):

预览: https://htmlpreview.github.io/?https://gist.githubusercontent.com/wpr317694-del/8eea6b760b4dc02aefecc159aa83cb50/raw/branch-picker-preview.html
源文件: https://gist.github.com/wpr317694-del/8eea6b760b4dc02aefecc159aa83cb50

内容是 Light / Dark 并排,四组:改动前 → 改动后的展开态、输入 feishu 的过滤态、搜中间段 cindy/auto、以及无匹配 / 加载中 / 加载失败三种非常规态。末尾列了对照的 DESIGN.md 条款。

需要说明的一点:这是按代码里的实际 class 与 token 值复刻的静态预览,不是实机截图。 所有色值直接取自 apps/desktop/src/renderer/themes/colors.tsregisterColor 默认值(HSL 形式的换算成等价 hex),尺寸圆角间距按组件里写死的 Tailwind class 还原。它能证明取值与结构对得上规范,但不能替代实机观感确认 —— 作者未在本机起 Desktop 目检,原因写在 PR 描述「未执行的验证」段(起 dev 实例会关掉承载当前工作会话的 Desktop 实例)。如果这条对放行是硬性的,请直接说,我会安排实机截图后补上。

HTML 源码(自包含单文件,点开可直接另存为 .html 打开)
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>PR #3844 界面效果 — 分支下拉加搜索框</title>
<style>
  /* 全部色值取自 apps/desktop/src/renderer/themes/colors.ts 的 registerColor 默认值,
     HSL 形式的 token 已换算成等价 hex。左右两栏 = 同一份结构在 light / dark 下的取值。 */
  .light {
    --surface: #f8f8f6;
    --surface-elevated: #ffffff;
    --popover: #ffffff;
    --border: #e6e6e6;
    --border-default: #d7d7d4;
    --foreground: #262626;
    --muted-foreground: #737373;
    --text-placeholder: #c4c4c4;
    --sidebar-item-hover: #e5e5e5;
    --accent: #f5f5f5;
    --ca-control-bg: #FCFCFC;
    --ca-control-border: #DCDFE3;
    --ca-control-text: #3C3F43;
  }
  .dark {
    --surface: #1f1f1e;
    --surface-elevated: #2c2c2a;
    --popover: #272725;
    --border: #4a4745;
    --border-default: #3c3c3a;
    --foreground: #d4d4d4;
    --muted-foreground: #a8a29f;
    --text-placeholder: #525252;
    --sidebar-item-hover: #3c3c3a;
    --accent: #363330;
    --ca-control-bg: #393838;
    --ca-control-border: #434343;
    --ca-control-text: #D4D4D4;
  }

  * { box-sizing: border-box; }
  body {
    margin: 0;
    padding: 28px 20px 56px;
    background: #16161a;
    color: #e8e8ea;
    font: 14px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
  }
  h1 { font-size: 19px; margin: 0 0 4px; font-weight: 600; }
  .sub { color: #9a9aa2; font-size: 13px; margin: 0 0 8px; }
  .note {
    max-width: 900px; margin: 14px auto 26px; padding: 11px 14px;
    border: 1px solid #40404a; border-left: 3px solid #d0a215; border-radius: 8px;
    background: #24241f; color: #ded9c4; font-size: 12.5px; line-height: 1.7;
  }
  .head { max-width: 900px; margin: 0 auto; }
  h2 {
    max-width: 900px; margin: 34px auto 12px; font-size: 15px; font-weight: 600;
    color: #e8e8ea; padding-bottom: 7px; border-bottom: 1px solid #33333c;
  }
  h2 .cap { display: block; font-weight: 400; font-size: 12.5px; color: #9a9aa2; margin-top: 4px; }
  .pair { max-width: 900px; margin: 0 auto; display: flex; gap: 16px; flex-wrap: wrap; }
  .pane { flex: 1 1 400px; min-width: 300px; border-radius: 10px; overflow: hidden; border: 1px solid #33333c; }
  .pane-hd {
    font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
    padding: 6px 12px; background: #22222a; color: #8f8f98; border-bottom: 1px solid #33333c;
  }
  .stage { background: var(--surface); padding: 18px 18px 26px; }

  /* ── [⎇ 分支 │ ☑ worktree] 联合控件 (compact 变体, h-30px) ── */
  .chip {
    display: inline-flex; align-items: stretch; height: 30px; border-radius: 9999px;
    overflow: hidden; border: 1px solid var(--ca-control-border);
    background: var(--ca-control-bg); color: var(--ca-control-text);
  }
  .chip-branch {
    display: inline-flex; align-items: center; gap: 6px; padding: 0 8px 0 12px;
    font-size: 12px; font-weight: 500; max-width: 180px;
  }
  .chip-div { width: 1px; align-self: center; height: 14px; background: var(--ca-control-border); }
  .chip-wt {
    display: inline-flex; align-items: center; gap: 6px; padding: 0 12px 0 8px;
    font-size: 12px; font-weight: 500;
  }
  .box {
    width: 14px; height: 14px; border-radius: 3px; border: 1.5px solid var(--muted-foreground);
  }
  svg { display: block; flex: none; }

  /* ── 下拉面板 ── */
  .panel {
    margin-top: 6px; width: 240px; border-radius: 12px; padding: 6px;
    border: 1px solid var(--border); background: var(--popover); color: var(--foreground);
    box-shadow: 0 10px 15px -3px rgba(0,0,0,.28), 0 4px 6px -4px rgba(0,0,0,.28);
    display: flex; flex-direction: column; gap: 6px;
  }
  .search {
    display: flex; align-items: center; gap: 8px; border-radius: 9999px;
    border: 1px solid var(--border); background: var(--surface-elevated); padding: 5px 10px;
  }
  .search .txt { font-size: 13px; color: var(--foreground); }
  .search .ph { font-size: 13px; color: var(--text-placeholder); }
  .caret {
    display: inline-block; width: 1px; height: 14px; background: var(--foreground);
    vertical-align: -2px; margin-left: 1px;
  }
  .list { display: flex; flex-direction: column; max-height: 240px; overflow: hidden; }
  .row {
    border-radius: 8px; padding: 6px 12px; font-size: 13px; color: var(--foreground);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .row.sel { background: color-mix(in srgb, var(--accent) 60%, transparent); }
  .row.hover { background: var(--sidebar-item-hover); }
  .row.dim { color: var(--muted-foreground); }
  .icon-mut { color: var(--muted-foreground); }

  .tag {
    display: inline-block; margin-left: 6px; padding: 0 6px; border-radius: 4px;
    font-size: 10px; line-height: 16px; vertical-align: 1px;
    background: #2f3b4d; color: #8fb6e8; font-weight: 500;
  }
  .legend { max-width: 900px; margin: 10px auto 0; font-size: 12px; color: #8f8f98; }
  .legend code { background: #26262e; padding: 1px 5px; border-radius: 4px; font-size: 11.5px; }
</style>
</head>
<body>

<div class="head">
  <h1>PR #3844 — 分支下拉加搜索框</h1>
  <p class="sub">apps/desktop/src/renderer/components/new-chat/WorktreeChipsRow.tsx · branchFilter.ts</p>
</div>

<div class="note">
  <strong>这是按代码里的实际 class 与 token 值复刻的静态预览,不是实机截图。</strong>
  所有色值直接取自 <code>apps/desktop/src/renderer/themes/colors.ts</code><code>registerColor</code> 默认值(HSL 形式的已换算成等价 hex),
  尺寸、圆角、间距按组件里写死的 Tailwind class 还原(面板 <code>rounded-xl</code> + <code>p-1.5</code> + <code>min-w-[200px]</code>,
  行 <code>rounded-[8px] px-3 py-1.5 text-13</code>,搜索行 pill + <code>--surface-elevated</code> 填充)。
  作者未在本机起 Desktop 目检(原因见 PR 描述「未执行的验证」段),因此这份预览只能证明取值与结构,
  不能替代实机观感确认。
</div>

<h2>1. 改动前 → 改动后(默认展开态)
  <span class="cap">面板顶部多一行搜索框,打开即聚焦。列表本身的行样式、选中态、面板宽度都没动。</span>
</h2>
<div class="pair">
  <div class="pane light">
    <div class="pane-hd">Light · 改动前</div>
    <div class="stage">
      <div class="chip">
        <span class="chip-branch">
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>
          main
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="icon-mut"><path d="m6 9 6 6 6-6"/></svg>
        </span>
        <span class="chip-div"></span>
        <span class="chip-wt"><span class="box"></span>worktree</span>
      </div>
      <div class="panel">
        <div class="list">
          <div class="row">cindy/auto-k6cgvq</div>
          <div class="row">cindy/auto-nv69fk</div>
          <div class="row">fix/feishu-ack-emoji</div>
          <div class="row sel">main</div>
        </div>
      </div>
    </div>
  </div>

  <div class="pane dark">
    <div class="pane-hd">Dark · 改动后</div>
    <div class="stage">
      <div class="chip">
        <span class="chip-branch">
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>
          main
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="icon-mut"><path d="m6 9 6 6 6-6"/></svg>
        </span>
        <span class="chip-div"></span>
        <span class="chip-wt"><span class="box"></span>worktree</span>
      </div>
      <div class="panel">
        <div class="search">
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="icon-mut"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
          <span class="ph">搜索分支…</span><span class="caret"></span>
        </div>
        <div class="list">
          <div class="row">cindy/auto-k6cgvq</div>
          <div class="row">cindy/auto-nv69fk</div>
          <div class="row">fix/feishu-ack-emoji</div>
          <div class="row sel">main</div>
        </div>
      </div>
    </div>
  </div>
</div>
<p class="legend">
  左边是改动前的形态(<code>DropdownMenu</code> + 纯列表),右边是改动后(<code>Popover</code> + 搜索行 + 独立滚动列表)。
  两栏色板不同是为了同屏对照 light / dark,不代表改动前只有 light。
</p>

<h2>2. 输入 <code style="color:#c8c8d0">feishu</code> 后的过滤态
  <span class="cap">大小写不敏感子串匹配。首项带 tabIndex=0(图中以焦点底色示意),其余为 -1。</span>
</h2>
<div class="pair">
  <div class="pane light">
    <div class="pane-hd">Light</div>
    <div class="stage">
      <div class="panel">
        <div class="search">
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="icon-mut"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
          <span class="txt">feishu</span><span class="caret"></span>
        </div>
        <div class="list">
          <div class="row hover">fix/feishu-ack-emoji<span class="tag">tabIndex 0</span></div>
        </div>
      </div>
    </div>
  </div>
  <div class="pane dark">
    <div class="pane-hd">Dark</div>
    <div class="stage">
      <div class="panel">
        <div class="search">
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="icon-mut"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
          <span class="txt">feishu</span><span class="caret"></span>
        </div>
        <div class="list">
          <div class="row hover">fix/feishu-ack-emoji<span class="tag">tabIndex 0</span></div>
        </div>
      </div>
    </div>
  </div>
</div>

<h2>3. 搜中间段 <code style="color:#c8c8d0">cindy/auto</code>
  <span class="cap"><code>/</code> 参与匹配,可以直接搜命名空间中间段。</span>
</h2>
<div class="pair">
  <div class="pane light">
    <div class="pane-hd">Light</div>
    <div class="stage">
      <div class="panel">
        <div class="search">
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="icon-mut"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
          <span class="txt">cindy/auto</span><span class="caret"></span>
        </div>
        <div class="list">
          <div class="row hover">cindy/auto-k6cgvq<span class="tag">tabIndex 0</span></div>
          <div class="row">cindy/auto-nv69fk</div>
        </div>
      </div>
    </div>
  </div>
  <div class="pane dark">
    <div class="pane-hd">Dark</div>
    <div class="stage">
      <div class="panel">
        <div class="search">
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="icon-mut"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
          <span class="txt">cindy/auto</span><span class="caret"></span>
        </div>
        <div class="list">
          <div class="row hover">cindy/auto-k6cgvq<span class="tag">tabIndex 0</span></div>
          <div class="row">cindy/auto-nv69fk</div>
        </div>
      </div>
    </div>
  </div>
</div>

<h2>4. 三种非常规态:无匹配 / 加载中 / 加载失败
  <span class="cap">无匹配与加载失败是两套文案,不复用。失败行可点,点了不关面板。</span>
</h2>
<div class="pair">
  <div class="pane light">
    <div class="pane-hd">Light</div>
    <div class="stage">
      <div class="panel" style="margin-top:0">
        <div class="search">
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="icon-mut"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
          <span class="txt">zzz</span><span class="caret"></span>
        </div>
        <div class="list"><div class="row dim">没有匹配的分支</div></div>
      </div>
      <div class="panel">
        <div class="search">
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="icon-mut"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
          <span class="ph">搜索分支…</span>
        </div>
        <div class="list"><div class="row dim">加载分支中…</div></div>
      </div>
      <div class="panel">
        <div class="search">
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="icon-mut"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
          <span class="ph">搜索分支…</span>
        </div>
        <div class="list"><div class="row dim">分支列表加载失败,点此重试</div></div>
      </div>
    </div>
  </div>
  <div class="pane dark">
    <div class="pane-hd">Dark</div>
    <div class="stage">
      <div class="panel" style="margin-top:0">
        <div class="search">
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="icon-mut"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
          <span class="txt">zzz</span><span class="caret"></span>
        </div>
        <div class="list"><div class="row dim">没有匹配的分支</div></div>
      </div>
      <div class="panel">
        <div class="search">
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="icon-mut"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
          <span class="ph">搜索分支…</span>
        </div>
        <div class="list"><div class="row dim">加载分支中…</div></div>
      </div>
      <div class="panel">
        <div class="search">
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="icon-mut"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
          <span class="ph">搜索分支…</span>
        </div>
        <div class="list"><div class="row dim">分支列表加载失败,点此重试</div></div>
      </div>
    </div>
  </div>
</div>

<h2>对照的设计规范条款</h2>
<div class="legend" style="max-width:900px;line-height:1.9">
  <div><strong style="color:#c8c8d0">DESIGN.md §4 Inputs &amp; Forms</strong><code>:221-231</code>)— 搜索行走单行输入规范:pill 圆角、<code>--surface-elevated</code> 填充、<code>--border-default</code> 1px 描边、placeholder 用 <code>--text-placeholder</code>(该节点名禁止 Silver <code>#a3a3a3</code>,本次未引入任何写死色值)。</div>
  <div><strong style="color:#c8c8d0">DESIGN.md §4 Select &amp; Dropdown</strong><code>:236-242</code>)— 面板 12px 圆角 + Card bg,选项行 8px 内层圆角,内层半径小于容器半径。该节还要求面板宽度绑定 trigger 宽度;这个菜单<strong>改动前</strong>用的就是 <code>min-w-[200px]</code> 而非绑定值,本次只在面板内加了一行控件,未新增违规,也没顺手改(会牵动 compact / 非 compact 两套 chip 宽度)。</div>
  <div><strong style="color:#c8c8d0">DESIGN.md §10 双模式交付门槛</strong><code>:382-387</code>)— 新增的搜索行、无匹配提示、列表行 hover / focus-visible 态全部走语义 token,无单模式硬编码。按该节最后一条:目检未做,不把「复用了 themed 样式」当成「双模式已验证」。</div>
</div>

</body>
</html>

@MagicLizi

Copy link
Copy Markdown
Contributor

@wpr317694-del 👋 这个 PR 目前与 main 有合并冲突,auto-review 因此暂时跳过、没法继续审查 / 合并。

请在本地 merge 最新的 origin/main 解决冲突后推送;冲突解除后,下一轮 auto-review 会自动重新处理这个 PR。

@MagicLizi MagicLizi added awaiting-discussion 等待维护者讨论(review-pr) and removed awaiting-discussion 等待维护者讨论(review-pr) labels Sep 5, 2026
@MagicLizi

Copy link
Copy Markdown
Contributor

@wpr317694-del 👋 这个 PR 现在在等维护者确认,确认之前流程不会合并它 —— 不是卡住了,也不是在等你再改一版(你推的改动流程都读到了,判的就是最新一版代码)。

  • 在拦的是:维护者确认门(产品 / UI 变更)。
  • 讨论 issue:PR #3844 维护者确认:分支下拉加搜索框 #3845
  • 通过方式只有一个:维护者在本 PR 上 Approve。维护者觉得要改会直接 Request Changes,那时候球才回到你手里。
  • 这期间如果还有 review 意见没处理完、CI 没过,照常修就行,不影响这条等待。

这条是流程自动发的状态提醒(同一版代码只发一次),不用回复。

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

Labels

awaiting-discussion 等待维护者讨论(review-pr) touches:product-ui 改动碰到产品 / UI 面(review-pr 自动维护,仅展示) touches:rules 改动碰到规则 / 规范文档(review-pr 自动维护,仅展示)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants