Add slash command management UI#4180
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| boundaryBase: 24, | ||
| includesBase: 26, | ||
| }), | ||
| ...(item.type === "slash-command" |
There was a problem hiding this comment.
🟡 Medium chat/composerSlashCommandSearch.ts:32
Built-in slash commands no longer match description-only queries. A search like normal build mode for /default previously returned the command via description scoring, but now returns no result because scoreSlashCommandItem skips the description score entirely when item.type === "slash-command". Consider keeping the description score for built-in commands so description text remains searchable.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/composerSlashCommandSearch.ts around line 32:
Built-in slash commands no longer match description-only queries. A search like `normal build mode` for `/default` previously returned the command via description scoring, but now returns no result because `scoreSlashCommandItem` skips the description score entirely when `item.type === "slash-command"`. Consider keeping the description score for built-in commands so description text remains searchable.
| sources: ReadonlyArray<{ command: { name: string } }>, | ||
| isVisible: boolean, | ||
| ) => { | ||
| const nextHidden = isVisible ? [] : sources.map((source) => source.command.name); |
There was a problem hiding this comment.
🟡 Medium settings/ProviderSlashCommandsSection.tsx:104
setAllCommandsVisibility overwrites the provider's entire hidden-command list with only the currently displayed sources, so any hidden commands that don't match the active search filter are silently made visible. When a search narrows the group, clicking "Exclude all" only hides the visible matches and un-hides everything else, and "Include all" deletes the hidden state for nonmatching commands. The bulk update should merge or remove only the displayed command names against the existing list instead of replacing the whole entry.
| const nextHidden = isVisible ? [] : sources.map((source) => source.command.name); | |
| const currentHidden: ReadonlyArray<string> = hiddenSlashCommandsByProvider[providerId] ?? []; | |
| const displayedNames = new Set(sources.map((source) => source.command.name.trim().toLowerCase())); | |
| const nextHidden = isVisible | |
| ? currentHidden.filter((value) => !displayedNames.has(value.trim().toLowerCase())) | |
| : [ | |
| ...currentHidden.filter((value) => !displayedNames.has(value.trim().toLowerCase())), | |
| ...sources.map((source) => source.command.name), | |
| ]; |
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/settings/ProviderSlashCommandsSection.tsx around line 104:
`setAllCommandsVisibility` overwrites the provider's entire hidden-command list with only the currently displayed `sources`, so any hidden commands that don't match the active search filter are silently made visible. When a search narrows the group, clicking "Exclude all" only hides the visible matches and un-hides everything else, and "Include all" deletes the hidden state for nonmatching commands. The bulk update should merge or remove only the displayed command names against the existing list instead of replacing the whole entry.
| options?: { | ||
| readonly hiddenSlashCommandsByProvider?: Readonly<Record<string, ReadonlyArray<string>>>; | ||
| }, | ||
| ): ComposerSlashCommandSource[] { |
There was a problem hiding this comment.
🟡 Medium lib/composerSlashCommands.ts:154
collectComposerSlashCommandSources drops commands that match hiddenSlashCommandsByProvider, so collectComposerSlashCommands omits those names entirely. The UI copy says hiding a command affects only "chat slash autocomplete," but hiding a provider command also stops collectComposerSlashCommandTokens from recognizing it, so its /name chip stops rendering in existing and new messages. Consider excluding hidden commands only in the autocomplete-filtering path instead of dropping them from the collection.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/lib/composerSlashCommands.ts around line 154:
`collectComposerSlashCommandSources` drops commands that match `hiddenSlashCommandsByProvider`, so `collectComposerSlashCommands` omits those names entirely. The UI copy says hiding a command affects only "chat slash autocomplete," but hiding a provider command also stops `collectComposerSlashCommandTokens` from recognizing it, so its `/name` chip stops rendering in existing and new messages. Consider excluding hidden commands only in the autocomplete-filtering path instead of dropping them from the collection.
| onTaskListChange?: ((input: { markerOffset: number; checked: boolean }) => void) | undefined; | ||
| isStreaming?: boolean; | ||
| skills?: ReadonlyArray<Pick<ServerProviderSkill, "name" | "displayName">>; | ||
| slashCommands?: ReadonlyArray<ComposerSlashCommandLike>; |
There was a problem hiding this comment.
🟡 Medium components/ChatMarkdown.tsx:117
The markdownComponents memo omits slashCommands from its dependency array, so when the slashCommands prop changes while ChatMarkdown is mounted — e.g. after creating or editing a custom command — the memoized p/li renderers keep using the stale list and command highlighting never updates until an unrelated dependency changes. Add slashCommands to the memo's dependency array.
Also found in 1 other location(s)
apps/web/src/components/ComposerPromptEditor.tsx:681
slashCommandSignatureomitsdisplayName,provider, andsourceKind, even though all three are copied intoComposerSlashCommandNodemetadata. If one of those fields changes whilename,description, andinput.hintremain unchanged (for example, editing only the capitalization of a custom command title so its slug stays the same),slashCommandsChangedremains false and the controlled-update effect does not rewrite the editor state. Existing slash-command tokens therefore continue displaying stale labels/source metadata until some unrelated value changes.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/ChatMarkdown.tsx around line 117:
The `markdownComponents` memo omits `slashCommands` from its dependency array, so when the `slashCommands` prop changes while `ChatMarkdown` is mounted — e.g. after creating or editing a custom command — the memoized `p`/`li` renderers keep using the stale list and command highlighting never updates until an unrelated dependency changes. Add `slashCommands` to the memo's dependency array.
Also found in 1 other location(s):
- apps/web/src/components/ComposerPromptEditor.tsx:681 -- `slashCommandSignature` omits `displayName`, `provider`, and `sourceKind`, even though all three are copied into `ComposerSlashCommandNode` metadata. If one of those fields changes while `name`, `description`, and `input.hint` remain unchanged (for example, editing only the capitalization of a custom command title so its slug stays the same), `slashCommandsChanged` remains false and the controlled-update effect does not rewrite the editor state. Existing slash-command tokens therefore continue displaying stale labels/source metadata until some unrelated value changes.
| const titleError = submitted && normalizedTitle.length === 0 ? "A title is required." : null; | ||
| const promptError = submitted && normalizedPrompt.length === 0 ? "A prompt is required." : null; |
There was a problem hiding this comment.
🟡 Medium settings/CustomSlashCommandsSection.tsx:87
When the user enters a title that consists entirely of characters that slugify to nothing (e.g. 你好 or !!!), clicking "Add command" silently does nothing — no error message is shown and the dialog stays open. handleSave rejects the command because commandName.length === 0, but the UI only shows titleError when normalizedTitle.length === 0, so the user has no indication of why the save failed. Consider rendering a validation message when commandName is empty despite a non-empty title, so the user understands the title needs to contain supported characters.
| const titleError = submitted && normalizedTitle.length === 0 ? "A title is required." : null; | |
| const promptError = submitted && normalizedPrompt.length === 0 ? "A prompt is required." : null; | |
| const titleError = submitted && normalizedTitle.length === 0 ? "A title is required." : null; | |
| const titleSlugError = submitted && normalizedTitle.length > 0 && commandName.length === 0 ? "Title must contain at least one letter, number, or hyphen." : null; |
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/settings/CustomSlashCommandsSection.tsx around lines 87-88:
When the user enters a title that consists entirely of characters that slugify to nothing (e.g. `你好` or `!!!`), clicking "Add command" silently does nothing — no error message is shown and the dialog stays open. `handleSave` rejects the command because `commandName.length === 0`, but the UI only shows `titleError` when `normalizedTitle.length === 0`, so the user has no indication of why the save failed. Consider rendering a validation message when `commandName` is empty despite a non-empty title, so the user understands the title needs to contain supported characters.
| readonly prompt: string; | ||
| }; | ||
|
|
||
| function slugifySlashCommandName(value: string): string { |
There was a problem hiding this comment.
🟡 Medium lib/composerSlashCommands.ts:89
slugifySlashCommandName produces names that don't start with an ASCII letter — e.g. "123 Fix" becomes "123-fix" and "_test" becomes "_test" — but SLASH_COMMAND_TOKEN_REGEX requires names to start with [a-zA-Z]. As a result, custom commands with these slugs appear in autocomplete and pass validation, yet typing them (e.g. /123-fix) never matches the token regex, so the command silently fails to render as a slash-command segment. The fix is to strip leading characters that aren't ASCII letters after collapsing non-allowed characters.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/lib/composerSlashCommands.ts around line 89:
`slugifySlashCommandName` produces names that don't start with an ASCII letter — e.g. `"123 Fix"` becomes `"123-fix"` and `"_test"` becomes `"_test"` — but `SLASH_COMMAND_TOKEN_REGEX` requires names to start with `[a-zA-Z]`. As a result, custom commands with these slugs appear in autocomplete and pass validation, yet typing them (e.g. `/123-fix`) never matches the token regex, so the command silently fails to render as a slash-command segment. The fix is to strip leading characters that aren't ASCII letters after collapsing non-allowed characters.
| const visibleGroups = useMemo(() => { | ||
| const normalizedQuery = query.trim().toLowerCase(); | ||
| const sources = collectComposerSlashCommandSources(serverProviders); | ||
| const sourcesByProvider = new Map<ProviderInstanceId, typeof sources>(); |
There was a problem hiding this comment.
🟡 Medium settings/ProviderSlashCommandsSection.tsx:38
Shared .agents commands are rendered twice on the slash commands settings page — once by AgentsSlashCommandsSection (embedded above) and again inside the per-provider groups below. The duplicates use different visibility settings: the global copy respects hiddenGlobalSlashCommands, while the provider copy respects hiddenProviderSlashCommands, so toggling one does not affect the other. This happens because visibleGroups calls collectComposerSlashCommandSources(serverProviders) without filtering, so commands whose sourceKind is "agents" are included in the provider groups even though they are already shown globally. Filter out sourceKind === "agents" sources when building the provider groups.
const visibleGroups = useMemo(() => {
const normalizedQuery = query.trim().toLowerCase();
- const sources = collectComposerSlashCommandSources(serverProviders);
+ const sources = collectComposerSlashCommandSources(serverProviders).filter(
+ (source) => source.command.sourceKind !== "agents",
+ );
const sourcesByProvider = new Map<ProviderInstanceId, typeof sources>();🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/settings/ProviderSlashCommandsSection.tsx around lines 38-41:
Shared `.agents` commands are rendered twice on the slash commands settings page — once by `AgentsSlashCommandsSection` (embedded above) and again inside the per-provider groups below. The duplicates use different visibility settings: the global copy respects `hiddenGlobalSlashCommands`, while the provider copy respects `hiddenProviderSlashCommands`, so toggling one does not affect the other. This happens because `visibleGroups` calls `collectComposerSlashCommandSources(serverProviders)` without filtering, so commands whose `sourceKind` is `"agents"` are included in the provider groups even though they are already shown globally. Filter out `sourceKind === "agents"` sources when building the provider groups.
| updateSettings, | ||
| ]); | ||
|
|
||
| const handleDelete = useCallback( |
There was a problem hiding this comment.
🟡 Medium settings/CustomSlashCommandsSection.tsx:120
handleDelete removes the command from customSlashCommands but leaves its normalized name in hiddenCustomSlashCommands. If the user hides a custom command, deletes it, and later creates a new command with the same title, the new command inherits the stale hidden entry and is unexpectedly absent from slash-command autocomplete until manually enabled. When deleting a command, also remove its normalized name from hiddenCustomSlashCommands.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/settings/CustomSlashCommandsSection.tsx around line 120:
`handleDelete` removes the command from `customSlashCommands` but leaves its normalized name in `hiddenCustomSlashCommands`. If the user hides a custom command, deletes it, and later creates a new command with the same title, the new command inherits the stale hidden entry and is unexpectedly absent from slash-command autocomplete until manually enabled. When deleting a command, also remove its normalized name from `hiddenCustomSlashCommands`.
| continue; | ||
| } | ||
| if (segment.type === "slash-command") { | ||
| const metadata = slashCommandMetadata.get(segment.name); |
There was a problem hiding this comment.
🟡 Medium components/ComposerPromptEditor.tsx:1062
slashCommandMetadata.get(segment.name) uses an exact, untrimmed key lookup, so slash-command chips lose their configured label, description, provider icon, and source-kind when the configured name has different casing or surrounding whitespace. For example, a configured name of review matches typed /Review during recognition (which normalizes with trim().toLowerCase()), but the metadata lookup at render time misses and falls back to raw values. The metadata map should be built and queried with the same normalized key so lookups always hit.
Also found in 1 other location(s)
apps/web/src/components/chat/SlashCommandInlineText.tsx:28
SlashCommandInlineTextbuildscommandsByNameand looks up the matched spelling without normalization, while the sharedcollectComposerSlashCommandTokenslogic trims and lowercases both command definitions and input. Thus a valid case-insensitive token such as/PLANfor commandplanis recognized by composer parsing but is rendered as plain text in the timeline instead of the slash-command chip.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/ComposerPromptEditor.tsx around line 1062:
`slashCommandMetadata.get(segment.name)` uses an exact, untrimmed key lookup, so slash-command chips lose their configured label, description, provider icon, and source-kind when the configured name has different casing or surrounding whitespace. For example, a configured name of `review` matches typed `/Review` during recognition (which normalizes with `trim().toLowerCase()`), but the metadata lookup at render time misses and falls back to raw values. The metadata map should be built and queried with the same normalized key so lookups always hit.
Also found in 1 other location(s):
- apps/web/src/components/chat/SlashCommandInlineText.tsx:28 -- `SlashCommandInlineText` builds `commandsByName` and looks up the matched spelling without normalization, while the shared `collectComposerSlashCommandTokens` logic trims and lowercases both command definitions and input. Thus a valid case-insensitive token such as `/PLAN` for command `plan` is recognized by composer parsing but is rendered as plain text in the timeline instead of the slash-command chip.
| return <p {...props}>{renderSkillInlineMarkdownChildren(children, skills)}</p>; | ||
| return ( | ||
| <p {...props}> | ||
| {renderSlashCommandInlineMarkdownChildren( |
There was a problem hiding this comment.
🟠 High components/ChatMarkdown.tsx:1358
Slash-command tokens in paragraph and list text are never converted into chips. renderSlashCommandInlineMarkdownChildren is called on the output of renderSkillInlineMarkdownChildren, which already replaced every string child with a SkillInlineText element that has no children prop. Since renderSlashCommandInlineMarkdownChildren only transforms string nodes, it finds no strings left and skips everything, so slash-command rendering in p and li is a no-op. Apply both transforms to the original string children (e.g. via a combined renderer) instead of chaining them.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/ChatMarkdown.tsx around line 1358:
Slash-command tokens in paragraph and list text are never converted into chips. `renderSlashCommandInlineMarkdownChildren` is called on the output of `renderSkillInlineMarkdownChildren`, which already replaced every string child with a `SkillInlineText` element that has no `children` prop. Since `renderSlashCommandInlineMarkdownChildren` only transforms string nodes, it finds no strings left and skips everything, so slash-command rendering in `p` and `li` is a no-op. Apply both transforms to the original string children (e.g. via a combined renderer) instead of chaining them.
ApprovabilityVerdict: Needs human review 10 blocking correctness issues found. New feature adding complete slash command management UI with settings page, custom command creation, visibility controls, and schema changes. Multiple unresolved review comments identify functional bugs, including a high-severity issue where slash-command chips fail to render in markdown content. You can customize Macroscope's approvability policy. Learn more. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 5 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit dbd46ff. Configure here.
| promptRef.current, | ||
| composerCursor, | ||
| composerSlashCommands, | ||
| ), |
There was a problem hiding this comment.
Custom slash menu selection ignored
High Severity
The onSelectComposerItem function doesn't process custom-slash-command items, even though they are correctly built and displayed in the composer's slash command menu. This prevents custom commands from being inserted into the prompt when selected from the autocomplete.
Reviewed by Cursor Bugbot for commit dbd46ff. Configure here.
| settings.customSlashCommands, | ||
| settings.hiddenProviderSlashCommands, | ||
| workspaceEntries.entries, | ||
| ]); |
There was a problem hiding this comment.
Hidden slash settings stale menu
Medium Severity
The composerMenuItems memo uses settings.hiddenCustomSlashCommands and settings.hiddenGlobalSlashCommands to filter slash commands, but these are missing from its dependency array. This can cause the open slash command menu to display stale visibility for commands after settings are changed.
Reviewed by Cursor Bugbot for commit dbd46ff. Configure here.
| } | ||
| return true; | ||
| }); | ||
| } |
There was a problem hiding this comment.
Disabled custom or global still chips
Medium Severity
The collectComposerSlashCommands function doesn't filter hidden custom or agent-sourced commands. While filterComposerSlashCommandsForAutocomplete contains the necessary logic, it's not applied to the command list used by the composer editor and timeline. This allows commands disabled in settings to still be tokenized into inline chips.
Reviewed by Cursor Bugbot for commit dbd46ff. Configure here.
| return { | ||
| value: promptRef.current, | ||
| cursor: composerCursor, | ||
| expandedCursor: expandCollapsedComposerCursor(promptRef.current, composerCursor), |
There was a problem hiding this comment.
Stale slash list in callbacks
Medium Severity
Several composer callbacks that handle cursor positioning and prompt manipulation use composerSlashCommands but omit it from their dependency arrays. This can cause outdated slash command lists to be used, leading to incorrect cursor calculations and trigger detection, particularly around slash command chips.
Reviewed by Cursor Bugbot for commit dbd46ff. Configure here.
| const start = (match.index ?? 0) + prefix.length; | ||
| const rawText = `/${name}`; | ||
| const command = commandsByName.get(name); | ||
| if (!command) { |
There was a problem hiding this comment.
Timeline slash chips case mismatch
Medium Severity
Timeline slash chip rendering indexes commands by raw command.name, while composer token parsing uses lowercased names. Message text matched as /ui will not resolve a provider command stored as UI, so chips render in the composer but stay plain text in chat markdown.
Reviewed by Cursor Bugbot for commit dbd46ff. Configure here.


Summary
Rebase the custom slash command UI work onto the current
upstream/mainand keep it compatible with the current composer, timeline, and settings surfaces.What changed
Validation
mise exec node@24.13.1 -- pnpm exec vp checkmise exec node@24.13.1 -- pnpm exec vp run typecheckmise exec node@24.13.1 -- pnpm exec vp test run --project unit src/composer-logic.test.ts src/components/chat/composerSlashCommands.test.ts src/components/chat/composerSlashCommandSearch.test.tsNotes
vp checkcompleted with existing lint warnings in upstream files such asChatMarkdown.tsx,CommandPalette.tsx, andSidebarUpdatePill.tsx; no hard errors were reported.Note
Medium Risk
Touches core composer cursor/token logic and prompt serialization; incorrect slash-token handling could break editing or display, but changes are covered by unit tests and are mostly UI/settings scoped.
Overview
Adds slash command management end-to-end: a new Settings → Slash Commands route, persisted client settings for custom commands and visibility/collapse preferences, and richer composer/timeline behavior.
Settings & persistence —
ClientSettingsgainscustomSlashCommands, per-scope hide lists (hiddenCustomSlashCommands,hiddenGlobalSlashCommands,hiddenProviderSlashCommands), and UI collapse flags. Users can create local custom prompts, toggle agent (.agents) and provider commands, and search/filter in a unified settings section.Composer & chat — Slash commands from all enabled providers (not only the selected one) feed autocomplete via
collectComposerSlashCommands/buildComposerSlashCommandItems. The Lexical composer gets aComposerSlashCommandNodewith source-specific icons (app logo, bot, provider). Cursor expand/collapse and inline-token navigation treat slash commands like skills/mentions. Chat markdown and timelines render known/commandsas inline chips throughSlashCommandInlineText, using the same visibility-aware command list as the composer.Menu UX — Slash autocomplete is grouped (Custom, Global, Built-in, per-provider); skill search dedupes across provider instances and marks
.agentsskills. Search ranking for slash items now keys tie-breaks on itemidand prefers command names over descriptions for built-ins.Reviewed by Cursor Bugbot for commit dbd46ff. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add slash command management UI with settings page and inline rendering
/settings/slash-commandspage with three sections: custom commands (create/delete/hide), agent-sourced global commands, and per-provider commands with bulk show/hide and collapse controls.customSlashCommands,hiddenCustomSlashCommands,hiddenGlobalSlashCommands,hiddenProviderSlashCommands, and related collapse fields toClientSettings.📊 Macroscope summarized dbd46ff. 18 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted
🗂️ Filtered Issues
No issues evaluated.