feat: execute slash commands immediately on autocomplete selection

When selecting a slash command from the autocomplete menu, the action
now executes immediately and clears the input, providing a more
streamlined UX.

Changes:
- Added onCommandExecute prop to MentionEditor
- Modified slash command suggestion to call onCommandExecute on selection
- Clears editor content immediately after selection
- Added handleCommandExecute callback in ChatViewer
- Executes action and shows toast notifications

Before: Type "/" → select "/join" → press Enter → executes
After: Type "/" → select "/join" → executes immediately

This matches the expected behavior for command selection - when you
choose a command from autocomplete, you want to execute it, not just
insert it into the text field.

Tests: All 804 tests passing
Build: Successful
Lint: No new errors
This commit is contained in:
Claude
2026-01-12 21:11:33 +00:00
parent fa88370c0c
commit c6e041eeb0
2 changed files with 46 additions and 9 deletions

View File

@@ -18,6 +18,7 @@ import { Nip29Adapter } from "@/lib/chat/adapters/nip-29-adapter";
import { Nip53Adapter } from "@/lib/chat/adapters/nip-53-adapter";
import type { ChatProtocolAdapter } from "@/lib/chat/adapters/base-adapter";
import type { Message } from "@/types/chat";
import type { ChatAction } from "@/types/chat-actions";
import { parseSlashCommand } from "@/lib/chat/slash-command-parser";
import { UserName } from "./nostr/UserName";
import { RichText } from "./nostr/RichText";
@@ -497,6 +498,36 @@ export function ChatViewer({
}
};
// Handle command execution from autocomplete
const handleCommandExecute = useCallback(
async (action: ChatAction) => {
if (!conversation || !hasActiveAccount || isSending) return;
setIsSending(true);
try {
const result = await adapter.executeAction(action.name, {
activePubkey: activeAccount.pubkey,
activeSigner: activeAccount.signer,
conversation,
});
if (result.success) {
toast.success(result.message || "Action completed");
} else {
toast.error(result.message || "Action failed");
}
} catch (error) {
console.error("[Chat] Failed to execute action:", error);
const errorMessage =
error instanceof Error ? error.message : "Action failed";
toast.error(errorMessage);
} finally {
setIsSending(false);
}
},
[conversation, hasActiveAccount, isSending, adapter, activeAccount],
);
// Handle reply button click
const handleReply = useCallback((messageId: string) => {
setReplyTo(messageId);
@@ -813,6 +844,7 @@ export function ChatViewer({
searchProfiles={searchProfiles}
searchEmojis={searchEmojis}
searchCommands={searchCommands}
onCommandExecute={handleCommandExecute}
onSubmit={(content, emojiTags) => {
if (content.trim()) {
handleSend(content, replyTo, emojiTags);

View File

@@ -56,6 +56,7 @@ export interface MentionEditorProps {
searchProfiles: (query: string) => Promise<ProfileSearchResult[]>;
searchEmojis?: (query: string) => Promise<EmojiSearchResult[]>;
searchCommands?: (query: string) => Promise<ChatAction[]>;
onCommandExecute?: (action: ChatAction) => Promise<void>;
autoFocus?: boolean;
className?: string;
}
@@ -159,6 +160,7 @@ export const MentionEditor = forwardRef<
searchProfiles,
searchEmojis,
searchCommands,
onCommandExecute,
autoFocus = false,
className = "",
},
@@ -620,15 +622,17 @@ export const MentionEditor = forwardRef<
...slashCommandSuggestion,
command: ({ editor, props }: any) => {
// props is the ChatAction
// Replace the entire content with just the command
editor
.chain()
.focus()
.deleteRange({ from: 0, to: editor.state.doc.content.size })
.insertContentAt(0, [
{ type: "text", text: `/${props.name}` },
])
.run();
// Execute the command immediately and clear the editor
editor.commands.clearContent();
if (onCommandExecute) {
// Execute action asynchronously
onCommandExecute(props).catch((error) => {
console.error(
"[MentionEditor] Command execution failed:",
error,
);
});
}
},
},
renderLabel({ node }) {
@@ -643,6 +647,7 @@ export const MentionEditor = forwardRef<
mentionSuggestion,
emojiSuggestion,
slashCommandSuggestion,
onCommandExecute,
placeholder,
]);