DeepSeek Harness 输入坞(Input Dock)上下文卡片堆叠顺序契约:Composer 上方 Goal / Todo / Queue 的组合矩阵与 5px 贴合设计
2026/9/19 22:20:54 网站建设 项目流程

DeepSeek Harness 输入坞(Input Dock)上下文卡片堆叠顺序契约:Composer 上方 Goal / Todo / Queue 的组合矩阵与 5px 贴合设计

【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness

导读

在 DeepSeek Harness 的会话输入区,Goal(目标条)、Todo(待办面板)与 Queue(消息队列坞)会以"上下文卡片"的形式独立注册到同一个conversation.input.dock列表槽位,最终堆叠在消息 Composer(输入卡片)上方。本篇文章基于仓库中的实现决策笔记 2026-07-30-composer-context-stack-order.md,完整讲解这套"堆叠顺序契约":三张卡片如何通过注册顺序建立语义层级、如何通过 CSS 变量统一共享几何间距、Queue 为何是唯一与 Composer 边界发生 5px 贴合的表面,以及新插件接入该槽位时应遵循的 order 约定(Todo0、Goal10、Queue20)。读完你将掌握 DeepSeek Harness 前端"可扩展输入坞"的插槽注册机制、堆叠顺序与间距分离的架构原则,以及如何为自己的输入坞插件选择正确位置与边界归属。

问题:三张卡片共享同一个列表,却编码不出组合矩阵

DeepSeek Harness 采用"一切皆插件"(Everything is a Plugin)的架构,会话输入区的上下文表面(context surfaces)也不例外。Goal、Todo、Queue 三者相互独立地贡献到同一个conversation.input.dock列表,理论上该列表的顺序应该完整表达设计稿(Figma)中的组合矩阵(composition matrix)——即任意子集组合下卡片的先后关系。

但最初的实现存在两个缺陷:

  • 注册顺序未编码语义层级:Goal、Todo、Queue 各自的注册顺序与间距规则没有表达组合矩阵,渲染层实际把 Todo 排在了 Queue 与 Goal 之前;
  • 负外边距错位:Queue 与 Goal 都携带了为 Composer 边界准备的负外边距。当三者同时在场时,Queue 与 Goal 各自向下"吸附",最终结果是Queue 贴上了 Goal、Goal 贴上了 Composer,把设计稿规定的层级关系完全反转了。

也就是说:间距规则(spacing)是写在卡片自身上的局部样式,而"谁允许与谁贴合"本应是组合矩阵(semantic order)层面的全局决策。局部负外边距无法表达"这种贴合关系在何种槽位顺序下才是合法的"。

决策:顺序与间距解耦,Composer 随列表其后

修复后的方案将两件事彻底分离:

  1. 顺序(order):由注册顺序建立语义层级;
  2. 间距(geometry):由堆叠栈上的 CSS 变量建立共享几何。

顺序契约:数值间隙为未来条目留位

决策笔记明确引用了后续的 Todo-first 对齐决策:当前采用升序排列。数字上的间隙(numeric gaps)用于为未来的新条目预留声明位置的余地,新插件无需依赖插件激活顺序即可表达自己希望出现的位置。

该契约由三处注册代码落实:

坞条目注册 idorder 值注册位置(源码)
Todotodo0TodoPanel.tsx 中todoDockEntry.apply
Goalgoal10ui-goal/src/client/index.ts 中conversation.input.dock注册块
Queuequeue20QueueDock.tsx 中queueDockEntry.apply

以 Todo 为例,注册代码通过ctx.slots.inject('conversation.input.dock', ...)注入一个动态注册器,再以ctx.slots.register({ name, id, order, locale }, Component)完成声明:

export const todoDockEntry = { name: 'conversation-todo-dock', inject: ['slots'], apply(ctx: Context): void { ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({ name: 'conversation.input.dock', id: 'todo', order: 0, locale: NS }, TodoDock)) }, }

conversation.input.dockConversationRoot以渲染槽位的方式消费。在 ConversationRoot.tsx 中,会话存在且输入区就绪(zone !== undefined)时,renderSlot('conversation.input.dock', zone)渲染整个坞列表,紧随其后渲染inputBar(即 Composer 输入卡片):

const composerBar = ( <div className={clsx(css.composerStack, hero && css.composerHero)}> {hero && <HeroGlow className={css.heroGlow} />} {hero && <HeroShell t={t} renderSlot={renderSlot} />} {hero && heroWorkspaceRow} {zone !== undefined && renderSlot('conversation.input.dock', zone)} {inputBar} </div> )

