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
This commit is contained in:
@@ -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<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
textareaRef.current?.focus();
|
||||
};
|
||||
|
||||
const handleCommandSelect = (command: { name: string; description?: string; agent?: string; model?: string }) => {
|
||||
const handleCommandSelect = (command: CommandInfo) => {
|
||||
|
||||
setMessage(`/${command.name} `);
|
||||
|
||||
|
||||
@@ -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<CommandAutocompleteHandle, C
|
||||
setLoading(true);
|
||||
try {
|
||||
const skillNames = new Set(skills.map((skill) => 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<CommandAutocompleteHandle, C
|
||||
|
||||
const builtInCommands: CommandInfo[] = [
|
||||
...(hasSession && !hasMessagesInCurrentSession
|
||||
? [{ name: 'init', description: 'Create/update AGENTS.md file', isBuiltIn: true }]
|
||||
? [{ id: 'openchamber:init', name: 'init', source: 'openchamber' as const, description: 'Create/update AGENTS.md file', isBuiltIn: true }]
|
||||
: []
|
||||
),
|
||||
...(hasSession // Show when session exists, not when hasMessages
|
||||
? [
|
||||
{ name: 'undo', description: 'Undo the last message', isBuiltIn: true },
|
||||
{ name: 'redo', description: 'Redo previously undone messages', isBuiltIn: true },
|
||||
{ id: 'openchamber:undo', name: 'undo', source: 'openchamber' as const, description: 'Undo the last message', isBuiltIn: true },
|
||||
{ id: 'openchamber:redo', name: 'redo', source: 'openchamber' as const, description: 'Redo previously undone messages', isBuiltIn: true },
|
||||
]
|
||||
: []
|
||||
),
|
||||
{ name: 'compact', description: 'Compress session history using AI to reduce context size', isBuiltIn: true },
|
||||
{ id: 'openchamber:compact', name: 'compact', source: 'openchamber' as const, description: 'Compress session history using AI to reduce context size', isBuiltIn: true },
|
||||
...(hasSession
|
||||
? [{ name: 'summary', description: 'Non-destructive session summary. Optional topic hint after the command.', isOpenChamber: true }]
|
||||
? [{ id: 'openchamber:summary', name: 'summary', source: 'openchamber' as const, description: 'Non-destructive session summary. Optional topic hint after the command.', isOpenChamber: true }]
|
||||
: []
|
||||
),
|
||||
];
|
||||
|
||||
const commandMap = new Map<string, CommandInfo>();
|
||||
|
||||
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<CommandAutocompleteHandle, C
|
||||
const allowInitCommand = !hasMessagesInCurrentSession;
|
||||
const builtInCommands: CommandInfo[] = [
|
||||
...(hasSession && !hasMessagesInCurrentSession
|
||||
? [{ name: 'init', description: 'Create/update AGENTS.md file', isBuiltIn: true }]
|
||||
? [{ id: 'openchamber:init', name: 'init', source: 'openchamber' as const, description: 'Create/update AGENTS.md file', isBuiltIn: true }]
|
||||
: []
|
||||
),
|
||||
...(hasSession // Show when session exists, not when hasMessages
|
||||
? [
|
||||
{ name: 'undo', description: 'Undo the last message', isBuiltIn: true },
|
||||
{ name: 'redo', description: 'Redo previously undone messages', isBuiltIn: true },
|
||||
{ id: 'openchamber:undo', name: 'undo', source: 'openchamber' as const, description: 'Undo the last message', isBuiltIn: true },
|
||||
{ id: 'openchamber:redo', name: 'redo', source: 'openchamber' as const, description: 'Redo previously undone messages', isBuiltIn: true },
|
||||
]
|
||||
: []
|
||||
),
|
||||
{ name: 'compact', description: 'Compress session history using AI to reduce context size', isBuiltIn: true },
|
||||
{ id: 'openchamber:compact', name: 'compact', source: 'openchamber' as const, description: 'Compress session history using AI to reduce context size', isBuiltIn: true },
|
||||
...(hasSession
|
||||
? [{ name: 'summary', description: 'Non-destructive session summary. Optional topic hint after the command.', isOpenChamber: true }]
|
||||
? [{ id: 'openchamber:summary', name: 'summary', source: 'openchamber' as const, description: 'Non-destructive session summary. Optional topic hint after the command.', isOpenChamber: true }]
|
||||
: []
|
||||
),
|
||||
];
|
||||
@@ -307,7 +306,7 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
|
||||
|
||||
return (
|
||||
<div
|
||||
key={command.name}
|
||||
key={command.id}
|
||||
ref={(el) => { 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<CommandAutocompleteHandle, C
|
||||
});
|
||||
|
||||
CommandAutocomplete.displayName = 'CommandAutocomplete';
|
||||
|
||||
export type { CommandInfo };
|
||||
|
||||
@@ -19,7 +19,7 @@ import type { CreateMultiRunParams, MultiRunModelSelection } from '@/types/multi
|
||||
import { ModelMultiSelect, generateInstanceId, type ModelSelectionWithId } from './ModelMultiSelect';
|
||||
import { BranchSelector, useBranchOptions } from './BranchSelector';
|
||||
import { AgentSelector } from './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 { isDesktopShell } from '@/lib/desktop';
|
||||
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||
@@ -505,7 +505,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
});
|
||||
}, [prompt, updateAutocompleteState]);
|
||||
|
||||
const handleAutocompleteCommandSelect = React.useCallback((command: { name: string }) => {
|
||||
const handleAutocompleteCommandSelect = React.useCallback((command: CommandInfo) => {
|
||||
const nextPrompt = `/${command.name} `;
|
||||
setPrompt(nextPrompt);
|
||||
setShowCommandAutocomplete(false);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<AgentManagerEmptyStateProps> = ({
|
||||
});
|
||||
}, [prompt, updateAutocompleteState]);
|
||||
|
||||
const handleAutocompleteCommandSelect = React.useCallback((command: { name: string }) => {
|
||||
const handleAutocompleteCommandSelect = React.useCallback((command: CommandInfo) => {
|
||||
const nextPrompt = `/${command.name} `;
|
||||
setPrompt(nextPrompt);
|
||||
setShowCommandAutocomplete(false);
|
||||
|
||||
Reference in New Issue
Block a user