Document two approaches for handling synthetic messages in prompt hooks: - Option 1: Separate PromptMessage type for lightweight prompt surgery - Option 2: PromptEditor API with append/prepend/insert mutators This enables plugin developers to inject instructions or context into prompts without manually fabricating message IDs and timestamps. The design supports resumable sessions while keeping the API simple for common prompt manipulation use cases.
2.8 KiB
2.0
What we would change if we could
Keybindings vs. Keymappings
Make it keymappings, closer to neovim. Can be layered like <leader>abc. Commands don't define their binding, but have an id that a key can be mapped to like
{ key: "ctrl+w", cmd: string | function, description }
Why
Currently its keybindings that have an id like message_redo and then a command can use that or define it's own binding. While some keybindings are just used with .match in arbitrary key handlers and there is no info what the key is used for, except the binding id maybe. It also is unknown in which context/scope what binding is active, so a plugin like which-key is nearly impossible to get right.
Message Shape
Problem:
- stored messages need enough data to replay and resume a session later
- prompt hooks often just want to append a synthetic user/assistant message
- today that means faking ids, timestamps, and request metadata
Option 1: Two Message Shapes
Keep User / Assistant for stored history, but clean them up.
type User = {
role: "user"
time: { created: number }
request: {
agent: string
model: ModelRef
variant?: string
format?: OutputFormat
system?: string
tools?: Record<string, boolean>
}
}
type Assistant = {
role: "assistant"
run: { agent: string; model: ModelRef; path: { cwd: string; root: string } }
usage: { cost: number; tokens: Tokens }
result: { finish?: string; error?: Error; structured?: unknown; kind: "reply" | "summary" }
}
Add a separate transient PromptMessage for prompt surgery.
type PromptMessage = {
role: "user" | "assistant"
parts: PromptPart[]
}
Plugin hook example:
prompt.push({
role: "user",
parts: [{ type: "text", text: "Summarize the tool output above and continue." }],
})
Tradeoff: prompt hooks get easy lightweight messages, but there are now two message shapes.
Option 2: Prompt Mutators
Keep User / Assistant as the stored history model.
Prompt hooks do not build messages directly. The runtime gives them prompt mutators.
type PromptEditor = {
append(input: { role: "user" | "assistant"; parts: PromptPart[] }): void
prepend(input: { role: "user" | "assistant"; parts: PromptPart[] }): void
appendTo(target: "last-user" | "last-assistant", parts: PromptPart[]): void
insertAfter(messageID: string, input: { role: "user" | "assistant"; parts: PromptPart[] }): void
insertBefore(messageID: string, input: { role: "user" | "assistant"; parts: PromptPart[] }): void
}
Plugin hook examples:
prompt.append({
role: "user",
parts: [{ type: "text", text: "Summarize the tool output above and continue." }],
})
prompt.appendTo("last-user", [{ type: "text", text: BUILD_SWITCH }])
Tradeoff: avoids a second full message type and avoids fake ids/timestamps, but moves more magic into the hook API.