diff --git a/apps/desktop/src/renderer/__tests__/unifiedModelPanelRendering.test.tsx b/apps/desktop/src/renderer/__tests__/unifiedModelPanelRendering.test.tsx index ab886027b9..ab86445206 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,71 @@ 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(); + // 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); + } + }); + + 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..73cac056f1 100644 --- a/apps/desktop/src/renderer/components/new-chat/UnifiedModelRail.tsx +++ b/apps/desktop/src/renderer/components/new-chat/UnifiedModelRail.tsx @@ -43,7 +43,18 @@ export function UnifiedModelRail({ const activeKey = railItemKey(active); 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 按钮)。 + // scrollbar-hide:原生滚动条必须藏 —— Windows 等非 overlay 环境下全局 12px 槽位会把 + // 内容区挤到 24px,34px 按钮放不下(#3516 review P2);折叠侧栏窄 rail 同一取舍 + // (CCAgentSidebarUpper)。滚轮 / 触控板 / 键盘照常滚动。 +
{items.map((item) => { const key = railItemKey(item); const isActive = activeKey === key; @@ -64,43 +75,46 @@ export function UnifiedModelRail({ return (
{separatorBefore && ( -
+
)} - +
); })}