Skip to content

Fix slash command triggering and prompt body exclusions #4865

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

Merged
merged 4 commits into from
Mar 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 17 additions & 50 deletions core/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,74 +1,41 @@
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";

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 }) {
// Remove slash command prefix from input
let userInput = input;
if (userInput.startsWith(`/${customCommand.name}`)) {
userInput = userInput
.slice(customCommand.name.length + 1, userInput.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(`/${customCommand.name}`),
)
) {
messages[i] = {
...message,
content: content.map((part) => {
if (
"text" in part &&
part.text.startsWith(`/${customCommand.name}`)
) {
return { type: "text", text: promptUserInput };
}
return part;
}),
};
break;
} else if (
typeof content === "string" &&
content.startsWith(`/${customCommand.name}`)
) {
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,
Expand Down
4 changes: 2 additions & 2 deletions core/promptFiles/v1/slashCommandFromPromptFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion core/promptFiles/v1/updateChatHistory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export function updateChatHistory(
export function replaceSlashCommandWithPromptInChatHistory(
history: any[],
commandName: string,
renderedPrompt: string,
Expand Down
4 changes: 2 additions & 2 deletions extensions/vscode/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 14 additions & 17 deletions gui/src/components/mainInput/tiptap/editorConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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: {
Expand Down

This file was deleted.

4 changes: 2 additions & 2 deletions gui/src/components/mainInput/tiptap/getSuggestion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
})
Expand All @@ -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",
Expand Down
100 changes: 56 additions & 44 deletions gui/src/components/mainInput/tiptap/resolveInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import {
MessageContent,
MessagePart,
RangeInFile,
SlashCommandDescription,
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";
Expand All @@ -27,6 +28,7 @@ interface ResolveEditorContentInput {
modifiers: InputModifiers;
ideMessenger: IIdeMessenger;
defaultContextProviders: DefaultContextProvider[];
availableSlashCommands: SlashCommandDescription[];
selectedModelTitle: string;
dispatch: Dispatch;
}
Expand All @@ -40,22 +42,37 @@ async function resolveEditorContent({
modifiers,
ideMessenger,
defaultContextProviders,
availableSlashCommands,
selectedModelTitle,
dispatch,
}: ResolveEditorContentInput): Promise<
[ContextItemWithId[], RangeInFile[], MessageContent]
[
ContextItemWithId[],
RangeInFile[],
MessageContent,
(
| {
command: SlashCommandDescription;
input: string;
}
| undefined
),
]
> {
let parts: MessagePart[] = [];
let contextItemAttrs: MentionAttrs[] = [];
const selectedCode: RangeInFile[] = [];
let slashCommand: string | undefined = undefined;
let slashCommandName: string | undefined = undefined;
let slashCommandWithInput:
| { command: SlashCommandDescription; 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);
Expand Down Expand Up @@ -113,7 +130,38 @@ async function resolveEditorContent({
}
}

const shouldGatherContext = modifiers.useCodebase || slashCommand;
if (slashCommandName) {
const command = availableSlashCommands.find(
(c) => c.name === slashCommandName,
);
if (command) {
const lastTextIndex = findLastIndex(
parts,
(part) => part.type === "text",
);
const lastTextPart = parts[lastTextIndex] as TextMessagePart;

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}` });
}

slashCommandWithInput = {
command,
input,
};
}
}

const shouldGatherContext = modifiers.useCodebase || slashCommandWithInput;
if (shouldGatherContext) {
dispatch(setIsGatheringContext(true));
}
Expand Down Expand Up @@ -179,21 +227,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<T>(
Expand Down Expand Up @@ -236,30 +274,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;
4 changes: 3 additions & 1 deletion gui/src/pages/gui/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,14 +235,16 @@ export function Chat() {
console.error("No selected chat model");
return;
}
const [contextItems, __, userInstructions] = await resolveEditorContent({

const [contextItems, __, userInstructions, _] = await resolveEditorContent({
editorState,
modifiers: {
noContext: true,
useCodebase: false,
},
ideMessenger,
defaultContextProviders: [],
availableSlashCommands: [],
dispatch,
selectedModelTitle: defaultModel.title,
});
Expand Down
2 changes: 1 addition & 1 deletion gui/src/redux/selectors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down
Loading
Loading