From 303e85c2755d450e867725af385de830b2d318d6 Mon Sep 17 00:00:00 2001 From: FIERsity Date: Thu, 27 Aug 2026 18:32:27 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(desktop):=20=E7=BB=9F=E4=B8=80=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E5=99=A8=20classic=20rail=20=E8=B6=85=E9=AB=98?= =?UTF-8?q?=E6=97=B6=E5=86=85=E9=83=A8=E6=BB=9A=E5=8A=A8,=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E5=8F=A0=E5=8E=8B=20footer=20(#3516)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 自定义来源一多,rail 的 34px 格位累计高度超过面板可用高度:根节点没有 min-h-0 / overflow-y-auto 约束,子项按 visible 溢出向下绘制,直接叠到底部 「添加模型」footer 上并越出弹层(#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 --- .../unifiedModelPanelRendering.test.tsx | 60 ++++++++++++ .../components/new-chat/UnifiedModelRail.tsx | 96 ++++++++++++------- 2 files changed, 119 insertions(+), 37 deletions(-) diff --git a/apps/desktop/src/renderer/__tests__/unifiedModelPanelRendering.test.tsx b/apps/desktop/src/renderer/__tests__/unifiedModelPanelRendering.test.tsx index ab886027b9..95b33f2a96 100644 --- a/apps/desktop/src/renderer/__tests__/unifiedModelPanelRendering.test.tsx +++ b/apps/desktop/src/renderer/__tests__/unifiedModelPanelRendering.test.tsx @@ -195,6 +195,7 @@ vi.mock('@/state/deviceLinkModelMirror', () => ({ })); import { ModelSelectorContent } from '@/components/new-chat/ModelSelector'; +import type { UnifiedRailItem } from '@/components/new-chat/unifiedModelSelection'; import { __resetForTest as resetEnginePrefs, getModelEngineOverride, @@ -3243,3 +3244,62 @@ describe('列表样式试用开关(badge · v7 引擎徽标行)', () => { expect(header.getAttribute('data-group-label')).toBe('Cindy AI'); }); }); + +// ── classic 左侧 rail 的高度约束(#3516)──────────────────────────────────── +// 自定义来源一多,rail 的 34px 格位累计高度会超过面板可用高度:根节点若无 +// min-h-0 / overflow-y-auto,子项按 visible 溢出向下绘制,直接叠到底部「添加模型」 +// footer 上。这里锁两件事:骨架类保留 + 溢出时格子一个不少、顺序不变。 +describe('classic 左侧 rail:超高时内部滚动,不叠压 footer(#3516)', () => { + function railItems(count: number): UnifiedRailItem[] { + return [ + { kind: 'favorites' }, + { kind: 'all' }, + ...Array.from({ length: count }, (_, index) => ({ + kind: 'provider' as const, + providerId: `src-${String(index).padStart(2, '0')}`, + })), + ]; + } + + async function mountRail(items: readonly UnifiedRailItem[]): Promise { + const { UnifiedModelRail } = await import('@/components/new-chat/UnifiedModelRail'); + render( + React.createElement(UnifiedModelRail, { + items, + active: { kind: 'all' }, + onSelect: vi.fn(), + providers: [], + providerLabel: (providerId: string) => providerId, + }), + ); + return document.querySelector('[data-unified-model-rail]') as HTMLElement; + } + + it('根节点保留 w-12 / shrink-0 骨架,并带 min-h-0 + overflow-y-auto 收缩滚动链', async () => { + const root = await mountRail(railItems(1)); + expect(root).not.toBeNull(); + for (const token of ['w-12', 'shrink-0', 'min-h-0', 'overflow-y-auto', 'overscroll-contain']) { + expect(root.className).toContain(token); + } + }); + + it('格位铺满超出可用高度时全部渲染、顺序不变(不靠裁剪或删项规避)', async () => { + const root = await mountRail(railItems(24)); + // ★收藏 → 全部(带唯一的段间分隔线)→ 各来源,定位走 data-rail-item 稳定标记。 + const keys = Array.from(root.querySelectorAll('button[data-rail-item]')).map((button) => + button.getAttribute('data-rail-item'), + ); + expect(keys).toHaveLength(26); + expect(keys[0]).toBe('favorites'); + expect(keys[1]).toBe('all'); + expect(root.querySelectorAll('[aria-hidden="true"]')).toHaveLength(1); + const providerIds = keys.slice(2).map((key) => key?.replace(/^provider:/, '')); + expect(providerIds[0]).toBe('src-00'); + expect(providerIds[23]).toBe('src-23'); + expect(new Set(providerIds).size).toBe(24); + // 可达性:title/aria-label 跟着 providerLabel 走,不能因为溢出就丢格子文案。 + const lastButton = root.querySelector('button[data-rail-item="provider:src-23"]'); + expect(lastButton?.getAttribute('title')).toBe('src-23'); + expect(lastButton?.getAttribute('aria-label')).toBe('src-23'); + }); +}); diff --git a/apps/desktop/src/renderer/components/new-chat/UnifiedModelRail.tsx b/apps/desktop/src/renderer/components/new-chat/UnifiedModelRail.tsx index 8add293bc1..aa75bb1138 100644 --- a/apps/desktop/src/renderer/components/new-chat/UnifiedModelRail.tsx +++ b/apps/desktop/src/renderer/components/new-chat/UnifiedModelRail.tsx @@ -1,8 +1,10 @@ import { LayoutGrid, Star } from 'lucide-react'; +import { useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import type { ProviderView } from '@cindy/model-providers'; +import { flashScrollbar } from '@/lib/scrollbarAutoHide'; import { cn } from '@/lib/utils'; import { agentOptionOf } from './agentOptions'; @@ -41,9 +43,26 @@ export function UnifiedModelRail({ // rail 常驻,不做「项数少就整条隐藏」——设计稿的分类栏在单来源时也在(★/全部/来源), // 隐藏会让收藏与快速切换不可发现(Chris 2026-08-13 实测反馈)。 const activeKey = railItemKey(active); + const railRef = useRef(null); + useEffect(() => { + const el = railRef.current; + if (!el) return; + // 格位铺得下时不闪滚动条;超出一屏才提示「下面还有来源」。与右侧列表同一套提示节奏。 + const raf = requestAnimationFrame(() => flashScrollbar(el)); + return () => cancelAnimationFrame(raf); + }, [items]); return ( // 设计稿 .rail:宽 48(含 6px 侧距 + 1px 右分隔线)、纵向 8px、格间 2px。 -
+ // min-h-0 + overflow-y-auto:自定义来源一多,34px 格位累计高度会超过面板可用高度 —— + // 根节点没有纵向约束时子项按 visible 溢出向下绘制,直接叠到面板底部「添加模型」 + // footer 上(#3516)。与右侧列表(UnifiedModelPanel 的 min-h-0 + overflow-y-auto) + // 同一套收缩滚动链:父级高度受限时 rail 自己收缩并内部滚动,w-12 shrink-0 骨架不变, + // 不引入折叠 / 截断等新的产品规则;不用 scrollbar-gutter:stable(48px 窄栏留给 34px 按钮)。 +
{items.map((item) => { const key = railItemKey(item); const isActive = activeKey === key; @@ -64,43 +83,46 @@ export function UnifiedModelRail({ return (
{separatorBefore && ( -
+
)} - +
); })} From 9500f4a76aeeaa52b98698caf7acf6f5d8ee91c8 Mon Sep 17 00:00:00 2001 From: FIERsity Date: Thu, 27 Aug 2026 18:47:07 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(desktop):=20rail=20=E9=9A=90=E8=97=8F?= =?UTF-8?q?=E5=8E=9F=E7=94=9F=E6=BB=9A=E5=8A=A8=E6=9D=A1=E6=A7=BD,?= =?UTF-8?q?=E7=AA=84=E6=A0=8F=E4=B8=8D=E5=86=8D=E8=A2=AB=20Windows=20?= =?UTF-8?q?=E9=9D=9E=20overlay=20=E6=BB=9A=E5=8A=A8=E6=9D=A1=E6=8C=A4?= =?UTF-8?q?=E5=8E=8B=20(#3516)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../unifiedModelPanelRendering.test.tsx | 11 ++++++++++- .../components/new-chat/UnifiedModelRail.tsx | 16 ++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/apps/desktop/src/renderer/__tests__/unifiedModelPanelRendering.test.tsx b/apps/desktop/src/renderer/__tests__/unifiedModelPanelRendering.test.tsx index 95b33f2a96..ab86445206 100644 --- a/apps/desktop/src/renderer/__tests__/unifiedModelPanelRendering.test.tsx +++ b/apps/desktop/src/renderer/__tests__/unifiedModelPanelRendering.test.tsx @@ -3278,7 +3278,16 @@ describe('classic 左侧 rail:超高时内部滚动,不叠压 footer(#3516)', () it('根节点保留 w-12 / shrink-0 骨架,并带 min-h-0 + overflow-y-auto 收缩滚动链', async () => { const root = await mountRail(railItems(1)); expect(root).not.toBeNull(); - for (const token of ['w-12', 'shrink-0', 'min-h-0', 'overflow-y-auto', 'overscroll-contain']) { + // scrollbar-hide:Windows 非 overlay 环境的全局 12px 滚动条槽会挤掉 34px 按钮 + // (#3516 review P2),原生槽必须藏;滚轮 / 触控板照常滚。 + for (const token of [ + 'w-12', + 'shrink-0', + 'min-h-0', + 'overflow-y-auto', + 'overscroll-contain', + 'scrollbar-hide', + ]) { expect(root.className).toContain(token); } }); diff --git a/apps/desktop/src/renderer/components/new-chat/UnifiedModelRail.tsx b/apps/desktop/src/renderer/components/new-chat/UnifiedModelRail.tsx index aa75bb1138..73cac056f1 100644 --- a/apps/desktop/src/renderer/components/new-chat/UnifiedModelRail.tsx +++ b/apps/desktop/src/renderer/components/new-chat/UnifiedModelRail.tsx @@ -1,10 +1,8 @@ import { LayoutGrid, Star } from 'lucide-react'; -import { useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import type { ProviderView } from '@cindy/model-providers'; -import { flashScrollbar } from '@/lib/scrollbarAutoHide'; import { cn } from '@/lib/utils'; import { agentOptionOf } from './agentOptions'; @@ -43,14 +41,6 @@ export function UnifiedModelRail({ // rail 常驻,不做「项数少就整条隐藏」——设计稿的分类栏在单来源时也在(★/全部/来源), // 隐藏会让收藏与快速切换不可发现(Chris 2026-08-13 实测反馈)。 const activeKey = railItemKey(active); - const railRef = useRef(null); - useEffect(() => { - const el = railRef.current; - if (!el) return; - // 格位铺得下时不闪滚动条;超出一屏才提示「下面还有来源」。与右侧列表同一套提示节奏。 - const raf = requestAnimationFrame(() => flashScrollbar(el)); - return () => cancelAnimationFrame(raf); - }, [items]); return ( // 设计稿 .rail:宽 48(含 6px 侧距 + 1px 右分隔线)、纵向 8px、格间 2px。 // min-h-0 + overflow-y-auto:自定义来源一多,34px 格位累计高度会超过面板可用高度 —— @@ -58,10 +48,12 @@ export function UnifiedModelRail({ // footer 上(#3516)。与右侧列表(UnifiedModelPanel 的 min-h-0 + overflow-y-auto) // 同一套收缩滚动链:父级高度受限时 rail 自己收缩并内部滚动,w-12 shrink-0 骨架不变, // 不引入折叠 / 截断等新的产品规则;不用 scrollbar-gutter:stable(48px 窄栏留给 34px 按钮)。 + // scrollbar-hide:原生滚动条必须藏 —— Windows 等非 overlay 环境下全局 12px 槽位会把 + // 内容区挤到 24px,34px 按钮放不下(#3516 review P2);折叠侧栏窄 rail 同一取舍 + // (CCAgentSidebarUpper)。滚轮 / 触控板 / 键盘照常滚动。
{items.map((item) => { const key = railItemKey(item);