Skip to content
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

Use a .kilocodemodes file #44

Merged
merged 1 commit into from
Mar 17, 2025
Merged
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
File renamed without changes.
2 changes: 1 addition & 1 deletion .vscodeignore
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ demo.gif
.gitattributes
.prettierignore
.clinerules*
.roomodes
.kilocodemodes
cline_docs/**
coverage/**

12 changes: 6 additions & 6 deletions src/core/config/CustomModesManager.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import { fileExistsAtPath } from "../../utils/fs"
import { arePathsEqual } from "../../utils/path"
import { logger } from "../../utils/logging"

const ROOMODES_FILENAME = ".roomodes"
const ROOMODES_FILENAME = ".kilocodemodes"

export class CustomModesManager {
private disposables: vscode.Disposable[] = []
@@ -149,27 +149,27 @@ export class CustomModesManager {
return
}

// Get modes from .roomodes if it exists (takes precedence)
// Get modes from .kilocodemodes if it exists (takes precedence)
const roomodesPath = await this.getWorkspaceRoomodes()
const roomodesModes = roomodesPath ? await this.loadModesFromFile(roomodesPath) : []

// Merge modes from both sources (.roomodes takes precedence)
// Merge modes from both sources (.kilocodemodes takes precedence)
const mergedModes = await this.mergeCustomModes(roomodesModes, result.data.customModes)
await this.context.globalState.update("customModes", mergedModes)
await this.onUpdate()
}
}),
)

// Watch .roomodes file if it exists
// Watch .kilocodemodes file if it exists
const roomodesPath = await this.getWorkspaceRoomodes()
if (roomodesPath) {
this.disposables.push(
vscode.workspace.onDidSaveTextDocument(async (document) => {
if (arePathsEqual(document.uri.fsPath, roomodesPath)) {
const settingsModes = await this.loadModesFromFile(settingsPath)
const roomodesModes = await this.loadModesFromFile(roomodesPath)
// .roomodes takes precedence
// .kilocodemodes takes precedence
const mergedModes = await this.mergeCustomModes(roomodesModes, settingsModes)
await this.context.globalState.update("customModes", mergedModes)
await this.onUpdate()
@@ -184,7 +184,7 @@ export class CustomModesManager {
const settingsPath = await this.getCustomModesFilePath()
const settingsModes = await this.loadModesFromFile(settingsPath)

// Get modes from .roomodes if it exists
// Get modes from .kilocodemodes if it exists
const roomodesPath = await this.getWorkspaceRoomodes()
const roomodesModes = roomodesPath ? await this.loadModesFromFile(roomodesPath) : []

26 changes: 13 additions & 13 deletions src/core/config/__tests__/CustomModesManager.test.ts
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ describe("CustomModesManager", () => {
// Use path.sep to ensure correct path separators for the current platform
const mockStoragePath = `${path.sep}mock${path.sep}settings`
const mockSettingsPath = path.join(mockStoragePath, "settings", "cline_custom_modes.json")
const mockRoomodes = `${path.sep}mock${path.sep}workspace${path.sep}.roomodes`
const mockRoomodes = `${path.sep}mock${path.sep}workspace${path.sep}.kilocodemodes`

beforeEach(() => {
mockOnUpdate = jest.fn()
@@ -56,7 +56,7 @@ describe("CustomModesManager", () => {
})

describe("getCustomModes", () => {
it("should merge modes with .roomodes taking precedence", async () => {
it("should merge modes with .kilocodemodes taking precedence", async () => {
const settingsModes = [
{ slug: "mode1", name: "Mode 1", roleDefinition: "Role 1", groups: ["read"] },
{ slug: "mode2", name: "Mode 2", roleDefinition: "Role 2", groups: ["read"] },
@@ -83,13 +83,13 @@ describe("CustomModesManager", () => {
expect(modes).toHaveLength(3)
expect(modes.map((m) => m.slug)).toEqual(["mode2", "mode3", "mode1"])

// mode2 should come from .roomodes since it takes precedence
// mode2 should come from .kilocodemodes since it takes precedence
const mode2 = modes.find((m) => m.slug === "mode2")
expect(mode2?.name).toBe("Mode 2 Override")
expect(mode2?.roleDefinition).toBe("Role 2 Override")
})

it("should handle missing .roomodes file", async () => {
it("should handle missing .kilocodemodes file", async () => {
const settingsModes = [{ slug: "mode1", name: "Mode 1", roleDefinition: "Role 1", groups: ["read"] }]

;(fileExistsAtPath as jest.Mock).mockImplementation(async (path: string) => {
@@ -108,7 +108,7 @@ describe("CustomModesManager", () => {
expect(modes[0].slug).toBe("mode1")
})

it("should handle invalid JSON in .roomodes", async () => {
it("should handle invalid JSON in .kilocodemodes", async () => {
const settingsModes = [{ slug: "mode1", name: "Mode 1", roleDefinition: "Role 1", groups: ["read"] }]

;(fs.readFile as jest.Mock).mockImplementation(async (path: string) => {
@@ -123,14 +123,14 @@ describe("CustomModesManager", () => {

const modes = await manager.getCustomModes()

// Should fall back to settings modes when .roomodes is invalid
// Should fall back to settings modes when .kilocodemodes is invalid
expect(modes).toHaveLength(1)
expect(modes[0].slug).toBe("mode1")
})
})

describe("updateCustomMode", () => {
it("should update mode in settings file while preserving .roomodes precedence", async () => {
it("should update mode in settings file while preserving .kilocodemodes precedence", async () => {
const newMode: ModeConfig = {
slug: "mode1",
name: "Updated Mode 1",
@@ -194,13 +194,13 @@ describe("CustomModesManager", () => {
}),
)

// Should update global state with merged modes where .roomodes takes precedence
// Should update global state with merged modes where .kilocodemodes takes precedence
expect(mockContext.globalState.update).toHaveBeenCalledWith(
"customModes",
expect.arrayContaining([
expect.objectContaining({
slug: "mode1",
name: "Roomodes Mode 1", // .roomodes version should take precedence
name: "Roomodes Mode 1", // .kilocodemodes version should take precedence
source: "project",
}),
]),
@@ -210,7 +210,7 @@ describe("CustomModesManager", () => {
expect(mockOnUpdate).toHaveBeenCalled()
})

it("creates .roomodes file when adding project-specific mode", async () => {
it("creates .kilocodemodes file when adding project-specific mode", async () => {
const projectMode: ModeConfig = {
slug: "project-mode",
name: "Project Mode",
@@ -219,7 +219,7 @@ describe("CustomModesManager", () => {
source: "project",
}

// Mock .roomodes to not exist initially
// Mock .kilocodemodes to not exist initially
let roomodesContent: any = null
;(fileExistsAtPath as jest.Mock).mockImplementation(async (path: string) => {
return path === mockSettingsPath
@@ -245,7 +245,7 @@ describe("CustomModesManager", () => {

await manager.updateCustomMode("project-mode", projectMode)

// Verify .roomodes was created with the project mode
// Verify .kilocodemodes was created with the project mode
expect(fs.writeFile).toHaveBeenCalledWith(
expect.any(String), // Don't check exact path as it may have different separators on different platforms
expect.stringContaining("project-mode"),
@@ -256,7 +256,7 @@ describe("CustomModesManager", () => {
const writeCall = (fs.writeFile as jest.Mock).mock.calls[0]
expect(path.normalize(writeCall[0])).toBe(path.normalize(mockRoomodes))

// Verify the content written to .roomodes
// Verify the content written to .kilocodemodes
expect(roomodesContent).toEqual({
customModes: [
expect.objectContaining({
6 changes: 3 additions & 3 deletions src/core/prompts/sections/modes.ts
Original file line number Diff line number Diff line change
@@ -27,11 +27,11 @@ ${allModes.map((mode: ModeConfig) => ` * "${mode.name}" mode (${mode.slug}) - $
- Custom modes can be configured in two ways:
1. Globally via '${customModesPath}' (created automatically on startup)
2. Per-workspace via '.roomodes' in the workspace root directory
2. Per-workspace via '.kilocodemodes' in the workspace root directory
When modes with the same slug exist in both files, the workspace-specific .roomodes version takes precedence. This allows projects to override global modes or define project-specific modes.
When modes with the same slug exist in both files, the workspace-specific .kilocodemodes version takes precedence. This allows projects to override global modes or define project-specific modes.
If asked to create a project mode, create it in .roomodes in the workspace root. If asked to create a global mode, use the global custom modes file.
If asked to create a project mode, create it in .kilocode in the workspace root. If asked to create a global mode, use the global custom modes file.
- The following fields are required and must not be empty:
* slug: A valid slug (lowercase letters, numbers, and hyphens). Must be unique, and shorter is better.
6 changes: 3 additions & 3 deletions webview-ui/src/components/prompts/PromptsView.tsx
Original file line number Diff line number Diff line change
@@ -456,7 +456,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
e.preventDefault() // Prevent blur
vscode.postMessage({
type: "openFile",
text: "./.roomodes",
text: "./.kilocodemodes",
values: {
create: true,
content: JSON.stringify({ customModes: [] }, null, 2),
@@ -465,7 +465,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
setShowConfigMenu(false)
}}
onClick={(e) => e.preventDefault()}>
Edit Project Modes (.roomodes)
Edit Project Modes (.kilocodemodes)
</div>
</div>
)}
@@ -1225,7 +1225,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</div>
</VSCodeRadio>
<VSCodeRadio value="project">
Project-specific (.roomodes)
Project-specific (.kilocodemodes)
<div className="text-xs text-vscode-descriptionForeground mt-0.5">
Only available in this workspace, takes precedence over global
</div>