-
-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Introduce keyboard (Up/Down/Enter/Escape) selection for slash commands #4402
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9cad6a6
26ac150
f0840ed
7549f51
3e85053
9389a2f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| import React, { useState, useRef, useEffect } from "react"; | ||
| import SlashCommandsButton, { | ||
| SlashCommands, | ||
| useSlashCommands, | ||
| // useSlashCommands, | ||
| } from "./SlashCommands"; | ||
| import debounce from "lodash.debounce"; | ||
| import { PaperPlaneRight } from "@phosphor-icons/react"; | ||
|
|
@@ -24,6 +24,7 @@ import { | |
| import useTextSize from "@/hooks/useTextSize"; | ||
| import { useTranslation } from "react-i18next"; | ||
| import Appearance from "@/models/appearance"; | ||
| import { useSlashCommands } from "@/hooks/useSlashCommands"; | ||
|
|
||
| export const PROMPT_INPUT_ID = "primary-prompt-input"; | ||
| export const PROMPT_INPUT_EVENT = "set_prompt_input"; | ||
|
|
@@ -40,10 +41,17 @@ export default function PromptInput({ | |
| const { isDisabled } = useIsDisabled(); | ||
| const [promptInput, setPromptInput] = useState(""); | ||
| const { showAgents, setShowAgents } = useAvailableAgents(); | ||
| const { showSlashCommand, setShowSlashCommand } = useSlashCommands(); | ||
| const { | ||
| showSlashCommand, | ||
| setShowSlashCommand, | ||
| handleKeyDown, | ||
| highlightedSlashCommand, | ||
| watchForSlash, | ||
| textareaRef, | ||
| slashCommands, | ||
| setSlashCommands, | ||
| } = useSlashCommands({ sendCommand }); | ||
| const formRef = useRef(null); | ||
| const textareaRef = useRef(null); | ||
| const [_, setFocused] = useState(false); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why was this removed? |
||
| const undoStack = useRef([]); | ||
| const redoStack = useRef([]); | ||
| const { textSizeClass } = useTextSize(); | ||
|
|
@@ -61,8 +69,7 @@ export default function PromptInput({ | |
| } | ||
|
|
||
| useEffect(() => { | ||
| if (!!window) | ||
angelplusultra marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| window.addEventListener(PROMPT_INPUT_EVENT, handlePromptUpdate); | ||
| if (window) window.addEventListener(PROMPT_INPUT_EVENT, handlePromptUpdate); | ||
| return () => | ||
| window?.removeEventListener(PROMPT_INPUT_EVENT, handlePromptUpdate); | ||
| }, []); | ||
|
|
@@ -88,7 +95,6 @@ export default function PromptInput({ | |
| const debouncedSaveState = debounce(saveCurrentState, 250); | ||
|
|
||
| function handleSubmit(e) { | ||
| setFocused(false); | ||
| submit(e); | ||
| } | ||
|
|
||
|
|
@@ -97,14 +103,6 @@ export default function PromptInput({ | |
| textareaRef.current.style.height = "auto"; | ||
| } | ||
|
|
||
| function checkForSlash(e) { | ||
| const input = e.target.value; | ||
| if (input === "/") setShowSlashCommand(true); | ||
| if (showSlashCommand) setShowSlashCommand(false); | ||
| return; | ||
| } | ||
| const watchForSlash = debounce(checkForSlash, 300); | ||
|
|
||
| function checkForAt(e) { | ||
| const input = e.target.value; | ||
| if (input === "@") return setShowAgents(true); | ||
|
|
@@ -121,6 +119,8 @@ export default function PromptInput({ | |
| // Is simple enter key press w/o shift key | ||
| if (event.keyCode === 13 && !event.shiftKey) { | ||
| event.preventDefault(); | ||
| // Don't submit if slash commands are showing - let the slash command handler take care of it | ||
| if (showSlashCommand) return; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good catch |
||
| if (isStreaming || isDisabled) return; // Prevent submission if streaming or disabled | ||
| return submit(event); | ||
| } | ||
|
|
@@ -243,12 +243,18 @@ export default function PromptInput({ | |
| } | ||
|
|
||
| return ( | ||
| <div className="w-full fixed md:absolute bottom-0 left-0 z-10 md:z-0 flex justify-center items-center"> | ||
| <div | ||
| onKeyDown={handleKeyDown} | ||
| className="w-full fixed md:absolute bottom-0 left-0 z-10 md:z-0 flex justify-center items-center" | ||
| > | ||
| <SlashCommands | ||
| showing={showSlashCommand} | ||
| setShowing={setShowSlashCommand} | ||
| sendCommand={sendCommand} | ||
| promptRef={textareaRef} | ||
| highlightedSlashCommand={highlightedSlashCommand} | ||
| slashCommands={slashCommands} | ||
| setSlashCommands={setSlashCommands} | ||
| /> | ||
| <AvailableAgents | ||
| showing={showAgents} | ||
|
|
@@ -274,9 +280,7 @@ export default function PromptInput({ | |
| handlePasteEvent(e); | ||
| }} | ||
| required={true} | ||
| onFocus={() => setFocused(true)} | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why was this removed? |
||
| onBlur={(e) => { | ||
| setFocused(false); | ||
| adjustTextArea(e); | ||
| }} | ||
| value={promptInput} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| import System from "@/models/system"; | ||
| import debounce from "lodash.debounce"; | ||
| import { useEffect, useRef, useState } from "react"; | ||
|
|
||
| export function useSlashCommands({ sendCommand }) { | ||
| const [showSlashCommand, setShowSlashCommand] = useState(false); | ||
| const [highlightedSlashCommand, setHighlightedSlashCommand] = useState(1); | ||
| const [slashCommands, setSlashCommands] = useState([]); | ||
| // Ref to the textarea element, this is needed to apply focus to the textarea when slash commands are triggered by clicking the slash command button | ||
| const textareaRef = useRef(null); | ||
|
|
||
| // Check for slash command to show slash commands | ||
| function checkForSlash(e) { | ||
| const input = e.target.value; | ||
| if (input === "/") { | ||
| setShowSlashCommand(true); | ||
| setHighlightedSlashCommand(0); // Start with reset command highlighted | ||
| } | ||
| if (showSlashCommand) setShowSlashCommand(false); | ||
| return; | ||
| } | ||
| const watchForSlash = debounce(checkForSlash, 300); | ||
| const fetchSlashCommands = async () => { | ||
| const presets = await System.getSlashCommandPresets(); | ||
| setSlashCommands(presets); | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| fetchSlashCommands(); | ||
| }, []); | ||
|
|
||
| // Focus the textarea when slash commands are shown, this is needed to enable keyboard navigation | ||
| useEffect(() => { | ||
| if (showSlashCommand) { | ||
| if (textareaRef.current) { | ||
| textareaRef.current.focus(); | ||
| } | ||
| } | ||
| }, [showSlashCommand]); | ||
|
|
||
| // Handle key down events for slash command navigation and selection | ||
| function handleKeyDown(e) { | ||
| // Only handle arrow keys when slash commands are showing | ||
| if (!showSlashCommand) return; | ||
|
|
||
| if (e.key === "ArrowUp") { | ||
| e.preventDefault(); | ||
| setHighlightedSlashCommand((prev) => { | ||
| if (prev <= 0) { | ||
| return slashCommands.length; | ||
| } | ||
| return prev - 1; | ||
| }); | ||
| } | ||
| if (e.key === "ArrowDown") { | ||
| e.preventDefault(); | ||
| setHighlightedSlashCommand((prev) => { | ||
| if (prev >= slashCommands.length) { | ||
| return 0; | ||
| } | ||
| return prev + 1; | ||
| }); | ||
| } | ||
| if (e.key === "Enter" && showSlashCommand) { | ||
| e.preventDefault(); | ||
|
|
||
| if (highlightedSlashCommand === 0) { | ||
| setShowSlashCommand(false); | ||
| sendCommand({ text: "/reset", autoSubmit: true }); | ||
| // Reset command is selected | ||
| return; | ||
| } | ||
|
|
||
| // Check if it's a preset selection (index 1 and above) | ||
| const presetIndex = highlightedSlashCommand - 1; | ||
| if (presetIndex >= 0 && presetIndex < slashCommands.length) { | ||
| const preset = slashCommands[presetIndex]; | ||
| setShowSlashCommand(false); | ||
| sendCommand({ text: `${preset.command} ` }); | ||
| textareaRef?.current?.focus(); | ||
| } | ||
| } | ||
|
|
||
| if (e.key === "Escape" && showSlashCommand) { | ||
| e.preventDefault(); | ||
| setShowSlashCommand(false); | ||
| } | ||
| } | ||
| return { | ||
| slashCommands, | ||
| setSlashCommands, | ||
| showSlashCommand, | ||
| setShowSlashCommand, | ||
| highlightedSlashCommand, | ||
| setHighlightedSlashCommand, | ||
| watchForSlash, | ||
| handleKeyDown, | ||
| textareaRef, | ||
| }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we not do highlight operation with
preset.id?