mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
refactor: consolidate prompt functionality into support-prompt module
- Move code action prompts from core/prompts to shared/support-prompt - Migrate enhance prompt functionality from modes to support-prompt - Add UI for managing code action prompts in PromptsView - Update types and interfaces for better prompt management
This commit is contained in:
@@ -1,76 +0,0 @@
|
|||||||
import { explainCodePrompt, fixCodePrompt, improveCodePrompt } from '../code-actions';
|
|
||||||
|
|
||||||
describe('Code Action Prompts', () => {
|
|
||||||
const testFilePath = 'test/file.ts';
|
|
||||||
const testCode = 'function test() { return true; }';
|
|
||||||
|
|
||||||
describe('explainCodePrompt', () => {
|
|
||||||
it('should format explain prompt correctly', () => {
|
|
||||||
const prompt = explainCodePrompt({
|
|
||||||
filePath: testFilePath,
|
|
||||||
selectedText: testCode
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(prompt).toContain(`@/${testFilePath}`);
|
|
||||||
expect(prompt).toContain(testCode);
|
|
||||||
expect(prompt).toContain('purpose and functionality');
|
|
||||||
expect(prompt).toContain('Key components');
|
|
||||||
expect(prompt).toContain('Important patterns');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('fixCodePrompt', () => {
|
|
||||||
it('should format fix prompt without diagnostics', () => {
|
|
||||||
const prompt = fixCodePrompt({
|
|
||||||
filePath: testFilePath,
|
|
||||||
selectedText: testCode
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(prompt).toContain(`@/${testFilePath}`);
|
|
||||||
expect(prompt).toContain(testCode);
|
|
||||||
expect(prompt).toContain('Address all detected problems');
|
|
||||||
expect(prompt).not.toContain('Current problems detected');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should format fix prompt with diagnostics', () => {
|
|
||||||
const diagnostics = [
|
|
||||||
{
|
|
||||||
source: 'eslint',
|
|
||||||
message: 'Missing semicolon',
|
|
||||||
code: 'semi'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'Unused variable',
|
|
||||||
severity: 1
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const prompt = fixCodePrompt({
|
|
||||||
filePath: testFilePath,
|
|
||||||
selectedText: testCode,
|
|
||||||
diagnostics
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(prompt).toContain('Current problems detected:');
|
|
||||||
expect(prompt).toContain('[eslint] Missing semicolon (semi)');
|
|
||||||
expect(prompt).toContain('[Error] Unused variable');
|
|
||||||
expect(prompt).toContain(testCode);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('improveCodePrompt', () => {
|
|
||||||
it('should format improve prompt correctly', () => {
|
|
||||||
const prompt = improveCodePrompt({
|
|
||||||
filePath: testFilePath,
|
|
||||||
selectedText: testCode
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(prompt).toContain(`@/${testFilePath}`);
|
|
||||||
expect(prompt).toContain(testCode);
|
|
||||||
expect(prompt).toContain('Code readability');
|
|
||||||
expect(prompt).toContain('Performance optimization');
|
|
||||||
expect(prompt).toContain('Best practices');
|
|
||||||
expect(prompt).toContain('Error handling');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
type PromptParams = Record<string, string | any[]>;
|
|
||||||
|
|
||||||
const generateDiagnosticText = (diagnostics?: any[]) => {
|
|
||||||
if (!diagnostics?.length) return '';
|
|
||||||
return `\nCurrent problems detected:\n${diagnostics.map(d =>
|
|
||||||
`- [${d.source || 'Error'}] ${d.message}${d.code ? ` (${d.code})` : ''}`
|
|
||||||
).join('\n')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createPrompt = (template: string, params: PromptParams): string => {
|
|
||||||
let result = template;
|
|
||||||
for (const [key, value] of Object.entries(params)) {
|
|
||||||
if (key === 'diagnostics') {
|
|
||||||
result = result.replaceAll('${diagnosticText}', generateDiagnosticText(value as any[]));
|
|
||||||
} else {
|
|
||||||
result = result.replaceAll(`\${${key}}`, value as string);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace any remaining user_input placeholders with empty string
|
|
||||||
result = result.replaceAll('${userInput}', '');
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EXPLAIN_TEMPLATE = `
|
|
||||||
Explain the following code from file path @/\${filePath}:
|
|
||||||
\${userInput}
|
|
||||||
|
|
||||||
\`\`\`
|
|
||||||
\${selectedText}
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
Please provide a clear and concise explanation of what this code does, including:
|
|
||||||
1. The purpose and functionality
|
|
||||||
2. Key components and their interactions
|
|
||||||
3. Important patterns or techniques used
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const FIX_TEMPLATE = `
|
|
||||||
Fix any issues in the following code from file path @/\${filePath}
|
|
||||||
\${diagnosticText}
|
|
||||||
\${userInput}
|
|
||||||
|
|
||||||
\`\`\`
|
|
||||||
\${selectedText}
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
Please:
|
|
||||||
1. Address all detected problems listed above (if any)
|
|
||||||
2. Identify any other potential bugs or issues
|
|
||||||
3. Provide corrected code
|
|
||||||
4. Explain what was fixed and why
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const IMPROVE_TEMPLATE = `
|
|
||||||
Improve the following code from file path @/\${filePath}:
|
|
||||||
\${userInput}
|
|
||||||
|
|
||||||
\`\`\`
|
|
||||||
\${selectedText}
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
Please suggest improvements for:
|
|
||||||
1. Code readability and maintainability
|
|
||||||
2. Performance optimization
|
|
||||||
3. Best practices and patterns
|
|
||||||
4. Error handling and edge cases
|
|
||||||
|
|
||||||
Provide the improved code along with explanations for each enhancement.
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const explainCodePrompt = (params: PromptParams) =>
|
|
||||||
createPrompt(EXPLAIN_TEMPLATE, params);
|
|
||||||
|
|
||||||
export const fixCodePrompt = (params: PromptParams) =>
|
|
||||||
createPrompt(FIX_TEMPLATE, params);
|
|
||||||
|
|
||||||
export const improveCodePrompt = (params: PromptParams) =>
|
|
||||||
createPrompt(IMPROVE_TEMPLATE, params);
|
|
||||||
|
|
||||||
// Get template based on prompt type
|
|
||||||
export const defaultTemplates = {
|
|
||||||
'EXPLAIN': EXPLAIN_TEMPLATE,
|
|
||||||
'FIX': FIX_TEMPLATE,
|
|
||||||
'IMPROVE': IMPROVE_TEMPLATE
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Anthropic } from "@anthropic-ai/sdk"
|
import { Anthropic } from "@anthropic-ai/sdk"
|
||||||
|
import delay from "delay"
|
||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import os from "os"
|
import os from "os"
|
||||||
@@ -23,7 +24,6 @@ import {
|
|||||||
modes,
|
modes,
|
||||||
CustomPrompts,
|
CustomPrompts,
|
||||||
PromptComponent,
|
PromptComponent,
|
||||||
enhance,
|
|
||||||
ModeConfig,
|
ModeConfig,
|
||||||
defaultModeSlug,
|
defaultModeSlug,
|
||||||
getModeBySlug,
|
getModeBySlug,
|
||||||
@@ -40,10 +40,7 @@ import { enhancePrompt } from "../../utils/enhance-prompt"
|
|||||||
import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git"
|
import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git"
|
||||||
import { ConfigManager } from "../config/ConfigManager"
|
import { ConfigManager } from "../config/ConfigManager"
|
||||||
import { CustomModesManager } from "../config/CustomModesManager"
|
import { CustomModesManager } from "../config/CustomModesManager"
|
||||||
import {
|
import { enhance, codeActionPrompt } from "../../shared/support-prompt"
|
||||||
defaultTemplates,
|
|
||||||
createPrompt
|
|
||||||
} from "../prompts/code-actions"
|
|
||||||
|
|
||||||
import { ACTION_NAMES } from "../CodeActionProvider"
|
import { ACTION_NAMES } from "../CodeActionProvider"
|
||||||
|
|
||||||
@@ -189,17 +186,27 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
|
|
||||||
public static async handleCodeAction(
|
public static async handleCodeAction(
|
||||||
promptType: keyof typeof ACTION_NAMES,
|
promptType: keyof typeof ACTION_NAMES,
|
||||||
params: Record<string, string | any[]>
|
params: Record<string, string | any[]>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const visibleProvider = ClineProvider.getVisibleInstance()
|
let visibleProvider = ClineProvider.getVisibleInstance()
|
||||||
|
|
||||||
|
// If no visible provider, try to show the sidebar view
|
||||||
|
if (!visibleProvider) {
|
||||||
|
await vscode.commands.executeCommand("roo-cline.SidebarProvider.focus")
|
||||||
|
// Wait briefly for the view to become visible
|
||||||
|
await delay(100)
|
||||||
|
visibleProvider = ClineProvider.getVisibleInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still no visible provider, return
|
||||||
if (!visibleProvider) {
|
if (!visibleProvider) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { utilPrompt } = await visibleProvider.getState()
|
const { customPrompts } = await visibleProvider.getState()
|
||||||
|
|
||||||
|
const prompt = codeActionPrompt.create(promptType, params, customPrompts)
|
||||||
|
|
||||||
const template = utilPrompt?.[promptType] ?? defaultTemplates[promptType]
|
|
||||||
const prompt = createPrompt(template, params)
|
|
||||||
await visibleProvider.initClineWithTask(prompt)
|
await visibleProvider.initClineWithTask(prompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,7 +304,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
experimentalDiffStrategy,
|
experimentalDiffStrategy,
|
||||||
} = await this.getState()
|
} = await this.getState()
|
||||||
|
|
||||||
const modePrompt = customPrompts?.[mode]
|
const modePrompt = customPrompts?.[mode] as PromptComponent
|
||||||
const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n")
|
const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n")
|
||||||
|
|
||||||
this.cline = new Cline(
|
this.cline = new Cline(
|
||||||
@@ -325,7 +332,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
experimentalDiffStrategy,
|
experimentalDiffStrategy,
|
||||||
} = await this.getState()
|
} = await this.getState()
|
||||||
|
|
||||||
const modePrompt = customPrompts?.[mode]
|
const modePrompt = customPrompts?.[mode] as PromptComponent
|
||||||
const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n")
|
const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n")
|
||||||
|
|
||||||
this.cline = new Cline(
|
this.cline = new Cline(
|
||||||
@@ -804,29 +811,49 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
|
|
||||||
await this.postStateToWebview()
|
await this.postStateToWebview()
|
||||||
break
|
break
|
||||||
case "updateEnhancedPrompt":
|
case "updateSupportPrompt":
|
||||||
const existingPrompts = (await this.getGlobalState("customPrompts")) || {}
|
try {
|
||||||
|
if (Object.keys(message?.values ?? {}).length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const updatedPrompts = {
|
const existingPrompts = (await this.getGlobalState("customPrompts")) || {}
|
||||||
...existingPrompts,
|
|
||||||
enhance: message.text,
|
const updatedPrompts = {
|
||||||
|
...existingPrompts,
|
||||||
|
...message.values,
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.updateGlobalState("customPrompts", updatedPrompts)
|
||||||
|
await this.postStateToWebview()
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error update support prompt:", error)
|
||||||
|
vscode.window.showErrorMessage("Failed to update support prompt")
|
||||||
}
|
}
|
||||||
|
break
|
||||||
|
case "resetSupportPrompt":
|
||||||
|
try {
|
||||||
|
if (!message?.text) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await this.updateGlobalState("customPrompts", updatedPrompts)
|
const existingPrompts = ((await this.getGlobalState("customPrompts")) || {}) as Record<
|
||||||
|
string,
|
||||||
|
any
|
||||||
|
>
|
||||||
|
|
||||||
// Get current state and explicitly include customPrompts
|
const updatedPrompts = {
|
||||||
const currentState = await this.getState()
|
...existingPrompts,
|
||||||
|
}
|
||||||
|
|
||||||
const stateWithPrompts = {
|
updatedPrompts[message.text] = undefined
|
||||||
...currentState,
|
|
||||||
customPrompts: updatedPrompts,
|
await this.updateGlobalState("customPrompts", updatedPrompts)
|
||||||
|
await this.postStateToWebview()
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error reset support prompt:", error)
|
||||||
|
vscode.window.showErrorMessage("Failed to reset support prompt")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post state with prompts
|
|
||||||
this.view?.webview.postMessage({
|
|
||||||
type: "state",
|
|
||||||
state: stateWithPrompts,
|
|
||||||
})
|
|
||||||
break
|
break
|
||||||
case "updatePrompt":
|
case "updatePrompt":
|
||||||
if (message.promptMode && message.customPrompt !== undefined) {
|
if (message.promptMode && message.customPrompt !== undefined) {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { ClineProvider } from "./core/webview/ClineProvider"
|
|||||||
import { createClineAPI } from "./exports"
|
import { createClineAPI } from "./exports"
|
||||||
import "./utils/path" // necessary to have access to String.prototype.toPosix
|
import "./utils/path" // necessary to have access to String.prototype.toPosix
|
||||||
import { ACTION_NAMES, CodeActionProvider } from "./core/CodeActionProvider"
|
import { ACTION_NAMES, CodeActionProvider } from "./core/CodeActionProvider"
|
||||||
import { explainCodePrompt, fixCodePrompt, improveCodePrompt } from "./core/prompts/code-actions"
|
|
||||||
import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
|
import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -162,14 +161,10 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
|
|
||||||
// Register code actions provider
|
// Register code actions provider
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
vscode.languages.registerCodeActionsProvider(
|
vscode.languages.registerCodeActionsProvider({ pattern: "**/*" }, new CodeActionProvider(), {
|
||||||
{ pattern: "**/*" },
|
providedCodeActionKinds: CodeActionProvider.providedCodeActionKinds,
|
||||||
new CodeActionProvider(),
|
}),
|
||||||
{
|
)
|
||||||
providedCodeActionKinds: CodeActionProvider.providedCodeActionKinds
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Helper function to handle code actions
|
// Helper function to handle code actions
|
||||||
const registerCodeAction = (
|
const registerCodeAction = (
|
||||||
@@ -177,51 +172,54 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
command: string,
|
command: string,
|
||||||
promptType: keyof typeof ACTION_NAMES,
|
promptType: keyof typeof ACTION_NAMES,
|
||||||
inputPrompt: string,
|
inputPrompt: string,
|
||||||
inputPlaceholder: string
|
inputPlaceholder: string,
|
||||||
) => {
|
) => {
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
vscode.commands.registerCommand(command, async (filePath: string, selectedText: string, diagnostics?: any[]) => {
|
vscode.commands.registerCommand(
|
||||||
const userInput = await vscode.window.showInputBox({
|
command,
|
||||||
prompt: inputPrompt,
|
async (filePath: string, selectedText: string, diagnostics?: any[]) => {
|
||||||
placeHolder: inputPlaceholder
|
const userInput = await vscode.window.showInputBox({
|
||||||
});
|
prompt: inputPrompt,
|
||||||
|
placeHolder: inputPlaceholder,
|
||||||
|
})
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
filePath,
|
filePath,
|
||||||
selectedText,
|
selectedText,
|
||||||
...(diagnostics ? { diagnostics } : {}),
|
...(diagnostics ? { diagnostics } : {}),
|
||||||
...(userInput ? { userInput } : {})
|
...(userInput ? { userInput } : {}),
|
||||||
};
|
}
|
||||||
|
|
||||||
await ClineProvider.handleCodeAction(promptType, params);
|
await ClineProvider.handleCodeAction(promptType, params)
|
||||||
})
|
},
|
||||||
);
|
),
|
||||||
};
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Register code action commands
|
// Register code action commands
|
||||||
registerCodeAction(
|
registerCodeAction(
|
||||||
context,
|
context,
|
||||||
"roo-cline.explainCode",
|
"roo-cline.explainCode",
|
||||||
'EXPLAIN',
|
"EXPLAIN",
|
||||||
"Any specific questions about this code?",
|
"Any specific questions about this code?",
|
||||||
"E.g. How does the error handling work?"
|
"E.g. How does the error handling work?",
|
||||||
);
|
)
|
||||||
|
|
||||||
registerCodeAction(
|
registerCodeAction(
|
||||||
context,
|
context,
|
||||||
"roo-cline.fixCode",
|
"roo-cline.fixCode",
|
||||||
'FIX',
|
"FIX",
|
||||||
"Any specific concerns about fixing this code?",
|
"Any specific concerns about fixing this code?",
|
||||||
"E.g. Maintain backward compatibility"
|
"E.g. Maintain backward compatibility",
|
||||||
);
|
)
|
||||||
|
|
||||||
registerCodeAction(
|
registerCodeAction(
|
||||||
context,
|
context,
|
||||||
"roo-cline.improveCode",
|
"roo-cline.improveCode",
|
||||||
'IMPROVE',
|
"IMPROVE",
|
||||||
"Any specific aspects you want to improve?",
|
"Any specific aspects you want to improve?",
|
||||||
"E.g. Focus on performance optimization"
|
"E.g. Focus on performance optimization",
|
||||||
);
|
)
|
||||||
|
|
||||||
return createClineAPI(outputChannel, sidebarProvider)
|
return createClineAPI(outputChannel, sidebarProvider)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ export interface WebviewMessage {
|
|||||||
| "requestVsCodeLmModels"
|
| "requestVsCodeLmModels"
|
||||||
| "mode"
|
| "mode"
|
||||||
| "updatePrompt"
|
| "updatePrompt"
|
||||||
| "updateEnhancedPrompt"
|
| "updateSupportPrompt"
|
||||||
|
| "resetSupportPrompt"
|
||||||
| "getSystemPrompt"
|
| "getSystemPrompt"
|
||||||
| "systemPrompt"
|
| "systemPrompt"
|
||||||
| "enhancementApiConfigId"
|
| "enhancementApiConfigId"
|
||||||
|
|||||||
138
src/shared/__tests__/support-prompts.test.ts
Normal file
138
src/shared/__tests__/support-prompts.test.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import { codeActionPrompt, type CodeActionType } from "../support-prompt"
|
||||||
|
|
||||||
|
describe("Code Action Prompts", () => {
|
||||||
|
const testFilePath = "test/file.ts"
|
||||||
|
const testCode = "function test() { return true; }"
|
||||||
|
|
||||||
|
describe("EXPLAIN action", () => {
|
||||||
|
it("should format explain prompt correctly", () => {
|
||||||
|
const prompt = codeActionPrompt.create("EXPLAIN", {
|
||||||
|
filePath: testFilePath,
|
||||||
|
selectedText: testCode,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(prompt).toContain(`@/${testFilePath}`)
|
||||||
|
expect(prompt).toContain(testCode)
|
||||||
|
expect(prompt).toContain("purpose and functionality")
|
||||||
|
expect(prompt).toContain("Key components")
|
||||||
|
expect(prompt).toContain("Important patterns")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("FIX action", () => {
|
||||||
|
it("should format fix prompt without diagnostics", () => {
|
||||||
|
const prompt = codeActionPrompt.create("FIX", {
|
||||||
|
filePath: testFilePath,
|
||||||
|
selectedText: testCode,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(prompt).toContain(`@/${testFilePath}`)
|
||||||
|
expect(prompt).toContain(testCode)
|
||||||
|
expect(prompt).toContain("Address all detected problems")
|
||||||
|
expect(prompt).not.toContain("Current problems detected")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should format fix prompt with diagnostics", () => {
|
||||||
|
const diagnostics = [
|
||||||
|
{
|
||||||
|
source: "eslint",
|
||||||
|
message: "Missing semicolon",
|
||||||
|
code: "semi",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Unused variable",
|
||||||
|
severity: 1,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const prompt = codeActionPrompt.create("FIX", {
|
||||||
|
filePath: testFilePath,
|
||||||
|
selectedText: testCode,
|
||||||
|
diagnostics,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(prompt).toContain("Current problems detected:")
|
||||||
|
expect(prompt).toContain("[eslint] Missing semicolon (semi)")
|
||||||
|
expect(prompt).toContain("[Error] Unused variable")
|
||||||
|
expect(prompt).toContain(testCode)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("IMPROVE action", () => {
|
||||||
|
it("should format improve prompt correctly", () => {
|
||||||
|
const prompt = codeActionPrompt.create("IMPROVE", {
|
||||||
|
filePath: testFilePath,
|
||||||
|
selectedText: testCode,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(prompt).toContain(`@/${testFilePath}`)
|
||||||
|
expect(prompt).toContain(testCode)
|
||||||
|
expect(prompt).toContain("Code readability")
|
||||||
|
expect(prompt).toContain("Performance optimization")
|
||||||
|
expect(prompt).toContain("Best practices")
|
||||||
|
expect(prompt).toContain("Error handling")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("get template", () => {
|
||||||
|
it("should return default template when no custom prompts provided", () => {
|
||||||
|
const template = codeActionPrompt.get(undefined, "EXPLAIN")
|
||||||
|
expect(template).toBe(codeActionPrompt.default.EXPLAIN)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return custom template when provided", () => {
|
||||||
|
const customTemplate = "Custom template for explaining code"
|
||||||
|
const customPrompts = {
|
||||||
|
EXPLAIN: customTemplate,
|
||||||
|
}
|
||||||
|
const template = codeActionPrompt.get(customPrompts, "EXPLAIN")
|
||||||
|
expect(template).toBe(customTemplate)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return default template when custom prompts does not include type", () => {
|
||||||
|
const customPrompts = {
|
||||||
|
SOMETHING_ELSE: "Other template",
|
||||||
|
}
|
||||||
|
const template = codeActionPrompt.get(customPrompts, "EXPLAIN")
|
||||||
|
expect(template).toBe(codeActionPrompt.default.EXPLAIN)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("create with custom prompts", () => {
|
||||||
|
it("should use custom template when provided", () => {
|
||||||
|
const customTemplate = "Custom template for ${filePath}"
|
||||||
|
const customPrompts = {
|
||||||
|
EXPLAIN: customTemplate,
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = codeActionPrompt.create(
|
||||||
|
"EXPLAIN",
|
||||||
|
{
|
||||||
|
filePath: testFilePath,
|
||||||
|
selectedText: testCode,
|
||||||
|
},
|
||||||
|
customPrompts,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(prompt).toContain(`Custom template for ${testFilePath}`)
|
||||||
|
expect(prompt).not.toContain("purpose and functionality")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should use default template when custom prompts does not include type", () => {
|
||||||
|
const customPrompts = {
|
||||||
|
EXPLAIN: "Other template",
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = codeActionPrompt.create(
|
||||||
|
"EXPLAIN",
|
||||||
|
{
|
||||||
|
filePath: testFilePath,
|
||||||
|
selectedText: testCode,
|
||||||
|
},
|
||||||
|
customPrompts,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(prompt).toContain("Other template")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -12,6 +12,16 @@ export type ModeConfig = {
|
|||||||
groups: readonly ToolGroup[] // Now uses groups instead of tools array
|
groups: readonly ToolGroup[] // Now uses groups instead of tools array
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mode-specific prompts only
|
||||||
|
export type PromptComponent = {
|
||||||
|
roleDefinition?: string
|
||||||
|
customInstructions?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CustomPrompts = {
|
||||||
|
[key: string]: PromptComponent | undefined | string
|
||||||
|
}
|
||||||
|
|
||||||
// Helper to get all tools for a mode
|
// Helper to get all tools for a mode
|
||||||
export function getToolsForMode(groups: readonly ToolGroup[]): string[] {
|
export function getToolsForMode(groups: readonly ToolGroup[]): string[] {
|
||||||
const tools = new Set<string>()
|
const tools = new Set<string>()
|
||||||
@@ -130,33 +140,6 @@ export function isToolAllowedForMode(
|
|||||||
return mode.groups.some((group) => TOOL_GROUPS[group].includes(tool as string))
|
return mode.groups.some((group) => TOOL_GROUPS[group].includes(tool as string))
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PromptComponent = {
|
|
||||||
roleDefinition?: string
|
|
||||||
customInstructions?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mode-specific prompts only
|
|
||||||
export type CustomPrompts = {
|
|
||||||
[key: string]: PromptComponent | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
// Separate enhance prompt type and definition
|
|
||||||
export type EnhanceConfig = {
|
|
||||||
prompt: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const enhance: EnhanceConfig = {
|
|
||||||
prompt: "Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes):",
|
|
||||||
} as const
|
|
||||||
|
|
||||||
// Completely separate enhance prompt handling
|
|
||||||
export const enhancePrompt = {
|
|
||||||
default: enhance.prompt,
|
|
||||||
get: (customPrompts: Record<string, any> | undefined): string => {
|
|
||||||
return customPrompts?.enhance ?? enhance.prompt
|
|
||||||
},
|
|
||||||
} as const
|
|
||||||
|
|
||||||
// Create the mode-specific default prompts
|
// Create the mode-specific default prompts
|
||||||
export const defaultPrompts: Readonly<CustomPrompts> = Object.freeze(
|
export const defaultPrompts: Readonly<CustomPrompts> = Object.freeze(
|
||||||
Object.fromEntries(modes.map((mode) => [mode.slug, { roleDefinition: mode.roleDefinition }])),
|
Object.fromEntries(modes.map((mode) => [mode.slug, { roleDefinition: mode.roleDefinition }])),
|
||||||
|
|||||||
118
src/shared/support-prompt.ts
Normal file
118
src/shared/support-prompt.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
// Separate enhance prompt type and definition
|
||||||
|
export type EnhanceConfig = {
|
||||||
|
prompt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enhance: EnhanceConfig = {
|
||||||
|
prompt: "Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes):",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
// Completely separate enhance prompt handling
|
||||||
|
export const enhancePrompt = {
|
||||||
|
default: enhance.prompt,
|
||||||
|
get: (customPrompts: Record<string, any> | undefined): string => {
|
||||||
|
return customPrompts?.enhance ?? enhance.prompt
|
||||||
|
},
|
||||||
|
} as const
|
||||||
|
|
||||||
|
// Code action prompts
|
||||||
|
type PromptParams = Record<string, string | any[]>
|
||||||
|
|
||||||
|
const generateDiagnosticText = (diagnostics?: any[]) => {
|
||||||
|
if (!diagnostics?.length) return ""
|
||||||
|
return `\nCurrent problems detected:\n${diagnostics
|
||||||
|
.map((d) => `- [${d.source || "Error"}] ${d.message}${d.code ? ` (${d.code})` : ""}`)
|
||||||
|
.join("\n")}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createPrompt = (template: string, params: PromptParams): string => {
|
||||||
|
let result = template
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
if (key === "diagnostics") {
|
||||||
|
result = result.replaceAll("${diagnosticText}", generateDiagnosticText(value as any[]))
|
||||||
|
} else {
|
||||||
|
result = result.replaceAll(`\${${key}}`, value as string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace any remaining user_input placeholders with empty string
|
||||||
|
result = result.replaceAll("${userInput}", "")
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const EXPLAIN_TEMPLATE = `
|
||||||
|
Explain the following code from file path @/\${filePath}:
|
||||||
|
\${userInput}
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
\${selectedText}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Please provide a clear and concise explanation of what this code does, including:
|
||||||
|
1. The purpose and functionality
|
||||||
|
2. Key components and their interactions
|
||||||
|
3. Important patterns or techniques used
|
||||||
|
`
|
||||||
|
|
||||||
|
const FIX_TEMPLATE = `
|
||||||
|
Fix any issues in the following code from file path @/\${filePath}
|
||||||
|
\${diagnosticText}
|
||||||
|
\${userInput}
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
\${selectedText}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Please:
|
||||||
|
1. Address all detected problems listed above (if any)
|
||||||
|
2. Identify any other potential bugs or issues
|
||||||
|
3. Provide corrected code
|
||||||
|
4. Explain what was fixed and why
|
||||||
|
`
|
||||||
|
|
||||||
|
const IMPROVE_TEMPLATE = `
|
||||||
|
Improve the following code from file path @/\${filePath}:
|
||||||
|
\${userInput}
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
\${selectedText}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Please suggest improvements for:
|
||||||
|
1. Code readability and maintainability
|
||||||
|
2. Performance optimization
|
||||||
|
3. Best practices and patterns
|
||||||
|
4. Error handling and edge cases
|
||||||
|
|
||||||
|
Provide the improved code along with explanations for each enhancement.
|
||||||
|
`
|
||||||
|
|
||||||
|
// Get template based on prompt type
|
||||||
|
const defaultTemplates = {
|
||||||
|
EXPLAIN: EXPLAIN_TEMPLATE,
|
||||||
|
FIX: FIX_TEMPLATE,
|
||||||
|
IMPROVE: IMPROVE_TEMPLATE,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
type CodeActionType = keyof typeof defaultTemplates
|
||||||
|
|
||||||
|
export const codeActionPrompt = {
|
||||||
|
default: defaultTemplates,
|
||||||
|
get: (customPrompts: Record<string, any> | undefined, type: CodeActionType): string => {
|
||||||
|
return customPrompts?.[type] ?? defaultTemplates[type]
|
||||||
|
},
|
||||||
|
create: (type: CodeActionType, params: PromptParams, customPrompts?: Record<string, any>): string => {
|
||||||
|
const template = codeActionPrompt.get(customPrompts, type)
|
||||||
|
return createPrompt(template, params)
|
||||||
|
},
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type { CodeActionType }
|
||||||
|
|
||||||
|
// User-friendly labels for code action types
|
||||||
|
export const codeActionLabels: Record<CodeActionType, string> = {
|
||||||
|
FIX: "Fix Issues",
|
||||||
|
EXPLAIN: "Explain Code",
|
||||||
|
IMPROVE: "Improve Code",
|
||||||
|
} as const
|
||||||
@@ -8,14 +8,14 @@ import {
|
|||||||
VSCodeCheckbox,
|
VSCodeCheckbox,
|
||||||
} from "@vscode/webview-ui-toolkit/react"
|
} from "@vscode/webview-ui-toolkit/react"
|
||||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||||
|
import { Mode, PromptComponent, getRoleDefinition, getAllModes, ModeConfig } from "../../../../src/shared/modes"
|
||||||
import {
|
import {
|
||||||
Mode,
|
|
||||||
PromptComponent,
|
|
||||||
getRoleDefinition,
|
|
||||||
getAllModes,
|
|
||||||
ModeConfig,
|
|
||||||
enhancePrompt,
|
enhancePrompt,
|
||||||
} from "../../../../src/shared/modes"
|
codeActionPrompt,
|
||||||
|
CodeActionType,
|
||||||
|
codeActionLabels,
|
||||||
|
} from "../../../../src/shared/support-prompt"
|
||||||
|
|
||||||
import { TOOL_GROUPS, GROUP_DISPLAY_NAMES, ToolGroup } from "../../../../src/shared/tool-groups"
|
import { TOOL_GROUPS, GROUP_DISPLAY_NAMES, ToolGroup } from "../../../../src/shared/tool-groups"
|
||||||
import { vscode } from "../../utils/vscode"
|
import { vscode } from "../../utils/vscode"
|
||||||
|
|
||||||
@@ -50,11 +50,12 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
const [selectedPromptTitle, setSelectedPromptTitle] = useState("")
|
const [selectedPromptTitle, setSelectedPromptTitle] = useState("")
|
||||||
const [isToolsEditMode, setIsToolsEditMode] = useState(false)
|
const [isToolsEditMode, setIsToolsEditMode] = useState(false)
|
||||||
const [isCreateModeDialogOpen, setIsCreateModeDialogOpen] = useState(false)
|
const [isCreateModeDialogOpen, setIsCreateModeDialogOpen] = useState(false)
|
||||||
|
const [activeCodeActionTab, setActiveCodeActionTab] = useState<CodeActionType>("FIX")
|
||||||
|
|
||||||
// Direct update functions
|
// Direct update functions
|
||||||
const updateAgentPrompt = useCallback(
|
const updateAgentPrompt = useCallback(
|
||||||
(mode: Mode, promptData: PromptComponent) => {
|
(mode: Mode, promptData: PromptComponent) => {
|
||||||
const existingPrompt = customPrompts?.[mode]
|
const existingPrompt = customPrompts?.[mode] as PromptComponent
|
||||||
const updatedPrompt = { ...existingPrompt, ...promptData }
|
const updatedPrompt = { ...existingPrompt, ...promptData }
|
||||||
|
|
||||||
// Only include properties that differ from defaults
|
// Only include properties that differ from defaults
|
||||||
@@ -256,8 +257,19 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
|
|
||||||
const updateEnhancePrompt = (value: string | undefined) => {
|
const updateEnhancePrompt = (value: string | undefined) => {
|
||||||
vscode.postMessage({
|
vscode.postMessage({
|
||||||
type: "updateEnhancedPrompt",
|
type: "updateSupportPrompt",
|
||||||
text: value,
|
values: {
|
||||||
|
enhance: value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateCodeActionPrompt = (type: CodeActionType, value: string | undefined) => {
|
||||||
|
vscode.postMessage({
|
||||||
|
type: "updateSupportPrompt",
|
||||||
|
values: {
|
||||||
|
[type]: value,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,7 +283,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
|
|
||||||
const handleAgentReset = (modeSlug: string) => {
|
const handleAgentReset = (modeSlug: string) => {
|
||||||
// Only reset role definition for built-in modes
|
// Only reset role definition for built-in modes
|
||||||
const existingPrompt = customPrompts?.[modeSlug]
|
const existingPrompt = customPrompts?.[modeSlug] as PromptComponent
|
||||||
updateAgentPrompt(modeSlug, {
|
updateAgentPrompt(modeSlug, {
|
||||||
...existingPrompt,
|
...existingPrompt,
|
||||||
roleDefinition: undefined,
|
roleDefinition: undefined,
|
||||||
@@ -279,13 +291,27 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleEnhanceReset = () => {
|
const handleEnhanceReset = () => {
|
||||||
updateEnhancePrompt(undefined)
|
vscode.postMessage({
|
||||||
|
type: "resetSupportPrompt",
|
||||||
|
text: "enhance",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCodeActionReset = (type: CodeActionType) => {
|
||||||
|
vscode.postMessage({
|
||||||
|
type: "resetSupportPrompt",
|
||||||
|
text: type,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getEnhancePromptValue = (): string => {
|
const getEnhancePromptValue = (): string => {
|
||||||
return enhancePrompt.get(customPrompts)
|
return enhancePrompt.get(customPrompts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getCodeActionPromptValue = (type: CodeActionType): string => {
|
||||||
|
return codeActionPrompt.get(customPrompts, type)
|
||||||
|
}
|
||||||
|
|
||||||
const handleTestEnhancement = () => {
|
const handleTestEnhancement = () => {
|
||||||
if (!testPrompt.trim()) return
|
if (!testPrompt.trim()) return
|
||||||
|
|
||||||
@@ -563,7 +589,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
<VSCodeTextArea
|
<VSCodeTextArea
|
||||||
value={(() => {
|
value={(() => {
|
||||||
const customMode = findModeBySlug(mode, customModes)
|
const customMode = findModeBySlug(mode, customModes)
|
||||||
const prompt = customPrompts?.[mode]
|
const prompt = customPrompts?.[mode] as PromptComponent
|
||||||
return customMode?.roleDefinition ?? prompt?.roleDefinition ?? getRoleDefinition(mode)
|
return customMode?.roleDefinition ?? prompt?.roleDefinition ?? getRoleDefinition(mode)
|
||||||
})()}
|
})()}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -680,7 +706,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
<VSCodeTextArea
|
<VSCodeTextArea
|
||||||
value={(() => {
|
value={(() => {
|
||||||
const customMode = findModeBySlug(mode, customModes)
|
const customMode = findModeBySlug(mode, customModes)
|
||||||
const prompt = customPrompts?.[mode]
|
const prompt = customPrompts?.[mode] as PromptComponent
|
||||||
return customMode?.customInstructions ?? prompt?.customInstructions ?? ""
|
return customMode?.customInstructions ?? prompt?.customInstructions ?? ""
|
||||||
})()}
|
})()}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -696,7 +722,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// For built-in modes, update the prompts
|
// For built-in modes, update the prompts
|
||||||
const existingPrompt = customPrompts?.[mode]
|
const existingPrompt = customPrompts?.[mode] as PromptComponent
|
||||||
updateAgentPrompt(mode, {
|
updateAgentPrompt(mode, {
|
||||||
...existingPrompt,
|
...existingPrompt,
|
||||||
customInstructions: value.trim() || undefined,
|
customInstructions: value.trim() || undefined,
|
||||||
@@ -759,6 +785,77 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
</VSCodeButton>
|
</VSCodeButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: "20px" }}>
|
||||||
|
<div style={{ fontWeight: "bold", marginBottom: "12px" }}>Code Action Prompts</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "16px",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: "12px",
|
||||||
|
overflowX: "auto",
|
||||||
|
flexWrap: "nowrap",
|
||||||
|
paddingBottom: "4px",
|
||||||
|
paddingRight: "20px",
|
||||||
|
}}>
|
||||||
|
{Object.keys(codeActionPrompt.default).map((type) => (
|
||||||
|
<button
|
||||||
|
key={type}
|
||||||
|
data-testid={`${type}-tab`}
|
||||||
|
data-active={activeCodeActionTab === type ? "true" : "false"}
|
||||||
|
onClick={() => setActiveCodeActionTab(type as CodeActionType)}
|
||||||
|
style={{
|
||||||
|
padding: "4px 8px",
|
||||||
|
border: "none",
|
||||||
|
background:
|
||||||
|
activeCodeActionTab === type ? "var(--vscode-button-background)" : "none",
|
||||||
|
color:
|
||||||
|
activeCodeActionTab === type
|
||||||
|
? "var(--vscode-button-foreground)"
|
||||||
|
: "var(--vscode-foreground)",
|
||||||
|
cursor: "pointer",
|
||||||
|
opacity: activeCodeActionTab === type ? 1 : 0.8,
|
||||||
|
borderRadius: "3px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}>
|
||||||
|
{codeActionLabels[type as CodeActionType]}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Show active tab content */}
|
||||||
|
<div key={activeCodeActionTab}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: "4px",
|
||||||
|
}}>
|
||||||
|
<div style={{ fontWeight: "bold" }}>{activeCodeActionTab} Prompt</div>
|
||||||
|
<VSCodeButton
|
||||||
|
appearance="icon"
|
||||||
|
onClick={() => handleCodeActionReset(activeCodeActionTab)}
|
||||||
|
title={`Reset ${activeCodeActionTab} prompt to default`}>
|
||||||
|
<span className="codicon codicon-discard"></span>
|
||||||
|
</VSCodeButton>
|
||||||
|
</div>
|
||||||
|
<VSCodeTextArea
|
||||||
|
value={getCodeActionPromptValue(activeCodeActionTab)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value =
|
||||||
|
(e as CustomEvent)?.detail?.target?.value ||
|
||||||
|
((e as any).target as HTMLTextAreaElement).value
|
||||||
|
const trimmedValue = value.trim()
|
||||||
|
updateCodeActionPrompt(activeCodeActionTab, trimmedValue || undefined)
|
||||||
|
}}
|
||||||
|
rows={4}
|
||||||
|
resize="vertical"
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3 style={{ color: "var(--vscode-foreground)", margin: "40px 0 20px 0" }}>Prompt Enhancement</h3>
|
<h3 style={{ color: "var(--vscode-foreground)", margin: "40px 0 20px 0" }}>Prompt Enhancement</h3>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user