mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
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:
parent
83c19ce2c8
commit
ec611f51b5
4 changed files with 285 additions and 3 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
224
src/core/task/__tests__/autoContextMentions.spec.ts
Normal file
224
src/core/task/__tests__/autoContextMentions.spec.ts
Normal 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>`,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Reference in a new issue