
DeepSeek Harness 折叠侧边栏控制轨:零宽度锁死问题的 56px Rail 修复方案【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness本篇基于 DeepSeek Harness(以下简称 dsh)仓库中一份已归档的 Bug 修复 Agent Note(.agents/notes/archived/bug-fix/2026-07-22-collapsed-sidebar-control-rail.md)展开,解析 Web 客户端三栏布局中侧边栏折叠后所有恢复控件消失这一锁死问题的完整修复方案:折叠状态为何映射为 56px 固定控制轨、布局求解器如何保证侧边栏列永不收缩,以及AppFrame与SidebarRoot之间通过 owner props 协作完成的 slide crossfade 动画细节。读完本文,你可以掌握该布局的几何契约(columns.ts常量表)、两状态切换的动画时序,以及配套测试如何锁定这些行为。问题:折叠侧边栏会锁死所有恢复入口修复前的行为链条非常清晰,全部记录在原 Agent Note 的 Problem 一节:用户点击侧边栏的关闭动作时,布局持久化了一个0 宽度偏好(width preference 0);布局把该偏好直接映射为一条0 宽度的 grid track;侧边栏中仅有的两个自救控件——侧边栏开关(toggle)和设置入口——恰好都位于这条被裁剪掉的轨道内部;于是折叠操作把每个可见的恢复控件一并移除,用户无法再展开侧边栏;刷新页面后,关闭偏好被保留,锁死状态被完整复现。换句话说,问题不只是宽度动画不自然,而是一个可用性死锁:偏好存储(persisted preference)与几何映射(track width)之间缺少一个中间态。修复的核心思路就是给关闭这个语义一个非零的渲染宽度,让恢复控件永远有一个可见的落脚点。决策:折叠态映射为 56px 固定控制轨Note 的 Decision 一节给出了最终方案,可以拆解为四条要点:几何映射:布局把侧边栏已关闭(持久化宽度为0)映射为固定常量SIDEBAR_COLLAPSED56px——即两侧 16px 水平内边距之间的一条 24px 图标列;求解器契约:侧边栏 track 在求解器中是定宽的——无论展开还是折叠,它都从不向视口压力让步(never concedes);只有 details 列会被压缩,随后被自动关闭;边界与偏好:collapsed状态下侧边栏保留右侧边框,而存储的展开宽度原封不动,展开时可直接恢复拖拽宽度;状态判定来源:AppFrame依据持久化宽度偏好而非解析后的 track 宽度来判定折叠态,折叠时移除拖拽手柄,并把collapsed作为 owner props 从渲染站点传入 sidebar slot。常量定义位于 columns.ts,其 JSDoc 直接说明了这条契约:a closed sidebar resolves to the fixedSIDEBAR_COLLAPSEDcontrol rail while closed details resolve to zero width。注意一个关键不对称:关闭的 details 解析为 0 宽度(子树保持挂载但不占位),而关闭的 sidebar 保留紧凑 rail——这正是修复锁死问题的几何基础。从源码结构看,这个 56px 的取值在 UI 层同样有对应实现:SidebarRoot.module.css 的注释说明 rail 内的控件是 36×36 的盒子,折叠态根节点切换为padding: 18px 10px 6px的 rail 几何,使图标在 56px 轨道内居中。布局求解器:永不收缩的三栏让步链computeColumns是纯函数,输入为 (viewport, sidebar 偏好, details 偏好),输出三列解析宽度。其让步链按契约固定排序,完整逻辑见 columns.ts:export function computeColumns(viewport: number, sidebar: number, details: number): Columns { // 侧边栏固定在偏好宽度(或 rail)上——它从不让步 const s sidebar 0 ? SIDEBAR_COLLAPSED : clampWidth(sidebar, SIDEBAR_MIN, SIDEBAR_MAX) const d0 details 0 ? 0 : clampWidth(details, DETAILS_MIN, DETAILS_MAX) // 步骤 1:所有列都以偏好宽度放得下 if (s d0 CENTER_MIN viewport) return { sidebar: s, center: viewport - s - d0, details: d0 } // 步骤 2:details 向其最小值收缩 const d1 d0 0 ? 0 : Math.max(DETAILS_MIN, viewport - s - CENTER_MIN) if (s d1 CENTER_MIN viewport) return { sidebar: s, center: CENTER_MIN, details: d1 } // 步骤 3:自动关闭 details(派生结果,偏好不动), // center 吸收剩余缺口(可能跌破 CENTER_MIN) return { sidebar: s, center: Math.max(0, viewport - s), details: 0 } }其中sidebar 0 ? SIDEBAR_COLLAPSED : ...这一行就是整个修复的几何锚点:持久化的0偏好在这里被翻译为 56px 轨道,后续所有步骤都不再触碰s。合同冻结的几何常量(定义于 columns.ts):常量值含义CENTER_MIN640中栏地板,仅最终 fallback 可跌破SIDEBAR_MIN/SIDEBAR_MAX264 / 420侧边栏拖拽夹取范围SIDEBAR_DEFAULT280未拖拽前的侧边栏宽度SIDEBAR_COLLAPSED56关闭态 rail:16px 边距间一条 24px 图标列SIDEBAR_AUTO_COLLAPSE1024视口低于此值时侧边栏自动折叠为 railDETAILS_MIN/DETAILS_MAX300 / 520details 拖拽夹取范围DETAILS_DEFAULT360未拖拽前的 details 宽度一个值得注意的设计:求解器是无滞回(hysteresis-free)的纯函数——输出仅是 (viewport, preferences) 的函数,所以窗口重新拉宽时布局自动恢复,无需额外状态。偏好在求解边界处被重新夹取,因为偏好跨越 store 边界,调用方仍可能传入过期范围。SIDEBAR_AUTO_COLLAPSE断点不在求解器内消费,而是由AppFrame在求解前决定有效侧边栏偏好,从而保持求解器无断点。窄视口下的手动展开走另一条语义:它翻转narrowExpanded覆盖位而非宽度偏好,因此拉宽窗口后自动折叠前的布局得以完整恢复。偏好存储:宽度即偏好,关闭即遗忘布局状态是纯数值偏好(0 关闭),定义于 stores.ts:type LayoutState { sidebar: number; details: number; narrow: boolean; narrowExpanded: boolean }动作集与本次修复直接相关的是toggleSidebar:toggleSidebar: (d) { if (d.narrow) d.narrowExpanded !d.narrowExpanded else d.sidebar d.sidebar 0 ? SIDEBAR_DEFAULT : 0 }从源码结构看,注释点明了一条重要约定:The preference IS the width, so closing a panel forgets its drag width —— reopening restores the contract default。即偏好就是宽度本身:关闭时宽度偏好被写为 0,重开时恢复为契约默认值 280 而非记住上次拖拽宽度。这正是 Decision 中stored expanded width remains untouched的另一面——在宽视口下折叠只影响求解与渲染层,不会破坏偏好语义;窄视口下则完全不动宽度偏好,只翻转narrowExpanded。AppFrame:owner props、手柄移除与轨道过渡AppFrame.tsx 是注册进内置rootslot 的三栏外壳,拥有 grid track 与拖拽手柄。与本次修复相关的三处关键代码:1. 折叠态由持久化偏好(及窄视口派生)决定,而非解析宽度(见 AppFrame.tsx#L146-L152):const narrow viewport SIDEBAR_AUTO_COLLAPSE useEffect(() { actions.setNarrow(narrow) }, [actions, narrow]) const sidebarCollapsed narrow ? !panels.narrowExpanded : panels.sidebar 0 const sidebarPreference sidebarCollapsed ? 0 : panels.sidebar 0 ? SIDEBAR_DEFAULT : panels.sidebar const cols computeColumns(viewport, sidebarPreference, detailsSession undefined ? 0 : panels.details)collapsed的判定来源是偏好(加上窄视口派生的自动折叠),这与 Note 中AppFrame marks the sidebar collapsed from the persisted width preference rather than from the resolved track width一致。若反过来从解析后的 track 宽度判断,56px 的 rail 本身又会反过来改变判定输入,形成自指。2. 折叠时移除拖拽手柄(见 AppFrame.tsx#L213-L215):{/* The collapsed rail is fixed-width: no resize handle while closed. */} {!sidebarCollapsed DragHandle sidesidebar left{cols.sidebar} ... /}56px rail 是定宽列,拖拽无意义,故手柄仅在展开态渲染;details 手柄则按cols.details 0条件渲染。3. 把collapsed作为 owner props 从渲染站点传入 sidebar slot(见 AppFrame.tsx#L188-L198):div className{css.sidebarCol} {/* 渲染站点 slot 调用:关闭态侧边栏以紧凑 rail 宽度保持 slot 挂载 */} {renderSlot(sidebar, { collapsed: sidebarCollapsed, width: cols.sidebar, })} /div这就是 Note 所说passescollapsedto the sidebar slot as owner props from the render site。sidebar slot 的契约类型(SidebarOwnerProps,含collapsed: boolean与width: number)可在 slot-catalog.ts 中检索到,其注释为:True when the sidebar is closed (the column renders the compact control rail)。轨道过渡定义在 AppFrame.module.css 中,与 Note 描述完全对应:/* 帧与手柄在 deepsuite sider 曲线上过渡轨道宽度和手柄 left */ transition: grid-template-columns var(--ds-transition-duration-slow) var(--ds-ease-in-out); /* 拖拽期间暂停:缓动轨道会把列边缘从指针上脱开 */ .frame[data-dragging] { transition: none; } media (prefers-reduced-motion: reduce) { .frame { transition: none; } }两个 CSS 变量--ds-ease-in-out与--ds-transition-duration-slow由 ui-theme 的 base sheet 提供。过渡在两处被暂停:拖拽期间(手柄以指针节奏写宽度,缓动会使列边缘脱离手柄)与prefers-reduced-motion下。SidebarRoot:slide crossfade 与 rail 控件SidebarRoot.tsx 读取 owner 传入的collapsedprop,把折叠实现为滑动(slide) 交叉淡入淡出(crossfade),而非形态插值(morph)。阶段一:冻结宽度淡出。折叠开始时,展开内容以行内样式冻结在其展开宽度上(SidebarRoot.tsx#L70-L74 的lastWideWidthref),在原位 150ms 内淡出,同时 AppFrame 的滑动 grid 轨道把它裁掉——滑动过程中不发生任何重排:// 折叠动画期间保持挂载(.collapsed .wide 淡出),settle 时卸载,展开时立即重挂载 const [settled, setSettled] useState(collapsed) useEffect(() { if (!collapsed) { setSettled(false); return } const timer window.setTimeout(() { setSettled(true) }, COLLAPSE_SETTLE_MS) return () { window.clearTimeout(timer) } }, [collapsed]) const wide !collapsed || !settledCSS 侧对应 .fading:/* 折叠阶段一:冻结宽度内容整体 150ms 原位淡出;settle 时子节点卸载/切入 rail 布局 */ .fading * { opacity: 0; transition: opacity 150ms var(--ds-ease-in-out); }COLLAPSE_SETTLE_MS 150与 150ms 淡出严格匹配,展开方向则用 200ms 的wide-in关键帧把 wide-only 内容淡回。阶段二:settle 时卸载宽态内容并切入 rail。到 settle 点,wide-only 内容(品牌、标签、输入框、会话树)卸载——这同时释放了 sessions 订阅,并把渲染树与可访问性树中不需要的控件一并清掉——而控制行则切换到 rail 布局(展开开关、新建会话、新建工作区、搜索,自上而下顺序与展开态行一致),并在滑动结束的剩余 150ms 内淡入:/* rail-in:4 个上部控件从 rail 右缘(translateX(49px))滑入,与轨道过渡收尾对齐 */ .railIn .iconButton, .railIn .newSession, .railIn .regionArea { animation: rail-in 150ms var(--ds-ease-in-out) backwards; } .railIn .footArea { animation: rail-fade-in 150ms var(--ds-ease-in-out) backwards; /* 底部设置位只淡入 */ }railIn类只加在一次实时的折叠上(由everWideref 判定):直接以折叠态冷启动(例如刷新时偏好就是关闭)会静态渲染 rail,不会出现延迟隐藏的图标闪现。rail 控件逐一继承展开态行为。每个 rail 控件保持其展开对应物的行为:搜索图标会展开侧边栏并在滑动结束后聚焦搜索框;搜索查询保存在 root 上,折叠往返(round trip)后依然保留;每个控件都带 tooltip;而展开开关的静止态是鲸鱼标记(whale mark,品牌 mark slot),hover 时切换为面板图标——这组行为在 SidebarRoot.tsx#L169-L186 与 CSS 的 .collapsed .toggle 规则 中实现:/* 折叠时:静止为鲸鱼 mark(品牌墨色、无 hover 圆底),hover 时显示面板图标 */ .collapsed .toggle .panelIcon { display: none; } .collapsed .toggle:hover .panelIcon { display: inline; } .collapsed .toggle:hover .railMark { display: none; }被否决的备选方案Note 的 Alternatives 一节记录了三个被否决的方向,其取舍逻辑对同类布局问题有参考价值:在中栏上方渲染一个展开按钮——否决:它只能恢复 toggle 这一个控件,恢复不了持久化的设置区,而且把侧边栏 chrome 拆给了两个包属主;保留 0 宽度 track,让 rail 溢出——否决:rail 会覆盖中栏,且 hit testing 与响应式几何同 grid 脱钩;保持完整侧边栏树挂载、仅用裁剪隐藏——否决:隐藏控件仍留在语义树中持续订阅与渲染,而折叠态只需要两个控件。三者分别对应恢复不完整、几何契约破坏与隐藏状态残留,最终选定的 rail 方案同时满足恢复完整、几何自洽与状态精简三个目标。影响与测试锁定行为影响(Note 的 Consequences 一节):折叠侧边栏占用 56px,而非把全部宽度让给中栏;展开时恢复持久化宽度与拖拽行为;设置入口保持可见,但沿用其既有的占位行为——本次修复不引入账户或设置界面;断言由三层测试锁定(见下)。测试覆盖,与 Consequences 中布局求解器测试钉住紧凑宽度、侧边栏组件测试钉住可见控件、web 冒烟测试钉住折叠与恢复一一对应:columns.client.spec.ts:验证sidebar 0时解析结果为SIDEBAR_COLLAPSED,例如 1920px 视口下{ sidebar: SIDEBAR_COLLAPSED, center: 1920 - SIDEBAR_COLLAPSED, details: 0 },并验证折叠 rail 仍参与让步链(空间受限时 details 先收缩、后自动关闭);app-frame.client.spec.tsx:验证帧级轨道[SIDEBAR_COLLAPSED, 0]与 slot 调用props为{ collapsed: true, width: SIDEBAR_COLLAPSED };sidebar-root.client.spec.tsx 及快照 sidebar-snapshot.client.spec.tsx.snap:钉住 rail 可见控件集合与折叠/展开的 DOM 结构。小结这次修复的本质是一次语义与几何的重新对齐:关闭在偏好层仍是 0,但在渲染层被解释为一个 56px 的固定 rail 契约——恢复控件(toggle 设置)永远有可见的落脚点,拖拽手柄、过渡与内容树则按两状态各自裁剪。整个方案由 columns.ts 的纯函数求解器、AppFrame.tsx 的 owner props 传递与 SidebarRoot.tsx 的 slide crossfade 动画共同支撑,并通过求解器、帧、组件与真实 bundle 冒烟四级测试钉住行为,是可作为布局折叠态死锁参考的完整案例。【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考