Files
deepseek-harness/.agents/notes/implemented/architecture/2026-07-25-web-input-machine-and-slash-pipeline.zh.md
Yif be9042e5a1 docs(client): sync package READMEs and agent notes with the input interaction rework
Update the six touched client package README pairs (slash menu ordering,
localized group titles and dismiss, permission label twin, goal pause,
plan hint localization, useAnchoredMaxHeight) and keep the owning agent
notes current: SlashSource.order and the MenuView dismiss/localize/clamp
face in the slash-pipeline note, the pause verb in the goal bar note.
2026-07-29 20:46:29 +08:00

16 KiB
Raw Blame History

Agent Note: Web 输入状态机、composer 坑位与 slash 管线ui-conversation input / ui-slash

Status: implemented

English | 中文

范围输入状态机occurrence 表 + claim 看护 + 提交事务、hub/facade 与发送编排、跨插件输入改写的三个 scoped bail 事件、/@ 触发检测与菜单管线ui-slash、composer 周边坑位体系。依赖会话作用域 note的 sctx / provide / session-maybe 与 blank 实体模型命令知识三型、目录、popup零涉——那是命令业务面 note的领地。

问题

两个各自为政的 composerheroEmptyState受控链直写 Session与会话内 InputBar普通受控 textarea行为、draft 所有权、发送路径全不一致。要让 / 命令、skill 引用、@ 引用三类触发进入输入面,必须回答:

  • 三类触发如何分层,谁对"命令"有知识、谁零知识;
  • 输入框如何表达"命令态"——从 draft 文本推导还是显式状态?退格、回车、空格、整行粘贴各是什么语义;
  • 提交是异步事务RPC 往返——晚到结果回灌、会话切换、React concurrent 重放如何防御;
  • 引用 chip 在纯 textarea 上如何表示undo/剪贴板/粘贴匹配/模型序列化各归谁;
  • 跨插件的输入改写菜单回填、引用插入、token 消费)如何做到依赖倒置;
  • 无 session → blank session 时哪些 React 外壳必须复用,哪些严格 session 输入体允许替换。

硬约束:组件一律经 slots 挂载;呈现物不进 session log键盘路径全程 IME 安全。

决策

输入状态机(InputMachine

纯状态机,事件进/效果出,注入时钟。四相 phaseplain / adjudicating / claimed / submitting。命令态永不从 draft 推导,由 pick 路径在离散时刻显式建立claim 由 draft.startsWith(token) 看护、退格破坏自动 releaseclaim 形状 {token, hint?}hint 供 ghost text

事件面(dispatch(ev) 单写入口,每个事件一个 transaction

  • draft-changed {draft, editRange?}——textarea 全量草稿editRange 缩小 occurrence 平移计算,缺省前后缀共扫。
  • newline {selection}——Ctrl+Enter 换行(不经浏览器 execCommand自管 undo 下浏览器写入会分叉双历史)。
  • begin-command {claim, span} / insert-ref {reference, span} / consume-token {guard}——三个 bail 事件的机器侧span CAS = draftRev 相等。
  • set-invalid {invalidIds}——owner resolution 结果的样式位(非 transaction
  • undo / redo——自管 transaction log环形 100单字符打字按注入时钟窗合并提交成功清 log
  • paste-begin {text, selection, components?, generation?}——粘贴 + 热快照同步匹配组件同 transactionUndo 一次回粘贴前);打开 PasteMatchAttempt。
  • paste-upgrade {attemptId, span, reference}——异步匹配升级为独立 transactionUndo 两段attempt 保持 currentinsertedRange 随升级收缩。
  • invalidate-paste——DOM 层观察到的 attempt 终结手势caret/selection 操作等)。
  • enter {mode} / adjudicated / adjudication-failed / submit-settled / release——提交事务平面SubmitAttemptseq + AbortSignal防回灌成功 commit 清稿,失败带漂移守卫 rollback回车时快照仅当 live draft 仍等于它才回填;用户已再输入则只发 notice

效果面shell 执行):adjudicate(调 SlashController.adjudicatebegin-submitclaim.submit 事务)、default-sink普通消息hub 编排)、notice

