From d448b5f4d8bb27cb1f3b00ec1fba808a4805ed2f Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Thu, 27 Mar 2025 11:40:43 -0700 Subject: [PATCH 1/4] fix slash command triggering --- core/commands/index.ts | 2 +- .../mainInput/tiptap/editorConfig.ts | 31 +++++++++---------- .../tiptap/extensions/FillerExtension.tsx | 20 ------------ .../mainInput/tiptap/getSuggestion.ts | 4 +-- gui/src/redux/selectors/index.ts | 2 +- 5 files changed, 18 insertions(+), 41 deletions(-) delete mode 100644 gui/src/components/mainInput/tiptap/extensions/FillerExtension.tsx diff --git a/core/commands/index.ts b/core/commands/index.ts index 486eaccf29..ba5c3403cb 100644 --- a/core/commands/index.ts +++ b/core/commands/index.ts @@ -19,7 +19,7 @@ export function slashFromCustomCommand( .slice(customCommand.name.length + 1, userInput.length) .trimStart(); } - + debugger; // Render prompt template let promptUserInput: string; if (customCommand.prompt.includes("{{{ input }}}")) { diff --git a/gui/src/components/mainInput/tiptap/editorConfig.ts b/gui/src/components/mainInput/tiptap/editorConfig.ts index d79981d398..985cadc64a 100644 --- a/gui/src/components/mainInput/tiptap/editorConfig.ts +++ b/gui/src/components/mainInput/tiptap/editorConfig.ts @@ -30,7 +30,6 @@ import { getFontSize } from "../../../util"; import { AddCodeToEdit } from "./extensions/AddCodeToEditExtension"; import { CodeBlockExtension } from "./extensions/CodeBlockExtension"; import { SlashCommand } from "./extensions/CommandsExtension"; -import { MockExtension } from "./extensions/FillerExtension"; import { Mention } from "./extensions/MentionExtension"; import { getContextProviderDropdownOptions, @@ -352,22 +351,20 @@ export function createEditorConfig(options: { }, }, }), - props.availableSlashCommands.length - ? SlashCommand.configure({ - HTMLAttributes: { - class: "mention", - }, - suggestion: getSlashCommandDropdownOptions( - availableSlashCommandsRef, - onClose, - onOpen, - ideMessenger, - ), - renderText: (props) => { - return props.node.attrs.label; - }, - }) - : MockExtension, + SlashCommand.configure({ + HTMLAttributes: { + class: "mention", + }, + suggestion: getSlashCommandDropdownOptions( + availableSlashCommandsRef, + onClose, + onOpen, + ideMessenger, + ), + renderText: (props) => { + return props.node.attrs.label; + }, + }), CodeBlockExtension, ], editorProps: { diff --git a/gui/src/components/mainInput/tiptap/extensions/FillerExtension.tsx b/gui/src/components/mainInput/tiptap/extensions/FillerExtension.tsx deleted file mode 100644 index 9b8baac86e..0000000000 --- a/gui/src/components/mainInput/tiptap/extensions/FillerExtension.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Node } from "@tiptap/core"; - -export const MockExtension = Node.create({ - name: "mockExtension", - - addOptions() { - return { - enabled: false, - }; - }, - addCommands() { - return {}; - }, - addKeyboardShortcuts() { - return {}; - }, - addProseMirrorPlugins() { - return []; - }, -}); diff --git a/gui/src/components/mainInput/tiptap/getSuggestion.ts b/gui/src/components/mainInput/tiptap/getSuggestion.ts index 264258dd50..f9faad8637 100644 --- a/gui/src/components/mainInput/tiptap/getSuggestion.ts +++ b/gui/src/components/mainInput/tiptap/getSuggestion.ts @@ -203,7 +203,7 @@ export function getSlashCommandDropdownOptions( const filteredCommands = query.length > 0 ? options.filter((slashCommand) => { - const sc = slashCommand.title.substring(1).toLowerCase(); + const sc = slashCommand.title.toLowerCase(); const iv = query.toLowerCase(); return sc.startsWith(iv); }) @@ -219,7 +219,7 @@ export function getSlashCommandDropdownOptions( action: provider.action, })); - if (query.length === 0 && commandItems.length > 0) { + if (query.length === 0 && commandItems.length === 0) { commandItems.push({ title: "Explore prompts", type: "action", diff --git a/gui/src/redux/selectors/index.ts b/gui/src/redux/selectors/index.ts index 3d46bf9ea9..bb7850dd55 100644 --- a/gui/src/redux/selectors/index.ts +++ b/gui/src/redux/selectors/index.ts @@ -8,7 +8,7 @@ export const selectSlashCommandComboBoxInputs = createSelector( return ( slashCommands?.map((cmd) => { return { - title: `${cmd.name}`, + title: cmd.name, description: cmd.description, type: "slashCommand" as ComboBoxItemType, }; From ca085d71d59ac12d5110cf26f08e9e3e6c98238b Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Thu, 27 Mar 2025 13:33:33 -0700 Subject: [PATCH 2/4] slash command progress --- core/commands/index.ts | 24 ++-- extensions/vscode/package-lock.json | 4 +- .../mainInput/tiptap/resolveInput.ts | 119 +++++++++++------- gui/src/redux/thunks/gatherContext.ts | 17 ++- gui/src/redux/thunks/streamResponse.ts | 46 ++----- 5 files changed, 110 insertions(+), 100 deletions(-) diff --git a/core/commands/index.ts b/core/commands/index.ts index ba5c3403cb..b6f2334858 100644 --- a/core/commands/index.ts +++ b/core/commands/index.ts @@ -7,19 +7,21 @@ import SlashCommands from "./slash"; export function slashFromCustomCommand( customCommand: CustomCommand, ): SlashCommand { + const commandName = customCommand.name.startsWith("/") + ? customCommand.name.substring(1) + : customCommand.name; return { - name: customCommand.name, + name: commandName, description: customCommand.description ?? "", prompt: customCommand.prompt, run: async function* ({ input, llm, history, ide, completionOptions }) { + console.log("SLASH COMMAND RUN", input, prompt, commandName); // Remove slash command prefix from input let userInput = input; - if (userInput.startsWith(`/${customCommand.name}`)) { - userInput = userInput - .slice(customCommand.name.length + 1, userInput.length) - .trimStart(); + if (userInput.startsWith(commandName)) { + userInput = userInput.substring(commandName.length).trimStart(); } - debugger; + // Render prompt template let promptUserInput: string; if (customCommand.prompt.includes("{{{ input }}}")) { @@ -44,17 +46,13 @@ export function slashFromCustomCommand( if ( Array.isArray(content) && content.some( - (part) => - "text" in part && part.text?.startsWith(`/${customCommand.name}`), + (part) => "text" in part && part.text?.startsWith(commandName), ) ) { messages[i] = { ...message, content: content.map((part) => { - if ( - "text" in part && - part.text.startsWith(`/${customCommand.name}`) - ) { + if ("text" in part && part.text.startsWith(commandName)) { return { type: "text", text: promptUserInput }; } return part; @@ -63,7 +61,7 @@ export function slashFromCustomCommand( break; } else if ( typeof content === "string" && - content.startsWith(`/${customCommand.name}`) + content.startsWith(commandName) ) { messages[i] = { ...message, content: promptUserInput }; break; diff --git a/extensions/vscode/package-lock.json b/extensions/vscode/package-lock.json index 678794d028..1170be7867 100644 --- a/extensions/vscode/package-lock.json +++ b/extensions/vscode/package-lock.json @@ -1,12 +1,12 @@ { "name": "continue", - "version": "1.1.16", + "version": "1.1.17", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "continue", - "version": "1.1.16", + "version": "1.1.17", "license": "Apache-2.0", "dependencies": { "@continuedev/fetch": "^1.0.3", diff --git a/gui/src/components/mainInput/tiptap/resolveInput.ts b/gui/src/components/mainInput/tiptap/resolveInput.ts index d9eb47b6c7..427edead14 100644 --- a/gui/src/components/mainInput/tiptap/resolveInput.ts +++ b/gui/src/components/mainInput/tiptap/resolveInput.ts @@ -7,6 +7,7 @@ import { MessageContent, MessagePart, RangeInFile, + SlashCommandDescription, TextMessagePart, } from "core"; import { ctxItemToRifWithContents } from "core/commands/util"; @@ -27,6 +28,7 @@ interface ResolveEditorContentInput { modifiers: InputModifiers; ideMessenger: IIdeMessenger; defaultContextProviders: DefaultContextProvider[]; + availableSlashCommands: SlashCommandDescription[]; selectedModelTitle: string; dispatch: Dispatch; } @@ -40,22 +42,36 @@ async function resolveEditorContent({ modifiers, ideMessenger, defaultContextProviders, + availableSlashCommands, selectedModelTitle, dispatch, }: ResolveEditorContentInput): Promise< - [ContextItemWithId[], RangeInFile[], MessageContent] + [ + ContextItemWithId[], + RangeInFile[], + MessageContent, + ( + | { + name: string; + input: string; + } + | undefined + ), + ] > { let parts: MessagePart[] = []; let contextItemAttrs: MentionAttrs[] = []; const selectedCode: RangeInFile[] = []; - let slashCommand: string | undefined = undefined; + let slashCommandName: string | undefined = undefined; + let slashCommandWithInput: { name: string; input: string } | undefined = + undefined; if (editorState?.content) { for (const p of editorState.content) { if (p.type === "paragraph") { const [text, ctxItems, foundSlashCommand] = resolveParagraph(p); - // Only take the first slash command\ - if (foundSlashCommand && typeof slashCommand === "undefined") { - slashCommand = foundSlashCommand; + // Only take the first slash command + if (foundSlashCommand && typeof slashCommandName === "undefined") { + slashCommandName = foundSlashCommand; } contextItemAttrs.push(...ctxItems); @@ -113,7 +129,60 @@ async function resolveEditorContent({ } } - const shouldGatherContext = modifiers.useCodebase || slashCommand; + // Add slash command text back in (this could be removed) + if (slashCommandName) { + const command = availableSlashCommands.find( + (c) => c.name === slashCommandWithName, + ); + if (command) { + slashCommandWithInput = { + name: command.name, + input: "TODO INPUT", + }; + } + } + const getSlashCommandForInput = ( + input: MessageContent, + slashCommands: SlashCommandDescription[], + ): [SlashCommandDescription, string] | undefined => { + let slashCommand: SlashCommandDescription | undefined; + let slashCommandName: string | undefined; + + let lastText = + typeof input === "string" + ? input + : ( + input.filter((part) => part.type === "text").slice(-1)[0] as + | TextMessagePart + | undefined + )?.text || ""; + + if (lastText.startsWith("/")) { + slashCommandName = lastText.split(" ")[0].substring(1); + slashCommand = slashCommands.find((command) => + lastText.startsWith(command), + ); + } + if (!slashCommand || !slashCommandName) { + return undefined; + } + + // Convert to actual slash command object with runnable function + return [slashCommand, renderChatMessage({ role: "user", content: input })]; + }; + + if (slashCommand) { + let lastTextIndex = findLastIndex(parts, (part) => part.type === "text"); + const lastTextPart = parts[lastTextIndex] as TextMessagePart; + const lastPart = `/${slashCommand} ${lastTextPart?.text || ""}`; + if (parts.length > 0) { + lastTextPart.text = lastPart; + } else { + parts = [{ type: "text", text: lastPart }]; + } + } + + const shouldGatherContext = modifiers.useCodebase || slashCommandWithInput; if (shouldGatherContext) { dispatch(setIsGatheringContext(true)); } @@ -179,21 +248,11 @@ async function resolveEditorContent({ contextItemsText += "\n"; } - if (slashCommand) { - let lastTextIndex = findLastIndex(parts, (part) => part.type === "text"); - const lastTextPart = parts[lastTextIndex] as TextMessagePart; - const lastPart = `${slashCommand} ${lastTextPart?.text || ""}`; - if (parts.length > 0) { - lastTextPart.text = lastPart; - } else { - parts = [{ type: "text", text: lastPart }]; - } - } if (shouldGatherContext) { dispatch(setIsGatheringContext(false)); } - return [contextItems, selectedCode, parts]; + return [contextItems, selectedCode, parts, slashCommandWithInput]; } function findLastIndex( @@ -236,30 +295,4 @@ function resolveParagraph( return [text, contextItems, slashCommand]; } -export function hasSlashCommandOrContextProvider( - editorState: JSONContent, -): boolean { - if (!editorState?.content) { - return false; - } - - for (const p of editorState.content) { - if (p.type === "paragraph" && p.content) { - for (const child of p.content) { - if (child.type === "slashcommand") { - return true; - } - if ( - child.type === "mention" && - child.attrs?.itemType === "contextProvider" - ) { - return true; - } - } - } - } - - return false; -} - export default resolveEditorContent; diff --git a/gui/src/redux/thunks/gatherContext.ts b/gui/src/redux/thunks/gatherContext.ts index ebd50e644e..5386881f7c 100644 --- a/gui/src/redux/thunks/gatherContext.ts +++ b/gui/src/redux/thunks/gatherContext.ts @@ -16,6 +16,12 @@ export const gatherContext = createAsyncThunk< selectedContextItems: ContextItemWithId[]; selectedCode: RangeInFile[]; content: MessageContent; + slashCommandWithInput: + | { + name: string; + input: string; + } + | undefined; }, { editorState: JSONContent; @@ -42,12 +48,13 @@ export const gatherContext = createAsyncThunk< } // Resolve context providers and construct new history - let [selectedContextItems, selectedCode, content] = + let [selectedContextItems, selectedCode, content, slashCommandWithInput] = await resolveEditorContent({ editorState, modifiers, ideMessenger: extra.ideMessenger, defaultContextProviders, + availableSlashCommands: state.config.config.slashCommands, dispatch, selectedModelTitle: defaultModel.title, }); @@ -107,7 +114,11 @@ export const gatherContext = createAsyncThunk< } } - // dispatch(addContextItems(contextItems)); - return { selectedContextItems, selectedCode, content }; + return { + selectedContextItems, + selectedCode, + content, + slashCommandWithInput, + }; }, ); diff --git a/gui/src/redux/thunks/streamResponse.ts b/gui/src/redux/thunks/streamResponse.ts index 84e2edbe58..cfdb5a9d27 100644 --- a/gui/src/redux/thunks/streamResponse.ts +++ b/gui/src/redux/thunks/streamResponse.ts @@ -1,13 +1,7 @@ import { createAsyncThunk, unwrapResult } from "@reduxjs/toolkit"; import { JSONContent } from "@tiptap/core"; -import { - InputModifiers, - MessageContent, - SlashCommandDescription, - TextMessagePart, -} from "core"; +import { InputModifiers } from "core"; import { constructMessages } from "core/llm/constructMessages"; -import { renderChatMessage } from "core/util/messageContent"; import posthog from "posthog-js"; import { v4 as uuidv4 } from "uuid"; import { selectDefaultModel } from "../slices/configSlice"; @@ -22,36 +16,6 @@ import { streamNormalInput } from "./streamNormalInput"; import { streamThunkWrapper } from "./streamThunkWrapper"; import { updateFileSymbolsFromFiles } from "./updateFileSymbols"; -const getSlashCommandForInput = ( - input: MessageContent, - slashCommands: SlashCommandDescription[], -): [SlashCommandDescription, string] | undefined => { - let slashCommand: SlashCommandDescription | undefined; - let slashCommandName: string | undefined; - - let lastText = - typeof input === "string" - ? input - : ( - input.filter((part) => part.type === "text").slice(-1)[0] as - | TextMessagePart - | undefined - )?.text || ""; - - if (lastText.startsWith("/")) { - slashCommandName = lastText.split(" ")[0].substring(1); - slashCommand = slashCommands.find((command) => - lastText.startsWith(`/${command.name} `), - ); - } - if (!slashCommand || !slashCommandName) { - return undefined; - } - - // Convert to actual slash command object with runnable function - return [slashCommand, renderChatMessage({ role: "user", content: input })]; -}; - export const streamResponseThunk = createAsyncThunk< void, { @@ -90,8 +54,12 @@ export const streamResponseThunk = createAsyncThunk< promptPreamble, }), ); - const unwrapped = unwrapResult(result); - const { selectedContextItems, selectedCode, content } = unwrapped; + const { + selectedContextItems, + selectedCode, + content, + slashCommandWithInput, + } = unwrapResult(result); // symbols for both context items AND selected codeblocks const filesForSymbols = [ From 7d5eb828f059fbccf47340eb30eab95674a66478 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Thu, 27 Mar 2025 15:09:34 -0700 Subject: [PATCH 3/4] slash command fixes --- core/commands/index.ts | 57 ++++----------- core/commands/slash/http.ts | 4 +- .../v1/slashCommandFromPromptFile.ts | 4 +- core/promptFiles/v1/updateChatHistory.ts | 2 +- .../CodeToEditCard/CodeToEditCard.tsx | 12 +-- .../mainInput/tiptap/resolveInput.ts | 73 +++++++------------ gui/src/pages/gui/Chat.tsx | 4 +- gui/src/redux/thunks/gatherContext.ts | 3 +- gui/src/redux/thunks/streamResponse.ts | 45 +++++------- 9 files changed, 74 insertions(+), 130 deletions(-) diff --git a/core/commands/index.ts b/core/commands/index.ts index b6f2334858..7115d22bec 100644 --- a/core/commands/index.ts +++ b/core/commands/index.ts @@ -1,5 +1,6 @@ import { CustomCommand, SlashCommand, SlashCommandDescription } from "../"; import { renderTemplatedString } from "../promptFiles/v1/renderTemplatedString"; +import { replaceSlashCommandWithPromptInChatHistory } from "../promptFiles/v1/updateChatHistory"; import { renderChatMessage } from "../util/messageContent"; import SlashCommands from "./slash"; @@ -15,58 +16,26 @@ export function slashFromCustomCommand( description: customCommand.description ?? "", prompt: customCommand.prompt, run: async function* ({ input, llm, history, ide, completionOptions }) { - console.log("SLASH COMMAND RUN", input, prompt, commandName); - // Remove slash command prefix from input - let userInput = input; - if (userInput.startsWith(commandName)) { - userInput = userInput.substring(commandName.length).trimStart(); - } - // Render prompt template - let promptUserInput: string; + let renderedPrompt: string; if (customCommand.prompt.includes("{{{ input }}}")) { - promptUserInput = await renderTemplatedString( + renderedPrompt = await renderTemplatedString( customCommand.prompt, ide.readFile.bind(ide), - { input: userInput }, + { input }, ); } else { - promptUserInput = customCommand.prompt + "\n\n" + userInput; + renderedPrompt = customCommand.prompt + "\n\n" + input; } - const messages = [...history]; - // Find the last chat message with this slash command and replace it with the user input - for (let i = messages.length - 1; i >= 0; i--) { - const message = messages[i]; - const { role, content } = message; - if (role !== "user") { - continue; - } - - if ( - Array.isArray(content) && - content.some( - (part) => "text" in part && part.text?.startsWith(commandName), - ) - ) { - messages[i] = { - ...message, - content: content.map((part) => { - if ("text" in part && part.text.startsWith(commandName)) { - return { type: "text", text: promptUserInput }; - } - return part; - }), - }; - break; - } else if ( - typeof content === "string" && - content.startsWith(commandName) - ) { - messages[i] = { ...message, content: promptUserInput }; - break; - } - } + // Replaces slash command messages with the rendered prompt + // which INCLUDES the input + const messages = replaceSlashCommandWithPromptInChatHistory( + history, + commandName, + renderedPrompt, + undefined, + ); for await (const chunk of llm.streamChat( messages, diff --git a/core/commands/slash/http.ts b/core/commands/slash/http.ts index 7a662561c5..1c0d386265 100644 --- a/core/commands/slash/http.ts +++ b/core/commands/slash/http.ts @@ -1,11 +1,11 @@ import { SlashCommand } from "../../index.js"; -import { removeQuotesAndEscapes } from "../../util/index.js"; import { streamResponse } from "../../llm/stream.js"; +import { removeQuotesAndEscapes } from "../../util/index.js"; const HttpSlashCommand: SlashCommand = { name: "http", description: "Call an HTTP endpoint to serve response", - run: async function* ({ ide, llm, input, params, fetch }) { + run: async function* ({ input, params, fetch }) { const url = params?.url; if (!url) { throw new Error("URL is not defined in params"); diff --git a/core/promptFiles/v1/slashCommandFromPromptFile.ts b/core/promptFiles/v1/slashCommandFromPromptFile.ts index b9d4e88c86..6abd4927fd 100644 --- a/core/promptFiles/v1/slashCommandFromPromptFile.ts +++ b/core/promptFiles/v1/slashCommandFromPromptFile.ts @@ -5,7 +5,7 @@ import { parsePromptFileV1V2 } from "../v2/parsePromptFileV1V2"; import { getContextProviderHelpers } from "./getContextProviderHelpers"; import { renderTemplatedString } from "./renderTemplatedString"; -import { updateChatHistory } from "./updateChatHistory"; +import { replaceSlashCommandWithPromptInChatHistory } from "./updateChatHistory"; export function extractName(preamble: { name?: string }, path: string): string { return preamble.name ?? getLastNPathParts(path, 1).split(".prompt")[0]; @@ -68,7 +68,7 @@ export function slashCommandFromPromptFileV1( const userInput = extractUserInput(context.input, name); const renderedPrompt = await renderPromptV1(prompt, context, userInput); - const messages = updateChatHistory( + const messages = replaceSlashCommandWithPromptInChatHistory( context.history, name, renderedPrompt, diff --git a/core/promptFiles/v1/updateChatHistory.ts b/core/promptFiles/v1/updateChatHistory.ts index 8ebc6f0387..04cbcd91bf 100644 --- a/core/promptFiles/v1/updateChatHistory.ts +++ b/core/promptFiles/v1/updateChatHistory.ts @@ -1,4 +1,4 @@ -export function updateChatHistory( +export function replaceSlashCommandWithPromptInChatHistory( history: any[], commandName: string, renderedPrompt: string, diff --git a/gui/src/components/CodeToEditCard/CodeToEditCard.tsx b/gui/src/components/CodeToEditCard/CodeToEditCard.tsx index 99149ba5fc..7727773377 100644 --- a/gui/src/components/CodeToEditCard/CodeToEditCard.tsx +++ b/gui/src/components/CodeToEditCard/CodeToEditCard.tsx @@ -1,16 +1,16 @@ +import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline"; +import type { CodeToEdit } from "core"; import { useContext, useState } from "react"; import { useDispatch } from "react-redux"; import { IdeMessengerContext } from "../../context/IdeMessenger"; -import CodeToEditListItem from "./CodeToEditListItem"; -import type { CodeToEdit } from "core"; -import AddFileButton from "./AddFileButton"; -import AddFileCombobox from "./AddFileCombobox"; -import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { useAppSelector } from "../../redux/hooks"; import { addCodeToEdit, removeCodeToEdit, } from "../../redux/slices/sessionSlice"; +import AddFileButton from "./AddFileButton"; +import AddFileCombobox from "./AddFileCombobox"; +import CodeToEditListItem from "./CodeToEditListItem"; export default function CodeToEditCard() { const dispatch = useDispatch(); @@ -56,7 +56,7 @@ export default function CodeToEditCard() { return (
-
+
{title} setShowAddFileCombobox(true)} />
diff --git a/gui/src/components/mainInput/tiptap/resolveInput.ts b/gui/src/components/mainInput/tiptap/resolveInput.ts index 427edead14..76a8681154 100644 --- a/gui/src/components/mainInput/tiptap/resolveInput.ts +++ b/gui/src/components/mainInput/tiptap/resolveInput.ts @@ -11,7 +11,7 @@ import { TextMessagePart, } from "core"; import { ctxItemToRifWithContents } from "core/commands/util"; -import { stripImages } from "core/util/messageContent"; +import { renderChatMessage, stripImages } from "core/util/messageContent"; import { getUriFileExtension } from "core/util/uri"; import { IIdeMessenger } from "../../../context/IdeMessenger"; import { setIsGatheringContext } from "../../../redux/slices/sessionSlice"; @@ -52,7 +52,7 @@ async function resolveEditorContent({ MessageContent, ( | { - name: string; + command: SlashCommandDescription; input: string; } | undefined @@ -63,8 +63,9 @@ async function resolveEditorContent({ let contextItemAttrs: MentionAttrs[] = []; const selectedCode: RangeInFile[] = []; let slashCommandName: string | undefined = undefined; - let slashCommandWithInput: { name: string; input: string } | undefined = - undefined; + let slashCommandWithInput: + | { command: SlashCommandDescription; input: string } + | undefined = undefined; if (editorState?.content) { for (const p of editorState.content) { if (p.type === "paragraph") { @@ -129,56 +130,34 @@ async function resolveEditorContent({ } } - // Add slash command text back in (this could be removed) if (slashCommandName) { const command = availableSlashCommands.find( - (c) => c.name === slashCommandWithName, + (c) => c.name === slashCommandName, ); if (command) { - slashCommandWithInput = { - name: command.name, - input: "TODO INPUT", - }; - } - } - const getSlashCommandForInput = ( - input: MessageContent, - slashCommands: SlashCommandDescription[], - ): [SlashCommandDescription, string] | undefined => { - let slashCommand: SlashCommandDescription | undefined; - let slashCommandName: string | undefined; - - let lastText = - typeof input === "string" - ? input - : ( - input.filter((part) => part.type === "text").slice(-1)[0] as - | TextMessagePart - | undefined - )?.text || ""; - - if (lastText.startsWith("/")) { - slashCommandName = lastText.split(" ")[0].substring(1); - slashCommand = slashCommands.find((command) => - lastText.startsWith(command), + const lastTextIndex = findLastIndex( + parts, + (part) => part.type === "text", ); - } - if (!slashCommand || !slashCommandName) { - return undefined; - } + const lastTextPart = parts[lastTextIndex] as TextMessagePart; - // Convert to actual slash command object with runnable function - return [slashCommand, renderChatMessage({ role: "user", content: input })]; - }; + let input: string; + // Get input and add text of last slash command text back in to last text node + if (lastTextPart) { + input = renderChatMessage({ + role: "user", + content: lastTextPart.text, + }).trimStart(); + lastTextPart.text = `/${command.name} ${lastTextPart.text}`; + } else { + input = ""; + parts.push({ type: "text", text: `/${command.name}` }); + } - if (slashCommand) { - let lastTextIndex = findLastIndex(parts, (part) => part.type === "text"); - const lastTextPart = parts[lastTextIndex] as TextMessagePart; - const lastPart = `/${slashCommand} ${lastTextPart?.text || ""}`; - if (parts.length > 0) { - lastTextPart.text = lastPart; - } else { - parts = [{ type: "text", text: lastPart }]; + slashCommandWithInput = { + command, + input, + }; } } diff --git a/gui/src/pages/gui/Chat.tsx b/gui/src/pages/gui/Chat.tsx index 05c1f2c47f..aea4bdadbb 100644 --- a/gui/src/pages/gui/Chat.tsx +++ b/gui/src/pages/gui/Chat.tsx @@ -235,7 +235,8 @@ export function Chat() { console.error("No selected chat model"); return; } - const [contextItems, __, userInstructions] = await resolveEditorContent({ + + const [contextItems, __, userInstructions, _] = await resolveEditorContent({ editorState, modifiers: { noContext: true, @@ -243,6 +244,7 @@ export function Chat() { }, ideMessenger, defaultContextProviders: [], + availableSlashCommands: [], dispatch, selectedModelTitle: defaultModel.title, }); diff --git a/gui/src/redux/thunks/gatherContext.ts b/gui/src/redux/thunks/gatherContext.ts index 5386881f7c..1c38e965b9 100644 --- a/gui/src/redux/thunks/gatherContext.ts +++ b/gui/src/redux/thunks/gatherContext.ts @@ -5,6 +5,7 @@ import { InputModifiers, MessageContent, RangeInFile, + SlashCommandDescription, } from "core"; import * as URI from "uri-js"; import resolveEditorContent from "../../components/mainInput/tiptap/resolveInput"; @@ -18,7 +19,7 @@ export const gatherContext = createAsyncThunk< content: MessageContent; slashCommandWithInput: | { - name: string; + command: SlashCommandDescription; input: string; } | undefined; diff --git a/gui/src/redux/thunks/streamResponse.ts b/gui/src/redux/thunks/streamResponse.ts index cfdb5a9d27..9b802ffbbe 100644 --- a/gui/src/redux/thunks/streamResponse.ts +++ b/gui/src/redux/thunks/streamResponse.ts @@ -94,36 +94,29 @@ export const streamResponseThunk = createAsyncThunk< }); posthog.capture("userInput", {}); - // Determine if the input is a slash command - let commandAndInput = getSlashCommandForInput(content, slashCommands); - - if (!commandAndInput) { - unwrapResult(await dispatch(streamNormalInput({ messages }))); - } else { - const [slashCommand, commandInput] = commandAndInput; - + if (slashCommandWithInput) { posthog.capture("step run", { - step_name: slashCommand.name, + step_name: slashCommandWithInput.command.name, params: {}, }); - - // TODO - handle non-legacy slash commands, update messages if relevant - // Pass around isFromConfigTs - unwrapResult( - await dispatch( - streamNormalInput({ - messages, - legacySlashCommandData: { - command: slashCommand, - contextItems: selectedContextItems, - historyIndex: inputIndex, - input: commandInput, - selectedCode, - }, - }), - ), - ); } + + unwrapResult( + await dispatch( + streamNormalInput({ + messages, + legacySlashCommandData: slashCommandWithInput + ? { + command: slashCommandWithInput.command, + contextItems: selectedContextItems, + historyIndex: inputIndex, + input: slashCommandWithInput.input, + selectedCode, + } + : undefined, + }), + ), + ); }), ); }, From 043402afc2b3644efff9c79fc6ffbe25570a6372 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Thu, 27 Mar 2025 15:14:23 -0700 Subject: [PATCH 4/4] reduce file changes --- core/commands/slash/http.ts | 4 ++-- gui/src/components/CodeToEditCard/CodeToEditCard.tsx | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/core/commands/slash/http.ts b/core/commands/slash/http.ts index 1c0d386265..7a662561c5 100644 --- a/core/commands/slash/http.ts +++ b/core/commands/slash/http.ts @@ -1,11 +1,11 @@ import { SlashCommand } from "../../index.js"; -import { streamResponse } from "../../llm/stream.js"; import { removeQuotesAndEscapes } from "../../util/index.js"; +import { streamResponse } from "../../llm/stream.js"; const HttpSlashCommand: SlashCommand = { name: "http", description: "Call an HTTP endpoint to serve response", - run: async function* ({ input, params, fetch }) { + run: async function* ({ ide, llm, input, params, fetch }) { const url = params?.url; if (!url) { throw new Error("URL is not defined in params"); diff --git a/gui/src/components/CodeToEditCard/CodeToEditCard.tsx b/gui/src/components/CodeToEditCard/CodeToEditCard.tsx index 7727773377..99149ba5fc 100644 --- a/gui/src/components/CodeToEditCard/CodeToEditCard.tsx +++ b/gui/src/components/CodeToEditCard/CodeToEditCard.tsx @@ -1,16 +1,16 @@ -import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline"; -import type { CodeToEdit } from "core"; import { useContext, useState } from "react"; import { useDispatch } from "react-redux"; import { IdeMessengerContext } from "../../context/IdeMessenger"; +import CodeToEditListItem from "./CodeToEditListItem"; +import type { CodeToEdit } from "core"; +import AddFileButton from "./AddFileButton"; +import AddFileCombobox from "./AddFileCombobox"; +import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { useAppSelector } from "../../redux/hooks"; import { addCodeToEdit, removeCodeToEdit, } from "../../redux/slices/sessionSlice"; -import AddFileButton from "./AddFileButton"; -import AddFileCombobox from "./AddFileCombobox"; -import CodeToEditListItem from "./CodeToEditListItem"; export default function CodeToEditCard() { const dispatch = useDispatch(); @@ -56,7 +56,7 @@ export default function CodeToEditCard() { return (
-
+
{title} setShowAddFileCombobox(true)} />