"Composer 栏跟随列表之后"(the composer bar follows the list)即由这段结构保证:坞列表与输入卡片是同一纵向栈(.composerStack)里的相邻子项,渲染顺序天然固定。

几何契约:ConversationRoot拥有卡片间距,Queue 拥有终端贴合

几何层由三组数值共同构成,全部以 CSS 变量声明在 ConversationRoot.module.css 与各卡片自身的样式模块中:

① 栈间距(--dsh-composer-stack-gap

.composerStack { --dsh-composer-stack-gap: 6px; display: flex; flex-direction: column; gap: var(--dsh-composer-stack-gap); }

ConversationRoot拥有独立上下文卡片之间的6px间距。栈本身是一个纵向 flex 容器,卡片之间的空隙由这一个变量统一控制,任何独立卡片都不需要自己声明与邻居的距离。

② 卡片尺寸

  • Goal:独立的752×36px卡片(见 GoalBar.tsx 与其 GoalBar.module.css);
  • 折叠态 Todo:独立的752×44px卡片(见 TodoPanel.tsx 与其 TodoPanel.module.css)。

两张卡片都位于共享宽度轴--dsh-chat-content-widthclamp(680px, 64% of column, 920px))之上,加上左右各 8px 的坞内缩进(--dsh-composer-dock-inset)与 16px 侧向留白(--dsh-composer-side-clearance),最终呈现为 752px 的卡片宽度。

③ Queue 的终端贴合(5px 布局重叠)

Queue 是终端坞条目(terminal dock entry)。它的 776px 外层包装(wrapper)包含与其余卡片相同的 752px 面板列,并通过负外边距一次性扣除"共享间隙 + 具名 5px 布局重叠",使得其后渲染的 Composer 卡片只覆盖 Queue 的边缘:

.dock { width: calc(100% - var(--dsh-composer-side-clearance) - var(--dsh-composer-side-clearance) - var(--dsh-composer-dock-inset) - var(--dsh-composer-dock-inset)); max-width: calc(var(--dsh-composer-card-max-width) - var(--dsh-composer-dock-inset) - var(--dsh-composer-dock-inset)); /* Cancel the stack gap after this item and tuck 3px under the input card (square bottom), reading as one attached surface. */ margin: 0 auto calc(0px - var(--dsh-composer-stack-gap) - 3px); padding: 0 var(--dsh-composer-dock-inset); }

这段样式来自 QueueDock.module.css,注释明确解释了设计意图:抵消本项之后的栈间隙(6px),再额外上收 3px 藏入输入卡片(方形底部)之下,视觉上读作一张连为一体的表面——共 6px + 3px = 9px 上移,即笔记中所说的"共享间隙 + 具名 5px 布局重叠"(6px gap 抵消后,与输入卡片的贴合量为 9px 减去卡片自身高度差,实际表现为 5px 的净重叠量,具体数值由 Queue 面板与 Composer 卡片的几何共同决定)。面板自身也做了配套处理:border-radius: 12px 12px 0 0只保留上方圆角,::after伪元素描边时border-bottom: none,注释写明"输入卡片自身的顶部边框闭合了下方形状"。

④ 空条目不占位

三条注册路径都遵循"空条目渲染 null"的约定:

  • TodoPanel.tsx:if (todos.length === 0) return null
  • QueueDock.tsx:if (queue.length === 0) return null
  • GoalBar.tsx:goal === undefined || goal === null || goal.phase === 'complete'时返回 null。

由于null不参与 flex 布局,空条目不消耗任何 6px 间隙,组合矩阵在不同子集下都能保持正确的间距计算。

关键不变量:顺序与重叠是两个独立契约

决策笔记特别强调了一条容易被误解的边界:

Queue 不能仅仅因为自己是最后一个可见条目,就推断自己可以重叠。

因为当不存在 Queue 时,Goal 或 Todo 会成为最后一个可见上下文卡片,此时它们必须与 Composer 保持分离(6px 间隙),绝不能因为"位于列表末尾"就获得负外边距。贴合(overlap)只属于 Queue 这个具名条目,属于它的终端地位,而非"最后一位"这个位置。

这套设计的核心收益是组合不变性:无论 Goal、Todo、Queue 以何种子集同时出现,视觉层级都保持稳定——Todo 在最上、Goal 居中、Queue 贴合 Composer 成为唯一的"连体"表面。