occurrence 表与 chip 三投影:

  • 每颗引用在 draft 中占一个 U+FFFC;表项 {occurrenceId, source, ref, offset, label, clipboardText, invalid?};同名 chip 因 occurrenceId 独立。
  • 一切编辑同 transaction 更新 draft 与表:区间平移;与占位符相交的删除/替换作用于整颗。
  • 单字符占位使键盘原子性大半原生成立caret 无内部位Backspace/方向键/Shift 扩选原生即整颗);鼠标点 chip 由 backdrop 命中 → 整颗 setSelectionRange。
  • 视觉投影 = labelbackdrop 在占位符 offset 渲染 chiptextarea 字形不可见invalid 走失效样式。
  • 剪贴板/持久化投影 = clipboardTextcopy/cut 把选区内占位符展开draft 持久化 mirror 写同一投影chat store 里永远是普通文本,刷新 seed 语义 = 全选复制→重开→粘贴chip 跨刷新降级为文本)。
  • 模型投影 = submit 时经 source codec.serialize 逐颗生成(归 submit attempt 的 signal 与 stale guardowner 缺失/失败/取消则不发送,不降级为 /name)。

跨插件输入改写:三个 scoped bail 事件

契约声明在 ui-slash依赖最底层生产者经 sctx.bail(sctx, ...) 派发,唯一消费侧是 hub 建 shell 时挂在 sctx 上的三个 listener返回 true ⟺ 机器过 phase + CAS 守卫并实际改写(发出事件 ≠ 修改成功Space 是否 preventDefault 以返回值为准):

  • slash/input-begin-command {claim, span}——菜单 pick / Space 裁决出的命令 claim 回填SlashController 派发)。
  • slash/input-insert-reference {reference, span}——引用 chip 插入SlashController 派发)。
  • slash/input-consume-token {guard: span | bare-token}——业务成功后消费命令 token下游命令面派发

不事件化的调用registry 注册 → 显式调用 → awaitInput 自身的 draft/submit、Enter 异步裁决、reference serializer、异步 paste matcher。@mode bail 已入 JSDoc parser 与 cordis catalog 门禁scripts/jsdoc.ts

slash 管线ui-slashroot SlashService + per-session SlashController

