Skip to content
Open
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
2 changes: 1 addition & 1 deletion extensions/cli/src/__mocks__/commands/commands.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { vi } from "vitest";

export const getAllSlashCommands = vi.fn(() => [
export const getAllSlashCommands = vi.fn(async () => [
{ name: "help", description: "Show help", category: "system" },
{ name: "login", description: "Login to Continue", category: "system" },
]);
20 changes: 10 additions & 10 deletions extensions/cli/src/commands/commands.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ describe("Slash Commands Integration", () => {
};

describe("System Commands Registration", () => {
it("should include all system commands in the commands list", () => {
const commands = getAllSlashCommands(mockAssistant);
it("should include all system commands in the commands list", async () => {
const commands = await getAllSlashCommands(mockAssistant);
const commandNames = commands.map((cmd) => cmd.name);

// Check that system commands are present (mode commands have been removed)
Expand All @@ -33,15 +33,15 @@ describe("Slash Commands Integration", () => {
expect(commandNames).toContain("config");
});

it("should include assistant prompt commands", () => {
const commands = getAllSlashCommands(mockAssistant);
it("should include assistant prompt commands", async () => {
const commands = await getAllSlashCommands(mockAssistant);
const commandNames = commands.map((cmd) => cmd.name);

expect(commandNames).toContain("test-prompt");
});

it("should categorize system commands correctly", () => {
const commands = getAllSlashCommands(mockAssistant);
it("should categorize system commands correctly", async () => {
const commands = await getAllSlashCommands(mockAssistant);
const systemCommands = commands.filter((cmd) =>
[
"help",
Expand All @@ -60,8 +60,8 @@ describe("Slash Commands Integration", () => {
});
});

it("should categorize assistant commands correctly", () => {
const commands = getAllSlashCommands(mockAssistant);
it("should categorize assistant commands correctly", async () => {
const commands = await getAllSlashCommands(mockAssistant);
const assistantCommands = commands.filter(
(cmd) => cmd.name === "test-prompt",
);
Expand All @@ -71,8 +71,8 @@ describe("Slash Commands Integration", () => {
});
});

it("should only show remote mode commands in remote mode", () => {
const commands = getAllSlashCommands(mockAssistant, {
it("should only show remote mode commands in remote mode", async () => {
const commands = await getAllSlashCommands(mockAssistant, {
isRemoteMode: true,
});
const commandNames = commands.map((cmd) => cmd.name);
Expand Down
42 changes: 39 additions & 3 deletions extensions/cli/src/commands/commands.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { type AssistantConfig } from "@continuedev/sdk";

import {
getSkillSlashCommandName,
loadMarkdownSkills,
} from "../util/loadMarkdownSkills.js";

// Export command functions
export { chat } from "./chat.js";
export { login } from "./login.js";
Expand Down Expand Up @@ -106,6 +111,16 @@ export const SYSTEM_SLASH_COMMANDS: SystemCommand[] = [
description: "List background jobs",
category: "system",
},
{
name: "skills",
description: "List all available skills",
category: "system",
},
{
name: "import-skill",
description: "Import a skill from a URL or name into ~/.continue/skills",
category: "system",
},
];

// Remote mode specific commands
Expand All @@ -130,10 +145,10 @@ export const REMOTE_MODE_SLASH_COMMANDS: SlashCommand[] = [
/**
* Get all available slash commands including system commands and assistant prompts
*/
export function getAllSlashCommands(
export async function getAllSlashCommands(
assistant: AssistantConfig,
options: { isRemoteMode?: boolean } = {},
): SlashCommand[] {
): Promise<SlashCommand[]> {
const { isRemoteMode = false } = options;

// In remote mode, only show the exit command
Expand All @@ -155,7 +170,15 @@ export function getAllSlashCommands(
// Get invokable rule commands
const invokableRuleCommands = getInvokableRuleSlashCommands(assistant);

return [...systemCommands, ...assistantCommands, ...invokableRuleCommands];
// Get skill commands
const skillCommands = await getSkillSlashCommands();

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

/**
Expand Down Expand Up @@ -202,3 +225,16 @@ export function getInvokableRuleSlashCommands(
};
});
}

/**
* Get skill-based slash commands from Markdown skills
*/
export async function getSkillSlashCommands(): Promise<SlashCommand[]> {
const { skills } = await loadMarkdownSkills();

return skills.map((skill) => ({
name: getSkillSlashCommandName(skill),
description: skill.description,
category: "assistant" as const,
}));
}
2 changes: 2 additions & 0 deletions extensions/cli/src/permissions/defaultPolicies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function getDefaultToolPolicies(
{ tool: "AskQuestion", permission: "allow" },
{ tool: "Checklist", permission: "allow" },
{ tool: "Diff", permission: "allow" },
{ tool: "Skills", permission: "allow" },
{ tool: "Exit", permission: "allow" }, // Exit tool is generally safe (headless mode only)
{ tool: "Fetch", permission: "allow" }, // Technically not read only but edge casey to post w query params
{ tool: "List", permission: "allow" },
Expand Down Expand Up @@ -56,6 +57,7 @@ export const PLAN_MODE_POLICIES: ToolPermissionPolicy[] = [
{ tool: "Read", permission: "allow" },
{ tool: "ReportFailure", permission: "allow" },
{ tool: "Search", permission: "allow" },
{ tool: "Skills", permission: "allow" },
{ tool: "Status", permission: "allow" },
{ tool: "UploadArtifact", permission: "allow" },

Expand Down
66 changes: 65 additions & 1 deletion extensions/cli/src/slashCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ import { reloadService, SERVICE_NAMES, services } from "./services/index.js";
import { getCurrentSession, updateSessionTitle } from "./session.js";
import { posthogService } from "./telemetry/posthogService.js";
import { telemetryService } from "./telemetry/telemetryService.js";
import { buildImportSkillPrompt } from "./tools/skills.js";
import { SlashCommandResult } from "./ui/hooks/useChat.types.js";
import {
getSkillSlashCommandName,
loadMarkdownSkills,
} from "./util/loadMarkdownSkills.js";

type CommandHandler = (
args: string[],
Expand Down Expand Up @@ -173,6 +178,49 @@ function handleJobs() {
return { openJobsSelector: true };
}

async function handleSkills(): Promise<SlashCommandResult> {
const { skills } = await loadMarkdownSkills();

if (!skills.length) {
return {
exit: false,
output: chalk.yellow(
"No skills found. Add skills under .continue/skills or .claude/skills.",
),
};
}

const header = chalk.bold("Available skills:");
const lines = skills.map(
(skill) =>
`${chalk.cyan(skill.name)} - ${skill.description} ${chalk.gray(
`(${skill.path})`,
)}`,
);

return {
exit: false,
output: [header, "", ...lines].join("\n"),
};
}

async function handleImportSkill(args: string[]): Promise<SlashCommandResult> {
const query = args.join(" ").trim();

if (!query) {
return {
exit: false,
output: chalk.yellow(
"Please provide a skill URL or name. Usage: /import-skill <url-or-name>",
),
};
}

return {
newInput: buildImportSkillPrompt(query),
};
}

const commandHandlers: Record<string, CommandHandler> = {
help: handleHelp,
clear: () => {
Expand Down Expand Up @@ -208,6 +256,8 @@ const commandHandlers: Record<string, CommandHandler> = {
return { openUpdateSelector: true };
},
jobs: handleJobs,
skills: () => handleSkills(),
"import-skill": (args) => handleImportSkill(args),
};

export async function handleSlashCommands(
Expand Down Expand Up @@ -254,8 +304,22 @@ export async function handleSlashCommands(
return { newInput };
}

const { skills } = await loadMarkdownSkills();
if (skills.length) {
const normalizedCommand = command.trim().toLowerCase();
const matchingSkill = skills.find(
(skill) => getSkillSlashCommandName(skill) === normalizedCommand,
);

if (matchingSkill) {
return {
newInput: `Load the skill using the **Skills** tool and then set the **skill_name** parameter to "${matchingSkill.name}".`,
};
}
}

// Check if this command would match any available commands (same logic as UI)
const allCommands = getAllSlashCommands(assistant, {
const allCommands = await getAllSlashCommands(assistant, {
isRemoteMode: options?.isRemoteMode,
});
const hasMatches = allCommands.some((cmd) =>
Expand Down
33 changes: 33 additions & 0 deletions extensions/cli/src/tools/skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,36 @@ ${skills.map((skill) => `\nname: ${skill.name}\ndescription: ${skill.description
},
};
};

export function buildImportSkillPrompt(identifier: string): string {
return `
# Overview

The user wants to import skills.

User-provided skill identifier:
${identifier}

# Guidelines
- There can be multiple skills in a single repository.
- Use the available tools to fetch content and write files. When you are done, briefly summarize which skill you imported and where you saved it.
- Use the "AskQuestion" tool where required to clarify with the user.

# Process:

**Identifier can either be a URL or a skill name**

- If it looks like a URL (for example, it starts with http:// or https://), open that URL and inspect its contents to find the code or files that define the skill.
- If the URL is a GitHub repository, look for the skills folder. There can be multiple skills within subdirectories.
- If it looks like a skill name, you should search for the most relevant open-source skill or repository that matches the skill identifier.
- Ask questions to the user to clarify which skill they are referring to if there are multiple options in your findings.

**Create the skill files**

- The skills should be created under the directory: ~/.continue/skills/<skill-name>
- The subdirectory name should match the name of the skill directory in the fetched repository.
- The relevant files and folders along with SKILL.md should be present inside the created skill subdirectory.
- If the skill already exists, ask question to the user to clarify whether they want to update it.
- Important: Before writing any files, ask the user if they want to proceed with the import.
`;
}
52 changes: 37 additions & 15 deletions extensions/cli/src/ui/SlashCommandUI.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { type AssistantConfig } from "@continuedev/sdk";
import { Box, Text } from "ink";
import React, { useMemo } from "react";
import React, { useEffect, useState } from "react";

import { getAllSlashCommands } from "../commands/commands.js";
import {
getAllSlashCommands,
type SlashCommand,
} from "../commands/commands.js";

const MAX_DESCRIPTION_LENGTH = 80;

Expand All @@ -29,20 +32,39 @@ const SlashCommandUI: React.FC<SlashCommandUIProps> = ({
selectedIndex,
isRemoteMode = false,
}) => {
// Memoize the slash commands to prevent excessive re-renders
const allCommands = useMemo(() => {
if (assistant || isRemoteMode) {
return getAllSlashCommands(assistant || ({} as AssistantConfig), {
isRemoteMode,
});
}

const [allCommands, setAllCommands] = useState<SlashCommand[]>(
// Fallback - basic commands without assistant
return [
{ name: "help", description: "Show help message" },
{ name: "clear", description: "Clear the chat history" },
{ name: "exit", description: "Exit the chat" },
];
[
{ name: "help", description: "Show help message", category: "system" },
{
name: "clear",
description: "Clear the chat history",
category: "system",
},
{ name: "exit", description: "Exit the chat", category: "system" },
],
);

useEffect(() => {
let stale = false;

const loadCommands = async () => {
if (assistant || isRemoteMode) {
const commands = await getAllSlashCommands(
assistant || ({} as AssistantConfig),
{ isRemoteMode },
);
if (!stale) {
setAllCommands(commands);
}
}
};

void loadCommands();

return () => {
stale = true;
};
}, [isRemoteMode, assistant?.prompts, assistant?.rules]);

// Filter commands based on the current filter
Expand Down
Loading
Loading