From 6886713b8e41fe4fa539c30ced9893927ebd0415 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 22 Apr 2026 22:08:34 +0300 Subject: [PATCH] fix: preserve duplicate slash commands in autocomplete Show commands with identical names from different sources Add stable command IDs so selection no longer depends on name dedupe Keep command insertion flow working across chat and prompt editors --- packages/ui/src/components/chat/ChatInput.tsx | 4 +- .../components/chat/CommandAutocomplete.tsx | 43 +++++++++---------- .../components/multirun/MultiRunLauncher.tsx | 4 +- .../session/ScheduledTaskEditorDialog.tsx | 4 +- .../agent-manager/AgentManagerEmptyState.tsx | 4 +- 5 files changed, 28 insertions(+), 31 deletions(-) diff --git a/packages/ui/src/components/chat/ChatInput.tsx b/packages/ui/src/components/chat/ChatInput.tsx index a7fa620a..cd5b950e 100644 --- a/packages/ui/src/components/chat/ChatInput.tsx +++ b/packages/ui/src/components/chat/ChatInput.tsx @@ -32,7 +32,7 @@ import { renderMagicPrompt } from '@/lib/magicPrompts'; import { AttachedFilesList } from './FileAttachment'; import { QueuedMessageChips } from './QueuedMessageChips'; import { FileMentionAutocomplete, type FileMentionHandle } from './FileMentionAutocomplete'; -import { CommandAutocomplete, type CommandAutocompleteHandle } from './CommandAutocomplete'; +import { CommandAutocomplete, type CommandAutocompleteHandle, type CommandInfo } from './CommandAutocomplete'; import { SkillAutocomplete, type SkillAutocompleteHandle } from './SkillAutocomplete'; import { cn, formatDirectoryName, isMacOS } from '@/lib/utils'; import { ModelControls } from './ModelControls'; @@ -2470,7 +2470,7 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo textareaRef.current?.focus(); }; - const handleCommandSelect = (command: { name: string; description?: string; agent?: string; model?: string }) => { + const handleCommandSelect = (command: CommandInfo) => { setMessage(`/${command.name} `); diff --git a/packages/ui/src/components/chat/CommandAutocomplete.tsx b/packages/ui/src/components/chat/CommandAutocomplete.tsx index ec13827b..07255f76 100644 --- a/packages/ui/src/components/chat/CommandAutocomplete.tsx +++ b/packages/ui/src/components/chat/CommandAutocomplete.tsx @@ -7,8 +7,12 @@ import { useCommandsStore } from '@/stores/useCommandsStore'; import { useSkillsStore } from '@/stores/useSkillsStore'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; -interface CommandInfo { +type CommandSource = 'openchamber' | 'opencode'; + +export interface CommandInfo { + id: string; name: string; + source: CommandSource; description?: string; agent?: string; model?: string; @@ -89,8 +93,10 @@ export const CommandAutocomplete = React.forwardRef skill.name)); - const customCommands: CommandInfo[] = commandsWithMetadata.map(cmd => ({ + const customCommands: CommandInfo[] = commandsWithMetadata.map((cmd, index) => ({ + id: `opencode:${cmd.scope ?? 'global'}:${cmd.name}:${cmd.agent ?? ''}:${cmd.model ?? ''}:${index}`, name: cmd.name, + source: 'opencode', description: cmd.description, agent: cmd.agent ?? undefined, model: cmd.model ?? undefined, @@ -101,30 +107,23 @@ export const CommandAutocomplete = React.forwardRef(); - - builtInCommands.forEach(cmd => commandMap.set(cmd.name, cmd)); - - customCommands.forEach(cmd => commandMap.set(cmd.name, cmd)); - - const allCommands = Array.from(commandMap.values()); + const allCommands = [...builtInCommands, ...customCommands]; const allowInitCommand = !hasMessagesInCurrentSession; const filtered = (searchQuery @@ -148,19 +147,19 @@ export const CommandAutocomplete = React.forwardRef { itemRefs.current[index] = el; }} className={cn( "flex items-start gap-2 px-3 py-2 cursor-pointer rounded-lg", @@ -425,5 +424,3 @@ export const CommandAutocomplete = React.forwardRef = ({ }); }, [prompt, updateAutocompleteState]); - const handleAutocompleteCommandSelect = React.useCallback((command: { name: string }) => { + const handleAutocompleteCommandSelect = React.useCallback((command: CommandInfo) => { const nextPrompt = `/${command.name} `; setPrompt(nextPrompt); setShowCommandAutocomplete(false); diff --git a/packages/ui/src/components/session/ScheduledTaskEditorDialog.tsx b/packages/ui/src/components/session/ScheduledTaskEditorDialog.tsx index 3c0d7508..641f7213 100644 --- a/packages/ui/src/components/session/ScheduledTaskEditorDialog.tsx +++ b/packages/ui/src/components/session/ScheduledTaskEditorDialog.tsx @@ -13,7 +13,7 @@ import { RiAddLine, RiCloseLine, RiCalendarLine, RiArrowLeftSLine, RiArrowRightS import { ModelSelector } from '@/components/sections/agents/ModelSelector'; import { AgentSelector } from '@/components/sections/commands/AgentSelector'; import { isPrimaryMode } from '@/components/chat/mobileControlsUtils'; -import { CommandAutocomplete, type CommandAutocompleteHandle } from '@/components/chat/CommandAutocomplete'; +import { CommandAutocomplete, type CommandAutocompleteHandle, type CommandInfo } from '@/components/chat/CommandAutocomplete'; import { FileMentionAutocomplete, type FileMentionHandle } from '@/components/chat/FileMentionAutocomplete'; import { useConfigStore } from '@/stores/useConfigStore'; import { useUIStore } from '@/stores/useUIStore'; @@ -907,7 +907,7 @@ export function ScheduledTaskEditorDialog(props: { }); }, [draft.execution.prompt, setPromptValue, updateAutocompleteState]); - const handleCommandSelect = React.useCallback((command: { name: string }) => { + const handleCommandSelect = React.useCallback((command: CommandInfo) => { const nextPrompt = `/${command.name} `; setPromptValue(nextPrompt); setShowCommandAutocomplete(false); diff --git a/packages/ui/src/components/views/agent-manager/AgentManagerEmptyState.tsx b/packages/ui/src/components/views/agent-manager/AgentManagerEmptyState.tsx index ae31a491..9a6fb7b2 100644 --- a/packages/ui/src/components/views/agent-manager/AgentManagerEmptyState.tsx +++ b/packages/ui/src/components/views/agent-manager/AgentManagerEmptyState.tsx @@ -20,7 +20,7 @@ import { useProjectsStore } from '@/stores/useProjectsStore'; import { ModelMultiSelect, generateInstanceId, type ModelSelectionWithId } from '@/components/multirun/ModelMultiSelect'; import { BranchSelector, useBranchOptions } from '@/components/multirun/BranchSelector'; import { AgentSelector } from '@/components/multirun/AgentSelector'; -import { CommandAutocomplete, type CommandAutocompleteHandle } from '@/components/chat/CommandAutocomplete'; +import { CommandAutocomplete, type CommandAutocompleteHandle, type CommandInfo } from '@/components/chat/CommandAutocomplete'; import { FileMentionAutocomplete, type FileMentionHandle } from '@/components/chat/FileMentionAutocomplete'; import { isIMECompositionEvent } from '@/lib/ime'; import { getWorktreeSetupCommands } from '@/lib/openchamberConfig'; @@ -294,7 +294,7 @@ export const AgentManagerEmptyState: React.FC = ({ }); }, [prompt, updateAutocompleteState]); - const handleAutocompleteCommandSelect = React.useCallback((command: { name: string }) => { + const handleAutocompleteCommandSelect = React.useCallback((command: CommandInfo) => { const nextPrompt = `/${command.name} `; setPrompt(nextPrompt); setShowCommandAutocomplete(false);