Skip to content

fix(desktop): 统一选择器 classic rail 超高时内部滚动,不再叠压 footer (#3516) - #3520

Open
FIERsity wants to merge 2 commits into
makecindy:mainfrom
FIERsity:fix/unified-rail-overflow
Open

fix(desktop): 统一选择器 classic rail 超高时内部滚动,不再叠压 footer (#3516)#3520
FIERsity wants to merge 2 commits into
makecindy:mainfrom
FIERsity:fix/unified-rail-overflow

Conversation

@FIERsity

@FIERsity FIERsity commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Closes #3516

这次改了什么

摘要

统一模型选择器 classic(样式 A)左侧 rail 原先没有纵向约束:自定义来源一多,34px 格位累计高度超过面板可用高度时,根节点既不收缩也不裁剪(默认 overflow: visible),溢出格位向下绘制,直接叠压底部「添加模型」footer 并越出弹层(#3516)。本 PR 给 rail 补上与右侧列表同一套收缩滚动链,超高时在自己内部滚动,footer 不再被遮挡。

变更类型

  • fix 缺陷修复

范围

  • 关联 Issue / 需求:新选择器左侧供应商栏项过多时不滚动,溢出叠压到底部「添加模型」footer #3516(按该 issue 维护者分析的建议方案收口)
  • 本 PR 包含:
    • UnifiedModelRail 根节点补 min-h-0 overflow-y-auto overscroll-contain;w-12 shrink-0 骨架不变;
    • scrollbar-hide(既有 .scrollbar-hide 工具类,globals.css F-FI-3):Windows 等非 overlay scrollbar 环境的全局 12px 槽位会把 48px 栏挤到放不下 34px 格位(review P2),原生槽必须藏、滚轮照常滚 —— 与折叠侧栏窄 rail(CCAgentSidebarUpper)同一取舍;
    • 按分析建议挂 data-unified-model-rail 定位标记,供测试定位;
    • 新增两条 rail 回归测试(见下)。
  • 明确不包含:折叠 / 截断等新的产品规则;scrollbar-gutter: stable;滚动条视觉提示(flashScrollbar 在隐藏原生槽后无视觉载体,已从初版方案中摘除);badge(样式 B)与 original 选择器不受影响(badge 本就不渲染 rail)。
  • 用户可见变化:来源较多时左栏内部可滚(无可见滚动条,跨平台一致),不再叠压「添加模型」。
  • 是否存在 breaking change:无。

UI 变化

平台:Desktop(macOS 实测,Electron renderer 层,CSS class 级改动跨三平台一致)。

界面效果证据(对比演示图,亦可从 pr-assets 分支 取 png 与自包含 HTML;修复前叠压形态见 #3516 原 issue 配图):

  • A 修复前:rail 格位溢出绘制、叠压「添加模型」footer;

  • B 修复后(本 PR 提交态):栏内滚动,footer 干净;

  • C 反例(review P2 已规避):不藏原生滚动条槽时 Windows 12px 槽对 34px 格位的挤压。

  • 引用的设计规范:

    • DESIGN.md §4 Select & Dropdown —— Composer 下拉菜单统一契约:rail 格位的 34px 尺寸、反色选中态、1px Board 分隔线全部原样保留,本 PR 只补滚动约束,不触碰行高亮 token(--model-item-hover)、圆角与宽度契约;
    • DESIGN.md §4 Dialog & Modal —— 多步向导的滚动区条款(「滚动区收在两条 hairline 之间、常驻操作钉在下方始终可见」):同一结构原则在 rail 上的落地 —— 滚动发生在 rail 内部,「添加模型」footer 固定在其下方、不被覆盖;
    • DESIGN.md §5 Layout Structure(层间用 1px Board 分隔线):rail 右缘 1px 分隔线不变。

怎么验证的

自动验证

pnpm vitest run src/renderer/__tests__/unifiedModelPanelRendering.test.tsx
结果:101 passed(含新增 2 条:rail 骨架类保留 w-12/shrink-0/min-h-0/overflow-y-auto/overscroll-contain/scrollbar-hide;24 来源共 26 格全部渲染、data-rail-item 顺序不变、段间分隔线唯一、title/aria-label 可达性文案不丢)

pnpm typecheck(desktop)
结果:通过

pnpm exec eslint <改动两文件>
pnpm exec prettier --check <改动两文件>
结果:均通过

手工验证

Linux/Windows 矮窗口 + 15+ 来源的实机矩阵(Light/Dark、滚轮/触控板在 rail 与列表间的滚动归属、Tab 焦点进入视口、badge/original 回归)随 CI 单测与本仓库实机 review 一并确认。

未执行的验证

未在本机逐一走查 Windows 形态(占宽问题经 review P2 指认后已按工具类先例消除,如 review 需要实机录屏再跟进)。

@FIERsity
FIERsity requested a review from a team as a code owner August 27, 2026 10:32
@greptile-apps

greptile-apps Bot commented Aug 27, 2026

Copy link
Copy Markdown

Greptile Summary

此 PR 修复 classic 统一模型选择器左侧来源栏在矮窗口中溢出并叠压 footer 的问题。

  • 为 rail 补充纵向收缩、内部滚动及 overscroll 约束,同时保持原有 48px 宽度
  • 来源项变化后复用现有滚动条提示机制,仅在内容确实可滚动时显示提示
  • 增加 rail 样式骨架、完整渲染顺序、分隔线及可访问性文案的回归测试

Confidence Score: 5/5

此 PR 看起来可以安全合并,左侧 rail 的滚动约束与现有面板布局链一致。

新增样式使超高 rail 在受限面板内纵向滚动,保留既有宽度和全部来源项;滚动条提示只随实际 rail 数据变化触发,未发现阻塞或非阻塞问题。

Important Files Changed

Filename Overview
apps/desktop/src/renderer/components/new-chat/UnifiedModelRail.tsx 新增的滚动约束与实际父级 flex 高度链匹配,滚动条提示 effect 也由已记忆化的 items 控制,未发现可达缺陷。
apps/desktop/src/renderer/tests/unifiedModelPanelRendering.test.tsx 新测试覆盖 rail 必需样式、超量来源的完整顺序、唯一分隔线及可访问性属性,未发现错误断言。

Reviews (1): Last reviewed commit: "fix(desktop): 统一选择器 classic rail 超高时内部滚动..." | Re-trigger Greptile

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a5ac684e6d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/desktop/src/renderer/components/new-chat/UnifiedModelRail.tsx Outdated
自定义来源一多,rail 的 34px 格位累计高度超过面板可用高度:根节点没有
min-h-0 / overflow-y-auto 约束,子项按 visible 溢出向下绘制,直接叠到底部
「添加模型」footer 上并越出弹层(makecindy#3516)。

按 issue 分析建议收口:
- 根节点补 min-h-0 + overflow-y-auto + overscroll-contain,w-12 shrink-0
  骨架不变 —— 与右侧列表(UnifiedModelPanel 的同一套收缩滚动链)对齐;
- 挂 data-unified-model-rail 定位标记,items 变化后对 rail 元素调用现有
  flashScrollbar(真可滚才闪),提示左栏下方还有来源;
- 不用 scrollbar-gutter:stable(48px 窄栏要留给 34px 按钮)。

测试:unifiedModelPanelRendering 增补两条 rail 接线锁 —— 骨架类保留;
24 个来源格全部渲染、data-rail-item 顺序与可达性文案不丢。

Signed-off-by: FIERsity <FIERsity@users.noreply.github.com>
@FIERsity
FIERsity force-pushed the fix/unified-rail-overflow branch from a5ac684 to 303e85c Compare August 27, 2026 10:39
…3516)

review P2(chatgpt-codex-connector):overflow-y-auto 让 rail 成为滚动容器后,
Windows 等非 overlay scrollbar 环境的全局 12px ::-webkit-scrollbar 槽位会扣除
布局宽度 —— 48px 栏减 12px 槽、12px 侧距与 1px 边框只剩 ~23px,34px 格位放不下。

处理:复用既有 .scrollbar-hide 工具类(F-FI-3,globals.css)隐藏原生槽,
滚轮 / 触控板 / 键盘照常内部滚动;折叠侧栏窄 rail(CCAgentSidebarUpper)
是同一取舍的先例。上一提交引入的 flashScrollbar 提示随之失去视觉载体,
一并摘除(effect + ref + import),data-unified-model-rail 定位标记保留。

测试:骨架断言补 scrollbar-hide;101 passed,typecheck 通过。

Signed-off-by: FIERsity <FIERsity@users.noreply.github.com>
@MagicLizi MagicLizi added awaiting-discussion 等待维护者讨论(review-pr) touches:product-ui 改动碰到产品 / UI 面(review-pr 自动维护,仅展示) labels Aug 27, 2026

@MagicLizi MagicLizi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

格式门未通过,请补全后再推。

  • Description 缺必填段落「风险」。fix 类 PR 需要写清这次改动的风险与回滚/影响面。

补上「风险」段落后重新请求审查即可。

@MagicLizi

Copy link
Copy Markdown
Contributor

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

这条是提醒,不单独阻断合并。

@FIERsity

FIERsity commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

补一下界面效果证据(按源码尺寸复刻的自包含演示页:48px rail / 34px 格位圆角 9 / 1px hairline / footer「添加模型」,面板高钳 300px、放入 ★ + 全部 + 8 个自定义来源):

修复前后对比

  • A 修复前(新选择器左侧供应商栏项过多时不滚动,溢出叠压到底部「添加模型」footer #3516:rail 无纵向约束 → 格位溢出绘制,「K」直接叠在「添加模型」上;
  • B 修复后(本 PR 提交态)min-h-0 + overflow-y-auto + overscroll-contain + scrollbar-hide → 栏内滚动,footer 干净;
  • C 反例(review P2,已规避):若不藏原生滚动条槽,Windows 非 overlay 环境的 12px 槽把 34px 格位裁切挤压(红虚线标出)——即 scrollbar-hide 存在的理由。

说明:这是按 main 源码同构复刻的演示页截图(非实机截图);真实运行态即 fix/unified-rail-overflow 分支的实现本身。完整 HTML 如下,保存为 .html 双击即可在浏览器里交互验证:

自包含演示页 unified-rail-demo.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>#3520 统一选择器 rail 溢出修复 · 界面对比</title>
<style>
  /* ── 近似 Cindy 浅色主题 token(仅用于演示还原)────────────────────── */
  :root {
    --page-bg: #efece5;
    --panel-bg: #fbfaf6;
    --panel-border: #dedbd2;
    --hairline: #e6e3da;
    --text: #2d2b27;
    --text-tertiary: #a39f94;
    --badge-bg: #f1efe8;
    --badge-fg: #55524a;
    --hover-row: #eeece4;
    --active-chip: #33312a;
    --chip-fg: #faf9f4;
  }
  * { box-sizing: border-box; margin: 0; padding: 0; }
  body {
    font-family: -apple-system, "PingFang SC", "Segoe UI", sans-serif;
    background: var(--page-bg);
    color: var(--text);
    padding: 28px 30px;
    font-size: 13px;
  }
  h1 { font-size: 15px; margin-bottom: 4px; }
  .sub { color: var(--text-tertiary); font-size: 12px; margin-bottom: 22px; }
  .row { display: flex; gap: 26px; align-items: flex-start; }
  .case { width: 380px; }
  .tag {
    display: block; font-size: 12px; margin-bottom: 8px; line-height: 1.45;
    color: var(--text); font-weight: 600;
  }
  .tag small { color: var(--text-tertiary); font-weight: 400; }

  /* ── 统一面板骨架(ModelSelector.tsx:2756 / UnifiedModelPanel / UnifiedModelRail 同构)
        面板高度故意钳到 300px,来源格位共 9 个(合计 >300px 触发超高)────────── */
  .panel {
    height: 300px;
    background: var(--panel-bg);
    border: 1px solid var(--panel-border);
    border-radius: 12px;
    display: flex;
    flex-direction: column;
    min-h: 0;
    overflow: hidden;
  }
  .search {
    flex-shrink: 0;
    display: flex; align-items: center; gap: 8px;
    padding: 11px 14px;
    border-bottom: 1px solid var(--hairline);
    color: var(--text-tertiary);
  }
  .mid { display: flex; min-height: 0; min-width: 0; flex: 1; }
  .list-col { position: relative; display: flex; min-height: 0; min-width: 0; flex: 1; flex-direction: column; }
  .models { flex: 1; min-height: 0; overflow-y: auto; padding: 8px 8px 10px; }
  .group-label { padding: 8px 8px 4px; color: var(--text-tertiary); font-size: 12px; }
  .mrow {
    display: flex; align-items: center; justify-content: space-between; gap: 8px;
    padding: 8px 10px; border-radius: 8px;
  }
  .mrow:hover, .mrow.sel { background: var(--hover-row); }
  .mrow .name { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .eff { color: var(--text-tertiary); font-size: 12px; flex-shrink: 0; }
  .dotmark {
    width: 22px; height: 22px; border-radius: 50%; flex-shrink: 0;
    background: var(--badge-bg); color: var(--badge-fg);
    display: inline-flex; align-items: center; justify-content: center;
    font-weight: 700; font-size: 12px; margin-right: 8px;
  }
  .footer {
    flex-shrink: 0;
    display: flex; align-items: center; justify-content: space-between; gap: 8px;
    padding: 9px 14px;
    border-top: 1px solid var(--hairline);
    color: var(--text-tertiary); font-size: 12.5px;
  }
  .footer .add { display: flex; align-items: center; gap: 6px; }

  /* ── rail(48px;34px 格位;格间 2px;侧距 6px;右缘 1px 分隔线)────────── */
  .rail {
    width: 48px;
    flex-shrink: 0;
    display: flex; flex-direction: column; align-items: center; gap: 2px;
    border-right: 1px solid var(--hairline);
    padding: 8px 6px;
  }
  .rail-btn {
    width: 34px; height: 34px; border-radius: 9px; flex-shrink: 0;
    border: none; background: none; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    color: var(--text-tertiary);
  }
  .rail-btn:hover { background: var(--hover-row); }
  .rail-btn.active { background: var(--active-chip); color: var(--chip-fg); }
  .rail-sep { width: 22px; border-top: 1px solid var(--hairline); margin: 3px 0; flex-shrink: 0; }

  /* ── 三种变体 ──────────────────────────────────────────── */
  /* A 修复前:根节点无 min-h-0 / overflow —— 子项按 visible 向下绘制,叠压 footer(#3516) */
  .before-fix .rail { min-height: auto; overflow: visible; }

  /* B 修复后:min-h-0 + overflow-y-auto + overscroll-contain + scrollbar-hide(PR 提交态) */
  .after-fix .rail {
    min-height: 0; overflow-y: auto; overscroll-behavior-y: contain;
    -ms-overflow-style: none; scrollbar-width: none;
  }
  .after-fix .rail::-webkit-scrollbar { display: none; }

  /* C 反例:同样式但保留原生滚动条槽 —— Windows 非 overlay 下 12px 槽挤掉格位(review P2) */
  .native-gutter .rail {
    min-height: 0; overflow-y: auto; overscroll-behavior-y: contain;
  }
  /* 显式样式化 ::-webkit-scrollbar 会把 Chromium 从 overlay 切到占布局的经典模式,
     等效于 Windows 非 overlay 环境:48px 内容区被扣掉 12px。 */
  .native-gutter .rail::-webkit-scrollbar {
    width: 12px;
  }
  .native-gutter .rail::-webkit-scrollbar-track {
    background: repeating-linear-gradient(45deg, #ddd9ce 0 4px, #efece3 4px 8px);
  }
  .native-gutter .rail::-webkit-scrollbar-thumb {
    background: #8f8c82; border-radius: 6px;
    border: 2px solid transparent; background-clip: padding-box;
  }
  /* 标出被挤压的 34px 格位(仅反例用,红虚线便于一眼看出溢出方向) */
  .native-gutter .rail-btn { outline: 1px dashed rgba(178, 74, 58, 0.55); outline-offset: -1px; }

  svg { display: block; }
</style>
</head>
<body>
<h1>统一模型选择器 · 左侧 rail 超高处理(#3520,对应 issue #3516)</h1>
<div class="sub">面板骨架与类名按 main 源码复刻(48px rail / 34px 格位圆角 9 / 1px hairline / footer「添加模型」)。面板高度固定 300px,放入 ★ + 全部 + 8 个自定义来源,格位总高必然超高。</div>

<div class="row">
  <!-- A -->
  <div class="case before-fix">
    <span class="tag">A · 修复前(#3516)<small> · rail 无纵向约束 → 格位溢出叠压「添加模型」</small></span>
    <div class="panel">
      <div class="search">🔍<span style="flex:1">搜索模型…</span></div>
      <div class="mid">
        <div class="rail" id="rail-a"></div>
        <div class="list-col"><div class="models" id="list"></div></div>
      </div>
      <div class="footer"><span class="add">➕ 添加模型</span><span>尝试样式B&nbsp;&nbsp;切回老版</span></div>
    </div>
  </div>

  <!-- B -->
  <div class="case after-fix">
    <span class="tag">B · 修复后(PR 提交态)<small> · min-h-0 + overflow-y-auto + scrollbar-hide → 栏内滚动,footer 干净</small></span>
    <div class="panel">
      <div class="search">🔍<span style="flex:1">搜索模型…</span></div>
      <div class="mid">
        <div class="rail" id="rail-b"></div>
        <div class="list-col"><div class="models" id="list-b"></div></div>
      </div>
      <div class="footer"><span class="add">➕ 添加模型</span><span>尝试样式B&nbsp;&nbsp;切回老版</span></div>
    </div>
  </div>

  <!-- C -->
  <div class="case native-gutter">
    <span class="tag">C · 反例(P2,已规避)<small> · 若不藏原生槽,Windows 12px 滚动条把 34px 格位挤变形</small></span>
    <div class="panel">
      <div class="search">🔍<span style="flex:1">搜索模型…</span></div>
      <div class="mid">
        <div class="rail" id="rail-c"></div>
        <div class="list-col"><div class="models" id="list-c"></div></div>
      </div>
      <div class="footer"><span class="add">➕ 添加模型</span><span>尝试样式B&nbsp;&nbsp;切回老版</span></div>
    </div>
  </div>
</div>

<script>
  // 屏幕阅读器兼容写法与源码一致:title/aria-label/data-rail-item。
  const providers = ["qwen", "grok", "glm", "deepseek", "kimi", "mistral", "llama", "flux"];
  const letter = (id) => id[0].toUpperCase();
  const RAIL_ALL_SVG = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>';
  const STAR_SVG = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26"/></svg>';

  function railHTML() {
    let out = `<button class="rail-btn" title="收藏" aria-label="收藏">${STAR_SVG}</button>`;
    out += `<div class="rail-sep" aria-hidden="true"></div>`;
    out += `<button class="rail-btn active" title="全部" aria-label="全部">${RAIL_ALL_SVG}</button>`;
    for (const p of providers) {
      out += `<button class="rail-btn" title="${p}" aria-label="${p}" data-rail-item="provider:${p}"><span class="dotmark" style="margin-right:0">${letter(p)}</span></button>`;
    }
    return out;
  }
  function modelsHTML(withGroup) {
    let out = "";
    if (withGroup) out += `<div class="group-label">内置目录</div>`;
    out += `<div class="mrow"><span class="dotmark">G</span><span class="name">GLM-5.3</span><span class="eff">▙ 高</span></div>`;
    out += `<div class="mrow sel"><span class="dotmark">G</span><span class="name">GLM-5.3-Flash</span><span class="eff">▙ 高</span></div>`;
    return out;
  }
  document.getElementById("rail-a").innerHTML = railHTML();
  document.getElementById("rail-b").innerHTML = railHTML();
  document.getElementById("rail-c").innerHTML = railHTML();
  document.getElementById("list").innerHTML = modelsHTML(true);
  document.getElementById("list-b").innerHTML = modelsHTML(true);
  document.getElementById("list-c").innerHTML = modelsHTML(true);
  // 让 B/C 与真实状态一致:默认停在「全部」视图。
</script>
</body>
</html>

@FIERsity

Copy link
Copy Markdown
Contributor Author
image 附一张遇到的bug的实机截图

@FIERsity
FIERsity requested a review from MagicLizi August 27, 2026 11:35
@MagicLizi MagicLizi added status:awaiting-author 等作者修改(review-pr 自动维护,仅展示) and removed awaiting-discussion 等待维护者讨论(review-pr) labels Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status:awaiting-author 等作者修改(review-pr 自动维护,仅展示) touches:product-ui 改动碰到产品 / UI 面(review-pr 自动维护,仅展示)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

新选择器左侧供应商栏项过多时不滚动,溢出叠压到底部「添加模型」footer

2 participants