对"命令"零知识的触发/菜单/pick 管线:

  • service 只有 source 注册表(SlashSource{trigger: '/'|'@', name, order?, candidates, onPick, matchSpace?, matchEnter?}(trigger,name) 唯一;可选 order 对 roster 排序——越小越靠前、默认 0、同值保持注册序——排序后的 roster 同时是组序与轮询序)与 sessionOf(sctx)。实现 match 钩子即参与空格/回车裁决的声明;管线按 roster 序轮询,首个非 undefined 应答胜出,无人认领落 default sink。matchSpace 同步空格在击键中触发只许热缓存matchEnter 异步(可 await 源自身预热,预热失败即 reject
  • controller 持有唯一权威 hit含 span菜单关闭后为 Space 保留、per-session menu store、候选 fetch generation、键盘仲裁combobox 模式:焦点始终在 textarea↑↓/Enter/Escape 拦截且全程过 IME composition 守卫,唯一例外 Shift+Enter 无条件先行、pick 编排outcome → 自派 bail 事件);dismiss() 动词支撑 MenuView 注入的 onDismiss(指针落在菜单与所在 composer 卡片之外即关闭菜单MenuView 还经 slash.menu locale 命名空间本地化组标题,并经 ui-primitives 的 useAnchoredMaxHeight 把高度收敛到 composer 上方的视口空间);每个 session scope 出生时对 source roster 做一次 warm(projection)projection 在该 scope 内只有稳定的 sessionId无 published/能力跃迁scope disposer 拆除 controller。
  • 触发检测词边界(user@host、URL / 永不触发、守卫分档plain/ 到处 + @ 行内 / claimed/ 抑制、@ 活 / frozen全无为冻结纯核。

hub / facade常驻外壳与严格 session 输入体

  • hubtrigger/decoration 注册表 + 发送编排)对 slash/command 服务是可选 ctx.get() 依赖:无 ui-slash/命令面时输入正常收发,优雅降级。
  • 每个实体 Session 只有一个 SessionInputShellfacade随 session scope 创建和拆除;无 session 时不造 input machine。ConversationRoot 自身是 session-maybe 常驻外壳,持有 HeroShell、Workspace picker、composer stack 与 chain fallback 外框。
  • 无 session 时外壳渲染纯展示的 DisabledInputBarconnectWorkspace 返回 blank session 后,仅输入体换成严格 session 的 InputBar。这里允许 textarea 重建,ConversationRoot、Hero 与布局骨架保持blank → engaging/active 仍是同一 session-bound InputBartextarea 不因 phase 翻转而重建。
  • ConversationRoot 的 Hero 判据是 sessionId === undefined || (composerPhase === 'blank' && (openState === 'open' || openState === 'loading'))。首次 submit 同步进入 engaging失败也保留 composer 与错误上下文,不退回 blank Herosidebar 的 blank 位只在 prompt 成功受理后翻 false。
  • 发送统一在 hub defaultSink乐观清稿后只走 session.prompt {mode:'queue'|'steer'};失败且 live draft 仍为空才回填,用户已经继续输入则不覆盖。不存在 Draft materialize 或 attach 事务。
  • blank Hero 改选 Workspace 时,外壳调用 connectWorkspace;目标 session 不同时把非空 draft 从当前 shell 搬到目标 shell再 open 新 id旧 blank session 留存但不再 current。
  • Notifier 双位契约:dirty(快照新鲜度,ensureFresh 拉取可清)与 notifyPending(通知欠账,只有 flush 清各自独立——拉取不得吞推送对象层推订阅者watchTransaction依赖这一保证。

纯文本引用(决策 21text outcome 与 lexicon 装饰

skill/@subagent 引用不走占位符 + occurrence 身份链——pick 直接把 /name @name 原文插进 draftchip 视觉纯派生:

  • PickOutcome 增 {text} arm新 scoped bail 事件 slash/input-insert-text {text, span}与另三个同契约draftRev CAS、返回 true ⟺ 实际改写facade.insertText 走 setDraft 拼接,机器零改动。
  • source 可选 lexicon?(session) 钩子:同步热快照名录,undefined = 数据未热——零装饰、永不触发 fetch渲染路径保持同步无副作用配对的可选 subscribeLexicon?(session, listener) 钩子是名录在 warm 之后仍会变化(目录 settle、子代生灭时的失效通道。controller 把各名录聚合进自己的 lexicon snapshot store每次 source 通知重拉scope 出生后才注册的 source 由 service 广播给活 controller补 warm 并并入名录。
  • decorations.scanTextRefs:词边界扫描 draft行首/空白后的 /name@namex/name 永不命中)对照名录,命中即 .textRef markbackdrop 纯 range 高亮,同 hlToken编辑破坏匹配形状下次扫描自然消失。
  • 发送即原文(不再 <skill> 序列化);气泡侧 MessageItem 双形状装饰legacy <skill> 标签 + 纯文本 token
  • 旧 occurrence/paste/serialize 链全部保留在盘未删additive删除另成将来一刀。装饰响应性InputBar 以 uSES 订阅 shell 的 lexicon sourcescope 出生预热后才 settle 的名录会直接点亮已有 draft token无需菜单交互或无关重渲染。

per-session 供数贡献与键盘私面

  • ui-conversationhub 兼贡献者)经 sessions.provide'input' hook机器状态 + queue overlay+ inputActions propsetDraft/submit,稳定 void 回调)。
  • 公私分界:公共 provide 只放 React 语汇成员;键盘/DOM 命令面track/arbitrate/space/undo/redo/paste/dismissPopup/bindMirror——同步返回值、disposer 语义)是 InputBar 独占,走 InputBar entry 自己的 inject 包内私递,不出插件边界。

