- 人工智能
- AI Agent
- 自主智能体
- 桌面应用
- MCP Clients
【免费下载链接】Kun
Local-first AI agent workspace for coding, writing, design, research, and automation — one runtime for desktop GUI and TUI.
导读
本篇文章基于 Kun 仓库中的 OpenSpec 变更提案《match-deepseek-harness-composer-queue-ui》展开,讲解 Kun 如何在桌面 GUI 中把"繁忙轮次期间排队等待发送的后续消息"从旧的QueueStrip + hover Portal交互,重构为贴合 DeepSeek Harness 冻结参考实现的Composer QueueDock(依附于输入框的队列停靠坞)。读完本文你将掌握:单行/多行折叠展开的 disclosure 语义、把排队消息"还原到输入框"的安全编辑策略、Guide/Retry/Remove 动作的串行化与失败保留、展开队列内的拖拽与键盘排序,以及 36px/28px/12px/180px 等固定几何参数与 Kun 语义主题令牌的映射方式,并了解对应组件、store 与测试在仓库中的具体位置。
一、为什么改:旧队列呈现的三个痛点
Kun 此前已经在 renderer 侧实现了完整的排队消息机制:线程级持久化、FIFO 顺序排空(drain)、暂停/失败恢复以及显式的"当前轮次引导"(mid-turn guidance)。但它的呈现层存在明显短板,提案 proposal.md 将其归纳为三点:
- 信息被压缩隐藏:旧实现只把第一条消息投影到一条脱离输入框的
QueueStrip上,其余消息折叠成+N徽标,完整列表要悬停或聚焦触发 Portal 才可见。多条待发输入难以被发现,也更难控制。 - 编辑语义是破坏性的:旧交互中的"编辑"(overflow Edit menu)会执行"出队到草稿"(dequeue-to-draft)操作,本质上是把消息从队列里取出,容易让人误以为只是修改。
- 失败消息被静默过滤:失败状态的排队消息被直接隐藏,用户无法看到失败原因,也没有重试入口。
而 DeepSeek Harness 的冻结参考实现(master@0a53fb55bea101816fa226bb964ae2bed71c343b)采用依附输入框的 QueueDock:单条直接展示、多条折叠成总数、编辑把消息交回输入框、失败时队列仍然可见。Kun 据此发起本次 UI 对齐改造,设计文档见 design.md,验收需求见 specs/composer-queue-dock/spec.md。
二、目标与非目标:对齐外观,不动运行时语义
设计文档明确划定了本次改造的边界:
Goals(要做)
- 完全对齐冻结参考 QueueDock 的层级、几何、折叠规则、编辑流程、动作顺序、可访问性、主题行为与窄宽度约束;
- 保留 Kun 现有的 FIFO 交付、图片/Plan/Graph 引导资格、暂停/失败恢复、幂等性、冻结的 model/provider/routing 快照以及线程级持久化;
- 让失败消息可见且可恢复;
- 允许用户在展开的 QueueDock 内通过直接操作把靠后的消息移到更前;
- 补充确定性的组件测试、store 测试与 Electron 交互/视觉证据。
Non-Goals(明确不做)
- 不改变运行时 steering 路由、终态密封(terminal sealing)、交付顺序、持久化格式、发送快捷键偏好或普通繁忙轮次的提交行为;
- 不引入 DSH 的全局队列/steer 快捷键策略,也不会自动 steer 排队消息;
- 不在列表折叠、行还原或其它队列变更进行中时提供排序或 overflow 菜单。
设计文档还特别强调:历史上存在一个被完全 revert 的提交8fbb9885c(混入了无关的消息时间线行为),因此实现必须从冻结的 Harness 源码按当前 Kun 合约重建 QueueDock,而不是直接 cherry-pick 那个提交。
三、核心交互一:单行直显 / 多行折叠展开(Disclosure)
组件 FloatingComposerQueuedMessages.tsx 实现了 DSH 风格停靠坞。其规则完全对应 spec 中的 Requirement "QueueDock uses reference disclosure behavior":
| 可见排队消息数 | 呈现方式 |
|---|---|
| 0 条 | 不渲染任何 dock(queue.length === 0时组件直接返回null) |
| 1 条 | 直接渲染完整行,无计数头部、无 Portal |
| 2 条及以上 | 默认折叠成"总计数"头部按钮,点击在原位展开/折叠完整的 FIFO 列表 |
关键实现细节在 FloatingComposerQueuedMessages.tsx:
if (queue.length === 0) return null const interactionActive = busy !== null const expanded = queue.length === 1 || !collapsed || interactionActivevisibleQueue负责过滤可见行(L124-L135):pending、paused、failed、starting、in_flight均可见;- 起始/在途行保持可见是有意设计:其用户消息已归属一个被受理的 turn,但用户仍可查看状态;一旦 runtime 真正开始执行该 turn(
queuedMessageStartedByRuntime),排空逻辑会将其从队列中消费掉,避免与时间线中的用户消息重复(见 queued-message-persistence.ts); - 交互期间强制保持展开:当 restore 或队列变更进行中且队列含多条消息时,头部折叠按钮被
disabled(disabled={interactionActive}),防止用户在异步动作进行中误折叠; - 队列清空后重置折叠状态:
useEffect在队列长度归零时setCollapsed(true)(L167-L169),保证下一次多行队列重新以折叠形态出现。
计数头部的文案由queuedMessagesTitle/queuedMessagesConfirming两个 i18n key 提供,并带count参数,多语言文案集中在 phone-composer.json 等 locale 文件中。
停靠坞如何"长"在输入框上
设计上,QueueDock 不再属于绝对定位的 hover 状态栈,而是通过FloatingComposerAboveInputStack新增的attachedDock座位渲染在浮动状态之前(FloatingComposerAboveInputStack.tsx),实际装配点在 FloatingComposerStackView.tsx 的attachedDock={(<FloatingComposerQueuedMessages ... />)}。这样 dock 的底边被 composer 卡片物理封住,不会被 Todo/Graph/Goal 等浮层从中间隔开。组件根节点上的data-composer-stack-item="queue"标记也使其可以参与 composer 栈的布局管理。
四、核心交互二:编辑 = 还原到输入框,且冻结投递设置
这是本次改造中最关键的语义变化:不再提供行内编辑器,编辑动作一律把消息从队列还原回 composer 输入框,随后输入框获得焦点供继续修改。
4.1 安全还原策略(restore eligibility)
不是所有排队消息都能被"忠实重建"回输入框。纯策略函数位于 queued-message-edit.ts,queuedMessageEditBlockReason对以下情况返回阻止原因:
- 存在
steeringRequest(引导请求在途); waitForRuntimeAdmission为 true(暂定受理未定);deliveryState是starting(确认中);- 携带 persona、guiPlan、graph 编排等派生结构;
agentSurface为write/design;- 携带 subagentResume、messageSource、fileReferences、composerContexts、设计画布/设计模式/设计工件、writeContext 等无法重建的负载;
- 附件中包含
document类附件(文档内容已被内联进 prompt,无法还原为输入框附件); - 文本与附件均为空。
只有"pending 的纯文本(可选displayText缺失或与text完全一致)或图片附件消息"才允许还原。queuedMessageComposerRestoreText还处理了一个细节:Plan/auto 队列消息会把用户原始请求存在displayText、内部计划 prompt 存在text,还原时取回displayText;纯图片消息的text是合成 prompt,还原时保留空文本、依赖附件恢复。
4.2 store 侧的还原动作
store 动作restoreQueuedMessage(id, accept)实现在 chat-store-thread-queue-actions.ts,流程是:
- 通过
withQueueMutation加互斥锁(见下文"动作串行化"); - 若行处于
starting且受理未定,先awaitQueueAdmission等待受理落定; - 用
queuedMessageEditBlockReason做二次校验,被拒绝则走现有error通知路径,行保持排队不动; - 通过
restoreQueuedMessageFromQueue(queued-message-edit.ts)按稳定 id从数组中移除该消息(不触碰其它记录、不改变顺序); - 若该行已进入 runtime 队列(有
deliveryTurnId),先saveQueueEditIntent把行标记为editIntent: 'cancelling'并调用provider.cancelQueuedTurn取消服务端排队 turn,再标记为restoring; - 通过
accept(message)回调把文本追加到现有草稿、恢复图片附件、重放冻结的 mode/model/reasoning/permission 设置,然后持久化并从队列中删除该行。
设计文档强调:还原被拒的场景(id 不存在、负载不支持、行已 paused/failed/starting/in-flight)都不会删除原行;还原过程中若队列实时更新导致该行被移除,composer 草稿保持完整,用户继续编辑即可。
五、核心交互三:动作串行化与失败可见(Guide / Retry / Remove)
5.1 动作单飞行(single-flight)
组件维护一个本地busy: { id, kind }状态(FloatingComposerQueuedMessages.tsx),applyAction包装器保证任意时刻只有一个动作在途(L255-L269):busy非空时,折叠头部、所有行的编辑/移除/引导按钮全部禁用,行内图标替换为旋转的Loader2spinner。
store 层还有第二道互斥:queue-mutation-fence.ts维护线程级mutations集合,withQueueMutation在动作执行期间占用、结束后释放(queue-mutation-fence.ts)。这与组件层busy配合,防止并发 drain 与用户操作竞争。
5.2 Guide 与 Retry
普通 pending 行的动作顺序为Edit → Remove → Guide(参考顺序)。canGuide判定规则(FloatingComposerQueuedMessages.tsx)很细致:
- pending 行:仅当有活跃 turn、
guidanceEligible !== false且queuedMessageGuidancePayload非空时可引导(引导受steeringRequest/deliveryTurnId等合约约束); - paused/failed 行:按钮文案变为Retry,仅当当前没有活跃 turn 且该行没有等待 runtime 受理(
!message.waitForRuntimeAdmission)时可用; waitForRuntimeAdmission的失败行保留 Remove 但禁用 Retry——因为其原始受理等待器已落定,重放会导致在 drain 期间消息再次消失。对应 i18n 文案为queuedMessageRetryUnavailable:"This provisional message cannot be retried safely; remove it and submit again"。
store 动作guideQueuedMessage(chat-store-thread-queue-actions.ts)负责真正执行:对 graph turn 走steerSourceTurn,否则调用 provider 的steerUserMessage;对 paused(已 admission)行优先resumeQueuedTurns恢复服务端队列;对 failed 行重新生成clientRequestId并标记为 pending 后重新 drain。引导成功后该行从队列移除并乐观插入用户块;失败时行保留在队列中,error路径负责通知。
5.3 失败消息保持可见
spec 的 Requirement "Queue actions are lossless and serialized" 明确:失败行在 QueueDock 中始终可见,行内显示失败状态与errorMessage/errorCode(36px 行高不变,完整错误通过title属性暴露),并提供 Remove;仅当活跃 turn 空闲且行未被已落定的受理等待器绑定时才启用 Retry。reconcileQueuedMessages(queued-message-persistence.ts)保证终态失败跨 reconciliation 保持failed,只有显式 Retry 或 Remove 才会改变它。
六、核心交互四:展开列表内的拖拽与键盘排序
DSH 参考实现本身不提供排序,这是 Kun 在保持参考几何不变的前提下为用户需求增加的扩展(见 design.md 的 "Reorder expanded queues by stable identity")。
启用条件(FloatingComposerQueuedMessages.tsx):
const reorderEnabled = Boolean(onReorder && expanded && queue.length > 1 && !interactionActive && !queue.some((message) => message.editIntent || message.deliveryState === 'starting'))即:列表已展开、可见行数 ≥ 2、无进行中的编辑/变更、无 restoring/starting 行时才显示 28px 的拖拽把手(GripVertical)。
拖拽逻辑:HTML5 drag 事件中,dragover时用指针与目标行中点的clientY比较选择before/after(L218-L232),CSS 通过[data-drop-position='before']::before/'after'渲染 2px 插入指示线(FloatingComposerQueuedMessages.module.css);drop时校验源/目标 id 都仍存在,再调用持久化的reorderQueuedMessage(sourceId, targetId, position)。
键盘排序:把手聚焦后按ArrowUp/ArrowDown每次移动一个可见位置,focusReorderHandle保证焦点停留在稳定把手上,同时通过role="status"的aria-live区域播报新位置(L190-L206)。
store 侧排序(chat-store-thread-queue-actions.ts):若两行都已进入 runtime 队列(有deliveryTurnId),先调用provider.moveQueuedTurn同步服务端顺序;随后在本地数组中按稳定 id 重新插入并持久化。queue store 是唯一顺序权威——组件本地拖拽状态从不修改影子数组,queueRevision变化即清空临时拖拽状态,源行/目标行在 drop 前被实时队列移除时不会误排序其它行。
七、几何与主题:固定 Harness 尺寸 + Kun 语义令牌
CSS Module FloatingComposerQueuedMessages.module.css 完整落地了参考几何:
| 几何参数 | 值 | CSS 位置 |
|---|---|---|
| 头部/行高 | 36px | .header, .row |
| 动作按钮尺寸 | 28px(width/height) | .action |
| 拖拽把手尺寸 | 28px | .dragHandle |
| 动作间距 | 10px(gap) | .actions |
| 顶部圆角 | 12px(底部直角贴合 composer) | .panel |
| 内部滚动上限 | 180px(max-height+overflow-y: auto) | .list |
| composer 上缘 3px 收进 | margin: 0 auto -3px | .dock |
色彩方面不引入任何硬编码的仅亮色颜色或独立的深色分支,全部映射到 Kun 语义令牌:--ds-surface-subtle(面板底)、--ds-border/--ds-border-muted(边框与分隔)、--ds-text/--ds-text-muted/--ds-text-faint(文本层级)、--ds-accent(焦点环与插入指示线)、--ds-danger(失败状态),暂停状态用#d97706与文本色混合。prefers-reduced-motion: reduce下 spinner 动画被禁用,状态传达不依赖动画。
窄布局约束:dock 宽度为calc(100% - 1rem)并居中,预览文本text-overflow: ellipsis单行省略,状态徽标限宽 30%,不再有固定 Portal 溢出视口。
八、可访问性与本地化
spec 的 Requirement "QueueDock matches reference geometry and accessibility" 对无指针用户有明确要求,实现要点:
- 折叠头部按钮带
aria-controls={listId}与aria-expanded={expanded}; - 每个动作按钮有可访问名称(
aria-label)与禁用说明(title);编辑按钮不可用时,label 切换为queuedMessageEditUnsupported等阻止原因文案; - 还原成功后 composer 输入框获得焦点;
- 排序把手带
aria-label(含位置序号与消息摘要)与aria-describedby提示,位置变更通过role="status"+aria-live="polite"播报; - 主题行为:语义表面/边框/文本令牌保证亮色、深色与自定义主题下状态对比度可读。
九、兼容性与迁移:持久化与交付合约不变
spec 的最后一个 Requirement "Existing queue delivery contract remains compatible" 与 design 的 Migration Plan 呼应:
- 无持久化迁移:排队记录仍存于
kun.queuedMessages.v1注册表(queued-message-persistence.ts),旧的 pending/paused/failed/starting/in_flight 记录由新 dock 的可见性规则直接投影,不重写存储; - 普通繁忙轮次提交(未激活 Guide)仍按原 FIFO 语义进入下一轮次交付,
drainQueuedMessages循环(chat-store-thread-queue-actions.ts)行为不变; - 回滚只需还原旧 renderer 组件,store/runtime 队列数据保持兼容。
十、验证与测试:从组件到 Electron 冒烟
任务清单 tasks.md 记录了四轮验证工作:
- Store/契约:新增并持久化
restoreQueuedMessage(id),纯函数安全还原策略,restore 经 Workbench 与 composer props 接通; - Renderer:
attachedDock座位、单/多行 disclosure、restore-to-composer 编辑、动作单飞行、失败保留与 Retry、CSS Module 几何与多语言文案; - 组件测试:FloatingComposerQueuedMessages.dock.test.ts 覆盖单/多行 disclosure、实时更新、还原编辑、动作锁定、失败状态与可访问性,以及拖拽/键盘排序与竞态安全 no-op;
- Electron 冒烟:FloatingComposerQueueDockSmokeFixture.tsx 提供
single/multi/long/failed四种确定性场景,覆盖 36/28/12/180 几何、与 composer 的贴合、窄布局以及亮/暗双主题截图;最后跑npm run typecheck、npm run build:kun、npm run build、ESLint、文件行数/OpenSpec/diff 检查,并基于独立 worktree 从本地developHEAD 以 fast-forward 合入、验证改动范围仅限 queue/composer/OpenSpec/测试文件。
十一、风险与权衡(来自 design.md)
| 风险 | 缓解 |
|---|---|
| 还原派生 prompt 导致可见文本与 runtime 文本失同步 | 仅允许 composer 可忠实重建的消息(纯文本镜像或图片附件),拒绝文档/结构化负载 |
| 实时队列更新移除了正在还原的行 | 行离开队列,composer 草稿完整,编辑在输入框继续 |
| 还原期间第二条消息到达 | 交互状态强制新多行 dock 展开 |
| 长队列盖过 composer | 列表上限 180px,仅列表内部滚动 |
| 拖拽与队列投递竞争 | 以稳定 id 寻址、变更期间禁拖、实时退役时清空临时拖拽态、store 空操作安全收敛 |
| 失败项包含超长错误文本 | 行保持 36px,显示紧凑失败指示,完整错误经 title 属性暴露 |
| 暂定受理失败被重试(waiter 已落定) | 禁用不安全 Retry,保留行,引导用户 Remove 后重新提交 |
结语
本次改造把 Kun 的排队消息呈现从"隐藏 + Portal"升级为"依附输入框的停靠坞",用一套精确到像素的几何参数对齐 DeepSeek Harness 参考实现,同时把编辑语义从破坏性出队改为安全的"还原到输入框",让失败消息可见可恢复,并以"store 为唯一顺序权威"的方式增加了拖拽/键盘排序。整套设计围绕一个不变式展开:UI 可以重做,但 FIFO 交付、持久化格式与运行时语义必须原样保留。如果你需要在 Kun 桌面端定位排队消息相关的进一步开发,可以从 FloatingComposerQueuedMessages.tsx(呈现)、chat-store-thread-queue-actions.ts(store 动作)与 queued-message-persistence.ts(持久化与调和)三条主线入手。
- 人工智能
- AI Agent
- 自主智能体
- 桌面应用
- MCP Clients
【免费下载链接】Kun
Local-first AI agent workspace for coding, writing, design, research, and automation — one runtime for desktop GUI and TUI.
相关推荐
DeepSeek Harness 待处理队列的寻址编辑与移除:InboxItemId、权威快照与 QueueDock 交互设计
DeepSeek Harness 待处理队列的寻址编辑与移除:InboxItemId、权威快照与 QueueDock 交互设计 本文依据仓库内 Agent No
人工智能AI AgentAgent 框架DeepSeek彻底解决消息乱序:nats-server消息排序的实现与最佳实践
彻底解决消息乱序:nats server消息排序的实现与最佳实践 你是否曾因分布式系统中的消息乱序问题而头疼?订单支付通知比下单消息先到达,导致状态异常;传感器
后端消息队列消息路由通信消息管理终极指南:Rocket.Chat消息撤回与编辑全攻略
消息管理终极指南:Rocket.Chat消息撤回与编辑全攻略 在团队协作中,发送错误消息或需要修改已发送内容的情况时有发生。Rocket.Chat作为一款注重数
即时通讯后端前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考