diff --git a/.roo/commands/release.md b/.roo/commands/release.md new file mode 100644 index 00000000000..9f38080ba9d --- /dev/null +++ b/.roo/commands/release.md @@ -0,0 +1,38 @@ +--- +description: "Create a new release of the Roo Code extension" +argument-hint: patch | minor | major +--- + +1. Identify the SHA corresponding to the most recent release using GitHub CLI: `gh release view --json tagName,targetCommitish,publishedAt` +2. Analyze changes since the last release using: `gh pr list --state merged --json number,title,author,url,mergedAt,closingIssuesReferences --limit 1000 -q '[.[] | select(.mergedAt > "TIMESTAMP") | {number, title, author: .author.login, url, mergedAt, issues: .closingIssuesReferences}] | sort_by(.number)'` +3. For each PR with linked issues, fetch the issue details to get the issue reporter: `gh issue view ISSUE_NUMBER --json number,author -q '{number, reporter: .author.login}'` +4. Summarize the changes. If the user did not specify, ask them whether this should be a major, minor, or patch release. +5. Create a changeset in .changeset/v[version].md instead of directly modifying package.json. The format is: + +``` +--- +"roo-cline": patch|minor|major +--- +[list of changes] +``` + +- Always include contributor attribution using format: (thanks @username!) +- For PRs that close issues, also include the issue number and reporter: "- Fix: Description (#123 by @reporter, PR by @contributor)" +- For PRs without linked issues, use the standard format: "- Add support for feature (thanks @contributor!)" +- Provide brief descriptions of each item to explain the change +- Order the list from most important to least important +- Example formats: + - With issue: "- Fix: Resolve memory leak in extension (#456 by @issueReporter, PR by @prAuthor)" + - Without issue: "- Add support for Gemini 2.5 Pro caching (thanks @contributor!)" +- CRITICAL: Include EVERY SINGLE PR in the changeset - don't assume you know which ones are important. Count the total PRs to verify completeness and cross-reference the list to ensure nothing is missed. + +6. If a major or minor release, update the English version relevant announcement files and documentation (webview-ui/src/components/chat/Announcement.tsx, README.md, and the `latestAnnouncementId` in src/core/webview/ClineProvider.ts) +7. Ask the user to confirm the English version +8. Use the new_task tool to create a subtask in `translate` mode with detailed instructions of which content needs to be translated into all supported languages +9. Create a new branch for the release preparation: `git checkout -b release/v[version]` +10. Commit and push the changeset file and any documentation updates to the repository: `git add . && git commit -m "chore: add changeset for v[version]" && git push origin release/v[version]` +11. Create a pull request for the release: `gh pr create --title "Release v[version]" --body "Release preparation for v[version]. This PR includes the changeset and any necessary documentation updates." --base main --head release/v[version]` +12. The GitHub Actions workflow will automatically: + - Create a version bump PR when changesets are merged to main + - Update the CHANGELOG.md with proper formatting + - Publish the release when the version bump PR is merged diff --git a/.roomodes b/.roomodes index ef42c867ecc..46229bd2696 100644 --- a/.roomodes +++ b/.roomodes @@ -41,42 +41,6 @@ customModes: - mcp customInstructions: Focus on UI refinement, component creation, and adherence to design best-practices. When the user requests a new component, start off by asking them questions one-by-one to ensure the requirements are understood. Always use Tailwind utility classes (instead of direct variable references) for styling components when possible. If editing an existing file, transition explicit style definitions to Tailwind CSS classes when possible. Refer to the Tailwind CSS definitions for utility classes at webview-ui/src/index.css. Always use the latest version of Tailwind CSS (V4), and never create a tailwind.config.js file. Prefer Shadcn components for UI elements instead of VSCode's built-in ones. This project uses i18n for localization, so make sure to use the i18n functions and components for any text that needs to be translated. Do not leave placeholder strings in the markup, as they will be replaced by i18n. Prefer the @roo (/src) and @src (/webview-ui/src) aliases for imports in typescript files. Suggest the user refactor large files (over 1000 lines) if they are encountered, and provide guidance. Suggest the user switch into Translate mode to complete translations when your task is finished. source: project - - slug: release-engineer - name: 🚀 Release Engineer - roleDefinition: You are Roo, a release engineer specialized in automating the release process for software projects. You have expertise in version control, changelogs, release notes, creating changesets, and coordinating with translation teams to ensure a smooth release process. - whenToUse: Automate the release process for software projects. - description: Automate the release process. - customInstructions: |- - When preparing a release: - 1. Identify the SHA corresponding to the most recent release using GitHub CLI: `gh release view --json tagName,targetCommitish,publishedAt` - 2. Analyze changes since the last release using: `gh pr list --state merged --json number,title,author,url,mergedAt,closingIssuesReferences --limit 1000 -q '[.[] | select(.mergedAt > "TIMESTAMP") | {number, title, author: .author.login, url, mergedAt, issues: .closingIssuesReferences}] | sort_by(.number)'` - 3. For each PR with linked issues, fetch the issue details to get the issue reporter: `gh issue view ISSUE_NUMBER --json number,author -q '{number, reporter: .author.login}'` - 4. Summarize the changes and ask the user whether this should be a major, minor, or patch release - 5. Create a changeset in .changeset/v[version].md instead of directly modifying package.json. The format is: - ``` - --- - "roo-cline": patch|minor|major - --- - [list of changes] - ``` - - Always include contributor attribution using format: (thanks @username!) - For PRs that close issues, also include the issue number and reporter: "- Fix: Description (#123 by @reporter, PR by @contributor)" - For PRs without linked issues, use the standard format: "- Add support for feature (thanks @contributor!)" - Provide brief descriptions of each item to explain the change - Order the list from most important to least important - Example formats: - - With issue: "- Fix: Resolve memory leak in extension (#456 by @issueReporter, PR by @prAuthor)" - - Without issue: "- Add support for Gemini 2.5 Pro caching (thanks @contributor!)" - - CRITICAL: Include EVERY SINGLE PR in the changeset - don't assume you know which ones are important. Count the total PRs to verify completeness and cross-reference the list to ensure nothing is missed. - 6. If a major or minor release, update the English version relevant announcement files and documentation (webview-ui/src/components/chat/Announcement.tsx, README.md, and the `latestAnnouncementId` in src/core/webview/ClineProvider.ts) - 7. Ask the user to confirm the English version - 8. Use the new_task tool to create a subtask in `translate` mode with detailed instructions of which content needs to be translated into all supported languages - 9. Create a new branch for the release preparation: `git checkout -b release/v[version]` - 10. Commit and push the changeset file and any documentation updates to the repository: `git add . && git commit -m "chore: add changeset for v[version]" && git push origin release/v[version]` 11. Create a pull request for the release: `gh pr create --title "Release v[version]" --body "Release preparation for v[version]. This PR includes the changeset and any necessary documentation updates." --base main --head release/v[version]` 12. The GitHub Actions workflow will automatically: - - Create a version bump PR when changesets are merged to main - - Update the CHANGELOG.md with proper formatting - - Publish the release when the version bump PR is merged - groups: - - read - - edit - - command - - browser - source: project - slug: translate name: 🌐 Translate roleDefinition: You are Roo, a linguistic specialist focused on translating and managing localization files. Your responsibility is to help maintain and update translation files for the application, ensuring consistency and accuracy across all language resources. diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 8269f83d2da..763e1181257 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2375,6 +2375,7 @@ export const webviewMessageHandler = async ( source: command.source, filePath: command.filePath, description: command.description, + argumentHint: command.argumentHint, })) await provider.postMessageToWebview({ diff --git a/src/services/command/__tests__/frontmatter-commands.spec.ts b/src/services/command/__tests__/frontmatter-commands.spec.ts index e40f351003f..1171a4b24cb 100644 --- a/src/services/command/__tests__/frontmatter-commands.spec.ts +++ b/src/services/command/__tests__/frontmatter-commands.spec.ts @@ -43,6 +43,7 @@ npm run build source: "project", filePath: path.join("/test/cwd", ".roo", "commands", "setup.md"), description: "Sets up the development environment", + argumentHint: undefined, }) }) @@ -66,6 +67,7 @@ npm run build source: "project", filePath: path.join("/test/cwd", ".roo", "commands", "setup.md"), description: undefined, + argumentHint: undefined, }) }) @@ -108,6 +110,7 @@ Command content here.` source: "project", filePath: path.join("/test/cwd", ".roo", "commands", "setup.md"), description: undefined, + argumentHint: undefined, }) }) @@ -142,6 +145,7 @@ Global setup instructions.` source: "project", filePath: path.join("/test/cwd", ".roo", "commands", "setup.md"), description: "Project-specific setup", + argumentHint: undefined, }) }) @@ -168,10 +172,118 @@ Global setup instructions.` source: "global", filePath: expect.stringContaining(path.join(".roo", "commands", "setup.md")), description: "Global setup command", + argumentHint: undefined, }) }) }) + describe("argument-hint functionality", () => { + it("should load command with argument-hint from frontmatter", async () => { + const commandContent = `--- +description: Create a new release of the Roo Code extension +argument-hint: patch | minor | major +--- + +# Release Command + +Create a new release.` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readFile = vi.fn().mockResolvedValue(commandContent) + + const result = await getCommand("/test/cwd", "release") + + expect(result).toEqual({ + name: "release", + content: "# Release Command\n\nCreate a new release.", + source: "project", + filePath: path.join("/test/cwd", ".roo", "commands", "release.md"), + description: "Create a new release of the Roo Code extension", + argumentHint: "patch | minor | major", + }) + }) + + it("should handle command with both description and argument-hint", async () => { + const commandContent = `--- +description: Deploy application to environment +argument-hint: staging | production +author: DevOps Team +--- + +# Deploy Command + +Deploy the application.` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readFile = vi.fn().mockResolvedValue(commandContent) + + const result = await getCommand("/test/cwd", "deploy") + + expect(result).toEqual({ + name: "deploy", + content: "# Deploy Command\n\nDeploy the application.", + source: "project", + filePath: path.join("/test/cwd", ".roo", "commands", "deploy.md"), + description: "Deploy application to environment", + argumentHint: "staging | production", + }) + }) + + it("should handle empty argument-hint in frontmatter", async () => { + const commandContent = `--- +description: Test command +argument-hint: "" +--- + +# Test Command + +Test content.` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readFile = vi.fn().mockResolvedValue(commandContent) + + const result = await getCommand("/test/cwd", "test") + + expect(result?.argumentHint).toBeUndefined() + }) + + it("should handle whitespace-only argument-hint in frontmatter", async () => { + const commandContent = `--- +description: Test command +argument-hint: " " +--- + +# Test Command + +Test content.` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readFile = vi.fn().mockResolvedValue(commandContent) + + const result = await getCommand("/test/cwd", "test") + + expect(result?.argumentHint).toBeUndefined() + }) + + it("should handle non-string argument-hint in frontmatter", async () => { + const commandContent = `--- +description: Test command +argument-hint: 123 +--- + +# Test Command + +Test content.` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readFile = vi.fn().mockResolvedValue(commandContent) + + const result = await getCommand("/test/cwd", "test") + + expect(result?.argumentHint).toBeUndefined() + }) + }) + describe("getCommands with frontmatter", () => { it("should load multiple commands with descriptions", async () => { const setupContent = `--- @@ -215,14 +327,62 @@ Build instructions without frontmatter.` expect.objectContaining({ name: "setup", description: "Sets up the development environment", + argumentHint: undefined, }), expect.objectContaining({ name: "deploy", description: "Deploys the application to production", + argumentHint: undefined, }), expect.objectContaining({ name: "build", description: undefined, + argumentHint: undefined, + }), + ]), + ) + }) + + it("should load multiple commands with argument hints", async () => { + const releaseContent = `--- +description: Create a new release +argument-hint: patch | minor | major +--- + +# Release Command + +Create a release.` + + const deployContent = `--- +description: Deploy to environment +argument-hint: staging | production +--- + +# Deploy Command + +Deploy the app.` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readdir = vi.fn().mockResolvedValue([ + { name: "release.md", isFile: () => true }, + { name: "deploy.md", isFile: () => true }, + ]) + mockFs.readFile = vi.fn().mockResolvedValueOnce(releaseContent).mockResolvedValueOnce(deployContent) + + const result = await getCommands("/test/cwd") + + expect(result).toHaveLength(2) + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: "release", + description: "Create a new release", + argumentHint: "patch | minor | major", + }), + expect.objectContaining({ + name: "deploy", + description: "Deploy to environment", + argumentHint: "staging | production", }), ]), ) diff --git a/src/services/command/commands.ts b/src/services/command/commands.ts index 00549675c03..452269511cf 100644 --- a/src/services/command/commands.ts +++ b/src/services/command/commands.ts @@ -9,6 +9,7 @@ export interface Command { source: "global" | "project" filePath: string description?: string + argumentHint?: string } /** @@ -70,6 +71,7 @@ async function tryLoadCommand( let parsed let description: string | undefined + let argumentHint: string | undefined let commandContent: string try { @@ -79,10 +81,15 @@ async function tryLoadCommand( typeof parsed.data.description === "string" && parsed.data.description.trim() ? parsed.data.description.trim() : undefined + argumentHint = + typeof parsed.data["argument-hint"] === "string" && parsed.data["argument-hint"].trim() + ? parsed.data["argument-hint"].trim() + : undefined commandContent = parsed.content.trim() } catch (frontmatterError) { // If frontmatter parsing fails, treat the entire content as command content description = undefined + argumentHint = undefined commandContent = content.trim() } @@ -92,6 +99,7 @@ async function tryLoadCommand( source, filePath, description, + argumentHint, } } catch (error) { // File doesn't exist or can't be read @@ -137,6 +145,7 @@ async function scanCommandDirectory( let parsed let description: string | undefined + let argumentHint: string | undefined let commandContent: string try { @@ -146,10 +155,15 @@ async function scanCommandDirectory( typeof parsed.data.description === "string" && parsed.data.description.trim() ? parsed.data.description.trim() : undefined + argumentHint = + typeof parsed.data["argument-hint"] === "string" && parsed.data["argument-hint"].trim() + ? parsed.data["argument-hint"].trim() + : undefined commandContent = parsed.content.trim() } catch (frontmatterError) { // If frontmatter parsing fails, treat the entire content as command content description = undefined + argumentHint = undefined commandContent = content.trim() } @@ -161,6 +175,7 @@ async function scanCommandDirectory( source, filePath, description, + argumentHint, }) } } catch (error) { diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 963305a561b..5320190a7b9 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -25,6 +25,7 @@ export interface Command { source: "global" | "project" filePath?: string description?: string + argumentHint?: string } // Type for marketplace installed metadata diff --git a/webview-ui/src/__tests__/command-autocomplete.spec.ts b/webview-ui/src/__tests__/command-autocomplete.spec.ts index 154b6c614ad..be87f3586b3 100644 --- a/webview-ui/src/__tests__/command-autocomplete.spec.ts +++ b/webview-ui/src/__tests__/command-autocomplete.spec.ts @@ -9,6 +9,7 @@ describe("Command Autocomplete", () => { { name: "deploy", source: "global" }, { name: "test-suite", source: "project" }, { name: "cleanup_old", source: "global" }, + { name: "release", source: "project", argumentHint: "patch | minor | major" }, ] const mockQueryItems = [ @@ -20,12 +21,12 @@ describe("Command Autocomplete", () => { it('should return all commands when query is just "/"', () => { const options = getContextMenuOptions("/", null, mockQueryItems, [], [], mockCommands) - // Should have 6 items: 1 section header + 5 commands - expect(options).toHaveLength(6) + // Should have 7 items: 1 section header + 6 commands + expect(options).toHaveLength(7) // Filter out section headers to check commands const commandOptions = options.filter((option) => option.type === ContextMenuOptionType.Command) - expect(commandOptions).toHaveLength(5) + expect(commandOptions).toHaveLength(6) const commandNames = commandOptions.map((option) => option.value) expect(commandNames).toContain("setup") @@ -33,6 +34,7 @@ describe("Command Autocomplete", () => { expect(commandNames).toContain("deploy") expect(commandNames).toContain("test-suite") expect(commandNames).toContain("cleanup_old") + expect(commandNames).toContain("release") }) it("should filter commands based on fuzzy search", () => { @@ -148,7 +150,7 @@ describe("Command Autocomplete", () => { const commandOptions = options.filter((option) => option.type === ContextMenuOptionType.Command) expect(modeOptions.length).toBe(2) - expect(commandOptions.length).toBe(5) + expect(commandOptions.length).toBe(6) }) it("should filter both modes and commands based on query", () => { @@ -181,6 +183,42 @@ describe("Command Autocomplete", () => { }) }) + describe("argument hint functionality", () => { + it("should include argumentHint in command options when present", () => { + const options = getContextMenuOptions("/release", null, mockQueryItems, [], [], mockCommands) + + const releaseOption = options.find((option) => option.value === "release") + expect(releaseOption).toBeDefined() + expect(releaseOption!.argumentHint).toBe("patch | minor | major") + }) + + it("should handle commands without argumentHint", () => { + const options = getContextMenuOptions("/setup", null, mockQueryItems, [], [], mockCommands) + + const setupOption = options.find((option) => option.value === "setup") + expect(setupOption).toBeDefined() + expect(setupOption!.argumentHint).toBeUndefined() + }) + + it("should preserve argumentHint through fuzzy search", () => { + const options = getContextMenuOptions("/rel", null, mockQueryItems, [], [], mockCommands) + + const releaseOption = options.find((option) => option.value === "release") + expect(releaseOption).toBeDefined() + expect(releaseOption!.argumentHint).toBe("patch | minor | major") + }) + + it("should handle commands with empty argumentHint", () => { + const commandsWithEmptyHint: Command[] = [{ name: "test-command", source: "project", argumentHint: "" }] + + const options = getContextMenuOptions("/test", null, mockQueryItems, [], [], commandsWithEmptyHint) + + const testOption = options.find((option) => option.value === "test-command") + expect(testOption).toBeDefined() + expect(testOption!.argumentHint).toBe("") + }) + }) + describe("edge cases", () => { it("should handle undefined commands gracefully", () => { const options = getContextMenuOptions("/setup", null, mockQueryItems, [], [], undefined) diff --git a/webview-ui/src/components/chat/ContextMenu.tsx b/webview-ui/src/components/chat/ContextMenu.tsx index d240d8600bf..86965fcb117 100644 --- a/webview-ui/src/components/chat/ContextMenu.tsx +++ b/webview-ui/src/components/chat/ContextMenu.tsx @@ -107,8 +107,18 @@ const ContextMenu: React.FC = ({ case ContextMenuOptionType.Command: return (
-
+
{option.slashCommand} + {option.argumentHint && ( + + {option.argumentHint} + + )}
{option.description && ( ({ type: ContextMenuOptionType.Command, value: command.name, slashCommand: `/${command.name}`, description: command.description, + argumentHint: command.argumentHint, })) if (matchingCommands.length > 0) {