Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
35 changes: 34 additions & 1 deletion extensions/cli/src/commands/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,10 @@ export function getAllSlashCommands(
category: "assistant" as const,
})) || [];

return [...systemCommands, ...assistantCommands];
// Get invokable rule commands
const invokableRuleCommands = getInvokableRuleSlashCommands(assistant);

return [...systemCommands, ...assistantCommands, ...invokableRuleCommands];
}

/**
Expand All @@ -163,3 +166,33 @@ export function getAssistantSlashCommands(
})) || []
);
}

/**
* Get invokable rule commands from assistant config
*/
export function getInvokableRuleSlashCommands(
assistant: AssistantConfig,
): SlashCommand[] {
if (!assistant?.rules) {
return [];
}

return assistant.rules
.filter((rule) => {
// Handle both string rules and rule objects
if (!rule || typeof rule === "string") {
return false;
}
// Only include rules with invokable: true
return rule.invokable === true;
})
.map((rule) => {
// TypeScript now knows rule is an object with invokable: true
const ruleObj = rule as any;
return {
name: ruleObj.name || "",
description: ruleObj.description || "",
category: "assistant" as const,
Comment on lines +191 to +195
Copy link
Contributor

@babblebey babblebey Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some Concerns x Clarifying Question 🤔

I can understand that this (rules) mostly firsthand would be coming from the continue-hub, which means the name x command just might seem unique but incases where we are reading invokable from local...

  • How do we handle conflicts in same slash-command???
  • Well yes, since you cannot have 2 files with same name in local, there might not be conflict... but what happens when a rule from the continue-hub meets with an invokable one in local???
  • Think what also happens when a systemCommand, assistantCommand conflicts an invokableRuleCommand?

I'm looking forward to having invokablePromptsCommand support though (see #8080), this where I figure the different init slash command...

  • One's a system prompt in the IDE extension that "Initialize Codebase"
  • Thee other's the slash command that "creates the AGENTS.md file" in the continue-cli

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@babblebey this is not a problem (yet) because we currently do not support local commands in the CLI. That said, there is an implicit order of resolution that goes roughly like this:

  1. System commands (highest priority)
  2. Assistant prompts
  3. Invokable rules (lowest priority)

I believe it is a part of our roadmap to also add local slash commands to the CLI, and when that moment comes we may need to come up with a better resolution system.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a problem until its a problem 😉

Good to know... and I definitely look forward to the local prompt support though... It give that feel of portability of continue with projects.

};
});
}
88 changes: 88 additions & 0 deletions extensions/cli/src/slashCommands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,5 +335,93 @@ describe("slashCommands", () => {
);
expect(result?.exit).toBe(false);
});

it("should handle custom assistant prompts", async () => {
const assistantWithPrompts: AssistantUnrolled = {
...mockAssistant,
prompts: [
{
name: "explain",
description: "Explain the code",
prompt: "Please explain the following code:",
},
],
};

const result = await handleSlashCommands(
"/explain some code",
assistantWithPrompts,
);

expect(result).toBeDefined();
expect(result?.newInput).toBe("Please explain the following code:some code");
});

it("should handle invokable rules", async () => {
const assistantWithRules: AssistantUnrolled = {
...mockAssistant,
rules: [
{
name: "review",
description: "Review this code",
rule: "Perform a code review on the following:",
invokable: true,
},
],
};

const result = await handleSlashCommands(
"/review some code",
assistantWithRules,
);

expect(result).toBeDefined();
expect(result?.newInput).toBe("Perform a code review on the following: some code");
});

it("should not treat non-invokable rules as slash commands", async () => {
const assistantWithNonInvokableRule: AssistantUnrolled = {
...mockAssistant,
rules: [
{
name: "regular",
description: "A regular rule",
rule: "This is a regular rule",
invokable: false,
},
],
};

const result = await handleSlashCommands(
"/regular",
assistantWithNonInvokableRule,
);

// Should return null because the rule is not invokable
expect(result).toBeNull();
});

it("should handle invokable rules from hub (string rules ignored)", async () => {
const assistantWithMixedRules: AssistantUnrolled = {
...mockAssistant,
rules: [
"string rule",
{
name: "analyze",
description: "Analyze code patterns",
rule: "Analyze the following code for patterns:",
invokable: true,
},
],
};

const result = await handleSlashCommands(
"/analyze my code",
assistantWithMixedRules,
);

expect(result).toBeDefined();
expect(result?.newInput).toBe("Analyze the following code for patterns: my code");
});
});
});
17 changes: 17 additions & 0 deletions extensions/cli/src/slashCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,23 @@ export async function handleSlashCommands(
return { newInput };
}

// Check for invokable rules
const invokableRule = assistant.rules?.find(
(rule) => {
// Handle both string rules and rule objects
if (!rule || typeof rule === "string") {
return false;
}
const ruleObj = rule as any;
return ruleObj.invokable === true && ruleObj.name === command;
},
);
if (invokableRule) {
const ruleObj = invokableRule as any;
const newInput = ruleObj.rule + " " + args.join(" ");
return { newInput };
}

// Check if this command would match any available commands (same logic as UI)
const allCommands = getAllSlashCommands(assistant, {
isRemoteMode: options?.isRemoteMode,
Expand Down
2 changes: 1 addition & 1 deletion extensions/cli/src/ui/SlashCommandUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const SlashCommandUI: React.FC<SlashCommandUIProps> = ({
{ name: "clear", description: "Clear the chat history" },
{ name: "exit", description: "Exit the chat" },
];
}, [isRemoteMode, assistant?.prompts]);
}, [isRemoteMode, assistant?.prompts, assistant?.rules]);

// Filter commands based on the current filter
const filteredCommands = allCommands
Expand Down
Loading