feat: implement automatic context mentions for custom modes

- Add autoContextMentions field to ModeConfig schema in packages/types/src/mode.ts
- Implement getAutoContextMentionsForCurrentMode() method in Task class
- Automatically inject context mentions when starting or resuming tasks
- Add comprehensive test suite covering all scenarios
- Update .roomodes with example configurations for test and design-engineer modes

Resolves #5154
This commit is contained in:
Roo Code 2025-06-26 23:29:27 +00:00
parent 83c19ce2c8
commit ec611f51b5
4 changed files with 285 additions and 3 deletions

View file

@ -60,6 +60,10 @@ customModes:
- - edit
- fileRegex: (__tests__/.*|__mocks__/.*|\.test\.(ts|tsx|js|jsx)$|\.spec\.(ts|tsx|js|jsx)$|/test/.*|vitest\.config\.(js|ts)$|vitest\.setup\.(js|ts)$)
description: Test files, mocks, and Vitest configuration
autoContextMentions:
- package.json
- vitest.config.ts
- tsconfig.json
customInstructions: |-
When writing tests:
- Always use describe/it blocks for clear test organization
@ -88,6 +92,10 @@ customModes:
- browser
- command
- mcp
autoContextMentions:
- webview-ui/src/index.css
- webview-ui/src/components/
- webview-ui/package.json
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

View file

@ -70,6 +70,7 @@ export const modeConfigSchema = z.object({
customInstructions: z.string().optional(),
groups: groupEntryArraySchema,
source: z.enum(["global", "project"]).optional(),
autoContextMentions: z.array(z.string()).optional(),
})
export type ModeConfig = z.infer<typeof modeConfigSchema>

View file