坑位体系

conversation 本身是 session-maybe其会话内容与 composer 输入坑位严格 sessionHero Workspace picker 保持 root。子坑均由 ui-conversation 的 conversation 注册声明:

  • conversation.sessionsingle——严格 session 的 header、view ring 与 chat storesession id 切换时重建。
  • conversation.composer.barsingle——InputBar 本体的坑位InputBar 是真 slot entry自家坑自注册composer chain fallback 的内容;不做 chain entry——chain 单选举会在 takeover 时卸载它,破坏 textarea DOM 存活。
  • conversation.input.overlay——输入卡内浮层锚点;注册者 inject 按 slot sessionId 解析各自 per-session controller。
  • conversation.input.dock——输入上方堆叠条QueueDock 的队列只读列表落此order 定序。
  • conversation.composer.dock——composer 上沿统计带。
  • conversation.input.left / conversation.input.right——工具行左右区。
  • conversation.input.plan / conversation.input.modelsingle——工具行两具名控制位bar 只传 lockedowner props空到 owning 插件注册为止,无占位 fallback。
  • conversation.hero.workspaceroot scope——无 session / blank Hero 共用的 Workspace pickerpick 经 connectWorkspace 复用或创建目标 blank session必要时搬运 draft 后切 current。

测试纪律

状态机全部行为由纯 JS 单测覆盖(事件序列进、断言状态与效果,零浏览器 DOM交互矩阵逐行投影测试。这一要求正是纯核 + 服务壳分层的成因。

Alternatives considered

弃案 一行理由
ActiveCommand 中间态 / registerMode 模式注册表 / 从 draft 推导命令态 claim 由 pick 路径显式建立——无表、无推导
bindTarget/bindDraft 对象直连 反向耦合 + root 单例跨会话误配scoped bail 事件保依赖倒置且路由结构性正确
统一 slash/input-apply 或全事件化 三个独立 payload 覆盖跨插件改写;异步链路保持 registry 显式调用
contenteditable / 富文本树 兼容性差textarea + U+FFFC + occurrence 表覆盖全部交互契约
draft 双持久化 {text, occurrences} mirror 写剪贴板投影零新概念chip 跨刷新降级可接受
原生 textarea undo 栈 受控 + 程序化写入下不可靠;粘贴两段 undo 语义只能自管
InputBar 收 16 员 wiring 回调包 消费矩阵实证 11 员 InputBar 独占、1 员死成员;标准件通道让组件自取,键盘面包内私递
空格裁决也认领即执行型命令 误触发防线:空格后整行是普通 prompt不可逆副作用只留显式入口
通用 tokenPattern 装饰机制 结构化 occurrence 记录取代模式扫描
占位 select 常驻工具行 具名坑位空到注册为止;占位件与真实现冲突时是双真相源
引用一律走 U+FFFC chip决策 21 前旧线) 纯文本 + 派生装饰零身份状态原文即模型投影undo/剪贴板免特判chip 链保留给需要不可分原子性的场景

后果

  • 一个常驻 conversation 外壳承接 no-session/blank/active无 session → blank 只保证大框架 React identity允许 disabled textarea 替换为严格 InputBar同一 blank session → engaging/active 保持 InputBar 与 textarea。EmptyState 与受控 intent 链(sessions.updateIntent/updatePendingPrompt/workspaces.sendSession)随最后消费者一并删除。
  • 输入面对命令零知识 + 可选依赖:无命令包时纯输入可用;@ 引用与 skill 引用免费复用同一菜单/pick 管线。代价是空格/回车裁决是逐 source 轮询协议,其应答语义(同步/异步、undefined 含义)为冻结契约。
  • 提交事务化attempt seq + 漂移守卫使晚到结果回灌、会话切换、concurrent 重放三类缺陷结构性不可能,由矩阵测试钉住。
  • 已知欠账chip 跨刷新保真可复用粘贴匹配未立项subagent 引用的模型表示待业务立项。