Web command business surfaces and assembly (ui-commands / ui-skill / ui-subagent)
Web 命令业务面与装配(ui-commands / ui-skill / ui-subagent)
The pipeline was ready but command knowledge had no landing spot: host-side `ctx.commands` and `ctx.skills` were complete while the web channel had no command capability. The business layer had to answer: - Command UI takes more than one shape (execute on the spot, pop a select box, backfill and keep typing arguments) — how do business packages ship with zero skeleton changes; - When is the directory fetched: pulling
English
Problem
The pipeline was ready but command knowledge had no landing spot: host-side ctx.commands and ctx.skills were complete while the web channel had no command capability. The business layer had to answer:
- Command UI takes more than one shape (execute on the spot, pop a select box, backfill and keep typing arguments) — how do business packages ship with zero skeleton changes;
- When is the directory fetched: pulling on every menu open is too slow, while a resident cache needs invalidation and reconnect stories;
- Sessions are always agent-backed (Session + Agent born in the same instant) — by what address does the client command surface honor the host's per-agent effective directory;
- Assembly-level acceptance: with the layers split apart, how the user-visible main chain is pinned once they come together.
Decision
ui-commands: a CommandUiRuntime + a session-keyed CommandDirectory + a per-session PopupSelectController
- The
ClientSessionContext { sessionId }projection is self-held in the ui-input-trigger contract (types.ts): sessions are always agent-backed, so session identity is the entire projection of command capability; the wire addresses by{sessionId}(bothcommand.listandcommand.execute; the host resolves the Agent from the session header). - The directory is compartmented by
SessionId, with per-key single-flight + an epoch guard (an old pull never overwrites newer state);commands/changedsoft-invalidates every key (the old snapshot keeps serving while the repull runs in the background),connection/resethard-invalidates every key and rewarms, Enter strong-waits on the current key, and a failure keeps the draft with no downgrade. Prewarming hangs on the source'swarmhook — once over the full roster at scope birth, which covers the entire session lifecycle (session capability is constant from birth). register(contribution)registers client commands (a descriptor +available(projection)+ a popupSelect spec); candidate synthesis = the host directory + contribution availability filtering, then the query/position pass, and a host/contribution name clash fails loud.- The three command kinds derive from the registration surfaces; developers never declare positions: a host descriptor with
input= leadingInput (backfill/name ␣+ claim, keep typing arguments, leading position only); a client-registered popupSelect spec = popupSelect (the official select-box shell, business ships zero components); neither = execute (run on selection, zero UI). - The dispatch decision table: the menu can trigger all three kinds; Space recognizes only leadingInput (the misfire defense: irreversible side effects keep explicit entry points only); Enter runs execute / opens the shell only on a bare token, while leadingInput tolerates trailing arguments.
- The popup from
popupFor(actx): search filters locally, select is single-flight, the projection is captured at open, onSelect consumes the token through the consume-token event only on success, a failure is retained for retry, and a session switch merely hides it. The popup shell is a transient layer (never in the state machine): the box holds focus, Enter/↑↓/Escape belong to it, and clicking outside the box dismisses (clicking the textarea also returns focus).
Reference sources (seeing only projections plus their own apply closures, on the root ctx)
- ui-skill:
skill.list({sessionId})addresses by session (the host resolves the project root from the session header); the directory cache is single-flight keyed by sessionId, prewarmed at birth by thewarmhook and fully cleared byconnection/reset. A pick produces a text outcome (the literal/nametext, the plain-text-reference decision);lexiconsupplies the roster from CatalogFetch's settled snapshot (undefinedwhile not warm), andsubscribeLexiconnotifies per-session listeners on settle and on invalidation. No match hook (references never enter command adjudication). Skill references ride ordinary prompts as literal text (outside the command plane; tool-skill unchanged, with the session-prefix directory providing the cooperative association). - ui-subagent: candidates are zero-RPC (the sessions.list snapshot filtered by parentId/running); a pick produces a text outcome (the literal
@nametext);lexiconderives from the same snapshot andsubscribeLexiconforwards the list store's change feed (the model-side representation awaits its business workstream).
Fixture command routing and assembly
- The connection fixture adds command routing (fixture + fake-api): the keyless rig can run the complete command flow (directory, execution, popup selection).
- The apps/cli assembly mounts all the new packages; the tsconfig path map / reference sets are filled in; catalogs/docs are regenerated with the wire and events.
Assembly-level acceptance: the slash-flow snapshot
apps/web/tests/slash-flow.snapshot.ts pins the user-visible main chain (assembled keyless; package mocks are no substitute for the assembled transcript): the composer disabled with no session → creating a Workspace and entering an already-materialized blank session → picking the /echo leadingInput from the / menu → the command executes but the blank bit does not flip and the list still shows New Session → the first ordinary prompt's successful acceptance converts that same row; the same session-bound textarea holds across blank → active. workspace-flow.snapshot.ts separately pins blank-row creation/reuse, first-prompt rejection backfill, and — on a Workspace switch before the first prompt — the draft moving across input machines with the old blank row hidden.
Alternatives considered
| Rejected | One-line reason |
|---|---|
| Inline prompt dispatch (command text riding the message into the host for parsing) | Conflates the command and message planes; command execution being independent of the message queue is existing host semantics |
| A bridge materializing skills as commands | Skills have their own directory; N registrations would be a detour; the tag form naturally avoids the command plane |
A skill.invoke RPC | The host has no such operation; skill references are plain text riding prompts |
| A new ContentBlock reference type | Full-chain cost (adapters/UI/compaction); text-as-truth plus structured occurrence records suffices |
| Client packages self-reporting command directories | The host is the single source of truth; the client only reads descriptors, with commands-changed pushing invalidation |
The requires: 'none' | 'agent' discriminant axis (an agentless directory + dual-addressed queries) | With sessions always agent-backed, the amphibious command has no owner; the whole axis is dropped, to be reopened on real demand |
| Dedicated commandresult / commandpanel slots | Results go through notices; the popup shell is a skeleton-internal overlay; rich result cards sit in the ledger |
An agent-type directory as the @ source | No type registry exists; the live-session snapshot already covers it |
| A PickAction/EnterCommand class family (class-inheritance pick products) | Cross-package runtime values break client bundle purity; pure data interfaces plus closure methods are equivalent |
Consequences
- Shipping a business command = a host registration plus one client
command.register(popupSelect) or zero registration (execute/leadingInput derive automatically), with zero skeleton changes; the cost is that the three-kind semantics concentrate in ui-commands, and a hypothetical fourth kind means changing it. - The resident directory cache plus push invalidation buys zero-latency menus and reliable enter adjudication; the cost is three invalidation paths (the change frame, reconnect, the epoch guard) that all need tests pinning them.
- sessionId addressing puts the host's per-agent effective directory (global + scoped shadows) straight on the wire, with the client presenting it as-is.
- Known gaps: the popupSelect shell has no shipped business consumer yet (model selection and its kin arrive with the host
selectModelwork in live-mutation shape, serving as the onboarding template then); the queue's second cut (per-item Inbox operations), rich result cards, and roster configurability sit in the ledger awaiting their triggers.
中文
问题
流水线就绪但没有命令知识的落点:host 侧 ctx.commands 与 ctx.skills 完整而 web 通道无命令能力。业务层要回答:
- 命令 UI 不止一种形态(当场执行、弹选择框、回填后继续打参数)——业务包如何零骨架改动上架;
- 目录何时拉取:每次开菜单现拉太慢,常驻缓存就要有失效与重连故事;
- 会话始终由 agent(智能体)支撑(Session+Agent 同瞬出生),client 命令面通过什么地址访问 host 的逐 agent 有效目录;
- 装配级验收:拆开的各层合起来,用户可见主链如何钉住。
决策
ui-commands:CommandUiRuntime + 按会话键控的 CommandDirectory + 逐会话 PopupSelectController
- 投影
ClientSessionContext { sessionId }自持于 ui-input-trigger 约定(types.ts):会话恒 agent-backed,会话身份即命令能力的全部投影;wire 以{sessionId}寻址(command.list/command.execute均是;host 从会话 header 解析 Agent)。 - 目录按
SessionId分区,per-key single-flight + epoch guard(旧拉取永不覆盖新态),commands/changed全 key 软失效(旧快照继续服务、后台重拉)、connection/reset全 key 硬失效并预热,Enter 必须等待当前 key 就绪、失败留草稿不降级。预热挂 source 的warm钩子——scope 出生时对全 roster 一次,即覆盖整个会话生命周期(会话能力自出生恒定)。 register(contribution)注册 client 命令(descriptor +available(projection)+ popupSelect spec);候选合成 = host 目录 + contribution 可用性过滤,再过 query/position,host/contribution 重名 fail loud。- 命令三型按注册面派生,开发者不声明位置:host descriptor 带
input= leadingInput(回填/name ␣+ claim,继续打参数,仅限行首);client 注册 popupSelect spec = popupSelect(官方选择框壳,业务零组件);两者皆无 = execute(选中即执行,零 UI)。 - 派发决策表:菜单可触发三型;Space 只认 leadingInput(误触发防线:不可逆副作用只留显式入口);Enter 裸 token 才 execute/开壳、leadingInput 容忍尾随参数。
popupFor(actx)的 popup:search 本地过滤、select single-flight、open 时捕获投影、onSelect 成功才经 consume-token 事件消 token、失败保留可重试、会话切换只隐藏。popup 壳是瞬态层(不进状态机):框持焦点、Enter/↑↓/Escape 归它、点框外即 dismiss(点 textarea 同时归还焦点)。
引用源(只见投影 + 自家 apply 闭包的 root ctx)
- ui-skill:
skill.list({sessionId})按会话寻址(host 从会话 header 解析项目根);目录缓存按 sessionId 键控 single-flight,warm钩子出生预热、connection/reset全清。pick 产出 text outcome(/name原文,纯文本引用决策);lexicon从 CatalogFetch 的 settled 快照给名录(未热undefined),subscribeLexicon在 settle 与失效时按会话通知监听者。无 match 钩子(引用不进命令裁决)。skill 引用以原文随普通提示词走(命令平面之外;tool-skill 不变,会话前缀目录提供协作关联)。 - ui-subagent:候选零 RPC(sessions.list 快照按 parentId/running 过滤);pick 产出 text outcome(
@name原文);lexicon同快照派生,subscribeLexicon转发 list store 的变更通道(模型侧表示待业务立项)。
fixture 命令路由与装配
- connection fixture 补命令路由(fixture + fake-api):keyless 台架可跑完整命令流(目录、执行、popup 选择)。
- apps/cli 装配挂全部新包;tsconfig path map / reference 集补齐;catalog/docs 随 wire 与事件再生成。
装配级验收:slash-flow 快照
apps/web/tests/slash-flow.snapshot.ts 钉住用户可见主链(assembled keyless,包 mock 不替代装配后的 transcript(文本记录)):无会话时 composer 禁用 → 创建 Workspace 并进入已实体化的 blank 会话 → / 菜单选 /echo leadingInput → 命令执行但 blank 位不翻转、列表仍显示 New Session → 首条普通提示词成功受理后同一行转正;同一个会话绑定的 textarea 在 blank → active 转换期间保持不变。workspace-flow.snapshot.ts 另钉住 blank 行创建/复用、首条提示词遭拒后的回填,以及在发出首条提示词前切换 Workspace 时 draft 跨 input machine 搬运且旧 blank 行隐藏。
曾考虑的替代方案
| 弃案 | 一行理由 |
|---|---|
| 提示词内联派发(命令文本随消息进 host 解析) | 混淆命令/消息平面;命令执行独立于消息队列是既有 host 语义 |
| skill 物化为 command 的桥 | skill 自有目录;N 笔注册是绕路;标签形式天然避开命令平面 |
skill.invoke RPC | host 无此操作;skill 引用是随提示词的普通文本 |
| 新 ContentBlock 引用类型 | 全链路成本(适配器/UI/压缩(compaction));文本即真身 + 结构化 occurrence 记录已足够 |
| client 各包自报命令目录 | host 是唯一真源;client 只读 descriptor,commands-changed 推失效 |
requires: 'none' | 'agent' 判别轴(agentless 目录 + 双址查询) | 会话恒 agent-backed 后两栖命令无 owner;整轴弃置,待真需求重开 |
| 专用 commandresult / commandpanel slot | 结果走 notice;popup 壳是骨架内浮层;富结果卡入台账 |
agent-type 目录做 @ 源 | 无类型注册表;实时会话快照已覆盖 |
| PickAction/EnterCommand 类族(类继承 pick 产物) | 跨包运行时值破坏 client bundle 纯度;纯数据接口 + 闭包方法等价 |
后果
- 业务命令上架 = host 注册 + client 一笔
command.register(popupSelect)或零注册(execute/leadingInput 自动派生),零骨架改动;代价是三型语义集中在 ui-commands,假想的第四型意味着改它。 - 常驻目录缓存 + 推失效换来菜单零延迟与回车裁决可靠;代价是三条失效路径(change 帧、重连、epoch guard)都需测试钉住。
- sessionId 寻址让 host 的 per-agent 有效目录(全局 + scoped shadows)直接上 wire,client 原样呈现。
- 已知欠账:popupSelect 壳暂无已上架业务消费方(模型选择等将随 host
selectModel工作以 live-mutation 形态到来,届时作接入样板);队列第二刀(逐项 Inbox 操作)、富结果卡、roster 可配置性入台账待触发。