@ -733,7 +733,17 @@ export class Task extends EventEmitter<ClineEvents> {
this.apiConversationHistory = []
await this.providerRef.deref()?.postStateToWebview()
await this.say("text", task, images)
// Get automatic context mentions for the current mode
const autoContextMentions = await this.getAutoContextMentionsForCurrentMode()
// Inject auto context mentions into the task if they exist
let enhancedTask = task || ""
if (autoContextMentions.length > 0) {
const mentionsText = autoContextMentions.map((mention) => `@${mention}`).join(" ")
enhancedTask = `${enhancedTask}\n\nAutomatic context mentions: ${mentionsText}`
}
await this.say("text", enhancedTask, images)
this.isInitialized = true
let imageBlocks: Anthropic.ImageBlockParam[] = formatResponse.imageBlocks(images)
@ -743,7 +753,7 @@ export class Task extends EventEmitter<ClineEvents> {
await this.initiateTaskLoop([
{
type: "text",
text: `<task>\n${task}\n</task>`,
text: `<task>\n${enhancedTask}\n</task>`,
},
...imageBlocks,
])
@ -986,6 +996,14 @@ export class Task extends EventEmitter<ClineEvents> {
const wasRecent = lastClineMessage?.ts && Date.now() - lastClineMessage.ts < 30_000
// Get automatic context mentions for the current mode
const autoContextMentions = await this.getAutoContextMentionsForCurrentMode()
let autoMentionsText = ""
if (autoContextMentions.length > 0) {
const mentionsText = autoContextMentions.map((mention) => `@${mention}`).join(" ")
autoMentionsText = `\n\nAutomatic context mentions: ${mentionsText}`
}
newUserContent.push({
type: "text",
text:
@ -996,7 +1014,8 @@ export class Task extends EventEmitter<ClineEvents> {
}` +
(responseText
? `\n\nNew instructions for task continuation:\n<user_message>\n${responseText}\n</user_message>`
: ""),
: "") +
autoMentionsText,
})
if (responseImages && responseImages.length > 0) {
@ -1913,4 +1932,34 @@ export class Task extends EventEmitter<ClineEvents> {
public get cwd() {
return this.workspacePath
}
// Get automatic context mentions for the current mode
private async getAutoContextMentionsForCurrentMode(): Promise<string[]> {
try {
const provider = this.providerRef.deref()
if (!provider) {
return []
}
const state = await provider.getState()
const currentMode = state?.mode
const customModes = state?.customModes || []
if (!currentMode) {
return []
}
// Find the current mode configuration
const modeConfig = customModes.find((mode) => mode.slug === currentMode)
if (!modeConfig || !modeConfig.autoContextMentions) {
return []
}
return modeConfig.autoContextMentions
} catch (error) {
console.error("[Task] Error getting auto context mentions:", error)
return []
}
}
}

View file

@ -0,0 +1,224 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import { Task } from "../Task"
import type { ModeConfig } from "@roo-code/types"
// Mock dependencies
vi.mock("../../webview/ClineProvider")
vi.mock("../../../api")
vi.mock("../../../services/browser/UrlContentFetcher")
vi.mock("../../../services/browser/BrowserSession")
vi.mock("../../context-tracking/FileContextTracker")
vi.mock("../../ignore/RooIgnoreController")
vi.mock("../../protect/RooProtectedController")
vi.mock("../../../integrations/editor/DiffViewProvider")
vi.mock("../../../utils/path", () => ({
getWorkspacePath: vi.fn(() => "/test/workspace"),
}))
// Mock TelemetryService
vi.mock("@roo-code/telemetry", async (importOriginal) => {
const actual = (await importOriginal()) as any
return {
...actual,
BaseTelemetryClient: vi.fn(),
TelemetryService: {
instance: {
captureTaskRestarted: vi.fn(),
captureTaskCreated: vi.fn(),
},
},
}
})
describe("Task Auto Context Mentions", () => {
let mockProvider: any
let mockApiConfiguration: any
let task: Task
beforeEach(() => {
vi.clearAllMocks()
mockApiConfiguration = {
apiProvider: "anthropic" as const,
anthropicApiKey: "test-key",
}
const mockCustomModes: ModeConfig[] = [
{
slug: "test-mode",
name: "Test Mode",
roleDefinition: "Test role",
groups: ["read"],
autoContextMentions: ["/package.json", "/src/config/", "/README.md"],
},
{
slug: "no-mentions-mode",
name: "No Mentions Mode",
roleDefinition: "Test role without mentions",
groups: ["read"],
},
]
mockProvider = {
getState: vi.fn().mockResolvedValue({
mode: "test-mode",
customModes: mockCustomModes,
}),
postStateToWebview: vi.fn().mockResolvedValue(undefined),
context: {
globalStorageUri: { fsPath: "/test/storage" },
},
}
// Create task with startTask: false to prevent automatic initialization
task = new Task({
provider: mockProvider,
apiConfiguration: mockApiConfiguration,
task: "Test task",
startTask: false,
})
})
describe("getAutoContextMentionsForCurrentMode", () => {
it("should return auto context mentions for current mode", async () => {
// Access the private method for testing
const mentions = await (task as any).getAutoContextMentionsForCurrentMode()
expect(mentions).toEqual(["/package.json", "/src/config/", "/README.md"])
})
it("should return empty array when mode has no auto context mentions", async () => {
mockProvider.getState.mockResolvedValue({
mode: "no-mentions-mode",
customModes: [
{
slug: "no-mentions-mode",
name: "No Mentions Mode",
roleDefinition: "Test role without mentions",
groups: ["read"],
},
],
})
const mentions = await (task as any).getAutoContextMentionsForCurrentMode()
expect(mentions).toEqual([])
})
it("should return empty array when mode is not found", async () => {
mockProvider.getState.mockResolvedValue({
mode: "non-existent-mode",
customModes: [],
})
const mentions = await (task as any).getAutoContextMentionsForCurrentMode()
expect(mentions).toEqual([])
})
it("should return empty array when provider is not available", async () => {
// Create task with invalid provider reference
const invalidProvider = {
context: {
globalStorageUri: { fsPath: "/test/storage" },
},
getState: vi.fn().mockRejectedValue(new Error("Provider not available")),
}
const taskWithInvalidProvider = new Task({
provider: invalidProvider as any,
apiConfiguration: mockApiConfiguration,
task: "Test task",
startTask: false,
})
// Simulate provider being garbage collected
;(taskWithInvalidProvider as any).providerRef = new WeakRef({})
const mentions = await (taskWithInvalidProvider as any).getAutoContextMentionsForCurrentMode()
expect(mentions).toEqual([])
})
it("should handle errors gracefully", async () => {
mockProvider.getState.mockRejectedValue(new Error("Test error"))
const mentions = await (task as any).getAutoContextMentionsForCurrentMode()
expect(mentions).toEqual([])
})
})
describe("startTask with auto context mentions", () => {
it("should inject auto context mentions into task", async () => {
const originalTask = "Create a new feature"
// Mock the say method to capture what gets passed
const mockSay = vi.fn().mockResolvedValue(undefined)
task.say = mockSay
// Mock the initiateTaskLoop method
const mockInitiateTaskLoop = vi.fn().mockResolvedValue(undefined)
;(task as any).initiateTaskLoop = mockInitiateTaskLoop
// Call startTask
await (task as any).startTask(originalTask, [])
// Verify say was called with enhanced task
expect(mockSay).toHaveBeenCalledWith("text", expect.stringContaining("Create a new feature"), [])
expect(mockSay).toHaveBeenCalledWith(
"text",
expect.stringContaining("Automatic context mentions: @/package.json @/src/config/ @/README.md"),
[],
)
// Verify initiateTaskLoop was called with enhanced task
expect(mockInitiateTaskLoop).toHaveBeenCalledWith([
{
type: "text",
text: expect.stringContaining(
"<task>\nCreate a new feature\n\nAutomatic context mentions: @/package.json @/src/config/ @/README.md\n</task>",
),
},
])
})
it("should work normally when no auto context mentions are defined", async () => {
mockProvider.getState.mockResolvedValue({
mode: "no-mentions-mode",
customModes: [
{
slug: "no-mentions-mode",
name: "No Mentions Mode",
roleDefinition: "Test role without mentions",
groups: ["read"],
},
],
})
const originalTask = "Create a new feature"
// Mock the say method to capture what gets passed
const mockSay = vi.fn().mockResolvedValue(undefined)
task.say = mockSay
// Mock the initiateTaskLoop method
const mockInitiateTaskLoop = vi.fn().mockResolvedValue(undefined)
;(task as any).initiateTaskLoop = mockInitiateTaskLoop
// Call startTask
await (task as any).startTask(originalTask, [])
// Verify say was called with original task only
expect(mockSay).toHaveBeenCalledWith("text", originalTask, [])
// Verify initiateTaskLoop was called with original task
expect(mockInitiateTaskLoop).toHaveBeenCalledWith([
{
type: "text",
text: `<task>\n${originalTask}\n</task>`,
},
])
})
})
})