feat: support plugin slash commands#1204
Conversation
🦋 Changeset detectedLatest commit: 9a90d81 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d0b3c62e66
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| return entries | ||
| .filter((entry) => entry.isFile() && entry.name.endsWith('.md')) | ||
| .map((entry) => path.join(dir, entry.name)) |
There was a problem hiding this comment.
Recurse into nested plugin command directories
When a plugin declares commands: "./commands", this only registers .md files that are direct children of that directory. Claude-style command trees support subdirectories for namespaced commands (for example commands/frontend/component.md), so those valid plugin commands are silently omitted from autocomplete and cannot be invoked. Please walk the directory recursively and preserve the relative path namespace when building command names.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 090d64efaf
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| export function buildPluginSlashCommands(defs: readonly PluginCommandDef[]): PluginSlashCommands { | ||
| const commandMap = new Map<string, string>(); | ||
| const commands = defs.map((def) => { | ||
| const commandName = pluginCommandName(def.pluginId, def.name); |
There was a problem hiding this comment.
Support nested plugin command names in dispatch
When a command comes from a nested file, plugin discovery preserves names like frontend/component, so this registers /my-plugin:frontend/component; however parseSlashInput returns null for any slash-command name containing /, causing the autocomplete suggestion to fall through as a normal user message instead of activating the plugin command. Either avoid registering slash-containing names here or relax/route the parser for plugin command namespaces.
Useful? React with 👍 / 👎.
| async activatePluginCommand({ | ||
| agentId, | ||
| ...payload | ||
| }: AgentScopedPayload<ActivatePluginCommandPayload>) { | ||
| await (await this.getAgent(agentId)).activatePluginCommand(payload); | ||
| } |
There was a problem hiding this comment.
Update prompt metadata for plugin command turns
For main-agent plugin commands, this starts a prompt but does not call updatePromptMetadata, unlike both normal prompts and skill activations. If the first or latest user action is a plugin command, the session state keeps an empty/stale lastPrompt and title, so session lists that exclude empty sessions and the picker’s last-prompt display can treat an active session as empty or outdated.
Useful? React with 👍 / 👎.
| case 'plugin_command': | ||
| return message.origin.trigger === 'user-slash'; |
There was a problem hiding this comment.
Replay plugin commands as command cards
This makes plugin_command origins count as replay user turns, but SessionReplayRenderer.renderUserMessage still only projects skill_activation origins before falling through to a plain user bubble. After resuming a session, plugin command turns therefore show the expanded command body rather than the intended /plugin:command card and args, making the transcript inconsistent and obscuring what the user actually invoked.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4171c4eab3
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| commandArgs: origin.commandArgs, | ||
| trigger: origin.trigger, | ||
| }); | ||
| this.turn.prompt([{ type: 'text', text: expanded }], origin); |
There was a problem hiding this comment.
When a plugin command is submitted, this records the turn with origin.kind === 'plugin_command', but the undo logic still only treats plain user prompts and user-slash skill activations as real user prompts. In the scenario where a user runs /my-plugin:deploy and then tries /undo, the active-context availability is 0 and core undo also will not count that command, so the command and response cannot be undone. Please include plugin-command origins in the undo anchor/user-prompt checks.
Useful? React with 👍 / 👎.
| backgroundTaskTranscriptedTerminal: Set<string> = new Set(); | ||
|
|
||
| renderedSkillActivationIds: Set<string> = new Set(); | ||
| renderedPluginCommandActivationIds: Set<string> = new Set(); |
There was a problem hiding this comment.
Clear plugin-command render IDs on session reset
This rendered-ID set is session runtime state, but resetRuntimeState() clears renderedSkillActivationIds without clearing renderedPluginCommandActivationIds. If a user invokes a plugin command, switches away, and then switches back/replays the same session, replay sees the previous activation ID and skips rendering that command card, leaving the transcript missing the invocation. Clear it alongside the skill activation set.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9df9b716c3
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| export interface PluginCommandOrigin { | ||
| readonly kind: 'plugin_command'; | ||
| readonly activationId: string; | ||
| readonly pluginId: string; | ||
| readonly commandName: string; | ||
| readonly commandArgs?: string | undefined; | ||
| readonly trigger: 'user-slash'; |
There was a problem hiding this comment.
Propagate plugin_command to turn projectors
Adding plugin_command as a new user-prompt origin also needs the consumers that mirror user-turn detection to be updated. In apps/kimi-web/src/composables/messagesToTurns.ts:382-388, only user and skill_activation are displayable, so a session containing a plugin slash command will hide that user turn in the web UI; apps/vis/server/src/lib/context-projector.ts:582-587 has the same stale mirror and will not count plugin commands as undo anchors in projections. This affects any session created from /plugin:command and then viewed through web/vis.
Useful? React with 👍 / 👎.
| const session = host.session; | ||
| if (session === undefined) { | ||
| host.showError(LLM_NOT_SET_MESSAGE); | ||
| return; |
There was a problem hiding this comment.
Require a model before activating plugin commands
When a session exists but no model is selected, this branch still calls activatePluginCommand; the normal prompt path and the skill branch both guard appState.model.trim().length === 0 first and show LLM_NOT_SET_MESSAGE. For the same no-model state, a plugin slash command now starts a session request and lets core fail later instead of being rejected immediately, which can leave the UI in the error/recovery path for an input that should never be sent.
Useful? React with 👍 / 👎.
| backgroundTaskTranscriptedTerminal: Set<string> = new Set(); | ||
|
|
||
| renderedSkillActivationIds: Set<string> = new Set(); | ||
| renderedPluginCommandActivationIds: Set<string> = new Set(); |
There was a problem hiding this comment.
Clear plugin command render IDs on session reset
This new per-session dedupe set is not cleared in resetRuntimeState() (unlike renderedSkillActivationIds), so after a plugin command is rendered live, switching away from that session and back keeps its activation id in memory. Replay then hits renderPluginCommand() and skips the card, making the /plugin:command invocation disappear from the transcript for that session until the TUI process restarts.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f7b1c16779
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| this.updateTerminalTitle(); | ||
| } | ||
| void this.refreshSkillCommands(this.session); | ||
| void this.refreshPluginCommands(this.session); |
There was a problem hiding this comment.
Refresh plugin commands after auth-created sessions
This new plugin-command cache is refreshed in the normal startup/switch paths, but I checked AuthFlowController and its login-created session path still only calls refreshSkillCommands(...) after createSession, and logout only clears skills. In the first session created immediately after an OAuth login, pluginCommandMap therefore stays empty, so /plugin:command falls through as a normal prompt until the user starts/reloads another session; after logout the autocomplete can also keep stale plugin commands. Please wire refreshPluginCommands(session)/refreshPluginCommands() through the auth flow wherever skills are refreshed.
Useful? React with 👍 / 👎.
| pluginCommand: isPluginCommand | ||
| ? { pluginId: origin.pluginId!, commandName: origin.commandName!, args: origin.commandArgs } |
There was a problem hiding this comment.
Render plugin commands in the web chat pane
When a resumed web transcript contains a plugin_command origin, this maps the user turn text to only commandArgs and records turn.pluginCommand, but I checked ChatPane.vue and it still only special-cases turn.skillActivation; plugin-command turns fall through to the plain user text renderer. As a result /plugin:deploy with no args shows an empty user bubble, and with args shows only the args rather than the command card/identity. Add web rendering/edit handling alongside the existing skill activation branch.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 574d0ae0c6
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| <div v-else-if="turn.pluginCommand" class="skill-act"> | ||
| <div class="skill-act-head"> | ||
| <span class="skill-act-arrow">▶</span> | ||
| <span>/{{ turn.pluginCommand.pluginId }}:{{ turn.pluginCommand.commandName }}</span> | ||
| </div> |
There was a problem hiding this comment.
Render plugin commands in the desktop chat view
This adds a plugin-command card only in the mobile childBubble branch, but the desktop user-message branch below still only special-cases turn.skillActivation and otherwise renders {{ turn.text }}. Since messagesToTurns now replaces plugin-command text with only commandArgs, desktop users invoking /plugin:command see just the args, or an empty user bubble when there are no args, instead of the command identity/card that mobile gets.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 17d9ca11fb
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| case 'plugin_command': { | ||
| const data = entry.pluginCommandData; | ||
| if (data === undefined) return null; | ||
| return new PluginCommandComponent(data.pluginId, data.commandName, data.args); |
There was a problem hiding this comment.
Treat plugin command cards as turn boundaries
When a session is driven by plugin slash commands, these cards are mounted as PluginCommandComponents, but trimTranscriptWindow()/mergeCurrentTurnSteps() only recognize UserMessageComponent as a turn boundary. Once old plugin-command turns exceed the transcript window, the trim path can remove their TranscriptEntrys without removing the mounted components, so the TUI keeps stale cards/steps around and the transcript window no longer bounds memory for those turns. Mark plugin command cards as turn-starting components or extend the boundary check to include them.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 79c6963243
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if ( | ||
| !(child instanceof UserMessageComponent) && | ||
| !(child instanceof SkillActivationComponent) && | ||
| !(child instanceof PluginCommandComponent) |
There was a problem hiding this comment.
Count plugin commands when trimming turns
When old turns are plugin slash commands, this new boundary type makes the rendered card a turn boundary, but trimTranscriptWindow() still increments boundariesToRemove only for entry.kind === 'user' && entry.turnId === undefined below. Once enough /plugin:... turns exceed the transcript window, toRemove contains plugin_command entries while boundariesToRemove stays too small (or zero), so the state entries are dropped without removing the corresponding rendered components, leaving stale transcript UI and defeating the window cap for those turns.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 76a26bf34a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| entry.kind === 'user' || | ||
| (entry.kind === 'skill_activation' && entry.skillTrigger === 'user-slash') | ||
| (entry.kind === 'skill_activation' && entry.skillTrigger === 'user-slash') || | ||
| entry.kind === 'plugin_command' |
There was a problem hiding this comment.
Preserve plugin command args when undoing
When a plugin command with arguments is made undoable here, the /undo selector later restores formatUndoChoiceInput(entry), which only special-cases skill activations and otherwise falls back to entry.content. Plugin command transcript entries store content as just /${pluginId}:${commandName} and keep the typed args in pluginCommandData.args, so selecting an undone /plugin:command prod refills the editor as /plugin:command and silently drops prod. Add plugin-command formatting alongside the skill-activation path for both the selector label and restored input.
Useful? React with 👍 / 👎.
|
Codex Review: Didn't find any major issues. Delightful! Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
Related Issue
No related issue. This is a follow-up to the plugin-hooks work: it lets plugins contribute slash commands, which is one of the pieces needed to run Claude-Code-style plugins (for example
vercel/vercel-plugin) in Kimi Code.Problem
Plugins can already provide skills, hooks, and MCP servers, but they cannot provide slash commands. A Claude-Code-style plugin that ships a
commands/directory (for example/vercel-plugin:deploy) has no way to register those commands in Kimi Code, so users cannot invoke them.What changed
commandsfield inkimi.plugin.json(a path or list of paths to.mdfiles or a directory of.mdfiles, scoped to the plugin root).name,description) plus a markdown body used as the prompt template.namefalls back to the file name;descriptionfalls back to the first body line.<plugin>:<command>and show up in the/autocomplete. Invoking one activates it server-side (mirroring skill activation): the agent receives the body with$ARGUMENTSexpanded (appending them when the body has no placeholder), while the TUI renders a compact▶ /plugin:commandcard rather than expanding the body into the chat.commandCount.This mirrors the existing plugin skills flow end to end (manifest → manager → session → RPC → SDK → TUI) and reuses the skill frontmatter parser.
Checklist
gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update.Plugin-author documentation for the new
commandsfield is not included yet and can follow in a separate docs PR.