Implemented through the core runtime, server, built-in Horton integration, and desktop/server UI composer. Remaining work is polish-level: broader browser/a11y coverage, richer slash-command argument UX, and future rich reference node types beyond slash commands.
Add a standardized parsed composer-input convention to Electric Agents and update the desktop app prompt input to produce that convention.
The core runtime contract is a well-known composer_input inbox message type. Its payload always preserves the original source text and may include a flat, ordered list of parsed composer nodes such as slash commands, files, symbols, and branches.
The desktop app prompt should become a ProseMirror-based composer, using ProseMirror directly rather than TipTap. It should provide a small editor schema, formatting for recognized composer tokens, and slash-command autocomplete backed by the entity's discovered slash-command collection.
- Standardize
composer_inputas a first-class inbox message type. - Preserve raw source text alongside parsed structure.
- Support multiple structured tokens in one input, including multiple slash commands.
- Treat slash commands as one first-class composer node kind.
- Add static slash-command declarations to entity definitions.
- Materialize static and dynamic slash commands into a built-in
db.collections.slashCommandscollection. - Expose handler-side slash-command helpers for dynamic registration.
- Put the current wake on handler context as
ctx.wake. - Update the desktop prompt input to use a direct ProseMirror implementation.
- Add slash-command autocomplete/popover in the desktop composer.
- Keep command execution and higher-level interpretation in handlers or helper libraries.
- Redefining
send. - Replacing the typed inbox message model.
- Introducing a centralized global command registry.
- Standardizing a full CLI-style argument grammar in v1.
- Fully specifying RPC request/response semantics.
- Using TipTap for the desktop composer.
- Making the ProseMirror document format part of the runtime wire contract.
- Defining nested composer parsing in v1.
UIs send parsed composer input as a normal typed inbox message:
export type SendRequest = {
from?: string
type: string
payload: unknown
mode?: 'immediate' | 'queued' | 'paused' | 'steer'
position?: string
}For parsed composer submissions:
{
"type": "composer_input",
"payload": {
"source": "/pr-review 123 in /worktree see @Branch",
"nodes": [
{
"kind": "slash_command",
"start": 0,
"end": 10,
"raw": "/pr-review",
"name": "pr-review"
},
{
"kind": "text",
"start": 10,
"end": 18,
"raw": " 123 in "
},
{
"kind": "slash_command",
"start": 18,
"end": 27,
"raw": "/worktree",
"name": "worktree"
},
{
"kind": "text",
"start": 27,
"end": 32,
"raw": " see "
},
{
"kind": "branch",
"start": 32,
"end": 39,
"raw": "@Branch",
"name": "Branch"
}
]
}
}Suggested payload types:
export type ComposerInputPayload = {
source: string
nodes?: ComposerNode[]
}
export type BaseComposerNode = {
kind: string
start: number
end: number
raw: string
}
export type TextNode = BaseComposerNode & {
kind: 'text'
}
export type SlashCommandNode = BaseComposerNode & {
kind: 'slash_command'
name: string
}
export type FileNode = BaseComposerNode & {
kind: 'file'
path: string
}
export type SymbolNode = BaseComposerNode & {
kind: 'symbol'
name: string
}
export type BranchNode = BaseComposerNode & {
kind: 'branch'
name: string
}
export type ComposerNode =
| TextNode
| SlashCommandNode
| FileNode
| SymbolNode
| BranchNodeThe runtime should validate structure and basic invariants, but not semantic meaning.
The wire format should tolerate unknown future node kinds for forward compatibility. Runtime validation may accept nodes with an unrecognized kind if they satisfy the base node invariants. Typed helper APIs should still expose a strongly discriminated union for known node kinds, with explicit fallback helpers for callers that need to inspect unknown nodes.
V1 validation should enforce:
sourceis a string.nodes, when present, is a flat ordered array.startandendare JavaScript string offsets intosource.start <= end.- node spans do not overlap.
raw === source.slice(start, end).- slash-command nodes have a valid command
name. - unknown node kinds satisfy the base node invariant and are preserved.
The runtime should not validate that a slash command exists, that a file path is accessible, or that a branch name is real. Handlers and helper libraries own those interpretations.
start and end are JavaScript string offsets. This keeps span behavior aligned with browser and ProseMirror text handling.
Any sender that includes nodes must ensure:
node.raw === payload.source.slice(node.start, node.end)Entities may declare slash commands in their definition:
export type SlashCommandDefinition = {
name: string
description?: string
arguments?: Array<{
name: string
type: 'string' | 'number' | 'boolean'
required?: boolean
description?: string
}>
}
export type EntityDefinition = {
slashCommands?: SlashCommandDefinition[]
}Slash-command names should be normalized without the leading slash. V1 should define allowed characters and case sensitivity before implementation. A conservative starting point is lowercase kebab-case names such as quickstart, pr-review, and worktree.
The runtime materializes the effective slash-command list into:
db.collections.slashCommandsUIs use this collection for autocomplete, command pickers, help text, and argument hints.
The collection should represent the effective command list for the current entity, including both static declarations and dynamic registrations.
The effective collection is a projection, not the only source of truth. Because dynamic commands can override static commands and later disappear, the runtime must preserve enough internal state to reconstruct the effective list from layers.
Required internal model:
- static command layer seeded from
EntityDefinition.slashCommands - dynamic command layer keyed by command
nameand dynamicowner - effective projection exposed through
db.collections.slashCommands - deterministic conflict resolution where dynamic commands override static commands with the same normalized name
- stable cleanup so removing a dynamic command reveals the static command with that name, if present
Handlers can register dynamic commands at runtime:
ctx.slashCommands.get(name)
ctx.slashCommands.register(command)
ctx.slashCommands.unregister(name)
ctx.slashCommands.list()Suggested behavior:
register(command)upserts by normalizedname.- Dynamic commands override static commands with the same name.
- Unregistering a dynamic command reveals the static declaration again, if one exists.
- The runtime needs enough provenance internally to distinguish static and dynamic layers.
- Dynamic command lifetime is source-owned and refreshable rather than permanently sticky.
Dynamic commands should carry provenance metadata that is at least available internally:
export type DynamicSlashCommandRegistration = SlashCommandDefinition & {
owner: string
version?: string
}The initial recommendation is:
- dynamic commands are persisted as registrations with
ownerprovenance - dynamic command sources, such as skill loaders, refresh their owned registrations when they run
- if a source changes, disappears, or reports a new command set, the runtime updates the dynamic layer and recomputes the effective projection
- helpers such as
skillsLoader(ctx)are idempotent and cheap, so they can be called at the top of every handler wake - loaders should be able to reconcile a whole owned set, not only register/unregister one command at a time
Suggested additional helper:
ctx.slashCommands.replaceOwned(owner, commands)replaceOwned(owner, commands) replaces all dynamic commands for that owner in one operation. This is the preferred API for sources that can report their full current command set, because it naturally removes stale commands when files, packages, feature flags, or environment capabilities change.
The runtime should watch or subscribe to command-source changes where the source supports it. For example, a skill-backed command loader should refresh registrations when skill metadata changes. If active watching is not available for a source, the source should still reconcile on each handler wake or on a periodic refresh so stale commands are eventually removed.
Move the current wake onto handler context as ctx.wake.
The goal is a discriminated union that lets helpers reliably detect composer-input inbox wakes:
type HandlerWake = InboxWake | SpawnWake | ScheduleWake | ObservationWake
type InboxWake = {
type: 'inbox'
message: {
type: string
payload: unknown
id?: string
from?: string
}
}Composer-input helpers can then inspect:
if (ctx.wake.type === 'inbox' && ctx.wake.message.type === 'composer_input') {
const payload = ctx.wake.message.payload as ComposerInputPayload
// inspect payload.source and payload.nodes
}Handler-side helper libraries should provide common operations such as:
getSlashCommandNodes(payload)hasSlashCommand(payload, name)firstSlashCommand(payload)textAfterNode(payload, node)nodesAfterNode(payload, node)sliceBetweenNodes(payload, startNode, endNode)knownNodes(payload)unknownNodes(payload)
These helpers prevent every handler from reinventing fragile string slicing logic.
The desktop app prompt input should be updated to a direct ProseMirror implementation.
This is an implementation detail of the desktop UI. The runtime wire format remains ComposerInputPayload, not the ProseMirror document.
- Use ProseMirror directly, not TipTap.
- Define a small composer schema.
- Support normal text editing.
- Support visual formatting for recognized composer tokens.
- Support slash-command autocomplete/popover while typing.
- Allow multiple slash commands in one input.
- Preserve a canonical source string on submit.
- Serialize the editor state to
ComposerInputPayload. - Keep autocomplete selection separate from execution; execution happens only on submit.
The first version should keep the schema small:
doc
block + paragraph
inline * inline
;text | hard_break | reference_atomThe desktop composer should support multiline input in v1. Serialization should preserve line breaks in source, using \n as the canonical line separator. ProseMirror paragraph boundaries and hard breaks both need deterministic conversion into that canonical source string.
Slash commands may be represented either as:
- plain text with decorations, which keeps typing and editing natural, or
- inline atom nodes/chips after explicit selection, which preserves metadata more strongly.
The implemented v1 uses slash-command inline atom nodes after explicit command selection, rendered as pill-like inline blocks. This makes accepted commands deleteable with one backspace while still preserving canonical visible source text on submit.
Unselected or pasted slash-command text can still be serialized by deriving flat composer nodes from the document text.
Build the parser/serializer before or alongside the full ProseMirror replacement. This gives the runtime and UI a shared set of real payload examples early.
The serializer should have one clear v1 policy:
- emit recognized structured nodes only
- do not emit
textnodes by default - preserve gaps as source text, accessible through helper functions such as
textAfterNode
TextNode remains part of the type model for clients that want to send a full covering token stream, but the desktop composer should not require text-node emission in v1.
The desktop composer should show a popover when the cursor is inside a slash-command token.
Trigger behavior:
- Open when the current token starts with
/. - Filter commands from
db.collections.slashCommands. - Match against command name and optionally description.
- Close on escape, blur, or when the cursor leaves the slash token.
- Support keyboard navigation.
- Insert or complete the selected command on enter/tab.
- Preserve normal text if no command is selected.
- Anchor the popover to the slash token range, not just the editor root.
- Support mouse selection without stealing focus from the editor unexpectedly.
- Use accessible listbox/menu semantics where practical.
- Keep active option state visible to screen readers where practical.
- Define behavior for IME composition so autocomplete does not corrupt partially composed text.
The popover should use the synced slash-command collection, so command updates appear without hardcoded UI configuration.
On submit, the composer serializes to:
type ComposerInputPayload = {
source: string
nodes?: ComposerNode[]
}Serialization rules:
sourceis the canonical visible text representation of the editor document.- Nodes are emitted in source order.
- Slash-command nodes include
kind,start,end,raw, and normalizedname. - The desktop composer emits recognized structured nodes by default, not text nodes.
- Text nodes may still be accepted from other clients that choose to send a full covering token stream.
- Rich reference atom nodes serialize to their matching node kinds.
- Every emitted node must satisfy
raw === source.slice(start, end). - Multiline content serializes with
\nline separators.
For pasted text, the composer should still recognize simple slash-command tokens where possible. It does not need to resolve rich references from pasted text in v1.
A handler-side loader can discover and register skill-backed slash commands:
export const handler = async (ctx: EntityHandlerContext) => {
const skills = createContextSkillLoader(skillsRegistry)
const loadedSkills = skills.load(ctx)
// continue normal handler flow
}The loader can:
- discover skill metadata
- register slash commands through
ctx.slashCommands - inspect
ctx.wake - detect relevant
composer_inputslash-command nodes - inject skill content or context
The loader should be idempotent and cheap on repeated wakes.
The implemented runtime helper is createContextSkillLoader. It reconciles
user-invocable skill slash commands with replaceOwned, proactively loads the
matching skill when the current composer_input wake contains a skill slash
command, returns skill tools (use_skill / remove_skill) for model-initiated
loading, and exposes a skills_catalog context source that handlers can merge
into their existing ctx.useContext call.
Skill context and manifest entries are flushed before the first agent run starts, so UI timelines and manifest drawers can show proactively loaded slash-command context during the response rather than only after the wake completes.
Likely runtime and server files:
packages/agents-server/src/electric-agents-types.tspackages/agents-runtime/src/types.tspackages/agents-runtime/src/entity-schema.tspackages/agents-runtime/src/define-entity.tspackages/agents-server/src/routing/entities-router.tspackages/agents-server/src/entity-manager.tspackages/agents-runtime/src/process-wake.tspackages/agents-runtime/src/context-factory.ts
Likely desktop and UI files:
packages/agents-desktoppackages/agents-server-ui/src/components/MessageInput.tsxpackages/agents-server-ui/src/lib/sendMessage.ts- a ProseMirror composer component or package shared by desktop and server UI if both need the same behavior
- slash-command picker/popover components
- composer serializer and parser helpers
Likely built-in agent files:
packages/agents/src/agents/horton.ts- skill loader and skill metadata types, if skills expose slash commands
- Add
ComposerInputPayloadandComposerNodetypes. - Add runtime validation for
composer_input. - Define offset and node invariant checks.
- Add tests for valid and invalid payloads.
- Add parser/serializer fixtures with real slash-command and multiline examples.
- Add
EntityDefinition.slashCommands. - Materialize static commands into
db.collections.slashCommands. - Add tests for entity definition registration and UI-readable command state.
- Move or mirror wake onto
ctx.wake. - Tighten wake typing with at least an inbox discriminant.
- Add composer-input helper functions.
- Add tests for inbox-to-wake mapping.
- Add
ctx.slashCommandshelper APIs. - Implement static/dynamic layering and override behavior.
- Implement dynamic command ownership and refresh semantics.
- Add
replaceOwned(owner, commands)for whole-source reconciliation. - Watch or refresh dynamic command sources and update stale registrations.
- Add tests for register, overwrite, unregister, and static reveal.
- Replace the desktop prompt input with a direct ProseMirror editor.
- Add composer schema, decorations, and submit serialization.
- Add slash-command popover sourced from
db.collections.slashCommands. - Add keyboard behavior and paste handling.
- Add multiline serialization and source-span tests.
- Add browser/a11y coverage for command popover behavior.
- Add focused UI tests where practical.
- Update built-in handlers to accept
composer_input. - Add skill-backed command registration if skill metadata supports it.
- Add examples such as
/quickstart,/pr-review, or/worktree. - Flush proactively loaded skill context before the first agent run starts.
- Add browser-level coverage for command popover focus, keyboard navigation, and screen-reader semantics.
- Add richer inline reference nodes for files, symbols, and branches once the resolver UX is designed.
- Decide whether
dynamic_layersshould remain visible onSlashCommandRowor be hidden behind an internal metadata shape in a later cleanup. - Extend slash-command argument metadata when the UI is ready to render structured argument hints beyond name/description.
- Dynamic slash commands are source-owned registrations with provenance. Sources refresh or replace their owned command set, and the runtime recomputes the effective collection.
- The runtime must support whole-owner reconciliation through an API such as
replaceOwned(owner, commands). - The runtime should watch source changes when possible and otherwise refresh on handler wakes or periodic checks.
- The effective slash-command collection is a projection from static and dynamic layers, not the only stored command state.
- The desktop composer supports multiline input in v1 and serializes canonical source text with
\nline separators. - The desktop composer emits recognized structured nodes by default. It does not emit text nodes unless a later use case requires a full covering token stream.
- Unknown node kinds are wire-compatible and preserved, but typed helpers should keep known node handling strongly discriminated.
- Parser/serializer fixtures should land early, before the ProseMirror replacement is complete.
Proceed with the composer_input convention on top of the existing typed inbox model and build the desktop prompt as the first full producer of that format.
Keep the runtime contract small and stable: raw source, flat parsed nodes, structural validation, typed wake access, and slash-command discovery. Keep ProseMirror details inside the UI layer and serialize to ComposerInputPayload at submit time.