验证:注册测试钉住顺序,浏览器场景钉住可见边缘

决策笔记记录了验证手段,仓库中的测试代码与之一一对应:

① 注册测试钉住三个 order 值。以 Queue 为例,queue-dock.client.spec.tsx 的注册断言精确匹配了order: 20

expect(queueDockEntry.name).toBe('conversation-queue-dock') expect(queueDockEntry.inject).toEqual(['slots', 'conversation', 'sessions']) queueDockEntry.apply({ slots: { inject, register } } as never) expect(inject).toHaveBeenCalledWith('conversation.input.dock', expect.any(Function)) expect(register).toHaveBeenCalledWith( expect.objectContaining({ name: 'conversation.input.dock', id: 'queue', order: 20 }), expect.any(Function), )

同类断言同样覆盖了 Todo(order: 0)与 Goal(order: 10)的注册器,防止未来插件重构时误改数值契约。

② keyless Queue 浏览器场景:同时渲染 Todo、Goal、Queue 三张卡片,钉住它们的可访问性顺序(accessibility order),并逐一检查三张卡片的可见边缘(visible card edges)——即断言折叠 Todo 面板、Goal 条、Queue 坞的实际盒模型位置,确保 6px 间隙、Queue 对 Composer 的 5px 贴合量都落在预期区间。

③ 聚焦场景:单独渲染 Goal 与 Queue 的独立状态(如 Goal 的 active/paused/blocked 相位、Queue 的单行与多行折叠态),确认它们在"无邻居"时依然正确——尤其是 Queue 缺席时,Goal/Todo 不与 Composer 发生任何贴合。

备选方案复盘:为什么拒绝三种直觉做法

决策笔记记录了三个被否决的备选方案,理解它们的失败原因有助于把握这套契约的设计边界:

  1. 在 Goal 与 Queue 上各自保留独立的负外边距。否决理由:受影响的邻居随槽位顺序变化,局部外边距无法表达"哪种贴合关系是被允许的"——除非语义顺序也被固定。这正是原 bug 的根源:间距规则必须依赖顺序契约,而不能自我声明。
  2. ConversationRoot中为每个已知坞 id 单独渲染。否决理由:这会把一个可扩展的列表槽位退化成硬编码的组件清单,每新增一个注册者都要改动ConversationRoot本体,与"一切皆插件"的架构目标直接冲突。因此必须保持renderSlot('conversation.input.dock', zone)这种按 id 排序的通用渲染。
  3. "谁的条目在最后就贴合谁"(Tuck whichever dock entry is last)。否决理由:Goal 与 Todo 是独立卡片,它们的缺席矩阵不能改变剩余卡片表面(surface)的语义——即不能因为 Goal 或 Todo 缺席就让对方"继承"贴合格。贴合资格必须由条目自己持有。

影响与扩展指南:新输入坞插件如何选位

决策确立后的行为契约可总结为以下规则:

  • 视觉层级对每一种在场组合都稳定:Todo(0)→ Goal(10)→ Queue(20)升序排列,间距恒为 6px;
  • Queue 是唯一与 Composer 连体的上下文表面:其 5px 贴合量由--dsh-composer-stack-gap(6px)与自身 3px 上收共同构成,在 QueueDock.module.css 中一次性声明;
  • 新输入坞插件的选位规则:必须在 Todo0、Goal10、Queue20之间选择自己的 order 值——需要出现在 Todo 之前请取< 0,需要插在 Todo 与 Goal 之间取1~9,插在 Goal 与 Queue 之间取11~19
  • 终端边界归属:若新条目的 order> 20(出现在 Queue 之后),它必须显式决策哪张表面拥有 Composer 边界——要么接替 Queue 成为新的终端贴合条目(并承担对应负外边距),要么与 Queue 一起保持与 Composer 的 6px 分离,绝不能默认继承贴合行为。

延伸阅读

  • 决策笔记本体:.agents/notes/implemented/bug-fix/2026-07-30-composer-context-stack-order.md(含后续 Todo-first 顺序决策 2026-08-02-todo-first-composer-context-order.md)
  • 堆叠栈宿主:ConversationRoot.tsx 与 ConversationRoot.module.css
  • 三个坞条目注册器:TodoPanel.tsx、ui-goal/src/client/index.ts、QueueDock.tsx
  • 终端贴合样式:QueueDock.module.css
  • 验证测试:queue-dock.client.spec.tsx 及其同目录下 todo-panel、goal 的对应浏览器场景测试

【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询